From 4ef5c33af90967f3801e8bdc3b7cc32631db884e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kre=C5=A1imir=20Ba=C4=8Di=C4=87?= <96543666+kureshimiru@users.noreply.github.com> Date: Tue, 25 Jul 2023 18:27:44 +0200 Subject: [PATCH 01/64] Heatmap: Add datalink support (#71016) --- .../plugins/panel/heatmap/HeatmapHoverView.tsx | 16 ++++++++++++++-- .../app/plugins/panel/heatmap/HeatmapPanel.tsx | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index 40fe2fe4897..5794e8bd88d 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -8,6 +8,9 @@ import { getFieldDisplayName, LinkModel, TimeRange, + getLinksSupplier, + InterpolateFunction, + ScopedVars, } from '@grafana/data'; import { HeatmapCellLayout } from '@grafana/schema'; import { LinkButton, VerticalGroup } from '@grafana/ui'; @@ -23,6 +26,8 @@ type Props = { hover: HeatmapHoverEvent; showHistogram?: boolean; timeRange: TimeRange; + replaceVars: InterpolateFunction; + scopedVars: ScopedVars[]; }; export const HeatmapHoverView = (props: Props) => { @@ -32,7 +37,7 @@ export const HeatmapHoverView = (props: Props) => { return ; }; -const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { +const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars }: Props) => { const index = hover.dataIdx; const xField = data.heatmap?.fields[0]; const yField = data.heatmap?.fields[1]; @@ -119,7 +124,14 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { const linkLookup = new Set(); for (const field of visibleFields ?? []) { - // TODO: Currently always undefined? (getLinks) + const hasLinks = field.config.links && field.config.links.length > 0; + if (hasLinks && data.heatmap) { + let appropriateScopedVars = scopedVars.filter( + (sv) => sv && sv.__dataContext && sv.__dataContext.value.field.name === nonNumericOrdinalDisplay + )[0]; + field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars ?? {}, replaceVars); + } + if (field.getLinks) { const v = field.values[index]; const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index a84729f0d83..b21560fdd4a 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -43,6 +43,16 @@ export const HeatmapPanel = ({ const styles = useStyles2(getStyles); const { sync } = usePanelContext(); + // necessary for enabling datalinks in hover view + let scopedVarsFromRawData = []; + for (const series of data.series) { + for (const field of series.fields) { + if (field.state?.scopedVars) { + scopedVarsFromRawData.push(field.state?.scopedVars); + } + } + } + // ugh let timeRangeRef = useRef(timeRange); timeRangeRef.current = timeRange; @@ -210,6 +220,8 @@ export const HeatmapPanel = ({ data={info} hover={hover} showHistogram={options.tooltip.yHistogram} + replaceVars={replaceVariables} + scopedVars={scopedVarsFromRawData} /> )} From 9ff193f6928956bd8f1d0586792e3760fceffe58 Mon Sep 17 00:00:00 2001 From: Ieva Date: Tue, 25 Jul 2023 17:46:46 +0100 Subject: [PATCH 02/64] Docs: update GitLab OAuth2 documentation (#71834) * gitlab doc update and update the default scopes * small fixes * fix a reference * update another reference * PR feedback: fix numbering of bulletpoints, reorder config options * linting --- conf/defaults.ini | 4 +- conf/sample.ini | 2 +- .../introduction/grafana-enterprise.md | 2 +- .../configure-authentication/github/index.md | 2 +- .../configure-authentication/gitlab/index.md | 320 +++++++----------- .../configure-security/configure-team-sync.md | 2 +- 6 files changed, 128 insertions(+), 204 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 76542724d10..b3cc15b3586 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -612,7 +612,7 @@ allow_sign_up = true auto_login = false client_id = some_id client_secret = -scopes = api +scopes = openid email profile auth_url = https://gitlab.com/oauth/authorize token_url = https://gitlab.com/oauth/token api_url = https://gitlab.com/api/v4 @@ -1278,7 +1278,7 @@ news_feed_enabled = true #################################### Query ############################# [query] # Set the number of data source queries that can be executed concurrently in mixed queries. Default is the number of CPUs. -concurrent_query_limit = +concurrent_query_limit = #################################### Query History ############################# [query_history] diff --git a/conf/sample.ini b/conf/sample.ini index 960f917dc71..265cb8a9b9a 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -595,7 +595,7 @@ ;auto_login = false ;client_id = some_id ;client_secret = some_secret -;scopes = api +;scopes = openid email profile ;auth_url = https://gitlab.com/oauth/authorize ;token_url = https://gitlab.com/oauth/token ;api_url = https://gitlab.com/api/v4 diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md index 4c60908b02d..e7ea3c27e3b 100644 --- a/docs/sources/introduction/grafana-enterprise.md +++ b/docs/sources/introduction/grafana-enterprise.md @@ -34,7 +34,7 @@ Supported auth providers: - [Auth Proxy]({{< relref "../setup-grafana/configure-security/configure-authentication/auth-proxy#team-sync-enterprise-only" >}}) - [Azure AD OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/azuread#team-sync-enterprise-only" >}}) - [GitHub OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/github#configure-team-synchronization" >}}) -- [GitLab OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/gitlab#team-sync-enterprise-only" >}}) +- [GitLab OAuth]({{< relref "../setup-grafana/configure-security/configure-authentication/gitlab#configure-team-synchronization" >}}) - [LDAP]({{< relref "../setup-grafana/configure-security/configure-authentication/enhanced-ldap#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "../setup-grafana/configure-security/configure-authentication/okta#configure-team-synchronization-enterprise-only" >}}) - [SAML]({{< relref "../setup-grafana/configure-security/configure-authentication/saml#configure-team-sync" >}}) diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md index 940d545a311..2ad0dcd173e 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md @@ -146,7 +146,7 @@ For example, `https://github.com/orgs/grafana/teams/developers` or `@grafana/dev To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}). -## Examples of GitHub configuration in Grafana +## Example of GitHub configuration in Grafana This section includes an example of GitHub configuration in the Grafana configuration file. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md index 2eddacc5c22..9759567471f 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/gitlab/index.md @@ -19,85 +19,83 @@ weight: 1000 # Configure GitLab OAuth2 authentication -To enable GitLab OAuth2 you must register the application in GitLab. GitLab will generate a client ID and secret key for you to use. +{{< docs/shared "auth/intro.md" >}} -## Create GitLab OAuth keys +This topic describes how to configure GitLab OAuth2 authentication. -You need to [create a GitLab OAuth application](https://docs.gitlab.com/ce/integration/oauth_provider.html). -Choose a descriptive _Name_, and use the following _Redirect URI_: +## Before you begin -``` -https://grafana.example.com/login/gitlab -``` +To follow this guide: -where `https://grafana.example.com` is the URL you use to connect to Grafana. -Adjust it as needed if you don't use HTTPS or if you use a different port; for -instance, if you access Grafana at `http://203.0.113.31:3000`, you should use +- Ensure that you have access to the [Grafana configuration file]({{< relref "../../../configure-grafana#configuration-file-location" >}}). +- Ensure you know how to create a GitLab OAuth application. Consult GitLab's documentation on [creating a GitLab OAuth application](https://docs.gitlab.com/ee/integration/oauth_provider.html) for more information. -``` -http://203.0.113.31:3000/login/gitlab -``` +## Steps -Finally, select `openid`, `email` and `profile` as the scopes and submit the form. +To configure GitLab authentication with Grafana, follow these steps: -You'll get an _Application Id_ and a _Secret_ in return; we'll call them -`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this -section. +1. Create an OAuth application in GitLab. -## Enable GitLab in Grafana + 1. Set the redirect URI to `http://:/login/gitlab`. -In this example, we'll assume you use the public `gitlab.com` instance, but you -can use your own instance of GitLab instead by replacing `auth_url`, `token_url` with the URL of your instance. + Ensure that the Redirect URI is the complete HTTP address that you use to access Grafana via your browser, but with the appended path of `/login/gitlab`. -You can find these URLs in the `well known` configuration file of your GitLab instance, for example `https://gitlab.com/.well-known/openid-configuration`. + For the Redirect URI to be correct, it might be necessary to set the `root_url` option in the `[server]`section of the Grafana configuration file. For example, if you are serving Grafana behind a proxy. -Add the following to your Grafana configuration file to enable GitLab -authentication: + 1. Set the OAuth2 scopes to `openid`, `email` and `profile`. -```bash -[auth.gitlab] -enabled = true -allow_sign_up = true -auto_login = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = openid email profile -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -allowed_groups = -role_attribute_path = -role_attribute_strict = false -allow_assign_grafana_admin = false -tls_skip_verify_insecure = false -tls_client_cert = -tls_client_key = -tls_client_ca = -use_pkce = true -``` +1. Refer to the following table to update field values located in the `[auth.gitlab]` section of the Grafana configuration file: -You may have to set the `root_url` option of `[server]` for the callback URL to be -correct. For example in case you are serving Grafana behind a proxy. + | Field | Description | + | ---------------------------- | -------------------------------------------------------------------------------------------- | + | `client_id`, `client_secret` | These values must match the client ID and client secret from your GitLab OAuth2 application. | + | `enabled` | Enables GitLab authentication. Set this value to `true`. | -Restart the Grafana backend for your changes to take effect. + Review the list of other GitLab [configuration options]({{< relref "#configuration-options" >}}) and complete them, as necessary. -With `allow_sign_up` set to `false`, only existing users will be able to login -using their GitLab account, but with `allow_sign_up` set to `true`, _any_ user -who can authenticate on GitLab will be able to login on your Grafana instance; -if you use the public `gitlab.com`, it means anyone in the world would be able -to login on your Grafana instance. +1. Optional: [Configure a refresh token]({{< relref "#configure-a-refresh-token" >}}): -You can limit access to only members of a given group or list of -groups by setting the `allowed_groups` option. + a. Enable `accessTokenExpirationCheck` feature toggle. -You can also specify the SSL/TLS configuration used by the client. + b. Set `use_refresh_token` to `true` in `[auth.gitlab]` section in Grafana configuration file. -- Set `tls_client_cert` to the path of the certificate. -- Set `tls_client_key` to the path containing the key. -- Set `tls_client_ca` to the path containing a trusted certificate authority list. +1. [Configure role mapping]({{< relref "#configure-role-mapping" >}}). +1. Optional: [Configure team synchronization]({{< relref "#configure-team-synchronization" >}}). +1. Restart Grafana. -`tls_skip_verify_insecure` controls whether a client verifies the server's certificate chain and host name. If it is true, then SSL/TLS accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. + You should now see a GitLab login button on the login page and be able to log in or sign up with your GitLab accounts. -### Configure refresh token +## Configuration options + +The table below describes all GitLab OAuth configuration options. Like any other Grafana configuration, you can apply these options as environment variables. + +| Setting | Required | Description | Default | +| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| `enabled` | Yes | Whether GitLab OAuth authentication is allowed. | `false` | +| `client_id` | Yes | Client ID provided by your GitLab OAuth app. | | +| `client_secret` | Yes | Client secret provided by your GitLab OAuth app. | | +| `auth_url` | Yes | Authorization endpoint of your GitLab OAuth provider. If you use your own instance of GitLab instead of gitlab.com, adjust `auth_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/authorize` | +| `token_url` | Yes | Endpoint used to obtain GitLab OAuth access token. If you use your own instance of GitLab instead of gitlab.com, adjust `token_url` by replacing the `gitlab.com` hostname with your own. | `https://gitlab.com/oauth/token` | +| `api_url` | No | Grafana uses `/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` | +| `name` | No | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` | +| `icon` | No | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` | +| `scopes` | No | List of comma- or space-separated GitLab OAuth scopes. | `openid email profile` | +| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` | +| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` | +| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | | +| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` | +| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` | +| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` | +| `allowed_domains` | No | List of comma- or space-separated domains. User must belong to at least one domain to log in. | | +| `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. If you configure `allowed_groups`, you must also configure `groups_attribute_path`. | | +| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` | +| `tls_client_cert` | No | The path to the certificate. | | +| `tls_client_key` | No | The path to the key. | | +| `tls_client_ca` | No | The path to the trusted certificate authority list. | | +| `use_pkce` | No | Set to `true` to use [Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636). Grafana uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. | `true` | +| `use_refresh_token` | No | Set to `true` to use refresh token and check access token expiration. The `accessTokenExpirationCheck` feature toggle should also be enabled to use refresh token. | `true` | + +### Configure a refresh token > Available in Grafana v9.3 and later versions. @@ -113,169 +111,95 @@ Refresh token fetching and access token expiration check is enabled by default f > **Note:** The `accessTokenExpirationCheck` feature toggle will be removed in Grafana v10.2.0 and the `use_refresh_token` configuration value will be used instead for configuring refresh token fetching and access token expiration check. -### allowed_groups +### Configure allowed groups To limit access to authenticated users that are members of one or more [GitLab groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` -to a comma- or space-separated list of groups. For instance, if you want to -only give access to members of the `example` group, set +to a comma- or space-separated list of groups. + +GitLab's groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`. +Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters. +Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. + +## Configure role mapping + +Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role retrieved from GitLab upon user login. + +The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option. +To map the server administrator role, use the `allow_assign_grafana_admin` configuration option. +Refer to [configuration options]({{< relref "#configuration-options" >}}) for more information. + +If no valid role is found, the user is assigned the role specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). +You can disable this default role assignment by setting `role_attribute_strict = true`. +This setting denies user access if no role or an invalid role is returned. + +To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads. + +### Role mapping examples + +This section includes examples of JMESPath expressions used for role mapping. + +#### Map roles using user information from OAuth token + +In this example, the user with email `admin@company.com` has been granted the `Admin` role. +All other users are granted the `Viewer` role. ```ini -allowed_groups = example +role_attribute_path = email=='admin@company.com' && 'Admin' || 'Viewer' ``` -If you want to also give access to members of the subgroup `bar`, which is in -the group `foo`, set - -```ini -allowed_groups = example, foo/bar -``` - -To put values containing spaces in the list, use the following JSON syntax: - -```ini -allowed_groups = ["Admins", "Software Engineers"] -``` - -Note that in GitLab, the group or subgroup name doesn't always match its -display name, especially if the display name contains spaces or special -characters. Make sure you always use the group or subgroup name as it appears -in the URL of the group or subgroup. - -Here's a complete example with `allow_sign_up` enabled, with access limited to -the `example` and `foo/bar` groups. The example also promotes all GitLab Admins to Grafana organization admins: - -```ini -[auth.gitlab] -enabled = true -allow_sign_up = true -auto_login = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = openid email profile -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -allowed_groups = example, foo/bar -role_attribute_path = is_admin && 'Admin' || 'Viewer' -role_attribute_strict = true -allow_assign_grafana_admin = false -tls_skip_verify_insecure = false -tls_client_cert = -tls_client_key = -tls_client_ca = -use_pkce = true -``` - -### PKCE - -IETF's [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) -introduces "proof key for code exchange" (PKCE) which provides -additional protection against some forms of authorization code -interception attacks. PKCE will be required in [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-03). - -> You can disable PKCE in Grafana by setting `use_pkce` to `false` in the`[auth.gitlab]` section. - -``` -use_pkce = true -``` - -Grafana always uses the SHA256 based `S256` challenge method and a 128 bytes (base64url encoded) code verifier. - -### Configure automatic login - -Set `auto_login` option to true to attempt login automatically, skipping the login screen. -This setting is ignored if multiple auth providers are configured to use auto login. - -``` -auto_login = true -``` - -### Map roles - -You can use GitLab OAuth to map roles. During mapping, Grafana checks for the presence of a role using the [JMESPath](http://jmespath.org/examples.html) specified via the `role_attribute_path` configuration option. - -For the path lookup, Grafana uses JSON obtained from querying GitLab's API [`/api/v4/user`](https://docs.gitlab.com/ee/api/users.html#list-current-user-for-normal-users) endpoint and a `groups` key containing all of the user's teams. The result of evaluating the `role_attribute_path` JMESPath expression must be a valid Grafana role, for example, `Viewer`, `Editor` or `Admin`. For more information about roles and permissions in Grafana, refer to [Roles and permissions]({{< relref "../../../../administration/roles-and-permissions" >}}). - -{{% admonition type="warning" %}} -Currently if no organization role mapping is found for a user, Grafana doesn't -update the user's organization role. This is going to change in Grafana 10. To avoid overriding manually set roles, -enable the `skip_org_role_sync` option. -See [Configure Grafana]({{< relref "../../../configure-grafana#authgitlab" >}}) for more information. -{{% /admonition %}} - -On first login, if the`role_attribute_path` property does not return a role, then the user is assigned the role -specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). -You can disable this default role assignment by setting `role_attribute_strict = true`. -It denies user access if no role or an invalid role is returned. - -{{% admonition type="warning" %}} -With Grafana 10, **on every login**, if the`role_attribute_path` property does not return a role, -then the user is assigned the role specified by -[the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}). -{{% /admonition %}} - -An example Query could look like the following: - -```ini -role_attribute_path = is_admin && 'Admin' || 'Viewer' -``` - -This allows every GitLab Admin to be an Admin in Grafana. - #### Map roles using groups -Groups can also be used to map roles. Group name (lowercased and unique) is used instead of display name for identifying groups - -For instance, if you have a group with display name 'Example-Group' you can use the following snippet to -ensure those members inherit the role 'Editor'. +In this example, the user from GitLab group 'example-group' have been granted the `Editor` role. +All other users are granted the `Viewer` role. ```ini role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer' ``` -Note: If a match is found in other fields, groups will be ignored. +#### Map server administrator role -#### Map server administrator privileges +In this example, the user with email `admin@company.com` has been granted the `Admin` organization role as well as the Grafana server admin role. +All other users are granted the `Viewer` role. -> Available in Grafana v9.2 and later versions. - -If the application role received by Grafana is `GrafanaAdmin`, Grafana grants the user server administrator privileges. -This is useful if you want to grant server administrator privileges to a subset of users. -Grafana also assigns the user the `Admin` role of the default organization. - -The setting `allow_assign_grafana_admin` under `[auth.gitlab]` must be set to `true` for this to work. -If the setting is set to `false`, the user is assigned the role of `Admin` of the default organization, but not server administrator privileges. - -```ini -allow_assign_grafana_admin = true +```bash +role_attribute_path = email=='admin@company.com' && 'GrafanaAdmin' || 'Viewer' ``` -Example: +## Configure team synchronization -```ini -role_attribute_path = is_admin && 'GrafanaAdmin' || 'Viewer' -``` +> **Note:** Available in [Grafana Enterprise]({{< relref "../../../../introduction/grafana-enterprise" >}}) and [Grafana Cloud](/docs/grafana-cloud/). -### Team Sync (Enterprise only) +By using Team Sync, you can map GitLab groups to teams within Grafana. This will automatically assign users to the appropriate teams. +Teams for each user are synchronized when the user logs in. -> Only available in Grafana Enterprise v6.4+ +GitLab groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`. +Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters. +Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. -With Team Sync you can map your GitLab groups to teams in Grafana so that your users will automatically be added to -the correct teams. +To learn more about Team Sync, refer to [Configure team sync]({{< relref "../../configure-team-sync" >}}). -Your GitLab groups can be referenced in the same way as `allowed_groups`, like `example` or `foo/bar`. +## Example of GitLab configuration in Grafana -[Learn more about Team Sync]({{< relref "../../configure-team-sync" >}}) +This section includes an example of GitLab configuration in the Grafana configuration file. -## Skip organization role sync - -To prevent the sync of organization roles from GitLab, set `skip_org_role_sync` to `true`. This is useful if you want to manage the organization roles for your users from within Grafana. -This also impacts the `allow_assign_grafana_admin` setting by not syncing the Grafana admin role from GitLab. - -```ini +```bash [auth.gitlab] -# .. -# prevents the sync of org roles from Github -skip_org_role_sync = true -`` +enabled = true +allow_sign_up = true +auto_login = false +client_id = YOUR_GITLAB_APPLICATION_ID +client_secret = YOUR_GITLAB_APPLICATION_SECRET +scopes = openid email profile +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +role_attribute_path = contains(groups[*], 'example-group') && 'Editor' || 'Viewer' +role_attribute_strict = false +allow_assign_grafana_admin = false +allowed_groups = ["admins", "software engineers", "developers/frontend"] +allowed_domains = mycompany.com mycompany.org +tls_skip_verify_insecure = false +use_pkce = true +use_refresh_token = true ``` diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index 09267daa8a1..a557e48d34c 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -30,7 +30,7 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea - [Auth Proxy]({{< relref "./configure-authentication/auth-proxy#team-sync-enterprise-only" >}}) - [Azure AD]({{< relref "./configure-authentication/azuread#team-sync-enterprise-only" >}}) - [GitHub OAuth]({{< relref "./configure-authentication/github#configure-team-synchronization" >}}) -- [GitLab OAuth]({{< relref "./configure-authentication/gitlab#team-sync-enterprise-only" >}}) +- [GitLab OAuth]({{< relref "./configure-authentication/gitlab#configure-team-synchronization" >}}) - [LDAP]({{< relref "./configure-authentication/enhanced-ldap#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "./configure-authentication/okta#configure-team-synchronization-enterprise-only" >}}) - [SAML]({{< relref "./configure-authentication/saml#configure-team-sync" >}}) From 1755f8c7b764f65449df837effd5197206ab3672 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 25 Jul 2023 20:18:49 +0200 Subject: [PATCH 03/64] PluginExtensions: Allow to specify unkown properties in override but they will be ignored (#72273) * fixed bug. * Update public/app/features/plugins/extensions/getPluginExtensions.ts Co-authored-by: Ben Sully * Update public/app/features/plugins/extensions/getPluginExtensions.test.ts Co-authored-by: Ben Sully * Update public/app/features/plugins/extensions/getPluginExtensions.ts Co-authored-by: Jack Westbrook * Update public/app/features/plugins/extensions/getPluginExtensions.test.ts Co-authored-by: Jack Westbrook --------- Co-authored-by: Ben Sully Co-authored-by: Jack Westbrook --- .../extensions/getPluginExtensions.test.ts | 16 ++++++++++++++-- .../plugins/extensions/getPluginExtensions.ts | 6 +++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/public/app/features/plugins/extensions/getPluginExtensions.test.ts b/public/app/features/plugins/extensions/getPluginExtensions.test.ts index 0ad86c5f080..ce106bce5a5 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.test.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.test.ts @@ -134,18 +134,30 @@ describe('getPluginExtensions()', () => { expect(extension.category).toBe('Machine Learning'); }); - test('should hide the extension if it tries to override not-allowed properties with the configure() function', () => { + test('should ignore restricted properties passed via the configure() function', () => { link2.configure = jest.fn().mockImplementation(() => ({ // The following props are not allowed to override type: 'unknown-type', pluginId: 'another-plugin', + + // Unknown properties + testing: false, + + // The following props are allowed to override + title: 'test', })); const registry = createPluginExtensionRegistry([{ pluginId, extensionConfigs: [link2] }]); const { extensions } = getPluginExtensions({ registry, extensionPointId: extensionPoint2 }); + const [extension] = extensions; expect(link2.configure).toHaveBeenCalledTimes(1); - expect(extensions).toHaveLength(0); + expect(extensions).toHaveLength(1); + expect(extension.title).toBe('test'); + expect(extension.type).toBe('link'); + expect(extension.pluginId).toBe('grafana-basic-app'); + //@ts-ignore + expect(extension.testing).toBeUndefined(); }); test('should pass a read only context to the configure() function', () => { const context = { title: 'New title from the context!' }; diff --git a/public/app/features/plugins/extensions/getPluginExtensions.ts b/public/app/features/plugins/extensions/getPluginExtensions.ts index e878989ebf1..71acd6a365b 100644 --- a/public/app/features/plugins/extensions/getPluginExtensions.ts +++ b/public/app/features/plugins/extensions/getPluginExtensions.ts @@ -139,10 +139,10 @@ function getLinkExtensionOverrides(pluginId: string, config: PluginExtensionLink assertStringProps({ title, description }, ['title', 'description']); if (Object.keys(rest).length > 0) { - throw new Error( - `Invalid extension "${config.title}". Trying to override not-allowed properties: ${Object.keys(rest).join( + logWarning( + `Extension "${config.title}", is trying to override restricted properties: ${Object.keys(rest).join( ', ' - )}` + )} which will be ignored.` ); } From c6ab1ddb704f882043c38d8013e7cce43afdada6 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:01:22 +0200 Subject: [PATCH 04/64] Docs: adds new alert rule creation flow (#72257) * Docs: adds new alert rule creation flow * updates configure alerting topic * Adds grafana-managed process * adds data source-managed and recording rule * takes out anchor --- .../sources/alerting/alerting-rules/_index.md | 16 +- .../create-grafana-managed-rule.md | 155 +++++++++++++----- ...reate-mimir-loki-managed-recording-rule.md | 8 +- .../create-mimir-loki-managed-rule.md | 120 ++++++++++---- 4 files changed, 215 insertions(+), 84 deletions(-) diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index 36377ae8894..da1bda8d87d 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -22,21 +22,15 @@ Configure the features and integrations that you need to create and manage your **Configure alert rules** -An alert rule is a set of evaluation criteria that determines whether an alert will fire. The alert rule consists of one or more queries and expressions, a condition, the frequency of evaluation, and optionally, the duration over which the condition is met. +[Configure Grafana-managed alert rules][create-grafana-managed-rule]. -While queries and expressions select the data set to evaluate, a condition sets the threshold that an alert must meet or exceed to create an alert. An interval specifies how frequently an alert rule is evaluated. Duration, when configured, indicates how long a condition must be met. Alert rules can also define alerting behavior in the absence of data. +[Configure data source-managed alert rules][create-mimir-loki-managed-rule] -You can: +**Configure recording rules** -- [Create Grafana Mimir or Loki managed alert rules][create-mimir-loki-managed-rule]. -- [Create Grafana Mimir or Loki managed recording rules][create-mimir-loki-managed-recording-rule]. -- [Edit Grafana Mimir or Loki rule groups and namespaces][edit-mimir-loki-namespace-group]. -- [Create Grafana managed alert rules][create-grafana-managed-rule]. +_Recording rules are only available for compatible Prometheus or Loki data sources._ -**Note:** -Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. - -Alert rules for an external Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles. +For more information, see [Configure recording rules][create-mimir-loki-managed-recording-rule]. **Configure contact points** diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 1259d24e016..3ad30c6a4bc 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -2,76 +2,153 @@ aliases: - ../unified-alerting/alerting-rules/create-grafana-managed-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule/ -description: Create Grafana managed alert rule +description: Configure Grafana-managed alert rules keywords: - grafana - alerting - guide - rules - create + - grafana-managed + - data source-managed labels: products: - cloud - enterprise - oss -title: Create Grafana-managed alert rules +title: Configure Grafana-managed alert rules weight: 400 --- -# Create Grafana-managed alert rules +# Configure Grafana-managed alert rules Grafana-managed rules are the most flexible alert rule type. They allow you to create alerts that can act on data from any of our supported data sources. In addition to supporting multiple data sources, you can also add expressions to transform your data and set alert conditions. Using images in alert notifications is also supported. This is the only type of rule that allows alerting from multiple data sources in a single rule definition. Multiple alert instances can be created as a result of one alert rule (also known as a multi-dimensional alerting). -For information on Grafana Alerting, see [Introduction to Grafana Alerting][fundamentals], which explains the key concepts and features of Grafana Alerting. +**Note:** -Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}} +Grafana managed alert rules can only be edited or deleted by users with Edit permissions for the folder storing the rules. -To create a Grafana-managed alert rule, complete the following steps. +Watch this video to learn more about creating alert rules: {{< vimeo 720001934 >}} -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -2. Click **Alert rules**. -3. Click **+ Create alert rule**. The new alert rule page opens where the **Grafana managed alerts** option is selected by default. -4. In Step 1, add the rule name. - - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. -5. In Step 2, add queries and expressions to evaluate, and then select the alert condition. +In the following sections, we’ll guide you through the process of creating your Grafana-managed alert rules. - - For queries, select a data source from the dropdown. - - Specify a [time range][time-units-and-relative-ranges]. +To create a Grafana-managed alert rule, use the in-product alert creation flow and follow these steps to help you. - **Note:** - Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. +1. Enter an alert rule name +1. Define query and alert condition +1. Set evaluation behavior +1. Add annotations +1. Configure notifications - It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. +## Set alert rule name - - Add one or more [queries][add-a-query] or [expressions][expression-queries]. - - For each expression, select either **Classic condition** to create a single alert rule, or choose from the **Math**, **Reduce**, and **Resample** options to generate separate alert for each series. For details on these options, see [Single and multi dimensional rule](#single-and-multi-dimensional-rule). - - Click **Run queries** to verify that the query is successful. - - Next, select the query or expression for your alert condition. +1. Click **Alerts & IRM** -> **Alert rules** -> **+ New alert rule**. +1. Enter a name to identify your alert rule. -6. In Step 3, specify the alert evaluation interval. + This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. - - From the **Condition** dropdown, select the query or expression to trigger the alert rule. - - For **Evaluate every**, specify the frequency of evaluation. Must be a multiple of 10 seconds. For examples, `1m`, `30s`. - - For **Evaluate for**, specify the duration for which the condition must be true before an alert fires. - > **Note:** Once a condition is breached, the alert goes into the Pending state. If the condition remains breached for the duration specified, the alert transitions to the `Firing` state, otherwise it reverts back to the `Normal` state. - - In **Configure no data and error handling**, configure alerting behavior in the absence of data. Use the guidelines in [No data and error handling](#configure-no-data-and-error-handling). - - Click **Preview** to check the result of running the query at this moment. Preview excludes no data and error handling. +## Define query and condition - **Note:** +Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. - You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. +1. Select a data source. +1. From the **Options** dropdown, specify a [time range][time-units-and-relative-ranges]. -7. In Step 4, add the storage location, rule group, as well as additional metadata associated with the rule. - - From the **Folder** dropdown, select the folder where you want to store the rule. - - For **Group**, specify a pre-defined group. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - - Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting][annotation-label]. - - Add Runbook URL, panel, dashboard, and alert IDs. -8. In Step 5, add custom labels. - - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . -9. Click **Save** to save the rule or **Save and exit** to save the rule and go back to the Alerting page. -10. Next, create a for the rule. + **Note:** + + Grafana Alerting only supports fixed relative time ranges, for example, `now-24hr: now`. + + It does not support absolute time ranges: `2021-12-02 00:00:00 to 2021-12-05 23:59:592` or semi-relative time ranges: `now/d to: now`. + +1. Add a query. + + To add multiple [queries][add-a-query], click **Add query**. + + All alert rules are managed by Grafana by default. If you want to switch to a data source-managed alert rule, click **Switch to data source-managed alert rule**. + +1. Add one or more [expressions][expression-queries]. + a. For each expression, select either **Classic condition** to create a single alert rule, or choose from the **Math**, **Reduce**, and **Resample** options to generate separate alert for each series. + + For details on these options, see [Single and multi dimensional rule] + b. Click **Preview** to verify that the expression is successful. + +1. Click **Set as alert condition** on the query or expression you want to set as your alert condition. + +## Set alert evaluation behavior + +Use alert rule evaluation to determine how frequently an alert rule should be evaluated and how quickly it should change its state. + +To do this, you need to make sure that your alert rule is in the right evaluation group and set a pending period time that works best for your use case. + +1. Select a folder or click **+ New folder**. +1. Select an evaluation group or click **+ New evaluation group**. + + If you are creating a new evaluation group, specify the interval for the group. + + All rules within the same group are evaluated sequentially over the same time interval. + +1. Enter a pending period. + + The pending period is the period in which an alert rule can be in breach of the condition until it fires. + + Once a condition is met, the alert goes into the **Pending** state. If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state. + +1. Turn on pause alert notifications, if required. + + **Note**: + + Pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. + + You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances. + +1. In **Configure no data and error handling**, configure alerting behavior in the absence of data. + + Use the guidelines in [No data and error handling](#configure-no-data-and-error-handling). + +## Add annotations + +Add [annotations][annotation-label]. to provide more context on the alert in your alert notifications. + +Annotations add metadata to provide more information on the alert in your alert notifications. For example, add a **Summary** annotation to tell you which value caused the alert to fire or which server it happened on. + +1. [Optional] Add a summary. + + Short summary of what happened and why. + +2. [Optional] Add a description. + + Description of what the alert rule does. + +3. [Optional] Add a Runbook URL. + + Webpage where you keep your runbook for the alert + +4. [Optional] Add a custom annotation +5. [Optional] Add a dashboard and panel link. + + Links alerts to panels in a dashboard. + +## Configure notifications + +Add labels to your alert rules to set which notification policy should handle your firing alert instances. + +All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. + +1. Add labels if you want to change the way your notifications are routed. + + Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. + +2. Preview your alert instance routing set up. + + Based on the labels added, alert instances are routed to the following notification policies displayed. + + Expand each notification policy below to view more details. + +3. Click **See details** to view alert routing details and an email preview. + +4. Click **Save rule**. ### Single and multi-dimensional rule diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index 45ba9ea2fcb..dc3151bc699 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -50,17 +50,17 @@ To create recording rules, follow these steps. 1. Click **Alert rules**. 1. Click the **More** dropdown and then **New recording rule**. -1. Add the rule name. +1. Set rule name. The recording rule name must be a Prometheus metric name and contain no whitespace. -1. Select a data source. +1. Define query. - Select your Loki or Prometheus data source. - Enter a query. -1. Add a namespace and a group. +1. Add namespace and group. - From the **Namespace** dropdown, select an existing rule namespace or add a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces][edit-mimir-loki-namespace-group]. - From the **Group** dropdown, select an existing group within the selected namespace or add a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. -1. Add custom labels. +1. Add labels. - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . 1. Click **Save rule** to save the rule or **Save rule and exit** to save the rule and go back to the Alerting page. diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index 5035b8271c0..9090458fe61 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -4,7 +4,7 @@ aliases: - ../unified-alerting/alerting-rules/create-mimir-loki-managed-recording-rule/ - ../unified-alerting/alerting-rules/create-mimir-loki-managed-rule/ canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/create-mimir-loki-managed-rule/ -description: Create Grafana Mimir or Loki managed alerting rule +description: Configure data source-managed alert rules keywords: - grafana - alerting @@ -16,19 +16,23 @@ labels: - cloud - enterprise - oss -title: Create Grafana Mimir or Loki managed alert rules +title: Configure data source-managed alert rules weight: 400 --- -# Create Grafana Mimir or Loki managed alert rules +# Configure data source-managed alert rules -Grafana allows you to create alerting rules for an external Grafana Mimir or Loki instance that has ruler API enabled. For information on Grafana Alerting, see [About Grafana Alerting][alerting] which explains the various components of Grafana Alerting. We also recommend that you familiarize yourself with some of the [fundamental concepts][fundamentals] of Grafana Alerting. +Create alert rules for an external Grafana Mimir or Loki instance that has ruler API enabled; these are called data source-managed alert rules. + +**Note**: + +Alert rules for an external Grafana Mimir or Loki instance can be edited or deleted by users with Editor or Admin roles. ## Before you begin -- Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alerting rules. +- Verify that you have write permission to the Prometheus or Loki data source. Otherwise, you will not be able to create or update Grafana Mimir managed alert rules. -- For Grafana Mimir and Loki data sources, enable the ruler API by configuring their respective services. +- For Grafana Mimir and Loki data sources, enable the Ruler API by configuring their respective services. - **Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types. @@ -36,34 +40,90 @@ Grafana allows you to create alerting rules for an external Grafana Mimir or Lok Watch this video to learn more about how to create a Mimir managed alert rule: {{< vimeo 720001865 >}} -_Refer to [Add a Grafana Mimir or Loki managed alerting rule]({{< relref "#add-a-grafana-mimir-or-loki-managed-alerting-rule" >}}) (following) for current instructions._ - {{% admonition type="note" %}} -If you do not want to manage alerting rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox. +If you do not want to manage alert rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox. {{% /admonition %}} -## Add a Grafana Mimir or Loki managed alerting rule +In the following sections, we’ll guide you through the process of creating your data source-managed alert rules. -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules**. -1. Click **+ Create alert rule**. The new alerting rule page opens where the **Grafana managed alerts** option is selected by default. -1. In Step 1, add the rule name. - - In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. -1. In Step 2, select **Mimir or Loki alert** option. - - Next, select your Loki or Prometheus data source and add the query to evaluate. - - Enter a PromQL or LogQL expression to query. The rule fires if the evaluation result has at least one series with a value that is greater than 0. An alert is created for each series. -1. In Step 3, specify the alert evaluation interval. - - In the **For** text box, specify the duration for which the condition must be true before an alert fires. If you specify `5m`, the condition must be true for 5 minutes before the alert fires. - > **Note:** Once a condition is met, the alert goes into the `Pending` state. If the condition remains active for the duration specified, the alert transitions to the `Firing` state, else it reverts to the `Normal` state. -1. In Step 4, add the namespace, rule group, as well as additional metadata associated with the rule. - - From the **Namespace** dropdown, select an existing rule namespace. Otherwise, click **Add new** and enter a name to create a new one. Namespaces can contain one or more rule groups and only have an organizational purpose. For more information, see [Grafana Mimir or Loki rule groups and namespaces][edit-mimir-loki-namespace-group]. - - From the **Group** dropdown, select an existing group within the selected namespace. Otherwise, click **Add new** and enter a name to create a new one. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time. - - Add a description and summary to customize alert messages. Use the guidelines in [Annotations and labels for alerting][annotation-label]. - - Add Runbook URL, panel, dashboard, and alert IDs. -1. In Step 5, add custom labels. - - Add custom labels selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value . -1. Click **Save** to save the rule or **Save and exit** to save the rule and go back to the Alerting page. -1. Next, create a notification for the rule. +To create a data source-managed alert rule, use the in-product alert creation flow and follow these steps to help you. + +1. Enter an alert rule name +2. Define query and alert condition +3. Set evaluation behavior +4. Add annotations +5. Configure notifications + +## Set alert rule name + +1. Click **Alerts & IRM** -> **Alert rules** -> **+ New alert rule**. +1. Enter a name to identify your alert rule. + + This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule. + +## Define query and condition + +Define a query to get the data you want to measure and a condition that needs to be met before an alert rule fires. + +**Note**: + +All alert rules are managed by Grafana by default. To switch to a data source-managed alert rule, click **Switch to data source-managed alert rule**. + +1. Select a data source. +1. Enter a PromQL or LogQL query. +1. Click **Preview alerts**. + +## Set alert evaluation behavior + +Use alert rule evaluation to determine how frequently an alert rule should be evaluated and how quickly it should change its state. + +1. Select a namespace or click **+ New namespace**. +1. Select an evaluation group or click **+ New evaluation group**. + + If you are creating a new evaluation group, specify the interval for the group. + + All rules within the same group are evaluated sequentially over the same time interval. + +1. Enter a pending period. + + The pending period is the period in which an alert rule can be in breach of the condition until it fires. + + Once a condition is met, the alert goes into the **Pending** state. If the condition remains active for the duration specified, the alert transitions to the **Firing** state, else it reverts to the **Normal** state. + +## Add annotations + +Add [annotations][annotation-label]. to provide more context on the alert in your alert notifications. + +Annotations add metadata to provide more information on the alert in your alert notifications. For example, add a **Summary** annotation to tell you which value caused the alert to fire or which server it happened on. + +1. [Optional] Add a summary. + + Short summary of what happened and why. + +2. [Optional] Add a description. + + Description of what the alert rule does. + +3. [Optional] Add a Runbook URL. + + Webpage where you keep your runbook for the alert + +4. [Optional] Add a custom annotation +5. [Optional] Add a dashboard and panel link. + + Links alerts to panels in a dashboard. + +## Configure notifications + +Add labels to your alert rules to set which notification policy should handle your firing alert instances. + +All alert rules and instances, irrespective of their labels, match the default notification policy. If there are no nested policies, or no nested policies match the labels in the alert rule or alert instance, then the default notification policy is the matching policy. + +1. Add labels if you want to change the way your notifications are routed. + + Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. + +1. Click **Save rule**. {{% docs/reference %}} [alerting]: "/docs/grafana/ -> /docs/grafana//alerting" From 19b239fba099d852fd1fbc5f9680f9f224ad0c4f Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 25 Jul 2023 22:34:14 +0200 Subject: [PATCH 05/64] Alerting: Fix inconsistencies in alert rule form depending on alert type (#72287) * Fix inconsistencies in alert rule form depending on alert type * Fix default annotations when comming from dashboard panel * Update texts following pr review comments * Fix texts --------- Co-authored-by: Virginia Cepeda --- .../components/rule-editor/AlertRuleForm.tsx | 11 +++++++++-- .../rule-editor/CloudEvaluationBehavior.tsx | 13 +++++++++++-- .../unified/components/rule-editor/DetailsStep.tsx | 7 ++++--- .../components/rule-editor/FolderAndGroup.tsx | 2 +- .../rule-editor/GrafanaEvaluationBehavior.tsx | 2 +- .../components/rule-editor/NotificationsStep.tsx | 2 +- .../QueryAndExpressionsStep.tsx | 9 ++++++--- .../features/alerting/unified/utils/constants.ts | 6 +++--- .../features/alerting/unified/utils/rule-form.ts | 5 +++-- 9 files changed, 39 insertions(+), 18 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index e8715f33eb0..038a776fb35 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -21,7 +21,13 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect import { deleteRuleAction, saveRuleFormAction } from '../../state/actions'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { initialAsyncRequestState } from '../../utils/redux'; -import { getDefaultFormValues, getDefaultQueries, MINUTE, rulerRuleToFormValues } from '../../utils/rule-form'; +import { + getDefaultFormValues, + getDefaultQueries, + MINUTE, + normalizeDefaultAnnotations, + rulerRuleToFormValues, +} from '../../utils/rule-form'; import * as ruleId from '../../utils/rule-id'; import { CloudEvaluationBehavior } from './CloudEvaluationBehavior'; @@ -50,7 +56,7 @@ const AlertRuleNameInput = () => { const ruleFormType = watch('type'); return ( - + { } = useFormContext(); const type = watch('type'); + const dataSourceName = watch('dataSourceName'); // cloud recording rules do not have alert conditions if (type === RuleFormType.cloudRecording) { @@ -28,8 +30,11 @@ export const CloudEvaluationBehavior = () => { } return ( - - + +
{ />
+ {type === RuleFormType.cloudAlerting && dataSourceName && ( + + )} +
); diff --git a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx index ad777757ee6..be14d996704 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx @@ -56,11 +56,12 @@ export function DetailsStep() { return ( - {(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && - dataSourceName && } + {ruleFormType === RuleFormType.cloudRecording && dataSourceName && ( + + )} {type !== RuleFormType.cloudRecording && } diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx index 64bb430b056..4b88fed73e9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/FolderAndGroup.tsx @@ -182,7 +182,7 @@ export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGraf Pending period diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index 0b0c6780189..f3c4cf41984 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -86,7 +86,7 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { return ( + {/* This is the cloud data source selector */} {(type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) && ( @@ -436,7 +439,7 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange }: P /> {/* Expression Queries */} Expressions -
Manipulate data returned from queries with math and other operations
+
Manipulate data returned from queries with math and other operations.
= { }; export const annotationDescriptions: Record = { - [Annotation.description]: 'Description of what the alert rule does', - [Annotation.summary]: 'Short summary of what happened and why', - [Annotation.runbookURL]: 'Webpage where you keep your runbook for the alert', + [Annotation.description]: 'Description of what the alert rule does.', + [Annotation.summary]: 'Short summary of what happened and why.', + [Annotation.runbookURL]: 'Webpage where you keep your runbook for the alert.', [Annotation.dashboardUID]: '', [Annotation.panelID]: '', [Annotation.alertId]: '', diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index bb5f3af1ad6..63eb52d6a68 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -94,7 +94,7 @@ export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { throw new Error(`unexpected rule type: ${type}`); } -function listifyLabelsOrAnnotations( +export function listifyLabelsOrAnnotations( item: Labels | Annotations | undefined, addEmpty: boolean ): Array<{ key: string; value: string }> { @@ -106,7 +106,7 @@ function listifyLabelsOrAnnotations( } //make sure default annotations are always shown in order even if empty -function normalizeDefaultAnnotations(annotations: Array<{ key: string; value: string }>) { +export function normalizeDefaultAnnotations(annotations: Array<{ key: string; value: string }>) { const orderedAnnotations = [...annotations]; const defaultAnnotationKeys = defaultAnnotations.map((annotation) => annotation.key); @@ -179,6 +179,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF return { ...defaultFormValues, ...alertingRuleValues, + annotations: normalizeDefaultAnnotations(listifyLabelsOrAnnotations(rule.annotations, false)), type: RuleFormType.cloudAlerting, dataSourceName: ruleSourceName, namespace, From 67a6a99e047d9978545a389d3f41f62e4fd4e89d Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Tue, 25 Jul 2023 23:13:59 +0200 Subject: [PATCH 06/64] Heatmap: Clean up datalink code (#72296) --- .../plugins/panel/heatmap/HeatmapHoverView.tsx | 17 ++++++++++------- .../app/plugins/panel/heatmap/HeatmapPanel.tsx | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx index 5794e8bd88d..5a05b0ba09e 100644 --- a/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapHoverView.tsx @@ -125,18 +125,21 @@ const HeatmapHoverCell = ({ data, hover, showHistogram, scopedVars, replaceVars for (const field of visibleFields ?? []) { const hasLinks = field.config.links && field.config.links.length > 0; + if (hasLinks && data.heatmap) { - let appropriateScopedVars = scopedVars.filter( - (sv) => sv && sv.__dataContext && sv.__dataContext.value.field.name === nonNumericOrdinalDisplay - )[0]; - field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars ?? {}, replaceVars); + const appropriateScopedVars = scopedVars.find( + (scopedVar) => + scopedVar && scopedVar.__dataContext && scopedVar.__dataContext.value.field.name === nonNumericOrdinalDisplay + ); + + field.getLinks = getLinksSupplier(data.heatmap, field, appropriateScopedVars || {}, replaceVars); } if (field.getLinks) { - const v = field.values[index]; - const disp = field.display ? field.display(v) : { text: `${v}`, numeric: +v }; + const value = field.values[index]; + const display = field.display ? field.display(value) : { text: `${value}`, numeric: +value }; - field.getLinks({ calculatedValue: disp, valueRowIndex: index }).forEach((link) => { + field.getLinks({ calculatedValue: display, valueRowIndex: index }).forEach((link) => { const key = `${link.title}/${link.href}`; if (!linkLookup.has(key)) { links.push(link); diff --git a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx index b21560fdd4a..dac87caa2f8 100644 --- a/public/app/plugins/panel/heatmap/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap/HeatmapPanel.tsx @@ -48,7 +48,7 @@ export const HeatmapPanel = ({ for (const series of data.series) { for (const field of series.fields) { if (field.state?.scopedVars) { - scopedVarsFromRawData.push(field.state?.scopedVars); + scopedVarsFromRawData.push(field.state.scopedVars); } } } From 88988e43374b660d4b452e78b93c0bcddc48a84c Mon Sep 17 00:00:00 2001 From: Coen van Leeuwen Date: Tue, 25 Jul 2023 23:20:00 +0200 Subject: [PATCH 07/64] XYChart: Prevent crash on point hover (#70225) --- public/app/plugins/panel/xychart/TooltipView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/xychart/TooltipView.tsx b/public/app/plugins/panel/xychart/TooltipView.tsx index bf8256bf309..5bfe57ff54c 100644 --- a/public/app/plugins/panel/xychart/TooltipView.tsx +++ b/public/app/plugins/panel/xychart/TooltipView.tsx @@ -72,8 +72,8 @@ export const TooltipView = ({ let yValue: YValue | null = null; let extraFacets: ExtraFacets | null = null; if (seriesMapping === SeriesMapping.Manual && manualSeriesConfigs) { - const colorFacetFieldName = manualSeriesConfigs[hoveredPointIndex].pointColor?.field ?? ''; - const sizeFacetFieldName = manualSeriesConfigs[hoveredPointIndex].pointSize?.field ?? ''; + const colorFacetFieldName = manualSeriesConfigs[hoveredPointIndex]?.pointColor?.field ?? ''; + const sizeFacetFieldName = manualSeriesConfigs[hoveredPointIndex]?.pointSize?.field ?? ''; const colorFacet = colorFacetFieldName ? findField(frame, colorFacetFieldName) : undefined; const sizeFacet = sizeFacetFieldName ? findField(frame, sizeFacetFieldName) : undefined; From bf5fa1813710bfa928062b6f8cac4f67be5a3f7a Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:18:13 +0200 Subject: [PATCH 08/64] Docs: updates to alert rule docs (#72313) * Docs: updates to alert rule docs * removes steps * edits to numbering * description parameter edit * fixing description frontmatter --- .../create-grafana-managed-rule.md | 14 ++--- ...reate-mimir-loki-managed-recording-rule.md | 6 +- .../create-mimir-loki-managed-rule.md | 8 +-- .../edit-mimir-loki-namespace-group.md | 63 ------------------- .../alert-rules/organising-alerts.md | 19 +++--- 5 files changed, 19 insertions(+), 91 deletions(-) delete mode 100644 docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md diff --git a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md index 3ad30c6a4bc..8ed5b22cd95 100644 --- a/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-grafana-managed-rule.md @@ -17,7 +17,7 @@ labels: - enterprise - oss title: Configure Grafana-managed alert rules -weight: 400 +weight: 100 --- # Configure Grafana-managed alert rules @@ -36,12 +36,6 @@ In the following sections, we’ll guide you through the process of creating you To create a Grafana-managed alert rule, use the in-product alert creation flow and follow these steps to help you. -1. Enter an alert rule name -1. Define query and alert condition -1. Set evaluation behavior -1. Add annotations -1. Configure notifications - ## Set alert rule name 1. Click **Alerts & IRM** -> **Alert rules** -> **+ New alert rule**. @@ -140,15 +134,15 @@ All alert rules and instances, irrespective of their labels, match the default n Add custom labels by selecting existing key-value pairs from the drop down, or add new labels by entering the new key or value. -2. Preview your alert instance routing set up. +1. Preview your alert instance routing set up. Based on the labels added, alert instances are routed to the following notification policies displayed. Expand each notification policy below to view more details. -3. Click **See details** to view alert routing details and an email preview. +1. Click **See details** to view alert routing details and an email preview. -4. Click **Save rule**. +1. Click **Save rule**. ### Single and multi-dimensional rule diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md index dc3151bc699..929f6276c25 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-recording-rule.md @@ -17,7 +17,7 @@ labels: - enterprise - oss title: Configure recording rules -weight: 400 +weight: 300 --- # Configure recording rules @@ -46,8 +46,8 @@ This setting has precedence over each individual rule frequency. If a rule frequ To create recording rules, follow these steps. -1. Click **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules**. +1. Click **Alerts & IRM** -> **Alerting** -> + **Alert rules**. 1. Click the **More** dropdown and then **New recording rule**. 1. Set rule name. diff --git a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md index 9090458fe61..cc58c44ae51 100644 --- a/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md +++ b/docs/sources/alerting/alerting-rules/create-mimir-loki-managed-rule.md @@ -17,7 +17,7 @@ labels: - enterprise - oss title: Configure data source-managed alert rules -weight: 400 +weight: 200 --- # Configure data source-managed alert rules @@ -48,12 +48,6 @@ In the following sections, we’ll guide you through the process of creating you To create a data source-managed alert rule, use the in-product alert creation flow and follow these steps to help you. -1. Enter an alert rule name -2. Define query and alert condition -3. Set evaluation behavior -4. Add annotations -5. Configure notifications - ## Set alert rule name 1. Click **Alerts & IRM** -> **Alert rules** -> **+ New alert rule**. diff --git a/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md b/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md deleted file mode 100644 index 7a9c35d2575..00000000000 --- a/docs/sources/alerting/alerting-rules/edit-mimir-loki-namespace-group.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -aliases: - - ../unified-alerting/alerting-rules/edit-cortex-loki-namespace-group/ - - ../unified-alerting/alerting-rules/edit-mimir-loki-namespace-group/ -canonical: https://grafana.com/docs/grafana/latest/alerting/alerting-rules/edit-mimir-loki-namespace-group/ -description: Edit Grafana Mimir or Loki rule groups and namespaces -keywords: - - grafana - - alerting - - guide - - group - - namespace - - grafana mimir - - loki -labels: - products: - - cloud - - enterprise - - oss -title: Grafana Mimir or Loki rule groups and namespaces -weight: 405 ---- - -# Grafana Mimir or Loki rule groups and namespaces - -A namespace contains one or more groups. The rules within a group are run sequentially at a regular interval. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. - -{{< figure src="/static/img/docs/alerting/unified/rule-list-edit-mimir-loki-icon-8-2.png" caption="Rule group list" alt="Group list" >}} - - - -## Rename a namespace - -To rename a namespace: - -1. In the left-side menu, click **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules** to view the list of existing alerts. -1. Find a Grafana Mimir or Loki managed rule with the group that belongs to the namespace you want to edit. -1. Click the **Edit** (pen) icon. -1. Enter a new name in the **Namespace** field, then click **Save changes**. - -A new namespace is created and all groups are copied into this namespace from the old one. The old namespace is deleted. - -## Rename rule group or change the rule group evaluation interval - -The rules within a group are run sequentially at a regular interval, the default interval is one (1) minute. You can modify this interval using the following instructions. - -1. In the left-side menu, click the **Alerts & IRM** and then **Alerting**. -1. Click **Alert rules** to view the list of existing alerts. -1. Find a Grafana Mimir or Loki managed rule with the group you want to edit. -1. Click **Edit** (pen) icon. -1. Modify the **Rule group** and **Rule group evaluation interval** information as necessary. -1. Click **Save changes**. - -When you rename the group, a new group with all the rules from the old group is created. The old group is deleted. - -![Group edit modal](/static/img/docs/alerting/unified/rule-list-mimir-loki-edit-ns-group-8-2.png 'Rule group edit modal screenshot') - - diff --git a/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md index e078524b4cc..5e5244c593e 100644 --- a/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md +++ b/docs/sources/alerting/fundamentals/alert-rules/organising-alerts.md @@ -1,6 +1,9 @@ --- +aliases: + - ../unified-alerting/alerting-rules/edit-cortex-loki-namespace-group/ + - ../unified-alerting/alerting-rules/edit-mimir-loki-namespace-group/ canonical: https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/organising-alerts/ -description: Learn how to organize alert rules +description: Namespaces, folders, and groups keywords: - grafana - alerting @@ -10,22 +13,22 @@ labels: - cloud - enterprise - oss -title: Organising alert rules +title: Namespaces, folders, and groups weight: 105 --- -## Namespaces and groups +## Namespaces, folders, and groups -Alerts can be organized using Folders for Grafana-managed rules and namespaces for Mimir or Loki rules and group names. +Alerts can be organized using folders for Grafana-managed rules and namespaces for Mimir or Loki rules and group names. -### Namespaces +### Namespaces and folders When creating Grafana-managed rules, the folder can be used to perform access control and grant or deny access to all rules within a specific folder. +A namespace contains one or more groups. The rules within a group are run sequentially at a regular interval. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. + ### Groups -All rules within a group are evaluated at the same **interval**. - -Alert rules and recording rules within a group will always be evaluated **sequentially**, meaning no rules will be evaluated at the same time and in order of appearance. +The rules within a group are run sequentially at a regular interval, meaning no rules will be evaluated at the same time and in order of appearance.. The default interval is one (1) minute. You can rename Grafana Mimir or Loki rule namespaces and groups, and edit group evaluation intervals. > **Note** If you want rules to be evaluated concurrently and with different intervals, consider storing them in different groups. From 5ce3a7c6db090ef9b325a4cd3cb5b962c2502434 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Wed, 26 Jul 2023 09:21:16 +0200 Subject: [PATCH 09/64] Doc's: Add google support for team sync (#72316) Add google support for team sync --- .../setup-grafana/configure-security/configure-team-sync.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/setup-grafana/configure-security/configure-team-sync.md b/docs/sources/setup-grafana/configure-security/configure-team-sync.md index a557e48d34c..2f74e9137db 100644 --- a/docs/sources/setup-grafana/configure-security/configure-team-sync.md +++ b/docs/sources/setup-grafana/configure-security/configure-team-sync.md @@ -31,6 +31,7 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea - [Azure AD]({{< relref "./configure-authentication/azuread#team-sync-enterprise-only" >}}) - [GitHub OAuth]({{< relref "./configure-authentication/github#configure-team-synchronization" >}}) - [GitLab OAuth]({{< relref "./configure-authentication/gitlab#configure-team-synchronization" >}}) +- [Google OAuth]({{< relref "./configure-authentication/google#configure-team-sync-for-google-oauth" >}}) - [LDAP]({{< relref "./configure-authentication/enhanced-ldap#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "./configure-authentication/okta#configure-team-synchronization-enterprise-only" >}}) - [SAML]({{< relref "./configure-authentication/saml#configure-team-sync" >}}) From da31b8083ad703ab54f0b424d4e9485ddae67597 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 08:00:18 +0000 Subject: [PATCH 10/64] Changelog: Updated changelog for 9.5.7 (#72321) Co-authored-by: grafanabot --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90da0d8c979..c114a045b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -407,6 +407,46 @@ The `database` field has been deprecated in the Elasticsearch datasource provisi - **InteractiveTable:** Updated design and minor tweak to Correlactions page. [#66443](https://github.com/grafana/grafana/issues/66443), [@torkelo](https://github.com/torkelo) + + +# 9.5.7 (2023-07-20) + +### Features and enhancements + +- **Alerting:** Sort NumberCaptureValues in EvaluationString. [#71930](https://github.com/grafana/grafana/issues/71930), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** No longer silence paused alerts during legacy migration. [#71765](https://github.com/grafana/grafana/issues/71765), [@JacobsonMT](https://github.com/JacobsonMT) +- **Chore:** Upgrade Go to 1.20.6. [#71446](https://github.com/grafana/grafana/issues/71446), [@sakjur](https://github.com/sakjur) +- **Alerting:** Remove and revert flag alertingBigTransactions. [#70910](https://github.com/grafana/grafana/issues/70910), [@santihernandezc](https://github.com/santihernandezc) +- **Alerting:** Migrate unknown NoData\Error settings to the default. [#70905](https://github.com/grafana/grafana/issues/70905), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Tempo:** Escape regex-sensitive characters in span name before building promql query. [#68318](https://github.com/grafana/grafana/issues/68318), [@joey-grafana](https://github.com/joey-grafana) +- **Alerting:** Update grafana/alerting to ce9fba9. [#67685](https://github.com/grafana/grafana/issues/67685), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Chore:** Upgrade Go to 1.20.6. (Enterprise) + +### Bug fixes + +- **Plugins:** Only configure plugin proxy transport once. [#71741](https://github.com/grafana/grafana/issues/71741), [@wbrowne](https://github.com/wbrowne) +- **Alerting:** Fix unique violation when updating rule group with title chains/cycles. [#71330](https://github.com/grafana/grafana/issues/71330), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Fix:** Change getExistingDashboardByTitleAndFolder to get dashboard by title, not slug. [#70961](https://github.com/grafana/grafana/issues/70961), [@yangkb09](https://github.com/yangkb09) +- **Alerting:** Convert 'Both' type Prometheus queries to 'Range' in migration. [#70907](https://github.com/grafana/grafana/issues/70907), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Support newer http_config struct. [#69718](https://github.com/grafana/grafana/issues/69718), [@gillesdemey](https://github.com/gillesdemey) +- **InfluxDB:** Interpolate retention policies. [#69299](https://github.com/grafana/grafana/issues/69299), [@itsmylife](https://github.com/itsmylife) +- **StatusHistory:** Fix rendering of value-mapped null. [#69107](https://github.com/grafana/grafana/issues/69107), [@leeoniya](https://github.com/leeoniya) +- **Alerting:** Fix provenance guard checks for Alertmanager configuration to not cause panic when compared nested objects. [#69092](https://github.com/grafana/grafana/issues/69092), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **AnonymousAuth:** Fix concurrent read-write crash. [#68802](https://github.com/grafana/grafana/issues/68802), [@Jguer](https://github.com/Jguer) +- **AzureMonitor:** Ensure legacy properties containing template variables are correctly migrated. [#68790](https://github.com/grafana/grafana/issues/68790), [@aangelisc](https://github.com/aangelisc) +- **Explore:** Remove data source onboarding page. [#68643](https://github.com/grafana/grafana/issues/68643), [@harisrozajac](https://github.com/harisrozajac) +- **Dashboard:** Re-align Save form. [#68625](https://github.com/grafana/grafana/issues/68625), [@polibb](https://github.com/polibb) +- **Azure Monitor:** Fix bug that did not show alert rule preview. [#68582](https://github.com/grafana/grafana/issues/68582), [@alyssabull](https://github.com/alyssabull) +- **Histogram:** Respect min/max panel settings for x-axis. [#68244](https://github.com/grafana/grafana/issues/68244), [@leeoniya](https://github.com/leeoniya) +- **Heatmap:** Fix color rendering for value ranges < 1. [#68163](https://github.com/grafana/grafana/issues/68163), [@leeoniya](https://github.com/leeoniya) +- **Heatmap:** Handle unsorted timestamps in calculate mode. [#68150](https://github.com/grafana/grafana/issues/68150), [@leeoniya](https://github.com/leeoniya) +- **Google Cloud Monitor:** Fix mem usage for dropdown. [#67949](https://github.com/grafana/grafana/issues/67949), [@asimpson](https://github.com/asimpson) +- **AzureMonitor:** Fix logs query multi-resource and timespan values. [#67932](https://github.com/grafana/grafana/issues/67932), [@aangelisc](https://github.com/aangelisc) +- **Utils:** Reimplement util.GetRandomString to avoid modulo bias. [#66970](https://github.com/grafana/grafana/issues/66970), [@DanCech](https://github.com/DanCech) +- **License:** Enable FeatureUserLimit for all products. (Enterprise) +- **Auth:** Remove ldap init sync. (Enterprise) + + # 9.5.6 (2023-07-11) From e094adb5a129157a510b41086c0dc8dacaab9aea Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Wed, 26 Jul 2023 10:37:22 +0200 Subject: [PATCH 11/64] Alerting: Fix refetching grafana rules on alert list panel (#72242) --- .../panel/alertlist/UnifiedAlertList.tsx | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 5f8c608b484..a29404959f7 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -135,18 +135,53 @@ export function UnifiedAlertList(props: PanelProps) { [parsedOptions.alertInstanceLabelFilter] ); + // If the datasource is not defined we should NOT skip the query + // Undefined dataSourceName means that there is no datasource filter applied and we should fetch all the rules + const shouldFetchGrafanaRules = !dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME; + + //For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store + //See https://github.com/grafana/grafana/pull/70482 + const { + currentData: grafanaPromRules = [], + isLoading: grafanaRulesLoading, + refetch: refetchGrafanaPromRules, + } = usePrometheusRulesByNamespaceQuery( + { + limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined, + matcher: matcherList, + state: stateList, + }, + { skip: !shouldFetchGrafanaRules } + ); + useEffect(() => { //we need promRules and rulerRules for getting the uid when creating the alert link in panel in case of being a rulerRule. if (!promRulesRequests.loading) { fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList }); } - const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => - fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList }) - ); + const sub = dashboard?.events.subscribe(TimeRangeUpdatedEvent, () => { + if (shouldFetchGrafanaRules) { + refetchGrafanaPromRules(); + } + + if (!dataSourceName || dataSourceName !== GRAFANA_RULES_SOURCE_NAME) { + fetchPromAndRuler({ dispatch, limitInstances, matcherList, dataSourceName, stateList }); + } + }); return () => { sub?.unsubscribe(); }; - }, [dispatch, dashboard, matcherList, stateList, limitInstances, dataSourceName, promRulesRequests.loading]); + }, [ + dispatch, + dashboard, + matcherList, + stateList, + limitInstances, + dataSourceName, + refetchGrafanaPromRules, + shouldFetchGrafanaRules, + promRulesRequests.loading, + ]); const handleInstancesLimit = (limit: boolean) => { if (limit) { @@ -158,18 +193,7 @@ export function UnifiedAlertList(props: PanelProps) { } }; - //For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store - //See https://github.com/grafana/grafana/pull/70482 - const { currentData: promRules = [], isLoading: grafanaRulesLoading } = usePrometheusRulesByNamespaceQuery( - { - limitAlerts: limitInstances ? INSTANCES_DISPLAY_LIMIT : undefined, - matcher: matcherList, - state: stateList, - }, - { skip: dataSourceName !== GRAFANA_RULES_SOURCE_NAME } - ); - - const combinedRules = useCombinedRuleNamespaces(undefined, promRules); + const combinedRules = useCombinedRuleNamespaces(undefined, grafanaPromRules); const someRulerRulesDispatched = isAsyncRequestMapSlicePartiallyDispatched(rulerRulesRequests); const haveResults = isAsyncRequestMapSlicePartiallyFulfilled(promRulesRequests); From 2dea069443557b5822847f1923a5e613f4c61e9c Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:47:32 +0300 Subject: [PATCH 12/64] Changelog: Updated changelog for 10.0.3 (#72324) Co-authored-by: grafanabot --- CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c114a045b91..d77da361b82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ + + +# 10.0.3 (2023-07-18) + +### Features and enhancements + +- **Alerting:** Sort NumberCaptureValues in EvaluationString. [#71931](https://github.com/grafana/grafana/issues/71931), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** No longer silence paused alerts during legacy migration. [#71761](https://github.com/grafana/grafana/issues/71761), [@JacobsonMT](https://github.com/JacobsonMT) +- **Auth:** Add support for custom signing keys in auth.azure_ad. [#71708](https://github.com/grafana/grafana/issues/71708), [@Jguer](https://github.com/Jguer) +- **Chore:** Upgrade Go to 1.20.6. [#71445](https://github.com/grafana/grafana/issues/71445), [@sakjur](https://github.com/sakjur) +- **Chore:** Upgrade Go to 1.20.6. (Enterprise) + +### Bug fixes + +- **Alerting:** Fix edit / view of webhook contact point when no authorization is set. [#71972](https://github.com/grafana/grafana/issues/71972), [@gillesdemey](https://github.com/gillesdemey) +- **AzureMonitor:** Set timespan in Logs Portal URL link. [#71910](https://github.com/grafana/grafana/issues/71910), [@aangelisc](https://github.com/aangelisc) +- **Plugins:** Only configure plugin proxy transport once. [#71742](https://github.com/grafana/grafana/issues/71742), [@wbrowne](https://github.com/wbrowne) +- **Elasticsearch:** Fix multiple max depth flatten of multi-level objects. [#71636](https://github.com/grafana/grafana/issues/71636), [@fridgepoet](https://github.com/fridgepoet) +- **Elasticsearch:** Fix histogram colors in backend mode. [#71447](https://github.com/grafana/grafana/issues/71447), [@gabor](https://github.com/gabor) +- **Alerting:** Fix state in expressions footer. [#71443](https://github.com/grafana/grafana/issues/71443), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron) +- **AppChromeService:** Fixes update to breadcrumb parent URL. [#71418](https://github.com/grafana/grafana/issues/71418), [@torkelo](https://github.com/torkelo) +- **Elasticsearch:** Fix using multiple indexes with comma separated string. [#71322](https://github.com/grafana/grafana/issues/71322), [@gabor](https://github.com/gabor) +- **Alerting:** Fix Alertmanager change detection for receivers with secure settings. [#71320](https://github.com/grafana/grafana/issues/71320), [@JacobsonMT](https://github.com/JacobsonMT) +- **Transformations:** Fix `extractFields` throwing Error if one value is undefined or null. [#71267](https://github.com/grafana/grafana/issues/71267), [@svennergr](https://github.com/svennergr) +- **XYChart:** Point size editor should reflect correct default (5). [#71229](https://github.com/grafana/grafana/issues/71229), [@Develer](https://github.com/Develer) +- **Annotations:** Fix database lock while updating annotations. [#71207](https://github.com/grafana/grafana/issues/71207), [@sakjur](https://github.com/sakjur) +- **TimePicker:** Fix issue with previous fiscal quarter not parsing correctly. [#71093](https://github.com/grafana/grafana/issues/71093), [@ashharrison90](https://github.com/ashharrison90) +- **AzureMonitor:** Correctly build multi-resource queries for Application Insights components. [#71039](https://github.com/grafana/grafana/issues/71039), [@aangelisc](https://github.com/aangelisc) +- **AzureMonitor:** Fix metric names for multi-resources. [#70994](https://github.com/grafana/grafana/issues/70994), [@asimpson](https://github.com/asimpson) +- **Logs:** Do not insert log-line into log-fields in json download. [#70954](https://github.com/grafana/grafana/issues/70954), [@gabor](https://github.com/gabor) +- **Loki:** Fix wrong query expression with inline comments. [#70948](https://github.com/grafana/grafana/issues/70948), [@svennergr](https://github.com/svennergr) + + # 10.0.2 (2023-07-11) From 0da199324a67ff8db86fa5633d7b9d9a1f94012b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 26 Jul 2023 10:56:26 +0200 Subject: [PATCH 13/64] logs: log-details: handle dataplane-compliant dataframes (#71935) * logs: log-details: handle dataplane-compliant dataframes * lint fix, removed unused import --- public/app/features/explore/Logs/Logs.tsx | 1 - .../features/explore/Logs/LogsTable.test.tsx | 34 +-- .../app/features/explore/Logs/LogsTable.tsx | 29 ++- .../logs/components/logParser.test.ts | 207 +++++++++++++++++- .../app/features/logs/components/logParser.ts | 134 ++++++------ 5 files changed, 292 insertions(+), 113 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index aa461951f34..72190ecebdb 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -667,7 +667,6 @@ class UnthemedLogs extends PureComponent {
{/* Width should be full width minus logsnavigation and padding */} { }; return ( undefined} timeZone={'utc'} @@ -140,26 +131,3 @@ describe('LogsTable', () => { }); }); }); - -const makeLog = (overrides: Partial): LogRowModel => { - const uid = overrides.uid || '1'; - const entry = `log message ${uid}`; - return { - uid, - entryFieldIndex: 0, - rowIndex: 0, - dataFrame: new MutableDataFrame(), - logLevel: LogLevel.debug, - entry, - hasAnsi: false, - hasUnescapedContent: false, - labels: {}, - raw: entry, - timeFromNow: '', - timeEpochMs: 1, - timeEpochNs: '1000000', - timeLocal: '', - timeUtc: '', - ...overrides, - }; -}; diff --git a/public/app/features/explore/Logs/LogsTable.tsx b/public/app/features/explore/Logs/LogsTable.tsx index 96f3f1c36f9..f4a7c75d828 100644 --- a/public/app/features/explore/Logs/LogsTable.tsx +++ b/public/app/features/explore/Logs/LogsTable.tsx @@ -6,7 +6,6 @@ import { applyFieldOverrides, DataFrame, Field, - LogRowModel, LogsSortOrder, sortDataFrame, SplitOpen, @@ -16,7 +15,7 @@ import { } from '@grafana/data'; import { config } from '@grafana/runtime'; import { Table } from '@grafana/ui'; -import { shouldRemoveField } from 'app/features/logs/components/logParser'; +import { separateVisibleFields } from 'app/features/logs/components/logParser'; import { parseLogsFrame } from 'app/features/logs/logsFrame'; import { getFieldLinksForExplore } from '../utils/links'; @@ -28,7 +27,6 @@ interface Props { splitOpen: SplitOpen; range: TimeRange; logsSortOrder: LogsSortOrder; - rows: LogRowModel[]; } const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => { @@ -40,7 +38,7 @@ const getTableHeight = memoizeOne((dataFrames: DataFrame[] | undefined) => { }); export const LogsTable: React.FunctionComponent = (props) => { - const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames, rows } = props; + const { timeZone, splitOpen, range, logsSortOrder, width, logsFrames } = props; const [tableFrame, setTableFrame] = useState(undefined); @@ -129,18 +127,17 @@ export const LogsTable: React.FunctionComponent = (props) => { }); // remove fields that should not be displayed - dataFrame.fields.forEach((field: Field, index: number) => { - const row = rows[0]; // we just take the first row as the relevant row - if (shouldRemoveField(field, index, row, false, false)) { - transformations.push({ - id: 'organize', - options: { - excludeByName: { - [field.name]: true, - }, + + const hiddenFields = separateVisibleFields(dataFrame, { keepBody: true, keepTimestamp: true }).hidden; + hiddenFields.forEach((field: Field, index: number) => { + transformations.push({ + id: 'organize', + options: { + excludeByName: { + [field.name]: true, }, - }); - } + }, + }); }); if (transformations.length > 0) { const [transformedDataFrame] = await lastValueFrom(transformDataFrame(transformations, [dataFrame])); @@ -150,7 +147,7 @@ export const LogsTable: React.FunctionComponent = (props) => { } }; prepare(); - }, [prepareTableFrame, logsFrames, logsSortOrder, rows]); + }, [prepareTableFrame, logsFrames, logsSortOrder]); if (!tableFrame) { return null; diff --git a/public/app/features/logs/components/logParser.test.ts b/public/app/features/logs/components/logParser.test.ts index 4219c3dc8cb..27fcbe390f9 100644 --- a/public/app/features/logs/components/logParser.test.ts +++ b/public/app/features/logs/components/logParser.test.ts @@ -1,4 +1,4 @@ -import { FieldType, MutableDataFrame } from '@grafana/data'; +import { DataFrameType, Field, FieldType, LogRowModel, MutableDataFrame } from '@grafana/data'; import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; import { createLogRow } from './__mocks__/logRow'; @@ -199,6 +199,211 @@ describe('logParser', () => { expect(fields.length).toBe(1); expect(fields.find((field) => field.keys[0] === testStringField.name)).not.toBe(undefined); }); + + describe('dataplane frames', () => { + const makeLogRow = (fields: Field[], entryFieldIndex: number): LogRowModel => + createLogRow({ + entryFieldIndex, + rowIndex: 0, + dataFrame: { + refId: 'A', + fields, + length: fields[0]?.values.length, + meta: { + type: DataFrameType.LogLines, + }, + }, + }); + + const expectHasField = (defs: FieldDef[], name: string): void => { + expect(defs.find((field) => field.keys[0] === name)).not.toBe(undefined); + }; + + it('should filter out fields with data links that have a nullish value', () => { + const createScenario = (value: unknown) => + makeLogRow( + [ + testTimeField, + testLineField, + { + name: 'link', + type: FieldType.string, + config: { + links: [ + { + title: 'link1', + url: 'https://example.com', + }, + ], + }, + values: [value], + }, + ], + 1 + ); + + expect(getAllFields(createScenario(null))).toHaveLength(0); + expect(getAllFields(createScenario(undefined))).toHaveLength(0); + expect(getAllFields(createScenario(''))).toHaveLength(1); + expect(getAllFields(createScenario('test'))).toHaveLength(1); + // technically this is a field-type-string, but i will add more + // falsy-values, just to be sure + expect(getAllFields(createScenario(false))).toHaveLength(1); + expect(getAllFields(createScenario(NaN))).toHaveLength(1); + expect(getAllFields(createScenario(0))).toHaveLength(1); + expect(getAllFields(createScenario(-0))).toHaveLength(1); + }); + + it('should filter out system-fields without data-links, but should keep severity', () => { + const row = makeLogRow( + [ + testTimeField, + testLineField, + { + config: {}, + name: 'id', + type: FieldType.string, + values: ['id1'], + }, + { + config: {}, + name: 'attributes', + type: FieldType.other, + values: [{ a: 1, b: 2 }], + }, + { + config: {}, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + testStringField, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(2); + expectHasField(output, 'test_field_string'); + expectHasField(output, 'severity'); + }); + + it('should keep system fields with data-links', () => { + const links = [ + { + title: 'link1', + url: 'https://example.com', + }, + ]; + + const row = makeLogRow( + [ + { + ...testTimeField, + config: { links }, + }, + { + ...testLineField, + config: { links }, + }, + { + config: { links }, + name: 'id', + type: FieldType.string, + values: ['id1'], + }, + { + config: { links }, + name: 'attributes', + type: FieldType.other, + values: [{ a: 1, b: 2 }], + }, + { + config: { links }, + name: 'severity', + type: FieldType.string, + values: ['info'], + }, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(5); + expectHasField(output, 'timestamp'); + expectHasField(output, 'body'); + expectHasField(output, 'id'); + expectHasField(output, 'attributes'); + expectHasField(output, 'severity'); + }); + + it('should filter out config-hidden fields', () => { + const row = makeLogRow( + [ + testTimeField, + testLineField, + { + ...testStringField, + config: { + custom: { + hidden: true, + }, + }, + }, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(0); + }); + + it('should filter out fields with null values', () => { + const row = makeLogRow( + [ + testTimeField, + testLineField, + { + // null-value + config: {}, + type: FieldType.string, + name: 'test1', + values: [null], + }, + { + // null-value and data-link + config: { + links: [ + { + title: 'link1', + url: 'https://example.com', + }, + ], + }, + type: FieldType.string, + name: 'test2', + values: [null], + }, + { + // normal value + config: {}, + type: FieldType.string, + name: 'test3', + values: ['testvalue'], + }, + ], + 1 + ); + + const output = getAllFields(row); + + expect(output).toHaveLength(1); + expectHasField(output, 'test3'); + }); + }); }); describe('createLogLineLinks', () => { diff --git a/public/app/features/logs/components/logParser.ts b/public/app/features/logs/components/logParser.ts index 63344030c3c..7e541ca3e53 100644 --- a/public/app/features/logs/components/logParser.ts +++ b/public/app/features/logs/components/logParser.ts @@ -1,9 +1,12 @@ +import { partition } from 'lodash'; import memoizeOne from 'memoize-one'; -import { DataFrame, Field, FieldType, LinkModel, LogRowModel } from '@grafana/data'; +import { DataFrame, Field, FieldWithIndex, LinkModel, LogRowModel } from '@grafana/data'; import { safeStringifyValue } from 'app/core/utils/explore'; import { ExploreFieldLinkModel } from 'app/features/explore/utils/links'; +import { parseLogsFrame } from '../logsFrame'; + export type FieldDef = { keys: string[]; values: string[]; @@ -65,76 +68,83 @@ export const getDataframeFields = memoizeOne( row: LogRowModel, getFieldLinks?: (field: Field, rowIndex: number, dataFrame: DataFrame) => Array> ): FieldDef[] => { - return row.dataFrame.fields - .map((field, index) => ({ ...field, index })) - .filter((field, index) => !shouldRemoveField(field, index, row)) - .map((field) => { - const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; - const fieldVal = field.values[row.rowIndex]; - const outputVal = - typeof fieldVal === 'string' || typeof fieldVal === 'number' - ? fieldVal.toString() - : safeStringifyValue(fieldVal); - return { - keys: [field.name], - values: [outputVal], - links: links, - fieldIndex: field.index, - }; - }); + const visibleFields = separateVisibleFields(row.dataFrame).visible; + const nonEmptyVisibleFields = visibleFields.filter((f) => f.values[row.rowIndex] != null); + return nonEmptyVisibleFields.map((field) => { + const links = getFieldLinks ? getFieldLinks(field, row.rowIndex, row.dataFrame) : []; + const fieldVal = field.values[row.rowIndex]; + const outputVal = + typeof fieldVal === 'string' || typeof fieldVal === 'number' + ? fieldVal.toString() + : safeStringifyValue(fieldVal); + return { + keys: [field.name], + values: [outputVal], + links: links, + fieldIndex: field.index, + }; + }); } ); -export function shouldRemoveField( - field: Field, - index: number, - row: LogRowModel, - shouldRemoveLine = true, - shouldRemoveTime = true -) { - // field that has empty value (we want to keep 0 or empty string) - if (field.values[row.rowIndex] == null) { - return true; +type VisOptions = { + keepTimestamp?: boolean; + keepBody?: boolean; +}; + +// return the fields (their indices to be exact) that should be visible +// based on the logs dataframe structure +function getVisibleFieldIndices(frame: DataFrame, opts: VisOptions): Set { + const logsFrame = parseLogsFrame(frame); + if (logsFrame === null) { + // should not really happen + return new Set(); } - // hidden field, remove - if (field.config.custom?.hidden) { - return true; + // we want to show every "extra" field + const visibleFieldIndices = new Set(logsFrame.extraFields.map((f) => f.index)); + + // we always show the severity field + if (logsFrame.severityField !== null) { + visibleFieldIndices.add(logsFrame.severityField.index); } - // field with data-links, keep - if ((field.config.links ?? []).length > 0) { - return false; - } - // the remaining checks use knowledge of how we parse logs-dataframes - - // Remove field if it is: - // "labels" field that is in Loki used to store all labels - if (field.name === 'labels' && field.type === FieldType.other) { - return true; - } - // id and tsNs are arbitrary added fields in the backend and should be hidden in the UI - if (field.name === 'id' || field.name === 'tsNs') { - return true; - } - if (shouldRemoveTime) { - const firstTimeField = row.dataFrame.fields.find((f) => f.type === FieldType.time); - if ( - field.name === firstTimeField?.name && - field.type === FieldType.time && - field.values[0] === firstTimeField.values[0] - ) { - return true; - } + if (opts.keepBody) { + visibleFieldIndices.add(logsFrame.bodyField.index); } - if (shouldRemoveLine) { - // first string-field is the log-line - const firstStringFieldIndex = row.dataFrame.fields.findIndex((f) => f.type === FieldType.string); - if (firstStringFieldIndex === index) { - return true; - } + if (opts.keepTimestamp) { + visibleFieldIndices.add(logsFrame.timeField.index); } - return false; + return visibleFieldIndices; +} + +// split the dataframe's fields into visible and hidden arrays. +// note: does not do any row-level checks, +// for example does not check if the field's values are nullish +// or not at a givn row. +export function separateVisibleFields( + frame: DataFrame, + opts?: VisOptions +): { visible: FieldWithIndex[]; hidden: FieldWithIndex[] } { + const fieldsWithIndex: FieldWithIndex[] = frame.fields.map((field, index) => ({ ...field, index })); + + const visibleFieldIndices = getVisibleFieldIndices(frame, opts ?? {}); + + const [visible, hidden] = partition(fieldsWithIndex, (f) => { + // hidden fields are always hidden + if (f.config.custom?.hidden) { + return false; + } + + // fields with data-links are visible + if ((f.config.links ?? []).length > 0) { + return true; + } + + return visibleFieldIndices.has(f.index); + }); + + return { visible, hidden }; } From 64c369e17ba24516482aab1226657d20dd9d0d1b Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 26 Jul 2023 12:00:14 +0300 Subject: [PATCH 14/64] Chore: update latest.json to 10.0.3 (#72330) --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index b8596242adc..c004d8d6be8 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "10.0.1", - "testing": "10.0.1" + "stable": "10.0.3", + "testing": "10.0.3" } From ce5609e8ee5ddfb7b1bf3966dc7db49bed352f55 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:07:34 +0200 Subject: [PATCH 15/64] refactor: add wrap for small screen widths (#71864) --- packages/grafana-ui/src/components/Modal/Modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index 696458402a0..3598166fac6 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -115,7 +115,7 @@ function ModalButtonRow({ leftItems, children }: { leftItems?: React.ReactNode; return (
- + {children}
From 4990f36d8b64d618605cf51d2b8631dc54916e81 Mon Sep 17 00:00:00 2001 From: RoxanaAnamariaTurc <106086831+RoxanaAnamariaTurc@users.noreply.github.com> Date: Wed, 26 Jul 2023 10:23:01 +0100 Subject: [PATCH 16/64] A11Y: SelectOptionGroup component fix lint rule about element interactions (#72213) * A11Y: SelectOptionGroup component fix lint rule about element interactions * Undone changes following feedback received --- .../src/components/Select/SelectOptionGroup.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index 368234d99af..08314d41474 100644 --- a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -81,11 +81,11 @@ class UnthemedSelectOptionGroup extends PureComponent return (
- {/* TODO: fix keyboard a11y */} - {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} -
+ {/*React Select doesn't support focusable option group headers, this will be skipped when using + the keyboard */} +
{label} - {' '} +
{expanded && children}
From 7a97bf7f152e86c96ec149719328e02b6bb7a9d3 Mon Sep 17 00:00:00 2001 From: mikkancso Date: Wed, 26 Jul 2023 12:23:05 +0200 Subject: [PATCH 17/64] Data Sources: Remove Admin/Data sources page in favour of Connections/Data sources (#72102) * don't show Admin/Data sources page in navtree * redirect from admin/datasources to connections/datasources * update link of DS plugins to connections/datasources * redirect edit page from datasources to connections * redirect to new datasource page under connections * redirect to datasouce dashboard page under connections * fix navId on datasource dashboards page * fix datasource dashboard page's nav * Revert "update link of DS plugins to connections/datasources" This reverts commit 0ebcb09b038b9db14f16bd0066c26869e57ff253. --- pkg/services/navtree/navtreeimpl/admin.go | 11 ---------- .../pages/DataSourceDashboardsPage.tsx | 9 ++++---- public/app/routes/routes.tsx | 22 +++++++------------ 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go index 8e93bdf3ced..3858c4e003d 100644 --- a/pkg/services/navtree/navtreeimpl/admin.go +++ b/pkg/services/navtree/navtreeimpl/admin.go @@ -4,7 +4,6 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/correlations" - "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" @@ -18,16 +17,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead) authConfigUIAvailable := s.license.FeatureEnabled("saml") - if hasAccess(datasources.ConfigurationPageAccess) { - configNodes = append(configNodes, &navtree.NavLink{ - Text: "Data sources", - Icon: "database", - SubTitle: "Add and configure data sources", - Id: "datasources", - Url: s.cfg.AppSubURL + "/datasources", - }) - } - // FIXME: while we don't have a permissions for listing plugins the legacy check has to stay as a default if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) { configNodes = append(configNodes, &navtree.NavLink{ diff --git a/public/app/features/connections/pages/DataSourceDashboardsPage.tsx b/public/app/features/connections/pages/DataSourceDashboardsPage.tsx index 6b2164c03bd..56095d29358 100644 --- a/public/app/features/connections/pages/DataSourceDashboardsPage.tsx +++ b/public/app/features/connections/pages/DataSourceDashboardsPage.tsx @@ -3,16 +3,15 @@ import { useParams } from 'react-router-dom'; import { Page } from 'app/core/components/Page/Page'; import { DataSourceDashboards } from 'app/features/datasources/components/DataSourceDashboards'; -import { useDataSourceSettingsNav } from 'app/features/datasources/state'; + +import { useDataSourceSettingsNav } from '../hooks/useDataSourceSettingsNav'; export function DataSourceDashboardsPage() { const { uid } = useParams<{ uid: string }>(); - const params = new URLSearchParams(location.search); - const pageId = params.get('page'); - const nav = useDataSourceSettingsNav(uid, pageId); + const { navId, pageNav } = useDataSourceSettingsNav(); return ( - + diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 5f727daa716..16f202022ea 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Redirect } from 'react-router-dom'; +import { Redirect, RouteComponentProps } from 'react-router-dom'; import { isTruthy } from '@grafana/data'; import { LoginPage } from 'app/core/components/Login/LoginPage'; @@ -10,6 +10,7 @@ import { contextSrv } from 'app/core/services/context_srv'; import UserAdminPage from 'app/features/admin/UserAdminPage'; import LdapPage from 'app/features/admin/ldap/LdapPage'; import { getAlertingRoutes } from 'app/features/alerting/routes'; +import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants'; import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/routes'; import { DATASOURCES_ROUTES } from 'app/features/datasources/constants'; import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; @@ -105,30 +106,23 @@ export function getAppRoutes(): RouteDescriptor[] { }, { path: DATASOURCES_ROUTES.List, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/pages/DataSourcesListPage') - ), + component: () => , }, { path: DATASOURCES_ROUTES.Edit, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "EditDataSourcePage"*/ '../features/datasources/pages/EditDataSourcePage') + component: (props: RouteComponentProps<{ uid: string }>) => ( + ), }, { path: DATASOURCES_ROUTES.Dashboards, - component: SafeDynamicImport( - () => - import( - /* webpackChunkName: "DataSourceDashboards"*/ 'app/features/datasources/pages/DataSourceDashboardsPage' - ) + component: (props: RouteComponentProps<{ uid: string }>) => ( + ), }, { path: DATASOURCES_ROUTES.New, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "NewDataSourcePage"*/ '../features/datasources/pages/NewDataSourcePage') - ), + component: () => , }, { path: '/datasources/correlations', From 600b930c478bf8343667305dd9c824f0ebc2eb62 Mon Sep 17 00:00:00 2001 From: Karol Stawowski <66377130+karolstawowski@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:47:58 +0200 Subject: [PATCH 18/64] NestedFolders: Add invalid state to NestedFolderPicker (#72175) * chore: add invalid state to NestedFolderPicker * NestedFolderPicker: pass invalid state to trigger * fix: remove redundant sharedInputStyle --- .../NestedFolderPicker/NestedFolderPicker.tsx | 13 ++++++++++++- .../core/components/NestedFolderPicker/Trigger.tsx | 12 +++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx index 78e11c61ed5..baff647e306 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.tsx @@ -31,6 +31,9 @@ export interface NestedFolderPickerProps { /* Folder UID to show as selected */ value?: string; + /** Show an invalid state around the folder picker */ + invalid?: boolean; + /* Whether to show the root 'Dashboards' (formally General) folder as selectable */ showRootFolder?: boolean; @@ -43,7 +46,13 @@ export interface NestedFolderPickerProps { const EXCLUDED_KINDS = ['empty-folder' as const, 'dashboard' as const]; -export function NestedFolderPicker({ value, showRootFolder = true, excludeUIDs, onChange }: NestedFolderPickerProps) { +export function NestedFolderPicker({ + value, + invalid, + showRootFolder = true, + excludeUIDs, + onChange, +}: NestedFolderPickerProps) { const styles = useStyles2(getStyles); const dispatch = useDispatch(); const selectedFolder = useGetFolderQuery(value || skipToken); @@ -212,6 +221,7 @@ export function NestedFolderPicker({ value, showRootFolder = true, excludeUIDs, return ( : null} placeholder={label ?? t('browse-dashboards.folder-picker.search-placeholder', 'Search folders')} value={search} + invalid={invalid} className={styles.search} onKeyDown={handleKeyDown} onChange={(e) => setSearch(e.currentTarget.value)} diff --git a/public/app/core/components/NestedFolderPicker/Trigger.tsx b/public/app/core/components/NestedFolderPicker/Trigger.tsx index 5814f7e307d..fb3a8581ca2 100644 --- a/public/app/core/components/NestedFolderPicker/Trigger.tsx +++ b/public/app/core/components/NestedFolderPicker/Trigger.tsx @@ -3,18 +3,20 @@ import React, { forwardRef, ReactNode, ButtonHTMLAttributes } from 'react'; import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; -import { useStyles2, Icon, getInputStyles } from '@grafana/ui'; +import { Icon, getInputStyles, useTheme2 } from '@grafana/ui'; import { focusCss } from '@grafana/ui/src/themes/mixins'; import { Text } from '@grafana/ui/src/unstable'; import { Trans } from 'app/core/internationalization'; interface TriggerProps extends ButtonHTMLAttributes { isLoading: boolean; + invalid?: boolean; label?: ReactNode; } -function Trigger({ isLoading, label, ...rest }: TriggerProps, ref: React.ForwardedRef) { - const styles = useStyles2(getStyles); +function Trigger({ isLoading, invalid, label, ...rest }: TriggerProps, ref: React.ForwardedRef) { + const theme = useTheme2(); + const styles = getStyles(theme, invalid); return (
@@ -52,8 +54,8 @@ function Trigger({ isLoading, label, ...rest }: TriggerProps, ref: React.Forward export default forwardRef(Trigger); -const getStyles = (theme: GrafanaTheme2) => { - const baseStyles = getInputStyles({ theme }); +const getStyles = (theme: GrafanaTheme2, invalid = false) => { + const baseStyles = getInputStyles({ theme, invalid }); return { wrapper: baseStyles.wrapper, From 488eac0e492d55e0cffdc880f1712f3e5dfa906a Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 26 Jul 2023 11:07:18 +0000 Subject: [PATCH 19/64] Chore: Remove topnav feature flag (#72337) * Remove topnav feature flag * Allow deprecated flags to be enabled by default * change topnav feature flag to deprecated instead * fix lint --- .../configure-grafana/feature-toggles/index.md | 1 - pkg/services/featuremgmt/registry.go | 6 ++++-- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 2 +- pkg/services/featuremgmt/toggles_gen_test.go | 4 ++-- .../features/datasources/pages/EditDataSourcePage.test.tsx | 3 +-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index a321a8e11f5..83f2879baf0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -24,7 +24,6 @@ Some features are enabled by default. You can disable these feature by setting t | `disableEnvelopeEncryption` | Disable envelope encryption (emergency only) | | | `featureHighlights` | Highlight Grafana Enterprise features | | | `dataConnectionsConsole` | Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins. | Yes | -| `topnav` | Enables new top navigation and page layouts | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes | | `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 45b7bd0dcd4..e93164e5926 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -166,9 +166,11 @@ var ( Owner: grafanaPluginsPlatformSquad, }, { + // Some plugins rely on topnav feature flag being enabled, so we cannot remove this until we + // can afford the breaking change, or we've detemined no one else is relying on it Name: "topnav", - Description: "Enables new top navigation and page layouts", - Stage: FeatureStageGeneralAvailability, + Description: "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", + Stage: FeatureStageDeprecated, Expression: "true", // enabled by default Owner: grafanaFrontendPlatformSquad, }, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 2de9a3a14c6..73abc787319 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -23,7 +23,7 @@ scenes,experimental,@grafana/dashboards-squad,false,false,false,true disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,false,true,false logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false,false dataConnectionsConsole,GA,@grafana/plugins-platform-backend,false,false,false,false -topnav,GA,@grafana/grafana-frontend-platform,false,false,false,false +topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false,false grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false,false entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 17f4e076494..ea7892e55b2 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -104,7 +104,7 @@ const ( FlagDataConnectionsConsole = "dataConnectionsConsole" // FlagTopnav - // Enables new top navigation and page layouts + // Enables topnav support in external plugins. The new Grafana navigation cannot be disabled. FlagTopnav = "topnav" // FlagGrpcServer diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 2e75aeb482c..5f7798195e6 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -30,8 +30,8 @@ func TestFeatureToggleFiles(t *testing.T) { t.Run("check registry constraints", func(t *testing.T) { for _, flag := range standardFeatureFlags { - if flag.Expression == "true" && flag.Stage != FeatureStageGeneralAvailability { - t.Errorf("only FeatureStageGeneralAvailability features can be enabled by default. See: %s", flag.Name) + if flag.Expression == "true" && !(flag.Stage == FeatureStageGeneralAvailability || flag.Stage == FeatureStageDeprecated) { + t.Errorf("only FeatureStageGeneralAvailability or FeatureStageDeprecated features can be enabled by default. See: %s", flag.Name) } if flag.RequiresDevMode && flag.Stage != FeatureStageExperimental { t.Errorf("only alpha features can require dev mode. See: %s", flag.Name) diff --git a/public/app/features/datasources/pages/EditDataSourcePage.test.tsx b/public/app/features/datasources/pages/EditDataSourcePage.test.tsx index 544e1cc01d8..4ad10721056 100644 --- a/public/app/features/datasources/pages/EditDataSourcePage.test.tsx +++ b/public/app/features/datasources/pages/EditDataSourcePage.test.tsx @@ -4,7 +4,7 @@ import { Store } from 'redux'; import { TestProvider } from 'test/helpers/TestProvider'; import { LayoutModes } from '@grafana/data'; -import { setAngularLoader, config, setPluginExtensionGetter } from '@grafana/runtime'; +import { setAngularLoader, setPluginExtensionGetter } from '@grafana/runtime'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { configureStore } from 'app/store/configureStore'; @@ -111,7 +111,6 @@ describe('', () => { }); it('should show updated action buttons when topnav is on', async () => { - config.featureToggles.topnav = true; setup(uid, store); await waitFor(() => { From e3ec53b41849bf9008efd278897bf20929376f86 Mon Sep 17 00:00:00 2001 From: Dimitris Sotirakis Date: Wed, 26 Jul 2023 14:18:00 +0300 Subject: [PATCH 20/64] CI: Fix `deb/rpm` bug for linux package publishing (#72336) Fix deb/rpm (cherry picked from commit c3ebd388e3cc192590c3f4d381429f7d9e345765) (cherry picked from commit 4c9bdef98dcfb24d8d24dac8117c9d3feb921a06) # Conflicts: # .drone.yml --- .drone.yml | 6 +++--- scripts/drone/steps/lib.star | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.drone.yml b/.drone.yml index 78f3325937b..c106d12d1f1 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3292,7 +3292,7 @@ steps: from_secret: packages_gpg_private_key gpg_public_key: from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*$${DRONE_TAG}/oss/**.deb + package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/oss/**.deb secret_access_key: from_secret: packages_secret_access_key service_account_json: @@ -3313,7 +3313,7 @@ steps: from_secret: packages_gpg_private_key gpg_public_key: from_secret: packages_gpg_public_key - package_path: gs://grafana-prerelease/artifacts/downloads/*$${DRONE_TAG}/oss/**.rpm + package_path: gs://grafana-prerelease/artifacts/downloads/*${DRONE_TAG}/oss/**.rpm secret_access_key: from_secret: packages_secret_access_key service_account_json: @@ -4968,6 +4968,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: 171ee57a344cc5faf219415c40714ca928e96d135f54282da73cde6cb778478e +hmac: 5379326ce6bb3db880a951fc714d01b10f7382a7b7ec73510898fee53061008b ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 42d89768fed..7fef29623d8 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1245,7 +1245,7 @@ def publish_linux_packages_step(package_manager = "deb"): "gpg_passphrase": from_secret("packages_gpg_passphrase"), "gpg_public_key": from_secret("packages_gpg_public_key"), "gpg_private_key": from_secret("packages_gpg_private_key"), - "package_path": "gs://grafana-prerelease/artifacts/downloads/*$${{DRONE_TAG}}/oss/**.{}".format( + "package_path": "gs://grafana-prerelease/artifacts/downloads/*${{DRONE_TAG}}/oss/**.{}".format( package_manager, ), }, From 876f96e5e8405d9c87efd4b834a735be8819790c Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 26 Jul 2023 15:06:42 +0300 Subject: [PATCH 21/64] InfluxDB: Change feature toggle stage (#72348) Change feature toggle stage --- .../setup-grafana/configure-grafana/feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 2 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 83f2879baf0..871eff54c13 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -62,6 +62,7 @@ Some features are enabled by default. You can disable these feature by setting t | `accessControlOnCall` | Access control primitives for OnCall | | `nestedFolders` | Enable folder nesting | | `alertingNoNormalState` | Stop maintaining state of alerts that are not firing | +| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | | `renderAuthJWT` | Uses JWT-based auth for rendering instead of relying on remote cache | | `refactorVariablesTimeRange` | Refactor time range variables flow to reduce number of API calls made when query variables are chained | | `enableElasticsearchBackendQuerying` | Enable the processing of queries and responses in the Elasticsearch data source through backend | @@ -96,7 +97,6 @@ Experimental features might be changed or removed without prior notice. | `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | | `individualCookiePreferences` | Support overriding cookie preferences per user | | `timeSeriesTable` | Enable time series table transformer & sparkline cell type | -| `influxdbBackendMigration` | Query InfluxDB InfluxQL without the proxy | | `clientTokenRotation` | Replaces the current in-request token rotation so that the client initiates the rotation | | `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | | `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e93164e5926..6d76135d3df 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -350,7 +350,7 @@ var ( { Name: "influxdbBackendMigration", Description: "Query InfluxDB InfluxQL without the proxy", - Stage: FeatureStageExperimental, + Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, }, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 73abc787319..401c28e3d99 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -49,7 +49,7 @@ gcomOnlyExternalOrgRoleSync,GA,@grafana/grafana-authnz-team,false,false,false,fa prometheusMetricEncyclopedia,GA,@grafana/observability-metrics,false,false,false,true timeSeriesTable,experimental,@grafana/app-o11y,false,false,false,true prometheusResourceBrowserCache,GA,@grafana/observability-metrics,false,false,false,true -influxdbBackendMigration,experimental,@grafana/observability-metrics,false,false,false,true +influxdbBackendMigration,preview,@grafana/observability-metrics,false,false,false,true clientTokenRotation,experimental,@grafana/grafana-authnz-team,false,false,false,false prometheusDataplane,GA,@grafana/observability-metrics,false,false,false,false lokiMetricDataplane,GA,@grafana/observability-logs,false,false,false,false From f629698876183cf97c4eee856e6a4317efbb5fa7 Mon Sep 17 00:00:00 2001 From: Ludovic Viaud Date: Wed, 26 Jul 2023 14:08:46 +0200 Subject: [PATCH 22/64] Toggle transformationsRedesign for prod (#72207) * Toggle transformationsRedesign for prod --- .../setup-grafana/configure-grafana/feature-toggles/index.md | 2 +- pkg/services/featuremgmt/registry.go | 3 ++- pkg/services/featuremgmt/toggles_gen.csv | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) 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 871eff54c13..1ff5ea82745 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -43,6 +43,7 @@ Some features are enabled by default. You can disable these feature by setting t | `alertingNotificationsPoliciesMatchingInstances` | Enables the preview of matching instances for notification policies | Yes | | `useCachingService` | When turned on, the new query and resource caching implementation using a wire service inject will be used in place of the previous middleware implementation | | | `advancedDataSourcePicker` | Enable a new data source picker with contextual information, recently used order and advanced mode | Yes | +| `transformationsRedesign` | Enables the transformations redesign | Yes | | `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes | ## Preview feature toggles @@ -121,7 +122,6 @@ Experimental features might be changed or removed without prior notice. | `prometheusIncrementalQueryInstrumentation` | Adds RudderStack events to incremental queries | | `logsExploreTableVisualisation` | A table visualisation for logs in Explore | | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `transformationsRedesign` | Enables the transformations redesign | | `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | | `disableTraceQLStreaming` | Disables the option to stream the response of TraceQL queries of the Tempo data source | diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6d76135d3df..48a1118c7a9 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -613,8 +613,9 @@ var ( { Name: "transformationsRedesign", Description: "Enables the transformations redesign", - Stage: FeatureStageExperimental, + Stage: FeatureStageGeneralAvailability, FrontendOnly: true, + Expression: "true", // enabled by default Owner: grafanaObservabilityMetricsSquad, }, { diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 401c28e3d99..5bb00b56c20 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -88,7 +88,7 @@ vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,false,true prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,false,true logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,false,true awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false,false -transformationsRedesign,experimental,@grafana/observability-metrics,false,false,false,true +transformationsRedesign,GA,@grafana/observability-metrics,false,false,false,true toggleLabelsInLogsUI,experimental,@grafana/observability-logs,false,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false disableTraceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true From e56f97ec71ac560fc31b90d35b9d90a08240aa4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Wed, 26 Jul 2023 14:17:02 +0200 Subject: [PATCH 23/64] grafana-data: handle reordering of field.nanos (#72290) * grafana-data: handle reordering of field.nanos * do not add nanos:undefined --- .../src/dataframe/processDataFrame.test.ts | 27 +++++++++++++++++++ .../src/dataframe/processDataFrame.ts | 21 +++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/grafana-data/src/dataframe/processDataFrame.test.ts b/packages/grafana-data/src/dataframe/processDataFrame.test.ts index 4d5c6485004..2e23be6e171 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.test.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.test.ts @@ -8,6 +8,7 @@ import { guessFieldTypes, isDataFrame, isTableData, + reverseDataFrame, sortDataFrame, toDataFrame, toLegacyResponseData, @@ -360,19 +361,45 @@ describe('sorted DataFrame', () => { { name: 'fist', type: FieldType.time, values: [1, 2, 3] }, { name: 'second', type: FieldType.string, values: ['a', 'b', 'c'] }, { name: 'third', type: FieldType.number, values: [2000, 3000, 1000] }, + { name: 'fourth', type: FieldType.time, values: [1, 2, 3], nanos: [10, 20, 30] }, ], }); it('Should sort numbers', () => { const sorted = sortDataFrame(frame, 0, true); expect(sorted.length).toEqual(3); expect(sorted.fields[0].values).toEqual([3, 2, 1]); + expect(sorted.fields[0].nanos).toBeUndefined(); expect(sorted.fields[1].values).toEqual(['c', 'b', 'a']); + expect(sorted.fields[1].nanos).toBeUndefined(); + expect(sorted.fields[3].values).toEqual([3, 2, 1]); + expect(sorted.fields[3].nanos).toEqual([30, 20, 10]); }); it('Should sort strings', () => { const sorted = sortDataFrame(frame, 1, true); expect(sorted.length).toEqual(3); expect(sorted.fields[0].values).toEqual([3, 2, 1]); + expect(sorted.fields[0].nanos).toBeUndefined(); expect(sorted.fields[1].values).toEqual(['c', 'b', 'a']); + expect(sorted.fields[1].nanos).toBeUndefined(); + expect(sorted.fields[3].values).toEqual([3, 2, 1]); + expect(sorted.fields[3].nanos).toEqual([30, 20, 10]); + }); +}); + +describe('reverse DataFrame', () => { + const frame = toDataFrame({ + fields: [ + { name: 'fist', type: FieldType.time, values: [1, 2, 3], nanos: [10, 20, 30] }, + { name: 'third', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + it('should reverse dataframe', () => { + const rev = reverseDataFrame(frame); + expect(rev.length).toEqual(3); + expect(rev.fields[0].values).toEqual([3, 2, 1]); + expect(rev.fields[0].nanos).toEqual([30, 20, 10]); + expect(rev.fields[1].values).toEqual(['c', 'b', 'a']); + expect(rev.fields[1].nanos).toBeUndefined(); }); }); diff --git a/packages/grafana-data/src/dataframe/processDataFrame.ts b/packages/grafana-data/src/dataframe/processDataFrame.ts index 09e67d8349d..402f4962508 100644 --- a/packages/grafana-data/src/dataframe/processDataFrame.ts +++ b/packages/grafana-data/src/dataframe/processDataFrame.ts @@ -431,10 +431,17 @@ export function sortDataFrame(data: DataFrame, sortIndex?: number, reverse = fal return { ...data, fields: data.fields.map((f) => { - return { + const newF = { ...f, values: f.values.map((v, i) => f.values[index[i]]), }; + + // only add .nanos if it exists + const { nanos } = f; + if (nanos !== undefined) { + newF.nanos = nanos.map((n, i) => nanos[index[i]]); + } + return newF; }), }; } @@ -448,10 +455,20 @@ export function reverseDataFrame(data: DataFrame): DataFrame { fields: data.fields.map((f) => { const values = [...f.values]; values.reverse(); - return { + + const newF = { ...f, values, }; + + // only add .nanos if it exists + const { nanos } = f; + if (nanos !== undefined) { + const revNanos = [...nanos]; + revNanos.reverse(); + newF.nanos = revNanos; + } + return newF; }), }; } From 07365f2a733a521c9a0cce5b1b4dbb0ab42fc621 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 26 Jul 2023 14:58:09 +0200 Subject: [PATCH 24/64] Chore: Remove unnecessary go.mod replace (#72346) --- go.mod | 4 +- go.sum | 675 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 677 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e61e4031503..b669b543f26 100644 --- a/go.mod +++ b/go.mod @@ -394,6 +394,8 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/term v0.10.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect @@ -485,8 +487,6 @@ replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-aler // grpc v1.46.0 removed "WithBalancerName()" API, still in use by weaveworks/commons. replace google.golang.org/grpc => google.golang.org/grpc v1.45.0 -replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 - // Use 1.10.6 of pq to avoid a change in 1.10.7 that has certificate validation issues. https://github.com/grafana/grafana/issues/65816 replace github.com/lib/pq => github.com/lib/pq v1.10.6 diff --git a/go.sum b/go.sum index 7ea1ef6cdb1..c7788fe323f 100644 --- a/go.sum +++ b/go.sum @@ -45,12 +45,103 @@ cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFO cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= @@ -61,6 +152,7 @@ cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQH cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0 h1:+9zda3WGgW1ZSTlVppLCYFIr48Pa35q1uG2N1itbCEQ= @@ -70,27 +162,310 @@ cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.1 h1:7hm1bRqGCA1GBRQUrp831TwJ9TWhP+tvLuP497CQS2g= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4= cloud.google.com/go/monitoring v1.4.0/go.mod h1:y6xnxfwI3hTFWOdkOaD7nfJVlwuC3/mS/5kvtT131p4= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.19.0/go.mod h1:/O9kmSe9bb9KRnIAWkzmqhPjHo6LtzGOBYd/kr06XSs= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/secretmanager v1.3.0/go.mod h1:+oLTkouyiYiabAQNugCeTS3PAArGiMJuBqvJnJsyH+U= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -101,10 +476,66 @@ cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKu cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A= cloud.google.com/go/trace v1.2.0/go.mod h1:Wc8y/uYyOhPy12KEnXG9XGrvfMz5F5SrYecQlbW1rwM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8= contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE= @@ -112,6 +543,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg= filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= @@ -190,6 +622,7 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.29.0/go.mod h1:spvB9eLJH9dutlbP github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= @@ -235,7 +668,10 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -259,8 +695,10 @@ github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3 github.com/apache/arrow/go/arrow v0.0.0-20210223225224-5bea62493d91/go.mod h1:c9sxoIT3YgLxH4UhLOCKaBlEojuMhVYpk4Ntv3opUTQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 h1:q4dksr6ICHXqG5hm0ZW5IHyeEJXoIJSOZeBLmWPNeIQ= github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40/go.mod h1:Q7yQnSMnLvcXlZ8RV+jwz/6y1rQTqbX6C82SndT52Zs= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -384,6 +822,7 @@ github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvF github.com/bmatcuk/doublestar/v2 v2.0.3/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ= @@ -440,6 +879,7 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230112175826-46e39c7b9b43 h1:XP+uhjN0yBCN/tPkr8Z0BNDc5rZam9RG6UWyf2FrSQ0= @@ -609,6 +1049,7 @@ github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.11.0 h1:jtLewhRR2vMRNnq2ZZUoCjUlgut+Y0+sDDWPOfwOi1o= github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= @@ -678,6 +1119,7 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= @@ -703,6 +1145,7 @@ github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs= github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -816,6 +1259,8 @@ github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbN github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= @@ -1099,6 +1544,7 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -1644,6 +2090,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -1652,9 +2099,11 @@ github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8 github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/knadh/koanf v0.14.1-0.20201201075439-e0853799f9ec/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= @@ -1787,6 +2236,7 @@ github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOq github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= @@ -1805,6 +2255,8 @@ github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJys github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo= github.com/miekg/dns v1.1.51/go.mod h1:2Z9d3CP1LQWihRZUf29mQ19yDThaI4DAYzte2CaQW5c= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= @@ -2021,9 +2473,11 @@ github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -2119,6 +2573,7 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn github.com/redis/go-redis/v9 v9.0.2 h1:BA426Zqe/7r56kCcvxYLWe1mkaz71LKF77GwgFzSxfE= github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= @@ -2159,6 +2614,7 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= @@ -2427,6 +2883,8 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= @@ -2646,6 +3104,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20230108222341-4b8118a2686a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= @@ -2659,7 +3118,13 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -2848,6 +3313,7 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= @@ -3007,6 +3473,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -3038,6 +3505,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -3058,6 +3526,7 @@ golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -3088,6 +3557,7 @@ golang.org/x/tools v0.0.0-20190104182027-498d95493402/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190111214448-fc1d57b08d7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190118193359-16909d206f00/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -3158,6 +3628,7 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -3167,9 +3638,11 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= @@ -3194,6 +3667,7 @@ gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= gonum.org/v1/gonum v0.7.0/go.mod h1:L02bwd0sqlsvRv41G7wGWFCsVNZFv/k1xzGIxeANHGM= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= @@ -3201,6 +3675,7 @@ gonum.org/v1/netlib v0.0.0-20191229114700-bbb4dff026f8/go.mod h1:2IgXn/sJaRbePPB gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.0.0-20200111075622-4abb28f724d5/go.mod h1:+HbaZVpsa73UwN7kXGCECULRHovLRJjH+t5cFPgxErs= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= @@ -3246,6 +3721,7 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= @@ -3253,6 +3729,7 @@ google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3p google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= @@ -3260,7 +3737,10 @@ google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91 google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= @@ -3274,8 +3754,166 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200911024640-645f7a48b24f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210429181445-86c259c2b4ab/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210517163617-5e0236093d7a/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79/go.mod h1:yiaVoXHpRzHGyxV3o4DktVWY4mSUErTKaeEOq6C3t3U= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3 h1:SeX3QUcBj3fciwnfPT9kt5gBhFy/FCZtYZ+I/RB8agc= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e h1:Ao9GzfUMPH3zjVfzXG5rlWlk+Q8MXWKwWpwVQE1MXfw= +google.golang.org/genproto v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v0.0.0-20200910201057-6591123024b3/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -3288,6 +3926,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= @@ -3378,6 +4018,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0= @@ -3412,11 +4053,45 @@ k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From cae68b955bfdf9997a5ce072df19ec4d30b2298c Mon Sep 17 00:00:00 2001 From: Polina Boneva <13227501+polibb@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:02:04 +0300 Subject: [PATCH 25/64] Dashboard: New Datasource picker link is keyboard accessible (#72134) * WIP * fixes for readability * fix * WIP * Keep tab index working with portal * Use callback and clean up * Fix linting errors * Ignore clickable element --------- Co-authored-by: Ivan Ortega --- .../components/picker/DataSourceDropdown.tsx | 291 +++++++++++------- 1 file changed, 182 insertions(+), 109 deletions(-) diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx index 78d7fe9c16e..7d0e99d3caa 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx +++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx @@ -1,5 +1,6 @@ import { css } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; +import { FocusScope } from '@react-aria/focus'; import { useOverlay } from '@react-aria/overlays'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { usePopper } from 'react-popper'; @@ -70,55 +71,25 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { ...restProps } = props; + const styles = useStyles2((theme: GrafanaTheme2) => getStylesDropdown(theme, props)); const [isOpen, setOpen] = useState(false); const [inputHasFocus, setInputHasFocus] = useState(false); - const [markerElement, setMarkerElement] = useState(); - const [selectorElement, setSelectorElement] = useState(); const [filterTerm, setFilterTerm] = useState(''); - const openDropdown = () => { - reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN }); - setOpen(true); - markerElement?.focus(); - }; + const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); + const ref = useRef(null); + + // Used to position the popper correctly and to bring back the focus when navigating from footer to input + const [markerElement, setMarkerElement] = useState(); + // Used to position the popper correctly + const [selectorElement, setSelectorElement] = useState(); + // Used to move the focus to the footer when tabbing from the input + const [footerRef, setFooterRef] = useState(); const currentDataSourceInstanceSettings = useDatasource(current); + const grafanaDS = useDatasource('-- Grafana --'); const currentValue = Boolean(!current && noDefault) ? undefined : currentDataSourceInstanceSettings; const prefixIcon = filterTerm && isOpen ? : ; - const { onKeyDown, keyboardEvents } = useKeyNavigationListener(); - - useEffect(() => { - const sub = keyboardEvents.subscribe({ - next: (keyEvent) => { - switch (keyEvent?.code) { - case 'ArrowDown': { - openDropdown(); - keyEvent.preventDefault(); - break; - } - case 'ArrowUp': - openDropdown(); - keyEvent.preventDefault(); - break; - case 'Escape': - onClose(); - markerElement?.focus(); - keyEvent.preventDefault(); - } - }, - }); - return () => sub.unsubscribe(); - }); - const grafanaDS = useDatasource('-- Grafana --'); - - const onClickAddCSV = () => { - if (!grafanaDS) { - return; - } - - onChange(grafanaDS, [defaultFileUploadQuery]); - }; - const popper = usePopper(markerElement, selectorElement, { placement: 'bottom-start', modifiers: [ @@ -136,9 +107,9 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { const onClose = useCallback(() => { setFilterTerm(''); setOpen(false); - }, [setOpen]); + markerElement?.focus(); + }, [setOpen, markerElement]); - const ref = useRef(null); const { overlayProps, underlayProps } = useOverlay( { onClose: onClose, @@ -150,9 +121,72 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { }, ref ); - const { dialogProps } = useDialog({}, ref); + const { dialogProps } = useDialog( + { + 'aria-label': 'Opened data source picker list', + }, + ref + ); - const styles = useStyles2((theme: GrafanaTheme2) => getStylesDropdown(theme, props)); + function openDropdown() { + reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN }); + setOpen(true); + markerElement?.focus(); + } + + function onClickAddCSV() { + if (!grafanaDS) { + return; + } + + onChange(grafanaDS, [defaultFileUploadQuery]); + } + + function onKeyDownInput(keyEvent: React.KeyboardEvent) { + // From the input, it navigates to the footer + if (keyEvent.key === 'Tab' && !keyEvent.shiftKey && isOpen) { + keyEvent.preventDefault(); + footerRef?.focus(); + } + // From the input, if we navigate back, it closes the dropdown + if (keyEvent.key === 'Tab' && keyEvent.shiftKey && isOpen) { + onClose(); + } + onKeyDown(keyEvent); + } + + function onNavigateOutsiteFooter(e: React.KeyboardEvent) { + // When navigating back, the dropdown keeps open and the input element is focused. + if (e.shiftKey) { + e.preventDefault(); + markerElement?.focus(); + // When navigating forward, the dropdown closes and and the element next to the input element is focused. + } else { + onClose(); + } + } + + useEffect(() => { + const sub = keyboardEvents.subscribe({ + next: (keyEvent) => { + switch (keyEvent?.code) { + case 'ArrowDown': + openDropdown(); + keyEvent.preventDefault(); + break; + case 'ArrowUp': + openDropdown(); + keyEvent.preventDefault(); + break; + case 'Escape': + onClose(); + keyEvent.preventDefault(); + break; + } + }, + }); + return () => sub.unsubscribe(); + }); return (
@@ -166,15 +200,13 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { prefix={currentValue ? prefixIcon : undefined} suffix={} placeholder={hideTextValue ? '' : dataSourceLabel(currentValue) || placeholder} - onClick={openDropdown} onFocus={() => { setInputHasFocus(true); }} onBlur={() => { setInputHasFocus(false); - onClose(); }} - onKeyDown={onKeyDown} + onKeyDown={onKeyDownInput} value={filterTerm} onChange={(e) => { openDropdown(); @@ -187,19 +219,16 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { {isOpen ? (
- {/* TODO: fix keyboard a11y */} - {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */} -
{ - e.preventDefault(); /** Need to prevent default here to stop onMouseDown to trigger onBlur of the input element */ - }} - > +
{ onClose(); if (ds.uid !== currentValue?.uid) { @@ -208,13 +237,9 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { } }} onClose={onClose} - current={currentValue} - style={popper.styles.popper} - ref={setSelectorElement} onClickAddCSV={onClickAddCSV} - {...restProps} onDismiss={onClose} - {...popper.attributes.popper} + onNavigateOutsiteFooter={onNavigateOutsiteFooter} />
@@ -249,10 +274,13 @@ export interface PickerContentProps extends DataSourceDropdownProps { filterTerm?: string; onClose: () => void; onDismiss: () => void; + footerRef: (element: HTMLElement | null) => void; + onNavigateOutsiteFooter: (e: React.KeyboardEvent) => void; } const PickerContent = React.forwardRef((props, ref) => { - const { filterTerm, onChange, onClose, onClickAddCSV, current, filter, uploadFile } = props; + const { filterTerm, onChange, onClose, onClickAddCSV, current, filter } = props; + const changeCallback = useCallback( (ds: DataSourceInstanceSettings) => { onChange(ds); @@ -285,50 +313,14 @@ const PickerContent = React.forwardRef((prop } > -
- - {({ showModal, hideModal }) => ( - - )} - - {uploadFile && config.featureToggles.editPanelCSVDragAndDrop && ( - - )} -
+ +
+
); }); @@ -359,3 +351,84 @@ function getStylesPickerContent(theme: GrafanaTheme2) { `, }; } + +export interface FooterProps extends PickerContentProps {} + +function Footer({ onClose, onChange, onClickAddCSV, ...props }: FooterProps) { + const styles = useStyles2(getStylesFooter); + const isUploadFileEnabled = props.uploadFile && config.featureToggles.editPanelCSVDragAndDrop; + + const onKeyDownLastButton = (e: React.KeyboardEvent) => { + if (e.key === 'Tab') { + props.onNavigateOutsiteFooter(e); + } + }; + const onKeyDownFirstButton = (e: React.KeyboardEvent) => { + if (e.key === 'Tab' && e.shiftKey) { + props.onNavigateOutsiteFooter(e); + } + }; + + return ( +
+ + {({ showModal, hideModal }) => ( + + )} + + {isUploadFileEnabled && ( + + )} +
+ ); +} + +function getStylesFooter(theme: GrafanaTheme2) { + return { + footer: css` + flex: 0; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + padding: ${theme.spacing(1.5)}; + border-top: 1px solid ${theme.colors.border.weak}; + background-color: ${theme.colors.background.secondary}; + `, + }; +} From d96067985b3f4752bef2a2545c926a97c65393b6 Mon Sep 17 00:00:00 2001 From: stratomonitor Date: Wed, 26 Jul 2023 15:21:14 +0200 Subject: [PATCH 26/64] Prometheus: Add present_over_time syntax highlighting (#72283) add prometheus present_over_time syntax highlighting to color present_over_time keyword for prometheus --- public/app/plugins/datasource/prometheus/promql.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index 8b4f032989a..6baa26d4899 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -521,6 +521,12 @@ export const FUNCTIONS = [ detail: 'last_over_time(range-vector)', documentation: 'The most recent point value in specified interval.', }, + { + insertText: 'present_over_time', + label: 'present_over_time', + detail: 'present_over_time(range-vector)', + documentation: 'The value 1 for any series in the specified interval.', + }, ]; export const PROM_KEYWORDS = FUNCTIONS.map((keyword) => keyword.label); From c3d6f795eaaaf880e08e2d25a07ad4fedcfd1f35 Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Wed, 26 Jul 2023 08:26:58 -0500 Subject: [PATCH 27/64] Only trigger downstream builds on Grafana (#72356) --- .drone.yml | 4 +++- scripts/drone/pipelines/trigger_downstream.star | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.drone.yml b/.drone.yml index c106d12d1f1..63e542b1e88 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2417,6 +2417,8 @@ trigger: - '*.md' - docs/** - latest.json + repo: + - grafana/grafana type: docker volumes: - host: @@ -4968,6 +4970,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: 5379326ce6bb3db880a951fc714d01b10f7382a7b7ec73510898fee53061008b +hmac: 3f94603ccb6df539771470e23415094c86d606d5fd823409cd685346de3742e0 ... diff --git a/scripts/drone/pipelines/trigger_downstream.star b/scripts/drone/pipelines/trigger_downstream.star index d35e2ce090a..9004bf6e712 100644 --- a/scripts/drone/pipelines/trigger_downstream.star +++ b/scripts/drone/pipelines/trigger_downstream.star @@ -23,6 +23,9 @@ trigger = { "latest.json", ], }, + "repo": [ + "grafana/grafana", + ], } def enterprise_downstream_pipeline(): From 89092a1e697788428f76fead65371b7c904cd13f Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Wed, 26 Jul 2023 14:33:16 +0100 Subject: [PATCH 28/64] Tempo: Use feature toggle to control TraceQL streaming (#72288) Rename traceql streaming feature toggle. Remove the manual toggle from Options component and use the feature toggle --- .../feature-toggles/index.md | 2 +- .../src/types/featureToggles.gen.ts | 2 +- .../dataquery/x/TempoDataQuery_types.gen.ts | 4 ---- pkg/services/featuremgmt/registry.go | 4 ++-- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 6 +++--- .../kinds/dataquery/types_dataquery_gen.go | 3 --- .../plugins/datasource/tempo/dataquery.cue | 2 -- .../plugins/datasource/tempo/dataquery.gen.ts | 4 ---- .../plugins/datasource/tempo/datasource.ts | 8 ++++---- .../traceql/TempoQueryBuilderOptions.tsx | 19 +------------------ 11 files changed, 13 insertions(+), 43 deletions(-) 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 1ff5ea82745..e7bb180363e 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -124,7 +124,7 @@ Experimental features might be changed or removed without prior notice. | `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | | `toggleLabelsInLogsUI` | Enable toggleable filters in log details view | | `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `disableTraceQLStreaming` | Disables the option to stream the response of TraceQL queries of the Tempo data source | +| `traceQLStreaming` | Enables response streaming of TraceQL queries of the Tempo data source | | `grafanaAPIServer` | Enable Kubernetes API Server for Grafana resources | | `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | | `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the `useCachingService` feature toggle is enabled and the datasource has caching and async query support enabled | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 570d7400ec5..449ebfd07b3 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -110,7 +110,7 @@ export interface FeatureToggles { transformationsRedesign?: boolean; toggleLabelsInLogsUI?: boolean; mlExpressions?: boolean; - disableTraceQLStreaming?: boolean; + traceQLStreaming?: boolean; grafanaAPIServer?: boolean; featureToggleAdminPage?: boolean; awsAsyncQueryCaching?: boolean; diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index e3099fc65cf..f148695125b 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -51,10 +51,6 @@ export interface TempoQuery extends common.DataQuery { * @deprecated Query traces by span name */ spanName?: string; - /** - * Use the streaming API to get partial results as they are available - */ - streaming?: boolean; } export const defaultTempoQuery: Partial = { diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 48a1118c7a9..feca32609ff 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -633,8 +633,8 @@ var ( Owner: grafanaAlertingSquad, }, { - Name: "disableTraceQLStreaming", - Description: "Disables the option to stream the response of TraceQL queries of the Tempo data source", + Name: "traceQLStreaming", + Description: "Enables response streaming of TraceQL queries of the Tempo data source", Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5bb00b56c20..7d16a0c55c6 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -91,7 +91,7 @@ awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false, transformationsRedesign,GA,@grafana/observability-metrics,false,false,false,true toggleLabelsInLogsUI,experimental,@grafana/observability-logs,false,false,false,true mlExpressions,experimental,@grafana/alerting-squad,false,false,false,false -disableTraceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true +traceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,false,true grafanaAPIServer,experimental,@grafana/grafana-app-platform-squad,false,false,false,false featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,false,true,false awsAsyncQueryCaching,experimental,@grafana/aws-datasources,false,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index ea7892e55b2..14adb86c21c 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -375,9 +375,9 @@ const ( // Enable support for Machine Learning in server-side expressions FlagMlExpressions = "mlExpressions" - // FlagDisableTraceQLStreaming - // Disables the option to stream the response of TraceQL queries of the Tempo data source - FlagDisableTraceQLStreaming = "disableTraceQLStreaming" + // FlagTraceQLStreaming + // Enables response streaming of TraceQL queries of the Tempo data source + FlagTraceQLStreaming = "traceQLStreaming" // FlagGrafanaAPIServer // Enable Kubernetes API Server for Grafana resources diff --git a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go index 771f71d6e09..9a1e4d8c9cc 100644 --- a/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go +++ b/pkg/tsdb/tempo/kinds/dataquery/types_dataquery_gen.go @@ -121,9 +121,6 @@ type TempoQuery struct { // @deprecated Query traces by span name SpanName *string `json:"spanName,omitempty"` - - // Use the streaming API to get partial results as they are available - Streaming *bool `json:"streaming,omitempty"` } // TempoQueryType search = Loki search, nativeSearch = Tempo search for backwards compatibility diff --git a/public/app/plugins/datasource/tempo/dataquery.cue b/public/app/plugins/datasource/tempo/dataquery.cue index 82016c131d3..a54bb10e116 100644 --- a/public/app/plugins/datasource/tempo/dataquery.cue +++ b/public/app/plugins/datasource/tempo/dataquery.cue @@ -44,8 +44,6 @@ composableKinds: DataQuery: { serviceMapIncludeNamespace?: bool // Defines the maximum number of traces that are returned from Tempo limit?: int64 - // Use the streaming API to get partial results as they are available - streaming?: bool filters: [...#TraceqlFilter] } @cuetsy(kind="interface") @grafana(TSVeneer="type") diff --git a/public/app/plugins/datasource/tempo/dataquery.gen.ts b/public/app/plugins/datasource/tempo/dataquery.gen.ts index 0ddba987cfd..7bfac5a1758 100644 --- a/public/app/plugins/datasource/tempo/dataquery.gen.ts +++ b/public/app/plugins/datasource/tempo/dataquery.gen.ts @@ -48,10 +48,6 @@ export interface TempoQuery extends common.DataQuery { * @deprecated Query traces by span name */ spanName?: string; - /** - * Use the streaming API to get partial results as they are available - */ - streaming?: boolean; } export const defaultTempoQuery: Partial = { diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index bc72734c7ea..bc58422a6fb 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -223,10 +223,10 @@ export class TempoDatasource extends DataSourceWithBackend(({ onChange, query }) query.limit = DEFAULT_LIMIT; } - if (!query.hasOwnProperty('streaming')) { - query.streaming = !config.featureToggles.disableTraceQLStreaming; - } - const onLimitChange = (e: React.FormEvent) => { onChange({ ...query, limit: parseInt(e.currentTarget.value, 10) }); }; - const onStreamingChange = (e: React.FormEvent) => { - onChange({ ...query, streaming: e.currentTarget.checked }); - }; - const collapsedInfoList = [`Limit: ${query.limit || DEFAULT_LIMIT}`]; - if (!config.featureToggles.disableTraceQLStreaming) { - collapsedInfoList.push(`Streaming: ${query.streaming ? 'Yes' : 'No'}`); - } return ( <> @@ -50,11 +38,6 @@ export const TempoQueryBuilderOptions = React.memo(({ onChange, query }) value={query.limit} /> - {!config.featureToggles.disableTraceQLStreaming && ( - - - - )} From 34ee3b09de84987abefacffc7649a210b19a36b4 Mon Sep 17 00:00:00 2001 From: Gareth Dawson Date: Wed, 26 Jul 2023 14:50:52 +0100 Subject: [PATCH 29/64] Loki / Prometheus: Fix query builder select component in safari (#71966) * partial fix * remove unused import * fix that doesnt remove error message --- .../prometheus/querybuilder/shared/OperationEditor.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx index d17cebd4105..bd552920d0c 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx @@ -254,8 +254,6 @@ const getStyles = (theme: GrafanaTheme2, isConflicting: boolean) => { card: css({ background: theme.colors.background.primary, border: `1px solid ${theme.colors.border.medium}`, - display: 'flex', - flexDirection: 'column', cursor: 'grab', borderRadius: theme.shape.borderRadius(1), position: 'relative', From 0c2b2219bb7ad4496b0dac6614b2a0f8116771e0 Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Wed, 26 Jul 2023 08:59:25 -0500 Subject: [PATCH 30/64] CI: use the base64 key in the windows installer steps (#72372) use the base64 key in the windows installer steps --- .drone.yml | 4 ++-- scripts/drone/steps/lib.star | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.yml b/.drone.yml index 63e542b1e88..fa5a1828282 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3328,7 +3328,7 @@ steps: - publish-linux-packages-rpm environment: GCP_KEY: - from_secret: gcp_grafanauploads + from_secret: gcp_grafanauploads_base64 GRAFANA_COM_API_KEY: from_secret: grafana_api_key image: grafana/grafana-ci-deploy:1.3.3 @@ -4970,6 +4970,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: 3f94603ccb6df539771470e23415094c86d606d5fd823409cd685346de3742e0 +hmac: b3a378789d0a84f8eedd22567d2ce7609cc9c079bdd58ea8e71d0d9456c423e8 ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index 7fef29623d8..f24a5dc0bd8 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1222,7 +1222,7 @@ def publish_grafanacom_step(ver_mode): ], "environment": { "GRAFANA_COM_API_KEY": from_secret("grafana_api_key"), - "GCP_KEY": from_secret(gcp_grafanauploads), + "GCP_KEY": from_secret(gcp_grafanauploads_base64), }, "commands": [ cmd, From 51b199e9866d7daae5102026b393d9022e6eed53 Mon Sep 17 00:00:00 2001 From: Adam Simpson Date: Wed, 26 Jul 2023 10:04:18 -0400 Subject: [PATCH 31/64] AzureMonitor: revert Variable Editor region changes (#72306) * Revert "remove regions/locations from variable editor" This reverts commit 41dc6a8bfb5dd90d56c108f7d76d20d79011d51a. * Revert "remove region pieces from e2e" This reverts commit 6b1f82f14a2f4f7338f5ad4b84a89c0e375e7348. * e2e: open resource picker correctly --- e2e/cloud-plugins-suite/azure-monitor.spec.ts | 11 +++++ .../VariableEditor/VariableEditor.test.tsx | 18 +++++++++ .../VariableEditor/VariableEditor.tsx | 40 +++++++++++++++++++ .../datasource/azuremonitor/variables.ts | 11 +++++ 4 files changed, 80 insertions(+) diff --git a/e2e/cloud-plugins-suite/azure-monitor.spec.ts b/e2e/cloud-plugins-suite/azure-monitor.spec.ts index 43528131776..c0f31cfe758 100644 --- a/e2e/cloud-plugins-suite/azure-monitor.spec.ts +++ b/e2e/cloud-plugins-suite/azure-monitor.spec.ts @@ -126,6 +126,10 @@ const addAzureMonitorVariable = ( .input() .find('input') .type(`${options?.namespace}{enter}`); + e2eSelectors.variableEditor.region + .input() + .find('input') + .type(`${options?.region}{enter}`); break; case AzureQueryType.MetricNamesQuery: e2eSelectors.variableEditor.subscription @@ -302,10 +306,14 @@ e2e.scenario({ subscription: '$subscription', resourceGroup: '$resourceGroups', }); + addAzureMonitorVariable('region', AzureQueryType.LocationsQuery, false, { + subscription: '$subscription', + }); addAzureMonitorVariable('resource', AzureQueryType.ResourceNamesQuery, false, { subscription: '$subscription', resourceGroup: '$resourceGroups', namespace: '$namespace', + region: '$region', }); e2e.pages.Dashboard.SubMenu.submenuItemLabels('subscription').click(); e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('grafanalabs-datasources-dev').click(); @@ -319,6 +327,8 @@ e2e.scenario({ .parent() .find('input') .type('microsoft.storage/storageaccounts{downArrow}{enter}'); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('button').click(); + e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('input').type('uk south{downArrow}{enter}'); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource').parent().find('button').click(); e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource') .parent() @@ -333,6 +343,7 @@ e2e.scenario({ e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription'); e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups'); e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces'); + e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region'); e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource'); e2eSelectors.queryEditor.resourcePicker.apply.button().click(); e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}'); diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx index 0547882c7d2..de3b4cfd9db 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.test.tsx @@ -258,10 +258,12 @@ describe('VariableEditor:', () => { await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); await selectAndRerender('select query type', 'Resource Names', onChange, rerender); await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + await selectAndRerender('select region', 'North Europe', onChange, rerender); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ queryType: AzureQueryType.ResourceNamesQuery, subscription: 'sub', + region: 'northeurope', refId: 'A', }) ); @@ -323,5 +325,21 @@ describe('VariableEditor:', () => { }) ); }); + + it('should run the query if requesting regions', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + // wait for initial load + await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument()); + await selectAndRerender('select query type', 'Regions', onChange, rerender); + await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender); + expect(onChange).toHaveBeenCalledWith( + expect.objectContaining({ + queryType: AzureQueryType.LocationsQuery, + subscription: 'sub', + refId: 'A', + }) + ); + }); }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx index f9dce486d07..321cc7a887f 100644 --- a/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/VariableEditor/VariableEditor.tsx @@ -30,6 +30,7 @@ const VariableEditor = (props: Props) => { { label: 'Subscriptions', value: AzureQueryType.SubscriptionsQuery }, { label: 'Resource Groups', value: AzureQueryType.ResourceGroupsQuery }, { label: 'Namespaces', value: AzureQueryType.NamespacesQuery }, + { label: 'Regions', value: AzureQueryType.LocationsQuery }, { label: 'Resource Names', value: AzureQueryType.ResourceNamesQuery }, { label: 'Metric Names', value: AzureQueryType.MetricNamesQuery }, { label: 'Workspaces', value: AzureQueryType.WorkspacesQuery }, @@ -50,6 +51,7 @@ const VariableEditor = (props: Props) => { const [requireSubscription, setRequireSubscription] = useState(false); const [hasResourceGroup, setHasResourceGroup] = useState(false); const [hasNamespace, setHasNamespace] = useState(false); + const [hasRegion, setHasRegion] = useState(false); const [requireResourceGroup, setRequireResourceGroup] = useState(false); const [requireNamespace, setRequireNamespace] = useState(false); const [requireResource, setRequireResource] = useState(false); @@ -57,6 +59,7 @@ const VariableEditor = (props: Props) => { const [resourceGroups, setResourceGroups] = useState([]); const [namespaces, setNamespaces] = useState([]); const [resources, setResources] = useState([]); + const [regions, setRegions] = useState([]); const [errorMessage, setError] = useLastError(); const queryType = typeof query === 'string' ? '' : query.queryType; @@ -88,6 +91,7 @@ const VariableEditor = (props: Props) => { setRequireSubscription(true); setHasResourceGroup(true); setHasNamespace(true); + setHasRegion(true); break; case AzureQueryType.MetricNamesQuery: setRequireSubscription(true); @@ -95,6 +99,9 @@ const VariableEditor = (props: Props) => { setRequireNamespace(true); setRequireResource(true); break; + case AzureQueryType.LocationsQuery: + setRequireSubscription(true); + break; } }, [queryType]); @@ -135,6 +142,16 @@ const VariableEditor = (props: Props) => { } }, [datasource, subscription, resourceGroup]); + useEffect(() => { + if (subscription) { + datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => { + const regions: SelectableValue[] = []; + rgs.forEach((r) => regions.push({ label: r.displayName, value: r.name })); + setRegions(regions); + }); + } + }, [datasource, subscription, resourceGroup]); + const namespace = (typeof query === 'object' && query.namespace) || ''; useEffect(() => { if (subscription) { @@ -191,6 +208,13 @@ const VariableEditor = (props: Props) => { }); }; + const onChangeRegion = (selectableValue: SelectableValue) => { + onChange({ + ...query, + region: selectableValue.value, + }); + }; + const onChangeResource = (selectableValue: SelectableValue) => { onChange({ ...query, @@ -296,6 +320,22 @@ const VariableEditor = (props: Props) => { /> )} + {hasRegion && ( + + diff --git a/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx b/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx index daa1fb3cf31..18154fdbebc 100644 --- a/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/AzureCredentialsForm.tsx @@ -1,6 +1,8 @@ +import { cx } from '@emotion/css'; import React, { ChangeEvent, useEffect, useReducer, useState } from 'react'; import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { InlineFormLabel, Button } from '@grafana/ui/src/components'; import { Input } from '@grafana/ui/src/components/Forms/Legacy/Input/Input'; import { Select } from '@grafana/ui/src/components/Forms/Legacy/Select/Select'; @@ -148,6 +150,7 @@ export const AzureCredentialsForm = (props: Props) => { onCredentialsChange(updated); } }; + const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth; return (
@@ -190,7 +193,7 @@ export const AzureCredentialsForm = (props: Props) => { Directory (tenant) ID
{ Application (client) ID
{ Client Secret - +
{!disabled && (
-
+
@@ -237,7 +249,7 @@ export const AzureCredentialsForm = (props: Props) => { Client Secret
{
Default Subscription -
+
updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })} + /> + {validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)} - } - interactive={true} - disabled={options.readOnly} - > - <> - updateValidDuration({ ...validDuration, timeInterval: e.currentTarget.value })} - /> - {validateInput(validDuration.timeInterval, DURATION_REGEX, durationError)} - - + +
+
+ {/* Query Timeout */} +
+
+ Set the Prometheus query timeout. {docsTip()}} + interactive={true} + disabled={options.readOnly} + > + <> + updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })} + /> + {validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)} + + +
- {/* Query Timeout */} -
+ + + +
Set the Prometheus query timeout. {docsTip()}} - interactive={true} - disabled={options.readOnly} - > - <> - updateValidDuration({ ...validDuration, queryTimeout: e.currentTarget.value })} - /> - {validateInput(validDuration.queryTimeout, DURATION_REGEX, durationError)} - - -
-
-
- -

Query editor

-
-
- Set default editor option for all users of this data source. {docsTip()}} - interactive={true} - disabled={options.readOnly} - > - o.value === options.jsonData.prometheusType)} - onChange={onChangeHandler( - 'prometheusType', - { - ...options, - jsonData: { ...options.jsonData, prometheusVersion: undefined }, - }, - (options) => { - // Check buildinfo api and set default version if we can - setPrometheusVersion(options, onOptionsChange, onUpdate); - return onOptionsChange({ - ...options, - jsonData: { ...options.jsonData, prometheusVersion: undefined }, - }); - } - )} + aria-label={`Default Editor (Code or Builder)`} + options={editorOptions} + value={ + editorOptions.find((o) => o.value === options.jsonData.defaultEditor) ?? + editorOptions.find((o) => o.value === QueryEditorMode.Builder) + } + onChange={onChangeHandler('defaultEditor', options, onOptionsChange)} width={40} />
+
+ + Checking this option will disable the metrics chooser and metric/label support in the query + field's autocomplete. This helps if you have performance issues with bigger Prometheus instances.{' '} + {docsTip()} + + } + interactive={true} + disabled={options.readOnly} + className={styles.switchField} + > + + +
-
- {options.jsonData.prometheusType && ( + + + + {!options.jsonData.prometheusType && !options.jsonData.prometheusVersion && options.readOnly && ( +
+ For more information on configuring prometheus type and version in data sources, see the{' '} + + provisioning documentation + + . +
+ )} +
+
- Use this to set the version of your {options.jsonData.prometheusType} instance if it is not - automatically configured. {docsTip()} + Set this to the type of your prometheus database, e.g. Prometheus, Cortex, Mimir or Thanos. Changing + this field will save your current settings, and attempt to detect the version. Certain types of + Prometheus support or do not support various APIs. For example, some types support regex matching + for label queries to improve performance. Some types have an API for metadata. If you set this + incorrectly you may experience odd behavior when querying metrics and labels. Please check your + Prometheus documentation to ensure you enter the correct type. {docsTip()} } interactive={true} disabled={options.readOnly} > o.value === options.jsonData.prometheusVersion + )} + onChange={onChangeHandler('prometheusVersion', options, onOptionsChange)} + width={40} + /> + +
+ )} +
+ {config.featureToggles.prometheusResourceBrowserCache && ( +
+
+ + Sets the browser caching level for editor queries. Higher cache settings are recommended for high + cardinality data sources. + + } + interactive={true} + disabled={options.readOnly} + > + o.value === options.jsonData.cacheLevel) ?? PrometheusCacheLevel.Low - } + <> + + updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value }) + } + className="width-20" + value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow} + onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)} + spellCheck={false} + /> + {validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)} + + + )} +
+ +
+
+ This feature will disable recording rules Turn this on to improve dashboard performance} + interactive={true} + className={styles.switchField} + disabled={options.readOnly} + > +
- )} - -
-
- - This feature will change the default behavior of relative queries to always request fresh data from - the prometheus instance, instead query results will be cached, and only new records are requested. - Turn this on to decrease database and network load. - - } - interactive={true} - className={styles.switchField} - disabled={options.readOnly} - > - - -
+ -
- {options.jsonData.incrementalQuerying && ( - - Set a duration like 10m or 120s or 0s. Default of 10 minutes. This duration will be added to the - duration of each incremental request. - - } - interactive={true} - disabled={options.readOnly} - > - <> + +
+
+
+ + Add custom parameters to the Prometheus query URL. For example timeout, partial_response, dedup, or + max_source_resolution. Multiple parameters should be concatenated together with an ‘&’. {docsTip()} + + } + interactive={true} + disabled={options.readOnly} + > - updateValidDuration({ ...validDuration, incrementalQueryOverlapWindow: e.currentTarget.value }) - } - className="width-25" - value={options.jsonData.incrementalQueryOverlapWindow ?? defaultPrometheusQueryOverlapWindow} - onChange={onChangeHandler('incrementalQueryOverlapWindow', options, onOptionsChange)} + className="width-20" + value={options.jsonData.customQueryParameters} + onChange={onChangeHandler('customQueryParameters', options, onOptionsChange)} spellCheck={false} + placeholder="Example: max_source_resolution=5m&timeout=10" /> - {validateInput(validDuration.incrementalQueryOverlapWindow, MULTIPLE_DURATION_REGEX, durationError)} - - - )} + +
+
+
+ {/* HTTP Method */} +
+ + You can use either POST or GET HTTP method to query your Prometheus data source. POST is the + recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version + older than 2.1 or if POST requests are restricted in your network. {docsTip()} + + } + interactive={true} + label="HTTP method" + disabled={options.readOnly} + > + - -
-
-
- {/* HTTP Method */} -
- - You can use either POST or GET HTTP method to query your Prometheus data source. POST is the - recommended method as it allows bigger queries. Change this to GET if you have a Prometheus version - older than 2.1 or if POST requests are restricted in your network. {docsTip()} - - } - interactive={true} - label="HTTP method" - disabled={options.readOnly} - > - + + + + ); +}; diff --git a/public/app/plugins/datasource/prometheus/configuration/overhaul/ConnectionSettings.tsx b/public/app/plugins/datasource/prometheus/configuration/overhaul/ConnectionSettings.tsx new file mode 100644 index 00000000000..8167b97ae3f --- /dev/null +++ b/public/app/plugins/datasource/prometheus/configuration/overhaul/ConnectionSettings.tsx @@ -0,0 +1,90 @@ +import { css, cx } from '@emotion/css'; +import React, { ReactNode } from 'react'; + +import { DataSourceJsonData, DataSourceSettings } from '@grafana/data'; +import { ConfigSection } from '@grafana/experimental'; +import { InlineField, Input, PopoverContent } from '@grafana/ui'; + +import { PromOptions } from '../../types'; +// THIS FILE IS COPIED FROM GRAFANA/EXPERIMENTAL +// BECAUSE IT CONTAINS TYPES THAT ARE REQUIRED IN THE ADVANCEDHTTPSETTINGS COMPONENT +// THE TYPES ARE WRITTEN IN EXPERIMENTAL WHERE THEY ARE NOT AS STRICT +// @ts-ignore +export type Config = DataSourceSettings< + // @ts-ignore + JSONData, + // @ts-ignore + SecureJSONData +>; +// @ts-ignore +export type OnChangeHandler = (options: DataSourceSettings) => void; +// @ts-ignore +export type Props = { + config: C; + onChange: OnChangeHandler; + description?: ReactNode; + urlPlaceholder?: string; + urlTooltip?: PopoverContent; + urlLabel?: string; + className?: string; +}; +// @ts-ignore +export const ConnectionSettings: (props: Props) => JSX.Element = ({ + config, + onChange, + description, + urlPlaceholder, + urlTooltip, + urlLabel, + className, +}) => { + const isValidUrl = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/.test( + config.url + ); + + const styles = { + container: css({ + maxWidth: 578, + }), + }; + + return ( + <> + + + Specify a complete HTTP URL +
+ (for example https://example.com:8080) + + ) + } + grow + disabled={config.readOnly} + required + invalid={!isValidUrl && !config.readOnly} + error={isValidUrl ? '' : 'Please enter a valid URL'} + interactive + > + + onChange({ + ...config, + url: event.currentTarget.value, + }) + } + value={config.url || ''} + placeholder={urlPlaceholder || 'URL'} + /> +
+
+ + ); +}; diff --git a/public/app/plugins/datasource/prometheus/configuration/overhaul/types.ts b/public/app/plugins/datasource/prometheus/configuration/overhaul/types.ts new file mode 100644 index 00000000000..5edb4da623c --- /dev/null +++ b/public/app/plugins/datasource/prometheus/configuration/overhaul/types.ts @@ -0,0 +1,11 @@ +import { ReactElement } from 'react'; + +// these are not available yet in grafana +export type CustomMethodId = `custom-${string}`; + +export type CustomMethod = { + id: CustomMethodId; + label: string; + description: string; + component: ReactElement; +}; diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index df895ce838f..f74d9a6a7bf 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -45,6 +45,7 @@ export interface PromOptions extends DataSourceJsonData { incrementalQuerying?: boolean; incrementalQueryOverlapWindow?: string; disableRecordingRules?: boolean; + sigV4Auth?: boolean; } export type ExemplarTraceIdDestination = { From 8415dd40d7d22783251677989ae7647ca1a1bfc1 Mon Sep 17 00:00:00 2001 From: Ivan Ortega Alba Date: Wed, 26 Jul 2023 18:40:51 +0200 Subject: [PATCH 41/64] DSPicker: Use new DS picker everywhere in Grafana (#70609) --- .betterer.results | 3 --- .../new-query-variable.spec.ts | 11 ++++----- .../src/selectors/components.ts | 2 +- .../src/components/DataSourcePicker.tsx | 3 ++- .../TraceToLogs/TraceToLogsSettings.tsx | 2 +- .../TraceToMetrics/TraceToMetricsSettings.tsx | 5 ++-- .../rule-editor/CloudRulesSourcePicker.tsx | 12 ++-------- .../Forms/ConfigureCorrelationSourceForm.tsx | 2 +- .../Forms/ConfigureCorrelationTargetForm.tsx | 2 +- .../AnnotationSettingsEdit.tsx | 3 ++- .../picker/DataSourceDropdown.test.tsx | 23 +++++++++++++++---- .../components/picker/DataSourceDropdown.tsx | 1 + .../components/ImportDashboardForm.tsx | 2 +- .../components/QueryEditorRowHeader.test.tsx | 2 +- .../adhoc/AdHocVariableEditor.test.tsx | 6 +++-- .../variables/adhoc/AdHocVariableEditor.tsx | 2 +- .../variables/query/QueryVariableEditor.tsx | 3 ++- .../cloudwatch/components/XrayLinkConfig.tsx | 6 ++--- .../elasticsearch/configuration/DataLink.tsx | 6 ++--- .../loki/configuration/DerivedField.test.tsx | 4 ++-- .../loki/configuration/DerivedField.tsx | 6 ++--- .../configuration/ExemplarSetting.tsx | 5 ++-- .../configuration/LokiSearchSettings.tsx | 10 +++++--- .../configuration/ServiceGraphSettings.tsx | 10 +++++--- public/app/plugins/panel/alertlist/module.tsx | 7 +++--- 25 files changed, 78 insertions(+), 60 deletions(-) diff --git a/.betterer.results b/.betterer.results index 7e0ee1091bd..1b66f1cfdac 100644 --- a/.betterer.results +++ b/.betterer.results @@ -792,9 +792,6 @@ exports[`better eslint`] = { "packages/grafana-runtime/src/analytics/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-runtime/src/components/DataSourcePicker.tsx:5381": [ - [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] - ], "packages/grafana-runtime/src/components/PanelRenderer.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index 2bae25ad442..3a1a4712d78 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -38,10 +38,9 @@ describe('Variables - Query - Add variable', () => { e2e().get('label').contains('Show on dashboard').should('be.visible'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect() - .should('be.visible') - .within((select) => { - e2e.components.Select.singleValue().should('have.text', 'gdev-testdata'); - }); + .get('input[placeholder="gdev-testdata"]') + .scrollIntoView() + .should('be.visible'); e2e().get('label').contains('Refresh').scrollIntoView().should('be.visible'); e2e().get('label').contains('On dashboard load').scrollIntoView().should('be.visible'); @@ -89,7 +88,7 @@ describe('Variables - Query - Add variable', () => { e2e().get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description'); - e2e.components.DataSourcePicker.inputV2().should('be.visible').type('gdev-testdata{enter}'); + e2e.components.DataSourcePicker.container().should('be.visible').type('gdev-testdata{enter}'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput() .should('be.visible') @@ -137,7 +136,7 @@ describe('Variables - Query - Add variable', () => { e2e().get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description'); - e2e.components.DataSourcePicker.inputV2().type('gdev-testdata{enter}'); + e2e.components.DataSourcePicker.container().type('gdev-testdata{enter}'); e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput() .should('be.visible') diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 572988f638a..3df323cd340 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -307,7 +307,7 @@ export const Components = { * @deprecated use inputV2 instead */ input: () => 'input[id="data-source-picker"]', - inputV2: 'Select a data source', + inputV2: 'data-testid Select a data source', }, TimeZonePicker: { /** diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 8d527350366..b53e83f124d 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -188,7 +188,8 @@ export class DataSourcePicker extends PureComponent + onChange={(ds: DataSourceInstanceSettings) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', { ...options.jsonData.tracesToMetrics, datasourceUid: ds.uid, diff --git a/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx b/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx index 0b255db5692..cd2c21eb57b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/CloudRulesSourcePicker.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { useAsync } from 'react-use'; import { DataSourceInstanceSettings } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { dispatch } from 'app/store/store'; import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler'; @@ -28,14 +28,6 @@ export function CloudRulesSourcePicker({ value, ...props }: Props): JSX.Element ); return ( - + ); } diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx index f692b547d02..5e3aed350a8 100644 --- a/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx +++ b/public/app/features/correlations/Forms/ConfigureCorrelationSourceForm.tsx @@ -3,8 +3,8 @@ import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; import { Card, Field, FieldSet, Input, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getVariableUsageInfo } from '../../explore/utils/links'; diff --git a/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx b/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx index 2643e7cfb49..9b66bea2a4b 100644 --- a/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx +++ b/public/app/features/correlations/Forms/ConfigureCorrelationTargetForm.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { Controller, useFormContext, useWatch } from 'react-hook-form'; import { DataSourceInstanceSettings } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; import { Field, FieldSet } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { QueryEditorField } from './QueryEditorField'; import { useCorrelationsFormContext } from './correlationsFormContext'; diff --git a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx index 2cf14413013..1e1daac9c33 100644 --- a/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx +++ b/public/app/features/dashboard/components/AnnotationSettings/AnnotationSettingsEdit.tsx @@ -11,7 +11,7 @@ import { } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { Stack } from '@grafana/experimental'; -import { DataSourcePicker, getDataSourceSrv, locationService } from '@grafana/runtime'; +import { getDataSourceSrv, locationService } from '@grafana/runtime'; import { AnnotationPanelFilter } from '@grafana/schema/src/raw/dashboard/x/dashboard_types.gen'; import { Button, @@ -27,6 +27,7 @@ import { import { ColorValueEditor } from 'app/core/components/OptionsUI/color'; import config from 'app/core/config'; import StandardAnnotationQueryEditor from 'app/features/annotations/components/StandardAnnotationQueryEditor'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DashboardModel } from '../../state/DashboardModel'; diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx index aac37c4968c..5b9ba51f778 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx +++ b/public/app/features/datasources/components/picker/DataSourceDropdown.test.tsx @@ -4,6 +4,7 @@ import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'; import React from 'react'; import { DataSourceInstanceSettings, DataSourcePluginMeta, PluginMetaInfo, PluginType } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { ModalRoot, ModalsProvider } from '@grafana/ui'; import config from 'app/core/config'; import { defaultFileUploadQuery } from 'app/plugins/datasource/grafana/types'; @@ -145,7 +146,10 @@ describe('DataSourceDropdown', () => { it('should display the current selected DS in the selector', async () => { getInstanceSettingsMock.mockReturnValue(mockDS2); render(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', mockDS2.name); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'placeholder', + mockDS2.name + ); expect(screen.getByAltText(`${mockDS2.meta.name} logo`)).toBeVisible(); }); @@ -166,7 +170,10 @@ describe('DataSourceDropdown', () => { it('should display the default DS as selected when `current` is not set', async () => { getInstanceSettingsMock.mockReturnValue(mockDS2); render(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', mockDS2.name); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'placeholder', + mockDS2.name + ); expect(screen.getByAltText(`${mockDS2.meta.name} logo`)).toBeVisible(); }); @@ -180,12 +187,15 @@ describe('DataSourceDropdown', () => { it('should disable the dropdown when `disabled` is true', () => { render(); - expect(screen.getByTestId('Select a data source')).toBeDisabled(); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toBeDisabled(); }); it('should assign the correct `id` to the input element to pair it with a label', () => { render(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('id', 'custom.input.id'); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'id', + 'custom.input.id' + ); }); it('should not set the default DS when setting `noDefault` to true and `current` is not provided', () => { @@ -195,7 +205,10 @@ describe('DataSourceDropdown', () => { // Doesn't try to get the default DS expect(getListMock).not.toBeCalled(); expect(getInstanceSettingsMock).not.toBeCalled(); - expect(screen.getByTestId('Select a data source')).toHaveAttribute('placeholder', 'Select data source'); + expect(screen.getByTestId(selectors.components.DataSourcePicker.inputV2)).toHaveAttribute( + 'placeholder', + 'Select data source' + ); }); }); diff --git a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx index 7d0e99d3caa..39fa1071752 100644 --- a/public/app/features/datasources/components/picker/DataSourceDropdown.tsx +++ b/public/app/features/datasources/components/picker/DataSourceDropdown.tsx @@ -197,6 +197,7 @@ export function DataSourceDropdown(props: DataSourceDropdownProps) { id={inputId || 'data-source-picker'} className={inputHasFocus ? undefined : styles.input} data-testid={selectors.components.DataSourcePicker.inputV2} + aria-label="Select a data source" prefix={currentValue ? prefixIcon : undefined} suffix={} placeholder={hideTextValue ? '' : dataSourceLabel(currentValue) || placeholder} diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index 1e735327472..61a1e185d36 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { selectors } from '@grafana/e2e-selectors'; -import { DataSourcePicker } from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { Button, @@ -15,6 +14,7 @@ import { Legend, } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DashboardInput, diff --git a/public/app/features/query/components/QueryEditorRowHeader.test.tsx b/public/app/features/query/components/QueryEditorRowHeader.test.tsx index c94fb627b03..a3fdae682bf 100644 --- a/public/app/features/query/components/QueryEditorRowHeader.test.tsx +++ b/public/app/features/query/components/QueryEditorRowHeader.test.tsx @@ -83,7 +83,7 @@ describe('QueryEditorRowHeader', () => { it('should render variables in the data source picker', async () => { renderScenario({ onChangeDataSource: () => {} }); - const dsSelect = screen.getByLabelText(selectors.components.DataSourcePicker.inputV2); + const dsSelect = screen.getByTestId(selectors.components.DataSourcePicker.container).querySelector('input')!; openMenu(dsSelect); expect(await screen.findByText('${dsVariable}')).toBeInTheDocument(); }); diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx index 66caf9b4cbc..cc4ae6e0bfb 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.test.tsx @@ -60,12 +60,14 @@ describe('AdHocVariableEditor', () => { it('has a datasource select menu', async () => { render(); - expect(await screen.findByLabelText(selectors.components.DataSourcePicker.inputV2)).toBeInTheDocument(); + expect(await screen.getByTestId(selectors.components.DataSourcePicker.container)).toBeInTheDocument(); }); it('calls the callback when changing the datasource', async () => { render(); - const selectEl = screen.getByLabelText(selectors.components.DataSourcePicker.inputV2); + const selectEl = screen + .getByTestId(selectors.components.DataSourcePicker.container) + .getElementsByTagName('input')[0]; await selectOptionInTest(selectEl, 'Loki'); expect(props.changeVariableDatasource).toBeCalledWith( diff --git a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx index c5f02f0d712..659fd7869c5 100644 --- a/public/app/features/variables/adhoc/AdHocVariableEditor.tsx +++ b/public/app/features/variables/adhoc/AdHocVariableEditor.tsx @@ -2,8 +2,8 @@ import React, { PureComponent } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; import { Alert, Field } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { StoreState } from 'app/types'; import { VariableLegend } from '../editor/VariableLegend'; diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index 50bd589a9e5..1077ceb1eb8 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -3,8 +3,9 @@ import { connect, ConnectedProps } from 'react-redux'; import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { DataSourcePicker, getTemplateSrv } from '@grafana/runtime'; +import { getTemplateSrv } from '@grafana/runtime'; import { Field } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { StoreState } from '../../../types'; import { getTimeSrv } from '../../dashboard/services/TimeSrv'; diff --git a/public/app/plugins/datasource/cloudwatch/components/XrayLinkConfig.tsx b/public/app/plugins/datasource/cloudwatch/components/XrayLinkConfig.tsx index 9efd3c75164..107a11e42d3 100644 --- a/public/app/plugins/datasource/cloudwatch/components/XrayLinkConfig.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/XrayLinkConfig.tsx @@ -1,9 +1,9 @@ import { css } from '@emotion/css'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { GrafanaTheme2, DataSourceInstanceSettings } from '@grafana/data'; import { Alert, InlineField, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; const getStyles = (theme: GrafanaTheme2) => ({ @@ -51,7 +51,7 @@ export function XrayLinkConfig({ datasourceUid, onChange }: Props) { > onChange(ds.uid)} + onChange={(ds: DataSourceInstanceSettings) => onChange(ds.uid)} current={datasourceUid} noDefault={true} /> diff --git a/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx b/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx index 74df666879d..dd4292cee11 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/css'; import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; -import { VariableSuggestion } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data'; import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DataLinkConfig } from '../types'; @@ -126,7 +126,7 @@ export const DataLink = (props: Props) => { { + onChange={(ds: DataSourceInstanceSettings) => { onChange({ ...value, datasourceUid: ds.uid, diff --git a/public/app/plugins/datasource/loki/configuration/DerivedField.test.tsx b/public/app/plugins/datasource/loki/configuration/DerivedField.test.tsx index 7b5b58fa119..c708e74bc10 100644 --- a/public/app/plugins/datasource/loki/configuration/DerivedField.test.tsx +++ b/public/app/plugins/datasource/loki/configuration/DerivedField.test.tsx @@ -67,7 +67,7 @@ describe('DerivedField', () => { ); expect(await screen.findByText('Name')).toBeInTheDocument(); - expect(screen.getByLabelText(selectors.components.DataSourcePicker.inputV2)).toBeInTheDocument(); + expect(screen.getByTestId(selectors.components.DataSourcePicker.container)).toBeInTheDocument(); }); it('shows url link if uid is not set', async () => { @@ -89,7 +89,7 @@ describe('DerivedField', () => { ); expect(await screen.findByText('Name')).toBeInTheDocument(); - expect(screen.queryByLabelText(selectors.components.DataSourcePicker.inputV2)).not.toBeInTheDocument(); + expect(await screen.queryByTestId(selectors.components.DataSourcePicker.container)).not.toBeInTheDocument(); }); it('shows only tracing datasources for internal link', async () => { diff --git a/public/app/plugins/datasource/loki/configuration/DerivedField.tsx b/public/app/plugins/datasource/loki/configuration/DerivedField.tsx index 356fa4a3223..b6cff64c3ca 100644 --- a/public/app/plugins/datasource/loki/configuration/DerivedField.tsx +++ b/public/app/plugins/datasource/loki/configuration/DerivedField.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/css'; import React, { ChangeEvent, useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; -import { GrafanaTheme2, VariableSuggestion } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { GrafanaTheme2, DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data'; import { Button, DataLinkInput, Field, Icon, Input, Label, Tooltip, useStyles2, Switch } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DerivedFieldConfig } from '../types'; @@ -145,7 +145,7 @@ export const DerivedField = (props: Props) => { + onChange={(ds: DataSourceInstanceSettings) => onChange({ ...value, datasourceUid: ds.uid, diff --git a/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx b/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx index ba13f98e692..f74752c420d 100644 --- a/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx +++ b/public/app/plugins/datasource/prometheus/configuration/ExemplarSetting.tsx @@ -1,8 +1,9 @@ import React, { useState } from 'react'; +import { DataSourceInstanceSettings } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { DataSourcePicker } from '@grafana/runtime'; import { Button, InlineField, Input, Switch, useTheme2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { ExemplarTraceIdDestination } from '../types'; @@ -58,7 +59,7 @@ export default function ExemplarSetting({ value, onChange, onDelete, disabled }: current={value.datasourceUid} noDefault={true} width={40} - onChange={(ds) => + onChange={(ds: DataSourceInstanceSettings) => onChange({ ...value, datasourceUid: ds.uid, diff --git a/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx b/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx index 8a5d9d87b53..7ae8e368ee2 100644 --- a/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx +++ b/public/app/plugins/datasource/tempo/configuration/LokiSearchSettings.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { + DataSourceInstanceSettings, + DataSourcePluginOptionsEditorProps, + updateDatasourcePluginJsonDataOption, +} from '@grafana/data'; import { Button, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { TempoJsonData } from '../types'; @@ -33,7 +37,7 @@ export function LokiSearchSettings({ options, onOptionsChange }: Props) { current={options.jsonData.lokiSearch?.datasourceUid} noDefault={true} width={40} - onChange={(ds) => + onChange={(ds: DataSourceInstanceSettings) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'lokiSearch', { datasourceUid: ds.uid, }) diff --git a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx index 46ecf18670c..646d004f888 100644 --- a/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx +++ b/public/app/plugins/datasource/tempo/configuration/ServiceGraphSettings.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data'; -import { DataSourcePicker } from '@grafana/runtime'; +import { + DataSourceInstanceSettings, + DataSourcePluginOptionsEditorProps, + updateDatasourcePluginJsonDataOption, +} from '@grafana/data'; import { Button, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { TempoJsonData } from '../types'; @@ -27,7 +31,7 @@ export function ServiceGraphSettings({ options, onOptionsChange }: Props) { current={options.jsonData.serviceMap?.datasourceUid} noDefault={true} width={40} - onChange={(ds) => + onChange={(ds: DataSourceInstanceSettings) => updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'serviceMap', { datasourceUid: ds.uid, }) diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 481d3ec14b0..33f3126f923 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { PanelPlugin } from '@grafana/data'; -import { config, DataSourcePicker } from '@grafana/runtime'; +import { DataSourceInstanceSettings, PanelPlugin } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { TagsInput } from '@grafana/ui'; import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { @@ -9,6 +9,7 @@ import { GENERAL_FOLDER, ReadonlyFolderPicker, } from 'app/core/components/Select/ReadonlyFolderPicker/ReadonlyFolderPicker'; +import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { PermissionLevelString } from 'app/types'; import { GRAFANA_DATASOURCE_NAME } from '../../../features/alerting/unified/utils/datasource'; @@ -259,7 +260,7 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi type={['prometheus', 'loki', 'grafana']} noDefault current={props.value} - onChange={(ds) => props.onChange(ds.name)} + onChange={(ds: DataSourceInstanceSettings) => props.onChange(ds.name)} onClear={() => props.onChange(null)} /> ); From c7598cc6fb492cb7cc34eec90fbd2a08d859cffd Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 26 Jul 2023 12:44:12 -0400 Subject: [PATCH 42/64] Alerting: Add ability to control scheduler tick interval via config (#71980) * add ability to control scheduler interval via config * add feature flag `configurableSchedulerTick` --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 9 +++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 +++ pkg/services/ngalert/schedule/schedule.go | 1 + pkg/setting/setting_unified_alerting.go | 21 ++++++++++++ pkg/setting/setting_unified_alerting_test.go | 33 +++++++++++++++++++ 7 files changed, 70 insertions(+) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index f6663c3c0c8..f28214c2e4c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -117,4 +117,5 @@ export interface FeatureToggles { splitScopes?: boolean; azureMonitorDataplane?: boolean; prometheusConfigOverhaulAuth?: boolean; + configurableSchedulerTick?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 1eb1cfb8af6..1c113caf336 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -681,5 +681,14 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, }, + { + Name: "configurableSchedulerTick", + Description: "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", + Stage: FeatureStageExperimental, + FrontendOnly: false, + Owner: grafanaAlertingSquad, + RequiresRestart: true, + HideFromDocs: true, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c8423b7e8df..6e380fb330b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -98,3 +98,4 @@ awsAsyncQueryCaching,experimental,@grafana/aws-datasources,false,false,false,fal splitScopes,preview,@grafana/grafana-authnz-team,false,false,true,false azureMonitorDataplane,GA,@grafana/partner-datasources,false,false,false,false prometheusConfigOverhaulAuth,experimental,@grafana/observability-metrics,false,false,false,false +configurableSchedulerTick,experimental,@grafana/alerting-squad,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a4abf9854ba..d7137fdf903 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -402,4 +402,8 @@ const ( // FlagPrometheusConfigOverhaulAuth // Update the Prometheus configuration page with the new auth component FlagPrometheusConfigOverhaulAuth = "prometheusConfigOverhaulAuth" + + // FlagConfigurableSchedulerTick + // Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval + FlagConfigurableSchedulerTick = "configurableSchedulerTick" ) diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index fef8ac9966b..0fe3835e7d6 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -130,6 +130,7 @@ func NewScheduler(cfg SchedulerCfg, stateManager *state.Manager) *schedule { } func (sch *schedule) Run(ctx context.Context) error { + sch.log.Info("Starting scheduler", "tickInterval", sch.baseInterval) t := ticker.New(sch.clock, sch.baseInterval, sch.metrics.Ticker) defer t.Stop() diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index c594a76315a..65573f44856 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -287,6 +287,27 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { uaCfg.BaseInterval = SchedulerBaseInterval + // The base interval of the scheduler for evaluating alerts. + // 1. It is used by the internal scheduler's timer to tick at this interval. + // 2. to spread evaluations of rules that need to be evaluated at the current tick T. In other words, the evaluation of rules at the tick T will be evenly spread in the interval from T to T+scheduler_tick_interval. + // For example, if there are 100 rules that need to be evaluated at tick T, and the base interval is 10s, rules will be evaluated every 100ms. + // 3. It increases delay between rule updates and state reset. + // NOTE: + // 1. All alert rule intervals should be times of this interval. Otherwise, the rules will not be evaluated. It is not recommended to set it lower than 10s or odd numbers. Recommended: 10s, 30s, 1m + // 2. The increasing of the interval will affect how slow alert rule updates will reset the state, and therefore reset notification. Higher the interval - slower propagation of the changes. + baseInterval, err := gtime.ParseDuration(valueAsString(ua, "scheduler_tick_interval", SchedulerBaseInterval.String())) + if cfg.IsFeatureToggleEnabled("configurableSchedulerTick") { // use literal to avoid cycle imports + if err != nil { + return fmt.Errorf("failed to parse setting 'scheduler_tick_interval' as duration: %w", err) + } + if baseInterval != SchedulerBaseInterval { + cfg.Logger.Warn("Scheduler tick interval is changed to non-default", "interval", baseInterval, "default", SchedulerBaseInterval) + } + uaCfg.BaseInterval = baseInterval + } else if baseInterval != SchedulerBaseInterval { + cfg.Logger.Warn("Scheduler tick interval is changed to non-default but the feature flag is not enabled. Using default.", "interval", baseInterval, "default", SchedulerBaseInterval) + } + uaMinInterval, err := gtime.ParseDuration(valueAsString(ua, "min_interval", uaCfg.BaseInterval.String())) if err != nil || uaMinInterval == uaCfg.BaseInterval { // unified option is invalid duration or equals the default // if the legacy option is invalid, fallback to 10 (unified alerting min interval default) diff --git a/pkg/setting/setting_unified_alerting_test.go b/pkg/setting/setting_unified_alerting_test.go index 92fa07648ad..af9b73678d6 100644 --- a/pkg/setting/setting_unified_alerting_test.go +++ b/pkg/setting/setting_unified_alerting_test.go @@ -39,6 +39,39 @@ func TestCfg_ReadUnifiedAlertingSettings(t *testing.T) { require.Len(t, cfg.UnifiedAlerting.HAPeers, 3) require.ElementsMatch(t, []string{"hostname1:9090", "hostname2:9090", "hostname3:9090"}, cfg.UnifiedAlerting.HAPeers) } + + t.Run("should read 'scheduler_tick_interval'", func(t *testing.T) { + tmp := cfg.IsFeatureToggleEnabled + t.Cleanup(func() { + cfg.IsFeatureToggleEnabled = tmp + }) + cfg.IsFeatureToggleEnabled = func(key string) bool { return key == "configurableSchedulerTick" } + + s, err := cfg.Raw.NewSection("unified_alerting") + require.NoError(t, err) + _, err = s.NewKey("scheduler_tick_interval", "1m") + require.NoError(t, err) + _, err = s.NewKey("min_interval", "3m") + require.NoError(t, err) + + require.NoError(t, cfg.ReadUnifiedAlertingSettings(cfg.Raw)) + require.Equal(t, time.Minute, cfg.UnifiedAlerting.BaseInterval) + require.Equal(t, 3*time.Minute, cfg.UnifiedAlerting.MinInterval) + + t.Run("and fail if it is wrong", func(t *testing.T) { + _, err = s.NewKey("scheduler_tick_interval", "test") + require.NoError(t, err) + + require.Error(t, cfg.ReadUnifiedAlertingSettings(cfg.Raw)) + }) + + t.Run("and use default if not specified", func(t *testing.T) { + s.DeleteKey("scheduler_tick_interval") + require.NoError(t, cfg.ReadUnifiedAlertingSettings(cfg.Raw)) + + require.Equal(t, SchedulerBaseInterval, cfg.UnifiedAlerting.BaseInterval) + }) + }) } func TestUnifiedAlertingSettings(t *testing.T) { From 982624cf51eb8eb9841dc395862b39403e7cdf0c Mon Sep 17 00:00:00 2001 From: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Date: Wed, 26 Jul 2023 12:46:57 -0600 Subject: [PATCH 43/64] Explore: Turn ExplorePage.test into unit test (#72022) * Extract logic from ExplorePage to a hook, add a test for the hook; remove ExplorePage test * Remove extracted stuff from ExplorePage * Clean up * Fix minWidth logic --- public/app/features/explore/ExplorePage.tsx | 47 +++------------ .../hooks/useSplitSizeUpdater.test.tsx | 57 +++++++++++++++++++ .../explore/hooks/useSplitSizeUpdater.ts | 47 +++++++++++++++ 3 files changed, 112 insertions(+), 39 deletions(-) create mode 100644 public/app/features/explore/hooks/useSplitSizeUpdater.test.tsx create mode 100644 public/app/features/explore/hooks/useSplitSizeUpdater.ts diff --git a/public/app/features/explore/ExplorePage.tsx b/public/app/features/explore/ExplorePage.tsx index bf0b0920c31..d55a300b348 100644 --- a/public/app/features/explore/ExplorePage.tsx +++ b/public/app/features/explore/ExplorePage.tsx @@ -1,24 +1,24 @@ import { css } from '@emotion/css'; -import { inRange } from 'lodash'; -import React, { useEffect, useState } from 'react'; -import { useWindowSize } from 'react-use'; +import React, { useEffect } from 'react'; import { ErrorBoundaryAlert } from '@grafana/ui'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; import { useGrafana } from 'app/core/context/GrafanaContext'; import { useNavModel } from 'app/core/hooks/useNavModel'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { useDispatch, useSelector } from 'app/types'; +import { useSelector } from 'app/types'; import { ExploreQueryParams } from 'app/types/explore'; import { ExploreActions } from './ExploreActions'; import { ExplorePaneContainer } from './ExplorePaneContainer'; import { useExplorePageTitle } from './hooks/useExplorePageTitle'; +import { useSplitSizeUpdater } from './hooks/useSplitSizeUpdater'; import { useStateSync } from './hooks/useStateSync'; import { useTimeSrvFix } from './hooks/useTimeSrvFix'; -import { splitSizeUpdateAction } from './state/main'; import { isSplit, selectPanesEntries } from './state/selectors'; +const MIN_PANE_WIDTH = 200; + const styles = { pageScrollbarWrapper: css` width: 100%; @@ -38,13 +38,9 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor // if we were to update the URL on state change, the title would not match the URL. // Ultimately the URL is the single source of truth from which state is derived, the page title is not different useExplorePageTitle(props.queryParams); - const dispatch = useDispatch(); const { keybindings, chrome } = useGrafana(); const navModel = useNavModel('explore'); - const [rightPaneWidthRatio, setRightPaneWidthRatio] = useState(0.5); - const { width: windowWidth } = useWindowSize(); - const minWidth = 200; - const exploreState = useSelector((state) => state.explore); + const { updateSplitSize, widthCalc } = useSplitSizeUpdater(MIN_PANE_WIDTH); const panes = useSelector(selectPanesEntries); const hasSplit = useSelector(isSplit); @@ -59,33 +55,6 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor keybindings.setupTimeRangeBindings(false); }, [keybindings]); - const updateSplitSize = (size: number) => { - const evenSplitWidth = windowWidth / 2; - const areBothSimilar = inRange(size, evenSplitWidth - 100, evenSplitWidth + 100); - if (areBothSimilar) { - dispatch(splitSizeUpdateAction({ largerExploreId: undefined })); - } else { - dispatch( - splitSizeUpdateAction({ - largerExploreId: size > evenSplitWidth ? panes[1][0] : panes[0][0], - }) - ); - } - - setRightPaneWidthRatio(size / windowWidth); - }; - - let widthCalc = 0; - if (hasSplit) { - if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) { - widthCalc = exploreState.maxedExploreId === panes[1][0] ? windowWidth - minWidth : minWidth; - } else if (exploreState.evenSplitPanes) { - widthCalc = Math.floor(windowWidth / 2); - } else if (rightPaneWidthRatio !== undefined) { - widthCalc = windowWidth * rightPaneWidthRatio; - } - } - return (
@@ -93,8 +62,8 @@ export default function ExplorePage(props: GrafanaRouteComponentProps<{}, Explor { + it('dispatches correct action and calculates widthCalc correctly', () => { + const store = configureStore({ + explore: { + ...initialExploreState, + panes: { + left: makeExplorePaneState(), + right: makeExplorePaneState(), + }, + }, + }); + + const minWidth = 200; + + const dispatchMock = jest.fn().mockImplementation(store.dispatch); + + const { result } = renderHook(() => useSplitSizeUpdater(minWidth), { + wrapper: ({ children }: { children: ReactNode }) => ( + {children} + ), + }); + + // 1. Panes have similar width + act(() => { + result.current.updateSplitSize(450); + + expect(dispatchMock).toHaveBeenCalledWith(splitSizeUpdateAction({ largerExploreId: undefined })); + expect(result.current.widthCalc).toBe(512); + }); + + // 2. Left pane is larger + act(() => { + result.current.updateSplitSize(300); + }); + expect(dispatchMock).toHaveBeenCalledWith(splitSizeUpdateAction({ largerExploreId: 'left' })); + expect(result.current.widthCalc).toBe(300); + + // 3. Right pane is larger + act(() => { + result.current.updateSplitSize(700); + }); + expect(dispatchMock).toHaveBeenCalledWith(splitSizeUpdateAction({ largerExploreId: 'right' })); + expect(result.current.widthCalc).toBe(700); + }); +}); diff --git a/public/app/features/explore/hooks/useSplitSizeUpdater.ts b/public/app/features/explore/hooks/useSplitSizeUpdater.ts new file mode 100644 index 00000000000..ea2f6279b8a --- /dev/null +++ b/public/app/features/explore/hooks/useSplitSizeUpdater.ts @@ -0,0 +1,47 @@ +import { inRange } from 'lodash'; +import { useState } from 'react'; +import { useWindowSize } from 'react-use'; + +import { useDispatch, useSelector } from 'app/types'; + +import { splitSizeUpdateAction } from '../state/main'; +import { isSplit, selectPanesEntries } from '../state/selectors'; + +export const useSplitSizeUpdater = (minWidth: number) => { + const dispatch = useDispatch(); + const { width: windowWidth } = useWindowSize(); + const panes = useSelector(selectPanesEntries); + const hasSplit = useSelector(isSplit); + const [rightPaneWidthRatio, setRightPaneWidthRatio] = useState(0.5); + + const exploreState = useSelector((state) => state.explore); + + const updateSplitSize = (size: number) => { + const evenSplitWidth = windowWidth / 2; + const areBothSimilar = inRange(size, evenSplitWidth - 100, evenSplitWidth + 100); + if (areBothSimilar) { + dispatch(splitSizeUpdateAction({ largerExploreId: undefined })); + } else { + dispatch( + splitSizeUpdateAction({ + largerExploreId: size > evenSplitWidth ? panes[1][0] : panes[0][0], + }) + ); + } + + setRightPaneWidthRatio(size / windowWidth); + }; + + let widthCalc = 0; + if (hasSplit) { + if (!exploreState.evenSplitPanes && exploreState.maxedExploreId) { + widthCalc = exploreState.maxedExploreId === panes[1][0] ? windowWidth - minWidth : minWidth; + } else if (exploreState.evenSplitPanes) { + widthCalc = Math.floor(windowWidth / 2); + } else if (rightPaneWidthRatio !== undefined) { + widthCalc = windowWidth * rightPaneWidthRatio; + } + } + + return { updateSplitSize, widthCalc }; +}; From 59bed9e156fec8bf99702dff44817fbbb270e7a2 Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Wed, 26 Jul 2023 11:58:55 -0700 Subject: [PATCH 44/64] Geomap: Add network layer (#70192) * Geomap: Add network layer * Support text labels for nodes * Add solid styling for edges * Remove symbol option for edge style menu * Add support for edge text labels * Fix linter issues * Simplify multiple data frame handling * Add TODO notes * Add node and edge style categories for options * Remove data frame hardcoding * Hide legend, attempt to hide tooltip by default * Mark network layer as beta * refactor updateEdge * Fix some linter issues * Remove attempt at disabling tooltip for network layer * For edge text add a stroke and increase z index * Restrict field selection based on frame type * refactor * add basic bad data handling (prevent entire panel from breaking) * generate non hard coded graph frames for style editor filtering * code cleanup; remove hardcoded reference to "edges" frame * fix select clearing for Data option * fix styling * fix lookup --------- Co-authored-by: nmarrs Co-authored-by: Adela Almasan --- .betterer.results | 4 +- packages/grafana-data/src/geo/layer.ts | 6 +- .../MatchersUI/FieldsByFrameRefIdMatcher.tsx | 2 +- .../features/geo/utils/frameVectorSource.ts | 2 +- .../panel/geomap/editor/StyleEditor.tsx | 55 ++- .../panel/geomap/editor/layerEditor.tsx | 2 +- .../plugins/panel/geomap/layers/data/index.ts | 18 +- .../panel/geomap/layers/data/networkLayer.tsx | 364 ++++++++++++++++++ public/app/plugins/panel/nodeGraph/types.ts | 7 +- .../panel/nodeGraph/useCategorizeFrames.ts | 18 +- public/app/plugins/panel/nodeGraph/utils.ts | 17 +- 11 files changed, 444 insertions(+), 51 deletions(-) create mode 100644 public/app/plugins/panel/geomap/layers/data/networkLayer.tsx diff --git a/.betterer.results b/.betterer.results index 1b66f1cfdac..d186fb188f7 100644 --- a/.betterer.results +++ b/.betterer.results @@ -208,7 +208,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "7"] ], "packages/grafana-data/src/geo/layer.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "packages/grafana-data/src/panel/PanelPlugin.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 4ad67866fbb..c51750c3600 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -5,6 +5,7 @@ import { ReactNode } from 'react'; import { MapLayerOptions, FrameGeometrySourceMode } from '@grafana/schema'; import { EventBus } from '../events'; +import { StandardEditorContext } from '../field/standardFieldConfigEditorRegistry'; import { GrafanaTheme2 } from '../themes'; import { PanelData } from '../types'; import { PanelOptionsEditorBuilder } from '../utils'; @@ -39,7 +40,10 @@ export interface MapLayerHandler { /** * Show custom elements in the panel edit UI */ - registerOptionsUI?: (builder: PanelOptionsEditorBuilder>) => void; + registerOptionsUI?: ( + builder: PanelOptionsEditorBuilder>, + context: StandardEditorContext + ) => void; } /** diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx index 65617bfacb4..05cf8d84bbd 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.tsx @@ -56,7 +56,7 @@ export function RefIDPicker({ value, data, onChange, placeholder }: Props) { const onFilterChange = useCallback( (v: SelectableValue) => { - onChange(v.value!); + onChange(v?.value!); }, [onChange] ); diff --git a/public/app/features/geo/utils/frameVectorSource.ts b/public/app/features/geo/utils/frameVectorSource.ts index 8f85f420af5..25cf679a44c 100644 --- a/public/app/features/geo/utils/frameVectorSource.ts +++ b/public/app/features/geo/utils/frameVectorSource.ts @@ -9,7 +9,7 @@ import { getGeometryField, LocationFieldMatchers } from './location'; export interface FrameVectorSourceOptions {} export class FrameVectorSource extends VectorSource { - constructor(private location: LocationFieldMatchers) { + constructor(public location: LocationFieldMatchers) { super({}); } diff --git a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx index ee6e369ef79..56aba8718aa 100644 --- a/public/app/plugins/panel/geomap/editor/StyleEditor.tsx +++ b/public/app/plugins/panel/geomap/editor/StyleEditor.tsx @@ -1,9 +1,9 @@ import { capitalize } from 'lodash'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useObservable } from 'react-use'; import { Observable, of } from 'rxjs'; -import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data'; +import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem, FrameMatcher } from '@grafana/data'; import { ScaleDimensionConfig, ResourceDimensionConfig, @@ -39,11 +39,22 @@ export interface StyleEditorOptions { layerInfo?: Observable; simpleFixedValues?: boolean; displayRotation?: boolean; + hideSymbol?: boolean; + frameMatcher?: FrameMatcher; } type Props = StandardEditorProps; -export const StyleEditor = ({ value, context, onChange, item }: Props) => { +export const StyleEditor = (props: Props) => { + const { value, onChange, item } = props; + const context = useMemo(() => { + if (!item.settings?.frameMatcher) { + return props.context; + } + + return { ...props.context, data: props.context.data.filter(item.settings.frameMatcher) }; + }, [props.context, item.settings]); + const settings = item.settings; const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => { @@ -188,24 +199,26 @@ export const StyleEditor = ({ value, context, onChange, item }: Props) => { } /> - - - + {!settings?.hideSymbol && ( + + + + )} = { + type: NETWORK_LAYER_ID, + name: '', // will get replaced + config: defaultOptions, + location: { + mode: FrameGeometrySourceMode.Auto, + }, +}; + +/** + * Map layer configuration for network overlay + */ +export const networkLayer: MapLayerRegistryItem = { + id: NETWORK_LAYER_ID, + name: 'Network', + description: 'Render a node graph as a map layer', + isBaseMap: false, + showLocation: true, + hideOpacity: true, + state: PluginState.beta, + + /** + * Function that configures transformation and returns a transformer + * @param map + * @param options + * @param eventBus + * @param theme + */ + create: async (map: Map, options: MapLayerOptions, eventBus: EventBus, theme: GrafanaTheme2) => { + // Assert default values + const config = { + ...defaultOptions, + ...options?.config, + }; + + const style = await getStyleConfigState(config.style); + const edgeStyle = await getStyleConfigState(config.edgeStyle); + const location = await getLocationMatchers(options.location); + const source = new FrameVectorSource(location); + + const vectorLayer = new VectorLayer({ + source, + }); + const hasArrows = config.arrow === 1 || config.arrow === -1 || config.arrow === 2; + + // TODO update legend to display edges as well + const legendProps = new ReplaySubject(1); + let legend: ReactNode = null; + if (config.showLegend) { + legend = ; + } + + vectorLayer.setStyle((feature: FeatureLike) => { + const geom = feature.getGeometry(); + const idx = feature.get('rowIndex'); + const dims = style.dims; + + if (!style.fields && !edgeStyle.fields && !hasArrows && geom?.getType() !== 'LineString') { + // Set a global style + return style.maker(style.base); + } + + // For edges + if (geom?.getType() === 'LineString' && geom instanceof SimpleGeometry) { + const edgeDims = edgeStyle.dims; + const edgeTextConfig = edgeStyle.config.textConfig; + const edgeId = Number(feature.getId()); + const coordinates = geom.getCoordinates(); + const opacity = edgeStyle.config.opacity ?? 1; + if (coordinates && edgeDims) { + const segmentStartCoords = coordinates[0]; + const segmentEndCoords = coordinates[1]; + const color1 = tinycolor( + theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color) + ) + .setAlpha(opacity) + .toString(); + const color2 = tinycolor( + theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color) + ) + .setAlpha(opacity) + .toString(); + const arrowSize1 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size; + const arrowSize2 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size; + const styles = []; + + const flowStyle = new FlowLine({ + visible: true, + lineCap: config.arrow === 0 ? 'round' : 'square', + color: color1, + color2: color2, + width: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size, + width2: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size, + }); + + if (config.arrow) { + flowStyle.setArrow(config.arrow); + if (config.arrow > 0) { + flowStyle.setArrowColor(color2); + flowStyle.setArrowSize((arrowSize2 ?? 0) * 2); + } else { + flowStyle.setArrowColor(color1); + flowStyle.setArrowSize((arrowSize1 ?? 0) * 2); + } + } + const LS = new LineString([segmentStartCoords, segmentEndCoords]); + flowStyle.setGeometry(LS); + + const fontFamily = theme.typography.fontFamily; + if (edgeDims.text) { + const labelStyle = new Style({ + zIndex: 10, + text: new Text({ + text: edgeDims.text.get(edgeId), + font: `normal ${edgeTextConfig?.fontSize}px ${fontFamily}`, + fill: new Fill({ color: color1 ?? defaultStyleConfig.color.fixed }), + stroke: new Stroke({ + color: tinycolor(theme.visualization.getColorByName('text')).setAlpha(opacity).toString(), + width: Math.max(edgeTextConfig?.fontSize! / 10, 1), + }), + ...edgeTextConfig, + }), + }); + labelStyle.setGeometry(LS); + styles.push(labelStyle); + } + styles.push(flowStyle); + return styles; + } + } + if (!dims || !isNumber(idx)) { + return style.maker(style.base); + } + + const values = { ...style.base }; + + if (dims.color) { + values.color = dims.color.get(idx); + } + if (dims.size) { + values.size = dims.size.get(idx); + } + if (dims.text) { + values.text = dims.text.get(idx); + } + if (dims.rotation) { + values.rotation = dims.rotation.get(idx); + } + return style.maker(values); + }); + + return { + init: () => vectorLayer, + legend: legend, + update: (data: PanelData) => { + if (!data.series?.length) { + source.clear(); + return; // ignore empty + } + + // Post updates to the legend component + if (legend) { + legendProps.next({ + styleConfig: style, + size: style.dims?.size, + layerName: options.name, + layer: vectorLayer, + }); + } + const graphFrames = getGraphFrame(data.series); + + for (const frame of data.series) { + if (frame === graphFrames.edges[0]) { + edgeStyle.dims = getStyleDimension(frame, edgeStyle, theme); + } else { + style.dims = getStyleDimension(frame, style, theme); + } + + updateEdge(source, graphFrames); + } + }, + + // Marker overlay options + registerOptionsUI: (builder, context) => { + const networkFrames = getGraphFrame(context.data); + const frameNodes = networkFrames.nodes[0]; + const frameEdges = networkFrames.edges[0]; + + builder + .addCustomEditor({ + id: 'config.style', + category: ['Node Styles'], + path: 'config.style', + name: 'Node Styles', + editor: StyleEditor, + settings: { + displayRotation: true, + frameMatcher: (frame: DataFrame) => frame === frameNodes, + }, + defaultValue: defaultOptions.style, + }) + .addCustomEditor({ + id: 'config.edgeStyle', + category: ['Edge Styles'], + path: 'config.edgeStyle', + name: 'Edge Styles', + editor: StyleEditor, + settings: { + hideSymbol: true, + frameMatcher: (frame: DataFrame) => frame === frameEdges, + }, + defaultValue: defaultOptions.style, + }) + .addRadio({ + path: 'config.arrow', + name: 'Arrow', + settings: { + options: [ + { label: 'None', value: 0 }, + { label: 'Forward', value: 1 }, + { label: 'Reverse', value: -1 }, + { label: 'Both', value: 2 }, + ], + }, + defaultValue: defaultOptions.arrow, + }) + .addBooleanSwitch({ + path: 'config.showLegend', + name: 'Show legend', + description: 'Show map legend', + defaultValue: defaultOptions.showLegend, + }); + }, + }; + }, + + // fill in the default values + defaultOptions, +}; + +function updateEdge(source: FrameVectorSource, graphFrames: GraphFrame) { + source.clear(true); + + const frameNodes = graphFrames.nodes[0]; + const frameEdges = graphFrames.edges[0]; + + if (!frameNodes || !frameEdges) { + // TODO: provide helpful error message / link to docs for how to format data + return; + } + + const info = getGeometryField(frameNodes, source.location); + if (!info.field) { + source.changed(); + return; + } + + // TODO: Fix this + // eslint-disable-next-line + const field = info.field as unknown as Field; + + // TODO for nodes, don't hard code id field name + const nodeIdIndex = frameNodes.fields.findIndex((f: Field) => f.name === 'id'); + const nodeIdValues = frameNodes.fields[nodeIdIndex].values; + + // Edges + // TODO for edges, don't hard code source and target fields + const sourceIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'source'); + const targetIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'target'); + + const sources = frameEdges.fields[sourceIndex].values; + const targets = frameEdges.fields[targetIndex].values; + + // Loop through edges, referencing node locations + for (let i = 0; i < sources.length; i++) { + // Create linestring for each edge + const sourceId = sources[i]; + const targetId = targets[i]; + + const sourceNodeIndex = nodeIdValues.findIndex((value: string) => value === sourceId); + const targetNodeIndex = nodeIdValues.findIndex((value: string) => value === targetId); + + if (!field.values[sourceNodeIndex] || !field.values[targetNodeIndex]) { + continue; + } + + const geometryEdge: Geometry = new LineString([ + field.values[sourceNodeIndex].getCoordinates(), + field.values[targetNodeIndex].getCoordinates(), + ]); + + const edgeFeature = new Feature({ + geometry: geometryEdge, + }); + edgeFeature.setId(i); + source['addFeatureInternal'](edgeFeature); // @TODO revisit? + } + + // Nodes + for (let i = 0; i < frameNodes.length; i++) { + source['addFeatureInternal']( + new Feature({ + frameNodes, + rowIndex: i, + geometry: info.field.values[i], + }) + ); + } + + // only call source at the end + source.changed(); +} diff --git a/public/app/plugins/panel/nodeGraph/types.ts b/public/app/plugins/panel/nodeGraph/types.ts index 8e2a1636c86..848437d26cf 100644 --- a/public/app/plugins/panel/nodeGraph/types.ts +++ b/public/app/plugins/panel/nodeGraph/types.ts @@ -1,6 +1,6 @@ import { SimulationNodeDatum, SimulationLinkDatum } from 'd3-force'; -import { Field, IconName } from '@grafana/data'; +import { DataFrame, Field, IconName } from '@grafana/data'; export { Options as NodeGraphOptions, ArcOption } from './panelcfg.gen'; @@ -43,3 +43,8 @@ export type NodesMarker = { node: NodeDatum; count: number; }; + +export type GraphFrame = { + nodes: DataFrame[]; + edges: DataFrame[]; +}; diff --git a/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts b/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts index a23162c313a..7b815a1d698 100644 --- a/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts +++ b/public/app/plugins/panel/nodeGraph/useCategorizeFrames.ts @@ -2,6 +2,8 @@ import { useMemo } from 'react'; import { DataFrame } from '@grafana/data'; +import { getGraphFrame } from './utils'; + /** * As we need 2 dataframes for the service map, one with nodes and one with edges we have to figure out which is which. * Right now we do not have any metadata for it so we just check preferredVisualisationType and then column names. @@ -9,20 +11,6 @@ import { DataFrame } from '@grafana/data'; */ export function useCategorizeFrames(series: DataFrame[]) { return useMemo(() => { - return series.reduce<{ - nodes: DataFrame[]; - edges: DataFrame[]; - }>( - (acc, frame) => { - const sourceField = frame.fields.filter((f) => f.name === 'source'); - if (sourceField.length) { - acc.edges.push(frame); - } else { - acc.nodes.push(frame); - } - return acc; - }, - { edges: [], nodes: [] } - ); + return getGraphFrame(series); }, [series]); } diff --git a/public/app/plugins/panel/nodeGraph/utils.ts b/public/app/plugins/panel/nodeGraph/utils.ts index 4fb8e5095a2..a0d533b72f7 100644 --- a/public/app/plugins/panel/nodeGraph/utils.ts +++ b/public/app/plugins/panel/nodeGraph/utils.ts @@ -9,7 +9,7 @@ import { NodeGraphDataFrameFieldNames, } from '@grafana/data'; -import { EdgeDatum, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } from './types'; +import { EdgeDatum, GraphFrame, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } from './types'; type Line = { x1: number; y1: number; x2: number; y2: number }; @@ -593,3 +593,18 @@ export const findConnectedNodesForNode = (nodes: NodeDatum[], edges: EdgeDatum[] } return []; }; + +export const getGraphFrame = (frames: DataFrame[]) => { + return frames.reduce( + (acc, frame) => { + const sourceField = frame.fields.filter((f) => f.name === 'source'); + if (sourceField.length) { + acc.edges.push(frame); + } else { + acc.nodes.push(frame); + } + return acc; + }, + { edges: [], nodes: [] } + ); +}; From 831e8acf15ca4c108fd220edb28008b34efe8dc2 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Wed, 26 Jul 2023 14:03:17 -0500 Subject: [PATCH 45/64] Prometheus: Add support for day_of_year (#72403) add querybuilder/code editor support for day_of_year --- public/app/plugins/datasource/prometheus/promql.ts | 7 +++++++ .../datasource/prometheus/querybuilder/operations.ts | 4 ++++ .../plugins/datasource/prometheus/querybuilder/types.ts | 1 + 3 files changed, 12 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index 6baa26d4899..c37f52796ba 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -238,6 +238,13 @@ export const FUNCTIONS = [ documentation: 'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.', }, + { + insertText: 'day_of_year', + label: 'day_of_year', + detail: 'day_of_year(v=vector(time()) instant-vector)', + documentation: + 'Returns the day of the year for each of the given times in UTC. Returned values are from 1 to 365 for non-leap years, and 1 to 366 in leap years.', + }, { insertText: 'days_in_month', label: 'days_in_month', diff --git a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts index 788689ee7da..2c091045bb2 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/operations.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/operations.ts @@ -164,6 +164,10 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { id: PromOperationId.DayOfWeek, category: PromVisualQueryOperationCategory.Time, }), + createFunction({ + id: PromOperationId.DayOfYear, + category: PromVisualQueryOperationCategory.Time, + }), createFunction({ id: PromOperationId.DaysInMonth, category: PromVisualQueryOperationCategory.Time, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/types.ts index 49cef909698..24750f21a96 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/types.ts @@ -53,6 +53,7 @@ export enum PromOperationId { CountValues = 'count_values', DayOfMonth = 'day_of_month', DayOfWeek = 'day_of_week', + DayOfYear = 'day_of_year', DaysInMonth = 'days_in_month', Deg = 'deg', Delta = 'delta', From fbd2412f37984c92f1065d25eee3aa0ab99c1f57 Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Wed, 26 Jul 2023 14:47:13 -0500 Subject: [PATCH 46/64] Prometheus: Update heatmap unit tests (#72404) introduce tests for new potential heatmap response type, revert prior changes to tests --- .../prometheus/result_transformer.test.ts | 132 +++++++++++++++++- 1 file changed, 126 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/result_transformer.test.ts b/public/app/plugins/datasource/prometheus/result_transformer.test.ts index f44856d27af..76240c37803 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.test.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.test.ts @@ -360,7 +360,8 @@ describe('Prometheus Result Transformer', () => { expect(series.data[1].meta?.preferredVisualisationType).toEqual('rawPrometheus' as PreferredVisualisationType); }); - it('results with deprecated heatmap format should be correctly transformed', () => { + // Heatmap frames can either have a name of the metric, or if there is no metric, a name of "Value" + it('results with heatmap format (no metric name) should be correctly transformed', () => { const options = { targets: [ { @@ -420,7 +421,7 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].name).toEqual('2'); expect(series.data[0].fields[3].name).toEqual('+Inf'); }); - it('results with heatmap format should be correctly transformed', () => { + it('results with heatmap format (with metric name) should be correctly transformed', () => { const options = { targets: [ { @@ -437,9 +438,10 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'metric_name', type: FieldType.number, values: [10, 10, 0], - labels: { le: '1' }, + labels: { le: '1', __name__: 'metric_name' }, }, ], }), @@ -448,9 +450,10 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'metric_name', type: FieldType.number, values: [30, 10, 40], - labels: { le: '+Inf' }, + labels: { le: '+Inf', __name__: 'metric_name' }, }, ], }), @@ -459,9 +462,10 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'metric_name', type: FieldType.number, values: [20, 10, 30], - labels: { le: '2' }, + labels: { le: '2', __name__: 'metric_name' }, }, ], }), @@ -478,7 +482,7 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[3].name).toEqual('+Inf'); }); - it('results with heatmap format from multiple queries should be correctly transformed', () => { + it('results with heatmap format (no metric name) from multiple queries should be correctly transformed', () => { const options = { targets: [ { @@ -499,6 +503,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -510,6 +515,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 30], labels: { le: '2' }, @@ -521,6 +527,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40], labels: { le: '+Inf' }, @@ -532,6 +539,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -543,6 +551,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 30], labels: { le: '2' }, @@ -554,6 +563,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40], labels: { le: '+Inf' }, @@ -569,6 +579,103 @@ describe('Prometheus Result Transformer', () => { expect(series.data[0].fields[2].values).toEqual([10, 0, 30]); expect(series.data[0].fields[3].values).toEqual([10, 0, 10]); }); + it('results with heatmap format (with metric name) from multiple queries should be correctly transformed', () => { + const options = { + targets: [ + { + format: 'heatmap', + refId: 'A', + }, + { + format: 'heatmap', + refId: 'B', + }, + ], + } as unknown as DataQueryRequest; + const response = { + state: 'Done', + data: [ + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [10, 10, 0], + labels: { le: '1', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [20, 10, 30], + labels: { le: '2', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [30, 10, 40], + labels: { le: '+Inf', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'B', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [10, 10, 0], + labels: { le: '1', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'B', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [20, 10, 30], + labels: { le: '2', __name__: 'metric_name' }, + }, + ], + }), + createDataFrame({ + refId: 'B', + fields: [ + { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, + { + name: 'metric_name', + type: FieldType.number, + values: [30, 10, 40], + labels: { le: '+Inf', __name__: 'metric_name' }, + }, + ], + }), + ], + } as unknown as DataQueryResponse; + + const series = transformV2(response, options, {}); + expect(series.data[0].fields.length).toEqual(4); + expect(series.data[0].fields[1].values).toEqual([10, 10, 0]); + expect(series.data[0].fields[2].values).toEqual([10, 0, 30]); + expect(series.data[0].fields[3].values).toEqual([10, 0, 10]); + }); it('results with heatmap format and multiple histograms should be grouped and de-accumulated by non-le labels', () => { const options = { @@ -588,6 +695,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1', additionalProperty: '10' }, @@ -599,6 +707,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 30], labels: { le: '2', additionalProperty: '10' }, @@ -610,6 +719,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40], labels: { le: '+Inf', additionalProperty: '10' }, @@ -622,6 +732,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [0, 10, 10], labels: { le: '1', additionalProperty: '20' }, @@ -633,6 +744,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [20, 10, 40], labels: { le: '2', additionalProperty: '20' }, @@ -644,6 +756,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 60], labels: { le: '+Inf', additionalProperty: '20' }, @@ -656,6 +769,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 30, 60], labels: { le: '1', additionalProperty: '30' }, @@ -667,6 +781,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [30, 40, 60], labels: { le: '2', additionalProperty: '30' }, @@ -678,6 +793,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [40, 40, 60], labels: { le: '+Inf', additionalProperty: '30' }, @@ -719,6 +835,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -736,6 +853,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40, 90, 14, 21], labels: { le: '6' }, @@ -767,6 +885,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4] }, { + name: 'Value', type: FieldType.number, values: [10, 10, 0], labels: { le: '1' }, @@ -784,6 +903,7 @@ describe('Prometheus Result Transformer', () => { fields: [ { name: 'Time', type: FieldType.time, values: [6, 5, 4, 3, 2, 1] }, { + name: 'Value', type: FieldType.number, values: [30, 10, 40, 90, 14, 21], labels: { le: '6' }, From 18a364eb2ff539bbabc1eb035289bbb255eb6319 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 26 Jul 2023 20:48:32 +0000 Subject: [PATCH 47/64] Chore: Use Github App credentials for pr-commands.yml workflow (#72400) * Use grafana-pr-automation credentials * Action! --- .github/workflows/pr-commands.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-commands.yml b/.github/workflows/pr-commands.yml index e034b8993d1..d868efb6926 100644 --- a/.github/workflows/pr-commands.yml +++ b/.github/workflows/pr-commands.yml @@ -19,9 +19,15 @@ jobs: ref: main - name: Install Actions run: npm install --production --prefix ./actions + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_ID }} + private_key: ${{ secrets.GRAFANA_PR_AUTOMATION_APP_PEM }} - name: Run Commands uses: ./actions/commands with: metricsWriteAPIKey: ${{secrets.GRAFANA_MISC_STATS_API_KEY}} - token: ${{secrets.GITHUB_TOKEN}} + token: ${{ steps.generate_token.outputs.token }} configPath: pr-commands From 3dc60cd2d769d1a99abc891e527f2ddf06a4be9b Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Wed, 26 Jul 2023 17:08:36 -0500 Subject: [PATCH 48/64] Transforms: Add Format Time Transform (Alpha) (#72319) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stub transform editor * Mostly working * Get things working 💪 * Add tests * Add alpha flag * Timezone support * Remove debug statement * Fix tests * Prettier fix * Fix linter error * One more linter fix --- .../src/transformations/transformers.ts | 2 + .../transformers/formatTime.test.ts | 89 ++++++++++++++++ .../transformers/formatTime.ts | 76 +++++++++++++ .../src/transformations/transformers/ids.ts | 1 + .../editors/FormatTimeTransformerEditor.tsx | 100 ++++++++++++++++++ .../transformers/standardTransformers.ts | 2 + 6 files changed, 270 insertions(+) create mode 100644 packages/grafana-data/src/transformations/transformers/formatTime.test.ts create mode 100644 packages/grafana-data/src/transformations/transformers/formatTime.ts create mode 100644 public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index 218e5f8e9c0..a087bd9a467 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -6,6 +6,7 @@ import { filterFieldsTransformer, filterFramesTransformer } from './transformers import { filterFieldsByNameTransformer } from './transformers/filterByName'; import { filterFramesByRefIdTransformer } from './transformers/filterByRefId'; import { filterByValueTransformer } from './transformers/filterByValue'; +import { formatTimeTransformer } from './transformers/formatTime'; import { groupByTransformer } from './transformers/groupBy'; import { groupingToMatrixTransformer } from './transformers/groupingToMatrix'; import { histogramTransformer } from './transformers/histogram'; @@ -29,6 +30,7 @@ export const standardTransformers = { filterFramesTransformer, filterFramesByRefIdTransformer, filterByValueTransformer, + formatTimeTransformer, orderFieldsTransformer, organizeFieldsTransformer, reduceTransformer, diff --git a/packages/grafana-data/src/transformations/transformers/formatTime.test.ts b/packages/grafana-data/src/transformations/transformers/formatTime.test.ts new file mode 100644 index 00000000000..5a63b53bfe7 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/formatTime.test.ts @@ -0,0 +1,89 @@ +import { toDataFrame } from '../../dataframe/processDataFrame'; +import { FieldType } from '../../types/dataFrame'; +import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; + +import { createTimeFormatter, formatTimeTransformer } from './formatTime'; + +describe('Format Time Transformer', () => { + beforeAll(() => { + mockTransformationsRegistry([formatTimeTransformer]); + }); + + it('will convert time to formatted string', () => { + const options = { + timeField: 'time', + outputFormat: 'YYYY-MM', + useTimezone: false, + }; + + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const frame = toDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000], + }, + ], + }); + + const newFrame = formatter(frame.fields); + expect(newFrame[0].values).toEqual(['2021-02', '2023-07', '2023-04', '2023-07', '2023-08']); + }); + + it('will handle formats with times', () => { + const options = { + timeField: 'time', + outputFormat: 'YYYY-MM h:mm:ss a', + useTimezone: false, + }; + + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const frame = toDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, 1691011200000], + }, + ], + }); + + const newFrame = formatter(frame.fields); + expect(newFrame[0].values).toEqual([ + '2021-02 1:46:40 am', + '2023-07 2:00:00 pm', + '2023-04 3:20:00 pm', + '2023-07 5:34:49 pm', + '2023-08 3:20:00 pm', + ]); + }); + + it('will handle null times', () => { + const options = { + timeField: 'time', + outputFormat: 'YYYY-MM h:mm:ss a', + useTimezone: false, + }; + + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + const frame = toDataFrame({ + fields: [ + { + name: 'time', + type: FieldType.time, + values: [1612939600000, 1689192000000, 1682025600000, 1690328089000, null], + }, + ], + }); + + const newFrame = formatter(frame.fields); + expect(newFrame[0].values).toEqual([ + '2021-02 1:46:40 am', + '2023-07 2:00:00 pm', + '2023-04 3:20:00 pm', + '2023-07 5:34:49 pm', + 'Invalid date', + ]); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/formatTime.ts b/packages/grafana-data/src/transformations/transformers/formatTime.ts new file mode 100644 index 00000000000..66406f0ef48 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/formatTime.ts @@ -0,0 +1,76 @@ +import moment from 'moment-timezone'; +import { map } from 'rxjs/operators'; + +import { getTimeZone, getTimeZoneInfo } from '../../datetime'; +import { Field, FieldType } from '../../types'; +import { DataTransformerInfo } from '../../types/transformations'; + +import { DataTransformerID } from './ids'; + +export interface FormatTimeTransformerOptions { + timeField: string; + outputFormat: string; + useTimezone: boolean; +} + +export const formatTimeTransformer: DataTransformerInfo = { + id: DataTransformerID.formatTime, + name: 'Format Time', + description: 'Set the output format of a time field', + defaultOptions: { timeField: '', outputFormat: '', useTimezone: true }, + operator: (options) => (source) => + source.pipe( + map((data) => { + // If a field and a format are configured + // then format the time output + const formatter = createTimeFormatter(options.timeField, options.outputFormat, options.useTimezone); + + if (!Array.isArray(data) || data.length === 0) { + return data; + } + + return data.map((frame) => ({ + ...frame, + fields: formatter(frame.fields), + })); + }) + ), +}; + +/** + * @internal + */ +export const createTimeFormatter = + (timeField: string, outputFormat: string, useTimezone: boolean) => (fields: Field[]) => { + const tz = getTimeZone(); + + return fields.map((field) => { + // Find the configured field + if (field.name === timeField) { + // Update values to use the configured format + const newVals = field.values.map((value) => { + const date = moment(value); + + // Apply configured timezone if the + // option has been set. Otherwise + // use the date directly + if (useTimezone) { + const info = getTimeZoneInfo(tz, value); + const realTz = info !== undefined ? info.ianaName : 'UTC'; + + return date.tz(realTz).format(outputFormat); + } else { + return date.format(outputFormat); + } + }); + + return { + ...field, + type: FieldType.string, + values: newVals, + }; + } + + return field; + }); + }; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index bce099ea1ca..b3cb08dce24 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -37,4 +37,5 @@ export enum DataTransformerID { limit = 'limit', partitionByValues = 'partitionByValues', timeSeriesTable = 'timeSeriesTable', + formatTime = 'formatTime', } diff --git a/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx new file mode 100644 index 00000000000..4a39104b6fd --- /dev/null +++ b/public/app/features/transformers/editors/FormatTimeTransformerEditor.tsx @@ -0,0 +1,100 @@ +import React, { useCallback, ChangeEvent } from 'react'; + +import { + DataTransformerID, + SelectableValue, + standardTransformers, + TransformerRegistryItem, + TransformerUIProps, + getFieldDisplayName, + PluginState, +} from '@grafana/data'; +import { FormatTimeTransformerOptions } from '@grafana/data/src/transformations/transformers/formatTime'; +import { Select, InlineFieldRow, InlineField, Input, InlineSwitch } from '@grafana/ui'; + +export function FormatTimeTransfomerEditor({ + input, + options, + onChange, +}: TransformerUIProps) { + const timeFields: Array> = []; + + // Get time fields + for (const frame of input) { + for (const field of frame.fields) { + if (field.type === 'time') { + const name = getFieldDisplayName(field, frame, input); + timeFields.push({ label: name, value: name }); + } + } + } + + const onSelectField = useCallback( + (value: SelectableValue) => { + const val = value?.value !== undefined ? value.value : ''; + onChange({ + ...options, + timeField: val, + }); + }, + [onChange, options] + ); + + const onFormatChange = useCallback( + (e: ChangeEvent) => { + const val = e.target.value; + onChange({ + ...options, + outputFormat: val, + }); + }, + [onChange, options] + ); + + const onUseTzChange = useCallback(() => { + onChange({ + ...options, + useTimezone: !options.useTimezone, + }); + }, [onChange, options]); + + return ( + <> + + + + + + + + + + ); +} + +export const formatTimeTransformerRegistryItem: TransformerRegistryItem = { + id: DataTransformerID.formatTime, + editor: FormatTimeTransfomerEditor, + transformation: standardTransformers.formatTimeTransformer, + name: standardTransformers.formatTimeTransformer.name, + state: PluginState.alpha, + description: standardTransformers.formatTimeTransformer.description, +}; diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index 8716a48a20a..5a188e091e0 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -9,6 +9,7 @@ import { concatenateTransformRegistryItem } from './editors/ConcatenateTransform import { convertFieldTypeTransformRegistryItem } from './editors/ConvertFieldTypeTransformerEditor'; import { filterFieldsByNameTransformRegistryItem } from './editors/FilterByNameTransformerEditor'; import { filterFramesByRefIdTransformRegistryItem } from './editors/FilterByRefIdTransformerEditor'; +import { formatTimeTransformerRegistryItem } from './editors/FormatTimeTransformerEditor'; import { groupByTransformRegistryItem } from './editors/GroupByTransformerEditor'; import { groupingToMatrixTransformRegistryItem } from './editors/GroupingToMatrixTransformerEditor'; import { histogramTransformRegistryItem } from './editors/HistogramTransformerEditor'; @@ -59,6 +60,7 @@ export const getStandardTransformers = (): Array> = limitTransformRegistryItem, joinByLabelsTransformRegistryItem, partitionByValuesTransformRegistryItem, + formatTimeTransformerRegistryItem, ...(config.featureToggles.timeSeriesTable ? [timeSeriesTableTransformRegistryItem] : []), ]; }; From eaca6c3f49e91be358efe10ad358dd219a1d020b Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:21:29 +0300 Subject: [PATCH 49/64] Release: Bump version to 10.2.0-pre (#72418) "Release: Updated versions in package to 10.2.0-pre" Co-authored-by: grafana-delivery-bot[bot] <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> --- lerna.json | 2 +- package.json | 2 +- packages/grafana-data/package.json | 4 +-- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-e2e/package.json | 4 +-- packages/grafana-eslint-rules/package.json | 2 +- packages/grafana-runtime/package.json | 8 +++--- packages/grafana-schema/package.json | 2 +- .../x/AlertGroupsPanelCfg_types.gen.ts | 2 +- .../x/AnnotationsListPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/BarChartPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/BarGaugePanelCfg_types.gen.ts | 2 +- .../x/CandlestickPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/CanvasPanelCfg_types.gen.ts | 2 +- .../x/CloudWatchDataQuery_types.gen.ts | 2 +- .../x/DashboardListPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/DatagridPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/DebugPanelCfg_types.gen.ts | 2 +- .../x/ElasticsearchDataQuery_types.gen.ts | 2 +- .../panelcfg/x/GaugePanelCfg_types.gen.ts | 2 +- .../panelcfg/x/GeomapPanelCfg_types.gen.ts | 2 +- .../x/GrafanaPyroscopeDataQuery_types.gen.ts | 2 +- .../panelcfg/x/HeatmapPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/HistogramPanelCfg_types.gen.ts | 2 +- .../logs/panelcfg/x/LogsPanelCfg_types.gen.ts | 2 +- .../dataquery/x/LokiDataQuery_types.gen.ts | 2 +- .../news/panelcfg/x/NewsPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/NodeGraphPanelCfg_types.gen.ts | 2 +- .../dataquery/x/ParcaDataQuery_types.gen.ts | 2 +- .../panelcfg/x/PieChartPanelCfg_types.gen.ts | 2 +- .../x/PrometheusDataQuery_types.gen.ts | 2 +- .../stat/panelcfg/x/StatPanelCfg_types.gen.ts | 2 +- .../x/StateTimelinePanelCfg_types.gen.ts | 2 +- .../x/StatusHistoryPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/TablePanelCfg_types.gen.ts | 2 +- .../dataquery/x/TempoDataQuery_types.gen.ts | 2 +- .../x/TestDataDataQuery_types.gen.ts | 2 +- .../text/panelcfg/x/TextPanelCfg_types.gen.ts | 2 +- .../x/TimeSeriesPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/TrendPanelCfg_types.gen.ts | 2 +- .../panelcfg/x/XYChartPanelCfg_types.gen.ts | 2 +- packages/grafana-toolkit/package.json | 2 +- packages/grafana-ui/package.json | 8 +++--- .../internal/input-datasource/package.json | 6 ++-- yarn.lock | 28 +++++++++---------- 45 files changed, 68 insertions(+), 68 deletions(-) diff --git a/lerna.json b/lerna.json index 78f2726b208..ea142398f53 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "npmClient": "yarn", "useWorkspaces": true, "packages": ["packages/*"], - "version": "10.1.0-pre" + "version": "10.2.0-pre" } diff --git a/package.json b/package.json index 41f6faa3edb..b9efa587f92 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "repository": "github:grafana/grafana", "scripts": { "build": "yarn i18n:compile && NODE_ENV=production webpack --progress --config scripts/webpack/webpack.prod.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index f1e107ae0e3..f11a265e670 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/data", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -36,7 +36,7 @@ }, "dependencies": { "@braintree/sanitize-url": "6.0.2", - "@grafana/schema": "10.1.0-pre", + "@grafana/schema": "10.2.0-pre", "@types/d3-interpolate": "^3.0.0", "@types/string-hash": "1.1.1", "d3-interpolate": "3.0.1", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 175f65417fb..9f631f17e6e 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e-selectors", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana End-to-End Test Selectors Library", "keywords": [ "cli", diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index caf6cae2d7f..f934fce091d 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/e2e", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -63,7 +63,7 @@ "@babel/core": "7.22.1", "@babel/preset-env": "7.22.4", "@cypress/webpack-preprocessor": "5.17.0", - "@grafana/e2e-selectors": "10.1.0-pre", + "@grafana/e2e-selectors": "10.2.0-pre", "@grafana/tsconfig": "^1.2.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "9.1.2", diff --git a/packages/grafana-eslint-rules/package.json b/packages/grafana-eslint-rules/package.json index 71fd2085f4b..d4a645f4df5 100644 --- a/packages/grafana-eslint-rules/package.json +++ b/packages/grafana-eslint-rules/package.json @@ -1,7 +1,7 @@ { "name": "@grafana/eslint-plugin", "description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "main": "./index.cjs", "author": "Grafana Labs", "license": "Apache-2.0", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 4dce798898f..dc68119355d 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/runtime", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -37,10 +37,10 @@ "postpack": "mv package.json.bak package.json" }, "dependencies": { - "@grafana/data": "10.1.0-pre", - "@grafana/e2e-selectors": "10.1.0-pre", + "@grafana/data": "10.2.0-pre", + "@grafana/e2e-selectors": "10.2.0-pre", "@grafana/faro-web-sdk": "1.1.0", - "@grafana/ui": "10.1.0-pre", + "@grafana/ui": "10.2.0-pre", "history": "4.10.1", "lodash": "4.17.21", "rxjs": "7.8.0", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index f1ce5354c27..37660ac2381 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/schema", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts index f939bc22714..637ae0fe943 100644 --- a/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/alertgroups/panelcfg/x/AlertGroupsPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts index a95d1392ffb..8f90a5613c2 100644 --- a/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/annotationslist/panelcfg/x/AnnotationsListPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { limit: number; diff --git a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts index bb81fbbbea7..69a5ce4b397 100644 --- a/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/barchart/panelcfg/x/BarChartPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip, common.OptionsWithTextFormatting { /** diff --git a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts index f9077c0cf9f..48cfa180a31 100644 --- a/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/bargauge/panelcfg/x/BarGaugePanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.SingleStatBaseOptions { displayMode: common.BarGaugeDisplayMode; diff --git a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts index aa093eed0b5..60507742e24 100644 --- a/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/candlestick/panelcfg/x/CandlestickPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum VizDisplayMode { Candles = 'candles', diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index 9897755f069..1305709fbae 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum HorizontalConstraint { Center = 'center', diff --git a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts index ff037e763af..627d55686dc 100644 --- a/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/cloudwatch/dataquery/x/CloudWatchDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface MetricStat { /** diff --git a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts index 7b0e46da0ab..dccc3f0feb3 100644 --- a/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/dashboardlist/panelcfg/x/DashboardListPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { folderId?: number; diff --git a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts index 03c7052393c..acbfd5312b2 100644 --- a/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/datagrid/panelcfg/x/DatagridPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { selectedSeries: number; diff --git a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts index 5faa988a6ac..2ee9860577b 100644 --- a/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/debug/panelcfg/x/DebugPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export type UpdateConfig = { render: boolean, diff --git a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts index fe56cc74a8e..e23aae27ea2 100644 --- a/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/elasticsearch/dataquery/x/ElasticsearchDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export type BucketAggregation = (DateHistogram | Histogram | Terms | Filters | GeoHashGrid | Nested); diff --git a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts index 7e1c8ac7848..72a7217a93a 100644 --- a/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/gauge/panelcfg/x/GaugePanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.SingleStatBaseOptions { showThresholdLabels: boolean; diff --git a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts index 233b1f4dfa5..86c57978539 100644 --- a/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/geomap/panelcfg/x/GeomapPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { basemap: ui.MapLayerOptions; diff --git a/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts index dab93eb8a54..04dc94f6811 100644 --- a/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/grafanapyroscope/dataquery/x/GrafanaPyroscopeDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export type PhlareQueryType = ('metrics' | 'profile' | 'both'); diff --git a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts index d4f7eee6879..4879f20dc24 100644 --- a/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/heatmap/panelcfg/x/HeatmapPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; /** * Controls the color mode of the heatmap diff --git a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts index 5e3bd8dabc1..cab0e48a193 100644 --- a/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/histogram/panelcfg/x/HistogramPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.OptionsWithLegend, common.OptionsWithTooltip { /** diff --git a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts index 7b67028fabe..0b68effc85e 100644 --- a/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/logs/panelcfg/x/LogsPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { dedupStrategy: common.LogsDedupStrategy; diff --git a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts index 88b865f15f5..5b8c5c60772 100644 --- a/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/loki/dataquery/x/LokiDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum QueryEditorMode { Builder = 'builder', diff --git a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts index 2996ab2ec96..5386a44ad59 100644 --- a/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/news/panelcfg/x/NewsPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts index cdb2a684265..897977ea96e 100644 --- a/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/nodegraph/panelcfg/x/NodeGraphPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface ArcOption { /** diff --git a/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts index 066215e8ba8..e02d1b3e8aa 100644 --- a/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/parca/dataquery/x/ParcaDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export type ParcaQueryType = ('metrics' | 'profile' | 'both'); diff --git a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts index 24f88b84fec..61ff9f688ba 100644 --- a/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/piechart/panelcfg/x/PieChartPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; /** * Select the pie chart display style. diff --git a/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts index b29c4332ecb..71d33de6a0f 100644 --- a/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/prometheus/dataquery/x/PrometheusDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum QueryEditorMode { Builder = 'builder', diff --git a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts index ffe4d06603e..15d0cc419a8 100644 --- a/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/stat/panelcfg/x/StatPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.SingleStatBaseOptions { colorMode: common.BigValueColorMode; diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index 776d3d3297c..5b538683870 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 2da6e92acb9..86a56a27ab1 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTimezones { /** diff --git a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts index c81ef10cc6e..ce898f47111 100644 --- a/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/table/panelcfg/x/TablePanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as ui from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options { /** diff --git a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts index f148695125b..3aa9d56be0e 100644 --- a/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/tempo/dataquery/x/TempoDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface TempoQuery extends common.DataQuery { filters: Array; diff --git a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts index 9379847c977..99910580613 100644 --- a/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/testdata/dataquery/x/TestDataDataQuery_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum TestDataQueryType { Annotations = 'annotations', diff --git a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts index cde543411d7..1d0b5bede3d 100644 --- a/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/text/panelcfg/x/TextPanelCfg_types.gen.ts @@ -9,7 +9,7 @@ // // Run 'make gen-cue' from repository root to regenerate. -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum TextMode { Code = 'code', diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 34579cbf4c3..8f7ffde9faa 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; diff --git a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts index efa9e95a8da..a6954740245 100644 --- a/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/trend/panelcfg/x/TrendPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; /** * Identical to timeseries... except it does not have timezone settings diff --git a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts index c399dabfa32..a070d6c1d39 100644 --- a/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/xychart/panelcfg/x/XYChartPanelCfg_types.gen.ts @@ -11,7 +11,7 @@ import * as common from '@grafana/schema'; -export const pluginVersion = "10.1.0-pre"; +export const pluginVersion = "10.2.0-pre"; export enum SeriesMapping { Auto = 'auto', diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index a7467ee05e3..e66f4dda63b 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/toolkit", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Toolkit", "keywords": [ "grafana", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ef7aef1ae37..02b239c7161 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -2,7 +2,7 @@ "author": "Grafana Labs", "license": "Apache-2.0", "name": "@grafana/ui", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -49,10 +49,10 @@ "dependencies": { "@emotion/css": "11.11.2", "@emotion/react": "11.11.1", - "@grafana/data": "10.1.0-pre", - "@grafana/e2e-selectors": "10.1.0-pre", + "@grafana/data": "10.2.0-pre", + "@grafana/e2e-selectors": "10.2.0-pre", "@grafana/faro-web-sdk": "1.1.0", - "@grafana/schema": "10.1.0-pre", + "@grafana/schema": "10.2.0-pre", "@leeoniya/ufuzzy": "1.0.8", "@monaco-editor/react": "4.5.1", "@popperjs/core": "2.11.6", diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 3c7a04c738c..533437505ce 100644 --- a/plugins-bundled/internal/input-datasource/package.json +++ b/plugins-bundled/internal/input-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@grafana-plugins/input-datasource", - "version": "10.1.0-pre", + "version": "10.2.0-pre", "description": "Input Datasource", "private": true, "repository": { @@ -28,8 +28,8 @@ "webpack": "5.76.0" }, "dependencies": { - "@grafana/data": "10.1.0-pre", - "@grafana/ui": "10.1.0-pre", + "@grafana/data": "10.2.0-pre", + "@grafana/ui": "10.2.0-pre", "react": "18.2.0", "tslib": "2.5.0" } diff --git a/yarn.lock b/yarn.lock index 6e080abb8bc..568961be878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3678,9 +3678,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: - "@grafana/data": 10.1.0-pre + "@grafana/data": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 10.1.0-pre + "@grafana/ui": 10.2.0-pre "@types/jest": 26.0.15 "@types/react": 18.0.28 copy-webpack-plugin: 11.0.0 @@ -3716,12 +3716,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@10.1.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@10.2.0-pre, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": version: 0.0.0-use.local resolution: "@grafana/data@workspace:packages/grafana-data" dependencies: "@braintree/sanitize-url": 6.0.2 - "@grafana/schema": 10.1.0-pre + "@grafana/schema": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 25.0.2 "@rollup/plugin-json": 6.0.0 @@ -3783,7 +3783,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@10.1.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@10.2.0-pre, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": version: 0.0.0-use.local resolution: "@grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors" dependencies: @@ -3820,7 +3820,7 @@ __metadata: "@babel/core": 7.22.1 "@babel/preset-env": 7.22.4 "@cypress/webpack-preprocessor": 5.17.0 - "@grafana/e2e-selectors": 10.1.0-pre + "@grafana/e2e-selectors": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-node-resolve": 15.1.0 @@ -3983,11 +3983,11 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": 10.1.0-pre - "@grafana/e2e-selectors": 10.1.0-pre + "@grafana/data": 10.2.0-pre + "@grafana/e2e-selectors": 10.2.0-pre "@grafana/faro-web-sdk": 1.1.0 "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 10.1.0-pre + "@grafana/ui": 10.2.0-pre "@rollup/plugin-commonjs": 25.0.2 "@rollup/plugin-node-resolve": 15.1.0 "@testing-library/dom": 9.3.0 @@ -4040,7 +4040,7 @@ __metadata: languageName: node linkType: hard -"@grafana/schema@10.1.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@10.2.0-pre, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -4090,17 +4090,17 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@10.1.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@10.2.0-pre, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: "@babel/core": 7.22.1 "@emotion/css": 11.11.2 "@emotion/react": 11.11.1 - "@grafana/data": 10.1.0-pre - "@grafana/e2e-selectors": 10.1.0-pre + "@grafana/data": 10.2.0-pre + "@grafana/e2e-selectors": 10.2.0-pre "@grafana/faro-web-sdk": 1.1.0 - "@grafana/schema": 10.1.0-pre + "@grafana/schema": 10.2.0-pre "@grafana/tsconfig": ^1.2.0-rc1 "@leeoniya/ufuzzy": 1.0.8 "@mdx-js/react": 1.6.22 From b019ef9a89baca9fb25badc81d7dab55dc6c483e Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Thu, 27 Jul 2023 01:31:35 -0500 Subject: [PATCH 50/64] CI: use base64 key in windows installer build step (#72413) use base64 key --- .drone.yml | 8 ++++---- scripts/drone/steps/lib.star | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.drone.yml b/.drone.yml index fa5a1828282..392ab2674dc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2994,7 +2994,7 @@ steps: - windows-init environment: GCP_KEY: - from_secret: gcp_key + from_secret: gcp_grafanauploads_base64 GITHUB_TOKEN: from_secret: github_token PRERELEASE_BUCKET: @@ -3543,7 +3543,7 @@ steps: - windows-init environment: GCP_KEY: - from_secret: gcp_key + from_secret: gcp_grafanauploads_base64 GITHUB_TOKEN: from_secret: github_token PRERELEASE_BUCKET: @@ -4292,7 +4292,7 @@ steps: - windows-init environment: GCP_KEY: - from_secret: gcp_key + from_secret: gcp_grafanauploads_base64 GITHUB_TOKEN: from_secret: github_token PRERELEASE_BUCKET: @@ -4970,6 +4970,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: b3a378789d0a84f8eedd22567d2ce7609cc9c079bdd58ea8e71d0d9456c423e8 +hmac: cc3cdda004221d95f32b2753fa6c1d4fde277abaeee96ca6e6287b5c100fb52e ... diff --git a/scripts/drone/steps/lib.star b/scripts/drone/steps/lib.star index f24a5dc0bd8..60d56cafc58 100644 --- a/scripts/drone/steps/lib.star +++ b/scripts/drone/steps/lib.star @@ -1362,7 +1362,7 @@ def get_windows_steps(ver_mode, bucket = "%PRERELEASE_BUCKET%"): "windows-init", ], "environment": { - "GCP_KEY": from_secret("gcp_key"), + "GCP_KEY": from_secret(gcp_grafanauploads_base64), "PRERELEASE_BUCKET": from_secret(prerelease_bucket), "GITHUB_TOKEN": from_secret("github_token"), }, From 0ffa72877e1d3aec291cb82356aba00e372ccd8f Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:56:31 +0200 Subject: [PATCH 51/64] Chore: Bump keycloak version (#72386) * Bump keycloak version * Remove troubleshooting * Remove script for M1 machines --- .../oauth/docker-build-keycloak-m1-image.sh | 9 --------- .../blocks/auth/oauth/docker-compose.yaml | 2 +- devenv/docker/blocks/auth/oauth/readme.md | 17 ----------------- 3 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh diff --git a/devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh b/devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh deleted file mode 100644 index 46c1ef09fa3..00000000000 --- a/devenv/docker/blocks/auth/oauth/docker-build-keycloak-m1-image.sh +++ /dev/null @@ -1,9 +0,0 @@ -#/bin/sh - -VERSION=12.0.1 # set version here - -cd /tmp -git clone git@github.com:keycloak/keycloak-containers.git -cd keycloak-containers/server -git checkout $VERSION -docker build -t "quay.io/keycloak/keycloak:${VERSION}" . diff --git a/devenv/docker/blocks/auth/oauth/docker-compose.yaml b/devenv/docker/blocks/auth/oauth/docker-compose.yaml index 762e3c5a7a9..f322a879757 100644 --- a/devenv/docker/blocks/auth/oauth/docker-compose.yaml +++ b/devenv/docker/blocks/auth/oauth/docker-compose.yaml @@ -10,7 +10,7 @@ restart: unless-stopped oauthkeycloak: - image: quay.io/keycloak/keycloak:21.1 + image: quay.io/keycloak/keycloak:22.0 container_name: oauthkeycloak command: --spi-login-protocol-openid-connect-legacy-logout-redirect-uri=true start-dev environment: diff --git a/devenv/docker/blocks/auth/oauth/readme.md b/devenv/docker/blocks/auth/oauth/readme.md index a935f2c8682..e9bdab5e1ba 100644 --- a/devenv/docker/blocks/auth/oauth/readme.md +++ b/devenv/docker/blocks/auth/oauth/readme.md @@ -120,20 +120,3 @@ docker-compose exec -T oauthkeycloakdb bash -c "pg_dump -U keycloak keycloak" > - grafana oauth editor login: oauth-editor:grafana - grafana oauth admin login: oauth-admin:grafana - grafana oauth server admin login: oauth-grafanaadmin:grafana - -# Troubleshooting - -## Mac M1 Users - -The new arm64 architecture does not build for the latest docker image of keycloak. Refer to https://github.com/docker/for-mac/issues/5310 for the issue to see if it resolved. -Until then you need to build the docker image locally and then run `devenv`. - -1. Remove any lingering keycloak image -```sh -$ docker rmi $(docker images | grep 'keycloak') -``` -1. Build keycloak image locally -```sh -$ ./docker-build-keycloak-m1-image.sh -``` -1. Start from beginning of this readme From 5f09d8f2a69e6a7ff24c582c11acdc83bbcb7cf8 Mon Sep 17 00:00:00 2001 From: Horst Gutmann Date: Thu, 27 Jul 2023 10:05:07 +0200 Subject: [PATCH 52/64] CI: Add community-release workflow (#72350) * CI: Add community-release workflow for posting to community.grafana.com * CI: Set delivery team as owners of community-release workflow --- .github/CODEOWNERS | 1 + .github/workflows/community-release.yml | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/community-release.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c13a332333e..b183e5a6a8b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -602,6 +602,7 @@ embed.go @grafana/grafana-as-code /.github/workflows/codeowners-validator.yml @tolzhabayev /.github/workflows/codeql-analysis.yml @DanCech /.github/workflows/commands.yml @torkelo +/.github/workflows/community-release.yml @grafana/grafana-delivery /.github/workflows/detect-breaking-changes-* @grafana/plugins-platform-frontend /.github/workflows/doc-validator.yml @grafana/docs-tooling /.github/workflows/epic-add-to-platform-ux-parent-project.yml @meanmina diff --git a/.github/workflows/community-release.yml b/.github/workflows/community-release.yml new file mode 100644 index 00000000000..502f96192c7 --- /dev/null +++ b/.github/workflows/community-release.yml @@ -0,0 +1,25 @@ +name: Create community release post +on: + workflow_dispatch: + inputs: + version: + required: true + description: 'Needs to match, exactly, the name of a milestone. The version to be released please respect: major.minor.patch, major.minor.patch-preview or major.minor.patch-preview format. example: 7.4.3, 7.4.3-preview or 7.4.3-preview1' +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: "Generate token" + id: generate_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_ID }} + private_key: ${{ secrets.GRAFANA_DELIVERY_BOT_APP_PEM }} + - name: Run community-release (manually invoked) + uses: grafana/grafana-github-actions-go/community-release@main + with: + token: ${{ steps.generate_token.outputs.token }} + version: ${{ inputs.version }} + metrics_api_key: ${{ secrets.GRAFANA_MISC_STATS_API_KEY }} + community_api_key: ${{ secrets.GRAFANABOT_FORUM_KEY }} + community_api_username: grafanabot From a4a87f6228986513023b9a11b54757e3959ad37b Mon Sep 17 00:00:00 2001 From: Jo Date: Thu, 27 Jul 2023 11:09:08 +0200 Subject: [PATCH 53/64] Auth: Rename Sessions to Devices in counting (#72432) * rename session to device * rename session to device --- pkg/server/wireexts_oss.go | 4 +- pkg/services/anonymous/anonimpl/impl.go | 18 ++--- pkg/services/anonymous/anonimpl/impl_test.go | 26 +++---- pkg/services/anonymous/anontest/fake.go | 2 +- pkg/services/anonymous/service.go | 2 +- pkg/services/authn/authnimpl/service.go | 4 +- pkg/services/authn/clients/anonymous.go | 20 ++--- pkg/services/authn/clients/anonymous_test.go | 8 +- pkg/services/contexthandler/contexthandler.go | 76 +++++++++---------- 9 files changed, 80 insertions(+), 80 deletions(-) diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index ae597e24e2b..1a679f50288 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -47,8 +47,8 @@ var wireExtsBasicSet = wire.NewSet( authimpl.ProvideUserAuthTokenService, wire.Bind(new(auth.UserTokenService), new(*authimpl.UserAuthTokenService)), wire.Bind(new(auth.UserTokenBackgroundService), new(*authimpl.UserAuthTokenService)), - anonimpl.ProvideAnonymousSessionService, - wire.Bind(new(anonymous.Service), new(*anonimpl.AnonSessionService)), + anonimpl.ProvideAnonymousDeviceService, + wire.Bind(new(anonymous.Service), new(*anonimpl.AnonDeviceService)), licensing.ProvideService, wire.Bind(new(licensing.Licensing), new(*licensing.OSSLicensingService)), setting.ProvideProvider, diff --git a/pkg/services/anonymous/anonimpl/impl.go b/pkg/services/anonymous/anonimpl/impl.go index 2a6fba1642e..c060b948f64 100644 --- a/pkg/services/anonymous/anonimpl/impl.go +++ b/pkg/services/anonymous/anonimpl/impl.go @@ -20,12 +20,12 @@ import ( const thirtyDays = 30 * 24 * time.Hour const anonCachePrefix = "anon-session" -type AnonSession struct { +type Device struct { ip string userAgent string } -func (a *AnonSession) Key() (string, error) { +func (a *Device) Key() (string, error) { key := strings.Builder{} key.WriteString(a.ip) key.WriteString(a.userAgent) @@ -38,14 +38,14 @@ func (a *AnonSession) Key() (string, error) { return strings.Join([]string{anonCachePrefix, hex.EncodeToString(hash.Sum(nil))}, ":"), nil } -type AnonSessionService struct { +type AnonDeviceService struct { remoteCache remotecache.CacheStorage log log.Logger localCache *localcache.CacheService } -func ProvideAnonymousSessionService(remoteCache remotecache.CacheStorage, usageStats usagestats.Service) *AnonSessionService { - a := &AnonSessionService{ +func ProvideAnonymousDeviceService(remoteCache remotecache.CacheStorage, usageStats usagestats.Service) *AnonDeviceService { + a := &AnonDeviceService{ remoteCache: remoteCache, log: log.New("anonymous-session-service"), localCache: localcache.New(29*time.Minute, 15*time.Minute), @@ -56,7 +56,7 @@ func ProvideAnonymousSessionService(remoteCache remotecache.CacheStorage, usageS return a } -func (a *AnonSessionService) usageStatFn(ctx context.Context) (map[string]interface{}, error) { +func (a *AnonDeviceService) usageStatFn(ctx context.Context) (map[string]interface{}, error) { sessionCount, err := a.remoteCache.Count(ctx, anonCachePrefix) if err != nil { return nil, nil @@ -67,7 +67,7 @@ func (a *AnonSessionService) usageStatFn(ctx context.Context) (map[string]interf }, nil } -func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Request) error { +func (a *AnonDeviceService) TagDevice(ctx context.Context, httpReq *http.Request) error { addr := web.RemoteAddr(httpReq) ip, err := network.GetIPFromAddress(addr) if err != nil { @@ -80,12 +80,12 @@ func (a *AnonSessionService) TagSession(ctx context.Context, httpReq *http.Reque clientIPStr = "" } - anonSession := &AnonSession{ + anonDevice := &Device{ ip: clientIPStr, userAgent: httpReq.UserAgent(), } - key, err := anonSession.Key() + key, err := anonDevice.Key() if err != nil { return err } diff --git a/pkg/services/anonymous/anonimpl/impl_test.go b/pkg/services/anonymous/anonimpl/impl_test.go index d42c94733a3..db3d5d4fd29 100644 --- a/pkg/services/anonymous/anonimpl/impl_test.go +++ b/pkg/services/anonymous/anonimpl/impl_test.go @@ -12,15 +12,15 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" ) -func TestAnonSessionKey(t *testing.T) { +func TestAnonDeviceKey(t *testing.T) { testCases := []struct { name string - session *AnonSession + session *Device expected string }{ { name: "should hash correctly", - session: &AnonSession{ + session: &Device{ ip: "10.10.10.10", userAgent: "test", }, @@ -28,7 +28,7 @@ func TestAnonSessionKey(t *testing.T) { }, { name: "should hash correctly with different ip", - session: &AnonSession{ + session: &Device{ ip: "10.10.10.1", userAgent: "test", }, @@ -36,7 +36,7 @@ func TestAnonSessionKey(t *testing.T) { }, { name: "should hash correctly with different user agent", - session: &AnonSession{ + session: &Device{ ip: "10.10.10.1", userAgent: "test2", }, @@ -58,7 +58,7 @@ func TestAnonSessionKey(t *testing.T) { } } -func TestIntegrationAnonSessionService_tag(t *testing.T) { +func TestIntegrationAnonDeviceService_tag(t *testing.T) { testCases := []struct { name string req []*http.Request @@ -134,10 +134,10 @@ func TestIntegrationAnonSessionService_tag(t *testing.T) { t.Run(tc.name, func(t *testing.T) { fakeStore := remotecache.NewFakeStore(t) - anonService := ProvideAnonymousSessionService(fakeStore, &usagestats.UsageStatsMock{}) + anonService := ProvideAnonymousDeviceService(fakeStore, &usagestats.UsageStatsMock{}) for _, req := range tc.req { - err := anonService.TagSession(context.Background(), req) + err := anonService.TagDevice(context.Background(), req) require.NoError(t, err) } @@ -150,9 +150,9 @@ func TestIntegrationAnonSessionService_tag(t *testing.T) { } // Ensure that the local cache prevents request from being tagged -func TestIntegrationAnonSessionService_localCacheSafety(t *testing.T) { +func TestIntegrationAnonDeviceService_localCacheSafety(t *testing.T) { fakeStore := remotecache.NewFakeStore(t) - anonService := ProvideAnonymousSessionService(fakeStore, &usagestats.UsageStatsMock{}) + anonService := ProvideAnonymousDeviceService(fakeStore, &usagestats.UsageStatsMock{}) req := &http.Request{ Header: http.Header{ @@ -161,17 +161,17 @@ func TestIntegrationAnonSessionService_localCacheSafety(t *testing.T) { }, } - anonSession := &AnonSession{ + anonDevice := &Device{ ip: "10.30.30.2", userAgent: "test", } - key, err := anonSession.Key() + key, err := anonDevice.Key() require.NoError(t, err) anonService.localCache.SetDefault(key, true) - err = anonService.TagSession(context.Background(), req) + err = anonService.TagDevice(context.Background(), req) require.NoError(t, err) stats, err := anonService.usageStatFn(context.Background()) diff --git a/pkg/services/anonymous/anontest/fake.go b/pkg/services/anonymous/anontest/fake.go index d71050ab20f..147a5c3ed20 100644 --- a/pkg/services/anonymous/anontest/fake.go +++ b/pkg/services/anonymous/anontest/fake.go @@ -8,6 +8,6 @@ import ( type FakeAnonymousSessionService struct { } -func (f *FakeAnonymousSessionService) TagSession(ctx context.Context, httpReq *http.Request) error { +func (f *FakeAnonymousSessionService) TagDevice(ctx context.Context, httpReq *http.Request) error { return nil } diff --git a/pkg/services/anonymous/service.go b/pkg/services/anonymous/service.go index d84eea6a169..3d9fdf5969c 100644 --- a/pkg/services/anonymous/service.go +++ b/pkg/services/anonymous/service.go @@ -6,5 +6,5 @@ import ( ) type Service interface { - TagSession(context.Context, *http.Request) error + TagDevice(context.Context, *http.Request) error } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 32dae82d428..7ad7b36a559 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -57,7 +57,7 @@ func ProvideService( apikeyService apikey.Service, userService user.Service, jwtService auth.JWTVerifierService, usageStats usagestats.Service, - anonSessionService anonymous.Service, + anonDeviceService anonymous.Service, userProtectionService login.UserProtectionService, loginAttempts loginattempt.Service, quotaService quota.Service, authInfoService login.AuthInfoService, renderService rendering.Service, @@ -88,7 +88,7 @@ func ProvideService( } if s.cfg.AnonymousEnabled { - s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonSessionService)) + s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonDeviceService)) } var proxyClients []authn.ProxyClient diff --git a/pkg/services/authn/clients/anonymous.go b/pkg/services/authn/clients/anonymous.go index d42d4717fd4..81ffa9903df 100644 --- a/pkg/services/authn/clients/anonymous.go +++ b/pkg/services/authn/clients/anonymous.go @@ -14,20 +14,20 @@ import ( var _ authn.ContextAwareClient = new(Anonymous) -func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, anonSessionService anonymous.Service) *Anonymous { +func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, anonDeviceService anonymous.Service) *Anonymous { return &Anonymous{ - cfg: cfg, - log: log.New("authn.anonymous"), - orgService: orgService, - anonSessionService: anonSessionService, + cfg: cfg, + log: log.New("authn.anonymous"), + orgService: orgService, + anonDeviceService: anonDeviceService, } } type Anonymous struct { - cfg *setting.Cfg - log log.Logger - orgService org.Service - anonSessionService anonymous.Service + cfg *setting.Cfg + log log.Logger + orgService org.Service + anonDeviceService anonymous.Service } func (a *Anonymous) Name() string { @@ -54,7 +54,7 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn. a.log.Warn("tag anon session panic", "err", err) } }() - if err := a.anonSessionService.TagSession(context.Background(), httpReqCopy); err != nil { + if err := a.anonDeviceService.TagDevice(context.Background(), httpReqCopy); err != nil { a.log.Warn("failed to tag anonymous session", "error", err) } }() diff --git a/pkg/services/authn/clients/anonymous_test.go b/pkg/services/authn/clients/anonymous_test.go index 20cf7ea6a30..724dfbd2dc2 100644 --- a/pkg/services/authn/clients/anonymous_test.go +++ b/pkg/services/authn/clients/anonymous_test.go @@ -46,10 +46,10 @@ func TestAnonymous_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { c := Anonymous{ - cfg: tt.cfg, - log: log.NewNopLogger(), - orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err}, - anonSessionService: &anontest.FakeAnonymousSessionService{}, + cfg: tt.cfg, + log: log.NewNopLogger(), + orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err}, + anonDeviceService: &anontest.FakeAnonymousSessionService{}, } identity, err := c.Authenticate(context.Background(), &authn.Request{}) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 5b38ede7502..a0b664cf8bf 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -52,50 +52,50 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer tracer tracing.Tracer, authProxy *authproxy.AuthProxy, loginService login.Service, apiKeyService apikey.Service, authenticator loginpkg.Authenticator, userService user.Service, orgService org.Service, oauthTokenService oauthtoken.OAuthTokenService, features *featuremgmt.FeatureManager, - authnService authn.Service, anonSessionService anonymous.Service, + authnService authn.Service, anonDeviceService anonymous.Service, ) *ContextHandler { return &ContextHandler{ - Cfg: cfg, - AuthTokenService: tokenService, - JWTAuthService: jwtService, - RemoteCache: remoteCache, - RenderService: renderService, - SQLStore: sqlStore, - tracer: tracer, - authProxy: authProxy, - authenticator: authenticator, - loginService: loginService, - apiKeyService: apiKeyService, - userService: userService, - orgService: orgService, - oauthTokenService: oauthTokenService, - features: features, - authnService: authnService, - anonSessionService: anonSessionService, - singleflight: new(singleflight.Group), + Cfg: cfg, + AuthTokenService: tokenService, + JWTAuthService: jwtService, + RemoteCache: remoteCache, + RenderService: renderService, + SQLStore: sqlStore, + tracer: tracer, + authProxy: authProxy, + authenticator: authenticator, + loginService: loginService, + apiKeyService: apiKeyService, + userService: userService, + orgService: orgService, + oauthTokenService: oauthTokenService, + features: features, + authnService: authnService, + anonDeviceService: anonDeviceService, + singleflight: new(singleflight.Group), } } // ContextHandler is a middleware. type ContextHandler struct { - Cfg *setting.Cfg - AuthTokenService auth.UserTokenService - JWTAuthService auth.JWTVerifierService - RemoteCache *remotecache.RemoteCache - RenderService rendering.Service - SQLStore db.DB - tracer tracing.Tracer - authProxy *authproxy.AuthProxy - authenticator loginpkg.Authenticator - loginService login.Service - apiKeyService apikey.Service - userService user.Service - orgService org.Service - oauthTokenService oauthtoken.OAuthTokenService - features *featuremgmt.FeatureManager - authnService authn.Service - singleflight *singleflight.Group - anonSessionService anonymous.Service + Cfg *setting.Cfg + AuthTokenService auth.UserTokenService + JWTAuthService auth.JWTVerifierService + RemoteCache *remotecache.RemoteCache + RenderService rendering.Service + SQLStore db.DB + tracer tracing.Tracer + authProxy *authproxy.AuthProxy + authenticator loginpkg.Authenticator + loginService login.Service + apiKeyService apikey.Service + userService user.Service + orgService org.Service + oauthTokenService oauthtoken.OAuthTokenService + features *featuremgmt.FeatureManager + authnService authn.Service + singleflight *singleflight.Group + anonDeviceService anonymous.Service // GetTime returns the current time. // Stubbable by tests. GetTime func() time.Time @@ -282,7 +282,7 @@ func (h *ContextHandler) initContextWithAnonymousUser(reqContext *contextmodel.R reqContext.Logger.Warn("tag anon session panic", "err", err) } }() - if err := h.anonSessionService.TagSession(context.Background(), httpReqCopy); err != nil { + if err := h.anonDeviceService.TagDevice(context.Background(), httpReqCopy); err != nil { reqContext.Logger.Warn("Failed to tag anonymous session", "error", err) } }() From a912c970e376322ec874cf7edae5d82afda4be29 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 27 Jul 2023 11:11:43 +0200 Subject: [PATCH 54/64] Provisioning: Fix overwrite SecureJSONData on provisioning (#72395) * Overwrite SecureJSONData on provisioning --- pkg/services/datasources/models.go | 1 + .../datasources/service/datasource.go | 8 +- .../datasources/service/datasource_test.go | 90 +++++++++++++++++++ .../provisioning/datasources/types.go | 33 +++---- 4 files changed, 113 insertions(+), 19 deletions(-) diff --git a/pkg/services/datasources/models.go b/pkg/services/datasources/models.go index b472b0f597d..101a789b13a 100644 --- a/pkg/services/datasources/models.go +++ b/pkg/services/datasources/models.go @@ -134,6 +134,7 @@ type UpdateDataSourceCommand struct { ReadOnly bool `json:"-"` EncryptedSecureJsonData map[string][]byte `json:"-"` UpdateSecretFn UpdateSecretFn `json:"-"` + IgnoreOldSecureJsonData bool `json:"-"` } // DeleteDataSourceCommand will delete a DataSource based on OrgID as well as the UID (preferred), ID, or Name. diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index cdfad67cd63..22eee0656cb 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -644,9 +644,11 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U cmd.SecureJsonData = make(map[string]string) } - for k, v := range decrypted { - if _, ok := cmd.SecureJsonData[k]; !ok { - cmd.SecureJsonData[k] = v + if !cmd.IgnoreOldSecureJsonData { + for k, v := range decrypted { + if _, ok := cmd.SecureJsonData[k]; !ok { + cmd.SecureJsonData[k] = v + } } } diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index e830d2e04f3..a9049ad1275 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -187,6 +187,96 @@ func TestService_UpdateDataSource(t *testing.T) { _, err = dsService.UpdateDataSource(context.Background(), cmd) require.ErrorIs(t, err, datasources.ErrDataSourceNameExists) }) + + t.Run("should merge cmd.SecureJsonData with db data", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) + secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) + quotaService := quotatest.New(false, nil) + mockPermission := acmock.NewMockedPermissionsService() + dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService) + require.NoError(t, err) + + mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + + expectedDbKey := "db-secure-key" + expectedDbValue := "db-secure-value" + ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 1, + Name: "test-datasource", + SecureJsonData: map[string]string{ + expectedDbKey: expectedDbValue, + }, + }) + require.NoError(t, err) + + expectedOgKey := "cmd-secure-key" + expectedOgValue := "cmd-secure-value" + + cmd := &datasources.UpdateDataSourceCommand{ + ID: ds.ID, + OrgID: ds.OrgID, + Name: "test-datasource-updated", + SecureJsonData: map[string]string{ + expectedOgKey: expectedOgValue, + }, + } + + ds, err = dsService.UpdateDataSource(context.Background(), cmd) + require.NoError(t, err) + + secret, err := dsService.DecryptedValues(context.Background(), ds) + require.NoError(t, err) + + assert.Equal(t, secret[expectedDbKey], expectedDbValue) + assert.Equal(t, secret[expectedOgKey], expectedOgValue) + }) + + t.Run("should preserve cmd.SecureJsonData when cmd.IgnoreOldSecureJsonData=true", func(t *testing.T) { + sqlStore := db.InitTestDB(t) + secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore()) + secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger")) + quotaService := quotatest.New(false, nil) + mockPermission := acmock.NewMockedPermissionsService() + dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), actest.FakeAccessControl{}, mockPermission, quotaService) + require.NoError(t, err) + + mockPermission.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil) + + notExpectedDbKey := "db-secure-key" + dbValue := "db-secure-value" + ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{ + OrgID: 1, + Name: "test-datasource", + SecureJsonData: map[string]string{ + notExpectedDbKey: dbValue, + }, + }) + require.NoError(t, err) + + expectedOgKey := "cmd-secure-key" + expectedOgValue := "cmd-secure-value" + + cmd := &datasources.UpdateDataSourceCommand{ + ID: ds.ID, + OrgID: ds.OrgID, + Name: "test-datasource-updated", + SecureJsonData: map[string]string{ + expectedOgKey: expectedOgValue, + }, + IgnoreOldSecureJsonData: true, + } + + ds, err = dsService.UpdateDataSource(context.Background(), cmd) + require.NoError(t, err) + + secret, err := dsService.DecryptedValues(context.Background(), ds) + require.NoError(t, err) + + assert.Equal(t, secret[expectedOgKey], expectedOgValue) + _, ok := secret[notExpectedDbKey] + assert.False(t, ok) + }) } func TestService_NameScopeResolver(t *testing.T) { diff --git a/pkg/services/provisioning/datasources/types.go b/pkg/services/provisioning/datasources/types.go index 69c7b0d7259..0ccaf68fe5c 100644 --- a/pkg/services/provisioning/datasources/types.go +++ b/pkg/services/provisioning/datasources/types.go @@ -242,21 +242,22 @@ func createUpdateCommand(ds *upsertDataSourceFromConfig, id int64) *datasources. } return &datasources.UpdateDataSourceCommand{ - ID: id, - UID: ds.UID, - OrgID: ds.OrgID, - Name: ds.Name, - Type: ds.Type, - Access: datasources.DsAccess(ds.Access), - URL: ds.URL, - User: ds.User, - Database: ds.Database, - BasicAuth: ds.BasicAuth, - BasicAuthUser: ds.BasicAuthUser, - WithCredentials: ds.WithCredentials, - IsDefault: ds.IsDefault, - JsonData: jsonData, - SecureJsonData: ds.SecureJSONData, - ReadOnly: !ds.Editable, + ID: id, + UID: ds.UID, + OrgID: ds.OrgID, + Name: ds.Name, + Type: ds.Type, + Access: datasources.DsAccess(ds.Access), + URL: ds.URL, + User: ds.User, + Database: ds.Database, + BasicAuth: ds.BasicAuth, + BasicAuthUser: ds.BasicAuthUser, + WithCredentials: ds.WithCredentials, + IsDefault: ds.IsDefault, + JsonData: jsonData, + SecureJsonData: ds.SecureJSONData, + ReadOnly: !ds.Editable, + IgnoreOldSecureJsonData: true, } } From f10527cfe3f900f4484f1ce3bdc4e8fdf72aee4a Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Thu, 27 Jul 2023 13:28:00 +0200 Subject: [PATCH 55/64] Alerting: Contact points v2 part 2 (#71135) --- .betterer.results | 11 +- .../alerting/unified/api/alertmanagerApi.ts | 26 ++ .../contact-points/ContactPoints.v2.test.tsx | 90 ++++++ .../contact-points/ContactPoints.v2.tsx | 279 +++++++++++------- .../components/contact-points/Modals.tsx | 89 ++++++ .../__mocks__/alertmanager.config.mock.json | 71 +++++ .../__mocks__/receivers.mock.json | 57 ++++ .../contact-points/__mocks__/server.ts | 24 ++ .../useContactPoints.test.tsx.snap | 119 ++++++++ .../contact-points/useContactPoints.test.tsx | 18 ++ .../contact-points/useContactPoints.tsx | 92 +++++- .../components/contact-points/utils.ts | 93 ++++++ .../receivers/form/GrafanaReceiverForm.tsx | 4 +- .../receivers/grafanaAppReceivers/types.ts | 4 +- .../app/features/alerting/unified/mockApi.ts | 6 +- .../alerting/unified/utils/receiver-form.ts | 13 +- .../alerting/unified/utils/receivers.ts | 43 ++- .../plugins/datasource/alertmanager/types.ts | 22 +- 18 files changed, 908 insertions(+), 153 deletions(-) create mode 100644 public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx create mode 100644 public/app/features/alerting/unified/components/contact-points/Modals.tsx create mode 100644 public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json create mode 100644 public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json create mode 100644 public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts create mode 100644 public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap create mode 100644 public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx create mode 100644 public/app/features/alerting/unified/components/contact-points/utils.ts diff --git a/.betterer.results b/.betterer.results index d186fb188f7..353577392c5 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1870,6 +1870,9 @@ exports[`better eslint`] = { "public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/alerting/unified/components/receivers/TemplateForm.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -3559,13 +3562,7 @@ exports[`better eslint`] = { "public/app/plugins/datasource/alertmanager/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Unexpected any. Specify a different type.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"] + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index 67040babaf8..238f28d0ff3 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -1,6 +1,7 @@ import { isEmpty } from 'lodash'; import { dispatch } from 'app/store/store'; +import { ReceiversStateDTO } from 'app/types/alerting'; import { AlertmanagerAlert, @@ -226,5 +227,30 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ }), invalidatesTags: ['AlertmanagerConfiguration'], }), + + // Grafana Managed Alertmanager only + getContactPointsStatus: build.query({ + query: () => ({ + url: `/api/alertmanager/${getDatasourceAPIUid(GRAFANA_RULES_SOURCE_NAME)}/config/api/v1/receivers`, + }), + // this transformer basically fixes the weird "0001-01-01T00:00:00.000Z" and "0001-01-01T00:00:00.00Z" timestamps + // and sets both last attempt and duration to an empty string to indicate there hasn't been an attempt yet + transformResponse: (response: ReceiversStateDTO[]) => { + const isLastNotifyNullDate = (lastNotify: string) => lastNotify.startsWith('0001-01-01'); + + return response.map((receiversState) => ({ + ...receiversState, + integrations: receiversState.integrations.map((integration) => { + const noAttempt = isLastNotifyNullDate(integration.lastNotifyAttempt); + + return { + ...integration, + lastNotifyAttempt: noAttempt ? '' : integration.lastNotifyAttempt, + lastNotifyAttemptDuration: noAttempt ? '' : integration.lastNotifyAttemptDuration, + }; + }), + })); + }, + }), }), }); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx new file mode 100644 index 00000000000..62eaecef77b --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.test.tsx @@ -0,0 +1,90 @@ +import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { noop } from 'lodash'; +import React from 'react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import { selectors } from '@grafana/e2e-selectors'; + +import { disableRBAC } from '../../mocks'; +import { AlertmanagerProvider } from '../../state/AlertmanagerContext'; + +import ContactPoints, { ContactPoint } from './ContactPoints.v2'; + +import './__mocks__/server'; + +/** + * There are lots of ways in which we test our pages and components. Here's my opinionated approach to testing them. + * + * Use MSW to mock API responses, you can copy the JSON results from the network panel and use them in a __mocks__ folder. + * + * 1. Make sure we have "presentation" components we can test without mocking data, + * test these if they have some logic in them (hiding / showing things) and sad paths. + * + * 2. For testing the "container" components, check if data fetching is working as intended (you can use loading state) + * and check if we're not in an error state (although you can test for that too for sad path). + * + * 3. Write tests for the hooks we call in the "container" components + * if those have any logic or data structure transformations in them. + */ +describe('ContactPoints', () => { + beforeAll(() => { + disableRBAC(); + }); + + it('should show / hide loading states', async () => { + render( + + + , + { wrapper: TestProvider } + ); + + await waitFor(async () => { + await expect(screen.getByText('Loading...')).toBeInTheDocument(); + await waitForElementToBeRemoved(screen.getByText('Loading...')); + await expect(screen.queryByTestId(selectors.components.Alert.alertV2('error'))).not.toBeInTheDocument(); + }); + + expect(screen.getByText('grafana-default-email')).toBeInTheDocument(); + expect(screen.getAllByTestId('contact-point')).toHaveLength(4); + }); +}); + +describe('ContactPoint', () => { + it('should call delete when clicked and not disabled', async () => { + const onDelete = jest.fn(); + + render(); + + const moreActions = screen.getByTestId('more-actions'); + await userEvent.click(moreActions); + + const deleteButton = screen.getByRole('menuitem', { name: /delete/i }); + await userEvent.click(deleteButton); + + expect(onDelete).toHaveBeenCalledWith('my-contact-point'); + }); + + it('should disabled buttons', async () => { + render(); + + const moreActions = screen.getByTestId('more-actions'); + const editAction = screen.getByTestId('edit-action'); + + expect(moreActions).toHaveProperty('disabled', true); + expect(editAction).toHaveProperty('disabled', true); + }); + + it('should disabled buttons when provisioned', async () => { + render(); + + expect(screen.getByText(/provisioned/i)).toBeInTheDocument(); + + const moreActions = screen.getByTestId('more-actions'); + const editAction = screen.getByTestId('edit-action'); + + expect(moreActions).toHaveProperty('disabled', true); + expect(editAction).toHaveProperty('disabled', true); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx index 4c10828a51a..feee11ab5dd 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.v2.tsx @@ -1,104 +1,129 @@ import { css } from '@emotion/css'; -import React from 'react'; +import { SerializedError } from '@reduxjs/toolkit'; +import { uniqueId, upperFirst } from 'lodash'; +import React, { ReactNode } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { dateTime, GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { Button, Dropdown, Icon, Menu, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Dropdown, Icon, LoadingPlaceholder, Menu, Tooltip, useStyles2 } from '@grafana/ui'; import { Text } from '@grafana/ui/src/unstable'; import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap'; -import { GrafanaNotifierType } from 'app/types/alerting'; +import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; +import { GrafanaNotifierType, NotifierStatus } from 'app/types/alerting'; +import { useAlertmanager } from '../../state/AlertmanagerContext'; import { INTEGRATION_ICONS } from '../../types/contact-points'; import { MetaText } from '../MetaText'; import { ProvisioningBadge } from '../Provisioning'; import { Spacer } from '../Spacer'; import { Strong } from '../Strong'; +import { useDeleteContactPointModal } from './Modals'; +import { RECEIVER_STATUS_KEY, useContactPointsWithStatus, useDeleteContactPoint } from './useContactPoints'; +import { getReceiverDescription, isProvisioned, ReceiverConfigWithStatus } from './utils'; + const ContactPoints = () => { + const { selectedAlertmanager } = useAlertmanager(); + const { isLoading, error, contactPoints } = useContactPointsWithStatus(selectedAlertmanager!); + const { deleteTrigger, updateAlertmanagerState } = useDeleteContactPoint(selectedAlertmanager!); + + const [DeleteModal, showDeleteModal] = useDeleteContactPointModal(deleteTrigger, updateAlertmanagerState.isLoading); + + if (error) { + // TODO fix this type casting, when error comes from "getContactPointsStatus" it probably won't be a SerializedError + return {(error as SerializedError).message}; + } + + if (isLoading) { + return ; + } + + return ( + <> + + {contactPoints.map((contactPoint) => { + const contactPointKey = selectedAlertmanager + contactPoint.name; + const provisioned = isProvisioned(contactPoint); + const disabled = updateAlertmanagerState.isLoading; + + return ( + + ); + })} + + {DeleteModal} + + ); +}; + +interface ContactPointProps { + name: string; + disabled?: boolean; + provisioned?: boolean; + receivers: ReceiverConfigWithStatus[]; + onDelete: (name: string) => void; +} + +export const ContactPoint = ({ + name, + disabled = false, + provisioned = false, + receivers, + onDelete, +}: ContactPointProps) => { const styles = useStyles2(getStyles); return ( - -
- - -
- -
-
-
+
+ + +
+ {receivers?.map((receiver) => { + const diagnostics = receiver[RECEIVER_STATUS_KEY]; + const sendingResolved = !Boolean(receiver.disableResolveMessage); -
- - -
- - - - -
-
-
- -
- - -
- -
-
-
- -
- - -
- -
-
-
- -
- - -
- - - - -
-
-
- -
- - -
- - - -
-
-
- + return ( + + ); + })} +
+
+
); }; interface ContactPointHeaderProps { name: string; - provenance?: string; + disabled?: boolean; + provisioned?: boolean; policies?: string[]; // some array of policies that refer to this contact point + onDelete: (name: string) => void; } const ContactPointHeader = (props: ContactPointHeaderProps) => { - const { name, provenance, policies = [] } = props; - + const { name, disabled = false, provisioned = false, policies = [], onDelete } = props; const styles = useStyles2(getStyles); - const isProvisioned = Boolean(provenance); + + const disableActions = disabled || provisioned; return (
@@ -112,12 +137,12 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { is used by {policies.length} notification policies ) : ( - is not used + is not used in any policy )} - {isProvisioned && } + {provisioned && } ( {children} @@ -129,7 +154,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { size="sm" icon="edit" type="button" - disabled={isProvisioned} + disabled={disableActions} aria-label="edit-action" data-testid="edit-action" > @@ -141,7 +166,13 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { - + onDelete(name)} + /> } > @@ -152,6 +183,7 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { type="button" aria-label="more-actions" data-testid="more-actions" + disabled={disableActions} /> @@ -161,16 +193,19 @@ const ContactPointHeader = (props: ContactPointHeaderProps) => { interface ContactPointReceiverProps { type: GrafanaNotifierType | string; - description?: string; - error?: string; + description?: ReactNode; sendingResolved?: boolean; + diagnostics?: NotifierStatus; } const ContactPointReceiver = (props: ContactPointReceiverProps) => { - const { type, description, error, sendingResolved = true } = props; + const { type, description, diagnostics, sendingResolved = true } = props; const styles = useStyles2(getStyles); const iconName = INTEGRATION_ICONS[type]; + const hasMetadata = diagnostics !== undefined; + // TODO get the actual name of the type from /ngalert if grafanaManaged AM + const receiverName = receiverTypeNames[type] ?? upperFirst(type); return (
@@ -180,7 +215,7 @@ const ContactPointReceiver = (props: ContactPointReceiverProps) => { {iconName && } - {type} + {receiverName} {description && ( @@ -190,43 +225,71 @@ const ContactPointReceiver = (props: ContactPointReceiverProps) => { )}
-
- - {error ? ( - <> - {/* TODO we might need an error variant for MetaText, dito for success */} - {/* TODO show error details on hover or elsewhere */} - - - - - Last delivery attempt failed - - - - - - ) : ( + {hasMetadata && } + +
+ ); +}; + +interface ContactPointReceiverMetadata { + sendingResolved: boolean; + diagnostics: NotifierStatus; +} + +const ContactPointReceiverMetadataRow = (props: ContactPointReceiverMetadata) => { + const { diagnostics, sendingResolved } = props; + const styles = useStyles2(getStyles); + + const failedToSend = Boolean(diagnostics.lastNotifyAttemptError); + const lastDeliveryAttempt = dateTime(diagnostics.lastNotifyAttempt); + const lastDeliveryAttemptDuration = diagnostics.lastNotifyAttemptDuration; + const hasDeliveryAttempt = lastDeliveryAttempt.isValid(); + + return ( +
+ + {/* this is shown when the last delivery failed – we don't show any additional metadata */} + {failedToSend ? ( + <> + {/* TODO we might need an error variant for MetaText, dito for success */} + + + + + Last delivery attempt failed + + + + + + ) : ( + <> + {/* this is shown when we have a last delivery attempt */} + {hasDeliveryAttempt && ( <> - Last delivery attempt 25 minutes ago + Last delivery attempt{' '} + + + {lastDeliveryAttempt.locale('en').fromNow()} + + - took 2s + took {lastDeliveryAttemptDuration} )} + {/* when we have no last delivery attempt */} + {!hasDeliveryAttempt && No delivery attempts} + {/* this is only shown for contact points that only want "firing" updates */} {!sendingResolved && ( Delivering only firing notifications )} - -
+ + )}
); diff --git a/public/app/features/alerting/unified/components/contact-points/Modals.tsx b/public/app/features/alerting/unified/components/contact-points/Modals.tsx new file mode 100644 index 00000000000..6c4fe8ec7df --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/Modals.tsx @@ -0,0 +1,89 @@ +import React, { useCallback, useMemo, useState } from 'react'; + +import { Button, Modal, ModalProps } from '@grafana/ui'; + +type ModalHook = [JSX.Element, (item: T) => void, () => void]; + +/** + * This hook controls the delete modal for contact points, showing loading and error states when appropriate + */ +export const useDeleteContactPointModal = ( + handleDelete: (name: string) => Promise, + isLoading: boolean +): ModalHook => { + const [showModal, setShowModal] = useState(false); + const [contactPoint, setContactPoint] = useState(); + const [error, setError] = useState(); + + const handleDismiss = useCallback(() => { + if (isLoading) { + return; + } + + setContactPoint(undefined); + setShowModal(false); + setError(undefined); + }, [isLoading]); + + const handleShow = useCallback((name: string) => { + setContactPoint(name); + setShowModal(true); + setError(undefined); + }, []); + + const handleSubmit = useCallback(() => { + if (contactPoint) { + handleDelete(contactPoint) + .then(() => setShowModal(false)) + .catch(setError); + } + }, [handleDelete, contactPoint]); + + const modalElement = useMemo(() => { + if (error) { + return ; + } + + return ( + +

Deleting this contact point will permanently remove it.

+

Are you sure you want to delete this contact point?

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

Failed to update your configuration:

+

+ {String(error)} +

+
+); diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json new file mode 100644 index 00000000000..b5d9f3f2c82 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/alertmanager.config.mock.json @@ -0,0 +1,71 @@ +{ + "template_files": {}, + "alertmanager_config": { + "receivers": [ + { + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [ + { + "uid": "xeKQrBrnk", + "name": "grafana-default-email", + "type": "email", + "disableResolveMessage": false, + "settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false }, + "secureFields": {} + } + ] + }, + { + "name": "provisioned-contact-point", + "grafana_managed_receiver_configs": [ + { + "uid": "s8SdCVjnk", + "name": "provisioned-contact-point", + "type": "email", + "disableResolveMessage": false, + "settings": { "addresses": "gilles.demey@grafana.com", "singleEmail": false }, + "secureFields": {}, + "provenance": "api" + } + ] + }, + { + "name": "lotsa-emails", + "grafana_managed_receiver_configs": [ + { + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + "name": "lotsa-emails", + "type": "email", + "disableResolveMessage": false, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false + }, + "secureFields": {} + } + ] + }, + { + "name": "Slack with multiple channels", + "grafana_managed_receiver_configs": [ + { + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + "name": "Slack with multiple channels", + "type": "slack", + "disableResolveMessage": false, + "settings": { "recipient": "test-alerts" }, + "secureFields": { "token": true } + }, + { + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + "name": "Slack with multiple channels", + "type": "slack", + "disableResolveMessage": false, + "settings": { "recipient": "test-alerts2" }, + "secureFields": { "token": true } + } + ] + } + ] + } +} diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json b/public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json new file mode 100644 index 00000000000..d6ceb50db23 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/receivers.mock.json @@ -0,0 +1,57 @@ +[ + { + "active": true, + "integrations": [ + { + "lastNotifyAttempt": "2023-07-02T21:35:34.841+02:00", + "lastNotifyAttemptDuration": "1ms", + "lastNotifyAttemptError": "failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused", + "name": "email", + "sendResolved": true + } + ], + "name": "grafana-default-email" + }, + { + "active": false, + "integrations": [ + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "email", + "sendResolved": true + } + ], + "name": "provisioned-contact-point" + }, + { + "active": false, + "integrations": [ + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "email", + "sendResolved": true + } + ], + "name": "lotsa-emails" + }, + { + "active": false, + "integrations": [ + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "slack", + "sendResolved": true + }, + { + "lastNotifyAttempt": "0001-01-01T00:00:00.000Z", + "lastNotifyAttemptDuration": "0s", + "name": "slack", + "sendResolved": true + } + ], + "name": "Slack with multiple channels" + } +] diff --git a/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts b/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts new file mode 100644 index 00000000000..951a7bc1eff --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__mocks__/server.ts @@ -0,0 +1,24 @@ +import { rest } from 'msw'; + +import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; +import { ReceiversStateDTO } from 'app/types'; + +import { setupMswServer } from '../../../mockApi'; + +import alertmanagerMock from './alertmanager.config.mock.json'; +import receiversMock from './receivers.mock.json'; + +const server = setupMswServer(); + +server.use( + // this endpoint is a grafana built-in alertmanager + rest.get('/api/alertmanager/grafana/config/api/v1/alerts', (_req, res, ctx) => + res(ctx.json(alertmanagerMock)) + ), + // this endpoint is only available for the built-in alertmanager + rest.get('/api/alertmanager/grafana/config/api/v1/receivers', (_req, res, ctx) => + res(ctx.json(receiversMock)) + ) +); + +export default server; diff --git a/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap new file mode 100644 index 00000000000..81bfcb128f2 --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/__snapshots__/useContactPoints.test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useContactPoints should return contact points with status 1`] = ` +{ + "contactPoints": [ + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "grafana-default-email", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "xeKQrBrnk", + Symbol(receiver_status): { + "lastNotifyAttempt": "2023-07-02T21:35:34.841+02:00", + "lastNotifyAttemptDuration": "1ms", + "lastNotifyAttemptError": "failed to send notification to email addresses: gilles.demey@grafana.com: dial tcp 192.168.1.21:1025: connect: connection refused", + "name": "email", + "sendResolved": true, + }, + }, + ], + "name": "grafana-default-email", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "provisioned-contact-point", + "provenance": "api", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "s8SdCVjnk", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "email", + "sendResolved": true, + }, + }, + ], + "name": "provisioned-contact-point", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "lotsa-emails", + "secureFields": {}, + "settings": { + "addresses": "gilles.demey+1@grafana.com, gilles.demey+2@grafana.com, gilles.demey+3@grafana.com, gilles.demey+4@grafana.com", + "singleEmail": false, + }, + "type": "email", + "uid": "af306c96-35a2-4d6e-908a-4993e245dbb2", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "email", + "sendResolved": true, + }, + }, + ], + "name": "lotsa-emails", + }, + { + "grafana_managed_receiver_configs": [ + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts", + }, + "type": "slack", + "uid": "c02ad56a-31da-46b9-becb-4348ec0890fd", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "slack", + "sendResolved": true, + }, + }, + { + "disableResolveMessage": false, + "name": "Slack with multiple channels", + "secureFields": { + "token": true, + }, + "settings": { + "recipient": "test-alerts2", + }, + "type": "slack", + "uid": "b286a3be-f690-49e2-8605-b075cbace2df", + Symbol(receiver_status): { + "lastNotifyAttempt": "", + "lastNotifyAttemptDuration": "", + "name": "slack", + "sendResolved": true, + }, + }, + ], + "name": "Slack with multiple channels", + }, + ], + "error": undefined, + "isLoading": false, +} +`; diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx new file mode 100644 index 00000000000..ca7ff791f9e --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.test.tsx @@ -0,0 +1,18 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { TestProvider } from 'test/helpers/TestProvider'; + +import './__mocks__/server'; +import { useContactPointsWithStatus } from './useContactPoints'; + +describe('useContactPoints', () => { + it('should return contact points with status', async () => { + const { result } = renderHook(() => useContactPointsWithStatus('grafana'), { + wrapper: TestProvider, + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + expect(result.current).toMatchSnapshot(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx index 32244114cd9..095c586afec 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -3,21 +3,87 @@ * and (if available) it will also fetch the status from the Grafana Managed status endpoint */ -import { NotifierType, NotifierStatus } from 'app/types'; +import { produce } from 'immer'; +import { remove } from 'lodash'; -// A Contact Point has 1 or more integrations -// each integration can have additional metadata assigned to it -export interface ContactPoint { - notifiers: T[]; +import { alertmanagerApi } from '../../api/alertmanagerApi'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; + +import { enhanceContactPointsWithStatus } from './utils'; + +export const RECEIVER_STATUS_KEY = Symbol('receiver_status'); +const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds + +/** + * This hook will combine data from two endpoints; + * 1. the alertmanager config endpoint where the definition of the receivers are + * 2. (if available) the alertmanager receiver status endpoint, currently Grafana Managed only + */ +export function useContactPointsWithStatus(selectedAlertmanager: string) { + const isGrafanaManagedAlertmanager = selectedAlertmanager === GRAFANA_RULES_SOURCE_NAME; + + // fetch receiver status if we're dealing with a Grafana Managed Alertmanager + const fetchContactPointsStatus = alertmanagerApi.endpoints.getContactPointsStatus.useQuery(undefined, { + // TODO these don't seem to work since we've not called setupListeners() + refetchOnFocus: true, + refetchOnReconnect: true, + // re-fetch status every so often for up-to-date information + pollingInterval: RECEIVER_STATUS_POLLING_INTERVAL, + // skip fetching receiver statuses if not Grafana AM + skip: !isGrafanaManagedAlertmanager, + }); + + // fetch the latest config from the Alertmanager + const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( + selectedAlertmanager, + { + refetchOnFocus: true, + refetchOnReconnect: true, + selectFromResult: (result) => ({ + ...result, + contactPoints: result.data ? enhanceContactPointsWithStatus(result.data, fetchContactPointsStatus.data) : [], + }), + } + ); + + // TODO kinda yucky to combine hooks like this, better alternative? + const error = fetchAlertmanagerConfiguration.error ?? fetchContactPointsStatus.error; + const isLoading = fetchAlertmanagerConfiguration.isLoading || fetchContactPointsStatus.isLoading; + + const contactPoints = fetchAlertmanagerConfiguration.contactPoints; + + return { + error, + isLoading, + contactPoints, + }; } -interface Notifier { - type: NotifierType; -} +export function useDeleteContactPoint(selectedAlertmanager: string) { + const [fetchAlertmanagerConfig] = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useLazyQuery(); + const [updateAlertManager, updateAlertmanagerState] = + alertmanagerApi.endpoints.updateAlertmanagerConfiguration.useMutation(); -// Grafana Managed contact points have receivers with additional diagnostics -export interface NotifierWithDiagnostics extends Notifier { - status: NotifierStatus; -} + const deleteTrigger = (contactPointName: string) => { + return fetchAlertmanagerConfig(selectedAlertmanager).then(({ data }) => { + if (!data) { + return; + } -export function useContactPoints(AlertManagerSourceName: string) {} + const newConfig = produce(data, (draft) => { + remove(draft?.alertmanager_config?.receivers ?? [], (receiver) => receiver.name === contactPointName); + return draft; + }); + + return updateAlertManager({ + selectedAlertmanager, + config: newConfig, + }).unwrap(); + }); + }; + + return { + deleteTrigger, + updateAlertmanagerState, + }; +} diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts new file mode 100644 index 00000000000..d9e42a44f1f --- /dev/null +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -0,0 +1,93 @@ +import { split } from 'lodash'; +import { ReactNode } from 'react'; + +import { + AlertManagerCortexConfig, + GrafanaManagedContactPoint, + GrafanaManagedReceiverConfig, +} from 'app/plugins/datasource/alertmanager/types'; +import { NotifierStatus, ReceiversStateDTO } from 'app/types'; + +import { extractReceivers } from '../../utils/receivers'; + +import { RECEIVER_STATUS_KEY } from './useContactPoints'; + +export function isProvisioned(contactPoint: GrafanaManagedContactPoint) { + // for some reason the provenance is on the receiver and not the entire contact point + const provenance = contactPoint.grafana_managed_receiver_configs?.find((receiver) => receiver.provenance)?.provenance; + + return Boolean(provenance); +} + +// TODO we should really add some type information to these receiver settings... +export function getReceiverDescription(receiver: GrafanaManagedReceiverConfig): ReactNode | undefined { + switch (receiver.type) { + case 'email': { + const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings + return hasEmailAddresses ? summarizeEmailAddresses(receiver.settings['addresses']) : undefined; + } + case 'slack': { + const channelName = receiver.settings['recipient']; + return channelName ? `#${channelName}` : undefined; + } + case 'kafka': { + const topicName = receiver.settings['kafkaTopic']; + return topicName; + } + default: + return undefined; + } +} + +// input: foo+1@bar.com, foo+2@bar.com, foo+3@bar.com, foo+4@bar.com +// output: foo+1@bar.com, foo+2@bar.com, +2 more +function summarizeEmailAddresses(addresses: string): string { + const MAX_ADDRESSES_SHOWN = 3; + const SUPPORTED_SEPARATORS = /,|;|\\n/; + + const emails = addresses.trim().split(SUPPORTED_SEPARATORS); + const notShown = emails.length - MAX_ADDRESSES_SHOWN; + + const truncatedAddresses = split(addresses, SUPPORTED_SEPARATORS, MAX_ADDRESSES_SHOWN); + if (notShown > 0) { + truncatedAddresses.push(`+${notShown} more`); + } + + return truncatedAddresses.join(', '); +} + +// Grafana Managed contact points have receivers with additional diagnostics +export interface ReceiverConfigWithStatus extends GrafanaManagedReceiverConfig { + // we're using a symbol here so we'll never have a conflict on keys for a receiver + // we also specify that the diagnostics might be "undefined" for vanilla Alertmanager + [RECEIVER_STATUS_KEY]?: NotifierStatus | undefined; +} + +export interface ContactPointWithStatus extends GrafanaManagedContactPoint { + grafana_managed_receiver_configs: ReceiverConfigWithStatus[]; +} + +/** + * This function adds the status information for each of the integrations (contact point types) in a contact point + * 1. we iterate over all contact points + * 2. for each contact point we "enhance" it with the status or "undefined" for vanilla Alertmanager + */ +export function enhanceContactPointsWithStatus( + result: AlertManagerCortexConfig, + status: ReceiversStateDTO[] = [] +): ContactPointWithStatus[] { + const contactPoints = result.alertmanager_config.receivers ?? []; + + return contactPoints.map((contactPoint) => { + const receivers = extractReceivers(contactPoint); + const statusForReceiver = status.find((status) => status.name === contactPoint.name); + + return { + ...contactPoint, + grafana_managed_receiver_configs: receivers.map((receiver, index) => ({ + ...receiver, + [RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index], + })), + }; + }); +} diff --git a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx index bc45af1f128..684a090ebfd 100644 --- a/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/GrafanaReceiverForm.tsx @@ -3,8 +3,8 @@ import React, { useEffect, useMemo, useState } from 'react'; import { LoadingPlaceholder } from '@grafana/ui'; import { AlertManagerCortexConfig, + GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, - Receiver, TestReceiversAlert, } from 'app/plugins/datasource/alertmanager/types'; import { useDispatch } from 'app/types'; @@ -32,7 +32,7 @@ import { TestContactPointModal } from './TestContactPointModal'; interface Props { alertManagerSourceName: string; config: AlertManagerCortexConfig; - existing?: Receiver; + existing?: GrafanaManagedContactPoint; } const defaultChannelValues: GrafanaChannelValues = Object.freeze({ diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts index 9c1c245c501..099a73b8fc8 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/types.ts @@ -1,4 +1,4 @@ -import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; +import { GrafanaManagedContactPoint } from '../../../../../../plugins/datasource/alertmanager/types'; import { SupportedPlugin } from '../../../types/pluginBridges'; export interface AmRouteReceiver { @@ -7,7 +7,7 @@ export interface AmRouteReceiver { grafanaAppReceiverType?: SupportedPlugin; } -export interface ReceiverWithTypes extends Receiver { +export interface ReceiverWithTypes extends GrafanaManagedContactPoint { grafanaAppReceiverType?: SupportedPlugin; } export const GRAFANA_APP_RECEIVERS_SOURCE_IMAGE: Record = { diff --git a/public/app/features/alerting/unified/mockApi.ts b/public/app/features/alerting/unified/mockApi.ts index 7ce3e754d48..99e840a893d 100644 --- a/public/app/features/alerting/unified/mockApi.ts +++ b/public/app/features/alerting/unified/mockApi.ts @@ -8,9 +8,9 @@ import { backendSrv } from '../../../core/services/backend_srv'; import { AlertmanagerConfig, AlertManagerCortexConfig, + AlertmanagerReceiver, EmailConfig, MatcherOperator, - Receiver, Route, } from '../../../plugins/datasource/alertmanager/types'; @@ -84,7 +84,7 @@ class EmailConfigBuilder { } class AlertmanagerReceiverBuilder { - private receiver: Receiver = { name: '', email_configs: [] }; + private receiver: AlertmanagerReceiver = { name: '', email_configs: [] }; withName(name: string): AlertmanagerReceiverBuilder { this.receiver.name = name; @@ -124,7 +124,7 @@ export function mockApi(server: SetupServer) { }; } -// Creates a MSW server and sets up beforeAll and afterAll handlers for it +// Creates a MSW server and sets up beforeAll, afterAll and beforeEach handlers for it export function setupMswServer() { const server = setupServer(); diff --git a/public/app/features/alerting/unified/utils/receiver-form.ts b/public/app/features/alerting/unified/utils/receiver-form.ts index b3de19969fa..a80459e354b 100644 --- a/public/app/features/alerting/unified/utils/receiver-form.ts +++ b/public/app/features/alerting/unified/utils/receiver-form.ts @@ -2,6 +2,8 @@ import { isArray, isNil, omitBy } from 'lodash'; import { AlertManagerCortexConfig, + AlertmanagerReceiver, + GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, Receiver, Route, @@ -18,7 +20,7 @@ import { } from '../types/receiver-form'; export function grafanaReceiverToFormValues( - receiver: Receiver, + receiver: GrafanaManagedContactPoint, notifiers: NotifierDTO[] ): [ReceiverFormValues, GrafanaChannelMap] { const channelMap: GrafanaChannelMap = {}; @@ -92,7 +94,7 @@ export function formValuesToCloudReceiver( values: ReceiverFormValues, defaults: CloudChannelValues ): Receiver { - const recv: Receiver = { + const recv: AlertmanagerReceiver = { name: values.name, }; values.items.forEach(({ __id, type, settings, sendResolved }) => { @@ -101,11 +103,10 @@ export function formValuesToCloudReceiver( send_resolved: sendResolved ?? defaults.sendResolved, }); - const configsKey = `${type}_configs`; - if (!recv[configsKey]) { - recv[configsKey] = [channel]; + if (!(`${type}_configs` in recv)) { + recv[`${type}_configs`] = [channel]; } else { - (recv[configsKey] as unknown[]).push(channel); + (recv[`${type}_configs`] as unknown[]).push(channel); } }); return recv; diff --git a/public/app/features/alerting/unified/utils/receivers.ts b/public/app/features/alerting/unified/utils/receivers.ts index c599e45514b..11bed1bf7fc 100644 --- a/public/app/features/alerting/unified/utils/receivers.ts +++ b/public/app/features/alerting/unified/utils/receivers.ts @@ -1,4 +1,4 @@ -import { capitalize } from 'lodash'; +import { capitalize, isEmpty, times } from 'lodash'; import { receiverTypeNames } from 'app/plugins/datasource/alertmanager/consts'; import { GrafanaManagedReceiverConfig, Receiver } from 'app/plugins/datasource/alertmanager/types'; @@ -9,7 +9,7 @@ import { NotifierDTO } from 'app/types'; type NotifierTypeCounts = Record; // name : count export function extractNotifierTypeCounts(receiver: Receiver, grafanaNotifiers: NotifierDTO[]): NotifierTypeCounts { - if (receiver['grafana_managed_receiver_configs']) { + if ('grafana_managed_receiver_configs' in receiver) { return getGrafanaNotifierTypeCounts(receiver.grafana_managed_receiver_configs ?? [], grafanaNotifiers); } return getCortexAlertManagerNotifierTypeCounts(receiver); @@ -29,6 +29,45 @@ function getCortexAlertManagerNotifierTypeCounts(receiver: Receiver): NotifierTy }, {}); } +/** + * This function will extract the integrations that have been defined for either grafana managed contact point + * or vanilla Alertmanager receiver. + * + * It will attempt to normalize the data structure to how they have been defined for Grafana managed contact points. + * That way we can work with the same data structure in the UI. + * + * We don't normalize the configuration settings and those are blank for vanilla Alertmanager receivers. + * + * Example input: + * { name: 'my receiver', email_configs: [{ from: "foo@bar.com" }] } + * + * Example output: + * { name: 'my receiver', grafana_managed_receiver_configs: [{ type: 'email', settings: {} }] } + */ +export function extractReceivers(receiver: Receiver): GrafanaManagedReceiverConfig[] { + if ('grafana_managed_receiver_configs' in receiver) { + return receiver.grafana_managed_receiver_configs ?? []; + } + + const integrations = Object.entries(receiver) + .filter(([key]) => key !== 'grafana_managed_receiver_configs' && key.endsWith('_configs')) + .filter(([_, value]) => Array.isArray(value) && !isEmpty(value)) + .reduce((acc: GrafanaManagedReceiverConfig[], [key, value]) => { + const type = key.replace('_configs', ''); + + const configs = times(value.length, () => ({ + name: receiver.name, + type: type, + settings: [], // we don't normalize the configuration values + disableResolveMessage: false, + })); + + return acc.concat(configs); + }, []); + + return integrations; +} + function getGrafanaNotifierTypeCounts( configs: GrafanaManagedReceiverConfig[], grafanaNotifiers: NotifierDTO[] diff --git a/public/app/plugins/datasource/alertmanager/types.ts b/public/app/plugins/datasource/alertmanager/types.ts index b8ff117bc1d..293b4850d3d 100644 --- a/public/app/plugins/datasource/alertmanager/types.ts +++ b/public/app/plugins/datasource/alertmanager/types.ts @@ -79,20 +79,22 @@ export type GrafanaManagedReceiverConfig = { provenance?: string; }; -export type Receiver = { +export interface GrafanaManagedContactPoint { + name: string; + grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[]; +} + +export interface AlertmanagerReceiver { name: string; email_configs?: EmailConfig[]; - pagerduty_configs?: any[]; - pushover_configs?: any[]; - slack_configs?: any[]; - opsgenie_configs?: any[]; webhook_configs?: WebhookConfig[]; - victorops_configs?: any[]; - wechat_configs?: any[]; - grafana_managed_receiver_configs?: GrafanaManagedReceiverConfig[]; - [key: string]: any; -}; + + // this is supposedly to support any *_configs + [key: `${string}_configs`]: any[] | undefined; +} + +export type Receiver = GrafanaManagedContactPoint | AlertmanagerReceiver; export type ObjectMatcher = [name: string, operator: MatcherOperator, value: string]; From 5e5e617693f62c7f6ddbc562105b9725e985b714 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 27 Jul 2023 12:29:41 +0000 Subject: [PATCH 56/64] Chore: Use GITHUB_TOKEN in stale instead of grot token (#72126) * Chore: Use GITHUB_TOKEN in stale instead of grot token * update permissions --- .github/workflows/stale.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 598193f696a..de50fe2f26b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,13 +3,16 @@ on: schedule: - cron: '30 1 * * *' +permissions: + pull-requests: write + jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v8 with: - repo-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + repo-token: ${{ secrets.GITHUB_TOKEN }} # Number of days of inactivity before a stale Issue or Pull Request is closed. # Set to -1 to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. days-before-close: 14 From 758d9884bc522f24dca77c86ba731dd886d98c9c Mon Sep 17 00:00:00 2001 From: Will Browne Date: Thu, 27 Jul 2023 15:29:13 +0200 Subject: [PATCH 57/64] Plugins: Plugins loader pipeline (#71438) * discovery * flesh out * add docs * remove unused func * bootstrap stage * fix docs * update docs * undo unnecessary changes * add end tag * update doc * fix linter * fix * tidy * update docs * add class to filter func * apply PR feedback * fix test --- pkg/api/plugin_resource_test.go | 11 +- .../manager/loader/assetpath/assetpath.go | 5 + pkg/plugins/manager/loader/loader.go | 282 +++++------------- pkg/plugins/manager/loader/loader_test.go | 113 ++----- .../manager/manager_integration_test.go | 13 +- .../manager/pipeline/bootstrap/bootstrap.go | 78 +++++ pkg/plugins/manager/pipeline/bootstrap/doc.go | 6 + .../manager/pipeline/bootstrap/factory.go | 76 +++++ .../manager/pipeline/bootstrap/steps.go | 146 +++++++++ .../manager/pipeline/bootstrap/steps_test.go | 89 ++++++ .../manager/pipeline/discovery/discovery.go | 74 +++++ pkg/plugins/manager/pipeline/discovery/doc.go | 6 + .../manager/pipeline/discovery/steps.go | 55 ++++ pkg/plugins/manager/pipeline/doc.go | 11 + pkg/plugins/manager/signature/manifest.go | 14 +- .../pluginsintegration/pipeline/discovery.go | 33 ++ .../pluginsintegration/pluginsintegration.go | 9 + 17 files changed, 709 insertions(+), 312 deletions(-) create mode 100644 pkg/plugins/manager/pipeline/bootstrap/bootstrap.go create mode 100644 pkg/plugins/manager/pipeline/bootstrap/doc.go create mode 100644 pkg/plugins/manager/pipeline/bootstrap/factory.go create mode 100644 pkg/plugins/manager/pipeline/bootstrap/steps.go create mode 100644 pkg/plugins/manager/pipeline/bootstrap/steps_test.go create mode 100644 pkg/plugins/manager/pipeline/discovery/discovery.go create mode 100644 pkg/plugins/manager/pipeline/discovery/doc.go create mode 100644 pkg/plugins/manager/pipeline/discovery/steps.go create mode 100644 pkg/plugins/manager/pipeline/doc.go create mode 100644 pkg/services/pluginsintegration/pipeline/discovery.go diff --git a/pkg/api/plugin_resource_test.go b/pkg/api/plugin_resource_test.go index 97c08eaf6e0..b3497f5e6d4 100644 --- a/pkg/api/plugin_resource_test.go +++ b/pkg/api/plugin_resource_test.go @@ -38,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken/oauthtokentest" "github.com/grafana/grafana/pkg/services/pluginsintegration" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -69,10 +70,14 @@ func TestCallResource(t *testing.T) { reg := registry.ProvideService() angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) + bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + l := loader.ProvideService(pCfg, fakes.NewFakeLicensingService(), signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), - angularInspector, &fakes.FakeOauthService{}) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/loader/assetpath/assetpath.go b/pkg/plugins/manager/loader/assetpath/assetpath.go index 76ee0637805..0b093b0b753 100644 --- a/pkg/plugins/manager/loader/assetpath/assetpath.go +++ b/pkg/plugins/manager/loader/assetpath/assetpath.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/pluginscdn" ) @@ -22,6 +23,10 @@ func ProvideService(cdn *pluginscdn.Service) *Service { return &Service{cdn: cdn} } +func DefaultService(cfg *config.Cfg) *Service { + return &Service{cdn: pluginscdn.ProvideService(cfg)} +} + // Base returns the base path for the specified plugin. func (s *Service) Base(pluginJSON plugins.JSONData, class plugins.Class, pluginDir string) (string, error) { if class == plugins.ClassCore { diff --git a/pkg/plugins/manager/loader/loader.go b/pkg/plugins/manager/loader/loader.go index 9852efcf9e7..88b7e722e8e 100644 --- a/pkg/plugins/manager/loader/loader.go +++ b/pkg/plugins/manager/loader/loader.go @@ -3,38 +3,35 @@ package loader import ( "context" "errors" - "fmt" - "path" - "strings" "time" "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/infra/slugify" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/config" "github.com/grafana/grafana/pkg/plugins/log" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" "github.com/grafana/grafana/pkg/plugins/oauth" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/util" ) var _ plugins.ErrorResolver = (*Loader)(nil) type Loader struct { - pluginFinder finder.Finder + discovery discovery.Discoverer + bootstrap bootstrap.Bootstrapper + processManager process.Service pluginRegistry registry.Service roleRegistry plugins.RoleRegistry pluginInitializer initializer.Initializer signatureValidator signature.Validator - signatureCalculator plugins.SignatureCalculator externalServiceRegistry oauth.ExternalServiceRegistry assetPath *assetpath.Service log log.Logger @@ -46,24 +43,23 @@ type Loader struct { } func ProvideService(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, - pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, pluginFinder finder.Finder, - roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, signatureCalculator plugins.SignatureCalculator, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { + pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, + roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { return New(cfg, license, authorizer, pluginRegistry, backendProvider, process.NewManager(pluginRegistry), - roleRegistry, assetPath, pluginFinder, signatureCalculator, angularInspector, externalServiceRegistry) + roleRegistry, assetPath, angularInspector, externalServiceRegistry, discovery, bootstrap) } func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLoaderAuthorizer, pluginRegistry registry.Service, backendProvider plugins.BackendFactoryProvider, - processManager process.Service, roleRegistry plugins.RoleRegistry, - assetPath *assetpath.Service, pluginFinder finder.Finder, signatureCalculator plugins.SignatureCalculator, - angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry) *Loader { + processManager process.Service, roleRegistry plugins.RoleRegistry, assetPath *assetpath.Service, + angularInspector angularinspector.Inspector, externalServiceRegistry oauth.ExternalServiceRegistry, + discovery discovery.Discoverer, bootstrap bootstrap.Bootstrapper) *Loader { return &Loader{ - pluginFinder: pluginFinder, pluginRegistry: pluginRegistry, pluginInitializer: initializer.New(cfg, backendProvider, license), signatureValidator: signature.NewValidator(authorizer), - signatureCalculator: signatureCalculator, processManager: processManager, errs: make(map[string]*plugins.SignatureError), log: log.New("plugin.loader"), @@ -72,70 +68,29 @@ func New(cfg *config.Cfg, license plugins.Licensing, authorizer plugins.PluginLo assetPath: assetPath, angularInspector: angularInspector, externalServiceRegistry: externalServiceRegistry, + discovery: discovery, + bootstrap: bootstrap, } } func (l *Loader) Load(ctx context.Context, src plugins.PluginSource) ([]*plugins.Plugin, error) { - found, err := l.pluginFinder.Find(ctx, src) + // + discoveredPlugins, err := l.discovery.Discover(ctx, src) if err != nil { return nil, err } + // - return l.loadPlugins(ctx, src, found) -} - -// nolint:gocyclo -func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { - loadedPlugins := make([]*plugins.Plugin, 0, len(found)) - - for _, p := range found { - if _, exists := l.pluginRegistry.Plugin(ctx, p.Primary.JSONData.ID); exists { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) - continue - } - - sig, err := l.signatureCalculator.Calculate(ctx, src, p.Primary) - if err != nil { - l.log.Warn("Could not calculate plugin signature state", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - plugin, err := l.createPluginBase(p.Primary.JSONData, src.PluginClass(ctx), p.Primary.FS) - if err != nil { - l.log.Error("Could not create primary plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - - plugin.Signature = sig.Status - plugin.SignatureType = sig.Type - plugin.SignatureOrg = sig.SigningOrg - - loadedPlugins = append(loadedPlugins, plugin) - - for _, c := range p.Children { - if _, exists := l.pluginRegistry.Plugin(ctx, c.JSONData.ID); exists { - l.log.Warn("Skipping plugin loading as it's a duplicate", "pluginID", p.Primary.JSONData.ID) - continue - } - - cp, err := l.createPluginBase(c.JSONData, plugin.Class, c.FS) - if err != nil { - l.log.Error("Could not create child plugin base", "pluginID", p.Primary.JSONData.ID, "err", err) - continue - } - cp.Parent = plugin - cp.Signature = sig.Status - cp.SignatureType = sig.Type - cp.SignatureOrg = sig.SigningOrg - - plugin.Children = append(plugin.Children, cp) - - loadedPlugins = append(loadedPlugins, cp) - } + // + bootstrappedPlugins, err := l.bootstrap.Bootstrap(ctx, src, discoveredPlugins) + if err != nil { + return nil, err } + // - // validate signatures - verifiedPlugins := make([]*plugins.Plugin, 0, len(loadedPlugins)) - for _, plugin := range loadedPlugins { + // + verifiedPlugins := make([]*plugins.Plugin, 0, len(bootstrappedPlugins)) + for _, plugin := range bootstrappedPlugins { signingError := l.signatureValidator.Validate(plugin) if signingError != nil { l.log.Warn("Skipping loading plugin due to problem with signature", @@ -149,14 +104,6 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun // clear plugin error if a pre-existing error has since been resolved delete(l.errs, plugin.ID) - // Hardcoded alias changes - switch plugin.ID { - case "grafana-pyroscope-datasource": // rebranding - plugin.Alias = "phlare" - case "debug": // panel plugin used for testing - plugin.Alias = "debugX" - } - // verify module.js exists for SystemJS to load. // CDN plugins can be loaded with plugin.json only, so do not warn for those. if !plugin.IsRenderer() && !plugin.IsCorePlugin() { @@ -173,37 +120,56 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun } } - if plugin.IsApp() { - setDefaultNavURL(plugin) - } + // detect angular for external plugins + if plugin.IsExternalPlugin() { + var err error - if plugin.Parent != nil && plugin.Parent.IsApp() { - configureAppChildPlugin(plugin.Parent, plugin) + cctx, canc := context.WithTimeout(ctx, time.Second*10) + plugin.AngularDetected, err = l.angularInspector.Inspect(cctx, plugin) + canc() + + if err != nil { + l.log.Warn("Could not inspect plugin for angular", "pluginID", plugin.ID, "err", err) + } + + // Do not initialize plugins if they're using Angular and Angular support is disabled + if plugin.AngularDetected && !l.cfg.AngularSupportEnabled { + l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", plugin.ID) + continue + } } verifiedPlugins = append(verifiedPlugins, plugin) } + // - // initialize plugins + // initializedPlugins := make([]*plugins.Plugin, 0, len(verifiedPlugins)) for _, p := range verifiedPlugins { - // detect angular for external plugins - if p.IsExternalPlugin() { - var err error + err = l.pluginInitializer.Initialize(ctx, p) + if err != nil { + l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) + continue + } - cctx, canc := context.WithTimeout(ctx, time.Second*10) - p.AngularDetected, err = l.angularInspector.Inspect(cctx, p) - canc() + if err = l.pluginRegistry.Add(ctx, p); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + continue + } - if err != nil { - l.log.Warn("Could not inspect plugin for angular", "pluginID", p.ID, "err", err) - } + if !p.IsCorePlugin() { + l.log.Info("Plugin registered", "pluginID", p.ID) + } - // Do not initialize plugins if they're using Angular and Angular support is disabled - if p.AngularDetected && !l.cfg.AngularSupportEnabled { - l.log.Error("Refusing to initialize plugin because it's using Angular, which has been disabled", "pluginID", p.ID) - continue - } + initializedPlugins = append(initializedPlugins, p) + } + // + + // + for _, p := range initializedPlugins { + if err = l.processManager.Start(ctx, p.ID); err != nil { + l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + continue } if p.ExternalServiceRegistration != nil && l.cfg.Features.IsEnabled(featuremgmt.FlagExternalServiceAuth) { @@ -215,27 +181,15 @@ func (l *Loader) loadPlugins(ctx context.Context, src plugins.PluginSource, foun p.ExternalService = s } - err := l.pluginInitializer.Initialize(ctx, p) - if err != nil { - l.log.Error("Could not initialize plugin", "pluginId", p.ID, "err", err) - continue - } - if errDeclareRoles := l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); errDeclareRoles != nil { - l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", errDeclareRoles) - } - - initializedPlugins = append(initializedPlugins, p) - } - - for _, p := range initializedPlugins { - if err := l.load(ctx, p); err != nil { - l.log.Error("Could not start plugin", "pluginId", p.ID, "err", err) + if err = l.roleRegistry.DeclarePluginRoles(ctx, p.ID, p.Name, p.Roles); err != nil { + l.log.Warn("Declare plugin roles failed.", "pluginID", p.ID, "err", err) } if !p.IsCorePlugin() && !p.IsBundledPlugin() { metrics.SetPluginBuildInformation(p.ID, string(p.Type), p.Info.Version, string(p.Signature)) } } + // return initializedPlugins, nil } @@ -256,18 +210,6 @@ func (l *Loader) Unload(ctx context.Context, pluginID string) error { return nil } -func (l *Loader) load(ctx context.Context, p *plugins.Plugin) error { - if err := l.pluginRegistry.Add(ctx, p); err != nil { - return err - } - - if !p.IsCorePlugin() { - l.log.Info("Plugin registered", "pluginID", p.ID) - } - - return l.processManager.Start(ctx, p.ID) -} - func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { l.log.Debug("Stopping plugin process", "pluginId", p.ID) @@ -289,94 +231,6 @@ func (l *Loader) unload(ctx context.Context, p *plugins.Plugin) error { return nil } -func (l *Loader) createPluginBase(pluginJSON plugins.JSONData, class plugins.Class, files plugins.FS) (*plugins.Plugin, error) { - baseURL, err := l.assetPath.Base(pluginJSON, class, files.Base()) - if err != nil { - return nil, fmt.Errorf("base url: %w", err) - } - moduleURL, err := l.assetPath.Module(pluginJSON, class, files.Base()) - if err != nil { - return nil, fmt.Errorf("module url: %w", err) - } - plugin := &plugins.Plugin{ - JSONData: pluginJSON, - FS: files, - BaseURL: baseURL, - Module: moduleURL, - Class: class, - } - - plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) - if err := l.setImages(plugin); err != nil { - return nil, err - } - - return plugin, nil -} - -func (l *Loader) setImages(p *plugins.Plugin) error { - var err error - for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { - *dst, err = l.assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) - if err != nil { - return fmt.Errorf("logo: %w", err) - } - } - for i := 0; i < len(p.Info.Screenshots); i++ { - screenshot := &p.Info.Screenshots[i] - screenshot.Path, err = l.assetPath.RelativeURL(p, screenshot.Path, "") - if err != nil { - return fmt.Errorf("screenshot %d relative url: %w", i, err) - } - } - return nil -} - -func setDefaultNavURL(p *plugins.Plugin) { - // slugify pages - for _, include := range p.Includes { - if include.Slug == "" { - include.Slug = slugify.Slugify(include.Name) - } - - if !include.DefaultNav { - continue - } - - if include.Type == "page" { - p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) - } - if include.Type == "dashboard" { - dboardURL := include.DashboardURLPath() - if dboardURL == "" { - p.Logger().Warn("Included dashboard is missing a UID field") - continue - } - - p.DefaultNavURL = dboardURL - } - } -} - -func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { - if !parent.IsApp() { - return - } - appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") - child.IncludedInAppID = parent.ID - child.BaseURL = parent.BaseURL - - if parent.IsCorePlugin() { - child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" - } else { - child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" - } -} - -func defaultLogoPath(pluginType plugins.Type) string { - return "public/img/icn-" + string(pluginType) + ".svg" -} - func (l *Loader) PluginErrors() []*plugins.Error { errs := make([]*plugins.Error, 0, len(l.errs)) for _, err := range l.errs { diff --git a/pkg/plugins/manager/loader/loader_test.go b/pkg/plugins/manager/loader/loader_test.go index b93eac7daf0..68f3a3275ab 100644 --- a/pkg/plugins/manager/loader/loader_test.go +++ b/pkg/plugins/manager/loader/loader_test.go @@ -18,10 +18,11 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" - "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/loader/initializer" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" - "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/plugins/manager/sources" "github.com/grafana/grafana/pkg/plugins/pluginscdn" "github.com/grafana/grafana/pkg/services/org" @@ -543,61 +544,6 @@ func TestLoader_Load_CustomSource(t *testing.T) { }) } -func TestLoader_setDefaultNavURL(t *testing.T) { - t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) { - pluginWithDashboard := &plugins.Plugin{ - JSONData: plugins.JSONData{Includes: []*plugins.Includes{ - { - Type: "dashboard", - DefaultNav: true, - UID: "", - }, - }}, - } - logger := log.NewTestLogger() - pluginWithDashboard.SetLogger(logger) - - t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { - setDefaultNavURL(pluginWithDashboard) - require.Equal(t, "", pluginWithDashboard.DefaultNavURL) - require.NotZero(t, logger.WarnLogs.Calls) - require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) - }) - - t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { - pluginWithDashboard.Includes[0].UID = "a1b2c3" - - setDefaultNavURL(pluginWithDashboard) - require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL) - }) - }) - - t.Run("When including a page with DefaultNav: true", func(t *testing.T) { - pluginWithPage := &plugins.Plugin{ - JSONData: plugins.JSONData{Includes: []*plugins.Includes{ - { - Type: "page", - DefaultNav: true, - Slug: "testPage", - }, - }}, - } - - t.Run("Default nav URL is set using slug", func(t *testing.T) { - setDefaultNavURL(pluginWithPage) - require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL) - }) - - t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) { - pluginWithPage.Includes[0].Slug = "" - pluginWithPage.Includes[0].Name = "My Test Page" - - setDefaultNavURL(pluginWithPage) - require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL) - }) - }) -} - func TestLoader_Load_MultiplePlugins(t *testing.T) { parentDir, err := filepath.Abs("../") if err != nil { @@ -1257,13 +1203,19 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { child.Parent = parent t.Run("Load nested External plugins", func(t *testing.T) { - reg := fakes.NewFakePluginRegistry() procPrvdr := fakes.NewFakeBackendProcessProvider() procMgr := fakes.NewFakeProcessManager() l := newLoader(t, &config.Cfg{}, func(l *Loader) { - l.pluginRegistry = reg l.processManager = procMgr l.pluginInitializer = initializer.New(&config.Cfg{}, procPrvdr, fakes.NewFakeLicensingService()) + l.discovery = discovery.New(l.cfg, discovery.Opts{ + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(l.pluginRegistry).Filter(ctx, bundles) + }, + }, + }, + ) }) got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1286,7 +1238,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } - verifyState(t, expected, reg, procPrvdr, procMgr) + verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) t.Run("Load will exclude plugins that already exist", func(t *testing.T) { got, err := l.Load(context.Background(), &fakes.FakePluginSource{ @@ -1308,7 +1260,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(got, expected, compareOpts...)) } - verifyState(t, expected, reg, procPrvdr, procMgr) + verifyState(t, expected, l.pluginRegistry, procPrvdr, procMgr) }) }) @@ -1466,36 +1418,15 @@ func TestLoader_Load_NestedPlugins(t *testing.T) { }) } -func Test_setPathsBasedOnApp(t *testing.T) { - t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { - child := &plugins.Plugin{ - FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), - } - parent := &plugins.Plugin{ - JSONData: plugins.JSONData{ - Type: plugins.TypeApp, - ID: "testdata-app", - }, - Class: plugins.ClassCore, - FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), - BaseURL: "public/app/plugins/app/testdata-app", - } - - configureAppChildPlugin(parent, child) - - require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) - require.Equal(t, "testdata-app", child.IncludedInAppID) - require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) - }) -} - func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Loader { angularInspector, err := angularinspector.NewStaticInspector() + reg := fakes.NewFakePluginRegistry() + assets := assetpath.ProvideService(pluginscdn.ProvideService(cfg)) require.NoError(t, err) - l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), fakes.NewFakePluginRegistry(), + l := New(cfg, &fakes.FakeLicensingService{}, signature.NewUnsignedAuthorizer(cfg), reg, fakes.NewFakeBackendProcessProvider(), fakes.NewFakeProcessManager(), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(cfg)), finder.NewLocalFinder(cfg.DevMode), - signature.ProvideService(statickey.New()), angularInspector, &fakes.FakeOauthService{}) + assets, angularInspector, &fakes.FakeOauthService{}, + discovery.New(cfg, discovery.Opts{}), bootstrap.New(cfg, bootstrap.Opts{})) for _, cb := range cbs { cb(l) @@ -1504,13 +1435,15 @@ func newLoader(t *testing.T, cfg *config.Cfg, cbs ...func(loader *Loader)) *Load return l } -func verifyState(t *testing.T, ps []*plugins.Plugin, reg *fakes.FakePluginRegistry, +func verifyState(t *testing.T, ps []*plugins.Plugin, reg registry.Service, procPrvdr *fakes.FakeBackendProcessProvider, procMngr *fakes.FakeProcessManager) { t.Helper() for _, p := range ps { - if !cmp.Equal(p, reg.Store[p.ID], compareOpts...) { - t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, reg.Store[p.ID], compareOpts...)) + regP, exists := reg.Plugin(context.Background(), p.ID) + require.True(t, exists) + if !cmp.Equal(p, regP, compareOpts...) { + t.Fatalf("Result mismatch (-want +got):\n%s", cmp.Diff(p, regP, compareOpts...)) } if p.Backend { diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index f87061bbee5..9526f49b7f7 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -10,7 +10,6 @@ import ( "github.com/grafana/grafana-azure-sdk-go/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" - "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" @@ -22,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/plugins/manager/client" "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/plugins/manager/loader" + "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" "github.com/grafana/grafana/pkg/plugins/manager/registry" @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/pluginsintegration/config" plicensing "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" @@ -120,10 +121,14 @@ func TestIntegrationPluginManager(t *testing.T) { lic := plicensing.ProvideLicensing(cfg, &licensing.OSSLicensingService{Cfg: cfg}) angularInspector, err := angularinspector.NewStaticInspector() require.NoError(t, err) + + discovery := pipeline.ProvideDiscoveryStage(pCfg, finder.NewLocalFinder(pCfg.DevMode), reg) + bootstrap := pipeline.ProvideBootstrapStage(pCfg, signature.ProvideService(statickey.New()), assetpath.ProvideService(pluginscdn.ProvideService(pCfg))) + l := loader.ProvideService(pCfg, lic, signature.NewUnsignedAuthorizer(pCfg), - reg, provider.ProvideService(coreRegistry), finder.NewLocalFinder(pCfg.DevMode), fakes.NewFakeRoleRegistry(), - assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), signature.ProvideService(statickey.New()), - angularInspector, &fakes.FakeOauthService{}) + reg, provider.ProvideService(coreRegistry), fakes.NewFakeRoleRegistry(), + assetpath.ProvideService(pluginscdn.ProvideService(pCfg)), + angularInspector, &fakes.FakeOauthService{}, discovery, bootstrap) srcs := sources.ProvideService(cfg, pCfg) ps, err := store.ProvideService(reg, srcs, l) require.NoError(t, err) diff --git a/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go new file mode 100644 index 00000000000..e4b8f2a45b8 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/bootstrap.go @@ -0,0 +1,78 @@ +package bootstrap + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/signature" +) + +// Bootstrapper is responsible for the Bootstrap stage of the plugin loader pipeline. +type Bootstrapper interface { + Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) +} + +// ConstructFunc is the function used for the Construct step of the Bootstrap stage. +type ConstructFunc func(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) + +// DecorateFunc is the function used for the Decorate step of the Bootstrap stage. +type DecorateFunc func(ctx context.Context, p *plugins.Plugin) (*plugins.Plugin, error) + +// Bootstrap implements the Bootstrapper interface. +// +// The Bootstrap stage is made up of the following steps (in order): +// - Construct: Create the initial plugin structs based on the plugin(s) found in the Discovery stage. +// - Decorate: Decorate the plugins with additional metadata. +// +// The Construct step is implemented by the ConstructFunc type. +// +// The Decorate step is implemented by the DecorateFunc type. +type Bootstrap struct { + constructStep ConstructFunc + decorateSteps []DecorateFunc + log log.Logger +} + +type Opts struct { + ConstructFunc ConstructFunc + DecorateFuncs []DecorateFunc +} + +// New returns a new Bootstrap stage. +func New(cfg *config.Cfg, opts Opts) *Bootstrap { + if opts.ConstructFunc == nil { + opts.ConstructFunc = DefaultConstructFunc(signature.DefaultCalculator(), assetpath.DefaultService(cfg)) + } + + if len(opts.DecorateFuncs) == 0 { + opts.DecorateFuncs = DefaultDecorateFuncs + } + + return &Bootstrap{ + constructStep: opts.ConstructFunc, + decorateSteps: opts.DecorateFuncs, + log: log.New("plugins.bootstrap"), + } +} + +// Bootstrap will execute the Construct and Decorate steps of the Bootstrap stage. +func (b *Bootstrap) Bootstrap(ctx context.Context, src plugins.PluginSource, found []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + ps, err := b.constructStep(ctx, src, found) + if err != nil { + return nil, err + } + + for _, p := range ps { + for _, decorator := range b.decorateSteps { + p, err = decorator(ctx, p) + if err != nil { + return nil, err + } + } + } + + return ps, nil +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/doc.go b/pkg/plugins/manager/pipeline/bootstrap/doc.go new file mode 100644 index 00000000000..d601296c698 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/doc.go @@ -0,0 +1,6 @@ +// Package bootstrap defines the second stage of the plugin loader pipeline. +// +// The Bootstrap stage must implement the Bootstrapper interface. +// - Bootstrap(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) + +package bootstrap diff --git a/pkg/plugins/manager/pipeline/bootstrap/factory.go b/pkg/plugins/manager/pipeline/bootstrap/factory.go new file mode 100644 index 00000000000..5e179b1f31c --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/factory.go @@ -0,0 +1,76 @@ +package bootstrap + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" +) + +type pluginFactoryFunc func(p plugins.FoundPlugin, pluginClass plugins.Class, sig plugins.Signature) (*plugins.Plugin, error) + +// DefaultPluginFactory is the default plugin factory used by the Construct step of the Bootstrap stage. +// +// It creates the plugin using plugin information found during the Discovery stage and makes use of the assetPath +// service to set the plugin's BaseURL, Module, Logos and Screenshots fields. +type DefaultPluginFactory struct { + assetPath *assetpath.Service +} + +// NewDefaultPluginFactory returns a new DefaultPluginFactory. +func NewDefaultPluginFactory(assetPath *assetpath.Service) *DefaultPluginFactory { + return &DefaultPluginFactory{assetPath: assetPath} +} + +func (f *DefaultPluginFactory) createPlugin(p plugins.FoundPlugin, class plugins.Class, + sig plugins.Signature) (*plugins.Plugin, error) { + baseURL, err := f.assetPath.Base(p.JSONData, class, p.FS.Base()) + if err != nil { + return nil, fmt.Errorf("base url: %w", err) + } + moduleURL, err := f.assetPath.Module(p.JSONData, class, p.FS.Base()) + if err != nil { + return nil, fmt.Errorf("module url: %w", err) + } + + plugin := &plugins.Plugin{ + JSONData: p.JSONData, + FS: p.FS, + BaseURL: baseURL, + Module: moduleURL, + Class: class, + Signature: sig.Status, + SignatureType: sig.Type, + SignatureOrg: sig.SigningOrg, + } + plugin.SetLogger(log.New(fmt.Sprintf("plugin.%s", plugin.ID))) + + if err = setImages(plugin, f.assetPath); err != nil { + return nil, err + } + + return plugin, nil +} + +func setImages(p *plugins.Plugin, assetPath *assetpath.Service) error { + var err error + for _, dst := range []*string{&p.Info.Logos.Small, &p.Info.Logos.Large} { + *dst, err = assetPath.RelativeURL(p, *dst, defaultLogoPath(p.Type)) + if err != nil { + return fmt.Errorf("logo: %w", err) + } + } + for i := 0; i < len(p.Info.Screenshots); i++ { + screenshot := &p.Info.Screenshots[i] + screenshot.Path, err = assetPath.RelativeURL(p, screenshot.Path, "") + if err != nil { + return fmt.Errorf("screenshot %d relative url: %w", i, err) + } + } + return nil +} + +func defaultLogoPath(pluginType plugins.Type) string { + return fmt.Sprintf("public/img/icn-%s.svg", string(pluginType)) +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps.go b/pkg/plugins/manager/pipeline/bootstrap/steps.go new file mode 100644 index 00000000000..4d808b00b73 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/steps.go @@ -0,0 +1,146 @@ +package bootstrap + +import ( + "context" + "path" + "strings" + + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/util" +) + +// DefaultConstructor implements the default ConstructFunc used for the Construct step of the Bootstrap stage. +// +// It uses a pluginFactoryFunc to create plugins and the signatureCalculator to calculate the plugin's signature state. +type DefaultConstructor struct { + pluginFactoryFunc pluginFactoryFunc + signatureCalculator plugins.SignatureCalculator + log log.Logger +} + +// DefaultConstructFunc is the default ConstructFunc used for the Construct step of the Bootstrap stage. +func DefaultConstructFunc(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) ConstructFunc { + return NewDefaultConstructor(signatureCalculator, assetPath).Construct +} + +// DefaultDecorateFuncs are the default DecorateFuncs used for the Decorate step of the Bootstrap stage. +var DefaultDecorateFuncs = []DecorateFunc{ + AliasDecorateFunc, + AppDefaultNavURLDecorateFunc, + AppChildDecorateFunc, +} + +// NewDefaultConstructor returns a new DefaultConstructor. +func NewDefaultConstructor(signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *DefaultConstructor { + return &DefaultConstructor{ + pluginFactoryFunc: NewDefaultPluginFactory(assetPath).createPlugin, + signatureCalculator: signatureCalculator, + log: log.New("plugins.construct"), + } +} + +// Construct will calculate the plugin's signature state and create the plugin using the pluginFactoryFunc. +func (c *DefaultConstructor) Construct(ctx context.Context, src plugins.PluginSource, bundles []*plugins.FoundBundle) ([]*plugins.Plugin, error) { + res := make([]*plugins.Plugin, 0, len(bundles)) + + for _, bundle := range bundles { + sig, err := c.signatureCalculator.Calculate(ctx, src, bundle.Primary) + if err != nil { + c.log.Warn("Could not calculate plugin signature state", "pluginID", bundle.Primary.JSONData.ID, "err", err) + continue + } + plugin, err := c.pluginFactoryFunc(bundle.Primary, src.PluginClass(ctx), sig) + if err != nil { + c.log.Error("Could not create primary plugin base", "pluginID", bundle.Primary.JSONData.ID, "err", err) + continue + } + res = append(res, plugin) + + children := make([]*plugins.Plugin, 0, len(bundle.Children)) + for _, child := range bundle.Children { + cp, err := c.pluginFactoryFunc(*child, plugin.Class, sig) + if err != nil { + c.log.Error("Could not create child plugin base", "pluginID", child.JSONData.ID, "err", err) + continue + } + cp.Parent = plugin + plugin.Children = append(plugin.Children, cp) + + children = append(children, cp) + } + res = append(res, children...) + } + + return res, nil +} + +// AliasDecorateFunc is a DecorateFunc that sets the alias for the plugin. +func AliasDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + switch p.ID { + case "grafana-pyroscope-datasource": // rebranding + p.Alias = "phlare" + case "debug": // panel plugin used for testing + p.Alias = "debugX" + } + return p, nil +} + +// AppDefaultNavURLDecorateFunc is a DecorateFunc that sets the default nav URL for app plugins. +func AppDefaultNavURLDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.IsApp() { + setDefaultNavURL(p) + } + return p, nil +} + +func setDefaultNavURL(p *plugins.Plugin) { + // slugify pages + for _, include := range p.Includes { + if include.Slug == "" { + include.Slug = slugify.Slugify(include.Name) + } + + if !include.DefaultNav { + continue + } + + if include.Type == "page" { + p.DefaultNavURL = path.Join("/plugins/", p.ID, "/page/", include.Slug) + } + if include.Type == "dashboard" { + dboardURL := include.DashboardURLPath() + if dboardURL == "" { + p.Logger().Warn("Included dashboard is missing a UID field") + continue + } + + p.DefaultNavURL = dboardURL + } + } +} + +// AppChildDecorateFunc is a DecorateFunc that configures child plugins of app plugins. +func AppChildDecorateFunc(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) { + if p.Parent != nil && p.Parent.IsApp() { + configureAppChildPlugin(p.Parent, p) + } + return p, nil +} + +func configureAppChildPlugin(parent *plugins.Plugin, child *plugins.Plugin) { + if !parent.IsApp() { + return + } + appSubPath := strings.ReplaceAll(strings.Replace(child.FS.Base(), parent.FS.Base(), "", 1), "\\", "/") + child.IncludedInAppID = parent.ID + child.BaseURL = parent.BaseURL + + if parent.IsCorePlugin() { + child.Module = util.JoinURLFragments("app/plugins/app/"+parent.ID, appSubPath) + "/module" + } else { + child.Module = util.JoinURLFragments("plugins/"+parent.ID, appSubPath) + "/module" + } +} diff --git a/pkg/plugins/manager/pipeline/bootstrap/steps_test.go b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go new file mode 100644 index 00000000000..f22ed0478b1 --- /dev/null +++ b/pkg/plugins/manager/pipeline/bootstrap/steps_test.go @@ -0,0 +1,89 @@ +package bootstrap + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/fakes" +) + +func TestSetDefaultNavURL(t *testing.T) { + t.Run("When including a dashboard with DefaultNav: true", func(t *testing.T) { + pluginWithDashboard := &plugins.Plugin{ + JSONData: plugins.JSONData{Includes: []*plugins.Includes{ + { + Type: "dashboard", + DefaultNav: true, + UID: "", + }, + }}, + } + logger := log.NewTestLogger() + pluginWithDashboard.SetLogger(logger) + + t.Run("Default nav URL is not set if dashboard UID field not is set", func(t *testing.T) { + setDefaultNavURL(pluginWithDashboard) + require.Equal(t, "", pluginWithDashboard.DefaultNavURL) + require.NotZero(t, logger.WarnLogs.Calls) + require.Equal(t, "Included dashboard is missing a UID field", logger.WarnLogs.Message) + }) + + t.Run("Default nav URL is set if dashboard UID field is set", func(t *testing.T) { + pluginWithDashboard.Includes[0].UID = "a1b2c3" + + setDefaultNavURL(pluginWithDashboard) + require.Equal(t, "/d/a1b2c3", pluginWithDashboard.DefaultNavURL) + }) + }) + + t.Run("When including a page with DefaultNav: true", func(t *testing.T) { + pluginWithPage := &plugins.Plugin{ + JSONData: plugins.JSONData{Includes: []*plugins.Includes{ + { + Type: "page", + DefaultNav: true, + Slug: "testPage", + }, + }}, + } + + t.Run("Default nav URL is set using slug", func(t *testing.T) { + setDefaultNavURL(pluginWithPage) + require.Equal(t, "/plugins/page/testPage", pluginWithPage.DefaultNavURL) + }) + + t.Run("Default nav URL is set using slugified Name field if Slug field is empty", func(t *testing.T) { + pluginWithPage.Includes[0].Slug = "" + pluginWithPage.Includes[0].Name = "My Test Page" + + setDefaultNavURL(pluginWithPage) + require.Equal(t, "/plugins/page/my-test-page", pluginWithPage.DefaultNavURL) + }) + }) +} + +func TestSetPathsBasedOnApp(t *testing.T) { + t.Run("When setting paths based on core plugin on Windows", func(t *testing.T) { + child := &plugins.Plugin{ + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app\\datasources\\datasource"), + } + parent := &plugins.Plugin{ + JSONData: plugins.JSONData{ + Type: plugins.TypeApp, + ID: "testdata-app", + }, + Class: plugins.ClassCore, + FS: fakes.NewFakePluginFiles("c:\\grafana\\public\\app\\plugins\\app\\testdata-app"), + BaseURL: "public/app/plugins/app/testdata-app", + } + + configureAppChildPlugin(parent, child) + + require.Equal(t, "app/plugins/app/testdata-app/datasources/datasource/module", child.Module) + require.Equal(t, "testdata-app", child.IncludedInAppID) + require.Equal(t, "public/app/plugins/app/testdata-app", child.BaseURL) + }) +} diff --git a/pkg/plugins/manager/pipeline/discovery/discovery.go b/pkg/plugins/manager/pipeline/discovery/discovery.go new file mode 100644 index 00000000000..f24b2b5bc75 --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/discovery.go @@ -0,0 +1,74 @@ +package discovery + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" +) + +// Discoverer is responsible for the Discovery stage of the plugin loader pipeline. +type Discoverer interface { + Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) +} + +// FindFunc is the function used for the Find step of the Discovery stage. +type FindFunc func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) + +// FindFilterFunc is the function used for the Filter step of the Discovery stage. +type FindFilterFunc func(ctx context.Context, class plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) + +// Discovery implements the Discoverer interface. +// +// The Discovery stage is made up of the following steps (in order): +// - Find: Find plugins (from disk, remote, etc.) +// - Filter: Filter the results based on some criteria. +// +// The Find step is implemented by the FindFunc type. +// +// The Filter step is implemented by the FindFilterFunc type. +type Discovery struct { + findStep FindFunc + findFilterSteps []FindFilterFunc + log log.Logger +} + +type Opts struct { + FindFunc FindFunc + FindFilterFuncs []FindFilterFunc +} + +// New returns a new Discovery stage. +func New(cfg *config.Cfg, opts Opts) *Discovery { + if opts.FindFunc == nil { + opts.FindFunc = DefaultFindFunc(cfg) + } + + if len(opts.FindFilterFuncs) == 0 { + opts.FindFilterFuncs = []FindFilterFunc{} // no filters by default + } + + return &Discovery{ + findStep: opts.FindFunc, + findFilterSteps: opts.FindFilterFuncs, + log: log.New("plugins.discovery"), + } +} + +// Discover will execute the Find and Filter steps of the Discovery stage. +func (d *Discovery) Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + found, err := d.findStep(ctx, src) + if err != nil { + return nil, err + } + + for _, filterStep := range d.findFilterSteps { + found, err = filterStep(ctx, src.PluginClass(ctx), found) + if err != nil { + return nil, err + } + } + + return found, nil +} diff --git a/pkg/plugins/manager/pipeline/discovery/doc.go b/pkg/plugins/manager/pipeline/discovery/doc.go new file mode 100644 index 00000000000..37d91ce8592 --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/doc.go @@ -0,0 +1,6 @@ +// Package discovery defines the first stage of the plugin loader pipeline. + +// The Discovery stage must implement the Discoverer interface. +// - Discover(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) + +package discovery diff --git a/pkg/plugins/manager/pipeline/discovery/steps.go b/pkg/plugins/manager/pipeline/discovery/steps.go new file mode 100644 index 00000000000..a51d2c4c2eb --- /dev/null +++ b/pkg/plugins/manager/pipeline/discovery/steps.go @@ -0,0 +1,55 @@ +package discovery + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +// DefaultFindFunc is the default function used for the Find step of the Discovery stage. It will scan the local +// filesystem for plugins. +func DefaultFindFunc(cfg *config.Cfg) FindFunc { + return finder.NewLocalFinder(cfg.DevMode).Find +} + +// DuplicatePluginValidation is a filter step that will filter out any plugins that are already registered with the +// registry. This includes both the primary plugin and any child plugins, which are matched using the plugin ID field. +type DuplicatePluginValidation struct { + registry registry.Service + log log.Logger +} + +// NewDuplicatePluginFilterStep returns a new DuplicatePluginValidation. +func NewDuplicatePluginFilterStep(registry registry.Service) *DuplicatePluginValidation { + return &DuplicatePluginValidation{ + registry: registry, + log: log.New("plugins.dedupe"), + } +} + +// Filter will filter out any plugins that are already registered with the registry. +func (d *DuplicatePluginValidation) Filter(ctx context.Context, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + res := make([]*plugins.FoundBundle, 0, len(bundles)) + for _, b := range bundles { + _, exists := d.registry.Plugin(ctx, b.Primary.JSONData.ID) + if exists { + d.log.Warn("Skipping loading of plugin as it's a duplicate", "pluginID", b.Primary.JSONData.ID) + continue + } + + for _, child := range b.Children { + _, exists = d.registry.Plugin(ctx, child.JSONData.ID) + if exists { + d.log.Warn("Skipping loading of child plugin as it's a duplicate", "pluginID", child.JSONData.ID) + continue + } + } + res = append(res, b) + } + + return res, nil +} diff --git a/pkg/plugins/manager/pipeline/doc.go b/pkg/plugins/manager/pipeline/doc.go new file mode 100644 index 00000000000..9b04096e158 --- /dev/null +++ b/pkg/plugins/manager/pipeline/doc.go @@ -0,0 +1,11 @@ +// Package pipeline defines a load pipeline for Grafana plugins. +// +// A pipeline is a sequence of stages that are executed in order. Each stage is made up of a series of steps. +// A plugin loader pipeline is defined by the following stages: +// Discovery: Find plugins (e.g. from disk, remote, etc.), and [optionally] filter the results based on some criteria. +// Bootstrap: Create the plugins found in the discovery stage and enrich them with metadata. +// Verification: Verify the plugins based on some criteria (e.g. signature validation, angular detection, etc.) +// Initialization: Initialize the plugin for use (e.g. register with Grafana, etc.) +// Post-Initialization: Perform any post-initialization tasks (e.g. start the backend process, declare RBAC roles etc.) + +package pipeline diff --git a/pkg/plugins/manager/signature/manifest.go b/pkg/plugins/manager/signature/manifest.go index ffbc25230ba..93829a76809 100644 --- a/pkg/plugins/manager/signature/manifest.go +++ b/pkg/plugins/manager/signature/manifest.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/log" + "github.com/grafana/grafana/pkg/plugins/manager/signature/statickey" "github.com/grafana/grafana/pkg/setting" ) @@ -64,9 +65,20 @@ type Signature struct { var _ plugins.SignatureCalculator = &Signature{} func ProvideService(kr plugins.KeyRetriever) *Signature { + return NewCalculator(kr) +} + +func NewCalculator(kr plugins.KeyRetriever) *Signature { return &Signature{ kr: kr, - log: log.New("plugin.signature"), + log: log.New("plugins.signature"), + } +} + +func DefaultCalculator() *Signature { + return &Signature{ + kr: statickey.New(), + log: log.New("plugins.signature"), } } diff --git a/pkg/services/pluginsintegration/pipeline/discovery.go b/pkg/services/pluginsintegration/pipeline/discovery.go new file mode 100644 index 00000000000..c4e29033a7b --- /dev/null +++ b/pkg/services/pluginsintegration/pipeline/discovery.go @@ -0,0 +1,33 @@ +package pipeline + +import ( + "context" + + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/config" + "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" + "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" + "github.com/grafana/grafana/pkg/plugins/manager/registry" +) + +func ProvideDiscoveryStage(cfg *config.Cfg, pluginFinder finder.Finder, pluginRegistry registry.Service) *discovery.Discovery { + return discovery.New(cfg, discovery.Opts{ + FindFunc: func(ctx context.Context, src plugins.PluginSource) ([]*plugins.FoundBundle, error) { + return pluginFinder.Find(ctx, src) + }, + FindFilterFuncs: []discovery.FindFilterFunc{ + func(ctx context.Context, _ plugins.Class, bundles []*plugins.FoundBundle) ([]*plugins.FoundBundle, error) { + return discovery.NewDuplicatePluginFilterStep(pluginRegistry).Filter(ctx, bundles) + }, + }, + }) +} + +func ProvideBootstrapStage(cfg *config.Cfg, signatureCalculator plugins.SignatureCalculator, assetPath *assetpath.Service) *bootstrap.Bootstrap { + return bootstrap.New(cfg, bootstrap.Opts{ + ConstructFunc: bootstrap.DefaultConstructFunc(signatureCalculator, assetPath), + DecorateFuncs: bootstrap.DefaultDecorateFuncs, + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index 554b171338a..9630b8290b6 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -2,6 +2,7 @@ package pluginsintegration import ( "github.com/google/wire" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin" @@ -14,6 +15,8 @@ import ( pAngularInspector "github.com/grafana/grafana/pkg/plugins/manager/loader/angular/angularinspector" "github.com/grafana/grafana/pkg/plugins/manager/loader/assetpath" "github.com/grafana/grafana/pkg/plugins/manager/loader/finder" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/bootstrap" + "github.com/grafana/grafana/pkg/plugins/manager/pipeline/discovery" "github.com/grafana/grafana/pkg/plugins/manager/process" "github.com/grafana/grafana/pkg/plugins/manager/registry" "github.com/grafana/grafana/pkg/plugins/manager/signature" @@ -34,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/keyretriever/dynamic" "github.com/grafana/grafana/pkg/services/pluginsintegration/keystore" "github.com/grafana/grafana/pkg/services/pluginsintegration/licensing" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service" @@ -57,6 +61,11 @@ var WireSet = wire.NewSet( pluginscdn.ProvideService, assetpath.ProvideService, + pipeline.ProvideDiscoveryStage, + wire.Bind(new(discovery.Discoverer), new(*discovery.Discovery)), + pipeline.ProvideBootstrapStage, + wire.Bind(new(bootstrap.Bootstrapper), new(*bootstrap.Bootstrap)), + angularpatternsstore.ProvideService, angulardetectorsprovider.ProvideDynamic, angularinspector.ProvideService, From 143683bd052154488f241b4d4290f9207ab7b326 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Thu, 27 Jul 2023 09:47:04 -0400 Subject: [PATCH 58/64] Alerting: Add more clear error to migration when rule cannot be parsed (#72374) --- pkg/services/sqlstore/migrations/ualert/dash_alert.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/ualert/dash_alert.go b/pkg/services/sqlstore/migrations/ualert/dash_alert.go index f6276507e33..707aea94cee 100644 --- a/pkg/services/sqlstore/migrations/ualert/dash_alert.go +++ b/pkg/services/sqlstore/migrations/ualert/dash_alert.go @@ -55,7 +55,8 @@ func (m *migration) slurpDashAlerts() ([]dashAlert, error) { for i := range dashAlerts { err = json.Unmarshal(dashAlerts[i].Settings, &dashAlerts[i].ParsedSettings) if err != nil { - return nil, err + da := dashAlerts[i] + return nil, fmt.Errorf("failed to parse alert rule ID:%d, name:'%s', orgID:%d: %w", da.Id, da.Name, da.OrgId, err) } } From f63d829bf6e5a4f0488e8ec26d1fc78787e87299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Thu, 27 Jul 2023 16:06:56 +0200 Subject: [PATCH 59/64] loki: tests: better nanosecond representation (#72456) * logs: tests: test for nanosecond-only differences * add nanos --- .../plugins/datasource/loki/sortDataFrame.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/loki/sortDataFrame.test.ts b/public/app/plugins/datasource/loki/sortDataFrame.test.ts index 53fc94fd481..bf35f3be1a3 100644 --- a/public/app/plugins/datasource/loki/sortDataFrame.test.ts +++ b/public/app/plugins/datasource/loki/sortDataFrame.test.ts @@ -9,7 +9,8 @@ const inputFrame: DataFrame = { name: 'time', type: FieldType.time, config: {}, - values: [1005, 1001, 1004, 1002, 1003], + values: [1005, 1001, 1003, 1002, 1003], + nanos: [0, 0, 5, 0, 0], }, { name: 'value', @@ -21,7 +22,7 @@ const inputFrame: DataFrame = { name: 'tsNs', type: FieldType.time, config: {}, - values: [`1005000000`, `1001000000`, `1004000000`, `1002000000`, `1003000000`], + values: [`1005000000`, `1001000000`, `1003000005`, `1002000000`, `1003000000`], }, ], length: 5, @@ -35,9 +36,9 @@ describe('loki sortDataFrame', () => { const lineValues = sortedFrame.fields[1].values; const tsNsValues = sortedFrame.fields[2].values; - expect(timeValues).toEqual([1001, 1002, 1003, 1004, 1005]); + expect(timeValues).toEqual([1001, 1002, 1003, 1003, 1005]); expect(lineValues).toEqual(['line1', 'line2', 'line3', 'line4', 'line5']); - expect(tsNsValues).toEqual([`1001000000`, `1002000000`, `1003000000`, `1004000000`, `1005000000`]); + expect(tsNsValues).toEqual([`1001000000`, `1002000000`, `1003000000`, `1003000005`, `1005000000`]); }); it('sorts a dataframe descending', () => { const sortedFrame = sortDataFrameByTime(inputFrame, SortDirection.Descending); @@ -46,8 +47,8 @@ describe('loki sortDataFrame', () => { const lineValues = sortedFrame.fields[1].values; const tsNsValues = sortedFrame.fields[2].values; - expect(timeValues).toEqual([1005, 1004, 1003, 1002, 1001]); + expect(timeValues).toEqual([1005, 1003, 1003, 1002, 1001]); expect(lineValues).toEqual(['line5', 'line4', 'line3', 'line2', 'line1']); - expect(tsNsValues).toEqual([`1005000000`, `1004000000`, `1003000000`, `1002000000`, `1001000000`]); + expect(tsNsValues).toEqual([`1005000000`, `1003000005`, `1003000000`, `1002000000`, `1001000000`]); }); }); From 70c05fff57473362a352f84a97e06487cd087be1 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 27 Jul 2023 14:27:20 +0000 Subject: [PATCH 60/64] =?UTF-8?q?Chore:=20use=20GITHUB=5FTOKEN=20in=20brea?= =?UTF-8?q?king=20changes=20workflow=20instead=20of=20grot=20=E2=80=A6=20(?= =?UTF-8?q?#72438)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chore: use GITHUB_TOKEN in breaking changes workflow instead of grot token --- .../detect-breaking-changes-report.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/detect-breaking-changes-report.yml b/.github/workflows/detect-breaking-changes-report.yml index 397729aad36..f69a58417f7 100644 --- a/.github/workflows/detect-breaking-changes-report.yml +++ b/.github/workflows/detect-breaking-changes-report.yml @@ -5,6 +5,9 @@ on: workflows: ["Levitate / Detect breaking changes"] types: [completed] +permissions: + pull-requests: write + jobs: notify: name: Report @@ -35,7 +38,7 @@ jobs: run_id: runId, }); const artifact = artifacts.data.artifacts.find(a => a.name === artifactName); - + if (!artifact) { throw new Error(`Could not find artifact ${ artifactName } in workflow (${ runId })`); } @@ -49,11 +52,11 @@ jobs: fs.mkdirSync(artifactFolder, { recursive: true }); fs.writeFileSync(`${ artifactFolder }/${ artifactName }.zip`, Buffer.from(download.data)); - + # Unzip artifact - name: Unzip artifact run: unzip "${ARTIFACT_FOLDER}/${ARTIFACT_NAME}.zip" -d "${ARTIFACT_FOLDER}" - + # Parse the artifact and register fields as step output variables # (All fields in the JSON will be available as ${{ steps.levitate-run.outputs. }} - name: Parsing levitate result @@ -137,7 +140,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.issues.addLabels({ issue_number: process.env.PR_NUMBER, @@ -153,7 +156,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.issues.removeLabel({ issue_number: process.env.PR_NUMBER, @@ -171,7 +174,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.pulls.requestReviewers({ pull_number: process.env.PR_NUMBER, @@ -188,7 +191,7 @@ jobs: env: PR_NUMBER: ${{ steps.levitate-run.outputs.pr_number }} with: - github-token: ${{ secrets.GH_BOT_ACCESS_TOKEN }} + github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.pulls.removeRequestedReviewers({ pull_number: process.env.PR_NUMBER, From 649cd08a19107b4a8fe57c04a779c746c9e3cc58 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Thu, 27 Jul 2023 15:34:27 +0100 Subject: [PATCH 61/64] SAML: Remove mention of config found in inifile (#71837) * remove config found in inifile * remove the text from the provider card * remove description that contained the notion of inifile --- .../features/auth-config/AuthConfigPage.tsx | 5 --- .../auth-config/components/ProviderCard.tsx | 34 ++----------------- public/app/types/configAuth.ts | 1 - 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/public/app/features/auth-config/AuthConfigPage.tsx b/public/app/features/auth-config/AuthConfigPage.tsx index 64e16ff6628..88d51a33a18 100644 --- a/public/app/features/auth-config/AuthConfigPage.tsx +++ b/public/app/features/auth-config/AuthConfigPage.tsx @@ -88,7 +88,6 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet displayName={provider.displayName} authType={provider.type} enabled={providerStatuses[provider.id]?.enabled} - configFoundInIniFile={providerStatuses[provider.id]?.configFoundInIniFile} configPath={provider.configPath} onClick={() => { onProviderCardClick(provider); @@ -103,9 +102,6 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet buttonIcon="plus-circle" buttonLink={getProviderUrl(firstAvailableProvider)} buttonTitle={`Configure ${firstAvailableProvider.type}`} - description={`Important: if you have ${firstAvailableProvider.type} configuration enabled via the .ini file Grafana is using it. - Configuring ${firstAvailableProvider.type} via UI will take precedence over any configuration in the .ini file. - No changes will be written into .ini file.`} onClick={onCTAClick} /> )} @@ -118,7 +114,6 @@ export const AuthConfigPageUnconnected = ({ providerStatuses, isLoading, loadSet displayName={provider.displayName} authType={provider.protocol} enabled={providerStatuses[provider.id]?.enabled} - configFoundInIniFile={providerStatuses[provider.id]?.configFoundInIniFile} configPath={provider.configPath} /> ))} diff --git a/public/app/features/auth-config/components/ProviderCard.tsx b/public/app/features/auth-config/components/ProviderCard.tsx index f15df0783a0..680f51916f0 100644 --- a/public/app/features/auth-config/components/ProviderCard.tsx +++ b/public/app/features/auth-config/components/ProviderCard.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Badge, Card, useStyles2, Icon, Tooltip } from '@grafana/ui'; +import { Badge, Card, useStyles2 } from '@grafana/ui'; import { BASE_PATH } from '../constants'; @@ -12,43 +12,19 @@ type Props = { providerId: string; displayName: string; enabled: boolean; - configFoundInIniFile?: boolean; configPath?: string; authType?: string; badges?: JSX.Element[]; onClick?: () => void; }; -export function ProviderCard({ - providerId, - displayName, - enabled, - configFoundInIniFile, - configPath, - authType, - badges, - onClick, -}: Props) { +export function ProviderCard({ providerId, displayName, enabled, configPath, authType, badges, onClick }: Props) { const styles = useStyles2(getStyles); configPath = BASE_PATH + (configPath || providerId); return ( onClick && onClick()}> {displayName} - {configFoundInIniFile && ( - <> - - - <> - - Configuration found in .ini file - - - - - )}
{authType && } {enabled ? : } @@ -82,11 +58,5 @@ export const getStyles = (theme: GrafanaTheme2) => { color: ${theme.colors.text.primary}; margin: 0; `, - initext: css` - font-size: ${theme.typography.bodySmall.fontSize}; - color: ${theme.colors.text.secondary}; - padding: ${theme.spacing(1)} 0; // Add some padding - max-width: 90%; // Add a max-width to prevent text from stretching too wide - `, }; }; diff --git a/public/app/types/configAuth.ts b/public/app/types/configAuth.ts index a50ad7f48e7..c72e05a038d 100644 --- a/public/app/types/configAuth.ts +++ b/public/app/types/configAuth.ts @@ -11,7 +11,6 @@ export interface AuthConfigState { export interface AuthProviderStatus { enabled: boolean; configured: boolean; - configFoundInIniFile?: boolean; hide?: boolean; } From 427714f8d0784cca873d8ccbab5323dcfca03e61 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Thu, 27 Jul 2023 13:11:15 -0300 Subject: [PATCH 62/64] Export: Remove DS input when dashboard is imported with a lib panel that already exists (#69412) --- .betterer.results | 11 +- pkg/services/libraryelements/api.go | 2 +- pkg/services/libraryelements/database.go | 30 +- .../libraryelements/libraryelements.go | 22 + .../DashExportModal/DashboardExporter.test.ts | 4 +- .../DashExportModal/DashboardExporter.ts | 40 +- .../components/ImportDashboardForm.tsx | 1 + .../manage-dashboards/state/actions.test.ts | 654 +++++++++++++++++- .../manage-dashboards/state/actions.ts | 186 +++-- .../manage-dashboards/state/reducers.ts | 3 +- .../app/features/manage-dashboards/types.ts | 6 + 11 files changed, 878 insertions(+), 81 deletions(-) diff --git a/.betterer.results b/.betterer.results index 353577392c5..63208ca90b0 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2731,10 +2731,10 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"], @@ -2746,8 +2746,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "16"], [0, 0, 0, "Unexpected any. Specify a different type.", "17"], [0, 0, 0, "Unexpected any. Specify a different type.", "18"], - [0, 0, 0, "Unexpected any. Specify a different type.", "19"], - [0, 0, 0, "Unexpected any. Specify a different type.", "20"] + [0, 0, 0, "Unexpected any. Specify a different type.", "19"] ], "public/app/features/manage-dashboards/state/reducers.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/pkg/services/libraryelements/api.go b/pkg/services/libraryelements/api.go index 95bbcb3cad4..00dc8248459 100644 --- a/pkg/services/libraryelements/api.go +++ b/pkg/services/libraryelements/api.go @@ -56,7 +56,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon cmd.FolderID = folder.ID } } - element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd) + element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd) if err != nil { return toLibraryElementError(err, "Failed to create library element") } diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 86ed8ae6e75..2d8bff6bea1 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -120,12 +120,22 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn return model.LibraryElementDTO{}, model.ErrLibraryElementUIDTooLong } } + + updatedModel := cmd.Model + var err error + if cmd.Kind == int64(model.PanelElement) { + updatedModel, err = l.addUidToLibraryPanel(cmd.Model, createUID) + if err != nil { + return model.LibraryElementDTO{}, err + } + } + element := model.LibraryElement{ OrgID: signedInUser.OrgID, FolderID: cmd.FolderID, UID: createUID, Name: cmd.Name, - Model: cmd.Model, + Model: updatedModel, Version: 1, Kind: cmd.Kind, @@ -140,7 +150,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn return model.LibraryElementDTO{}, err } - err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { + err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error { if err := l.requireEditPermissionsOnFolder(c, signedInUser, cmd.FolderID); err != nil { return err } @@ -228,7 +238,7 @@ func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedIn } // getLibraryElements gets a Library Element where param == value -func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) { +func (l *LibraryElementService) getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signedInUser *user.SignedInUser, params []Pair, features featuremgmt.FeatureToggles, cmd model.GetLibraryElementCommand) ([]model.LibraryElementDTO, error) { libraryElements := make([]model.LibraryElementWithMeta, 0) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() @@ -267,6 +277,14 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed leDtos := make([]model.LibraryElementDTO, len(libraryElements)) for i, libraryElement := range libraryElements { + var updatedModel json.RawMessage + if libraryElement.Kind == int64(model.PanelElement) { + updatedModel, err = l.addUidToLibraryPanel(libraryElement.Model, libraryElement.UID) + if err != nil { + return []model.LibraryElementDTO{}, err + } + } + leDtos[i] = model.LibraryElementDTO{ ID: libraryElement.ID, OrgID: libraryElement.OrgID, @@ -277,7 +295,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed Kind: libraryElement.Kind, Type: libraryElement.Type, Description: libraryElement.Description, - Model: libraryElement.Model, + Model: updatedModel, Version: libraryElement.Version, Meta: model.LibraryElementDTOMeta{ FolderName: libraryElement.FolderName, @@ -304,7 +322,7 @@ func getLibraryElements(c context.Context, store db.DB, cfg *setting.Cfg, signed // getLibraryElementByUid gets a Library Element by uid. func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signedInUser *user.SignedInUser, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) { - libraryElements, err := getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd) + libraryElements, err := l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{key: "org_id", value: signedInUser.OrgID}, {key: "uid", value: cmd.UID}}, l.features, cmd) if err != nil { return model.LibraryElementDTO{}, err } @@ -317,7 +335,7 @@ func (l *LibraryElementService) getLibraryElementByUid(c context.Context, signed // getLibraryElementByName gets a Library Element by name. func (l *LibraryElementService) getLibraryElementsByName(c context.Context, signedInUser *user.SignedInUser, name string) ([]model.LibraryElementDTO, error) { - return getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features, + return l.getLibraryElements(c, l.SQLStore, l.Cfg, signedInUser, []Pair{{"org_id", signedInUser.OrgID}, {"name", name}}, l.features, model.GetLibraryElementCommand{ FolderName: dashboards.RootFolderName, }) diff --git a/pkg/services/libraryelements/libraryelements.go b/pkg/services/libraryelements/libraryelements.go index 3a0c4aa1e2b..4abc2666547 100644 --- a/pkg/services/libraryelements/libraryelements.go +++ b/pkg/services/libraryelements/libraryelements.go @@ -2,6 +2,7 @@ package libraryelements import ( "context" + "encoding/json" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" @@ -75,3 +76,24 @@ func (l *LibraryElementService) DisconnectElementsFromDashboard(c context.Contex func (l *LibraryElementService) DeleteLibraryElementsInFolder(c context.Context, signedInUser *user.SignedInUser, folderUID string) error { return l.deleteLibraryElementsInFolderUID(c, signedInUser, folderUID) } + +func (l *LibraryElementService) addUidToLibraryPanel(model []byte, newUid string) (json.RawMessage, error) { + var modelMap map[string]interface{} + err := json.Unmarshal(model, &modelMap) + if err != nil { + return nil, err + } + + if libraryPanel, ok := modelMap["libraryPanel"].(map[string]interface{}); ok { + if uid, ok := libraryPanel["uid"]; ok && uid == "" { + libraryPanel["uid"] = newUid + } + } + + updatedModel, err := json.Marshal(modelMap) + if err != nil { + return nil, err + } + + return updatedModel, nil +} diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index b84ac5caefb..10b4448d88f 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -164,8 +164,8 @@ it('replaces datasource ref in library panel', async () => { if ('error' in exported) { throw new Error('error should not be returned when making exportable json'); } - expect(exported.__elements['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}'); - expect(exported.__inputs[0].name).toBe('DS_GFDB'); + expect(exported.__elements!['c46a6b49-de40-43b3-982c-1b5e1ec084a4'].model.datasource.uid).toBe('${DS_GFDB}'); + expect(exported.__inputs![0].name).toBe('DS_GFDB'); }); it('If a panel queries has no datasource prop ignore it', async () => { diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 96f9ce2fa69..c57069f339d 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -13,12 +13,21 @@ import { VariableOption, VariableRefresh } from '../../../variables/types'; import { DashboardModel } from '../../state/DashboardModel'; import { GridPos } from '../../state/PanelModel'; -interface Input { +export interface InputUsage { + libraryPanels?: LibraryPanel[]; +} + +export interface LibraryPanel { + name: string; + uid: string; +} +export interface Input { name: string; type: string; label: string; value: any; description: string; + usage?: InputUsage; } interface Requires { @@ -30,20 +39,17 @@ interface Requires { }; } -interface ExternalDashboard { - __inputs: Input[]; - __elements: Record; - __requires: Array; +export interface ExternalDashboard { + __inputs?: Input[]; + __elements?: Record; + __requires?: Array; panels: Array; } interface PanelWithExportableLibraryPanel { gridPos: GridPos; id: number; - libraryPanel: { - name: string; - uid: string; - }; + libraryPanel: LibraryPanel; } function isExportableLibraryPanel(p: any): p is PanelWithExportableLibraryPanel { @@ -58,6 +64,7 @@ interface DataSources { type: string; pluginId: string; pluginName: string; + usage?: InputUsage; }; } @@ -132,7 +139,10 @@ export class DashboardExporter { return; } - const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); + const libraryPanel = obj.libraryPanel; + const libraryPanelSuffix = !!libraryPanel ? '-for-library-panel' : ''; + let refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase() + libraryPanelSuffix.toUpperCase(); + datasources[refName] = { name: refName, label: ds.name, @@ -140,8 +150,18 @@ export class DashboardExporter { type: 'datasource', pluginId: ds.meta?.id, pluginName: ds.meta?.name, + usage: datasources[refName]?.usage, }; + if (!!libraryPanel) { + const libPanels = datasources[refName]?.usage?.libraryPanels || []; + libPanels.push({ name: libraryPanel.name, uid: libraryPanel.uid }); + + datasources[refName].usage = { + libraryPanels: libPanels, + }; + } + obj.datasource = { type: ds.meta.id, uid: '${' + refName + '}' }; }); }; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx index 61a1e185d36..e76d552b614 100644 --- a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -118,6 +118,7 @@ export const ImportDashboardForm = ({ return ( { it('Should send data source uid', async () => { const form: ImportDashboardDTO = { @@ -107,3 +117,643 @@ describe('validateDashboardJson', () => { expect(validateDashboardJsonNotValid).toBe('Not valid JSON'); }); }); + +describe('processDashboard', () => { + const panel = new PanelModel({ + datasource: { + type: 'mysql', + uid: '${DS_GDEV-MYSQL}', + }, + }); + + const panelWithLibPanel = { + gridPos: { + h: 8, + w: 12, + x: 0, + y: 8, + }, + id: 3, + libraryPanel: { + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + name: 'another prom lib panel', + }, + }; + const libPanel = { + 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90': { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + kind: 1, + model: { + datasource: { + type: 'prometheus', + uid: '${DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL}', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + libraryPanel: { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_bucket', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + }, + }; + + const panelWithSecondLibPanel = { + gridPos: { + h: 8, + w: 12, + x: 0, + y: 16, + }, + id: 1, + libraryPanel: { + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + }, + }; + const secondLibPanel = { + 'c46a6b49-de40-43b3-982c-1b5e1ec084a4': { + name: 'Testing lib panel', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + kind: 1, + model: { + datasource: { + type: 'prometheus', + uid: '${DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL}', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + libraryPanel: { + name: 'Testing lib panel', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_count', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + }, + }; + + const importedJson: DashboardJson = { + ...defaultDashboard, + __inputs: [ + { + name: 'DS_GDEV-MYSQL', + label: 'gdev-mysql', + description: '', + type: 'datasource', + value: '', + }, + { + name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL', + label: 'gdev-prometheus', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + ], + }, + }, + ], + __elements: { + ...libPanel, + }, + __requires: [ + { + type: 'grafana', + id: 'grafana', + name: 'Grafana', + version: '10.1.0-pre', + }, + { + type: 'datasource', + id: 'mysql', + name: 'MySQL', + version: '1.0.0', + }, + { + type: 'datasource', + id: 'prometheus', + name: 'Prometheus', + version: '1.0.0', + }, + { + type: 'panel', + id: 'table', + name: 'Table', + version: '', + }, + ], + panels: [], + }; + + it("Should return 2 inputs, 1 for library panel because it's used for 2 panels", async () => { + mocks.getLibraryPanel.mockImplementation(() => { + throw { status: 404 }; + }); + const importDashboardState = initialImportDashboardState; + const dashboardJson: DashboardJson = { + ...importedJson, + panels: [panel, panelWithLibPanel, panelWithLibPanel], + }; + const libPanelInputs = await getLibraryPanelInputs(dashboardJson); + const newDashboardState = { + ...importDashboardState, + inputs: { + ...importDashboardState.inputs, + libraryPanels: libPanelInputs!, + }, + }; + + const processedDashboard = processDashboard(dashboardJson, newDashboardState); + const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels); + expect(processedDashboard.__inputs).toHaveLength(2); + expect(dsInputsForLibPanels).toHaveLength(1); + }); + it('Should return 3 inputs, 2 for library panels', async () => { + mocks.getLibraryPanel.mockImplementation(() => { + throw { status: 404 }; + }); + const importDashboardState = initialImportDashboardState; + const dashboardJson: DashboardJson = { + ...importedJson, + __inputs: [ + { + name: 'DS_GDEV-MYSQL', + label: 'gdev-mysql', + description: '', + type: 'datasource', + value: '', + }, + { + name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL', + label: 'gdev-prometheus', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + ], + }, + }, + { + name: 'DS_GDEV-MYSQL-FOR-LIBRARY-PANEL', + label: 'gdev-mysql-2', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + }, + ], + }, + }, + ], + __elements: { + ...libPanel, + ...secondLibPanel, + }, + panels: [panel, panelWithLibPanel, panelWithSecondLibPanel], + }; + const libPanelInputs = await getLibraryPanelInputs(dashboardJson); + const newDashboardState = { + ...importDashboardState, + inputs: { + ...importDashboardState.inputs, + libraryPanels: libPanelInputs!, + }, + }; + + const processedDashboard = processDashboard(dashboardJson, newDashboardState); + const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels); + expect(processedDashboard.__inputs).toHaveLength(3); + expect(dsInputsForLibPanels).toHaveLength(2); + }); + + it('Should return 1 input, since library panels already exist in the instance', async () => { + const getLibPanelFirstRS: LibraryElementDTO = { + folderUid: '', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + name: 'another prom lib panel', + type: 'timeseries', + description: '', + model: { + transparent: false, + transformations: [], + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.PaletteClassic, + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_bucket', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + version: 1, + }; + + const getLibPanelSecondRS: LibraryElementDTO = { + folderUid: '', + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + type: 'timeseries', + description: '', + model: { + transparent: false, + transformations: [], + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + description: '', + fieldConfig: { + defaults: { + color: { + mode: FieldColorModeId.PaletteClassic, + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 0, + gradientMode: 'none', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 80, + }, + ], + }, + }, + overrides: [], + }, + options: { + legend: { + calcs: [], + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + targets: [ + { + datasource: { + type: 'prometheus', + uid: 'gdev-prometheus', + }, + editorMode: 'builder', + expr: 'access_evaluation_duration_count', + instant: false, + range: true, + refId: 'A', + }, + ], + title: 'Panel Title', + type: 'timeseries', + }, + version: 1, + }; + mocks.getLibraryPanel + .mockReturnValueOnce(Promise.resolve(getLibPanelFirstRS)) + .mockReturnValueOnce(Promise.resolve(getLibPanelSecondRS)); + + const importDashboardState = initialImportDashboardState; + const dashboardJson: DashboardJson = { + ...importedJson, + __inputs: [ + { + name: 'DS_GDEV-MYSQL', + label: 'gdev-mysql', + description: '', + type: 'datasource', + value: '', + }, + { + name: 'DS_GDEV-PROMETHEUS-FOR-LIBRARY-PANEL', + label: 'gdev-prometheus', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + name: 'another prom lib panel', + uid: 'a0379b21-fa20-4313-bf12-d7fd7ceb6f90', + }, + ], + }, + }, + { + name: 'DS_GDEV-MYSQL-FOR-LIBRARY-PANEL', + label: 'gdev-mysql-2', + description: '', + type: 'datasource', + value: '', + usage: { + libraryPanels: [ + { + uid: 'c46a6b49-de40-43b3-982c-1b5e1ec084a4', + name: 'Testing lib panel', + }, + ], + }, + }, + ], + __elements: { + ...libPanel, + ...secondLibPanel, + }, + panels: [panel, panelWithLibPanel, panelWithSecondLibPanel], + }; + const libPanelInputs = await getLibraryPanelInputs(dashboardJson); + const newDashboardState = { + ...importDashboardState, + inputs: { + ...importDashboardState.inputs, + libraryPanels: libPanelInputs!, + }, + }; + + const processedDashboard = processDashboard(dashboardJson, newDashboardState); + const dsInputsForLibPanels = processedDashboard.__inputs!.filter((input) => !!input.usage?.libraryPanels); + expect(processedDashboard.__inputs).toHaveLength(1); + expect(dsInputsForLibPanels).toHaveLength(0); + }); +}); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index c244a5193b8..5474afacf21 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,22 +1,28 @@ import { DataSourceInstanceSettings, locationUtil } from '@grafana/data'; -import { getDataSourceSrv, locationService, getBackendSrv, isFetchError } from '@grafana/runtime'; +import { getBackendSrv, getDataSourceSrv, isFetchError, locationService } from '@grafana/runtime'; import { notifyApp } from 'app/core/actions'; import { createErrorNotification } from 'app/core/copy/appNotification'; import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DashboardDTO, FolderInfo, PermissionLevelString, SearchQueryType, ThunkResult } from 'app/types'; -import { LibraryElementExport } from '../../dashboard/components/DashExportModal/DashboardExporter'; +import { + Input, + InputUsage, + LibraryElementExport, + LibraryPanel, +} from '../../dashboard/components/DashExportModal/DashboardExporter'; import { getLibraryPanel } from '../../library-panels/state/api'; import { LibraryElementDTO, LibraryElementKind } from '../../library-panels/types'; import { DashboardSearchHit } from '../../search/types'; -import { DeleteDashboardResponse } from '../types'; +import { DashboardJson, DeleteDashboardResponse } from '../types'; import { clearDashboard, fetchDashboard, fetchFailed, ImportDashboardDTO, + ImportDashboardState, InputType, LibraryPanelInput, LibraryPanelInputState, @@ -31,9 +37,9 @@ export function fetchGcomDashboard(id: string): ThunkResult { try { dispatch(fetchDashboard()); const dashboard = await getBackendSrv().get(`/api/gnet/dashboards/${id}`); - dispatch(setGcomDashboard(dashboard)); - dispatch(processInputs(dashboard.json)); - dispatch(processElements(dashboard.json)); + await dispatch(processElements(dashboard.json)); + await dispatch(processGcomDashboard(dashboard)); + dispatch(processInputs()); } catch (error) { dispatch(fetchFailed()); if (isFetchError(error)) { @@ -45,17 +51,66 @@ export function fetchGcomDashboard(id: string): ThunkResult { export function importDashboardJson(dashboard: any): ThunkResult { return async (dispatch) => { - dispatch(setJsonDashboard(dashboard)); - dispatch(processInputs(dashboard)); - dispatch(processElements(dashboard)); + await dispatch(processElements(dashboard)); + await dispatch(processJsonDashboard(dashboard)); + dispatch(processInputs()); }; } -function processInputs(dashboardJson: any): ThunkResult { - return (dispatch) => { - if (dashboardJson && dashboardJson.__inputs) { +const getNewLibraryPanelsByInput = (input: Input, state: ImportDashboardState): LibraryPanel[] | undefined => { + return input?.usage?.libraryPanels?.filter((usageLibPanel) => + state.inputs.libraryPanels.some( + (libPanel) => libPanel.state !== LibraryPanelInputState.Exists && libPanel.model.uid === usageLibPanel.uid + ) + ); +}; + +export function processDashboard(dashboardJson: DashboardJson, state: ImportDashboardState): DashboardJson { + let inputs = dashboardJson.__inputs; + if (!!state.inputs.libraryPanels?.length) { + const filteredUsedInputs: Input[] = []; + dashboardJson.__inputs?.forEach((input: Input) => { + if (!input?.usage?.libraryPanels) { + filteredUsedInputs.push(input); + return; + } + + const newLibraryPanels = getNewLibraryPanelsByInput(input, state); + input.usage = { libraryPanels: newLibraryPanels }; + + const isInputBeingUsedByANewLibraryPanel = !!newLibraryPanels?.length; + if (isInputBeingUsedByANewLibraryPanel) { + filteredUsedInputs.push(input); + } + }); + inputs = filteredUsedInputs; + } + + return { ...dashboardJson, __inputs: inputs }; +} + +function processGcomDashboard(dashboard: { json: DashboardJson }): ThunkResult { + return (dispatch, getState) => { + const state = getState().importDashboard; + const dashboardJson = processDashboard(dashboard.json, state); + dispatch(setGcomDashboard({ ...dashboard, json: dashboardJson })); + }; +} + +function processJsonDashboard(dashboardJson: DashboardJson): ThunkResult { + return (dispatch, getState) => { + const state = getState().importDashboard; + const dashboard = processDashboard(dashboardJson, state); + dispatch(setJsonDashboard(dashboard)); + }; +} + +function processInputs(): ThunkResult { + return (dispatch, getState) => { + const dashboard = getState().importDashboard.dashboard; + if (dashboard && dashboard.__inputs) { const inputs: any[] = []; - dashboardJson.__inputs.forEach((input: any) => { + dashboard.__inputs.forEach((input: any) => { const inputModel: any = { name: input.name, label: input.label, @@ -66,6 +121,8 @@ function processInputs(dashboardJson: any): ThunkResult { options: [], }; + inputModel.description = getDataSourceDescription(input); + if (input.type === InputType.DataSource) { getDataSourceOptions(input, inputModel); } else if (!inputModel.info) { @@ -81,50 +138,57 @@ function processInputs(dashboardJson: any): ThunkResult { function processElements(dashboardJson?: { __elements?: Record }): ThunkResult { return async function (dispatch) { - if (!dashboardJson || !dashboardJson.__elements) { - return; - } - - const libraryPanelInputs: LibraryPanelInput[] = []; - - for (const element of Object.values(dashboardJson.__elements)) { - if (element.kind !== LibraryElementKind.Panel) { - continue; - } - - const model = element.model; - const { type, description } = model; - const { uid, name } = element; - const input: LibraryPanelInput = { - model: { - model, - uid, - name, - version: 0, - type, - kind: LibraryElementKind.Panel, - description, - } as LibraryElementDTO, - state: LibraryPanelInputState.New, - }; - - try { - const panelInDb = await getLibraryPanel(uid, true); - input.state = LibraryPanelInputState.Exists; - input.model = panelInDb; - } catch (e: any) { - if (e.status !== 404) { - throw e; - } - } - - libraryPanelInputs.push(input); - } - + const libraryPanelInputs = await getLibraryPanelInputs(dashboardJson); dispatch(setLibraryPanelInputs(libraryPanelInputs)); }; } +export async function getLibraryPanelInputs(dashboardJson?: { + __elements?: Record; +}): Promise { + if (!dashboardJson || !dashboardJson.__elements) { + return []; + } + + const libraryPanelInputs: LibraryPanelInput[] = []; + + for (const element of Object.values(dashboardJson.__elements)) { + if (element.kind !== LibraryElementKind.Panel) { + continue; + } + + const model = element.model; + const { type, description } = model; + const { uid, name } = element; + const input: LibraryPanelInput = { + model: { + model, + uid, + name, + version: 0, + type, + kind: LibraryElementKind.Panel, + description, + } as LibraryElementDTO, + state: LibraryPanelInputState.New, + }; + + try { + const panelInDb = await getLibraryPanel(uid, true); + input.state = LibraryPanelInputState.Exists; + input.model = panelInDb; + } catch (e: any) { + if (e.status !== 404) { + throw e; + } + } + + libraryPanelInputs.push(input); + } + + return libraryPanelInputs; +} + export function clearLoadedDashboard(): ThunkResult { return (dispatch) => { dispatch(clearDashboard()); @@ -182,6 +246,22 @@ const getDataSourceOptions = (input: { pluginId: string; pluginName: string }, i } }; +const getDataSourceDescription = (input: { usage?: InputUsage }): string | undefined => { + if (!input.usage) { + return undefined; + } + + if (input.usage.libraryPanels) { + const libPanelNames = input.usage.libraryPanels.reduce( + (acc: string, libPanel, index) => (index === 0 ? libPanel.name : `${acc}, ${libPanel.name}`), + '' + ); + return `List of affected library panels: ${libPanelNames}`; + } + + return undefined; +}; + export async function moveFolders(folderUIDs: string[], toFolder: FolderInfo) { const result = { totalCount: folderUIDs.length, diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 97f05697fa4..bea14221a51 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -34,6 +34,7 @@ export enum LibraryPanelInputState { export interface DashboardInput { name: string; label: string; + description?: string; info: string; value: string; type: InputType; @@ -100,7 +101,7 @@ const importDashboardSlice = createSlice({ state.inputs = { dataSources: action.payload.filter((p) => p.type === InputType.DataSource), constants: action.payload.filter((p) => p.type === InputType.Constant), - libraryPanels: [], + libraryPanels: state.inputs.libraryPanels || [], }; }, setLibraryPanelInputs: (state: Draft, action: PayloadAction) => { diff --git a/public/app/features/manage-dashboards/types.ts b/public/app/features/manage-dashboards/types.ts index 0b24c223403..48bd056afb7 100644 --- a/public/app/features/manage-dashboards/types.ts +++ b/public/app/features/manage-dashboards/types.ts @@ -1,3 +1,7 @@ +import { Dashboard } from '@grafana/schema/src/veneer/dashboard.types'; + +import { ExternalDashboard } from '../dashboard/components/DashExportModal/DashboardExporter'; + export interface Snapshot { created: string; expires: string; @@ -36,3 +40,5 @@ export interface PublicDashboardListResponse { export interface PublicDashboardListWithPagination extends PublicDashboardListWithPaginationResponse { totalPages: number; } + +export type DashboardJson = ExternalDashboard & Omit; From f3b6e7d7eb02c328986627bb04281bf0370ff8d8 Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Thu, 27 Jul 2023 09:31:03 -0700 Subject: [PATCH 63/64] Trace to logs: Add service name and namespace to default tags (#71776) * Add service name and namespace to default trace to logs tags * Add deployment.environment. Update docs * Revert metrics query tags type --- docs/sources/datasources/jaeger/_index.md | 20 +++++++-------- docs/sources/datasources/tempo/_index.md | 20 +++++++-------- docs/sources/datasources/zipkin/_index.md | 20 +++++++-------- .../TraceToLogs/TagMappingInput.tsx | 6 +++-- .../TraceToLogs/TraceToLogsSettings.tsx | 11 +++++--- .../explore/TraceView/createSpanLink.test.ts | 25 ++++++++++--------- .../explore/TraceView/createSpanLink.tsx | 20 ++++++++++++--- 7 files changed, 71 insertions(+), 51 deletions(-) diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 96d493bd9d6..e5ecfc09ada 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -104,16 +104,16 @@ To use a variable you need to wrap it in `${}`. For example: `${__span.name}`. The following table describes the ways in which you can configure your trace to logs settings: -| Setting name | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | -| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | -| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | -| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | -| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | +| Setting name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | +| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | +| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | +| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`, `deployment.environment`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | +| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ### Trace to metrics diff --git a/docs/sources/datasources/tempo/_index.md b/docs/sources/datasources/tempo/_index.md index 4f3ac4931d9..c10c18f00b2 100644 --- a/docs/sources/datasources/tempo/_index.md +++ b/docs/sources/datasources/tempo/_index.md @@ -104,16 +104,16 @@ To use a variable you need to wrap it in `${}`. For example `${__span.name}`. The following table describes the ways in which you can configure your trace to logs settings: -| Setting name | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | -| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | -| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | -| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | -| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | +| Setting name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | +| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | +| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | +| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`, `deployment.environment`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | +| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ### Trace to metrics diff --git a/docs/sources/datasources/zipkin/_index.md b/docs/sources/datasources/zipkin/_index.md index 993aa73ece2..cf7633a7954 100644 --- a/docs/sources/datasources/zipkin/_index.md +++ b/docs/sources/datasources/zipkin/_index.md @@ -102,16 +102,16 @@ To use a variable you need to wrap it in `${}`. For example `${__span.name}`. The following table describes the ways in which you can configure your trace to logs settings: -| Setting name | Description | -| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | -| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | -| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | -| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | -| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | -| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | -| **Use custom query** | Toggles use of custom query with interpolation. | -| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | +| Setting name | Description | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. | +| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default: `0`. | +| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default: `0`. | +| **Tags** | Defines the tags to use in the logs query. Default: `cluster`, `hostname`, `namespace`, `pod`, `service.name`, `service.namespace`, `deployment.environment`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example, map `http.status` to `http_status`. | +| **Filter by trace ID** | Toggles whether to append the trace ID to the logs query. | +| **Filter by span ID** | Toggles whether to append the span ID to the logs query. | +| **Use custom query** | Toggles use of custom query with interpolation. | +| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. | ### Trace to metrics diff --git a/public/app/core/components/TraceToLogs/TagMappingInput.tsx b/public/app/core/components/TraceToLogs/TagMappingInput.tsx index 8757260a45a..3b99a5b26c2 100644 --- a/public/app/core/components/TraceToLogs/TagMappingInput.tsx +++ b/public/app/core/components/TraceToLogs/TagMappingInput.tsx @@ -4,9 +4,11 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SegmentInput, useStyles2, InlineLabel, Icon } from '@grafana/ui'; +import { TraceToLogsTag } from './TraceToLogsSettings'; + interface Props { - values: Array<{ key: string; value?: string }>; - onChange: (values: Array<{ key: string; value?: string }>) => void; + values: TraceToLogsTag[]; + onChange: (values: TraceToLogsTag[]) => void; id?: string; } diff --git a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx index ddf806cd45f..e5c01230233 100644 --- a/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx +++ b/public/app/core/components/TraceToLogs/TraceToLogsSettings.tsx @@ -11,11 +11,16 @@ import { IntervalInput } from '../IntervalInput/IntervalInput'; import { TagMappingInput } from './TagMappingInput'; +export interface TraceToLogsTag { + key: string; + value?: string; +} + // @deprecated use getTraceToLogsOptions to get the v2 version of this config from jsonData export interface TraceToLogsOptions { datasourceUid?: string; tags?: string[]; - mappedTags?: Array<{ key: string; value?: string }>; + mappedTags?: TraceToLogsTag[]; mapTagNamesEnabled?: boolean; spanStartTimeShift?: string; spanEndTimeShift?: string; @@ -26,7 +31,7 @@ export interface TraceToLogsOptions { export interface TraceToLogsOptionsV2 { datasourceUid?: string; - tags?: Array<{ key: string; value?: string }>; + tags?: TraceToLogsTag[]; spanStartTimeShift?: string; spanEndTimeShift?: string; filterByTraceID?: boolean; @@ -151,7 +156,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) { diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index d9d9c35c55d..7fe502a3fb4 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -61,7 +61,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1_uid","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\", service_namespace=\\"namespace1\\"}","refId":""}]}' )}` ); }); @@ -163,7 +163,7 @@ describe('createSpanLinkFactory', () => { datasource: 'loki1_uid', queries: [ { - expr: '{cluster="cluster1", hostname="hostname1"} |="7946b05c2e2e4e5a" |="6605c7b08e715d6c"', + expr: '{cluster="cluster1", hostname="hostname1", service_namespace="namespace1"} |="7946b05c2e2e4e5a" |="6605c7b08e715d6c"', refId: '', }, ], @@ -265,10 +265,7 @@ describe('createSpanLinkFactory', () => { createTraceSpan({ process: { serviceName: 'service', - tags: [ - { key: 'service.name', value: 'serviceName' }, - { key: 'k8s.pod.name', value: 'podName' }, - ], + tags: [{ key: 'k8s.pod.name', value: 'podName' }], }, }) ); @@ -351,7 +348,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"splunkUID","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" service_namespace=\\"namespace1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}]}' )}` ); }); @@ -692,7 +689,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( - `datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]` + `datasource":"${searchUID}","queries":[{"query":"cluster:\\"cluster1\\" AND hostname:\\"hostname1\\" AND service_namespace:\\"namespace1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]` ); }); @@ -731,7 +728,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` + `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster:\\"cluster1\\" AND hostname:\\"hostname1\\" AND service_namespace:\\"namespace1\\"","refId":"","metrics":[{"id":"1","type":"logs"}]}]}` )}` ); }); @@ -882,7 +879,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef).toBeDefined(); expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(decodeURIComponent(linkDef!.href)).toContain( - `datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]` + `datasource":"${searchUID}","queries":[{"query":"cluster=\\"cluster1\\" AND hostname=\\"hostname1\\" AND service_namespace=\\"namespace1\\"","refId":""}]` ); }); @@ -921,7 +918,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\"","refId":""}]}` + `{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"${searchUID}","queries":[{"query":"\\"6605c7b08e715d6c\\" AND \\"7946b05c2e2e4e5a\\" AND cluster=\\"cluster1\\" AND hostname=\\"hostname1\\" AND service_namespace=\\"namespace1\\"","refId":""}]}` )}` ); }); @@ -1145,7 +1142,7 @@ describe('createSpanLinkFactory', () => { expect(linkDef?.type).toBe(SpanLinkType.Logs); expect(linkDef!.href).toBe( `/explore?left=${encodeURIComponent( - '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' + '{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"falconLogScaleUID","queries":[{"lsql":"cluster=\\"cluster1\\" OR hostname=\\"hostname1\\" OR service_namespace=\\"namespace1\\" or \\"7946b05c2e2e4e5a\\" or \\"6605c7b08e715d6c\\"","refId":""}]}' )}` ); }); @@ -1326,6 +1323,10 @@ function createTraceSpan(overrides: Partial = {}) { key: 'label2', value: 'val2', }, + { + key: 'service.namespace', + value: 'namespace1', + }, ], }, ...overrides, diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 26f1ecb1c1e..d0a4d181393 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -17,7 +17,7 @@ import { import { getTemplateSrv } from '@grafana/runtime'; import { DataQuery } from '@grafana/schema'; import { Icon } from '@grafana/ui'; -import { TraceToLogsOptionsV2 } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; +import { TraceToLogsOptionsV2, TraceToLogsTag } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; import { TraceToMetricQuery, TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { PromQuery } from 'app/plugins/datasource/prometheus/types'; @@ -115,7 +115,18 @@ export function createSpanLinkFactory({ /** * Default keys to use when there are no configured tags. */ -const defaultKeys = ['cluster', 'hostname', 'namespace', 'pod'].map((k) => ({ key: k })); +const defaultKeys = [ + 'cluster', + 'hostname', + 'namespace', + 'pod', + 'service.name', + 'service.namespace', + 'deployment.environment', +].map((k) => ({ + key: k, + value: k.includes('.') ? k.replace('.', '_') : undefined, +})); function legacyCreateSpanLinkFactory( splitOpenFn: SplitOpen, @@ -149,7 +160,8 @@ function legacyCreateSpanLinkFactory( // deprecated blob format and we can map the link easily in data frame. if (logsDataSourceSettings && traceToLogsOptions) { const customQuery = traceToLogsOptions.customQuery ? traceToLogsOptions.query : undefined; - const tagsToUse = traceToLogsOptions.tags || defaultKeys; + const tagsToUse = + traceToLogsOptions.tags && traceToLogsOptions.tags.length > 0 ? traceToLogsOptions.tags : defaultKeys; switch (logsDataSourceSettings?.type) { case 'loki': tags = getFormattedTags(span, tagsToUse); @@ -480,7 +492,7 @@ function getQueryForFalconLogScale(span: TraceSpan, options: TraceToLogsOptionsV */ function getFormattedTags( span: TraceSpan, - tags: Array<{ key: string; value?: string }>, + tags: TraceToLogsTag[], { labelValueSign = '=', joinBy = ', ' }: { labelValueSign?: string; joinBy?: string } = {} ) { // In order, try to use mapped tags -> tags -> default tags From 21d1d54689bdafca45fef0301b2908561e441469 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Thu, 27 Jul 2023 12:36:59 -0400 Subject: [PATCH 64/64] fix: correct devenv postgres tag (#72465) * fix: correct docker tag for postgres 11.20 --- devenv/docker/blocks/postgres/.env | 2 +- devenv/docker/blocks/postgres_tests/.env | 2 +- devenv/docker/blocks/postgres_tests/Dockerfile | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/devenv/docker/blocks/postgres/.env b/devenv/docker/blocks/postgres/.env index 8bd1265589e..e916eb4ee21 100644 --- a/devenv/docker/blocks/postgres/.env +++ b/devenv/docker/blocks/postgres/.env @@ -1 +1 @@ -postgres_version=11.20 +postgres_version=11.20-alpine3.18 diff --git a/devenv/docker/blocks/postgres_tests/.env b/devenv/docker/blocks/postgres_tests/.env index 8bd1265589e..e916eb4ee21 100644 --- a/devenv/docker/blocks/postgres_tests/.env +++ b/devenv/docker/blocks/postgres_tests/.env @@ -1 +1 @@ -postgres_version=11.20 +postgres_version=11.20-alpine3.18 diff --git a/devenv/docker/blocks/postgres_tests/Dockerfile b/devenv/docker/blocks/postgres_tests/Dockerfile index aad5e348cf5..090fb4a52b3 100644 --- a/devenv/docker/blocks/postgres_tests/Dockerfile +++ b/devenv/docker/blocks/postgres_tests/Dockerfile @@ -1,4 +1,4 @@ -ARG postgres_version=11.20 +ARG postgres_version=11.20-alpine3.18 FROM postgres:${postgres_version} ADD setup.sql /docker-entrypoint-initdb.d RUN chown -R postgres:postgres /docker-entrypoint-initdb.d/