From adac77dd1fa058e7f52c2af77110cae750bc2081 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 09:43:49 -0400 Subject: [PATCH 01/95] ClipboardButton: Simplify callbacks (#49847) (#49852) (cherry picked from commit 70980fbb44fbb9b5b2ccb7ee8d0f406e9532544b) Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> --- .../ClipboardButton/ClipboardButton.tsx | 28 ++++--------------- .../rules/RuleDetailsActionButtons.tsx | 4 +-- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx index 61f7a19debe..67007913992 100644 --- a/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx +++ b/packages/grafana-ui/src/components/ClipboardButton/ClipboardButton.tsx @@ -2,41 +2,25 @@ import React, { useCallback, useRef } from 'react'; import { Button, ButtonProps } from '../Button'; -/** @deprecated Will be removed in next major release */ -interface ClipboardEvent { - action: string; - text: string; - trigger: Element; - clearSelection(): void; -} - export interface Props extends ButtonProps { /** A function that returns text to be copied */ getText(): string; /** Callback when the text has been successfully copied */ - onClipboardCopy?(e: ClipboardEvent): void; + onClipboardCopy?(copiedText: string): void; /** Callback when there was an error copying the text */ - onClipboardError?(e: ClipboardEvent): void; + onClipboardError?(copiedText: string, error: unknown): void; } -const dummyClearFunc = () => {}; - export function ClipboardButton({ onClipboardCopy, onClipboardError, children, getText, ...buttonProps }: Props) { const buttonRef = useRef(null); const copyTextCallback = useCallback(async () => { const textToCopy = getText(); - // Can be removed in 9.x - const dummyEvent: ClipboardEvent = { - action: 'copy', - clearSelection: dummyClearFunc, - text: textToCopy, - trigger: buttonRef.current!, - }; + try { await copyText(textToCopy, buttonRef); - onClipboardCopy?.(dummyEvent); - } catch { - onClipboardError?.(dummyEvent); + onClipboardCopy?.(textToCopy); + } catch (e) { + onClipboardError?.(textToCopy, e); } }, [getText, onClipboardCopy, onClipboardError]); diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx index 9c52cdd2755..74caa1165dc 100644 --- a/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx +++ b/public/app/features/alerting/unified/components/rules/RuleDetailsActionButtons.tsx @@ -203,8 +203,8 @@ export const RuleDetailsActionButtons: FC = ({ rule, rulesSource }) => { onClipboardCopy={() => { notifyApp.success('URL copied!'); }} - onClipboardError={(e) => { - notifyApp.error('Error while copying URL', e.text); + onClipboardError={(copiedText) => { + notifyApp.error('Error while copying URL', copiedText); }} className={style.button} size="sm" From 4b5adfb1baf9e142710c668cc1bc0ca110a4ffe0 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Tue, 31 May 2022 09:27:07 -0500 Subject: [PATCH 02/95] Docs: combines all SAML topics into one topic (#49443) (#49755) * Docs: combines all SAML topics into one topic (#49443) * combines all SAML topics * resolves merge conflicts * makes prettier (cherry picked from commit 8c84d62e3d6fd9fd714cd7f0140aa67e8040a980) * Fix broken relrefs Signed-off-by: Jack Baldry Co-authored-by: Jack Baldry --- docs/sources/administration/_index.md | 12 +- .../administration/api-keys/about-api-keys.md | 2 +- .../administration/api-keys/create-api-key.md | 2 +- docs/sources/administration/cli.md | 2 +- docs/sources/administration/configuration.md | 6 +- .../administration/configure-docker.md | 4 +- .../administration/database-encryption.md | 2 +- .../manage-user-preferences/_index.md | 4 +- .../about-users-and-permissions.md | 14 +- .../manage-dashboard-permissions/_index.md | 2 +- .../change-user-org-permissions.md | 2 +- .../manage-org-users/invite-user-join-org.md | 4 +- .../manage-org-users/remove-user-from-org.md | 2 +- .../manage-org-users/view-list-org-users.md | 2 +- .../manage-server-users/_index.md | 4 +- .../add-remove-user-to-org.md | 6 +- .../manage-server-users/add-user.md | 4 +- .../assign-remove-server-admin-privileges.md | 2 +- .../change-user-org-permissions.md | 2 +- .../grant-editor-admin-permissions.md | 2 +- .../manage-server-users/view-list-users.md | 2 +- docs/sources/administration/security.md | 2 +- .../about-service-accounts.md | 4 +- .../add-service-account-token.md | 4 +- .../create-service-account.md | 4 +- .../enable-service-accounts.md | 4 +- .../set-up-for-high-availability.md | 2 +- .../view-server/internal-metrics.md | 2 +- docs/sources/alerting/_index.md | 12 +- docs/sources/alerting/alert-groups/_index.md | 4 +- .../sources/alerting/alerting-rules/_index.md | 10 +- .../create-mimir-loki-managed-rule.md | 2 +- .../alerting/alerting-rules/rule-list.md | 2 +- .../sources/alerting/contact-points/_index.md | 2 +- .../message-templating/_index.md | 4 +- .../message-templating/template-data.md | 2 +- docs/sources/alerting/fundamentals/_index.md | 9 +- .../fundamentals/annotation-label/_index.md | 8 +- .../alerting/high-availability/_index.md | 2 +- .../high-availability/enable-alerting-ha.md | 2 +- docs/sources/alerting/silences/_index.md | 2 +- docs/sources/auth/_index.md | 22 +- docs/sources/auth/enhanced_ldap.md | 2 +- docs/sources/auth/ldap.md | 2 +- docs/sources/auth/okta.md | 2 +- docs/sources/auth/overview.md | 24 +- docs/sources/auth/saml.md | 2 +- docs/sources/auth/team-sync.md | 2 +- docs/sources/basics/exemplars/_index.md | 2 +- docs/sources/best-practices/_index.md | 8 +- .../dashboard-management-maturity-levels.md | 2 +- docs/sources/dashboards/_index.md | 28 +- .../sources/dashboards/dashboard-ui/_index.md | 2 +- docs/sources/dashboards/previews.md | 4 +- docs/sources/dashboards/reporting.md | 2 +- .../datasources/azuremonitor/_index.md | 6 +- docs/sources/datasources/elasticsearch.md | 6 +- .../preconfig-cloud-monitoring-dashboards.md | 2 +- docs/sources/datasources/jaeger.md | 2 +- docs/sources/datasources/loki.md | 6 +- docs/sources/datasources/tempo.md | 2 +- docs/sources/datasources/zipkin.md | 8 +- .../developers/http_api/access_control.md | 50 ++-- docs/sources/developers/http_api/admin.md | 2 +- .../developers/http_api/annotations.md | 2 +- docs/sources/developers/http_api/auth.md | 2 +- .../developers/http_api/curl-examples.md | 2 +- docs/sources/developers/http_api/dashboard.md | 2 +- .../http_api/dashboard_permissions.md | 2 +- .../developers/http_api/data_source.md | 2 +- .../http_api/datasource_permissions.md | 4 +- .../http_api/external_group_sync.md | 4 +- docs/sources/developers/http_api/folder.md | 2 +- .../http_api/folder_dashboard_search.md | 2 +- .../developers/http_api/folder_permissions.md | 2 +- docs/sources/developers/http_api/licensing.md | 4 +- docs/sources/developers/http_api/org.md | 2 +- docs/sources/developers/http_api/reporting.md | 4 +- .../developers/http_api/serviceaccount.md | 2 +- docs/sources/developers/http_api/team.md | 2 +- docs/sources/developers/http_api/user.md | 2 +- docs/sources/developers/plugins/_index.md | 16 +- ...-authentication-for-data-source-plugins.md | 2 +- .../plugins/add-support-for-annotations.md | 2 +- .../add-support-for-explore-queries.md | 2 +- .../developers/plugins/backend/_index.md | 4 +- .../build-a-logs-data-source-plugin.md | 2 +- .../build-a-streaming-data-source-plugin.md | 2 +- docs/sources/enterprise/_index.md | 2 +- .../enterprise/access-control/about-rbac.md | 8 +- .../access-control/assign-rbac-roles.md | 36 +-- .../custom-role-actions-scopes.md | 6 +- .../access-control/manage-rbac-roles.md | 38 +-- .../plan-rbac-rollout-strategy.md | 6 +- .../rbac-fixed-basic-role-definitions.md | 4 +- .../access-control/rbac-provisioning.md | 6 +- docs/sources/enterprise/auditing.md | 2 +- .../enterprise/{saml => }/configure-saml.md | 240 ++++++++++++++++-- .../enterprise/enterprise-configuration.md | 2 +- ...ing-aws-kms-to-encrypt-database-secrets.md | 2 +- docs/sources/enterprise/license/_index.md | 4 +- .../activate-license-on-eks.md | 2 +- ...ctivate-license-on-instance-outside-aws.md | 4 +- .../manage-license-in-aws-marketplace.md | 2 +- .../license/license-restrictions.md | 6 +- docs/sources/enterprise/query-caching.md | 4 +- docs/sources/enterprise/reporting.md | 10 +- docs/sources/enterprise/saml/enable-saml.md | 61 ----- .../enterprise/saml/troubleshoot-saml.md | 64 ----- docs/sources/enterprise/team-sync.md | 2 +- .../getting-started/getting-started.md | 4 +- docs/sources/image-rendering/_index.md | 2 +- docs/sources/image-rendering/monitoring.md | 2 +- .../image-rendering/troubleshooting.md | 6 +- docs/sources/installation/_index.md | 14 +- docs/sources/installation/requirements.md | 12 +- docs/sources/installation/upgrading.md | 2 +- docs/sources/old-alerting/create-alerts.md | 4 +- docs/sources/old-alerting/notifications.md | 14 +- docs/sources/panels/_index.md | 2 +- .../panels/configure-thresholds/_index.md | 14 +- .../library-panels/add-library-panel.md | 2 +- .../delete-a-field-override.md | 2 +- .../edit-field-override.md | 2 +- .../view-field-override.md | 2 +- .../download-raw-query-results.md | 2 +- .../inspect-query-performance.md | 2 +- .../inspect-request-and-response-data.md | 2 +- .../panels/query-a-data-source/share-query.md | 2 +- .../write-an-expression.md | 4 +- .../panels/working-with-panels/add-panel.md | 2 +- .../add-title-and-description.md | 2 +- .../apply-color-to-series.md | 2 +- .../working-with-panels/configure-legend.md | 2 +- .../format-standard-fields.md | 2 +- .../navigate-panel-editor.md | 6 +- .../working-with-panels/view-json-model.md | 2 +- docs/sources/plugins/_index.md | 4 +- docs/sources/plugins/installation.md | 2 +- docs/sources/release-notes/_index.md | 174 ++++++------- .../variables/variable-types/_index.md | 2 +- docs/sources/visualizations/_index.md | 52 ++-- docs/sources/visualizations/candlestick.md | 4 +- docs/sources/visualizations/graph-panel.md | 2 +- docs/sources/visualizations/table/_index.md | 2 +- .../visualizations/time-series/_index.md | 10 +- .../time-series/graph-time-series-as-bars.md | 2 +- .../time-series/graph-time-series-as-lines.md | 2 +- docs/sources/whatsnew/_index.md | 52 ++-- docs/sources/whatsnew/whats-new-in-v5-4.md | 4 +- docs/sources/whatsnew/whats-new-in-v6-0.md | 2 +- docs/sources/whatsnew/whats-new-in-v6-5.md | 2 +- docs/sources/whatsnew/whats-new-in-v6-7.md | 4 +- docs/sources/whatsnew/whats-new-in-v7-0.md | 6 +- docs/sources/whatsnew/whats-new-in-v7-3.md | 6 +- docs/sources/whatsnew/whats-new-in-v7-4.md | 6 +- docs/sources/whatsnew/whats-new-in-v8-0.md | 2 +- docs/sources/whatsnew/whats-new-in-v8-1.md | 2 +- 158 files changed, 729 insertions(+), 663 deletions(-) rename docs/sources/enterprise/{saml => }/configure-saml.md (57%) delete mode 100644 docs/sources/enterprise/saml/enable-saml.md delete mode 100644 docs/sources/enterprise/saml/troubleshoot-saml.md diff --git a/docs/sources/administration/_index.md b/docs/sources/administration/_index.md index 84e4e51d12c..bf07627ba45 100644 --- a/docs/sources/administration/_index.md +++ b/docs/sources/administration/_index.md @@ -10,9 +10,9 @@ weight: 40 This section includes information for Grafana administrators, team administrators, and users performing administrative tasks: -- [Change Preferences]({{< relref "preferences" >}}) -- [Configuration]({{< relref "configuration" >}}) -- [Configure Docker image]({{< relref "configure-docker" >}}) -- [Security]({{< relref "security" >}}) -- [Database encryption]({{< relref "database-encryption" >}}) -- [Service accounts]({{< relref "service-accounts" >}}) +- [Change Preferences]({{< relref "preferences/" >}}) +- [Configuration]({{< relref "configuration/" >}}) +- [Configure Docker image]({{< relref "configure-docker/" >}}) +- [Security]({{< relref "security/" >}}) +- [Database encryption]({{< relref "database-encryption/" >}}) +- [Service accounts]({{< relref "service-accounts/" >}}) diff --git a/docs/sources/administration/api-keys/about-api-keys.md b/docs/sources/administration/api-keys/about-api-keys.md index 901e8dcb1f5..29258ffa2b2 100644 --- a/docs/sources/administration/api-keys/about-api-keys.md +++ b/docs/sources/administration/api-keys/about-api-keys.md @@ -11,4 +11,4 @@ weight: 30 An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs. -When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "./create-api-key.md#" >}}). +When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "create-api-key.md#" >}}). diff --git a/docs/sources/administration/api-keys/create-api-key.md b/docs/sources/administration/api-keys/create-api-key.md index d200bc05536..2f01823650b 100644 --- a/docs/sources/administration/api-keys/create-api-key.md +++ b/docs/sources/administration/api-keys/create-api-key.md @@ -14,7 +14,7 @@ weight: 50 Create an API key when you want to manage your computed workload with a user. -For more information about API keys, refer to [About API keys in Grafana]({{< relref "./about-api-keys.md" >}}). +For more information about API keys, refer to [About API keys in Grafana]({{< relref "about-api-keys.md" >}}). This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../developers/http_api/create-api-tokens-for-org.md#how-to-create-a-new-organization-and-an-api-token" >}}). diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 849d51a76af..d76296422cb 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -137,7 +137,7 @@ grafana-cli --homepath "/usr/share/grafana" admin reset-admin-password }}) for more information about configuring Grafana and default configuration file locations. +`--config value` overrides the default location where Grafana expects the configuration file. Refer to [Configuration]({{< relref "configuration.md" >}}) for more information about configuring Grafana and default configuration file locations. **Example:** diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 311a8fa453c..857fd5bd37d 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -127,7 +127,7 @@ password = $__file{/etc/secrets/gf_sql_password} The `vault` provider allows you to manage your secrets with [Hashicorp Vault](https://www.hashicorp.com/products/vault). -> Vault provider is only available in Grafana Enterprise v7.1+. For more information, refer to [Vault integration]({{< relref "../enterprise/vault.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> Vault provider is only available in Grafana Enterprise v7.1+. For more information, refer to [Vault integration]({{< relref "../enterprise/vault.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise/" >}}).
@@ -673,7 +673,7 @@ Path to the default home dashboard. If this value is empty, then Grafana uses St Set to `false` to prohibit users from being able to sign up / create user accounts. Default is `false`. The admin user can still create -users. For more information about creating a user, refer to [Add a user]({{< relref "../administration/manage-users-and-permissions/manage-server-users/add-user.md" >}}). +users. For more information about creating a user, refer to [Add a user]({{< relref "manage-users-and-permissions/manage-server-users/add-user.md" >}}). ### allow_org_create @@ -815,7 +815,7 @@ Set to `true` to enable verbose request signature logging when AWS Signature Ver ## [auth.anonymous] -Refer to [Anonymous authentication]({{< relref "../auth/grafana.md/#anonymous-authentication" >}}) for detailed instructions. +Refer to [Anonymous authentication]({{< relref "../auth/grafana.md#anonymous-authentication" >}}) for detailed instructions.
diff --git a/docs/sources/administration/configure-docker.md b/docs/sources/administration/configure-docker.md index 1c1d800b95b..2e367d77e2d 100644 --- a/docs/sources/administration/configure-docker.md +++ b/docs/sources/administration/configure-docker.md @@ -14,7 +14,7 @@ weight: 200 # Configure a Grafana Docker image -If you are running Grafana in a Docker image, then you configure Grafana using [environment variables]({{< relref "../administration/configuration.md#configure-with-environment-variables" >}}) rather than directly editing the configuration file. If you want to save your data, then you also need to designate persistent storage or bind mounts for the Grafana container. +If you are running Grafana in a Docker image, then you configure Grafana using [environment variables]({{< relref "configuration.md#configure-with-environment-variables" >}}) rather than directly editing the configuration file. If you want to save your data, then you also need to designate persistent storage or bind mounts for the Grafana container. > **Note:** These examples use the Grafana Enterprise docker image. You can use the Grafana Open Source edition by changing the docker image to `grafana/grafana-oss`. @@ -59,7 +59,7 @@ The following settings are hard-coded when launching the Grafana Docker containe ## Logging -Logs in the Docker container go to standard out by default, as is common in the Docker world. Change this by setting a different [log mode]({{< relref "../administration/configuration.md#mode" >}}). +Logs in the Docker container go to standard out by default, as is common in the Docker world. Change this by setting a different [log mode]({{< relref "configuration.md#mode" >}}). Example: diff --git a/docs/sources/administration/database-encryption.md b/docs/sources/administration/database-encryption.md index 0af9ad244ee..269cf8fa7cb 100644 --- a/docs/sources/administration/database-encryption.md +++ b/docs/sources/administration/database-encryption.md @@ -101,7 +101,7 @@ New data keys for encryption operations are generated on-demand. > those secrets still encrypted with it. Look at [secrets re-encryption](#re-encrypt-secrets) to completely stop using > rotated data keys for both encryption and decryption. -> **Note:** This operation is available through Grafana [Admin API]({{< relref "../developers/configuration/admin/#rotate-data-encryption-keys" >}}). +> **Note:** This operation is available through Grafana [Admin API]({{< relref "../developers/http_api/admin/#rotate-data-encryption-keys" >}}). > It's safe to run more than once. # KMS integration diff --git a/docs/sources/administration/manage-user-preferences/_index.md b/docs/sources/administration/manage-user-preferences/_index.md index 809fe239374..c8093efcdf0 100644 --- a/docs/sources/administration/manage-user-preferences/_index.md +++ b/docs/sources/administration/manage-user-preferences/_index.md @@ -47,7 +47,7 @@ Your profile includes your name, user name, and email address, which you can upd ## Edit your preferences -You can choose the way you would like data to appear in Grafana, including the UI theme, home dashboard, timezone, and first day of the week. You can set these preferences for your own account, for a team, for an organization, or Grafana-wide using configuration settings. Your user preferences take precedence over team, organization, and Grafana default preferences. For more information, see [Grafana preferences]({{< relref "../../administration/preferences/_index.md" >}}). +You can choose the way you would like data to appear in Grafana, including the UI theme, home dashboard, timezone, and first day of the week. You can set these preferences for your own account, for a team, for an organization, or Grafana-wide using configuration settings. Your user preferences take precedence over team, organization, and Grafana default preferences. For more information, see [Grafana preferences]({{< relref "../preferences/_index.md" >}}). - **UI theme** determines whether Grafana appears in light mode or dark mode. By default, UI theme is set to dark mode. - **Home dashboard** refers to the dashboard you see when you sign in to Grafana. By default, this is set to the Home dashboard. @@ -81,7 +81,7 @@ Every user is a member of at least one organization. You can have different role 1. Hover your cursor over the user icon in the lower-left corner of the page and click **Preferences**. 1. Scroll down to the **Organizations** section and review the following information: - **Name**: The name of the organizations of which you are a member. - - **Role**: The role to which you are assigned in the organization. For more information about roles and permissions, refer to [Organization users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}). + - **Role**: The role to which you are assigned in the organization. For more information about roles and permissions, refer to [Organization users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}). - **Current**: Grafana indicates the organization that you are currently signed into as _Current_. If you are a member of multiple organizations, you can click **Select** to switch to that organization. ## View your Grafana sessions diff --git a/docs/sources/administration/manage-users-and-permissions/about-users-and-permissions.md b/docs/sources/administration/manage-users-and-permissions/about-users-and-permissions.md index 1fa41c94d7c..f287b1f7054 100644 --- a/docs/sources/administration/manage-users-and-permissions/about-users-and-permissions.md +++ b/docs/sources/administration/manage-users-and-permissions/about-users-and-permissions.md @@ -31,7 +31,7 @@ A server administrator can perform the following tasks: - Manage users and permissions - Create, edit, and delete organizations -- View server-wide settings defined in the [Configuration]({{< relref "../../administration/configuration.md" >}}) file +- View server-wide settings defined in the [Configuration]({{< relref "../configuration.md" >}}) file - View Grafana server statistics, including total users and active sessions - Upgrade the server to Grafana Enterprise. @@ -97,9 +97,9 @@ You can specify the following permissions to dashboards and folders. - **Edit**: Can create and edit dashboards. Editors _cannot_ change folder or dashboard permissions, or add, edit, or delete folders. - **View**: Can only view dashboards and folders. -For more information about assigning dashboard folder permissions, refer to [Grant dashboard folder permissions]({{< relref "./manage-dashboard-permissions/_index.md#grant-dashboard-folder-permissions" >}}). +For more information about assigning dashboard folder permissions, refer to [Grant dashboard folder permissions]({{< relref "manage-dashboard-permissions/_index.md#grant-dashboard-folder-permissions" >}}). -For more information about assigning dashboard permissions, refer to [Grant dashboard permissions]({{< relref "./manage-dashboard-permissions/_index.md#grant-dashboard-permissions" >}}). +For more information about assigning dashboard permissions, refer to [Grant dashboard permissions]({{< relref "manage-dashboard-permissions/_index.md#grant-dashboard-permissions" >}}). ## Editors with administrator permissions @@ -109,7 +109,7 @@ If you have access to the Grafana server, you can modify the default editor role This setting can be used to enable self-organizing teams to administer their own dashboards. -For more information about assigning administrator permissions to editors, refer to [Grant editors administrator permissions]({{< relref "./manage-server-users/grant-editor-admin-permissions.md" >}}). +For more information about assigning administrator permissions to editors, refer to [Grant editors administrator permissions]({{< relref "manage-server-users/grant-editor-admin-permissions.md" >}}). ## Viewers with dashboard preview and Explore permissions @@ -120,7 +120,7 @@ If you have access to the Grafana server, you can modify the default viewer role Extending the viewer role is useful for public Grafana installations where you want anonymous users to be able to edit panels and queries, but not be able to save or create new dashboards. -For more information about assigning dashboard preview permissions to viewers, refer to [Enable viewers to preview dashboards and use Explore]({{< relref "./manage-dashboard-permissions/_index.md#enable-viewers-to-preview-dashboards-and-use-explore" >}}). +For more information about assigning dashboard preview permissions to viewers, refer to [Enable viewers to preview dashboards and use Explore]({{< relref "manage-dashboard-permissions/_index.md#enable-viewers-to-preview-dashboards-and-use-explore" >}}). ## Teams and permissions @@ -131,7 +131,7 @@ You can assign a team member one of the following permissions: - **Member**: Includes the user as a member of the team. Members do not have team administrator privileges. - **Admin**: Administrators have permission to manage various aspects of the team, including team membership, permissions, and settings. -Because teams exist inside an organization, the organization administrator can manage all teams. When the `editors_can_admin` setting is enabled, editors can create teams and manage teams that they create. For more information about the `editors_can_admin` setting, refer to [Grant editors administrator permissions]({{< relref "./manage-server-users/grant-editor-admin-permissions.md" >}}). +Because teams exist inside an organization, the organization administrator can manage all teams. When the `editors_can_admin` setting is enabled, editors can create teams and manage teams that they create. For more information about the `editors_can_admin` setting, refer to [Grant editors administrator permissions]({{< relref "manage-server-users/grant-editor-admin-permissions.md" >}}). ## Grafana Enterprise user permissions features @@ -152,7 +152,7 @@ Data source permissions enable you to restrict data source query permissions to RBAC provides you a way of granting, changing, and revoking user read and write access to Grafana resources, such as users, reports, and authentication. -For more information about RBAC, refer to [Role-based access control]({{< relref "../../enterprise/access-control" >}}). +For more information about RBAC, refer to [Role-based access control]({{< relref "../../enterprise/access-control/" >}}). ### Learn more diff --git a/docs/sources/administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md b/docs/sources/administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md index 0786ad403ba..49260ece40d 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md @@ -76,7 +76,7 @@ This modification is useful for public Grafana installations where you want anon 1. Open the Grafana configuration file. - For more information about the Grafana configuration file and its location, refer to [Configuration]({{< relref "../../../administration/configuration" >}}). + For more information about the Grafana configuration file and its location, refer to [Configuration]({{< relref "../../configuration/" >}}). 1. Locate the `viewers_can_edit` parameter. 1. Set the `viewers_can_edit` value to `true`. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-org-users/change-user-org-permissions.md b/docs/sources/administration/manage-users-and-permissions/manage-org-users/change-user-org-permissions.md index ea769aa8ff9..62e4e5faab1 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-org-users/change-user-org-permissions.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-org-users/change-user-org-permissions.md @@ -25,4 +25,4 @@ Update user permissions when you want to enhance or restrict a user's access to 1. Select the role that you want to assign. 1. Click **Update**. -> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also [change a user's organization permissions]({{< relref "../../manage-users-and-permissions/manage-server-users/change-user-org-permissions.md" >}}) in the Server Admin section. +> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also [change a user's organization permissions]({{< relref "../manage-server-users/change-user-org-permissions.md" >}}) in the Server Admin section. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-org-users/invite-user-join-org.md b/docs/sources/administration/manage-users-and-permissions/manage-org-users/invite-user-join-org.md index 8cef28ced99..047062d8a91 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-org-users/invite-user-join-org.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-org-users/invite-user-join-org.md @@ -12,7 +12,7 @@ When you invite users to join an organization, you assign the **Admin**, **Edito - If you know that the user already has access Grafana and you know their user name, then you issue an invitation by entering their user name. - If the user is new to Grafana, then use their email address to issue an invitation. The system automatically creates the user account on first sign in. -> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also manually [add a user to an organization]({{< relref "../../manage-users-and-permissions/manage-server-users/add-remove-user-to-org.md" >}}). +> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also manually [add a user to an organization]({{< relref "../manage-server-users/add-remove-user-to-org.md" >}}). ## Before you begin @@ -35,7 +35,7 @@ When you invite users to join an organization, you assign the **Admin**, **Edito | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Email or username | Either the email or username that the user will use to sign in to Grafana. | | Name | The user's name. | - | Role | Click the organization role to assign this user. For more information about organization roles, refer to [Organization roles]({{< relref "../about-users-and-permissions#organization-roles" >}}).. | + | Role | Click the organization role to assign this user. For more information about organization roles, refer to [Organization roles]({{< relref "../about-users-and-permissions/#organization-roles" >}}).. | | Send invite email | Switch to on if your organization has configured. The system sends an email to the user inviting them to sign in to Grafana and join the organization. Switch to off if you are not using email. The user can sign in to Grafana with the email or username you entered. | 1. Click **Submit**. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-org-users/remove-user-from-org.md b/docs/sources/administration/manage-users-and-permissions/manage-org-users/remove-user-from-org.md index 1d6b68ac0ee..9506b7f106e 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-org-users/remove-user-from-org.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-org-users/remove-user-from-org.md @@ -25,4 +25,4 @@ This action does not remove the user account from the Grafana server. 1. Click the red **X** to remove the user from the organization. -> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also [remove a user from an organization]({{< relref "../../manage-users-and-permissions/manage-server-users/add-remove-user-to-org.md#remove-a-user-from-an-organization" >}}) on the Users page of the Server Admin section. +> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also [remove a user from an organization]({{< relref "../manage-server-users/add-remove-user-to-org.md#remove-a-user-from-an-organization" >}}) on the Users page of the Server Admin section. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-org-users/view-list-org-users.md b/docs/sources/administration/manage-users-and-permissions/manage-org-users/view-list-org-users.md index 019027eb360..d8173bcd0a1 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-org-users/view-list-org-users.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-org-users/view-list-org-users.md @@ -20,4 +20,4 @@ You can see a list of users with accounts in your Grafana organization. If neces ![Org Admin user list](/static/img/docs/manage-users/org-user-list-7-3.png) -> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also [view a global list of users]({{< relref "../../manage-users-and-permissions/manage-server-users/view-list-users.md" >}}) in the Server Admin section of Grafana. +> **Note:** If you have [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can also [view a global list of users]({{< relref "../manage-server-users/view-list-users.md" >}}) in the Server Admin section of Grafana. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/_index.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/_index.md index 91467e02ca7..7cc3502a20f 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/_index.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/_index.md @@ -15,6 +15,6 @@ If you have [server administrator]({{< relref "../about-users-and-permissions.md {{< section >}} -If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, refer to [Manage users in a organization]({{< relref "../../manage-users-and-permissions/manage-org-users/_index.md" >}}). +If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, refer to [Manage users in a organization]({{< relref "../manage-org-users/_index.md" >}}). -For more information about users and permissions, refer to [About users and permissions]({{< relref "../about-users-and-permissions" >}}). +For more information about users and permissions, refer to [About users and permissions]({{< relref "../about-users-and-permissions/" >}}). diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-remove-user-to-org.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-remove-user-to-org.md index 50b9b47c800..bda00b06c09 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-remove-user-to-org.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-remove-user-to-org.md @@ -15,7 +15,7 @@ You are required to specify an Admin role for each organization. The first user ## Before you begin - [Create an organization]({{< relref "../../manage-organizations/_index.md" >}}) -- [Add a user]({{< relref "./add-user.md" >}}) to Grafana +- [Add a user]({{< relref "add-user.md" >}}) to Grafana - Ensure you have Grafana server administrator privileges **To add a user to an organization**: @@ -32,7 +32,7 @@ You are required to specify an Admin role for each organization. The first user The next time the user signs in, they will be able to navigate to their new organization using the Switch Organizations option in the user profile menu. -> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still [invite a user to join an organization]({{< relref "../../manage-users-and-permissions/manage-org-users/invite-user-join-org.md" >}}). +> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still [invite a user to join an organization]({{< relref "../manage-org-users/invite-user-join-org.md" >}}). # Remove a user from an organization @@ -50,4 +50,4 @@ Remove a user from an organization when they no longer require access to the das 1. In the **Organization** section, click **Remove from organization** next to the organization from which you want to remove the user. 1. Click **Confirm removal**. -> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still [remove a user from an organization]({{< relref "../../manage-users-and-permissions/manage-org-users/remove-user-from-org.md" >}}) in the Users section of organization configuration. +> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still [remove a user from an organization]({{< relref "../manage-org-users/remove-user-from-org.md" >}}) in the Users section of organization configuration. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-user.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-user.md index 9450c881d8d..58c7a61f17b 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-user.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/add-user.md @@ -24,6 +24,6 @@ When you configure advanced authentication using Oauth, SAML, LDAP, or the Auth 1. Click **New user**. 1. Complete the fields and click **Create user**. -When you create a user, the system assigns the user viewer permissions in a default organization, which you can change. You can now [add a user to a second organization]({{< relref "./add-remove-user-to-org.md" >}}). +When you create a user, the system assigns the user viewer permissions in a default organization, which you can change. You can now [add a user to a second organization]({{< relref "add-remove-user-to-org.md" >}}). -> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still add users by [inviting a user to join an organization]({{< relref "../../manage-users-and-permissions/manage-org-users/invite-user-join-org.md" >}}). +> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still add users by [inviting a user to join an organization]({{< relref "../manage-org-users/invite-user-join-org.md" >}}). diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/assign-remove-server-admin-privileges.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/assign-remove-server-admin-privileges.md index 27b64fc08c8..24c9e87c13c 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/assign-remove-server-admin-privileges.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/assign-remove-server-admin-privileges.md @@ -13,7 +13,7 @@ Grafana server administrators are responsible for creating users, organizations, ## Before you begin -- [Add a user]({{< relref "./add-user.md" >}}) +- [Add a user]({{< relref "add-user.md" >}}) - Ensure you have Grafana server administrator privileges **To assign or remove Grafana administrator privileges**: diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/change-user-org-permissions.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/change-user-org-permissions.md index 5eb323753fb..bbea6bb05f1 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/change-user-org-permissions.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/change-user-org-permissions.md @@ -11,7 +11,7 @@ Update organization permissions when you want to enhance or restrict a user's ac ## Before you begin -- [Add a user to an organization]({{< relref "./add-remove-user-to-org.md" >}}) +- [Add a user to an organization]({{< relref "add-remove-user-to-org.md" >}}) - Ensure you have Grafana server administrator privileges **To change a user's organization permissions**: diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/grant-editor-admin-permissions.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/grant-editor-admin-permissions.md index 5736cb7df84..a360960dbc6 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/grant-editor-admin-permissions.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/grant-editor-admin-permissions.md @@ -26,7 +26,7 @@ When `editors_can_admin` is enabled: 1. Log in to the Grafana server and open the Grafana configuration file. - For more information about the Grafana configuration file and its location, refer to [Configuration]({{< relref "../../../administration/configuration" >}}). + For more information about the Grafana configuration file and its location, refer to [Configuration]({{< relref "../../../administration/configuration/" >}}). 1. Locate the `editors_can_admin` parameter. 1. Set the `editors_can_admin` value to `true`. diff --git a/docs/sources/administration/manage-users-and-permissions/manage-server-users/view-list-users.md b/docs/sources/administration/manage-users-and-permissions/manage-server-users/view-list-users.md index b834bdb4a8b..b3a86efc72d 100644 --- a/docs/sources/administration/manage-users-and-permissions/manage-server-users/view-list-users.md +++ b/docs/sources/administration/manage-users-and-permissions/manage-server-users/view-list-users.md @@ -20,4 +20,4 @@ You can see a list of users with accounts on your Grafana server. This action mi ![Server Admin user list](/static/img/docs/manage-users/server-user-list-7-3.png) -> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still [view of list of users in a given organization]({{< relref "../../manage-users-and-permissions/manage-org-users/view-list-org-users.md" >}}). +> **Note:** If you have [organization administrator]({{< relref "../about-users-and-permissions.md#organization-roles" >}}) permissions and _not_ [server administrator]({{< relref "../about-users-and-permissions.md#grafana-server-administrators" >}}) permissions, you can still [view of list of users in a given organization]({{< relref "../manage-org-users/view-list-org-users.md" >}}). diff --git a/docs/sources/administration/security.md b/docs/sources/administration/security.md index 76cec13081e..d6a4a785edf 100644 --- a/docs/sources/administration/security.md +++ b/docs/sources/administration/security.md @@ -23,7 +23,7 @@ You can configure Grafana to only allow certain IP addresses or hostnames to be ## Request security -The request security configuration option allows users to limit requests from the Grafana server. It targets requests that are generated by users. For more information, refer to [Request security]({{< relref "../enterprise/request-security.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +The request security configuration option allows users to limit requests from the Grafana server. It targets requests that are generated by users. For more information, refer to [Request security]({{< relref "../enterprise/request-security.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise/" >}}). > **Note:** Request security is available in Grafana Enterprise v7.4 and later versions. diff --git a/docs/sources/administration/service-accounts/about-service-accounts.md b/docs/sources/administration/service-accounts/about-service-accounts.md index 8d0737ed0f0..692c60572ec 100644 --- a/docs/sources/administration/service-accounts/about-service-accounts.md +++ b/docs/sources/administration/service-accounts/about-service-accounts.md @@ -11,7 +11,7 @@ weight: 30 A service account can be used to run automated workloads in Grafana, like dashboard provisioning, configuration, or report generation. Create service accounts and tokens to authenticate applications like Terraform with the Grafana API. -> **Note:** Service accounts are available in Grafana 8.5+ as a beta feature. To enable service accounts, refer to [Enable service accounts]({{< relref "./enable-service-accounts.md#" >}}) section. Service accounts will eventually replace [API keys]({{< relref "../api-keys/_index.md" >}}) as the primary way to authenticate applications that interact with Grafana. +> **Note:** Service accounts are available in Grafana 8.5+ as a beta feature. To enable service accounts, refer to [Enable service accounts]({{< relref "enable-service-accounts.md#" >}}) section. Service accounts will eventually replace [API keys]({{< relref "../api-keys/_index.md" >}}) as the primary way to authenticate applications that interact with Grafana. A common use case for creating a service account is to perform operations on automated or triggered tasks. You can use service accounts to: @@ -46,4 +46,4 @@ The added benefits of service accounts to API keys include: - Service accounts resemble Grafana users and can be enabled/disabled, granted specific permissions, and remain active until they are deleted or disabled. API keys are only valid until their expiry date. - Service accounts can be associated with multiple tokens. - Unlike API keys, service account tokens are not associated with a specific user, which means that applications can be authenticated even if a Grafana user is deleted. -- You can grant granular permissions to service accounts by leveraging [fine-grained access control]({{< relref "../../enterprise/access-control" >}}). For more information about permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#" >}}). +- You can grant granular permissions to service accounts by leveraging [fine-grained access control]({{< relref "../../enterprise/access-control/" >}}). For more information about permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#" >}}). diff --git a/docs/sources/administration/service-accounts/add-service-account-token.md b/docs/sources/administration/service-accounts/add-service-account-token.md index a038c33ad2c..5a8d97a5d3a 100644 --- a/docs/sources/administration/service-accounts/add-service-account-token.md +++ b/docs/sources/administration/service-accounts/add-service-account-token.md @@ -9,13 +9,13 @@ weight: 60 # Add a token to a service account in Grafana -A service account token is a generated random string that acts as an alternative to a password when authenticating with Grafana’s HTTP API. For more information about service accounts, refer to [About service accounts in Grafana]({{< relref "./about-service-accounts.md" >}}). +A service account token is a generated random string that acts as an alternative to a password when authenticating with Grafana’s HTTP API. For more information about service accounts, refer to [About service accounts in Grafana]({{< relref "about-service-accounts.md" >}}). You can create a service account token using the Grafana UI or via the API. For more information about creating a service account token via the API, refer to [Create service account tokens using the HTTP API]({{< relref "../../developers/http_api/serviceaccount.md#create-service-account-tokens" >}}). ## Before you begin -- Ensure you have added the `serviceAccounts` feature toggle to Grafana. For more information about adding the feature toggle, refer to [Enable service accounts]({{< relref "./enable-service-accounts.md#" >}}). +- Ensure you have added the `serviceAccounts` feature toggle to Grafana. For more information about adding the feature toggle, refer to [Enable service accounts]({{< relref "enable-service-accounts.md#" >}}). - Ensure you have permission to create and edit service accounts. By default, the organization administrator role is required to create and edit service accounts. For more information about user permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#" >}}). ## To add a token to a service account diff --git a/docs/sources/administration/service-accounts/create-service-account.md b/docs/sources/administration/service-accounts/create-service-account.md index 2e107a74bd7..cf2e878df8f 100644 --- a/docs/sources/administration/service-accounts/create-service-account.md +++ b/docs/sources/administration/service-accounts/create-service-account.md @@ -11,13 +11,13 @@ weight: 50 # Create a service account in Grafana -A service account can be used to run automated workloads in Grafana, like dashboard provisioning, configuration, or report generation. For more information about how you can use service accounts, refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts.md#" >}}). +A service account can be used to run automated workloads in Grafana, like dashboard provisioning, configuration, or report generation. For more information about how you can use service accounts, refer to [About service accounts]({{< relref "about-service-accounts.md#" >}}). For more information about creating service accounts via the API, refer to [Create a service account in the HTTP API]({{< relref "../../developers/http_api/serviceaccount.md#create-service-account" >}}). ## Before you begin -- Ensure you have added the feature toggle for service accounts `serviceAccounts`. For more information about adding the feature toggle, refer to [Enable service accounts]({{< relref "./enable-service-accounts.md#" >}}). +- Ensure you have added the feature toggle for service accounts `serviceAccounts`. For more information about adding the feature toggle, refer to [Enable service accounts]({{< relref "enable-service-accounts.md#" >}}). - Ensure you have permission to create and edit service accounts. By default, the organization administrator role is required to create and edit service accounts. For more information about user permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#" >}}). ## To create a service account diff --git a/docs/sources/administration/service-accounts/enable-service-accounts.md b/docs/sources/administration/service-accounts/enable-service-accounts.md index e6b5325ca27..158bfe3fc59 100644 --- a/docs/sources/administration/service-accounts/enable-service-accounts.md +++ b/docs/sources/administration/service-accounts/enable-service-accounts.md @@ -25,7 +25,7 @@ You can enable service accounts by: This topic shows you how to enable service accounts by modifying the Grafana configuration file. 1. Sign in to the Grafana server and locate the configuration file. For more information about finding the configuration file, refer to LINK. -2. Open the configuration file and locate the [feature toggles section]({{< relref "../../administration/configuration.md#feature_toggles" >}}). Add `serviceAccounts` as a [feature_toggle]({{< relref "../../administration/configuration.md#feature_toggle" >}}). +2. Open the configuration file and locate the [feature toggles section]({{< relref "../configuration.md#feature_toggles" >}}). Add `serviceAccounts` as a [feature_toggle]({{< relref "../configuration.md#feature_toggle" >}}). ``` [feature_toggles] @@ -39,6 +39,6 @@ enable = serviceAccounts This topic shows you how to enable service accounts by setting environment variables before starting Grafana. -Follow the instructions to [override configuration with environment variables]({{< relref "../../administration/configuration.md#override-configuration-with-environment-variables" >}}). Set the following environment variable: `GF_FEATURE_TOGGLES_ENABLE = serviceAccounts`. +Follow the instructions to [override configuration with environment variables]({{< relref "../configuration.md#override-configuration-with-environment-variables" >}}). Set the following environment variable: `GF_FEATURE_TOGGLES_ENABLE = serviceAccounts`. > **Note:** Environment variables override configuration file settings. diff --git a/docs/sources/administration/set-up-for-high-availability.md b/docs/sources/administration/set-up-for-high-availability.md index ff9acc98a66..c6bf6574874 100644 --- a/docs/sources/administration/set-up-for-high-availability.md +++ b/docs/sources/administration/set-up-for-high-availability.md @@ -23,7 +23,7 @@ and other persistent data. So the default embedded SQLite database will not work ## Configure multiple servers to use the same database First, you need to set up MySQL or Postgres on another server and configure Grafana to use that database. -You can find the configuration for doing that in the [[database]]({{< relref "../administration/configuration.md#database" >}}) section in the Grafana config. +You can find the configuration for doing that in the [[database]]({{< relref "configuration.md#database" >}}) section in the Grafana config. Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on the database you're using. ## Alerting high availability diff --git a/docs/sources/administration/view-server/internal-metrics.md b/docs/sources/administration/view-server/internal-metrics.md index eb18e74534a..c457f6645ec 100644 --- a/docs/sources/administration/view-server/internal-metrics.md +++ b/docs/sources/administration/view-server/internal-metrics.md @@ -15,7 +15,7 @@ weight: 200 Grafana collects some metrics about itself internally. Grafana supports pushing metrics to Graphite or exposing them to be scraped by Prometheus. -For more information about configuration options related to Grafana metrics, refer to [metrics]({{< relref "../../administration/configuration/#metrics" >}}) and [metrics.graphite]({{< relref "../../administration/configuration/#metrics-graphite" >}}) in [Configuration]({{< relref "../../administration/configuration.md" >}}). +For more information about configuration options related to Grafana metrics, refer to [metrics]({{< relref "../../administration/configuration/#metrics" >}}) and [metrics.graphite]({{< relref "../../administration/configuration/#metrics-graphite" >}}) in [Configuration]({{< relref "../configuration.md" >}}). ## Available metrics diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index 6662edb456b..ff0d66edb68 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -22,7 +22,7 @@ For new installations or existing installs without alerting configured, Grafana Existing installations that upgrade to v9.0 will have Grafana alerting enabled by default. For more information on migrating from legacy or the cloud alerting plugin, see [Migrating to Grafana alerting]({{< relref "./migrating-alerts/_index.md" >}}). -Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "./fundamentals/_index.md" >}}) of Grafana alerting. Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using role-based permissions. +Before you begin, we recommend that you familiarize yourself with some of the [fundamental concepts]({{< relref "fundamentals/_index.md" >}}) of Grafana alerting. Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using role-based permissions. - [About alert rules]({{< relref "./fundamentals/alert-rules/_index.md" >}}) - [Migrating legacy alerts]({{< relref "./migrating-alerts/_index.md" >}}) @@ -30,8 +30,8 @@ Before you begin, we recommend that you familiarize yourself with some of the [f - [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}}) - [Create Grafana Mimir or Loki managed alerting rules]({{< relref "alerting-rules/create-mimir-loki-managed-rule.md" >}}) - [View existing alerting rules and manage their current state]({{< relref "alerting-rules/rule-list.md" >}}) -- [View the state and health of alerting rules]({{< relref "./fundamentals/state-and-health.md" >}}) -- [View alert groupings]({{< relref "./alert-groups/_index.md" >}}) -- [Add or edit an alert contact point]({{< relref "./contact-points/_index.md" >}}) -- [Add or edit notification policies]({{< relref "./notifications/_index.md" >}}) -- [Add or edit silences]({{< relref "./silences/_index.md" >}}) +- [View the state and health of alerting rules]({{< relref "fundamentals/state-and-health.md" >}}) +- [View alert groupings]({{< relref "alert-groups/_index.md" >}}) +- [Add or edit an alert contact point]({{< relref "contact-points/_index.md" >}}) +- [Add or edit notification policies]({{< relref "notifications/_index.md" >}}) +- [Add or edit silences]({{< relref "silences/_index.md" >}}) diff --git a/docs/sources/alerting/alert-groups/_index.md b/docs/sources/alerting/alert-groups/_index.md index 364b7a875f5..28366c1e318 100644 --- a/docs/sources/alerting/alert-groups/_index.md +++ b/docs/sources/alerting/alert-groups/_index.md @@ -17,5 +17,5 @@ Alert groups show grouped alerts from an Alertmanager instance. By default, the For more information, see: -- [View alert groupings]({{< relref "./view-alert-grouping.md" >}}) -- [Filter alerts by group]({{< relref "./filter-alerts.md" >}}) +- [View alert groupings]({{< relref "view-alert-grouping.md" >}}) +- [Filter alerts by group]({{< relref "filter-alerts.md" >}}) diff --git a/docs/sources/alerting/alerting-rules/_index.md b/docs/sources/alerting/alerting-rules/_index.md index bbbf57cae8c..dae48011307 100644 --- a/docs/sources/alerting/alerting-rules/_index.md +++ b/docs/sources/alerting/alerting-rules/_index.md @@ -15,9 +15,9 @@ While queries and expressions select the data set to evaluate, a condition sets You can: -- [Create Grafana Mimir or Loki managed alert rule]({{< relref "./create-mimir-loki-managed-rule.md" >}}) -- [Create Grafana Mimir or Loki managed recording rule]({{< relref "./create-mimir-loki-managed-recording-rule.md" >}}) -- [Edit Grafana Mimir or Loki rule groups and namespaces]({{< relref "./edit-mimir-loki-namespace-group.md" >}}) -- [Create Grafana managed alert rule]({{< relref "./create-grafana-managed-rule.md" >}}) +- [Create Grafana Mimir or Loki managed alert rule]({{< relref "create-mimir-loki-managed-rule.md" >}}) +- [Create Grafana Mimir or Loki managed recording rule]({{< relref "create-mimir-loki-managed-recording-rule.md" >}}) +- [Edit Grafana Mimir or Loki rule groups and namespaces]({{< relref "edit-mimir-loki-namespace-group.md" >}}) +- [Create Grafana managed alert rule]({{< relref "create-grafana-managed-rule.md" >}}) - [State and health of alerting rules]({{< relref "../fundamentals/state-and-health.md" >}}) -- [Manage alerting rules]({{< relref "./rule-list.md" >}}) +- [Manage alerting rules]({{< relref "rule-list.md" >}}) 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 79f66448792..f9a6145812e 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 @@ -42,7 +42,7 @@ Grafana allows you to create alerting rules for an external Grafana Mimir or Lok > **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 3, add the rule name, namespace, rule group, as well as additional metadata associated with the rule. - 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. - - From the **Namespace** drop-down, 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]({{< relref "./edit-mimir-loki-namespace-group.md" >}}). + - From the **Namespace** drop-down, 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]({{< relref "edit-mimir-loki-namespace-group.md" >}}). - From the **Group** drop-down, 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]({{< relref "../fundamentals/annotation-label/_index.md" >}}). - Add Runbook URL, panel, dashboard, and alert IDs. diff --git a/docs/sources/alerting/alerting-rules/rule-list.md b/docs/sources/alerting/alerting-rules/rule-list.md index 5d09c68992a..56e6317f46a 100644 --- a/docs/sources/alerting/alerting-rules/rule-list.md +++ b/docs/sources/alerting/alerting-rules/rule-list.md @@ -62,5 +62,5 @@ Grafana managed alerting rules can only be edited or deleted by users with Edit To edit or delete a rule: 1. Expand a rule row until you can see the rule controls of **View**, **Edit**, and **Delete**. -1. Click **Edit** to open the create rule page. Make updates following instructions in [Create a Grafana managed alerting rule]({{< relref "./create-grafana-managed-rule.md" >}}) or [Create a Grafana Mimir or Loki managed alerting rule]({{< relref "./create-mimir-loki-managed-rule.md" >}}). +1. Click **Edit** to open the create rule page. Make updates following instructions in [Create a Grafana managed alerting rule]({{< relref "create-grafana-managed-rule.md" >}}) or [Create a Grafana Mimir or Loki managed alerting rule]({{< relref "create-mimir-loki-managed-rule.md" >}}). 1. Click **Delete** to delete a rule. diff --git a/docs/sources/alerting/contact-points/_index.md b/docs/sources/alerting/contact-points/_index.md index cf78e0dd760..f155418a48b 100644 --- a/docs/sources/alerting/contact-points/_index.md +++ b/docs/sources/alerting/contact-points/_index.md @@ -16,7 +16,7 @@ weight: 430 # Contact points -Use contact points to define how your contacts are notified when an alert fires. A contact point can have one or more contact point types, for example, email, slack, webhook, and so on. When an alert fires, a notification is sent to all contact point types listed for a contact point. Optionally, use [message templates]({{< relref "./message-templating/_index.md" >}}) to customize notification messages for the contact point types. +Use contact points to define how your contacts are notified when an alert fires. A contact point can have one or more contact point types, for example, email, slack, webhook, and so on. When an alert fires, a notification is sent to all contact point types listed for a contact point. Optionally, use [message templates]({{< relref "message-templating/_index.md" >}}) to customize notification messages for the contact point types. You can configure Grafana managed contact points as well as contact points for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}). diff --git a/docs/sources/alerting/contact-points/message-templating/_index.md b/docs/sources/alerting/contact-points/message-templating/_index.md index 299b974f4cd..95d92b8b016 100644 --- a/docs/sources/alerting/contact-points/message-templating/_index.md +++ b/docs/sources/alerting/contact-points/message-templating/_index.md @@ -15,9 +15,9 @@ weight: 400 # Message templating -Notifications sent via [contact points]({{< relref "../../contact-points/_index.md" >}}) are built using messaging templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go), is a useful reference for custom templates. +Notifications sent via [contact points]({{< relref "../_index.md" >}}) are built using messaging templates. Grafana's default templates are based on the [Go templating system](https://golang.org/pkg/text/template) where some fields are evaluated as text, while others are evaluated as HTML (which can affect escaping). The default template, defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go), is a useful reference for custom templates. -Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. The [template data]({{< relref "./template-data.md" >}}) topic lists variables that are available for templating. The default template is defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go) which can serve as a useful reference or starting point for custom templates. +Since most of the contact point fields can be templated, you can create reusable custom templates and use them in multiple contact points. The [template data]({{< relref "template-data.md" >}}) topic lists variables that are available for templating. The default template is defined in [default_template.go](https://github.com/grafana/grafana/blob/main/pkg/services/ngalert/notifier/channels/default_template.go) which can serve as a useful reference or starting point for custom templates. ### Using templates diff --git a/docs/sources/alerting/contact-points/message-templating/template-data.md b/docs/sources/alerting/contact-points/message-templating/template-data.md index 7f770f643da..d4341ef2aa4 100644 --- a/docs/sources/alerting/contact-points/message-templating/template-data.md +++ b/docs/sources/alerting/contact-points/message-templating/template-data.md @@ -14,7 +14,7 @@ weight: 120 # Template data -Template data is passed on to [message templates]({{< relref "./_index.md" >}}) as well as sent as payload to webhook pushes. +Template data is passed on to [message templates]({{< relref "_index.md" >}}) as well as sent as payload to webhook pushes. | Name | Type | Notes | | ----------------- | -------- | -------------------------------------------------------------------------------------------------------------------- | diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 98ede25db8a..47dca9fb6af 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -11,8 +11,7 @@ weight: 110 This section includes the following fundamental concepts of Grafana alerting: -- [Alert rules]({{< relref "./alert-rules/_index.md" >}}) -- [Annotations and labels for alerting rules]({{< relref "./annotation-label/_index.md" >}}) -- [Alertmanager]({{< relref "./alertmanager.md" >}}) -- [State and health of alerting rules]({{< relref "./state-and-health.md" >}}) -- [Evaluating Grafana managed alerts]({{< relref "./evaluate-grafana-alerts.md" >}}) +- [Annotations and labels for alerting rules]({{< relref "annotation-label/_index.md" >}}) +- [Alertmanager]({{< relref "alertmanager.md" >}}) +- [State and health of alerting rules]({{< relref "state-and-health.md" >}}) +- [Evaluating Grafana managed alerts]({{< relref "evaluate-grafana-alerts.md" >}}) diff --git a/docs/sources/alerting/fundamentals/annotation-label/_index.md b/docs/sources/alerting/fundamentals/annotation-label/_index.md index a1478c0bf78..997b8c11ca9 100644 --- a/docs/sources/alerting/fundamentals/annotation-label/_index.md +++ b/docs/sources/alerting/fundamentals/annotation-label/_index.md @@ -15,7 +15,7 @@ weight: 401 # Annotations and labels for alerting rules -Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in [templates]({{< relref "../../contact-points/message-templating/_index.md" >}}) and [template functions]({{< relref "../../contact-points/message-templating/template-functions" >}}) to create notification contact dynamically. +Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in [templates]({{< relref "../../contact-points/message-templating/_index.md" >}}) and [template functions]({{< relref "../../contact-points/message-templating/template-functions/" >}}) to create notification contact dynamically. ## Annotations @@ -27,6 +27,6 @@ Labels are key-value pairs that contain information about, and are used to uniqu Before you begin using annotations and labels, familiarize yourself with: -- [Labels in Grafana alerting]({{< relref "./how-to-use-labels.md" >}}) -- [How label matching works]({{< relref "./how-to-use-labels.md" >}}) -- [Template variables for alerting rule labels and annotations]({{< relref "./variables-label-annotation.md" >}}) +- [Labels in Grafana alerting]({{< relref "how-to-use-labels.md" >}}) +- [How label matching works]({{< relref "how-to-use-labels.md" >}}) +- [Template variables for alerting rule labels and annotations]({{< relref "variables-label-annotation.md" >}}) diff --git a/docs/sources/alerting/high-availability/_index.md b/docs/sources/alerting/high-availability/_index.md index 8f07d00fd58..b45af3e6d93 100644 --- a/docs/sources/alerting/high-availability/_index.md +++ b/docs/sources/alerting/high-availability/_index.md @@ -30,4 +30,4 @@ The two types of messages gossiped between Grafana instances are: The notification logs and silences are persisted in the database periodically and during a graceful Grafana shut down. -For configuration instructions, refer to [enable alerting high availability]({{< relref "./enable-alerting-ha.md" >}}). +For configuration instructions, refer to [enable alerting high availability]({{< relref "enable-alerting-ha.md" >}}). diff --git a/docs/sources/alerting/high-availability/enable-alerting-ha.md b/docs/sources/alerting/high-availability/enable-alerting-ha.md index 41aa31bcf9c..c040be3cf00 100644 --- a/docs/sources/alerting/high-availability/enable-alerting-ha.md +++ b/docs/sources/alerting/high-availability/enable-alerting-ha.md @@ -15,7 +15,7 @@ weight: 450 # Enable alerting high availability -You can enable [alerting high availability]({{< relref "./_index.md" >}}) support by updating the Grafana configuration file. On Kubernetes, you can enable alerting high availability by updating the Kubernetes container definition. +You can enable [alerting high availability]({{< relref "_index.md" >}}) support by updating the Grafana configuration file. On Kubernetes, you can enable alerting high availability by updating the Kubernetes container definition. ## Update Grafana configuration file diff --git a/docs/sources/alerting/silences/_index.md b/docs/sources/alerting/silences/_index.md index 259baa64ed0..84a6e5a8b57 100644 --- a/docs/sources/alerting/silences/_index.md +++ b/docs/sources/alerting/silences/_index.md @@ -25,4 +25,4 @@ See also: - [Create a silence]({{< relref "./create-silence.md" >}}) - [Create a URL to link to a silence form]({{< relref "./linking-to-silence-form.md" >}}) - [Edit silences]({{< relref "./edit-silence.md" >}}) -- [Remove a silences]({{< relref "./remove-silence.md" >}}) +- [Remove silences]({{< relref "./remove-silence.md" >}}) diff --git a/docs/sources/auth/_index.md b/docs/sources/auth/_index.md index c68ce381a0f..0249f47be01 100644 --- a/docs/sources/auth/_index.md +++ b/docs/sources/auth/_index.md @@ -14,14 +14,14 @@ Here is a table showing all supported authentication providers and the features See also, [Grafana Authentication]({{< relref "grafana.md" >}}). -| Provider | Support | Role mapping | Team sync
_(Enterprise only)_ | Active sync
_(Enterprise only)_ | -| -------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | -| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | -| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | -| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | -| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | -| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | -| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | -| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | -| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | -| [SAML]({{< relref "../enterprise/saml/" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | +| Provider | Support | Role mapping | Team sync
_(Enterprise only)_ | Active sync
_(Enterprise only)_ | +| ------------------------------------------------------------------------ | :-----: | :----------: | :-------------------------------: | :---------------------------------: | +| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | +| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | +| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | +| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | +| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | +| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | +| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | +| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | +| [SAML]({{< relref "../enterprise/configure-saml/" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | diff --git a/docs/sources/auth/enhanced_ldap.md b/docs/sources/auth/enhanced_ldap.md index 25085763a3d..503e7687db7 100644 --- a/docs/sources/auth/enhanced_ldap.md +++ b/docs/sources/auth/enhanced_ldap.md @@ -17,4 +17,4 @@ weight: 400 The enhanced LDAP integration adds additional functionality on top of the existing [LDAP integration]({{< relref "ldap.md" >}}). -> Enhanced LDAP integration is only available in Grafana Enterprise. For more information, refer to [Enhanced LDAP integration]({{< relref "../enterprise/enhanced_ldap.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> Enhanced LDAP integration is only available in Grafana Enterprise. For more information, refer to [Enhanced LDAP integration]({{< relref "../enterprise/enhanced_ldap.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise/" >}}). diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index 2a961a4d47c..36d2c697f91 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -18,7 +18,7 @@ weight: 300 The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP group memberships and Grafana Organization user roles. -> [Enhanced LDAP authentication]({{< relref "../enterprise/enhanced_ldap.md" >}}) is available in [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> [Enhanced LDAP authentication]({{< relref "../enterprise/enhanced_ldap.md" >}}) is available in [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise/" >}}). > Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how you can control access with role-based permissions. diff --git a/docs/sources/auth/okta.md b/docs/sources/auth/okta.md index 2d58b9b3a9b..63d0bbef56f 100644 --- a/docs/sources/auth/okta.md +++ b/docs/sources/auth/okta.md @@ -79,7 +79,7 @@ Grafana can attempt to do role mapping through Okta OAuth. In order to achieve t Grafana uses JSON obtained from querying the `/userinfo` endpoint for the path lookup. The result after evaluating the `role_attribute_path` JMESPath expression needs to be a valid Grafana role, i.e. `Viewer`, `Editor` or `Admin`. Refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}) for more information about roles and permissions in Grafana. -Read about how to [add custom claims](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/add-custom-claim/) to the user info in Okta. Also, check Generic OAuth page for [JMESPath examples]({{< relref "generic-oauth.md/#jmespath-examples" >}}). +Read about how to [add custom claims](https://developer.okta.com/docs/guides/customize-tokens-returned-from-okta/add-custom-claim/) to the user info in Okta. Also, check Generic OAuth page for [JMESPath examples]({{< relref "generic-oauth.md#jmespath-examples" >}}). ### Team Sync (Enterprise only) diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index e6f899d3cf3..ccbc4aaf66d 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -12,18 +12,18 @@ Grafana provides many ways to authenticate users. Some authentication integratio The following table shows all supported authentication providers and the features available for them. [Team sync]({{< relref "../enterprise/team-sync.md" >}}) and [active sync]({{< relref "../enterprise/enhanced_ldap.md#active-ldap-synchronization" >}}) are only available in Grafana Enterprise. -| Provider | Support | Role mapping | Team sync
_(Enterprise only)_ | Active sync
_(Enterprise only)_ | -| -------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | -| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | -| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | -| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | -| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | -| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | -| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | -| [JWT]({{< relref "jwt.md" >}}) | v8.0+ | - | - | - | -| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | -| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | -| [SAML]({{< relref "../enterprise/saml/" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | +| Provider | Support | Role mapping | Team sync
_(Enterprise only)_ | Active sync
_(Enterprise only)_ | +| -------------------------------------------------------------------------- | :-----: | :----------: | :-------------------------------: | :---------------------------------: | +| [Auth Proxy]({{< relref "auth-proxy.md" >}}) | v2.1+ | - | v6.3+ | - | +| [Azure AD OAuth]({{< relref "azuread.md" >}}) | v6.7+ | v6.7+ | v6.7+ | - | +| [Generic OAuth]({{< relref "generic-oauth.md" >}}) | v4.0+ | v6.5+ | - | - | +| [GitHub OAuth]({{< relref "github.md" >}}) | v2.0+ | - | v6.3+ | - | +| [GitLab OAuth]({{< relref "gitlab.md" >}}) | v5.3+ | - | v6.4+ | - | +| [Google OAuth]({{< relref "google.md" >}}) | v2.0+ | - | - | - | +| [JWT]({{< relref "jwt.md" >}}) | v8.0+ | - | - | - | +| [LDAP]({{< relref "ldap.md" >}}) | v2.1+ | v2.1+ | v5.3+ | v6.3+ | +| [Okta OAuth]({{< relref "okta.md" >}}) | v7.0+ | v7.0+ | v7.0+ | - | +| [SAML]({{< relref "../enterprise/configure-saml.md" >}}) (Enterprise only) | v6.3+ | v7.0+ | v7.0+ | - | ## Grafana Auth diff --git a/docs/sources/auth/saml.md b/docs/sources/auth/saml.md index efb7ee278c0..ce9a38ec00f 100644 --- a/docs/sources/auth/saml.md +++ b/docs/sources/auth/saml.md @@ -15,4 +15,4 @@ weight: 1100 The SAML authentication integration allows your Grafana users to log in by using an external SAML Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. -> SAML authentication integration is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. For more information, refer to [SAML authentication]({{< relref "../enterprise/saml/" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> SAML authentication integration is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. For more information, refer to [SAML authentication]({{< relref "../enterprise/configure-saml/" >}}) in [Grafana Enterprise]({{< relref "../enterprise/" >}}). diff --git a/docs/sources/auth/team-sync.md b/docs/sources/auth/team-sync.md index c47b4678610..e1d688bc06f 100644 --- a/docs/sources/auth/team-sync.md +++ b/docs/sources/auth/team-sync.md @@ -24,4 +24,4 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea
-> Team Sync is available in both Grafana Enterprise and Grafana Cloud Advanced. For more information, refer to [Team sync]({{< relref "../enterprise/team-sync.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> Team Sync is available in both Grafana Enterprise and Grafana Cloud Advanced. For more information, refer to [Team sync]({{< relref "../enterprise/team-sync.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise/" >}}). diff --git a/docs/sources/basics/exemplars/_index.md b/docs/sources/basics/exemplars/_index.md index c4bb32ac882..9e8c545dda5 100644 --- a/docs/sources/basics/exemplars/_index.md +++ b/docs/sources/basics/exemplars/_index.md @@ -27,4 +27,4 @@ Grafana shows exemplars alongside a metric in the Explore view and in dashboards {{< figure src="/static/img/docs/v74/exemplars.png" class="docs-image--no-shadow" max-width= "750px" caption="Screenshot showing the detail window of an Exemplar" >}} -Refer to [View exemplar data]({{< relref "./view-exemplars.md" >}}) for instructions on how to drill down and view exemplar trace details from metrics and logs. To know more about exemplars, refer to the blogpost [Intro to exemplars, which enable Grafana Tempo’s distributed tracing at massive scale](https://grafana.com/blog/2021/03/31/intro-to-exemplars-which-enable-grafana-tempos-distributed-tracing-at-massive-scale/). +Refer to [View exemplar data]({{< relref "view-exemplars.md" >}}) for instructions on how to drill down and view exemplar trace details from metrics and logs. To know more about exemplars, refer to the blogpost [Intro to exemplars, which enable Grafana Tempo’s distributed tracing at massive scale](https://grafana.com/blog/2021/03/31/intro-to-exemplars-which-enable-grafana-tempos-distributed-tracing-at-massive-scale/). diff --git a/docs/sources/best-practices/_index.md b/docs/sources/best-practices/_index.md index cb443d43145..0410112f78f 100644 --- a/docs/sources/best-practices/_index.md +++ b/docs/sources/best-practices/_index.md @@ -10,7 +10,7 @@ weight: 20 This section provides information about best practices for intermediate Grafana administrators and users. Click on each of the links before for more information. -- [Best practices for creating dashboards]({{< relref "best-practices-for-creating-dashboards" >}}) -- [Best practices for managing dashboards]({{< relref "best-practices-for-managing-dashboards" >}}) -- [Common observability strategies]({{< relref "common-observability-strategies" >}}) -- [Dashboard management maturity model]({{< relref "dashboard-management-maturity-levels" >}}) +- [Best practices for creating dashboards]({{< relref "best-practices-for-creating-dashboards/" >}}) +- [Best practices for managing dashboards]({{< relref "best-practices-for-managing-dashboards/" >}}) +- [Common observability strategies]({{< relref "common-observability-strategies/" >}}) +- [Dashboard management maturity model]({{< relref "dashboard-management-maturity-levels/" >}}) diff --git a/docs/sources/best-practices/dashboard-management-maturity-levels.md b/docs/sources/best-practices/dashboard-management-maturity-levels.md index 7c6d2bd727c..49c1c74ff0d 100644 --- a/docs/sources/best-practices/dashboard-management-maturity-levels.md +++ b/docs/sources/best-practices/dashboard-management-maturity-levels.md @@ -48,7 +48,7 @@ How can you tell you are here? - Compare like to like: split service dashboards when the magnitude differs. Make sure aggregated metrics don't drown out important information. - Expressive charts with meaningful use of color and normalizing axes where you can. - - Example of meaningful color: Blue means it's good, red means it's bad. [Thresholds]({{< relref "../panels/configure-thresholds" >}}) can help with that. + - Example of meaningful color: Blue means it's good, red means it's bad. [Thresholds]({{< relref "../panels/configure-thresholds/" >}}) can help with that. - Example of normalizing axes: When comparing CPU usage, measure by percentage rather than raw number, because machines can have a different number of cores. Normalizing CPU usage by the number of cores reduces cognitive load because the viewer can trust that at 100% all cores are being used, without having to know the number of CPUs. - Directed browsing cuts down on "guessing." - Template variables make it harder to “just browse” randomly or aimlessly. diff --git a/docs/sources/dashboards/_index.md b/docs/sources/dashboards/_index.md index e33089c3a07..6c0a834f478 100644 --- a/docs/sources/dashboards/_index.md +++ b/docs/sources/dashboards/_index.md @@ -14,17 +14,17 @@ Dashboard snapshots are static . Queries and expressions cannot be re-executed f Before you begin, ensure that you have configured a data source. See also: -- [Working with Grafana dashboard UI]({{< relref "./dashboard-ui/_index.md" >}}) -- [Dashboard folders]({{< relref "./dashboard-folders.md" >}}) -- [Create dashboard]({{< relref "./dashboard-create" >}}) -- [Manage dashboards]({{< relref "./dashboard-manage.md" >}}) -- [Annotations]({{< relref "./annotations.md" >}}) -- [Playlist]({{< relref "./playlist.md" >}}) -- [Search]({{< relref "./search.md" >}}) -- [Keyboard shortcuts]({{< relref "./shortcuts.md" >}}) -- [Reporting]({{< relref "./reporting.md" >}}) -- [Time range controls]({{< relref "./time-range-controls.md" >}}) -- [Dashboard version history]({{< relref "./dashboard-history.md" >}}) -- [Dashboard export and import]({{< relref "./export-import.md" >}}) -- [Dashboard JSON model]({{< relref "./json-model.md" >}}) -- [Scripted dashboards]({{< relref "./scripted-dashboards.md" >}}) +- [Working with Grafana dashboard UI]({{< relref "dashboard-ui/_index.md" >}}) +- [Dashboard folders]({{< relref "dashboard-folders.md" >}}) +- [Create dashboard]({{< relref "dashboard-create/" >}}) +- [Manage dashboards]({{< relref "dashboard-manage.md" >}}) +- [Annotations]({{< relref "annotations.md" >}}) +- [Playlist]({{< relref "playlist.md" >}}) +- [Search]({{< relref "search.md" >}}) +- [Keyboard shortcuts]({{< relref "shortcuts.md" >}}) +- [Reporting]({{< relref "reporting.md" >}}) +- [Time range controls]({{< relref "time-range-controls.md" >}}) +- [Dashboard version history]({{< relref "dashboard-history.md" >}}) +- [Dashboard export and import]({{< relref "export-import.md" >}}) +- [Dashboard JSON model]({{< relref "json-model.md" >}}) +- [Scripted dashboards]({{< relref "scripted-dashboards.md" >}}) diff --git a/docs/sources/dashboards/dashboard-ui/_index.md b/docs/sources/dashboards/dashboard-ui/_index.md index 6822d53a687..222b1a0da33 100644 --- a/docs/sources/dashboards/dashboard-ui/_index.md +++ b/docs/sources/dashboards/dashboard-ui/_index.md @@ -18,4 +18,4 @@ The dashboard UI has the following sections to allow you to customize the presen - **Dashboard panel** (4) Click the panel title to edit panels. - **Graph legend** (5) Change series colors, y-axis and series visibility directly from the legend. -For more details, see [Dashboard header]({{< relref "./dashboard-header.md" >}}) and [Dashboard rows]({{< relref "./dashboard-row.md" >}}). +For more details, see [Dashboard header]({{< relref "dashboard-header.md" >}}) and [Dashboard rows]({{< relref "dashboard-row.md" >}}). diff --git a/docs/sources/dashboards/previews.md b/docs/sources/dashboards/previews.md index fe0a546951d..fda993fe82f 100644 --- a/docs/sources/dashboards/previews.md +++ b/docs/sources/dashboards/previews.md @@ -77,7 +77,7 @@ Use the new [contextPerRenderKey]({{< relref "../image-rendering/#rendering-mode ### Saving previews -The crawler saves previews and their metadata in Grafana's DB. Preview's metadata contains, among other things, the [dashboard version]({{< relref "./dashboard-history" >}}) from the time of taking the screenshot. During subsequent runs, the crawler uses the saved version to find stale dashboard previews. +The crawler saves previews and their metadata in Grafana's DB. Preview's metadata contains, among other things, the [dashboard version]({{< relref "dashboard-history/" >}}) from the time of taking the screenshot. During subsequent runs, the crawler uses the saved version to find stale dashboard previews. ## Permissions @@ -86,7 +86,7 @@ The crawler saves previews and their metadata in Grafana's DB. Preview's metadat The crawler is set up with the required permissions to display all dashboards and query all data sources. The way the permissions are set up depends on the version of Grafana. In OSS and Enterprise Grafana instances without RBAC enabled, the crawler uses a special user with an `Admin` role. -In an Enterprise Grafana instance with RBAC enabled, the crawler uses [service accounts]({{< relref "../administration/service-accounts" >}}) with three fixed roles: +In an Enterprise Grafana instance with RBAC enabled, the crawler uses [service accounts]({{< relref "../administration/service-accounts/" >}}) with three fixed roles: - `fixed:dashboards:reader` - `fixed:datasources:reader` diff --git a/docs/sources/dashboards/reporting.md b/docs/sources/dashboards/reporting.md index b3573056b54..f2d1154b785 100644 --- a/docs/sources/dashboards/reporting.md +++ b/docs/sources/dashboards/reporting.md @@ -16,4 +16,4 @@ Reporting allows you to generate PDFs from any of your dashboards and have them {{< figure src="/static/img/docs/enterprise/reports_list.png" max-width="500px" class="docs-image--no-shadow" >}} -> Reporting is only available in Grafana Enterprise, v6.4 or later. For more information, refer to [Reporting]({{< relref "../enterprise/reporting.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> Reporting is only available in Grafana Enterprise, v6.4 or later. For more information, refer to [Reporting]({{< relref "../enterprise/reporting.md" >}}) in [Grafana Enterprise]({{< relref "../enterprise/" >}}). diff --git a/docs/sources/datasources/azuremonitor/_index.md b/docs/sources/datasources/azuremonitor/_index.md index 5528e2c3548..a9e09cf2d03 100644 --- a/docs/sources/datasources/azuremonitor/_index.md +++ b/docs/sources/datasources/azuremonitor/_index.md @@ -280,9 +280,9 @@ If a request exceeds the [maximum allowed value of records](https://docs.microso See the following topics to learn more about the Azure Monitor data source: -- [Azure Monitor template variables]({{< relref "./template-variables.md" >}}) for more interactive, dynamic, and reusable dashboards. -- [Provisioning Azure Monitor]({{< relref "./provisioning.md" >}}) for configuring the Azure Monitor data source using YAML files -- [Deprecating Application Insights]({{< relref "./provisioning.md" >}}) and migrating to Metrics and Logs queries +- [Azure Monitor template variables]({{< relref "template-variables.md" >}}) for more interactive, dynamic, and reusable dashboards. +- [Provisioning Azure Monitor]({{< relref "provisioning.md" >}}) for configuring the Azure Monitor data source using YAML files +- [Deprecating Application Insights]({{< relref "provisioning.md" >}}) and migrating to Metrics and Logs queries ### Configuring using Managed Identity diff --git a/docs/sources/datasources/elasticsearch.md b/docs/sources/datasources/elasticsearch.md index e66013dd91e..e1f4ac8e623 100644 --- a/docs/sources/datasources/elasticsearch.md +++ b/docs/sources/datasources/elasticsearch.md @@ -81,7 +81,7 @@ When `X-Pack enabled` is active and the configured Elasticsearch version is high ### Logs There are two parameters, `Message field name` and `Level field name`, that can optionally be configured from the data source settings page that determine -which fields will be used for log messages and log levels when visualizing logs in [Explore]({{< relref "../explore" >}}). +which fields will be used for log messages and log levels when visualizing logs in [Explore]({{< relref "../explore/" >}}). For example, if you're using a default setup of Filebeat for shipping logs to Elasticsearch the following configuration should work: @@ -194,7 +194,7 @@ for annotation events. ## Querying Logs -Querying and displaying log data from Elasticsearch is available in [Explore]({{< relref "../explore" >}}), and in the [logs panel]({{< relref "../visualizations/logs-panel.md" >}}) in dashboards. +Querying and displaying log data from Elasticsearch is available in [Explore]({{< relref "../explore/" >}}), and in the [logs panel]({{< relref "../visualizations/logs-panel.md" >}}) in dashboards. Select the Elasticsearch data source, and then optionally enter a lucene query to display your logs. When switching from a Prometheus or Loki data source in Explore, your query is translated to an Elasticsearch log query with a correct Lucene filter. @@ -264,6 +264,6 @@ For more details on AWS SigV4, refer to the [AWS documentation](https://docs.aws In order to sign requests to your Amazon Elasticsearch Service domain, SigV4 can be enabled in the Grafana [configuration]({{< relref "../administration/configuration.md#sigv4_auth_enabled" >}}). -Once AWS SigV4 is enabled, it can be configured on the Elasticsearch data source configuration page. Refer to [Cloudwatch authentication]({{< relref "../datasources/aws-cloudwatch/aws-authentication.md" >}}) for more information about authentication options. +Once AWS SigV4 is enabled, it can be configured on the Elasticsearch data source configuration page. Refer to [Cloudwatch authentication]({{< relref "aws-cloudwatch/aws-authentication.md" >}}) for more information about authentication options. {{< figure src="/static/img/docs/v73/elasticsearch-sigv4-config-editor.png" max-width="500px" class="docs-image--no-shadow" caption="SigV4 configuration for AWS Elasticsearch Service" >}} diff --git a/docs/sources/datasources/google-cloud-monitoring/preconfig-cloud-monitoring-dashboards.md b/docs/sources/datasources/google-cloud-monitoring/preconfig-cloud-monitoring-dashboards.md index c23a2be9a73..8222fbae20e 100644 --- a/docs/sources/datasources/google-cloud-monitoring/preconfig-cloud-monitoring-dashboards.md +++ b/docs/sources/datasources/google-cloud-monitoring/preconfig-cloud-monitoring-dashboards.md @@ -17,7 +17,7 @@ weight: 10 # Preconfigured Cloud Monitoring dashboards -Google Cloud Monitoring data source ships with pre-configured dashboards for some of the most popular GCP services. These curated dashboards are based on similar dashboards in the GCP dashboard samples repository. See also, [Using Google Cloud Monitoring in Grafana]({{< relref "./_index.md" >}}) for detailed instructions on how to add and configure the Google Cloud Monitoring data source. +Google Cloud Monitoring data source ships with pre-configured dashboards for some of the most popular GCP services. These curated dashboards are based on similar dashboards in the GCP dashboard samples repository. See also, [Using Google Cloud Monitoring in Grafana]({{< relref "_index.md" >}}) for detailed instructions on how to add and configure the Google Cloud Monitoring data source. ## Curated dashboards diff --git a/docs/sources/datasources/jaeger.md b/docs/sources/datasources/jaeger.md index f6a5518b58d..52ea81519cd 100644 --- a/docs/sources/datasources/jaeger.md +++ b/docs/sources/datasources/jaeger.md @@ -34,7 +34,7 @@ To access Jaeger settings, click the **Configuration** (gear) icon, then click * > **Note:** This feature is available in Grafana 7.4+. -This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki and Splunk \[logs\] data sources) and select which tags will be used in the logs query. +This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration/" >}}). Select target data source (at this moment limited to Loki and Splunk \[logs\] data sources) and select which tags will be used in the logs query. - **Data source -** Target data source. - **Tags -** The tags that will be used in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. diff --git a/docs/sources/datasources/loki.md b/docs/sources/datasources/loki.md index 7c2f7fbea70..5645cb0950d 100644 --- a/docs/sources/datasources/loki.md +++ b/docs/sources/datasources/loki.md @@ -16,7 +16,7 @@ weight: 800 Grafana ships with built-in support for Loki, an open source log aggregation system by Grafana Labs. This topic explains options, variables, querying, and other options specific to this data source. -Add it as a data source and you are ready to build dashboards or query your log data in [Explore]({{< relref "../explore" >}}). Refer to [Add a data source]({{< relref "add-a-data-source.md" >}}) for instructions on how to add a data source to Grafana. Only users with the organization admin role can add data sources. +Add it as a data source and you are ready to build dashboards or query your log data in [Explore]({{< relref "../explore/" >}}). Refer to [Add a data source]({{< relref "add-a-data-source.md" >}}) for instructions on how to add a data source to Grafana. Only users with the organization admin role can add data sources. ## Hosted Loki @@ -138,7 +138,7 @@ There are two types of LogQL queries: ### Log queries -Loki log queries return the contents of the log lines. Querying and displaying log data from Loki is available via [Explore]({{< relref "../explore" >}}), and with the [logs panel]({{< relref "../visualizations/logs-panel.md" >}}) in dashboards. Select the Loki data source, and then enter a LogQL query to display your logs.F or more information about log queries and LogQL, refer to the [Loki log queries documentation](https://grafana.com/docs/loki/latest/logql/log_queries/) +Loki log queries return the contents of the log lines. Querying and displaying log data from Loki is available via [Explore]({{< relref "../explore/" >}}), and with the [logs panel]({{< relref "../visualizations/logs-panel.md" >}}) in dashboards. Select the Loki data source, and then enter a LogQL query to display your logs.F or more information about log queries and LogQL, refer to the [Loki log queries documentation](https://grafana.com/docs/loki/latest/logql/log_queries/) #### Log context @@ -222,7 +222,7 @@ You can use some global built-in variables in query variables; `$__interval`, `$ ## Annotations -You can use any non-metric Loki query as a source for [annotations]({{< relref "../dashboards/annotations" >}}). Log content will be used as annotation text and your log stream labels as tags, so there is no need for additional mapping. +You can use any non-metric Loki query as a source for [annotations]({{< relref "../dashboards/annotations/" >}}). Log content will be used as annotation text and your log stream labels as tags, so there is no need for additional mapping. ## Configure the data source with provisioning diff --git a/docs/sources/datasources/tempo.md b/docs/sources/datasources/tempo.md index 6ca483d28a3..8aebdfeaf4d 100644 --- a/docs/sources/datasources/tempo.md +++ b/docs/sources/datasources/tempo.md @@ -34,7 +34,7 @@ To access Tempo settings, click the **Configuration** (gear) icon, then click ** > **Note:** This feature is available in Grafana 7.4+. -This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki or Splunk \[logs\] data sources) and select which tags will be used in the logs query. +This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration/" >}}). Select target data source (at this moment limited to Loki or Splunk \[logs\] data sources) and select which tags will be used in the logs query. - **Data source -** Target data source. - **Tags -** The tags that will be used in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. diff --git a/docs/sources/datasources/zipkin.md b/docs/sources/datasources/zipkin.md index e07d659848b..695284cef80 100644 --- a/docs/sources/datasources/zipkin.md +++ b/docs/sources/datasources/zipkin.md @@ -14,7 +14,7 @@ weight: 1600 # Zipkin data source Grafana ships with built-in support for Zipkin, an open source, distributed tracing system. -Just add it as a data source and you are ready to query your traces in [Explore]({{< relref "../explore" >}}). +Just add it as a data source and you are ready to query your traces in [Explore]({{< relref "../explore/" >}}). ## Adding the data source @@ -33,7 +33,7 @@ To access Zipkin settings, click the **Configuration** (gear) icon, then click * > **Note:** This feature is available in Grafana 7.4+. -This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration" >}}). Select target data source (at this moment limited to Loki or Splunk \[logs\] data sources) and select which tags will be used in the logs query. +This is a configuration for the [trace to logs feature]({{< relref "../explore/trace-integration/" >}}). Select target data source (at this moment limited to Loki or Splunk \[logs\] data sources) and select which tags will be used in the logs query. - **Data source -** Target data source. - **Tags -** The tags that will be used in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. @@ -66,7 +66,7 @@ This is a configuration for the beta Node Graph visualization. The Node Graph is ## Query traces -Querying and displaying traces from Zipkin is available via [Explore]({{< relref "../explore" >}}). +Querying and displaying traces from Zipkin is available via [Explore]({{< relref "../explore/" >}}). {{< figure src="/static/img/docs/v70/zipkin-query-editor.png" class="docs-image--no-shadow" caption="Screenshot of the Zipkin query editor" >}} @@ -116,4 +116,4 @@ Here is an example JSON: ## Linking Trace ID from logs -You can link to Zipkin trace from logs in Loki or Splunk by configuring a derived field with internal link. See [Loki documentation]({{< relref "loki#derived-fields" >}}) for details. +You can link to Zipkin trace from logs in Loki or Splunk by configuring a derived field with internal link. See [Loki documentation]({{< relref "loki/#derived-fields" >}}) for details. diff --git a/docs/sources/developers/http_api/access_control.md b/docs/sources/developers/http_api/access_control.md index b523a59ceba..d6c1a4cbc16 100644 --- a/docs/sources/developers/http_api/access_control.md +++ b/docs/sources/developers/http_api/access_control.md @@ -17,7 +17,7 @@ title: RBAC HTTP API # RBAC API -> Role-based access control API is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise" >}}). +> Role-based access control API is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). The API can be used to create, update, delete, get, and list roles. @@ -222,7 +222,7 @@ Content-Type: application/json; charset=UTF-8 `POST /api/access-control/roles` -Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as [Fixed roles]({{< relref "../../enterprise/access-control/about-rbac#fixed-roles" >}}) can't be created. +Creates a new custom role and maps given permissions to that role. Note that roles with the same prefix as [Fixed roles]({{< relref "../../enterprise/access-control/about-rbac/#fixed-roles" >}}) can't be created. #### Required permissions @@ -260,24 +260,24 @@ Content-Type: application/json #### JSON body schema -| Field Name | Date Type | Required | Description | -| ----------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| uid | string | No | UID of the role. If not present, the UID will be automatically created for you and returned in response. Refer to the [Custom roles]({{< relref "../../enterprise/access-control/about-rbac#custom-roles" >}}) for more information. | -| global | boolean | No | A flag indicating if the role is global or not. If set to `false`, the default org ID of the authenticated user will be used from the request. | -| version | number | No | Version of the role. If not present, version 0 will be assigned to the role and returned in the response. Refer to the [Custom roles]({{< relref "../../enterprise/access-control/about-rbac#custom-roles" >}}) for more information. | -| name | string | Yes | Name of the role. Refer to [Custom roles]({{< relref "../../enterprise/access-control/about-rbac#custom-roles" >}}) for more information. | -| description | string | No | Description of the role. | -| displayName | string | No | Display name of the role, visible in the UI. | -| group | string | No | The group name the role belongs to. | -| hidden | boolean | No | Specify whether the role is hidden or not. If set to `true`, then the role does not show in the role picker. It will not be listed by API endpoints unless explicitly specified. | -| permissions | Permission | No | If not present, the role will be created without any permissions. | +| Field Name | Date Type | Required | Description | +| ----------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| uid | string | No | UID of the role. If not present, the UID will be automatically created for you and returned in response. Refer to the [Custom roles]({{< relref "../../enterprise/access-control/about-rbac/#custom-roles" >}}) for more information. | +| global | boolean | No | A flag indicating if the role is global or not. If set to `false`, the default org ID of the authenticated user will be used from the request. | +| version | number | No | Version of the role. If not present, version 0 will be assigned to the role and returned in the response. Refer to the [Custom roles]({{< relref "../../enterprise/access-control/about-rbac/#custom-roles" >}}) for more information. | +| name | string | Yes | Name of the role. Refer to [Custom roles]({{< relref "../../enterprise/access-control/about-rbac/#custom-roles" >}}) for more information. | +| description | string | No | Description of the role. | +| displayName | string | No | Display name of the role, visible in the UI. | +| group | string | No | The group name the role belongs to. | +| hidden | boolean | No | Specify whether the role is hidden or not. If set to `true`, then the role does not show in the role picker. It will not be listed by API endpoints unless explicitly specified. | +| permissions | Permission | No | If not present, the role will be created without any permissions. | **Permission** -| Field Name | Data Type | Required | Description | -| ---------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| action | string | Yes | Refer to [Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for full list of available actions. | -| scope | string | No | If not present, no scope will be mapped to the permission. Refer to [[Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for full list of available scopes. | +| Field Name | Data Type | Required | Description | +| ---------- | --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| action | string | Yes | Refer to [Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for full list of available actions. | +| scope | string | No | If not present, no scope will be mapped to the permission. Refer to [[Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for full list of available scopes. | #### Example response @@ -375,10 +375,10 @@ Content-Type: application/json **Permission** -| Field Name | Data Type | Required | Description | -| ---------- | --------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| action | string | Yes | Refer to [Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for full list of available actions. | -| scope | string | No | If not present, no scope will be mapped to the permission. Refer to [Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for full list of available scopes. | +| Field Name | Data Type | Required | Description | +| ---------- | --------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| action | string | Yes | Refer to [Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for full list of available actions. | +| scope | string | No | If not present, no scope will be mapped to the permission. Refer to [Custom role actions and scopes]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for full list of available scopes. | #### Example response @@ -448,10 +448,10 @@ Accept: application/json #### Query parameters -| Param | Type | Required | Description | -| ------ | ------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| force | boolean | No | When set to `true`, the role will be deleted with all it's assignments. | -| global | boolean | No | A flag indicating if the role is global or not. If set to false, the default org ID of the authenticated user will be used from the request. Refer to the [About RBAC]({{< relref "../../enterprise/access-control/about-rbac" >}}) for more information. | +| Param | Type | Required | Description | +| ------ | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| force | boolean | No | When set to `true`, the role will be deleted with all it's assignments. | +| global | boolean | No | A flag indicating if the role is global or not. If set to false, the default org ID of the authenticated user will be used from the request. Refer to the [About RBAC]({{< relref "../../enterprise/access-control/about-rbac/" >}}) for more information. | #### Example response diff --git a/docs/sources/developers/http_api/admin.md b/docs/sources/developers/http_api/admin.md index 412db306a79..d33b9f94ee8 100644 --- a/docs/sources/developers/http_api/admin.md +++ b/docs/sources/developers/http_api/admin.md @@ -18,7 +18,7 @@ The Admin HTTP API does not currently work with an API Token. API Tokens are cur the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user must have the Grafana Admin permission. (The default admin user is called `admin` and has permission to use this API.) -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Fetch settings diff --git a/docs/sources/developers/http_api/annotations.md b/docs/sources/developers/http_api/annotations.md index 7e6709140bd..251156ca36d 100644 --- a/docs/sources/developers/http_api/annotations.md +++ b/docs/sources/developers/http_api/annotations.md @@ -18,7 +18,7 @@ title: 'Annotations HTTP API ' This is the API documentation for the new Grafana Annotations feature released in Grafana 4.6. Annotations are saved in the Grafana database (sqlite, mysql or postgres). Annotations can be organization annotations that can be shown on any dashboard by configuring an annotation data source - they are filtered by tags. Or they can be tied to a panel on a dashboard and are then only shown on that panel. -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Find Annotations diff --git a/docs/sources/developers/http_api/auth.md b/docs/sources/developers/http_api/auth.md index 6cec6f4949c..1783e53f6e5 100644 --- a/docs/sources/developers/http_api/auth.md +++ b/docs/sources/developers/http_api/auth.md @@ -15,7 +15,7 @@ title: 'Authentication HTTP API ' # Authentication API -> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you would need to have relevant permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Tokens diff --git a/docs/sources/developers/http_api/curl-examples.md b/docs/sources/developers/http_api/curl-examples.md index f8f7be5c2fc..8b600ff74bd 100644 --- a/docs/sources/developers/http_api/curl-examples.md +++ b/docs/sources/developers/http_api/curl-examples.md @@ -22,7 +22,7 @@ The most basic example for a dashboard for which there is no authentication. You curl http://localhost:3000/api/search ``` -Here’s a cURL command that works for getting the home dashboard when you are running Grafana locally with [basic authentication]({{< relref "../../auth#basic-auth" >}}) enabled using the default admin credentials: +Here’s a cURL command that works for getting the home dashboard when you are running Grafana locally with [basic authentication]({{< relref "../../auth/#basic-auth" >}}) enabled using the default admin credentials: ``` curl http://admin:admin@localhost:3000/api/search diff --git a/docs/sources/developers/http_api/dashboard.md b/docs/sources/developers/http_api/dashboard.md index 1ff52ba193c..05b45769ace 100644 --- a/docs/sources/developers/http_api/dashboard.md +++ b/docs/sources/developers/http_api/dashboard.md @@ -14,7 +14,7 @@ title: 'Dashboard HTTP API ' # Dashboard API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Identifier (id) vs unique identifier (uid) diff --git a/docs/sources/developers/http_api/dashboard_permissions.md b/docs/sources/developers/http_api/dashboard_permissions.md index a04d70b8cde..e111a66d29c 100644 --- a/docs/sources/developers/http_api/dashboard_permissions.md +++ b/docs/sources/developers/http_api/dashboard_permissions.md @@ -28,7 +28,7 @@ The permission levels for the permission field: - 2 = Edit - 4 = Admin -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get permissions for a dashboard diff --git a/docs/sources/developers/http_api/data_source.md b/docs/sources/developers/http_api/data_source.md index 6630998829c..008ac9f43d7 100644 --- a/docs/sources/developers/http_api/data_source.md +++ b/docs/sources/developers/http_api/data_source.md @@ -15,7 +15,7 @@ title: 'Data source HTTP API ' # Data source API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get all data sources diff --git a/docs/sources/developers/http_api/datasource_permissions.md b/docs/sources/developers/http_api/datasource_permissions.md index 15bbb406e95..e2b4128b1b2 100644 --- a/docs/sources/developers/http_api/datasource_permissions.md +++ b/docs/sources/developers/http_api/datasource_permissions.md @@ -19,9 +19,9 @@ title: 'Datasource Permissions HTTP API ' # Data Source Permissions API -> The Data Source Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise" >}}). +> The Data Source Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. This API can be used to enable, disable, list, add and remove permissions for a data source. diff --git a/docs/sources/developers/http_api/external_group_sync.md b/docs/sources/developers/http_api/external_group_sync.md index 681b894d5e5..34225e0d0f2 100644 --- a/docs/sources/developers/http_api/external_group_sync.md +++ b/docs/sources/developers/http_api/external_group_sync.md @@ -18,9 +18,9 @@ title: 'External Group Sync HTTP API ' # External Group Synchronization API -> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise" >}}). +> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get External Groups diff --git a/docs/sources/developers/http_api/folder.md b/docs/sources/developers/http_api/folder.md index 845c47f6249..98c737c6ed5 100644 --- a/docs/sources/developers/http_api/folder.md +++ b/docs/sources/developers/http_api/folder.md @@ -14,7 +14,7 @@ title: 'Folder HTTP API ' # Folder API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Identifier (id) vs unique identifier (uid) diff --git a/docs/sources/developers/http_api/folder_dashboard_search.md b/docs/sources/developers/http_api/folder_dashboard_search.md index f0f1da0d848..85ff060589f 100644 --- a/docs/sources/developers/http_api/folder_dashboard_search.md +++ b/docs/sources/developers/http_api/folder_dashboard_search.md @@ -20,7 +20,7 @@ title: 'Folder/Dashboard Search HTTP API ' `GET /api/search/` -> Note: When using [Role-based access control]({{< relref "../../enterprise/access-control" >}}), search results will contain only dashboards and folders which you have access to. +> Note: When using [Role-based access control]({{< relref "../../enterprise/access-control/" >}}), search results will contain only dashboards and folders which you have access to. Query parameters: diff --git a/docs/sources/developers/http_api/folder_permissions.md b/docs/sources/developers/http_api/folder_permissions.md index b3bff5feccd..e5989981921 100644 --- a/docs/sources/developers/http_api/folder_permissions.md +++ b/docs/sources/developers/http_api/folder_permissions.md @@ -28,7 +28,7 @@ The permission levels for the permission field: - 2 = Edit - 4 = Admin -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Get permissions for a folder diff --git a/docs/sources/developers/http_api/licensing.md b/docs/sources/developers/http_api/licensing.md index 923cd1c5104..3328b214d01 100644 --- a/docs/sources/developers/http_api/licensing.md +++ b/docs/sources/developers/http_api/licensing.md @@ -15,9 +15,9 @@ title: 'Licensing HTTP API ' # Enterprise License API -Licensing is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise" >}}). +Licensing is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Check license availability diff --git a/docs/sources/developers/http_api/org.md b/docs/sources/developers/http_api/org.md index b395874221c..5b3f152c822 100644 --- a/docs/sources/developers/http_api/org.md +++ b/docs/sources/developers/http_api/org.md @@ -19,7 +19,7 @@ The Organization HTTP API is divided in two resources, `/api/org` (current organ and `/api/orgs` (admin organizations). One big difference between these are that the admin of all organizations API only works with basic authentication, see [Admin Organizations API](#admin-organizations-api) for more information. -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Current Organization API diff --git a/docs/sources/developers/http_api/reporting.md b/docs/sources/developers/http_api/reporting.md index 0c53c5a495b..9f0750b1bf5 100644 --- a/docs/sources/developers/http_api/reporting.md +++ b/docs/sources/developers/http_api/reporting.md @@ -15,9 +15,9 @@ title: Reporting API This API allows you to interact programmatically with the [Reporting]({{< relref "../../enterprise/reporting.md" >}}) feature. -> Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise" >}}). +> Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Send a report diff --git a/docs/sources/developers/http_api/serviceaccount.md b/docs/sources/developers/http_api/serviceaccount.md index 017296891e3..0e12e8e285e 100644 --- a/docs/sources/developers/http_api/serviceaccount.md +++ b/docs/sources/developers/http_api/serviceaccount.md @@ -14,7 +14,7 @@ title: 'Service account HTTP API ' # Service account API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Search service accounts with Paging diff --git a/docs/sources/developers/http_api/team.md b/docs/sources/developers/http_api/team.md index 9e0eb7d7654..8b6aebb6bc2 100644 --- a/docs/sources/developers/http_api/team.md +++ b/docs/sources/developers/http_api/team.md @@ -25,7 +25,7 @@ Access to these API endpoints is restricted as follows: - If you enable `editors_can_admin` configuration flag, then Organization Editors can create teams and manage teams where they are Admin. - If you enable `editors_can_admin` configuration flag, Editors can find out whether a team that they are not members of exists by trying to create a team with the same name. -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Team Search With Paging diff --git a/docs/sources/developers/http_api/user.md b/docs/sources/developers/http_api/user.md index 3e7ccedfa4a..ab45168a5ee 100644 --- a/docs/sources/developers/http_api/user.md +++ b/docs/sources/developers/http_api/user.md @@ -14,7 +14,7 @@ title: 'User HTTP API ' # User API -> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes" >}}) for more information. +> If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../enterprise/access-control/custom-role-actions-scopes/" >}}) for more information. ## Search Users diff --git a/docs/sources/developers/plugins/_index.md b/docs/sources/developers/plugins/_index.md index abfabf73b15..e66992f6477 100644 --- a/docs/sources/developers/plugins/_index.md +++ b/docs/sources/developers/plugins/_index.md @@ -24,8 +24,8 @@ npx @grafana/toolkit plugin:create my-grafana-plugin If you want a more guided introduction to plugin development, check out our tutorials: -- [Build a panel plugin]({{< relref "/tutorials/build-a-panel-plugin.md" >}}) -- [Build a data source plugin]({{< relref "/tutorials/build-a-data-source-plugin.md" >}}) +- [Build a panel plugin]({{< relref "tutorials/build-a-panel-plugin.md" >}}) +- [Build a data source plugin]({{< relref "tutorials/build-a-data-source-plugin.md" >}}) ## Go further @@ -35,19 +35,19 @@ Learn more about specific areas of plugin development. If you're looking to build your first plugin, check out these introductory tutorials: -- [Build a panel plugin]({{< relref "/tutorials/build-a-panel-plugin.md" >}}) -- [Build a data source plugin]({{< relref "/tutorials/build-a-data-source-plugin.md" >}}) -- [Build a data source backend plugin]({{< relref "/tutorials/build-a-data-source-backend-plugin.md" >}}) +- [Build a panel plugin]({{< relref "tutorials/build-a-panel-plugin.md" >}}) +- [Build a data source plugin]({{< relref "tutorials/build-a-data-source-plugin.md" >}}) +- [Build a data source backend plugin]({{< relref "tutorials/build-a-data-source-backend-plugin.md" >}}) Ready to learn more? Check out our other tutorials: -- [Build a panel plugin with D3.js]({{< relref "/tutorials/build-a-panel-plugin-with-d3.md" >}}) +- [Build a panel plugin with D3.js]({{< relref "tutorials/build-a-panel-plugin-with-d3.md" >}}) ### Guides Improve an existing plugin with one of our guides: -- [Add authentication for data source plugins]({{< relref "add-authentication-for-data-source-plugins" >}}) +- [Add authentication for data source plugins]({{< relref "add-authentication-for-data-source-plugins/" >}}) - [Add support for annotations]({{< relref "add-support-for-annotations.md" >}}) - [Add support for Explore queries]({{< relref "add-support-for-explore-queries.md" >}}) - [Add support for variables]({{< relref "add-support-for-variables.md" >}}) @@ -88,4 +88,4 @@ Learn more about Grafana options and packages. #### Go -- [Grafana Plugin SDK for Go]({{< relref "backend/grafana-plugin-sdk-for-go" >}}) +- [Grafana Plugin SDK for Go]({{< relref "backend/grafana-plugin-sdk-for-go/" >}}) diff --git a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md index a92566fb965..4385945f1a7 100644 --- a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md +++ b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md @@ -268,7 +268,7 @@ While the data source proxy supports the most common authentication methods for - Proxy routes only support HTTP or HTTPS - Proxy routes don't support custom token authentication -If any of these limitations apply to your plugin, you need to add a [backend plugin]({{< relref "./backend/_index.md" >}}). Since backend plugins run on the server they can access decrypted secrets, which makes it easier to implement custom authentication methods. +If any of these limitations apply to your plugin, you need to add a [backend plugin]({{< relref "backend/_index.md" >}}). Since backend plugins run on the server they can access decrypted secrets, which makes it easier to implement custom authentication methods. The decrypted secrets are available from the `DecryptedSecureJSONData` field in the instance settings. diff --git a/docs/sources/developers/plugins/add-support-for-annotations.md b/docs/sources/developers/plugins/add-support-for-annotations.md index 8355ae00fb3..1a6795f6ae9 100644 --- a/docs/sources/developers/plugins/add-support-for-annotations.md +++ b/docs/sources/developers/plugins/add-support-for-annotations.md @@ -8,7 +8,7 @@ title: Add support for annotations This guide explains how to add support for [annotations]({{< relref "../../dashboards/annotations.md" >}}) to an existing data source plugin. -This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "/tutorials/build-a-data-source-plugin.md" >}}). +This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "tutorials/build-a-data-source-plugin.md" >}}). > **Note:** Annotation support for React plugins was released in Grafana 7.2. To support earlier versions, refer to the [Add support for annotation for Grafana 7.1](https://grafana.com/docs/grafana/v7.1/developers/plugins/add-support-for-annotations/). diff --git a/docs/sources/developers/plugins/add-support-for-explore-queries.md b/docs/sources/developers/plugins/add-support-for-explore-queries.md index 224cff98120..6eb58df963e 100644 --- a/docs/sources/developers/plugins/add-support-for-explore-queries.md +++ b/docs/sources/developers/plugins/add-support-for-explore-queries.md @@ -8,7 +8,7 @@ title: Add support for Explore queries This guide explains how to improve support for [Explore]({{< relref "../../explore/_index.md" >}}) in an existing data source plugin. -This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "/tutorials/build-a-data-source-plugin.md" >}}). +This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "tutorials/build-a-data-source-plugin.md" >}}). With Explore, users can make ad-hoc queries without the use of a dashboard. This is useful when users want to troubleshoot or to learn more about the data. diff --git a/docs/sources/developers/plugins/backend/_index.md b/docs/sources/developers/plugins/backend/_index.md index a370973db06..f536e1b6f54 100644 --- a/docs/sources/developers/plugins/backend/_index.md +++ b/docs/sources/developers/plugins/backend/_index.md @@ -26,7 +26,7 @@ Data source plugins can be extended with a backend component. In the future we p The following examples gives you an idea of why you'd consider implementing a backend plugin: -- Enable [Grafana alerting]({{< relref "../../../alerting" >}}) for data sources. +- Enable [Grafana alerting]({{< relref "../../../alerting/" >}}) for data sources. - Connect to non-HTTP services that normally can't be connected to from a web browser, e.g. SQL database servers. - Keep state between users, e.g. query caching for data sources. - Use custom authentication methods and/or authorization checks that aren't supported in Grafana. @@ -49,7 +49,7 @@ Grafana's backend plugin system exposes a couple of different capabilities, or b ### Query data -The query data capability allows a backend plugin to handle data source queries that are submitted from a [dashboard]({{< relref "../../../dashboards/_index.md" >}}), [Explore]({{< relref "../../../explore/_index.md" >}}) or [Grafana Alerting]({{< relref "../../../alerting" >}}). The response contains [data frames]({{< relref "../data-frames.md" >}}), which are used to visualize metrics, logs, and traces. The query data capability is required to implement for a backend data source plugin. +The query data capability allows a backend plugin to handle data source queries that are submitted from a [dashboard]({{< relref "../../../dashboards/_index.md" >}}), [Explore]({{< relref "../../../explore/_index.md" >}}) or [Grafana Alerting]({{< relref "../../../alerting/" >}}). The response contains [data frames]({{< relref "../data-frames.md" >}}), which are used to visualize metrics, logs, and traces. The query data capability is required to implement for a backend data source plugin. ### Resources diff --git a/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md b/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md index bda48878be4..6abe0ab9029 100644 --- a/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md +++ b/docs/sources/developers/plugins/build-a-logs-data-source-plugin.md @@ -8,7 +8,7 @@ title: Build a logs data source plugin This guide explains how to build a logs data source plugin. -Data sources in Grafana supports both metrics and log data. The steps to build a logs data source plugin are largely the same as for a metrics data source. This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "/tutorials/build-a-data-source-plugin.md" >}}) for metrics. +Data sources in Grafana supports both metrics and log data. The steps to build a logs data source plugin are largely the same as for a metrics data source. This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "tutorials/build-a-data-source-plugin.md" >}}) for metrics. ## Add logs support to your data source diff --git a/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md b/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md index 06e1b748c29..f14e50ebaf4 100644 --- a/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md +++ b/docs/sources/developers/plugins/build-a-streaming-data-source-plugin.md @@ -8,7 +8,7 @@ title: Build a streaming data source plugin This guide explains how to build a streaming data source plugin. -This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "/tutorials/build-a-data-source-plugin.md" >}}). +This guide assumes that you're already familiar with how to [Build a data source plugin]({{< relref "tutorials/build-a-data-source-plugin.md" >}}). When monitoring critical applications, you want your dashboard to refresh as soon as your data does. In Grafana, you can set your dashboards to automatically refresh at a certain interval, no matter what data source you use. Unfortunately, this means that your queries are requesting all the data to be sent again, regardless of whether the data has actually changed. diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index 0e5139cad96..56766be88d9 100644 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -40,7 +40,7 @@ Supported auth providers: - [GitLab OAuth]({{< relref "../auth/gitlab.md#team-sync-enterprise-only" >}}) - [LDAP]({{< relref "enhanced_ldap.md#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "../auth/okta.md#team-sync-enterprise-only" >}}) -- [SAML]({{< relref "./saml/configure-saml.md#configure-team-sync" >}}) +- [SAML]({{< relref "configure-saml.md#configure-team-sync" >}}) ### Enhanced LDAP integration diff --git a/docs/sources/enterprise/access-control/about-rbac.md b/docs/sources/enterprise/access-control/about-rbac.md index 4eefb2cd96e..22175cd181e 100644 --- a/docs/sources/enterprise/access-control/about-rbac.md +++ b/docs/sources/enterprise/access-control/about-rbac.md @@ -58,8 +58,8 @@ You can use RBAC to modify the permissions associated with any basic role, which Note that any modification to any of these basic role is not propagated to the other basic roles. For example, if you modify Viewer basic role and grant additional permission, Editors or Admins won't have that additional grant. -For more information about the permissions associated with each basic role, refer to [Basic role definitions]({{< relref "./rbac-fixed-basic-role-definitions#basic-role-assignments" >}}). -To interact with the API and view or modify basic roles permissions, refer to [the table]({{< relref "./manage-rbac-roles#basic-role-uid-mapping" >}}) that maps basic role names to the associated UID. +For more information about the permissions associated with each basic role, refer to [Basic role definitions]({{< relref "rbac-fixed-basic-role-definitions/#basic-role-assignments" >}}). +To interact with the API and view or modify basic roles permissions, refer to [the table]({{< relref "manage-rbac-roles/#basic-role-uid-mapping" >}}) that maps basic role names to the associated UID. ## Fixed roles @@ -85,7 +85,7 @@ Assign fixed roles when the basic roles do not meet your permission requirements - [Teams]({{< relref "../../administration/manage-users-and-permissions/manage-teams/_index.md" >}}) - [Users]({{< relref "../../administration/manage-users-and-permissions/manage-server-users/_index.md" >}}) -To learn more about the permissions you can grant for each resource, refer to [RBAC role definitions]({{< relref "./rbac-fixed-basic-role-definitions.md" >}}). +To learn more about the permissions you can grant for each resource, refer to [RBAC role definitions]({{< relref "rbac-fixed-basic-role-definitions.md" >}}). ## Custom roles @@ -101,7 +101,7 @@ Consider creating a custom role when fixed roles do not meet your permissions re You can use either of the following methods to create, assign, and manage custom roles: -- Grafana provisioning: You can use a YAML file to configure roles. For more information about using provisioning to create custom roles, refer to [Manage RBAC roles]({{< relref "./manage-rbac-roles.md" >}}). For more information about using provisioning to assign RBAC roles to users or teams, refer to [Assign RBAC roles]({{< relref "./assign-rbac-roles.md" >}}). +- Grafana provisioning: You can use a YAML file to configure roles. For more information about using provisioning to create custom roles, refer to [Manage RBAC roles]({{< relref "manage-rbac-roles.md" >}}). For more information about using provisioning to assign RBAC roles to users or teams, refer to [Assign RBAC roles]({{< relref "assign-rbac-roles.md" >}}). - RBAC API: As an alternative, you can use the Grafana HTTP API to create and manage roles. For more information about the HTTP API, refer to [RBAC API]({{< relref "../../developers/http_api/access_control.md" >}}). ## Limitation diff --git a/docs/sources/enterprise/access-control/assign-rbac-roles.md b/docs/sources/enterprise/access-control/assign-rbac-roles.md index cbf100be36d..3a8b6ee0d93 100644 --- a/docs/sources/enterprise/access-control/assign-rbac-roles.md +++ b/docs/sources/enterprise/access-control/assign-rbac-roles.md @@ -28,10 +28,10 @@ In both cases, the assignment applies only to the user or team within the affect **Before you begin:** -- [Plan your RBAC rollout strategy]({{< relref "./plan-rbac-rollout-strategy.md" >}}). +- [Plan your RBAC rollout strategy]({{< relref "plan-rbac-rollout-strategy.md" >}}). - Identify the fixed roles that you want to assign to the user or team. - For more information about available fixed roles, refer to [RBAC role definitions]({{< relref "./rbac-fixed-basic-role-definitions.md" >}}). + For more information about available fixed roles, refer to [RBAC role definitions]({{< relref "rbac-fixed-basic-role-definitions.md" >}}). - Ensure that your own user account has the correct permissions: - If you are assigning permissions to a user or team within an organization, you must have organization administrator or server administrator permissions. @@ -69,8 +69,8 @@ Instead of using the Grafana role picker, you can use file-based provisioning to **Before you begin:** -- Refer to [Role provisioning]({{< relref "./rbac-provisioning#rbac-provisioning" >}}) -- Ensure that the team to which you are adding the fixed role exists. For more information about creating teams, refer to [Manage teams]({{< relref "../../administration/manage-users-and-permissions/manage-teams/_index.md">}}) +- Refer to [Role provisioning]({{< relref "rbac-provisioning/#rbac-provisioning" >}}) +- Ensure that the team to which you are adding the fixed role exists. For more information about creating teams, refer to [Manage teams]({{< relref "../../administration/manage-users-and-permissions/manage-teams/_index.md" >}}) **To assign a role to a team:** @@ -78,21 +78,21 @@ Instead of using the Grafana role picker, you can use file-based provisioning to 1. Refer to the following table to add attributes and values. - | Attribute | Description | - | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | `roles` | Enter the custom role or custom roles you want to create/update. | - | `roles > name` | Enter the name of the custom role. | - | `roles > version` | Enter the custom role version number. Role assignments are independent of the role version number. | - | `roles > global` | Enter `true`. You can specify the `orgId` otherwise. | - | `roles > permissions` | Enter the permissions `action` and `scope` values. For more information about permissions actions and scopes, refer to [RBAC permissions, actions, and scopes]({{< relref "./custom-role-actions-scopes.md" >}}) | - | `teams` | Enter the team or teams to which you are adding the custom role. | - | `teams > orgId` | Because teams belong to organizations, you must add the `orgId` value. | - | `teams > name` | Enter the name of the team. | - | `teams > roles` | Enter the custom or fixed role or roles that you want to grant to the team. | - | `teams > roles > name` | Enter the name of the role. | - | `teams > roles > global` | Enter `true`, or specify `orgId` of the role you want to assign to the team. Fixed roles are global. | + | Attribute | Description | + | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | + | `roles` | Enter the custom role or custom roles you want to create/update. | + | `roles > name` | Enter the name of the custom role. | + | `roles > version` | Enter the custom role version number. Role assignments are independent of the role version number. | + | `roles > global` | Enter `true`. You can specify the `orgId` otherwise. | + | `roles > permissions` | Enter the permissions `action` and `scope` values. For more information about permissions actions and scopes, refer to [RBAC permissions, actions, and scopes]({{< relref "custom-role-actions-scopes.md" >}}) | + | `teams` | Enter the team or teams to which you are adding the custom role. | + | `teams > orgId` | Because teams belong to organizations, you must add the `orgId` value. | + | `teams > name` | Enter the name of the team. | + | `teams > roles` | Enter the custom or fixed role or roles that you want to grant to the team. | + | `teams > roles > name` | Enter the name of the role. | + | `teams > roles > global` | Enter `true`, or specify `orgId` of the role you want to assign to the team. Fixed roles are global. | - For more information about managing custom roles, refer to [Create custom roles using provisioning]({{< relref "./manage-rbac-roles/#create-custom-roles-using-provisioning" >}}). + For more information about managing custom roles, refer to [Create custom roles using provisioning]({{< relref "manage-rbac-roles/#create-custom-roles-using-provisioning" >}}). 1. Reload the provisioning configuration file. diff --git a/docs/sources/enterprise/access-control/custom-role-actions-scopes.md b/docs/sources/enterprise/access-control/custom-role-actions-scopes.md index 9679536f5fa..71844c782b0 100644 --- a/docs/sources/enterprise/access-control/custom-role-actions-scopes.md +++ b/docs/sources/enterprise/access-control/custom-role-actions-scopes.md @@ -12,7 +12,7 @@ weight: 80 A permission is comprised of an action and a scope. When creating a custom role, consider the actions the user can perform and the resource(s) on which they can perform those actions. -To learn more about the Grafana resources to which you can apply RBAC, refer to [Resources with RBAC permissions]({{< relref "./about-rbac.md#fixed-roles" >}}). +To learn more about the Grafana resources to which you can apply RBAC, refer to [Resources with RBAC permissions]({{< relref "about-rbac.md#fixed-roles" >}}). - **Action:** An action describes what tasks a user can perform on a resource. - **Scope:** A scope describes where an action can be performed, such as reading a specific user profile. In this example, a permission is associated with the scope `users:` to the relevant role. @@ -101,7 +101,7 @@ The following list contains role-based access control actions. | `roles:write` | `permissions:type:escalate` | Reset basic roles to their default permissions. | | `server.stats:read` | n/a | Read Grafana instance statistics. | | `settings:read` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../administration/configuration/_index.md" >}}) | -| `settings:write` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../enterprise/settings-updates/_index.md" >}}). | +| `settings:write` | `settings:*`
`settings:auth.saml:*`
`settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../settings-updates/_index.md" >}}). | | `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. | | `teams.permissions:read` | `teams:*`
`teams:id:*` | Read members and External Group Synchronization setup for teams. | | `teams.permissions:write` | `teams:*`
`teams:id:*` | Add, remove and update members and manage External Group Synchronization setup for teams. | @@ -146,7 +146,7 @@ The following list contains role-based access control scopes. | `orgs:*`
`orgs:id:*` | Restrict an action to a set of organizations. For example, `orgs:*` matches any organization and `orgs:id:1` matches the organization whose ID is `1`. | | `permissions:type:delegate` | The scope is only applicable for roles associated with the Access Control itself and indicates that you can delegate your permissions only, or a subset of it, by creating a new role or making an assignment. | | `permissions:type:escalate` | The scope is required to trigger the reset of basic roles permissions. It indicates that users might acquire additional permissions they did not previously have. | -| `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "./custom-role-actions-scopes" >}}). | +| `provisioners:*` | Restrict an action to a set of provisioners. For example, `provisioners:*` matches any provisioner, and `provisioners:accesscontrol` matches the role-based access control [provisioner]({{< relref "custom-role-actions-scopes/" >}}). | | `reports:*`
`reports:id:*` | Restrict an action to a set of reports. For example, `reports:*` matches any report and `reports:id:1` matches the report whose ID is `1`. | | `roles:*`
`roles:uid:*` | Restrict an action to a set of roles. For example, `roles:*` matches any role and `roles:uid:randomuid` matches only the role whose UID is `randomuid`. | | `services:accesscontrol` | Restrict an action to target only the role-based access control service. You can use this in conjunction with the `status:accesscontrol` actions. | diff --git a/docs/sources/enterprise/access-control/manage-rbac-roles.md b/docs/sources/enterprise/access-control/manage-rbac-roles.md index 3a8deebfdf4..f7c0c048e75 100644 --- a/docs/sources/enterprise/access-control/manage-rbac-roles.md +++ b/docs/sources/enterprise/access-control/manage-rbac-roles.md @@ -86,9 +86,9 @@ Create a custom role when basic roles and fixed roles do not meet your permissio **Before you begin:** -- [Plan your RBAC rollout strategy]({{< relref "./plan-rbac-rollout-strategy" >}}). -- Determine which permissions you want to add to the custom role. To see a list of actions and scope, refer to [RBAC permissions actions and scopes]({{< relref "./custom-role-actions-scopes.md" >}}). -- [Enable role provisioning]({{< relref "./rbac-provisioning" >}}). +- [Plan your RBAC rollout strategy]({{< relref "plan-rbac-rollout-strategy/" >}}). +- Determine which permissions you want to add to the custom role. To see a list of actions and scope, refer to [RBAC permissions actions and scopes]({{< relref "custom-role-actions-scopes.md" >}}). +- [Enable role provisioning]({{< relref "rbac-provisioning/" >}}). - Ensure that you have permissions to create a custom role. - By default, the Grafana Admin role has permission to create custom roles. - A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:type:delegate` scope. @@ -101,21 +101,21 @@ File-based provisioning is one method you can use to create custom roles. 1. Refer to the following table to add attributes and values. -| Attribute | Description | -| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | A human-friendly identifier for the role that helps administrators understand the purpose of a role. `name` is required and cannot be longer than 190 characters. We recommend that you use ASCII characters. Role names must be unique within an organization. | -| `uid` | A unique identifier associated with the role. The UID enables you to change or delete the role. You can either generate a UID yourself, or let Grafana generate one for you. You cannot use the same UID within the same Grafana instance. | -| `orgId` | Identifies the organization to which the role belongs. The [default org ID]({{< relref "../../administration/configuration#auto_assign_org_id" >}}) is used if you do not specify `orgId`. | -| `global` | Global roles are not associated with any specific organization, which means that you can reuse them across all organizations. This setting overrides `orgId`. | -| `displayName` | Human-friendly text that is displayed in the UI. Role display name cannot be longer than 190 ASCII-based characters. For fixed roles, the display name is shown as specified. If you do not set a display name the display name replaces `':'` (a colon) with `' '` (a space). | -| `description` | Human-friendly text that describes the permissions a role provides. | -| `group` | Organizes roles in the role picker. | -| `version` | A positive integer that defines the current version of the role, which prevents overwriting newer changes. | -| `hidden` | Hidden roles do not appear in the role picker. | -| `state` | State of the role. Defaults to `present`, but if set to `absent` the role will be removed. | -| `force` | Can be used in addition to state `absent`, to force the removal of a role and all its assignments. | -| `from` | An optional list of roles from which you want to copy permissions. | -| `permissions` | Provides users access to Grafana resources. For a list of permissions, refer to [RBAC permissions actions and scopes]({{< relref "./rbac-fixed-basic-role-definitions.md" >}}). If you do not know which permissions to assign, you can create and assign roles without any permissions as a placeholder. Using the `from` attribute, you can specify additional permissions or permissions to remove by adding a `state` to your permission list. | +| Attribute | Description | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `name` | A human-friendly identifier for the role that helps administrators understand the purpose of a role. `name` is required and cannot be longer than 190 characters. We recommend that you use ASCII characters. Role names must be unique within an organization. | +| `uid` | A unique identifier associated with the role. The UID enables you to change or delete the role. You can either generate a UID yourself, or let Grafana generate one for you. You cannot use the same UID within the same Grafana instance. | +| `orgId` | Identifies the organization to which the role belongs. The [default org ID]({{< relref "../../administration/configuration/#auto_assign_org_id" >}}) is used if you do not specify `orgId`. | +| `global` | Global roles are not associated with any specific organization, which means that you can reuse them across all organizations. This setting overrides `orgId`. | +| `displayName` | Human-friendly text that is displayed in the UI. Role display name cannot be longer than 190 ASCII-based characters. For fixed roles, the display name is shown as specified. If you do not set a display name the display name replaces `':'` (a colon) with `' '` (a space). | +| `description` | Human-friendly text that describes the permissions a role provides. | +| `group` | Organizes roles in the role picker. | +| `version` | A positive integer that defines the current version of the role, which prevents overwriting newer changes. | +| `hidden` | Hidden roles do not appear in the role picker. | +| `state` | State of the role. Defaults to `present`, but if set to `absent` the role will be removed. | +| `force` | Can be used in addition to state `absent`, to force the removal of a role and all its assignments. | +| `from` | An optional list of roles from which you want to copy permissions. | +| `permissions` | Provides users access to Grafana resources. For a list of permissions, refer to [RBAC permissions actions and scopes]({{< relref "rbac-fixed-basic-role-definitions.md" >}}). If you do not know which permissions to assign, you can create and assign roles without any permissions as a placeholder. Using the `from` attribute, you can specify additional permissions or permissions to remove by adding a `state` to your permission list. | 1. Reload the provisioning configuration file. @@ -245,7 +245,7 @@ If the default basic role definitions do not meet your requirements, you can cha **Before you begin:** -- Determine the permissions you want to add or remove from a basic role. For more information about the permissions associated with basic roles, refer to [RBAC role definitions]({{< relref "./rbac-fixed-basic-role-definitions#basic-role-assignments" >}}). +- Determine the permissions you want to add or remove from a basic role. For more information about the permissions associated with basic roles, refer to [RBAC role definitions]({{< relref "rbac-fixed-basic-role-definitions/#basic-role-assignments" >}}). **To change permissions from a basic role:** diff --git a/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md b/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md index d2c07ba4ecc..c02e7963f48 100644 --- a/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md +++ b/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md @@ -27,8 +27,8 @@ As a first step in determining your permissions rollout strategy, we recommend t To learn more about basic roles and fixed roles, refer to the following documentation: -- [Basic role definitions]({{< relref "./rbac-fixed-basic-role-definitions#basic-role-assignments" >}}) -- [Fixed role definitions]({{< relref "./rbac-fixed-basic-role-definitions#fixed-role-definitions" >}}) +- [Basic role definitions]({{< relref "rbac-fixed-basic-role-definitions/#basic-role-assignments" >}}) +- [Fixed role definitions]({{< relref "rbac-fixed-basic-role-definitions/#fixed-role-definitions" >}}) ## User and team considerations @@ -200,7 +200,7 @@ roles: global: true ``` -> **Note:** The `fixed:reports:writer` role assigns more permissions than just creating reports. For more information about fixed role permission assignments, refer to [Fixed role definitions]({{< relref "./rbac-fixed-basic-role-definitions#fixed-role-definitions" >}}). +> **Note:** The `fixed:reports:writer` role assigns more permissions than just creating reports. For more information about fixed role permission assignments, refer to [Fixed role definitions]({{< relref "rbac-fixed-basic-role-definitions/#fixed-role-definitions" >}}). - Add the following permissions to the `basic:viewer` role, using provisioning or the [RBAC HTTP API]({{< relref "../../developers/http_api/access_control.md#update-a-role" >}}): diff --git a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md index 70b9a852f15..4bcb53bed71 100644 --- a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md +++ b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md @@ -80,11 +80,11 @@ The following tables list permissions associated with basic and fixed roles. ### Alerting roles -If alerting is [enabled]({{< relref "../../alerting/migrating-alerts/opt-in.md" >}}), you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. +If alerting is [enabled]({{< relref "../../alerting/migrating-alerts/opt-out.md" >}}), you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder. Access to Grafana alert rules is an intersection of many permissions: - Permission to read a folder. For example, the fixed role `fixed:folders:reader` includes the action `folders:read` and a folder scope `folders:id:`. - Permission to query **all** data sources that a given alert rule uses. If a user cannot query a given data source, they cannot see any alert rules that query that data source. -For more information about the permissions required to access alert rules, refer to [Create a custom role to access alerts in a folder]({{< relref "./plan-rbac-rollout-strategy#create-a-custom-role-to-access-alerts-in-a-folder" >}}). +For more information about the permissions required to access alert rules, refer to [Create a custom role to access alerts in a folder]({{< relref "plan-rbac-rollout-strategy/#create-a-custom-role-to-access-alerts-in-a-folder" >}}). diff --git a/docs/sources/enterprise/access-control/rbac-provisioning.md b/docs/sources/enterprise/access-control/rbac-provisioning.md index 2593af6f9aa..fb10a77f775 100644 --- a/docs/sources/enterprise/access-control/rbac-provisioning.md +++ b/docs/sources/enterprise/access-control/rbac-provisioning.md @@ -10,7 +10,7 @@ weight: 60 # Grafana RBAC provisioning -You can create, change or remove [Custom roles]({{< relref "./manage-rbac-roles.md#create-custom-roles-using-provisioning" >}}) and create or remove [basic role assignments]({{< relref "./assign-rbac-roles.md#assign-a-fixed-role-to-a-basic-role-using-provisioning" >}}), by adding one or more YAML configuration files in the `provisioning/access-control/` directory. +You can create, change or remove [Custom roles]({{< relref "manage-rbac-roles.md#create-custom-roles-using-provisioning" >}}) and create or remove [basic role assignments]({{< relref "assign-rbac-roles.md#assign-a-fixed-role-to-a-basic-role-using-provisioning" >}}), by adding one or more YAML configuration files in the `provisioning/access-control/` directory. If you choose to use provisioning to assign and manage role, you must first enable it. @@ -28,11 +28,11 @@ Grafana performs provisioning during startup. After you make a change to the con 3. Create a new YAML in the following folder: **provisioning/access-control**. For example, `provisioning/access-control/custom-roles.yml` -4. Add RBAC provisioning details to the configuration file. See [manage RBAC roles]({{< relref "manage-rbac-roles.md" >}}) and [assign RBAC roles]({{< relref "assign-rbac-roles.md" >}}) for instructions, and see this [example role provisioning file]({{< relref "rbac-provisioning#example" >}}) for a complete example of a provisioning file. +4. Add RBAC provisioning details to the configuration file. See [manage RBAC roles]({{< relref "manage-rbac-roles.md" >}}) and [assign RBAC roles]({{< relref "assign-rbac-roles.md" >}}) for instructions, and see this [example role provisioning file]({{< relref "rbac-provisioning/#example" >}}) for a complete example of a provisioning file. 5. Reload the provisioning configuration file. - For more information about reloading the provisioning configuration at runtime, refer to [Reload provisioning configurations]({{< relref "../../http_api/admin/#reload-provisioning-configurations" >}}). + For more information about reloading the provisioning configuration at runtime, refer to [Reload provisioning configurations]({{< relref "../../developers/http_api/admin/#reload-provisioning-configurations" >}}). ## Example role configuration file using Grafana provisioning diff --git a/docs/sources/enterprise/auditing.md b/docs/sources/enterprise/auditing.md index 78d187edf8b..a6df6e660ee 100644 --- a/docs/sources/enterprise/auditing.md +++ b/docs/sources/enterprise/auditing.md @@ -128,7 +128,7 @@ pattern of the `requestUri` field is given. \* Where `AUTH-MODULE` is the name of the authentication module: `grafana`, `saml`, `ldap`, etc. \ -\*\* Includes manual log out, token expired/revoked, and [SAML Single Logout]({{< relref "./saml/configure-saml.md#single-logout" >}}). +\*\* Includes manual log out, token expired/revoked, and [SAML Single Logout]({{< relref "configure-saml.md#single-logout" >}}). #### User management diff --git a/docs/sources/enterprise/saml/configure-saml.md b/docs/sources/enterprise/configure-saml.md similarity index 57% rename from docs/sources/enterprise/saml/configure-saml.md rename to docs/sources/enterprise/configure-saml.md index 848585e9663..bcef82584ec 100644 --- a/docs/sources/enterprise/saml/configure-saml.md +++ b/docs/sources/enterprise/configure-saml.md @@ -1,23 +1,167 @@ --- aliases: - /docs/grafana/latest/auth/saml/ + - /docs/grafana/latest/enterprise/saml/about-saml/ + - /docs/grafana/latest/enterprise/saml/ + - /docs/grafana/latest/enterprise/saml/enable-saml/ - /docs/grafana/latest/enterprise/saml/configure-saml/ -description: This contains information on how to configure SAML authentication in - Grafana -keywords: - - grafana - - saml - - documentation - - saml-configuration - - enterprise -menuTitle: Configure SAML + - /docs/grafana/latest/enterprise/saml/set-up-saml-with-okta/ + - /docs/grafana/latest/enterprise/saml/troubleshoot-saml/ +description: Learn how to configure SAML authentication in Grafana +menuTitle: Configure SAML authentication title: Configure SAML authentication in Grafana -weight: 40 +weight: 160 --- # Configure SAML authentication in Grafana -The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../../administration/configuration.md#configure-with-environment-variables" >}}). +SAML authentication integration allows your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. + +The SAML single sign-on (SSO) standard is varied and flexible. Our implementation contains a subset of features needed to provide a smooth authentication experience into Grafana. + +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). + +## Supported SAML + +Grafana supports the following SAML 2.0 bindings: + +- From the Service Provider (SP) to the Identity Provider (IdP): + + - `HTTP-POST` binding + - `HTTP-Redirect` binding + +- From the Identity Provider (IdP) to the Service Provider (SP): + - `HTTP-POST` binding + +In terms of security: + +- Grafana supports signed and encrypted assertions. +- Grafana does not support signed or encrypted requests. + +In terms of initiation, Grafana supports: + +- SP-initiated requests +- IdP-initiated requests + +By default, SP-initiated requests are enabled. For instructions on how to enable IdP-initiated logins, see [IdP-initiated Single Sign-On (SSO)]({{< relref "#idp-initiated-single-sign-on-sso" >}}). + +### Edit SAML options in the Grafana config file + +1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`]({{< relref "enterprise-configuration.md#enabled" >}}) to `true`. +1. Configure the [certificate and private key]({{< relref "#certificate-and-private-key" >}}). +1. On the Okta application page where you have been redirected after application created, navigate to the **Sign On** tab and find **Identity Provider metadata** link in the **Settings** section. +1. Set the [`idp_metadata_url`]({{< relref "enterprise-configuration.md#idp-metadata-url" >}}) to the URL obtained from the previous step. The URL should look like `https://.okta.com/app//sso/saml/metadata`. +1. Set the following options to the attribute names configured at the **step 10** of the SAML integration setup. You can find this attributes on the **General** tab of the application page (**ATTRIBUTE STATEMENTS** and **GROUP ATTRIBUTE STATEMENTS** in the **SAML Settings** section). + - [`assertion_attribute_login`]({{< relref "enterprise-configuration.md#assertion-attribute-login" >}}) + - [`assertion_attribute_email`]({{< relref "enterprise-configuration.md#assertion-attribute-email" >}}) + - [`assertion_attribute_name`]({{< relref "enterprise-configuration.md#assertion-attribute-name" >}}) + - [`assertion_attribute_groups`]({{< relref "enterprise-configuration.md#assertion-attribute-groups" >}}) +1. Save the configuration file and and then restart the Grafana server. + +When you are finished, the Grafana configuration might look like this example: + +```bash +[server] +root_url = https://grafana.example.com + +[auth.saml] +enabled = true +private_key_path = "/path/to/private_key.pem" +certificate_path = "/path/to/certificate.cert" +idp_metadata_url = "https://my-org.okta.com/app/my-application/sso/saml/metadata" +assertion_attribute_name = DisplayName +assertion_attribute_login = Login +assertion_attribute_email = Email +assertion_attribute_groups = Group +``` + +## Enable SAML authentication in Grafana + +To use the SAML integration, in the `auth.saml` section of in the Grafana custom configuration file, set `enabled` to `true`. + +Refer to [Configuration]({{< relref "../administration/configuration.md" >}}) for more information about configuring Grafana. + +## Certificate and private key + +The SAML SSO standard uses asymmetric encryption to exchange information between the SP (Grafana) and the IdP. To perform such encryption, you need a public part and a private part. In this case, the X.509 certificate provides the public part, while the private key provides the private part. The private key needs to be issued in a [PKCS#8](https://en.wikipedia.org/wiki/PKCS_8) format. + +Grafana supports two ways of specifying both the `certificate` and `private_key`. + +- Without a suffix (`certificate` or `private_key`), the configuration assumes you've supplied the base64-encoded file contents. +- With the `_path` suffix (`certificate_path` or `private_key_path`), then Grafana treats the value entered as a file path and attempts to read the file from the file system. + +> **Note:** You can only use one form of each configuration option. Using multiple forms, such as both `certificate` and `certificate_path`, results in an error. + +--- + +### **Example** of how to generate SAML credentials: + +An example of how to generate a self-signed certificate and private key that's valid for one year: + +```sh +$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes​ +``` + +Base64-encode the cert.pem and key.pem files: +(-w0 switch is not needed on Mac, only for Linux) + +```sh +$ base64 -w0 key.pem > key.pem.base64 +$ base64 -w0 cert.pem > cert.pem.base64 +``` + +The base64-encoded values (`key.pem.base64, cert.pem.base64` files) are then used for certificate and private_key. + +The keys you provide should look like: + +``` +-----BEGIN PRIVATE KEY----- +... +... +-----END PRIVATE KEY----- +``` + +## Set up SAML with Okta + +Grafana supports user authentication through Okta, which is useful when you want your users to access Grafana using single sign on. This guide will follow you through the steps of configuring SAML authentication in Grafana with [Okta](https://okta.com/). You need to be an admin in your Okta organization to access Admin Console and create SAML integration. You also need permissions to edit Grafana config file and restart Grafana server. + +**Before you begin:** + +- To configure SAML integration with Okta, create integration inside the Okta organization first. [Add integration in Okta](https://help.okta.com/en/prod/Content/Topics/Apps/apps-overview-add-apps.htm) +- Ensure you have permission to administer SAML authentication. For more information about permissions, refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md#" >}}). + +**To set up SAML with Okta:** + +1. Log in to the [Okta portal](https://login.okta.com/). +1. Go to the Admin Console in your Okta organization by clicking **Admin** in the upper-right corner. If you are in the Developer Console, then click **Developer Console** in the upper-left corner and then click **Classic UI** to switch over to the Admin Console. +1. In the Admin Console, navigate to **Applications** > **Applications**. +1. Click **Add Application**. +1. Click **Create New App** to start the Application Integration Wizard. +1. Choose **Web** as a platform. +1. Select **SAML 2.0** in the Sign on method section. +1. Click **Create**. +1. On the **General Settings** tab, enter a name for your Grafana integration. You can also upload a logo. +1. On the **Configure SAML** tab, enter the SAML information related to your Grafana instance: + + - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. + - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. + - Leave the default values for **Name ID format** and **Application username**. + - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana, for example: + + | Attribute name (in Grafana) | Value (in Okta profile) | + | --------------------------- | -------------------------------------- | + | Login | `user.login` | + | Email | `user.email` | + | DisplayName | `user.firstName + " " + user.lastName` | + + - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. + +1. Click **Next**. +1. On the final Feedback tab, fill out the form and then click **Finish**. + +## Configure SAML authentication in Grafana + +The table below describes all SAML configuration options. Continue reading below for details on specific options. Like any other Grafana configuration, you can apply these options as [environment variables]({{< relref "../administration/configuration.md#configure-with-environment-variables" >}}). | Setting | Required | Description | Default | | ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | @@ -103,7 +247,7 @@ For Grafana to map the user information, it looks at the individual attributes w Grafana provides configuration options that let you modify which keys to look at for these values. The data we need to create the user in Grafana is Name, Login handle, and email. -##### The `assertion_attribute_name` option +#### The `assertion_attribute_name` option `assertion_attribute_name` is a special assertion mapping that can either be a simple key, indicating a mapping to a single assertion attribute on the SAML response, or a complex template with variables using the `$__saml{}` syntax. If this property is misconfigured, Grafana will log an error message on startup and disallow SAML sign-ins. Grafana will also log errors after a login attempt if a variable in the template is missing from the SAML response. @@ -127,24 +271,24 @@ By default, new Grafana users using SAML authentication will have an account cre > Team sync support for SAML only available in Grafana v7.0+ -To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. +To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref "enterprise-configuration.md#assertion-attribute-groups" >}}) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. -[Learn more about Team Sync]({{< relref "../../enterprise/team-sync.md" >}}) +[Learn more about Team Sync]({{< relref "team-sync.md" >}}) ### Configure role sync > Only available in Grafana v7.0+ -Role sync allows you to map user roles from an identity provider to Grafana. To enable role sync, configure role attribute and possible values for the Editor, Admin, and Grafana Admin roles. For more information about user roles, refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}). +Role sync allows you to map user roles from an identity provider to Grafana. To enable role sync, configure role attribute and possible values for the Editor, Admin, and Grafana Admin roles. For more information about user roles, refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}). -1. In the configuration file, set [`assertion_attribute_role`]({{< relref ".././enterprise-configuration.md#assertion-attribute-role" >}}) option to the attribute name where the role information will be extracted from. -1. Set the [`role_values_editor`]({{< relref ".././enterprise-configuration.md#role-values-editor" >}}) option to the values mapped to the `Editor` role. -1. Set the [`role_values_admin`]({{< relref ".././enterprise-configuration.md#role-values-admin" >}}) option to the values mapped to the organization `Admin` role. -1. Set the [`role_values_grafana_admin`]({{< relref ".././enterprise-configuration.md#role-values-grafana-admin" >}}) option to the values mapped to the `Grafana Admin` role. +1. In the configuration file, set [`assertion_attribute_role`]({{< relref "enterprise-configuration.md#assertion-attribute-role" >}}) option to the attribute name where the role information will be extracted from. +1. Set the [`role_values_editor`]({{< relref "enterprise-configuration.md#role-values-editor" >}}) option to the values mapped to the `Editor` role. +1. Set the [`role_values_admin`]({{< relref "enterprise-configuration.md#role-values-admin" >}}) option to the values mapped to the organization `Admin` role. +1. Set the [`role_values_grafana_admin`]({{< relref "enterprise-configuration.md#role-values-grafana-admin" >}}) option to the values mapped to the `Grafana Admin` role. If a user role doesn't match any of configured values, then the `Viewer` role will be assigned. -Refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}) for more information about roles and permissions in Grafana. +Refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}) for more information about roles and permissions in Grafana. Example configuration: @@ -164,8 +308,8 @@ role_values_grafana_admin = superadmin Organization mapping allows you to assign users to particular organization in Grafana depending on attribute value obtained from identity provider. -1. In configuration file, set [`assertion_attribute_org`]({{< relref ".././enterprise-configuration.md#assertion-attribute-org" >}}) to the attribute name you store organization info in. This attribute can be an array if you want a user to be in multiple organizations. -1. Set [`org_mapping`]({{< relref ".././enterprise-configuration.md#org-mapping" >}}) option to the comma-separated list of `Organization:OrgId` pairs to map organization from IdP to Grafana organization specified by id. If you want users to have different roles in multiple organizations, you can set this option to a comma-separated list of `Organization:OrgId:Role` mappings. +1. In configuration file, set [`assertion_attribute_org`]({{< relref "enterprise-configuration.md#assertion-attribute-org" >}}) to the attribute name you store organization info in. This attribute can be an array if you want a user to be in multiple organizations. +1. Set [`org_mapping`]({{< relref "enterprise-configuration.md#org-mapping" >}}) option to the comma-separated list of `Organization:OrgId` pairs to map organization from IdP to Grafana organization specified by id. If you want users to have different roles in multiple organizations, you can set this option to a comma-separated list of `Organization:OrgId:Role` mappings. For example, use following configuration to assign users from `Engineering` organization to the Grafana organization with id `2` as Editor and users from `Sales` - to the org with id `3` as Admin, based on `Org` assertion attribute value: @@ -188,9 +332,9 @@ You can use `*` as an Organization if you want all your users to be in some orga > Only available in Grafana v7.0+ -With the [`allowed_organizations`]({{< relref ".././enterprise-configuration.md#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. +With the [`allowed_organizations`]({{< relref "enterprise-configuration.md#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. -## Example SAML configuration +### Example SAML configuration ```bash [auth.saml] @@ -213,3 +357,51 @@ role_values_grafana_admin = superadmin org_mapping = Engineering:2:Editor, Engineering:3:Viewer, Sales:3:Editor, *:1:Editor allowed_organizations = Engineering, Sales ``` + +## Troubleshoot SAML authentication in Grafana + +To troubleshoot and get more log information, enable SAML debug logging in the configuration file. Refer to [Configuration]({{< relref "../administration/configuration.md#filters" >}}) for more information. + +```bash +[log] +filters = saml.auth:debug +``` + +## Known issues + +### SAML authentication fails with error: + +- `asn1: structure error: tags don't match` + +We only support one private key format: PKCS#8. + +The keys may be in a different format (PKCS#1 or PKCS#12); in that case, it may be necessary to convert the private key format. + +The following command creates a pkcs8 key file. + +```bash +$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes​ +``` + +#### **Convert** the private key format to base64 + +The following command converts keys to base64 format. + +Base64-encode the cert.pem and key.pem files: +(-w0 switch is not needed on Mac, only for Linux) + +```sh +$ base64 -w0 key.pem > key.pem.base64 +$ base64 -w0 cert.pem > cert.pem.base64 +``` + +The base64-encoded values (`key.pem.base64, cert.pem.base64` files) are then used for certificate and private_key. + +The keys you provide should look like: + +``` +-----BEGIN PRIVATE KEY----- +... +... +-----END PRIVATE KEY----- +``` diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index 36ce654d29d..f3c0bd9c744 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -371,7 +371,7 @@ Setting 'enabled' to `true` allows users to configure query caching for data sou This value is `true` by default. -> **Note:** This setting enables the caching feature, but it does not turn on query caching for any data source. To turn on query caching for a data source, update the setting on the data source configuration page. For more information, refer to the [query caching docs]({{< relref "./query-caching.md#enable-and-configure-query-caching" >}}). +> **Note:** This setting enables the caching feature, but it does not turn on query caching for any data source. To turn on query caching for a data source, update the setting on the data source configuration page. For more information, refer to the [query caching docs]({{< relref "query-caching.md#enable-and-configure-query-caching" >}}). ### ttl diff --git a/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md b/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md index 51877da2748..d4c3dc7d004 100644 --- a/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md +++ b/docs/sources/enterprise/enterprise-encryption/using-aws-kms-to-encrypt-database-secrets.md @@ -27,7 +27,7 @@ You can use an encryption key from AWS Key Management Service to encrypt secrets 3. Create a [programmatic credential](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys) (access key ID and secret access key), which has permission to view the key that you created.

In AWS, you can control access to your KMS keys by using [key policies](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html), [IAM policies](https://docs.aws.amazon.com/kms/latest/developerguide/iam-policies.html), and [grants](https://docs.aws.amazon.com/kms/latest/developerguide/grants.html). You can also create [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_use-resources.html), which must provide a session token along with an access key ID and a secret access key. -4. From within Grafana, turn on [envelope encryption]({{< relref "../../administration//database-encryption.md" >}}). +4. From within Grafana, turn on [envelope encryption]({{< relref "../../administration/database-encryption.md" >}}). 5. Add your AWS KMS details to the Grafana configuration file; depending on your operating system, it is usually named `grafana.ini`:

a. Add a new section to the configuration file, with a name in the format of `[security.encryption.awskms.]`, where `` is any name that uniquely identifies this key among other provider keys.

b. Fill in the section with the following values: diff --git a/docs/sources/enterprise/license/_index.md b/docs/sources/enterprise/license/_index.md index 37352ece76e..9cf66ffcacc 100644 --- a/docs/sources/enterprise/license/_index.md +++ b/docs/sources/enterprise/license/_index.md @@ -14,8 +14,8 @@ weight: 10 When you become a Grafana Enterprise customer, you gain access to Grafana's premium observability features, including enterprise data source plugins, reporting, and role-based access control. In order to use these [enhanced features of Grafana Enterprise]({{< relref "../_index.md" >}}), you must purchase and activate a Grafana Enterprise license. -To purchase a license directly from Grafana Labs, [Contact a Grafana Labs representative](https://grafana.com/contact?about=grafana-enterprise). To activate an Enterprise license purchased from Grafana Labs, refer to [Activate an Enterprise license]({{< relref "./activate-license.md" >}}). +To purchase a license directly from Grafana Labs, [Contact a Grafana Labs representative](https://grafana.com/contact?about=grafana-enterprise). To activate an Enterprise license purchased from Grafana Labs, refer to [Activate an Enterprise license]({{< relref "activate-license.md" >}}). -You can also purchase a Grafana Enterprise license through the AWS Marketplace. To learn more about activating a license purchased through AWS, refer to [Activate a Grafana Enterprise license purchased through AWS Marketplace]({{< relref "../license/activate-aws-marketplace-license" >}}). +You can also purchase a Grafana Enterprise license through the AWS Marketplace. To learn more about activating a license purchased through AWS, refer to [Activate a Grafana Enterprise license purchased through AWS Marketplace]({{< relref "activate-aws-marketplace-license/" >}}). {{< section >}} diff --git a/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-eks.md b/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-eks.md index 18a70d719ed..db5fdcda11e 100644 --- a/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-eks.md +++ b/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-eks.md @@ -121,6 +121,6 @@ To restart Grafana on a Kubernetes cluster, 1. After you update the service, navigate to your Grafana instance, sign in with Grafana Admin credentials, and navigate to the Statistics and Licensing page to validate that your license is active. -For more information about restarting Grafana, refer to [Restart Grafana]({{< relref "../../../installation/restart-grafana" >}}). +For more information about restarting Grafana, refer to [Restart Grafana]({{< relref "../../../installation/restart-grafana/" >}}). > If you experience issues when you update the EKS cluster, refer to [Amazon EKS troubleshooting](https://docs.aws.amazon.com/eks/latest/userguide/troubleshooting.html). diff --git a/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-instance-outside-aws.md b/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-instance-outside-aws.md index 9b472872d54..c6c4937733e 100644 --- a/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-instance-outside-aws.md +++ b/docs/sources/enterprise/license/activate-aws-marketplace-license/activate-license-on-instance-outside-aws.md @@ -32,7 +32,7 @@ To activate a Grafana Enterprise license from AWS on a Grafana Enterprise instan To install Grafana, refer to the documentation specific to your implementation. - [Install Grafana]({{< relref "../../../installation/" >}}). -- [Run Grafana Docker image]({{< relref "../../../installation/docker" >}}). +- [Run Grafana Docker image]({{< relref "../../../installation/docker/" >}}). - [Deploy Grafana on Kubernetes]({{< relref "../../../installation/kubernetes/#deploy-grafana-enterprise-on-kubernetes" >}}). ## Task 2: Create an AWS IAM user with access to your Grafana Enterprise license @@ -127,4 +127,4 @@ Choose one of the following options to update the [license_validation_type]({{< To activate Grafana Enterprise features, start (or restart) Grafana. -For information about restarting Grafana, refer to [Restart Grafana]({{< relref "../../../installation/restart-grafana" >}}). +For information about restarting Grafana, refer to [Restart Grafana]({{< relref "../../../installation/restart-grafana/" >}}). diff --git a/docs/sources/enterprise/license/activate-aws-marketplace-license/manage-license-in-aws-marketplace.md b/docs/sources/enterprise/license/activate-aws-marketplace-license/manage-license-in-aws-marketplace.md index 861d723031a..4594aa4c42b 100644 --- a/docs/sources/enterprise/license/activate-aws-marketplace-license/manage-license-in-aws-marketplace.md +++ b/docs/sources/enterprise/license/activate-aws-marketplace-license/manage-license-in-aws-marketplace.md @@ -38,4 +38,4 @@ You can use AWS Marketplace to make the following modifications to your Grafana This action retrieves updated license information from AWS. -> To learn more about licensing and active users, refer to [Understanding Grafana Enterprise licensing]({{< relref "../../license/license-restrictions" >}}). +> To learn more about licensing and active users, refer to [Understanding Grafana Enterprise licensing]({{< relref "../../license/license-restrictions/" >}}). diff --git a/docs/sources/enterprise/license/license-restrictions.md b/docs/sources/enterprise/license/license-restrictions.md index 574b72f934f..8f836bfc7c5 100644 --- a/docs/sources/enterprise/license/license-restrictions.md +++ b/docs/sources/enterprise/license/license-restrictions.md @@ -122,7 +122,7 @@ For example, if you purchase 150 licenses, you can have 20 admins, 70 editors, a ### Transition to combined license model To transition from the tiered licensing model to the combined license model, contact your Grafana account team and request to switch to combined user pricing. Once you update your contract with the account team, they will issue you a new license token. -For instructions about how to update your license, refer to [Activate an Enterprise license]({{< relref "./activate-license.md" >}}). +For instructions about how to update your license, refer to [Activate an Enterprise license]({{< relref "activate-license.md" >}}). After you apply the token, Grafana Enterprise resets your license and updates the user counts on the **Utilization** panel. @@ -153,7 +153,7 @@ Your license is controlled by the following rules: **License expiration date:** The license includes an expiration date, which is the date when a license becomes inactive. -As the license expiration date approaches, you will see a banner in Grafana that encourages you to renew. To learn about how to renew your license and what happens in Grafana when a license expires, refer to [License expiration]({{< relref "./license-expiration.md" >}}). +As the license expiration date approaches, you will see a banner in Grafana that encourages you to renew. To learn about how to renew your license and what happens in Grafana when a license expires, refer to [License expiration]({{< relref "license-expiration.md" >}}). **Grafana License URL:** Your license does not work with an instance of Grafana with a different root URL. @@ -175,4 +175,4 @@ Usage billing involves a contractual agreement between you and Grafana Labs, and To increase the number of licensed users within Grafana, extend a license, or change your licensed URL, contact [Grafana support](https://grafana.com/profile/org#support) or your Grafana Labs account team. They will update your license, which you can activate from within Grafana. -For instructions about how to activate your license after it is updated, refer to [Activate an Enterprise license]({{< relref "./activate-license.md" >}}). +For instructions about how to activate your license after it is updated, refer to [Activate an Enterprise license]({{< relref "activate-license.md" >}}). diff --git a/docs/sources/enterprise/query-caching.md b/docs/sources/enterprise/query-caching.md index c0c9b51814d..b1f1ca56d84 100644 --- a/docs/sources/enterprise/query-caching.md +++ b/docs/sources/enterprise/query-caching.md @@ -65,7 +65,7 @@ By default, data source queries are not cached. To enable query caching for a si > **Note:** If query caching is enabled and the Cache tab is not visible in a data source's settings, then query caching is not available for that data source. -To configure global settings for query caching, refer to the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}). +To configure global settings for query caching, refer to the [Query caching section of Enterprise Configuration]({{< relref "enterprise-configuration.md#caching" >}}). ## Disable query caching @@ -75,7 +75,7 @@ To disable query caching for a single data source: 1. In the data source list, click the data source that you want to turn off caching for. 1. In the Cache tab, click Disable. -To disable query caching for an entire Grafana instance, set the `enabled` flag to `false` in the [Query caching section of Enterprise Configuration]({{< relref "./enterprise-configuration.md#caching" >}}). You will no longer see the Cache tab on any data sources, and no data source queries will be cached. +To disable query caching for an entire Grafana instance, set the `enabled` flag to `false` in the [Query caching section of Enterprise Configuration]({{< relref "enterprise-configuration.md#caching" >}}). You will no longer see the Cache tab on any data sources, and no data source queries will be cached. ## Clear cache diff --git a/docs/sources/enterprise/reporting.md b/docs/sources/enterprise/reporting.md index 9bb0c6de6cd..4fddbd4052f 100644 --- a/docs/sources/enterprise/reporting.md +++ b/docs/sources/enterprise/reporting.md @@ -14,7 +14,7 @@ weight: 800 Reporting allows you to automatically generate PDFs from any of your dashboards and have Grafana email them to interested parties on a schedule. This is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. -> If you have [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, for some actions you would need to have relevant permissions. +> If you have [Role-based access control]({{< relref "access-control/_index.md" >}}) enabled, for some actions you would need to have relevant permissions. > Refer to specific guides to understand what permissions are required. {{< figure src="/static/img/docs/enterprise/reports_list_8.1.png" max-width="500px" class="docs-image--no-shadow" >}} @@ -28,11 +28,11 @@ Any changes you make to a dashboard used in a report are reflected the next time ## Access control -When [RBAC]({{< relref "../enterprise/access-control/_index.md" >}}) is enabled, you need to have the relevant [Permissions]({{< relref "../enterprise/access-control/rbac-fixed-basic-role-definitions" >}}) to create and manage reports. +When [RBAC]({{< relref "access-control/_index.md" >}}) is enabled, you need to have the relevant [Permissions]({{< relref "../enterprise/access-control/rbac-fixed-basic-role-definitions/" >}}) to create and manage reports. ## Create or update a report -Only organization admins can create reports by default. You can customize who can create reports with [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}). +Only organization admins can create reports by default. You can customize who can create reports with [Role-based access control]({{< relref "access-control/_index.md" >}}). 1. Click on the reports icon in the side menu. The Reports tab allows you to view, create, and update your reports. 1. Enter report information. All fields are required unless otherwise indicated. @@ -84,7 +84,7 @@ By default, reports use the saved time range of the dashboard. Changing the time The page header of the report displays the time range for the dashboard's data queries. Dashboards set to use the browser's time zone will use the time zone on the Grafana server. -If the time zone is set differently between your Grafana server and its remote image renderer, then the time ranges in the report might be different between the page header and the time axes in the panels. To avoid this, set the time zone to UTC for dashboards when using a remote renderer. Each dashboard's time zone setting is visible in the [time range controls]({{< relref "../dashboards/time-range-controls.md/#dashboard-time-settings" >}}). +If the time zone is set differently between your Grafana server and its remote image renderer, then the time ranges in the report might be different between the page header and the time axes in the panels. To avoid this, set the time zone to UTC for dashboards when using a remote renderer. Each dashboard's time zone setting is visible in the [time range controls]({{< relref "../dashboards/time-range-controls.md#dashboard-time-settings" >}}). ### Layout and orientation @@ -152,7 +152,7 @@ You can pause sending of reports from the report list view by clicking the pause ## Send report via the API -You can send reports programmatically with the [send report]({{< relref "../developers/http_api/reporting.md#send-report" >}}) endpoint in the [HTTP APIs]({{< relref "../developers/http_api" >}}). +You can send reports programmatically with the [send report]({{< relref "../developers/http_api/reporting.md#send-report" >}}) endpoint in the [HTTP APIs]({{< relref "../developers/http_api/" >}}). ## Rendering configuration diff --git a/docs/sources/enterprise/saml/enable-saml.md b/docs/sources/enterprise/saml/enable-saml.md deleted file mode 100644 index d6e104d7a24..00000000000 --- a/docs/sources/enterprise/saml/enable-saml.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -aliases: - - /docs/grafana/latest/auth/saml/ - - /docs/grafana/latest/enterprise/saml/enable-saml/ -description: This contains information to enable SAML authentication in Grafana -keywords: - - grafana - - saml - - documentation - - saml-auth - - enterprise -menuTitle: Enable SAML authentication -title: Enable SAML authentication in Grafana -weight: 30 ---- - -# Enable SAML authentication in Grafana - -To use the SAML integration, in the `auth.saml` section of in the Grafana custom configuration file, set `enabled` to `true`. - -Refer to [Configuration]({{< relref "../../administration/configuration.md" >}}) for more information about configuring Grafana. - -## Certificate and private key - -The SAML SSO standard uses asymmetric encryption to exchange information between the SP (Grafana) and the IdP. To perform such encryption, you need a public part and a private part. In this case, the X.509 certificate provides the public part, while the private key provides the private part. The private key needs to be issued in a [PKCS#8](https://en.wikipedia.org/wiki/PKCS_8) format. - -Grafana supports two ways of specifying both the `certificate` and `private_key`. - -- Without a suffix (`certificate` or `private_key`), the configuration assumes you've supplied the base64-encoded file contents. -- With the `_path` suffix (`certificate_path` or `private_key_path`), then Grafana treats the value entered as a file path and attempts to read the file from the file system. - -> **Note:** You can only use one form of each configuration option. Using multiple forms, such as both `certificate` and `certificate_path`, results in an error. - ---- - -### **Example** of how to generate SAML credentials: - -An example of how to generate a self-signed certificate and private key that's valid for one year: - -```sh -$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes​ -``` - -Base64-encode the cert.pem and key.pem files: -(-w0 switch is not needed on Mac, only for Linux) - -```sh -$ base64 -w0 key.pem > key.pem.base64 -$ base64 -w0 cert.pem > cert.pem.base64 -``` - -The base64-encoded values (`key.pem.base64, cert.pem.base64` files) are then used for certificate and private_key. - -The keys you provide should look like: - -``` ------BEGIN PRIVATE KEY----- -... -... ------END PRIVATE KEY----- -``` diff --git a/docs/sources/enterprise/saml/troubleshoot-saml.md b/docs/sources/enterprise/saml/troubleshoot-saml.md deleted file mode 100644 index 678c105c197..00000000000 --- a/docs/sources/enterprise/saml/troubleshoot-saml.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -aliases: - - /docs/grafana/latest/auth/saml/ - - /docs/grafana/latest/enterprise/saml/troubleshoot-saml/ -description: This contains information on how to troubleshoot SAML authentication - in Grafana -keywords: - - grafana - - saml - - documentation - - saml-auth - - enterprise -menuTitle: Troubleshoot SAML Authentication -title: Troubleshoot SAML Authentication in Grafana -weight: 50 ---- - -# Troubleshoot SAML authentication in Grafana - -To troubleshoot and get more log information, enable SAML debug logging in the configuration file. Refer to [Configuration]({{< relref "../../administration/configuration.md#filters" >}}) for more information. - -```bash -[log] -filters = saml.auth:debug -``` - -## Known issues - -### SAML authentication fails with error: - -- `asn1: structure error: tags don't match` - -We only support one private key format: PKCS#8. - -The keys may be in a different format (PKCS#1 or PKCS#12); in that case, it may be necessary to convert the private key format. - -The following command creates a pkcs8 key file. - -```bash -$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes​ -``` - -#### **Convert** the private key format to base64 - -The following command converts keys to base64 format. - -Base64-encode the cert.pem and key.pem files: -(-w0 switch is not needed on Mac, only for Linux) - -```sh -$ base64 -w0 key.pem > key.pem.base64 -$ base64 -w0 cert.pem > cert.pem.base64 -``` - -The base64-encoded values (`key.pem.base64, cert.pem.base64` files) are then used for certificate and private_key. - -The keys you provide should look like: - -``` ------BEGIN PRIVATE KEY----- -... -... ------END PRIVATE KEY----- -``` diff --git a/docs/sources/enterprise/team-sync.md b/docs/sources/enterprise/team-sync.md index 21d63ede58c..aa844d58e4d 100644 --- a/docs/sources/enterprise/team-sync.md +++ b/docs/sources/enterprise/team-sync.md @@ -34,7 +34,7 @@ This mechanism allows Grafana to remove an existing synchronized user from a tea - [GitLab OAuth]({{< relref "../auth/gitlab.md#team-sync-enterprise-only" >}}) - [LDAP]({{< relref "enhanced_ldap.md#ldap-group-synchronization-for-teams" >}}) - [Okta]({{< relref "../auth/okta.md#team-sync-enterprise-only" >}}) -- [SAML]({{< relref "./saml/configure-saml.md#configure-team-sync" >}}) +- [SAML]({{< relref "configure-saml.md#configure-team-sync" >}}) ## Synchronize a Grafana team with an external group diff --git a/docs/sources/getting-started/getting-started.md b/docs/sources/getting-started/getting-started.md index 860e47699e0..623374e9cec 100644 --- a/docs/sources/getting-started/getting-started.md +++ b/docs/sources/getting-started/getting-started.md @@ -41,7 +41,7 @@ To create your first dashboard: 1. Click the **+** icon on the side menu. 1. On the dashboard, click **Add an empty panel**. 1. In the New dashboard/Edit panel view, go to the **Query** tab. -1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query" >}}) by selecting `-- Grafana --` from the data source selector. This generates the Random Walk dashboard. +1. Configure your [query]({{< relref "../panels/query-a-data-source/add-a-query/" >}}) by selecting `-- Grafana --` from the data source selector. This generates the Random Walk dashboard. 1. Click the **Save** icon in the top right corner of your screen to save the dashboard. 1. Add a descriptive name, and then click **Save**. @@ -49,7 +49,7 @@ Congratulations, you have created your first dashboard and it is displaying resu ## Next steps -Continue to experiment with what you have built, try the [explore workflow]({{< relref "../explore/_index.md" >}}) or another visualization feature. Refer to [Data sources]({{< relref "../datasources" >}}) for a list of supported data sources and instructions on how to [add a data source]({{< relref "../datasources/add-a-data-source.md" >}}). The following topics will be of interest to you: +Continue to experiment with what you have built, try the [explore workflow]({{< relref "../explore/_index.md" >}}) or another visualization feature. Refer to [Data sources]({{< relref "../datasources/" >}}) for a list of supported data sources and instructions on how to [add a data source]({{< relref "../datasources/add-a-data-source.md" >}}). The following topics will be of interest to you: - [Panels]({{< relref "../panels/_index.md" >}}) - [Dashboards]({{< relref "../dashboards/_index.md" >}}) diff --git a/docs/sources/image-rendering/_index.md b/docs/sources/image-rendering/_index.md index 4b293b79495..db5a21ea205 100644 --- a/docs/sources/image-rendering/_index.md +++ b/docs/sources/image-rendering/_index.md @@ -143,7 +143,7 @@ RENDERING_MODE=reusable #### Optimize the performance, CPU and memory usage of the image renderer -The performance and resources consumption of the different modes depend a lot on the number of concurrent requests your service is handling. To understand how many concurrent requests your service is handling, [monitor your image renderer service]({{< relref "./monitoring/" >}}). +The performance and resources consumption of the different modes depend a lot on the number of concurrent requests your service is handling. To understand how many concurrent requests your service is handling, [monitor your image renderer service]({{< relref "monitoring/" >}}). With no concurrent requests, the different modes show very similar performance and CPU / memory usage. diff --git a/docs/sources/image-rendering/monitoring.md b/docs/sources/image-rendering/monitoring.md index 33c1331d95a..57744a1faaf 100644 --- a/docs/sources/image-rendering/monitoring.md +++ b/docs/sources/image-rendering/monitoring.md @@ -14,7 +14,7 @@ weight: 100 # Monitoring the image renderer -Rendering images requires a lot of memory, mainly because Grafana creates browser instances in the background for the actual rendering. Monitoring your service can help you allocate the right amount of resources to your rendering service and set the right [rendering mode]({{< relref "./#rendering-mode" >}}). +Rendering images requires a lot of memory, mainly because Grafana creates browser instances in the background for the actual rendering. Monitoring your service can help you allocate the right amount of resources to your rendering service and set the right [rendering mode]({{< relref "/#rendering-mode" >}}). ## Enable Prometheus metrics endpoint diff --git a/docs/sources/image-rendering/troubleshooting.md b/docs/sources/image-rendering/troubleshooting.md index d8f272892ee..736f27c9adb 100644 --- a/docs/sources/image-rendering/troubleshooting.md +++ b/docs/sources/image-rendering/troubleshooting.md @@ -29,9 +29,9 @@ filters = rendering:debug You can also enable more logs in image renderer service itself by: -- Increasing the [log level]({{< relref "./#log-level" >}}). -- Enabling [verbose logging]({{< relref "./#verbose-logging" >}}). -- [Capturing headless browser output]({{< relref "./#capture-browser-output" >}}). +- Increasing the [log level]({{< relref "/#log-level" >}}). +- Enabling [verbose logging]({{< relref "/#verbose-logging" >}}). +- [Capturing headless browser output]({{< relref "/#capture-browser-output" >}}). ## Missing libraries diff --git a/docs/sources/installation/_index.md b/docs/sources/installation/_index.md index b5b3714c7c2..7632b9ee849 100644 --- a/docs/sources/installation/_index.md +++ b/docs/sources/installation/_index.md @@ -17,13 +17,13 @@ weight: 30 This section discusses the hardware and software requirements as well as the process of installing Grafana on different operating systems. This section has the following topics: -- [Requirements]({{< relref "requirements" >}}) -- [Install on Debian or Ubuntu]({{< relref "debian" >}}) -- [Install on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)]({{< relref "rpm" >}}) -- [Install on macOS]({{< relref "mac" >}}) -- [Install on Windows]({{< relref "windows" >}}) -- [Run Docker image]({{< relref "docker" >}}) -- [Deploy Grafana on Kubernetes]({{< relref "kubernetes" >}}) +- [Requirements]({{< relref "requirements/" >}}) +- [Install on Debian or Ubuntu]({{< relref "debian/" >}}) +- [Install on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)]({{< relref "rpm/" >}}) +- [Install on macOS]({{< relref "mac/" >}}) +- [Install on Windows]({{< relref "windows/" >}}) +- [Run Docker image]({{< relref "docker/" >}}) +- [Deploy Grafana on Kubernetes]({{< relref "kubernetes/" >}}) For upgrade instructions, refer to [Upgrade Grafana]({{< relref "upgrading.md" >}}). To restart Grafana, refer to [Restart Grafana]({{< relref "restart-grafana.md" >}}). diff --git a/docs/sources/installation/requirements.md b/docs/sources/installation/requirements.md index 3939f5f7f88..100f879d4aa 100644 --- a/docs/sources/installation/requirements.md +++ b/docs/sources/installation/requirements.md @@ -22,10 +22,10 @@ Grafana uses other open source software. Refer to [package.json](https://github. The following operating systems are supported for Grafana installation: -- [Debian / Ubuntu]({{< relref "debian" >}}) -- [RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)]({{< relref "rpm" >}}) -- [macOS]({{< relref "mac" >}}) -- [Windows]({{< relref "windows" >}}) +- [Debian / Ubuntu]({{< relref "debian/" >}}) +- [RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)]({{< relref "rpm/" >}}) +- [macOS]({{< relref "mac/" >}}) +- [Windows]({{< relref "windows/" >}}) Installation of Grafana on other operating systems is possible, but it is neither recommended nor supported. @@ -39,8 +39,8 @@ Minimum recommended CPU: 1 Some features might require more memory or CPUs. Features require more resources include: - [Server side rendering of images](https://grafana.com/grafana/plugins/grafana-image-renderer#requirements) -- [Alerting]({{< relref "../alerting" >}}) -- [Data source proxy]({{< relref "../developers/http_api/data_source" >}}) +- [Alerting]({{< relref "../alerting/" >}}) +- [Data source proxy]({{< relref "../developers/http_api/data_source/" >}}) ## Supported databases diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index edf6136c704..7e79b328496 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -180,7 +180,7 @@ you can do that by: - For data sources created through UI, you need to go to data source config, re-enter the password or basic auth password and save the data source. - For data sources created by provisioning, you need to update your config file and use secureJsonData.password or - secureJsonData.basicAuthPassword field. See [provisioning docs]({{< relref "../administration/provisioning" >}}) for example of current + secureJsonData.basicAuthPassword field. See [provisioning docs]({{< relref "../administration/provisioning/" >}}) for example of current configuration. ### Embedding Grafana diff --git a/docs/sources/old-alerting/create-alerts.md b/docs/sources/old-alerting/create-alerts.md index 98f31f734bb..6f76a168338 100644 --- a/docs/sources/old-alerting/create-alerts.md +++ b/docs/sources/old-alerting/create-alerts.md @@ -41,7 +41,7 @@ This section describes the fields you fill out to create an alert. ### Rule -- **Name -** Enter a descriptive name. The name will be displayed in the Alert Rules list. This field supports [templating]({{< relref "./add-notification-template.md" >}}). +- **Name -** Enter a descriptive name. The name will be displayed in the Alert Rules list. This field supports [templating]({{< relref "add-notification-template.md" >}}). - **Evaluate every -** Specify how often the scheduler should evaluate the alert rule. This is referred to as the _evaluation interval_. - **For -** Specify how long the query needs to violate the configured thresholds before the alert notification triggers. @@ -125,7 +125,7 @@ The actual notifications are configured and shared between multiple alerts. Read [Alert notifications]({{< relref "notifications.md" >}}) for information on how to configure and set up notifications. - **Send to -** Select an alert notification channel if you have one set up. -- **Message -** Enter a text message to be sent on the notification channel. Some alert notifiers support transforming the text to HTML or other rich formats. This field supports [templating]({{< relref "./add-notification-template.md" >}}). +- **Message -** Enter a text message to be sent on the notification channel. Some alert notifiers support transforming the text to HTML or other rich formats. This field supports [templating]({{< relref "add-notification-template.md" >}}). - **Tags -** Specify a list of tags (key/value) to be included in the notification. It is only supported by [some notifiers]({{< relref "notifications/#all-supported-notifiers" >}}). ## Alert state history and annotations diff --git a/docs/sources/old-alerting/notifications.md b/docs/sources/old-alerting/notifications.md index cf2e213777b..05ea49b98e5 100644 --- a/docs/sources/old-alerting/notifications.md +++ b/docs/sources/old-alerting/notifications.md @@ -122,12 +122,12 @@ If you are using the token for a slack bot, then you have to invite the bot to t To setup Opsgenie you will need an API Key and the Alert API Url. These can be obtained by configuring a new [Grafana Integration](https://docs.opsgenie.com/docs/grafana-integration). -| Setting | Description | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Alert API URL | The API URL for your Opsgenie instance. This will normally be either `https://api.opsgenie.com` or, for EU customers, `https://api.eu.opsgenie.com`. | -| API Key | The API Key as provided by Opsgenie for your configured Grafana integration. | -| Override priority | Configures the alert priority using the `og_priority` tag. The `og_priority` tag must have one of the following values: `P1`, `P2`, `P3`, `P4`, or `P5`. Default is `False`. | -| Send notification tags as | Specify how you would like [Notification Tags]({{< relref "create-alerts.md/#notifications" >}}) delivered to Opsgenie. They can be delivered as `Tags`, `Extra Properties` or both. Default is Tags. See note below for more information. | +| Setting | Description | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Alert API URL | The API URL for your Opsgenie instance. This will normally be either `https://api.opsgenie.com` or, for EU customers, `https://api.eu.opsgenie.com`. | +| API Key | The API Key as provided by Opsgenie for your configured Grafana integration. | +| Override priority | Configures the alert priority using the `og_priority` tag. The `og_priority` tag must have one of the following values: `P1`, `P2`, `P3`, `P4`, or `P5`. Default is `False`. | +| Send notification tags as | Specify how you would like [Notification Tags]({{< relref "create-alerts.md#notifications" >}}) delivered to Opsgenie. They can be delivered as `Tags`, `Extra Properties` or both. Default is Tags. See note below for more information. | > **Note:** When notification tags are sent as `Tags` they are concatenated into a string with a `key:value` format. If you prefer to receive the notifications tags as key/values under Extra Properties in Opsgenie then change the `Send notification tags as` to either `Extra Properties` or `Tags & Extra Properties`. @@ -286,4 +286,4 @@ This URL is based on the [domain]({{< relref "../../administration/configuration > **Note:** Alert notification templating is only available in Grafana v7.4 and above. -The alert notification template feature allows you to take the [label]({{< relref "../../basics/timeseries-dimensions.md#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "./add-notification-template.md" >}}). +The alert notification template feature allows you to take the [label]({{< relref "../../basics/timeseries-dimensions.md#labels" >}}) value from an alert query and [inject that into alert notifications]({{< relref "add-notification-template.md" >}}). diff --git a/docs/sources/panels/_index.md b/docs/sources/panels/_index.md index 6ff9bf7d93a..2f07a4714ad 100644 --- a/docs/sources/panels/_index.md +++ b/docs/sources/panels/_index.md @@ -14,6 +14,6 @@ The _panel_ is the basic visualization building block in Grafana. Each panel has There are a wide variety of styling and formatting options for each panel. Panels can be dragged and dropped and rearranged on the dashboard. They can also be resized. -Before you begin, ensure that you have configured a data source. For more information about data sources, refer to [Data Sources]({{< relref "../datasources" >}}). +Before you begin, ensure that you have configured a data source. For more information about data sources, refer to [Data Sources]({{< relref "../datasources/" >}}). {{< section >}} diff --git a/docs/sources/panels/configure-thresholds/_index.md b/docs/sources/panels/configure-thresholds/_index.md index 499e5ac2647..11f2d84d2b6 100644 --- a/docs/sources/panels/configure-thresholds/_index.md +++ b/docs/sources/panels/configure-thresholds/_index.md @@ -21,17 +21,17 @@ This section includes information about using thresholds in your visualizations. A threshold is a value that you specify for a metric that is visually reflected in a dashboard when the threshold value is met or exceeded. -Thresholds provide one method for you to conditionally style and color your visualizations based on query results. You can apply thresholds to most, but not all, visualizations. For more information about visualizations, refer to [Visualization panels]({{< relref "../../visualizations" >}}). +Thresholds provide one method for you to conditionally style and color your visualizations based on query results. You can apply thresholds to most, but not all, visualizations. For more information about visualizations, refer to [Visualization panels]({{< relref "../../visualizations/" >}}). You can use thresholds to: -- Color grid lines or grid ares areas in the [Time-series visualization]({{< relref "../../visualizations/time-series" >}}) +- Color grid lines or grid ares areas in the [Time-series visualization]({{< relref "../../visualizations/time-series/" >}}) - Color lines in the [Time-series visualization]({{< relref "../../visualizations/time-series/graph-color-scheme/#from-thresholds" >}}) -- Color the background or value text in the [Stat visualization]({{< relref "../../visualizations/stat-panel" >}}) -- Color the gauge and threshold markers in the [Gauge visualization]({{< relref "../../visualizations/gauge-panel" >}}) -- Color markers in the [Geomap visualization]({{< relref "../../visualizations/geomap" >}}) -- Color cell text or background in the [Table visualization]({{< relref "../../visualizations/table" >}}) -- Define regions and region colors in the [State timeline visualization]({{< relref "../../visualizations/state-timeline" >}}) +- Color the background or value text in the [Stat visualization]({{< relref "../../visualizations/stat-panel/" >}}) +- Color the gauge and threshold markers in the [Gauge visualization]({{< relref "../../visualizations/gauge-panel/" >}}) +- Color markers in the [Geomap visualization]({{< relref "../../visualizations/geomap/" >}}) +- Color cell text or background in the [Table visualization]({{< relref "../../visualizations/table/" >}}) +- Define regions and region colors in the [State timeline visualization]({{< relref "../../visualizations/state-timeline/" >}}) There are two types of thresholds: diff --git a/docs/sources/panels/library-panels/add-library-panel.md b/docs/sources/panels/library-panels/add-library-panel.md index 041a11dbc38..563945656d3 100644 --- a/docs/sources/panels/library-panels/add-library-panel.md +++ b/docs/sources/panels/library-panels/add-library-panel.md @@ -12,7 +12,7 @@ Add a Grafana library panel to a dashboard when you want to provide visualizatio ## Before you begin -- [Create a library panel]({{< relref "../library-panels/create-library-panel.md" >}}). +- [Create a library panel]({{< relref "create-library-panel.md" >}}). **To add a library panel to a dashboard**: diff --git a/docs/sources/panels/override-field-values/delete-a-field-override.md b/docs/sources/panels/override-field-values/delete-a-field-override.md index c214374213a..8130cda10f3 100644 --- a/docs/sources/panels/override-field-values/delete-a-field-override.md +++ b/docs/sources/panels/override-field-values/delete-a-field-override.md @@ -15,7 +15,7 @@ When you delete an override, the appearance of value defaults to its original fo ## Before you begin - [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). -- [Add a field override]({{< relref "../override-field-values/add-a-field-override.md" >}}). +- [Add a field override]({{< relref "add-a-field-override.md" >}}). **To delete a field override**: diff --git a/docs/sources/panels/override-field-values/edit-field-override.md b/docs/sources/panels/override-field-values/edit-field-override.md index cecfd3a583f..2d6b6c23bd7 100644 --- a/docs/sources/panels/override-field-values/edit-field-override.md +++ b/docs/sources/panels/override-field-values/edit-field-override.md @@ -13,7 +13,7 @@ Edit a field override when you want to make changes to an override setting. ## Before you begin - [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). -- [Add a field override]({{< relref "../override-field-values/add-a-field-override.md" >}}). +- [Add a field override]({{< relref "add-a-field-override.md" >}}). **To edit a field override**: diff --git a/docs/sources/panels/override-field-values/view-field-override.md b/docs/sources/panels/override-field-values/view-field-override.md index d7ecfae17c8..7d5fd161727 100644 --- a/docs/sources/panels/override-field-values/view-field-override.md +++ b/docs/sources/panels/override-field-values/view-field-override.md @@ -13,7 +13,7 @@ You can view field overrides in the panel display options. ## Before you begin - [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). -- [Add a field override]({{< relref "../override-field-values/add-a-field-override.md" >}}). +- [Add a field override]({{< relref "add-a-field-override.md" >}}). **To view field overrides**: diff --git a/docs/sources/panels/query-a-data-source/download-raw-query-results.md b/docs/sources/panels/query-a-data-source/download-raw-query-results.md index 85d1f5cdad5..5d5ad740969 100644 --- a/docs/sources/panels/query-a-data-source/download-raw-query-results.md +++ b/docs/sources/panels/query-a-data-source/download-raw-query-results.md @@ -13,7 +13,7 @@ Grafana generates a CSV file that contains your data, including any transformati ## Before you begin - [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). -- [Add a query]({{< relref "../query-a-data-source/add-a-query.md" >}}). +- [Add a query]({{< relref "add-a-query.md" >}}). **To download raw query results**: diff --git a/docs/sources/panels/query-a-data-source/inspect-query-performance.md b/docs/sources/panels/query-a-data-source/inspect-query-performance.md index bfece2faf3d..426bf338357 100644 --- a/docs/sources/panels/query-a-data-source/inspect-query-performance.md +++ b/docs/sources/panels/query-a-data-source/inspect-query-performance.md @@ -13,7 +13,7 @@ The **Stats** tab displays statistics that tell you how long your query takes, h ## Before you begin - [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). -- [Add a query]({{< relref "../query-a-data-source/add-a-query.md" >}}). +- [Add a query]({{< relref "add-a-query.md" >}}). **To inspect query performance**: diff --git a/docs/sources/panels/query-a-data-source/inspect-request-and-response-data.md b/docs/sources/panels/query-a-data-source/inspect-request-and-response-data.md index e542369b186..e52bae895d7 100644 --- a/docs/sources/panels/query-a-data-source/inspect-request-and-response-data.md +++ b/docs/sources/panels/query-a-data-source/inspect-request-and-response-data.md @@ -13,7 +13,7 @@ Inspect query request and response data when you want to troubleshoot a query th ## Before you begin - [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). -- [Add a query]({{< relref "../query-a-data-source/add-a-query.md" >}}). +- [Add a query]({{< relref "add-a-query.md" >}}). **To inspect query request and response data**: diff --git a/docs/sources/panels/query-a-data-source/share-query.md b/docs/sources/panels/query-a-data-source/share-query.md index 1aee1fae4fd..29cfa593d36 100644 --- a/docs/sources/panels/query-a-data-source/share-query.md +++ b/docs/sources/panels/query-a-data-source/share-query.md @@ -19,7 +19,7 @@ This strategy can drastically reduce the number of queries being made when you f 1. [Create a dashboard]({{< relref "../../getting-started/getting-started.md/#step-3-create-a-dashboard" >}}). 1. [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). 1. Change the title to "Source panel". You'll use this panel as a source for the other panels. -1. Define the [query]({{< relref "../query-a-data-source/add-a-query.md" >}}) or queries that you want share. +1. Define the [query]({{< relref "add-a-query.md" >}}) or queries that you want share. If you don't have a data source available, use the **Grafana** data source, which returns a random time series that you can use for testing. diff --git a/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/write-an-expression.md b/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/write-an-expression.md index 8cd0bfbbd8c..d87768b79bb 100644 --- a/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/write-an-expression.md +++ b/docs/sources/panels/query-a-data-source/use-expressions-to-manipulate-data/write-an-expression.md @@ -10,7 +10,7 @@ weight: 20 If your data source supports them, then Grafana displays the **Expression** button and shows any existing expressions in the query editor list. -For more information about expressions, refer to [About expressions]({{< relref "./about-expressions.md" >}}). +For more information about expressions, refer to [About expressions]({{< relref "about-expressions.md" >}}). ## Before you begin @@ -22,7 +22,7 @@ For more information about expressions, refer to [About expressions]({{< relref 1. Below the query, click **Expression**. 1. In the **Operation** field, select the type of expression you want to write. - For more information about expression operations, refer to [About expressions]({{< relref "./about-expressions.md" >}}). + For more information about expression operations, refer to [About expressions]({{< relref "about-expressions.md" >}}). 1. Write the expression. 1. Click **Apply**. diff --git a/docs/sources/panels/working-with-panels/add-panel.md b/docs/sources/panels/working-with-panels/add-panel.md index 05d80a5c0e0..8df88fc16e3 100644 --- a/docs/sources/panels/working-with-panels/add-panel.md +++ b/docs/sources/panels/working-with-panels/add-panel.md @@ -50,7 +50,7 @@ Panels allow you to show your data in visual form. Each panel needs at least one - [Visualization-specific options]({{< relref "../../visualizations/_index.md" >}}) - [Override field values]({{< relref "../override-field-values/about-field-overrides.md" >}}) - [Configure thresholds]({{< relref "../configure-thresholds/" >}}) - - [Apply color to series and fields]({{< relref "./apply-color-to-series.md" >}}) + - [Apply color to series and fields]({{< relref "apply-color-to-series.md" >}}) 1. Add a note to describe the visualization (or describe your changes) and then click **Save** in the upper-right corner of the page. diff --git a/docs/sources/panels/working-with-panels/add-title-and-description.md b/docs/sources/panels/working-with-panels/add-title-and-description.md index 324e9fd6db4..96bdaac5237 100644 --- a/docs/sources/panels/working-with-panels/add-title-and-description.md +++ b/docs/sources/panels/working-with-panels/add-title-and-description.md @@ -12,7 +12,7 @@ Add a title and description to a panel to share with users any important informa ## Before you begin: -- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). +- [Add a panel to a dashboard]({{< relref "add-panel.md" >}}). **To add a title and description to a panel**: diff --git a/docs/sources/panels/working-with-panels/apply-color-to-series.md b/docs/sources/panels/working-with-panels/apply-color-to-series.md index ea29194c87e..4b70d6ea6f1 100644 --- a/docs/sources/panels/working-with-panels/apply-color-to-series.md +++ b/docs/sources/panels/working-with-panels/apply-color-to-series.md @@ -15,7 +15,7 @@ Continuous color interpolates a color using the percentage of a value relative t ## Before you begin -- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). +- [Add a panel to a dashboard]({{< relref "add-panel.md" >}}). **To apply color to series and fields**: diff --git a/docs/sources/panels/working-with-panels/configure-legend.md b/docs/sources/panels/working-with-panels/configure-legend.md index 49e5e7bbd07..ee67bb0b70f 100644 --- a/docs/sources/panels/working-with-panels/configure-legend.md +++ b/docs/sources/panels/working-with-panels/configure-legend.md @@ -18,7 +18,7 @@ When you apply your changes, the visualization changes appear to all users of th ### Before you begin -- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). +- [Add a panel to a dashboard]({{< relref "add-panel.md" >}}). **To isolate series data in a visualization**: diff --git a/docs/sources/panels/working-with-panels/format-standard-fields.md b/docs/sources/panels/working-with-panels/format-standard-fields.md index 0c6c34c1066..5f0337be684 100644 --- a/docs/sources/panels/working-with-panels/format-standard-fields.md +++ b/docs/sources/panels/working-with-panels/format-standard-fields.md @@ -18,7 +18,7 @@ For a complete list of field formatting options, refer to [Standard field defini ## Before you begin -- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). +- [Add a panel to a dashboard]({{< relref "add-panel.md" >}}). **To format a standard field**: diff --git a/docs/sources/panels/working-with-panels/navigate-panel-editor.md b/docs/sources/panels/working-with-panels/navigate-panel-editor.md index ba89626877e..fc4963298dc 100644 --- a/docs/sources/panels/working-with-panels/navigate-panel-editor.md +++ b/docs/sources/panels/working-with-panels/navigate-panel-editor.md @@ -33,9 +33,9 @@ This page describes the parts of the Grafana panel editor. 4. Panel display options: The display options section contains tabs where you configure almost every aspect of your data visualization, including: - - [Apply color to series and fields]({{< relref "./apply-color-to-series.md" >}}) - - [Format a standard field]({{< relref "./format-standard-fields.md" >}}) - - [Add a title and description to a panel]({{< relref "./add-title-and-description.md" >}}) + - [Apply color to series and fields]({{< relref "apply-color-to-series.md" >}}) + - [Format a standard field]({{< relref "format-standard-fields.md" >}}) + - [Add a title and description to a panel]({{< relref "add-title-and-description.md" >}}) > Not all options are available for each visualization. diff --git a/docs/sources/panels/working-with-panels/view-json-model.md b/docs/sources/panels/working-with-panels/view-json-model.md index a576cff4895..25469c0057e 100644 --- a/docs/sources/panels/working-with-panels/view-json-model.md +++ b/docs/sources/panels/working-with-panels/view-json-model.md @@ -12,7 +12,7 @@ Explore and export panel, panel data, and data frame JSON models. ## Before you begin: -- [Add a panel to a dashboard]({{< relref "../working-with-panels/add-panel.md" >}}). +- [Add a panel to a dashboard]({{< relref "add-panel.md" >}}). **To view a panel JSON model**: diff --git a/docs/sources/plugins/_index.md b/docs/sources/plugins/_index.md index 0566d20224a..af53d6a8f64 100644 --- a/docs/sources/plugins/_index.md +++ b/docs/sources/plugins/_index.md @@ -41,6 +41,6 @@ Use app plugins when you want to create an custom out-of-the-box monitoring expe ## Learn more -- [Install plugins]({{< relref "./installation.md" >}}) -- [Plugin signatures]({{< relref "./plugin-signatures.md" >}}) +- [Install plugins]({{< relref "installation.md" >}}) +- [Plugin signatures]({{< relref "plugin-signatures.md" >}}) - Browse the available [Plugins](https://grafana.com/grafana/plugins) diff --git a/docs/sources/plugins/installation.md b/docs/sources/plugins/installation.md index 206e35b2b98..9056ed11517 100644 --- a/docs/sources/plugins/installation.md +++ b/docs/sources/plugins/installation.md @@ -26,7 +26,7 @@ Follow the instructions on the Install tab. You can either install the plugin wi For more information about Grafana CLI plugin commands, refer to [Plugin commands]({{< relref "../administration/cli.md#plugins-commands" >}}). -As of Grafana v8.0, a plugin catalog app was introduced in order to make managing plugins easier. For more information, refer to [Plugin catalog]({{< relref "./catalog.md" >}}). +As of Grafana v8.0, a plugin catalog app was introduced in order to make managing plugins easier. For more information, refer to [Plugin catalog]({{< relref "catalog.md" >}}). ### Install a packaged plugin diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index efc34bb3fd5..e3ee8dc7b04 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -10,90 +10,90 @@ weight: 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. -- [Release notes for 8.5.3]({{< relref "release-notes-8-5-3" >}}) -- [Release notes for 8.5.2]({{< relref "release-notes-8-5-2" >}}) -- [Release notes for 8.5.1]({{< relref "release-notes-8-5-1" >}}) -- [Release notes for 8.5.0]({{< relref "release-notes-8-5-0" >}}) -- [Release notes for 8.5.0-beta1]({{< relref "release-notes-8-5-0-beta1" >}}) -- [Release notes for 8.4.7]({{< relref "release-notes-8-4-7" >}}) -- [Release notes for 8.4.6]({{< relref "release-notes-8-4-6" >}}) -- [Release notes for 8.4.5]({{< relref "release-notes-8-4-5" >}}) -- [Release notes for 8.4.4]({{< relref "release-notes-8-4-4" >}}) -- [Release notes for 8.4.3]({{< relref "release-notes-8-4-3" >}}) -- [Release notes for 8.4.2]({{< relref "release-notes-8-4-2" >}}) -- [Release notes for 8.4.1]({{< relref "release-notes-8-4-1" >}}) -- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}}) -- [Release notes for 8.3.7]({{< relref "release-notes-8-3-7" >}}) -- [Release notes for 8.3.6]({{< relref "release-notes-8-3-6" >}}) -- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}}) -- [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}}) -- [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}}) -- [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}}) -- [Release notes for 8.3.1]({{< relref "release-notes-8-3-1" >}}) -- [Release notes for 8.3.0]({{< relref "release-notes-8-3-0" >}}) -- [Release notes for 8.3.0-beta2]({{< relref "release-notes-8-3-0-beta2" >}}) -- [Release notes for 8.3.0-beta1]({{< relref "release-notes-8-3-0-beta1" >}}) -- [Release notes for 8.2.7]({{< relref "release-notes-8-2-7" >}}) -- [Release notes for 8.2.6]({{< relref "release-notes-8-2-6" >}}) -- [Release notes for 8.2.5]({{< relref "release-notes-8-2-5" >}}) -- [Release notes for 8.2.4]({{< relref "release-notes-8-2-4" >}}) -- [Release notes for 8.2.3]({{< relref "release-notes-8-2-3" >}}) -- [Release notes for 8.2.2]({{< relref "release-notes-8-2-2" >}}) -- [Release notes for 8.2.1]({{< relref "release-notes-8-2-1" >}}) -- [Release notes for 8.2.0]({{< relref "release-notes-8-2-0" >}}) -- [Release notes for 8.2.0-beta2]({{< relref "release-notes-8-2-0-beta2" >}}) -- [Release notes for 8.2.0-beta1]({{< relref "release-notes-8-2-0-beta1" >}}) -- [Release notes for 8.1.8]({{< relref "release-notes-8-1-8" >}}) -- [Release notes for 8.1.7]({{< relref "release-notes-8-1-7" >}}) -- [Release notes for 8.1.6]({{< relref "release-notes-8-1-6" >}}) -- [Release notes for 8.1.5]({{< relref "release-notes-8-1-5" >}}) -- [Release notes for 8.1.4]({{< relref "release-notes-8-1-4" >}}) -- [Release notes for 8.1.3]({{< relref "release-notes-8-1-3" >}}) -- [Release notes for 8.1.2]({{< relref "release-notes-8-1-2" >}}) -- [Release notes for 8.1.1]({{< relref "release-notes-8-1-1" >}}) -- [Release notes for 8.1.0]({{< relref "release-notes-8-1-0" >}}) -- [Release notes for 8.1.0-beta3]({{< relref "release-notes-8-1-0-beta3" >}}) -- [Release notes for 8.1.0-beta2]({{< relref "release-notes-8-1-0-beta2" >}}) -- [Release notes for 8.1.0-beta1]({{< relref "release-notes-8-1-0-beta1" >}}) -- [Release notes for 8.0.7]({{< relref "release-notes-8-0-7" >}}) -- [Release notes for 8.0.6]({{< relref "release-notes-8-0-6" >}}) -- [Release notes for 8.0.5]({{< relref "release-notes-8-0-5" >}}) -- [Release notes for 8.0.4]({{< relref "release-notes-8-0-4" >}}) -- [Release notes for 8.0.3]({{< relref "release-notes-8-0-3" >}}) -- [Release notes for 8.0.2]({{< relref "release-notes-8-0-2" >}}) -- [Release notes for 8.0.1]({{< relref "release-notes-8-0-1" >}}) -- [Release notes for 8.0.0]({{< relref "release-notes-8-0-0" >}}) -- [Release notes for 8.0.0-beta3]({{< relref "release-notes-8-0-0-beta3" >}}) -- [Release notes for 8.0.0-beta2]({{< relref "release-notes-8-0-0-beta2" >}}) -- [Release notes for 8.0.0-beta1]({{< relref "release-notes-8-0-0-beta1" >}}) -- [Release notes for 7.5.15]({{< relref "release-notes-7-5-15" >}}) -- [Release notes for 7.5.13]({{< relref "release-notes-7-5-13" >}}) -- [Release notes for 7.5.12]({{< relref "release-notes-7-5-12" >}}) -- [Release notes for 7.5.11]({{< relref "release-notes-7-5-11" >}}) -- [Release notes for 7.5.10]({{< relref "release-notes-7-5-10" >}}) -- [Release notes for 7.5.9]({{< relref "release-notes-7-5-9" >}}) -- [Release notes for 7.5.8]({{< relref "release-notes-7-5-8" >}}) -- [Release notes for 7.5.7]({{< relref "release-notes-7-5-7" >}}) -- [Release notes for 7.5.6]({{< relref "release-notes-7-5-6" >}}) -- [Release notes for 7.5.5]({{< relref "release-notes-7-5-5" >}}) -- [Release notes for 7.5.4]({{< relref "release-notes-7-5-4" >}}) -- [Release notes for 7.5.3]({{< relref "release-notes-7-5-3" >}}) -- [Release notes for 7.5.2]({{< relref "release-notes-7-5-2" >}}) -- [Release notes for 7.5.1]({{< relref "release-notes-7-5-1" >}}) -- [Release notes for 7.5.0]({{< relref "release-notes-7-5-0" >}}) -- [Release notes for 7.5.0-beta2]({{< relref "release-notes-7-5-0-beta2" >}}) -- [Release notes for 7.5.0-beta1]({{< relref "release-notes-7-5-0-beta1" >}}) -- [Release notes for 7.4.5]({{< relref "release-notes-7-4-5" >}}) -- [Release notes for 7.4.3]({{< relref "release-notes-7-4-3" >}}) -- [Release notes for 7.4.2]({{< relref "release-notes-7-4-2" >}}) -- [Release notes for 7.4.1]({{< relref "release-notes-7-4-1" >}}) -- [Release notes for 7.4.0]({{< relref "release-notes-7-4-0" >}}) -- [Release notes for 7.3.10]({{< relref "release-notes-7-3-10" >}}) -- [Release notes for 7.3.7]({{< relref "release-notes-7-3-7" >}}) -- [Release notes for 7.3.6]({{< relref "release-notes-7-3-6" >}}) -- [Release notes for 7.3.5]({{< relref "release-notes-7-3-5" >}}) -- [Release notes for 7.3.4]({{< relref "release-notes-7-3-4" >}}) -- [Release notes for 7.3.3]({{< relref "release-notes-7-3-3" >}}) -- [Release notes for 7.3.2]({{< relref "release-notes-7-3-2" >}}) -- [Release notes for 7.3.1]({{< relref "release-notes-7-3-1" >}}) -- [Release notes for 7.3.0]({{< relref "release-notes-7-3-0" >}}) +- [Release notes for 8.5.3]({{< relref "release-notes-8-5-3/" >}}) +- [Release notes for 8.5.2]({{< relref "release-notes-8-5-2/" >}}) +- [Release notes for 8.5.1]({{< relref "release-notes-8-5-1/" >}}) +- [Release notes for 8.5.0]({{< relref "release-notes-8-5-0/" >}}) +- [Release notes for 8.5.0-beta1]({{< relref "release-notes-8-5-0-beta1/" >}}) +- [Release notes for 8.4.7]({{< relref "release-notes-8-4-7/" >}}) +- [Release notes for 8.4.6]({{< relref "release-notes-8-4-6/" >}}) +- [Release notes for 8.4.5]({{< relref "release-notes-8-4-5/" >}}) +- [Release notes for 8.4.4]({{< relref "release-notes-8-4-4/" >}}) +- [Release notes for 8.4.3]({{< relref "release-notes-8-4-3/" >}}) +- [Release notes for 8.4.2]({{< relref "release-notes-8-4-2/" >}}) +- [Release notes for 8.4.1]({{< relref "release-notes-8-4-1/" >}}) +- [Release notes for 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1/" >}}) +- [Release notes for 8.3.7]({{< relref "release-notes-8-3-7/" >}}) +- [Release notes for 8.3.6]({{< relref "release-notes-8-3-6/" >}}) +- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5/" >}}) +- [Release notes for 8.3.4]({{< relref "release-notes-8-3-4/" >}}) +- [Release notes for 8.3.3]({{< relref "release-notes-8-3-3/" >}}) +- [Release notes for 8.3.2]({{< relref "release-notes-8-3-2/" >}}) +- [Release notes for 8.3.1]({{< relref "release-notes-8-3-1/" >}}) +- [Release notes for 8.3.0]({{< relref "release-notes-8-3-0/" >}}) +- [Release notes for 8.3.0-beta2]({{< relref "release-notes-8-3-0-beta2/" >}}) +- [Release notes for 8.3.0-beta1]({{< relref "release-notes-8-3-0-beta1/" >}}) +- [Release notes for 8.2.7]({{< relref "release-notes-8-2-7/" >}}) +- [Release notes for 8.2.6]({{< relref "release-notes-8-2-6/" >}}) +- [Release notes for 8.2.5]({{< relref "release-notes-8-2-5/" >}}) +- [Release notes for 8.2.4]({{< relref "release-notes-8-2-4/" >}}) +- [Release notes for 8.2.3]({{< relref "release-notes-8-2-3/" >}}) +- [Release notes for 8.2.2]({{< relref "release-notes-8-2-2/" >}}) +- [Release notes for 8.2.1]({{< relref "release-notes-8-2-1/" >}}) +- [Release notes for 8.2.0]({{< relref "release-notes-8-2-0/" >}}) +- [Release notes for 8.2.0-beta2]({{< relref "release-notes-8-2-0-beta2/" >}}) +- [Release notes for 8.2.0-beta1]({{< relref "release-notes-8-2-0-beta1/" >}}) +- [Release notes for 8.1.8]({{< relref "release-notes-8-1-8/" >}}) +- [Release notes for 8.1.7]({{< relref "release-notes-8-1-7/" >}}) +- [Release notes for 8.1.6]({{< relref "release-notes-8-1-6/" >}}) +- [Release notes for 8.1.5]({{< relref "release-notes-8-1-5/" >}}) +- [Release notes for 8.1.4]({{< relref "release-notes-8-1-4/" >}}) +- [Release notes for 8.1.3]({{< relref "release-notes-8-1-3/" >}}) +- [Release notes for 8.1.2]({{< relref "release-notes-8-1-2/" >}}) +- [Release notes for 8.1.1]({{< relref "release-notes-8-1-1/" >}}) +- [Release notes for 8.1.0]({{< relref "release-notes-8-1-0/" >}}) +- [Release notes for 8.1.0-beta3]({{< relref "release-notes-8-1-0-beta3/" >}}) +- [Release notes for 8.1.0-beta2]({{< relref "release-notes-8-1-0-beta2/" >}}) +- [Release notes for 8.1.0-beta1]({{< relref "release-notes-8-1-0-beta1/" >}}) +- [Release notes for 8.0.7]({{< relref "release-notes-8-0-7/" >}}) +- [Release notes for 8.0.6]({{< relref "release-notes-8-0-6/" >}}) +- [Release notes for 8.0.5]({{< relref "release-notes-8-0-5/" >}}) +- [Release notes for 8.0.4]({{< relref "release-notes-8-0-4/" >}}) +- [Release notes for 8.0.3]({{< relref "release-notes-8-0-3/" >}}) +- [Release notes for 8.0.2]({{< relref "release-notes-8-0-2/" >}}) +- [Release notes for 8.0.1]({{< relref "release-notes-8-0-1/" >}}) +- [Release notes for 8.0.0]({{< relref "release-notes-8-0-0/" >}}) +- [Release notes for 8.0.0-beta3]({{< relref "release-notes-8-0-0-beta3/" >}}) +- [Release notes for 8.0.0-beta2]({{< relref "release-notes-8-0-0-beta2/" >}}) +- [Release notes for 8.0.0-beta1]({{< relref "release-notes-8-0-0-beta1/" >}}) +- [Release notes for 7.5.15]({{< relref "release-notes-7-5-15/" >}}) +- [Release notes for 7.5.13]({{< relref "release-notes-7-5-13/" >}}) +- [Release notes for 7.5.12]({{< relref "release-notes-7-5-12/" >}}) +- [Release notes for 7.5.11]({{< relref "release-notes-7-5-11/" >}}) +- [Release notes for 7.5.10]({{< relref "release-notes-7-5-10/" >}}) +- [Release notes for 7.5.9]({{< relref "release-notes-7-5-9/" >}}) +- [Release notes for 7.5.8]({{< relref "release-notes-7-5-8/" >}}) +- [Release notes for 7.5.7]({{< relref "release-notes-7-5-7/" >}}) +- [Release notes for 7.5.6]({{< relref "release-notes-7-5-6/" >}}) +- [Release notes for 7.5.5]({{< relref "release-notes-7-5-5/" >}}) +- [Release notes for 7.5.4]({{< relref "release-notes-7-5-4/" >}}) +- [Release notes for 7.5.3]({{< relref "release-notes-7-5-3/" >}}) +- [Release notes for 7.5.2]({{< relref "release-notes-7-5-2/" >}}) +- [Release notes for 7.5.1]({{< relref "release-notes-7-5-1/" >}}) +- [Release notes for 7.5.0]({{< relref "release-notes-7-5-0/" >}}) +- [Release notes for 7.5.0-beta2]({{< relref "release-notes-7-5-0-beta2/" >}}) +- [Release notes for 7.5.0-beta1]({{< relref "release-notes-7-5-0-beta1/" >}}) +- [Release notes for 7.4.5]({{< relref "release-notes-7-4-5/" >}}) +- [Release notes for 7.4.3]({{< relref "release-notes-7-4-3/" >}}) +- [Release notes for 7.4.2]({{< relref "release-notes-7-4-2/" >}}) +- [Release notes for 7.4.1]({{< relref "release-notes-7-4-1/" >}}) +- [Release notes for 7.4.0]({{< relref "release-notes-7-4-0/" >}}) +- [Release notes for 7.3.10]({{< relref "release-notes-7-3-10/" >}}) +- [Release notes for 7.3.7]({{< relref "release-notes-7-3-7/" >}}) +- [Release notes for 7.3.6]({{< relref "release-notes-7-3-6/" >}}) +- [Release notes for 7.3.5]({{< relref "release-notes-7-3-5/" >}}) +- [Release notes for 7.3.4]({{< relref "release-notes-7-3-4/" >}}) +- [Release notes for 7.3.3]({{< relref "release-notes-7-3-3/" >}}) +- [Release notes for 7.3.2]({{< relref "release-notes-7-3-2/" >}}) +- [Release notes for 7.3.1]({{< relref "release-notes-7-3-1/" >}}) +- [Release notes for 7.3.0]({{< relref "release-notes-7-3-0/" >}}) diff --git a/docs/sources/variables/variable-types/_index.md b/docs/sources/variables/variable-types/_index.md index 8c6d857a2d7..433477869af 100644 --- a/docs/sources/variables/variable-types/_index.md +++ b/docs/sources/variables/variable-types/_index.md @@ -18,5 +18,5 @@ Grafana uses several types of variables. | Data source | Quickly change the data source for an entire dashboard. [Add a data source variable]({{< relref "add-data-source-variable.md" >}}). | | Interval | Interval variables represent time spans. [Add an interval variable]({{< relref "add-interval-variable.md" >}}). | | Ad hoc filters | Key/value filters that are automatically added to all metric queries for a data source (InfluxDB, Prometheus, and Elasticsearch only). [Add ad hoc filters]({{< relref "add-ad-hoc-filters.md" >}}). | -| Global variables | Built-in variables that can be used in expressions in the query editor. Refer to [Global variables]({{< relref "global-variables" >}}). | +| Global variables | Built-in variables that can be used in expressions in the query editor. Refer to [Global variables]({{< relref "global-variables/" >}}). | | Chained variables | Variable queries can contain other variables. Refer to [Chained variables]({{< relref "chained-variables.md" >}}). | diff --git a/docs/sources/visualizations/_index.md b/docs/sources/visualizations/_index.md index 4e1435d2f2d..d338ed8dfff 100644 --- a/docs/sources/visualizations/_index.md +++ b/docs/sources/visualizations/_index.md @@ -13,28 +13,28 @@ Grafana offers a variety of visualizations to support different use cases. This > **Note:** If you are unsure which visualization to pick, Grafana can provide visualization suggestions based on the panel query. When you select a visualization, Grafana will show a preview with that visualization applied. For more information, see the [add a panel]({{< relref "../panels/working-with-panels/add-panel.md" >}}) documentation. - Graphs & charts - - [Time series]({{< relref "./time-series/_index.md" >}}) is the default and main Graph visualization. - - [State timeline]({{< relref "./state-timeline.md" >}}) for state changes over time. - - [Status history]({{< relref "./status-history.md" >}}) for periodic state over time. - - [Bar chart]({{< relref "./bar-chart.md" >}}) shows any categorical data. - - [Histogram]({{< relref "./histogram.md" >}}) calculates and shows value distribution in a bar chart. - - [Heatmap]({{< relref "./heatmap.md" >}}) visualizes data in two dimensions, used typically for the magnitude of a phenomenon. - - [Pie chart]({{< relref "./pie-chart-panel.md" >}}) is typically used where proportionality is important. - - [Candlestick]({{< relref "./candlestick.md" >}}) is typically for financial data where the focus is price/data movement. + - [Time series]({{< relref "time-series/_index.md" >}}) is the default and main Graph visualization. + - [State timeline]({{< relref "state-timeline.md" >}}) for state changes over time. + - [Status history]({{< relref "status-history.md" >}}) for periodic state over time. + - [Bar chart]({{< relref "bar-chart.md" >}}) shows any categorical data. + - [Histogram]({{< relref "histogram.md" >}}) calculates and shows value distribution in a bar chart. + - [Heatmap]({{< relref "heatmap.md" >}}) visualizes data in two dimensions, used typically for the magnitude of a phenomenon. + - [Pie chart]({{< relref "pie-chart-panel.md" >}}) is typically used where proportionality is important. + - [Candlestick]({{< relref "candlestick.md" >}}) is typically for financial data where the focus is price/data movement. - Stats & numbers - - [Stat]({{< relref "./stat-panel.md" >}}) for big stats and optional sparkline. - - [Gauge]({{< relref "./gauge-panel.md" >}}) is a normal radial gauge. - - [Bar gauge]({{< relref "./bar-gauge-panel.md" >}}) is a horizontal or vertical bar gauge. + - [Stat]({{< relref "stat-panel.md" >}}) for big stats and optional sparkline. + - [Gauge]({{< relref "gauge-panel.md" >}}) is a normal radial gauge. + - [Bar gauge]({{< relref "bar-gauge-panel.md" >}}) is a horizontal or vertical bar gauge. - Misc - - [Table]({{< relref "./table/_index.md" >}}) is the main and only table visualization. - - [Logs]({{< relref "./logs-panel.md" >}}) is the main visualization for logs. - - [Node Graph]({{< relref "./node-graph.md" >}}) for directed graphs or networks. - - [Traces]({{< relref "./traces.md" >}}) is the main visualization for traces. + - [Table]({{< relref "table/_index.md" >}}) is the main and only table visualization. + - [Logs]({{< relref "logs-panel.md" >}}) is the main visualization for logs. + - [Node Graph]({{< relref "node-graph.md" >}}) for directed graphs or networks. + - [Traces]({{< relref "traces.md" >}}) is the main visualization for traces. - Widgets - - [Dashboard list]({{< relref "./dashboard-list-panel.md" >}}) can list dashboards. - - [Alert list]({{< relref "./alert-list-panel.md" >}}) can list alerts. - - [Text panel]({{< relref "./text-panel.md" >}}) can show markdown and html. - - [News panel]({{< relref "./news-panel.md" >}}) can show RSS feeds. + - [Dashboard list]({{< relref "dashboard-list-panel.md" >}}) can list dashboards. + - [Alert list]({{< relref "alert-list-panel.md" >}}) can list alerts. + - [Text panel]({{< relref "text-panel.md" >}}) can show markdown and html. + - [News panel]({{< relref "news-panel.md" >}}) can show RSS feeds. ## Get more @@ -46,11 +46,11 @@ Below you can find some good examples for how all the visualizations in Grafana ### Graphs -For time based line, area and bar charts we recommend the default [Time series]({{< relref "./time-series/_index.md" >}}) visualization. [This public demo dashboard](https://play.grafana.org/d/000000016/1-time-series-graphs?orgId=1) contains many different examples for how this visualization can be configured and styled. +For time based line, area and bar charts we recommend the default [Time series]({{< relref "time-series/_index.md" >}}) visualization. [This public demo dashboard](https://play.grafana.org/d/000000016/1-time-series-graphs?orgId=1) contains many different examples for how this visualization can be configured and styled. {{< figure src="/static/img/docs/time-series-panel/time_series_small_example.png" max-width="700px" caption="Time series" >}} -For categorical data use the [Bar chart]({{< relref "./bar-chart.md" >}}) visualization. +For categorical data use the [Bar chart]({{< relref "bar-chart.md" >}}) visualization. {{< figure src="/static/img/docs/bar-chart-panel/barchart_small_example.png" max-width="700px" caption="Bar chart" >}} @@ -62,29 +62,29 @@ The [Stat](stat-panel/) visualization shows one large stat value with an optiona ### Gauge -If you want to present a value as it relates to a min and max value you have two options. First a standard [Radial Gauge]({{< relref "./gauge-panel.md" >}}) shown below. +If you want to present a value as it relates to a min and max value you have two options. First a standard [Radial Gauge]({{< relref "gauge-panel.md" >}}) shown below. {{< figure src="/static/img/docs/v66/gauge_panel_cover.png" max-width="700px" >}} -Secondly Grafana also has a horizontal or vertical [Bar gauge]({{< relref "./bar-gauge-panel.md" >}}) with three different distinct display modes. +Secondly Grafana also has a horizontal or vertical [Bar gauge]({{< relref "bar-gauge-panel.md" >}}) with three different distinct display modes. {{< figure src="/static/img/docs/v66/bar_gauge_lcd.png" max-width="700px" >}} ### Table -To show data in a table layout, use the [Table]({{< relref "./table/_index.md" >}}) visualization. +To show data in a table layout, use the [Table]({{< relref "table/_index.md" >}}) visualization. {{< figure src="/static/img/docs/tables/table_visualization.png" max-width="700px" lightbox="true" caption="Table visualization" >}} ### Pie chart -Grafana now ships with an included [Pie chart]({{< relref "./pie-chart-panel.md" >}}) visualization. +Grafana now ships with an included [Pie chart]({{< relref "pie-chart-panel.md" >}}) visualization. {{< figure src="/static/img/docs/pie-chart-panel/pie-chart-example.png" max-width="700px" lightbox="true" caption="Pie chart visualization" >}} ### Heatmaps -To show value distribution over, time use the [heatmap]({{< relref "./heatmap.md" >}}) visualization. +To show value distribution over, time use the [heatmap]({{< relref "heatmap.md" >}}) visualization. {{< figure src="/static/img/docs/v43/heatmap_panel_cover.jpg" max-width="1000px" lightbox="true" caption="Heatmap" >}} diff --git a/docs/sources/visualizations/candlestick.md b/docs/sources/visualizations/candlestick.md index 8208781452b..1bbe8c427a0 100644 --- a/docs/sources/visualizations/candlestick.md +++ b/docs/sources/visualizations/candlestick.md @@ -20,7 +20,7 @@ The Candlestick panel allows you to visualize data that includes a number of con {{< figure src="/static/img/docs/candlestick-panel/candlestick-panel-8-3.png" max-width="1200px" caption="Candlestick panel" >}} -The Candlestick panel builds upon the foundation of the [time series]({{< relref "./time-series/_index.md" >}}) panel and includes many common configuration settings. +The Candlestick panel builds upon the foundation of the [time series]({{< relref "time-series/_index.md" >}}) panel and includes many common configuration settings. ## Mode @@ -56,4 +56,4 @@ The candlestick panel will attempt to map fields to the appropriate dimension. T ## Additional fields -The candlestick panel is based on the time series panel. It can visualization additional data dimensions beyond open, high, low, close, and volume The **Include** and **Ignore** options allow the panel to visualize other included data such as simple moving averages, Bollinger bands and more, using the same styles and configurations available in the [time series]({{< relref "./time-series/_index.md" >}}) panel. +The candlestick panel is based on the time series panel. It can visualization additional data dimensions beyond open, high, low, close, and volume The **Include** and **Ignore** options allow the panel to visualize other included data such as simple moving averages, Bollinger bands and more, using the same styles and configurations available in the [time series]({{< relref "time-series/_index.md" >}}) panel. diff --git a/docs/sources/visualizations/graph-panel.md b/docs/sources/visualizations/graph-panel.md index 4fbb88db7a4..d59158be92c 100644 --- a/docs/sources/visualizations/graph-panel.md +++ b/docs/sources/visualizations/graph-panel.md @@ -16,7 +16,7 @@ weight: 500 # Graph panel (old) -> **Note:** [Time series panel]({{< relref "./time-series/_index.md" >}}) visualization is going to replace the Graph panel visualization in a future release. +> **Note:** [Time series panel]({{< relref "time-series/_index.md" >}}) visualization is going to replace the Graph panel visualization in a future release. The graph panel can render metrics as a line, a path of dots, or a series of bars. This type of graph is versatile enough to display almost any time-series data. diff --git a/docs/sources/visualizations/table/_index.md b/docs/sources/visualizations/table/_index.md index 5edbe5a5e52..9dad366bf35 100644 --- a/docs/sources/visualizations/table/_index.md +++ b/docs/sources/visualizations/table/_index.md @@ -113,7 +113,7 @@ Enables value inspection from table cell. The raw value is presented in a modal ## Column filter -You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "./filter-table-columns.md" >}}). +You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "filter-table-columns.md" >}}). ## Pagination diff --git a/docs/sources/visualizations/time-series/_index.md b/docs/sources/visualizations/time-series/_index.md index b4d5b0428a2..17e19819a1e 100644 --- a/docs/sources/visualizations/time-series/_index.md +++ b/docs/sources/visualizations/time-series/_index.md @@ -37,11 +37,11 @@ Choose which of the [standard calculations]({{< relref "../../panels/calculation Use these options to choose how to display your time series data. -- [Graph time series as lines]({{< relref "./graph-time-series-as-lines.md" >}}) -- [Graph time series as bars]({{< relref "./graph-time-series-as-bars.md" >}}) -- [Graph time series as points]({{< relref "./graph-time-series-as-points.md" >}}) -- [Graph stacked time series]({{< relref "./graph-time-series-stacking.md" >}}) -- [Graph and color schemes]({{< relref "./graph-color-scheme.md" >}}) +- [Graph time series as lines]({{< relref "graph-time-series-as-lines.md" >}}) +- [Graph time series as bars]({{< relref "graph-time-series-as-bars.md" >}}) +- [Graph time series as points]({{< relref "graph-time-series-as-points.md" >}}) +- [Graph stacked time series]({{< relref "graph-time-series-stacking.md" >}}) +- [Graph and color schemes]({{< relref "graph-color-scheme.md" >}}) ### Transform diff --git a/docs/sources/visualizations/time-series/graph-time-series-as-bars.md b/docs/sources/visualizations/time-series/graph-time-series-as-bars.md index 41116db1218..f863dcba80f 100644 --- a/docs/sources/visualizations/time-series/graph-time-series-as-bars.md +++ b/docs/sources/visualizations/time-series/graph-time-series-as-bars.md @@ -109,7 +109,7 @@ Gradient color is generated based on the hue of the line color. #### Scheme -In this mode the whole bar will use a color gradient defined by your Color scheme. For more information, refer to [Apply color to series and fields]({{< relref "../../panels/working-with-panels/apply-color-to-series.md" >}}). There is more information on this option in [Graph and color scheme]({{< relref "./graph-color-scheme.md" >}}). +In this mode the whole bar will use a color gradient defined by your Color scheme. For more information, refer to [Apply color to series and fields]({{< relref "../../panels/working-with-panels/apply-color-to-series.md" >}}). There is more information on this option in [Graph and color scheme]({{< relref "graph-color-scheme.md" >}}). {{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_bars.png" max-width="1200px" caption="Gradient color scheme mode" >}} diff --git a/docs/sources/visualizations/time-series/graph-time-series-as-lines.md b/docs/sources/visualizations/time-series/graph-time-series-as-lines.md index 3de1cda08c1..5a3c0856356 100644 --- a/docs/sources/visualizations/time-series/graph-time-series-as-lines.md +++ b/docs/sources/visualizations/time-series/graph-time-series-as-lines.md @@ -114,7 +114,7 @@ Gradient color is generated based on the hue of the line color. #### Scheme -In this mode the whole line will use a color gradient defined by your Color scheme. For more information, refer to [Apply color to series and fields]({{< relref "../../panels/working-with-panels/apply-color-to-series.md" >}}). There is more information on this option in [Graph and color scheme]({{< relref "./graph-color-scheme.md" >}}). +In this mode the whole line will use a color gradient defined by your Color scheme. For more information, refer to [Apply color to series and fields]({{< relref "../../panels/working-with-panels/apply-color-to-series.md" >}}). There is more information on this option in [Graph and color scheme]({{< relref "graph-color-scheme.md" >}}). {{< figure src="/static/img/docs/time-series-panel/gradient_mode_scheme_line.png" max-width="1200px" caption="Gradient mode scheme" >}} diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index 0cbebc88aaf..a2bbba5d6cc 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -9,7 +9,7 @@ weight: 1 # What's new Grafana Grafana is changing all the time. For release highlights checkout links below, if you want a complete list of every change, as well -as info on deprecations, breaking changes and plugin development read the [release notes]({{< relref "../release-notes" >}}). +as info on deprecations, breaking changes and plugin development read the [release notes]({{< relref "../release-notes/" >}}). ## Grafana 9 @@ -17,37 +17,37 @@ as info on deprecations, breaking changes and plugin development read the [relea ## Grafana 8 -- [What's new in 8.5]({{< relref "whats-new-in-v8-5" >}}) -- [What's new in 8.4]({{< relref "whats-new-in-v8-4" >}}) -- [What's new in 8.3]({{< relref "whats-new-in-v8-3" >}}) -- [What's new in 8.2]({{< relref "whats-new-in-v8-2" >}}) -- [What's new in 8.1]({{< relref "whats-new-in-v8-1" >}}) -- [What's new in 8.0]({{< relref "whats-new-in-v8-0" >}}) +- [What's new in 8.5]({{< relref "whats-new-in-v8-5/" >}}) +- [What's new in 8.4]({{< relref "whats-new-in-v8-4/" >}}) +- [What's new in 8.3]({{< relref "whats-new-in-v8-3/" >}}) +- [What's new in 8.2]({{< relref "whats-new-in-v8-2/" >}}) +- [What's new in 8.1]({{< relref "whats-new-in-v8-1/" >}}) +- [What's new in 8.0]({{< relref "whats-new-in-v8-0/" >}}) ## Grafana 7 -- [What's new in 7.5]({{< relref "whats-new-in-v7-5" >}}) -- [What's new in 7.4]({{< relref "whats-new-in-v7-4" >}}) -- [What's new in 7.3]({{< relref "whats-new-in-v7-3" >}}) -- [What's new in 7.2]({{< relref "whats-new-in-v7-2" >}}) -- [What's new in 7.1]({{< relref "whats-new-in-v7-1" >}}) -- [What's new in 7.0]({{< relref "whats-new-in-v7-0" >}}) +- [What's new in 7.5]({{< relref "whats-new-in-v7-5/" >}}) +- [What's new in 7.4]({{< relref "whats-new-in-v7-4/" >}}) +- [What's new in 7.3]({{< relref "whats-new-in-v7-3/" >}}) +- [What's new in 7.2]({{< relref "whats-new-in-v7-2/" >}}) +- [What's new in 7.1]({{< relref "whats-new-in-v7-1/" >}}) +- [What's new in 7.0]({{< relref "whats-new-in-v7-0/" >}}) ## Grafana 6 -- [What's new in 6.7]({{< relref "whats-new-in-v6-7" >}}) -- [What's new in 6.6]({{< relref "whats-new-in-v6-6" >}}) -- [What's new in 6.5]({{< relref "whats-new-in-v6-5" >}}) -- [What's new in 6.4]({{< relref "whats-new-in-v6-4" >}}) -- [What's new in 6.3]({{< relref "whats-new-in-v6-3" >}}) -- [What's new in 6.2]({{< relref "whats-new-in-v6-2" >}}) -- [What's new in 6.1]({{< relref "whats-new-in-v6-1" >}}) -- [What's new in 6.0]({{< relref "whats-new-in-v6-0" >}}) +- [What's new in 6.7]({{< relref "whats-new-in-v6-7/" >}}) +- [What's new in 6.6]({{< relref "whats-new-in-v6-6/" >}}) +- [What's new in 6.5]({{< relref "whats-new-in-v6-5/" >}}) +- [What's new in 6.4]({{< relref "whats-new-in-v6-4/" >}}) +- [What's new in 6.3]({{< relref "whats-new-in-v6-3/" >}}) +- [What's new in 6.2]({{< relref "whats-new-in-v6-2/" >}}) +- [What's new in 6.1]({{< relref "whats-new-in-v6-1/" >}}) +- [What's new in 6.0]({{< relref "whats-new-in-v6-0/" >}}) ## Grafana 5 -- [What's new in 5.4]({{< relref "whats-new-in-v5-4" >}}) -- [What's new in 5.3]({{< relref "whats-new-in-v5-3" >}}) -- [What's new in 5.2]({{< relref "whats-new-in-v5-2" >}}) -- [What's new in 5.1]({{< relref "whats-new-in-v5-1" >}}) -- [What's new in 5.0]({{< relref "whats-new-in-v5-0" >}}) +- [What's new in 5.4]({{< relref "whats-new-in-v5-4/" >}}) +- [What's new in 5.3]({{< relref "whats-new-in-v5-3/" >}}) +- [What's new in 5.2]({{< relref "whats-new-in-v5-2/" >}}) +- [What's new in 5.1]({{< relref "whats-new-in-v5-1/" >}}) +- [What's new in 5.0]({{< relref "whats-new-in-v5-0/" >}}) diff --git a/docs/sources/whatsnew/whats-new-in-v5-4.md b/docs/sources/whatsnew/whats-new-in-v5-4.md index 6d1892dd4af..93931c266a2 100644 --- a/docs/sources/whatsnew/whats-new-in-v5-4.md +++ b/docs/sources/whatsnew/whats-new-in-v5-4.md @@ -52,9 +52,9 @@ Stackdriver is the first data source which has support for a custom templating q create their very own templating query editor. Additionally, if Grafana is running on a Google Compute Engine (GCE) virtual machine, it is now possible for Grafana to automatically retrieve default credentials from the metadata server. -This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. [Learn more]({{< relref "../datasources/google-cloud-monitoring/_index.md/#using-gce-default-service-account" >}}). +This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. [Learn more]({{< relref "../datasources/google-cloud-monitoring/_index.md#using-gce-default-service-account" >}}). -Please read [Using Google Stackdriver in Grafana]({{< relref "../datasources/google-cloud-monitoring/_index.md/" >}}) for more detailed information on how to get started and use it. +Please read [Using Google Stackdriver in Grafana]({{< relref "../datasources/google-cloud-monitoring/_index.md" >}}) for more detailed information on how to get started and use it.
diff --git a/docs/sources/whatsnew/whats-new-in-v6-0.md b/docs/sources/whatsnew/whats-new-in-v6-0.md index c60c326f868..c19265cb086 100644 --- a/docs/sources/whatsnew/whats-new-in-v6-0.md +++ b/docs/sources/whatsnew/whats-new-in-v6-0.md @@ -121,7 +121,7 @@ will be shared soon. Built-in support for [Google Stackdriver](https://cloud.google.com/stackdriver/) is officially released in Grafana 6.0. Beta support was added in Grafana 5.3 and we have added lots of improvements since then. -To get started read the guide: [Using Google Stackdriver in Grafana]({{< relref "../datasources/google-cloud-monitoring/_index.md/" >}}). +To get started read the guide: [Using Google Stackdriver in Grafana]({{< relref "../datasources/google-cloud-monitoring/_index.md" >}}). ## Azure Monitor data source diff --git a/docs/sources/whatsnew/whats-new-in-v6-5.md b/docs/sources/whatsnew/whats-new-in-v6-5.md index 6240925bd41..36ff103297f 100644 --- a/docs/sources/whatsnew/whats-new-in-v6-5.md +++ b/docs/sources/whatsnew/whats-new-in-v6-5.md @@ -179,7 +179,7 @@ In the Explore split view, you can now link the two timepickers so that if you c ### Alerting support for Azure Application Insights -The [Azure Monitor]({{< relref "../datasources/azuremonitor/" >}}) data source supports multiple services in the Azure cloud. Before Grafana v6.5, only the Azure Monitor service had support for [Grafana Alerting]({{< relref "../alerting" >}}). In Grafana 6.5, alerting support has been implemented for the [Application Insights service]({{< relref "../datasources/azuremonitor/#querying-the-application-insights-service" >}}). +The [Azure Monitor]({{< relref "../datasources/azuremonitor/" >}}) data source supports multiple services in the Azure cloud. Before Grafana v6.5, only the Azure Monitor service had support for [Grafana Alerting]({{< relref "../alerting/" >}}). In Grafana 6.5, alerting support has been implemented for the [Application Insights service]({{< relref "../datasources/azuremonitor/#querying-the-application-insights-service" >}}). ### Allow saving of provisioned dashboards from UI diff --git a/docs/sources/whatsnew/whats-new-in-v6-7.md b/docs/sources/whatsnew/whats-new-in-v6-7.md index 954e38fbbec..39efc5c9a1e 100644 --- a/docs/sources/whatsnew/whats-new-in-v6-7.md +++ b/docs/sources/whatsnew/whats-new-in-v6-7.md @@ -42,7 +42,7 @@ General features are included in all Grafana editions. Query history is a new feature that lets you view and interact with the queries that you have previously run in Explore. You can add queries to the Explore query editor, write comments, create and share URL links, star your favorite queries, and much more. Starred queries are displayed in Starred tab, so it is easier to reuse queries that you run often without typing them from scratch. -Learn more about query history in [Explore]({{< relref "../explore" >}}). +Learn more about query history in [Explore]({{< relref "../explore/" >}}). {{< figure src="/static/img/docs/v67/rich-history.gif" max-width="1024px" caption="Query history" >}} @@ -54,7 +54,7 @@ Grafana v6.7 comes with a new OAuth integration for Microsoft Azure Active Direc Allowing a low dashboard refresh interval can cause severe load on data sources and Grafana. Grafana v6.7 allows you to restrict the dashboard refresh interval so it cannot be set lower than a given interval. This provides a way for administrators to control dashboard refresh behavior on a global level. -Refer to min_refresh_interval in [Configuration]({{< relref "../administration/configuration#min-refresh-interval" >}}) for more information and how to enable this feature. +Refer to min_refresh_interval in [Configuration]({{< relref "../administration/configuration/#min-refresh-interval" >}}) for more information and how to enable this feature. ### Stackdriver project selector diff --git a/docs/sources/whatsnew/whats-new-in-v7-0.md b/docs/sources/whatsnew/whats-new-in-v7-0.md index e4165bbf46f..10248a62208 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-0.md +++ b/docs/sources/whatsnew/whats-new-in-v7-0.md @@ -182,11 +182,11 @@ Read more about [Image Rendering]({{< relref "../image-rendering/" >}}) in the d The Query history feature lets you view and interact with the queries that you have previously run in Explore. You can add queries to the Explore query editor, write comments, create and share URL links, star your favorite queries, and much more. Starred queries are displayed in the Starred tab, so it is easier to reuse queries that you run often without typing them from scratch. -It was released as a beta feature in Grafana 6.7. The feedback has been really positive and it is now out of beta for the 7.0 release. Learn more about query history in [Explore]({{< relref "../explore" >}}). +It was released as a beta feature in Grafana 6.7. The feedback has been really positive and it is now out of beta for the 7.0 release. Learn more about query history in [Explore]({{< relref "../explore/" >}}). ## Stackdriver data source supports Service Monitoring -[Service monitoring](https://cloud.google.com/service-monitoring) in Google Cloud Platform (GCP) enables you to monitor based on Service Level Objectives (SLOs) for your GCP services. The new SLO query builder in the Stackdriver data source allows you to display SLO data in Grafana. Read more about it in the [Stackdriver data source documentation]({{< relref "../datasources/google-cloud-monitoring/_index.md/#slo-service-level-objective-queries" >}}). +[Service monitoring](https://cloud.google.com/service-monitoring) in Google Cloud Platform (GCP) enables you to monitor based on Service Level Objectives (SLOs) for your GCP services. The new SLO query builder in the Stackdriver data source allows you to display SLO data in Grafana. Read more about it in the [Stackdriver data source documentation]({{< relref "../datasources/google-cloud-monitoring/_index.md#slo-service-level-objective-queries" >}}). ## Time zone support @@ -221,7 +221,7 @@ This release includes a series of features that build on our new usage analytics ### SAML Role and Team Sync -SAML support in Grafana Enterprise is improved by adding Role and Team Sync. Read more about how to use these features in the [SAML team sync documentation]({{< relref "../enterprise/saml/configure-saml.md#configure-team-sync" >}}). +SAML support in Grafana Enterprise is improved by adding Role and Team Sync. Read more about how to use these features in the [SAML team sync documentation]({{< relref "../enterprise/configure-saml.md#configure-team-sync" >}}). ### Okta OAuth Team Sync diff --git a/docs/sources/whatsnew/whats-new-in-v7-3.md b/docs/sources/whatsnew/whats-new-in-v7-3.md index b435b83b3f8..7f9813dbfad 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-3.md +++ b/docs/sources/whatsnew/whats-new-in-v7-3.md @@ -82,7 +82,7 @@ The updated Google Cloud monitoring data source is shipped with pre-configured d To import the pre-configured dashboards, go to the configuration page of your Google Cloud Monitoring data source and click on the `Dashboards` tab. Click `Import` for the dashboard you would like to use. To customize the dashboard, we recommend to save the dashboard under a different name, because otherwise the dashboard will be overwritten when a new version of the dashboard is released. -For more details, see the [Google Cloud Monitoring docs]({{< relref "../datasources/google-cloud-monitoring/_index.md/#out-of-the-box-dashboards" >}}) +For more details, see the [Google Cloud Monitoring docs]({{< relref "../datasources/google-cloud-monitoring/_index.md#out-of-the-box-dashboards" >}}) ## Shorten URL for dashboards and Explore @@ -142,11 +142,11 @@ Insights: ### SAML single logout -SAML’s single logout (SLO) capability allows users to log out from all applications associated with the current identity provider (IdP) session established via SAML SSO. For more information, refer to the [docs]({{< relref "../enterprise/saml/#single-logout" >}}). +SAML’s single logout (SLO) capability allows users to log out from all applications associated with the current identity provider (IdP) session established via SAML SSO. For more information, refer to the [docs]({{< relref "../enterprise/configure-saml/#single-logout" >}}). ### SAML IdP-initiated single sign on -IdP-initiated single sign on (SSO) allows the user to log in directly from the SAML identity provider (IdP). It is disabled by default for security reasons. For more information, refer to the [docs]({{< relref "../enterprise/saml/#idp-initiated-single-sign-on-sso" >}}). +IdP-initiated single sign on (SSO) allows the user to log in directly from the SAML identity provider (IdP). It is disabled by default for security reasons. For more information, refer to the [docs]({{< relref "../enterprise/configure-saml/#idp-initiated-single-sign-on-sso" >}}). ## Upgrading diff --git a/docs/sources/whatsnew/whats-new-in-v7-4.md b/docs/sources/whatsnew/whats-new-in-v7-4.md index 735dd5f2a10..dc5a2787f15 100644 --- a/docs/sources/whatsnew/whats-new-in-v7-4.md +++ b/docs/sources/whatsnew/whats-new-in-v7-4.md @@ -40,7 +40,7 @@ The following documentation topics were added for this feature: - [Time series panel]({{< relref "../visualizations/time-series/_index.md" >}}) - [Graph time series as lines]({{< relref "../visualizations/time-series/graph-time-series-as-lines.md" >}}) - [Graph time series as bars]({{< relref "../visualizations/time-series/graph-time-series-as-bars.md" >}}) -- [Graph time series as points]({{< relref "../visualizations/time-series/graph-time-series-as-points" >}}) +- [Graph time series as points]({{< relref "../visualizations/time-series/graph-time-series-as-points/" >}}) - [Change axis display]({{< relref "../visualizations/time-series/change-axis-display.md" >}}) ### Node graph panel visualization (Beta) @@ -171,7 +171,7 @@ Google Cloud Monitoring data source ships with pre-configured dashboards for som If you want to customize a dashboard, we recommend that you save it under a different name. Otherwise the dashboard will be overwritten when a new version of the dashboard is released. -For more information, refer to the [Google Cloud Monitoring docs]({{< relref "../datasources/google-cloud-monitoring/_index.md/#out-of-the-box-dashboards" >}}). +For more information, refer to the [Google Cloud Monitoring docs]({{< relref "../datasources/google-cloud-monitoring/_index.md#out-of-the-box-dashboards" >}}). ### Query Editor Help @@ -209,7 +209,7 @@ For more information, refer to [Export logs of usage insights]({{< relref "../en ### New audit log events -New log out events are logged based on when a token expires or is revoked, as well as [SAML Single Logout]({{< relref "../enterprise/saml/configure-saml.md#single-logout" >}}). A `tokenId` field was added to all audit logs to help understand which session was logged out of. +New log out events are logged based on when a token expires or is revoked, as well as [SAML Single Logout]({{< relref "../enterprise/configure-saml.md#single-logout" >}}). A `tokenId` field was added to all audit logs to help understand which session was logged out of. Also, a counter for audit log writing actions with status (success / failure) and logger (loki / file / console) labels was added. diff --git a/docs/sources/whatsnew/whats-new-in-v8-0.md b/docs/sources/whatsnew/whats-new-in-v8-0.md index 4d10b2d8df3..d5db7cebfc4 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-0.md +++ b/docs/sources/whatsnew/whats-new-in-v8-0.md @@ -45,7 +45,7 @@ In addition to data source integration, events can be sent to dashboards by post These metrics will be broadcast to all dashboards connected to that stream endpoint. -For more information about real-time streaming, refer to [Grafana Live documentation]({{< relref "../live/set-up-grafana-live" >}}). +For more information about real-time streaming, refer to [Grafana Live documentation]({{< relref "../live/set-up-grafana-live/" >}}). ### Prometheus metrics browser diff --git a/docs/sources/whatsnew/whats-new-in-v8-1.md b/docs/sources/whatsnew/whats-new-in-v8-1.md index 167a3a43806..92b4c48a4d8 100644 --- a/docs/sources/whatsnew/whats-new-in-v8-1.md +++ b/docs/sources/whatsnew/whats-new-in-v8-1.md @@ -136,7 +136,7 @@ We’d love as much feedback as possible about this change, because we are consi ### High availability setup support for Grafana Live -We have added an experimental HA setup support for Grafana Live with Redis. This resolves the limitation when clients were connected to different Grafana instances and those instances had no shared state. For additional information, refer to [Configure Grafana Live HA setup]({{< relref "../live//set-up-grafana-live/#configure-grafana-live-ha-setup" >}}). +We have added an experimental HA setup support for Grafana Live with Redis. This resolves the limitation when clients were connected to different Grafana instances and those instances had no shared state. For additional information, refer to [Configure Grafana Live HA setup]({{< relref "../live/set-up-grafana-live/#configure-grafana-live-ha-setup" >}}). ## Enterprise features From 9cb10d735f48e4b424108636c984a159968146c1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 10:46:21 -0400 Subject: [PATCH 03/95] ColorPicker: Remove deprecated onColorChange prop (#49923) (#49927) (cherry picked from commit 8d59ba2be99ce18f4d069dc5f9ef7b1635d2ef38) Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> --- .../src/components/ColorPicker/ColorPicker.tsx | 13 +++---------- .../components/ColorPicker/ColorPickerPopover.tsx | 13 +++---------- .../warnAboutColorPickerPropsDeprecation.ts | 10 ---------- 3 files changed, 6 insertions(+), 30 deletions(-) delete mode 100644 packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 453c7d430c6..e88b8f0240f 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -8,7 +8,7 @@ import { closePopover } from '../../utils/closePopover'; import { Popover } from '../Tooltip/Popover'; import { PopoverController } from '../Tooltip/PopoverController'; -import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover'; +import { ColorPickerPopover, ColorPickerProps } from './ColorPickerPopover'; import { ColorSwatch } from './ColorSwatch'; import { SeriesColorPickerPopover } from './SeriesColorPickerPopover'; @@ -34,19 +34,12 @@ export const colorPickerFactory = ( static displayName = displayName; pickerTriggerRef = createRef(); - onColorChange = (color: string) => { - const { onColorChange, onChange } = this.props; - const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler; - - return changeHandler(color); - }; - render() { - const { theme, children } = this.props; + const { theme, children, onChange } = this.props; const styles = getStyles(theme); const popoverElement = React.createElement(popover, { ...{ ...this.props, children: null }, - onChange: this.onColorChange, + onChange, }); return ( diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx index 3a77661ac44..992eda49792 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx @@ -10,7 +10,6 @@ import { PopoverContentProps } from '../Tooltip'; import { NamedColorsPalette } from './NamedColorsPalette'; import SpectrumPalette from './SpectrumPalette'; -import { warnAboutColorPickerPropsDeprecation } from './warnAboutColorPickerPropsDeprecation'; export type ColorPickerChangeHandler = (color: string) => void; @@ -18,10 +17,6 @@ export interface ColorPickerProps extends Themeable2 { color: string; onChange: ColorPickerChangeHandler; - /** - * @deprecated Use onChange instead - */ - onColorChange?: ColorPickerChangeHandler; enableNamedColors?: boolean; } @@ -48,7 +43,6 @@ class UnThemedColorPickerPopover extends Reac this.state = { activePicker: 'palette', }; - warnAboutColorPickerPropsDeprecation('ColorPickerPopover', props); } getTabClassName = (tabName: PickerType | keyof T) => { @@ -57,12 +51,11 @@ class UnThemedColorPickerPopover extends Reac }; handleChange = (color: any) => { - const { onColorChange, onChange, enableNamedColors, theme } = this.props; - const changeHandler = onColorChange || onChange; + const { onChange, enableNamedColors, theme } = this.props; if (enableNamedColors) { - return changeHandler(color); + return onChange(color); } - changeHandler(colorManipulator.asHexString(theme.visualization.getColorByName(color))); + onChange(colorManipulator.asHexString(theme.visualization.getColorByName(color))); }; onTabChange = (tab: PickerType | keyof T) => { diff --git a/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts b/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts deleted file mode 100644 index 9f58ba70285..00000000000 --- a/packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { deprecationWarning } from '@grafana/data'; - -import { ColorPickerProps } from './ColorPickerPopover'; - -export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => { - const { onColorChange } = props; - if (onColorChange) { - deprecationWarning(componentName, 'onColorChange', 'onChange'); - } -}; From 7de6880ba49727b66cc6f8e970c45e2f0759448d Mon Sep 17 00:00:00 2001 From: Jack Baldry Date: Tue, 31 May 2022 16:02:35 +0100 Subject: [PATCH 04/95] Backport 49786 and 49207 to v9.0.x (#49932) * Docs: Identify which Grafana editions are relevant to each Enterprise doc (#49207) * Add section to Ent docs index re: Cloud features * Add and update notes identifying Enterprise and Cloud features * Address feedback (cherry picked from commit 3ecee0663085cf4dc80e53f80a5ee8545cbf50e0) * Use ref links for external content (#49786) Signed-off-by: Jack Baldry (cherry picked from commit e82784bff0a911464fb4039e06f5997a7efa4f33) Co-authored-by: Garrett Guillotte <100453168+gguillotte-grafana@users.noreply.github.com> --- docs/sources/enterprise/_index.md | 22 +++++++++------- docs/sources/enterprise/auditing.md | 6 ++--- docs/sources/enterprise/configure-saml.md | 14 +++++----- .../enterprise/datasource_permissions.md | 2 +- docs/sources/enterprise/enhanced_ldap.md | 6 ++--- .../enterprise/enterprise-configuration.md | 10 +++---- docs/sources/enterprise/export-pdf.md | 8 +++--- docs/sources/enterprise/query-caching.md | 4 ++- docs/sources/enterprise/recorded-queries.md | 2 ++ docs/sources/enterprise/reporting.md | 26 ++++++++++--------- docs/sources/enterprise/request-security.md | 11 +++----- docs/sources/enterprise/saml/_index.md | 2 +- docs/sources/enterprise/saml/about-saml.md | 2 +- docs/sources/enterprise/settings-updates.md | 6 ++--- docs/sources/enterprise/team-sync.md | 2 +- .../enterprise/usage-insights/_index.md | 2 ++ .../dashboard-datasource-insights.md | 4 +-- .../enterprise/usage-insights/export-logs.md | 2 +- .../usage-insights/improved-search.md | 2 +- .../usage-insights/presence-indicator.md | 2 +- docs/sources/enterprise/vault.md | 5 ++-- docs/sources/enterprise/white-labeling.md | 2 +- 22 files changed, 74 insertions(+), 68 deletions(-) diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index 56766be88d9..d3dd95c64cf 100644 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -20,9 +20,13 @@ weight: 150 Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. -Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive datasource plugins]({{< relref "#enterprise-plugins">}}) and [additional features]({{< relref "#enterprise-features">}}). On top of that you get 24x7x365 support and training from the core Grafana team. +Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive datasource plugins]({{< relref "#enterprise-plugins">}}) and [additional features]({{< relref "#enterprise-features">}}). You also get 24x7x365 support and training from the core Grafana team. -To learn more about Grafana Enterprise, refer to [our product page.](https://grafana.com/enterprise) +To learn more about Grafana Enterprise, refer to [our product page](https://grafana.com/enterprise). + +## Enterprise features in Grafana Cloud + +Many Grafana Enterprise features are also available in [Grafana Cloud]({{< ref "/docs/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< ref "/docs/grafana-cloud/reference/enterprise-features" >}}). ## Authentication @@ -44,23 +48,23 @@ Supported auth providers: ### Enhanced LDAP integration -With Grafana Enterprise [enhanced LDAP]({{< relref "enhanced_ldap.md" >}}), you can set up active LDAP synchronization. +With [enhanced LDAP integration]({{< relref "enhanced_ldap.md" >}}), you can set up active LDAP synchronization. ### SAML authentication -[SAML authentication]({{< relref "./saml" >}}) enables your Grafana Enterprise users to authenticate with SAML. +[SAML authentication]({{< relref "./saml" >}}) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML). ## Enterprise features -With Grafana Enterprise, you get access to the following features: +Grafana Enterprise adds the following features: -- [Role-based access control]({{< relref "./access-control/_index.md" >}}) to control access with role-based permissions. +- [Role-based access control]({{< relref "./access-control/" >}}) to control access with role-based permissions. - [Data source permissions]({{< relref "datasource_permissions.md" >}}) to restrict query access to specific teams and users. - [Data source query caching]({{< relref "query-caching.md" >}}) to temporarily store query results in Grafana to reduce data source load and rate limiting. - [Reporting]({{< relref "reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose. - [Export dashboard as PDF]({{< relref "export-pdf.md" >}}) - [White labeling]({{< relref "white-labeling.md" >}}) to customize Grafana from the brand and logo to the footer links. -- [Usage insights]({{< relref "usage-insights/_index.md" >}}) to understand how your Grafana instance is used. +- [Usage insights]({{< relref "./usage-insights/" >}}) to understand how your Grafana instance is used. - [Vault integration]({{< relref "vault.md" >}}) to manage your configuration or provisioning secrets with Vault. - [Auditing]({{< relref "auditing.md" >}}) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements. - [Request security]({{< relref "request-security.md" >}}) makes it possible to restrict outgoing requests from the Grafana server. @@ -68,7 +72,7 @@ With Grafana Enterprise, you get access to the following features: ## Enterprise data sources -With a Grafana Enterprise license, you get access to premium data sources, including: +With a Grafana Enterprise license, you also get access to premium data sources, including: - [AppDynamics](https://grafana.com/grafana/plugins/dlopes7-appdynamics-datasource) - [Azure Devops](https://grafana.com/grafana/plugins/grafana-azuredevops-datasource) @@ -90,4 +94,4 @@ With a Grafana Enterprise license, you get access to premium data sources, inclu ## Try Grafana Enterprise -To purchase or obtain a trial license contact the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). +To purchase or obtain a trial license, contact the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). diff --git a/docs/sources/enterprise/auditing.md b/docs/sources/enterprise/auditing.md index a6df6e660ee..a727158f2b3 100644 --- a/docs/sources/enterprise/auditing.md +++ b/docs/sources/enterprise/auditing.md @@ -13,10 +13,10 @@ weight: 1100 # Auditing -> **Note:** Only available in Grafana Enterprise v7.3+. - Auditing allows you to track important changes to your Grafana instance. By default, audit logs are logged to file but the auditing feature also supports sending logs directly to Loki. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.3 and later, and [Grafana Cloud Advanced]({{< ref "/docs/grafana-cloud" >}}). + ## Audit logs Audit logs are JSON objects representing user actions like: @@ -336,7 +336,7 @@ max_file_size_mb = 256 Audit logs are sent to a [Loki](/oss/loki/) service, through HTTP or gRPC. -> The HTTP option for the Loki exporter is only available in Grafana Enterprise v7.4+. +> **Note:** The HTTP option for the Loki exporter is available only in Grafana Enterprise version 7.4 and later. ```ini [auditing.logs.loki] diff --git a/docs/sources/enterprise/configure-saml.md b/docs/sources/enterprise/configure-saml.md index bcef82584ec..1abed00ed88 100644 --- a/docs/sources/enterprise/configure-saml.md +++ b/docs/sources/enterprise/configure-saml.md @@ -190,7 +190,7 @@ The table below describes all SAML configuration options. Continue reading below ### Signature algorithm -> Only available in Grafana v7.3+ +> **Note:** Available in Grafana version 7.3 and later. The SAML standard recommends using a digital signature for some types of messages, like authentication or logout requests. If the `signature_algorithm` option is configured, Grafana will put a digital signature into SAML requests. Supported signature types are `rsa-sha1`, `rsa-sha256`, `rsa-sha512`. This option should match your IdP configuration, otherwise, signature validation will fail. Grafana uses key and certificate configured with `private_key` and `certificate` options for signing SAML requests. @@ -227,7 +227,7 @@ The integration provides two key endpoints as part of Grafana: ### IdP-initiated Single Sign-On (SSO) -> Only available in Grafana v7.3+ +> **Note:** Available in Grafana version 7.3 and later. By default, Grafana allows only service provider (SP) initiated logins (when the user logs in with SAML via Grafana’s login page). If you want users to log in into Grafana directly from your identity provider (IdP), set the `allow_idp_initiated` configuration option to `true` and configure `relay_state` with the same value specified in the IdP configuration. @@ -235,7 +235,7 @@ IdP-initiated SSO has some security risks, so make sure you understand the risks ### Single logout -> Only available in Grafana v7.3+ +> **Note:** Available in Grafana version 7.3 and later. SAML's single logout feature allows users to log out from all applications associated with the current IdP session established via SAML SSO. If the `single_logout` option is set to `true` and a user logs out, Grafana requests IdP to end the user session which in turn triggers logout from all other applications the user is logged into using the same IdP session (applications should support single logout). Conversely, if another application connected to the same IdP logs out using single logout, Grafana receives a logout request from IdP and ends the user session. @@ -269,7 +269,7 @@ By default, new Grafana users using SAML authentication will have an account cre ### Configure team sync -> Team sync support for SAML only available in Grafana v7.0+ +> **Note:** Team sync support for SAML is available in Grafana version 7.0 and later. To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref "enterprise-configuration.md#assertion-attribute-groups" >}}) to the attribute name where you store user groups. Then Grafana will use attribute values extracted from SAML assertion to add user into the groups with the same name configured on the External group sync tab. @@ -277,7 +277,7 @@ To use SAML Team sync, set [`assertion_attribute_groups`]({{< relref "enterprise ### Configure role sync -> Only available in Grafana v7.0+ +> **Note:** Available in Grafana version 7.0 and later. Role sync allows you to map user roles from an identity provider to Grafana. To enable role sync, configure role attribute and possible values for the Editor, Admin, and Grafana Admin roles. For more information about user roles, refer to [About users and permissions]({{< relref "../administration/manage-users-and-permissions/about-users-and-permissions.md" >}}). @@ -304,7 +304,7 @@ role_values_grafana_admin = superadmin ### Configure organization mapping -> Only available in Grafana v7.0+ +> **Note:** Available in Grafana version 7.0 and later. Organization mapping allows you to assign users to particular organization in Grafana depending on attribute value obtained from identity provider. @@ -330,7 +330,7 @@ You can use `*` as an Organization if you want all your users to be in some orga ### Configure allowed organizations -> Only available in Grafana v7.0+ +> **Note:** Available in Grafana version 7.0 and later. With the [`allowed_organizations`]({{< relref "enterprise-configuration.md#allowed-organizations" >}}) option you can specify a list of organizations where the user must be a member of at least one of them to be able to log in to Grafana. diff --git a/docs/sources/enterprise/datasource_permissions.md b/docs/sources/enterprise/datasource_permissions.md index d79ff9fc49c..a4967eaa402 100644 --- a/docs/sources/enterprise/datasource_permissions.md +++ b/docs/sources/enterprise/datasource_permissions.md @@ -20,7 +20,7 @@ weight: 500 Data source permissions allow you to restrict access for users to query a data source. For each data source there is a permission page that allows you to enable permissions and restrict query permissions to specific **Users** and **Teams**. -> Only available in Grafana Enterprise. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). ## Enable data source permissions diff --git a/docs/sources/enterprise/enhanced_ldap.md b/docs/sources/enterprise/enhanced_ldap.md index 8da2ba4a99b..d2b86fd4a62 100644 --- a/docs/sources/enterprise/enhanced_ldap.md +++ b/docs/sources/enterprise/enhanced_ldap.md @@ -17,9 +17,9 @@ weight: 600 The enhanced LDAP integration adds additional functionality on top of the [LDAP integration]({{< relref "../auth/ldap.md" >}}) available in the open source edition of Grafana. -> Enhanced LDAP integration is only available in [Grafana Cloud Advanced](https://grafana.com/docs/grafana-cloud/) and in [Grafana Enterprise]({{< relref "../enterprise" >}}). +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< ref "/docs/grafana-cloud" >}}). -> Refer to [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to understand how you can control access with role-based permissions. +> To control user access with role-based permissions, refer to [role-based access control]({{< relref "./access-control" >}}). ## LDAP group synchronization for teams @@ -40,7 +40,7 @@ a user as member of a team, and it will not be removed when the user signs in. T In the open source version of Grafana, user data from LDAP is synchronized only during the login process when authenticating using LDAP. -With active LDAP synchronization, available in Grafana Enterprise v6.3+, you can configure Grafana to actively sync users with LDAP servers in the background. Only users that have logged into Grafana at least once are synchronized. +With active LDAP synchronization, available in Grafana Enterprise version 6.3 and later, you can configure Grafana to actively sync users with LDAP servers in the background. Only users that have logged into Grafana at least once are synchronized. Users with updated role and team membership will need to refresh the page to get access to the new features. diff --git a/docs/sources/enterprise/enterprise-configuration.md b/docs/sources/enterprise/enterprise-configuration.md index f3c0bd9c744..2621744f24f 100644 --- a/docs/sources/enterprise/enterprise-configuration.md +++ b/docs/sources/enterprise/enterprise-configuration.md @@ -24,7 +24,7 @@ Defaults to `/license.jwt`. ### license_text -> **Note:** Available in Grafana Enterprise v7.4+. +> **Note:** Available in Grafana Enterprise version 7.4 and later. When set to the text representation (i.e. content of the license file) of the license, Grafana will evaluate and apply the given license to @@ -32,7 +32,7 @@ the instance. ### auto_refresh_license -> **Note:** Available in Grafana Enterprise v7.4+. +> **Note:** Available in Grafana Enterprise version 7.4 and later. When enabled, Grafana will send the license and usage statistics to the license issuer. If the license has been updated on the issuer's @@ -42,7 +42,7 @@ automatically. Defaults to `true`. ### license_validation_type -> **Note:** Available in Grafana Enterprise v8.3+. +> **Note:** Available in Grafana Enterprise version 8.3 and later. When set to `aws`, Grafana will validate its license status with Amazon Web Services (AWS) instead of with Grafana Labs. Only use this setting if you purchased an Enterprise license from AWS Marketplace. Defaults to empty, which means that by default Grafana Enterprise will validate using a license issued by Grafana Labs. For details about licenses issued by AWS, refer to [Activate a Grafana Enterprise license purchased through AWS Marketplace]({{< relref "../enterprise/license/activate-aws-marketplace-license/" >}}). @@ -327,7 +327,7 @@ New duration for renewed tokens. Vault may be configured to ignore this value an ## [security.egress] -> **Note:** Available in Grafana Enterprise v7.4 and later versions. +> **Note:** Available in Grafana Enterprise version 7.4 and later. Security egress makes it possible to control outgoing traffic from the Grafana server. @@ -355,7 +355,7 @@ Encryption algorithm used to encrypt secrets stored in the database and cookies. ## [caching] -> **Note:** Available in Grafana Enterprise v7.5 and later versions. +> **Note:** Available in Grafana Enterprise version 7.5 and later. When query caching is enabled, Grafana can temporarily store the results of data source queries and serve cached responses to similar requests. diff --git a/docs/sources/enterprise/export-pdf.md b/docs/sources/enterprise/export-pdf.md index 73c56740c8c..aa593c0ccdb 100644 --- a/docs/sources/enterprise/export-pdf.md +++ b/docs/sources/enterprise/export-pdf.md @@ -13,11 +13,11 @@ weight: 1400 # Export dashboard as PDF -You can generate PDFs from any of your dashboards and save it to file. +You can generate and save PDF files from any of your dashboards. -> Only available in Grafana Enterprise v6.7+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}). 1. In the upper right corner of the dashboard that you want to export as PDF, click the **Share dashboard** icon. 1. On the PDF tab, select the layout option for exported dashboard: **Portrait** or **Landscape**. -1. Click **Save as PDF** to render dashboard as a PDF document. - Grafana opens the PDF in a new window or browser tab. +1. Click **Save as PDF** to render the dashboard as a PDF file. + Grafana opens the PDF file in a new window or browser tab. diff --git a/docs/sources/enterprise/query-caching.md b/docs/sources/enterprise/query-caching.md index b1f1ca56d84..415d0ca38fb 100644 --- a/docs/sources/enterprise/query-caching.md +++ b/docs/sources/enterprise/query-caching.md @@ -17,6 +17,8 @@ When query caching is enabled, Grafana temporarily stores the results of data so Query caching works for all backend data sources, and queries sent through the data source proxy. You can enable the cache globally and configure the cache duration (also called Time to Live, or TTL). +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). + The following cache backends are available: in-memory, Redis, and Memcached. > **Note:** Storing cached queries in-memory can increase Grafana's memory footprint. In production environments, a Redis or Memcached backend is highly recommended. @@ -35,7 +37,7 @@ You can make a panel retrieve fresh data more frequently by increasing the **Max ## Data sources that work with query caching -Query caching works for all [Enterprise data sources](https://grafana.com/grafana/plugins/?type=datasource&enterprise=1), and it works for the following [built-in data sources]({{< relref "../datasources/_index.md" >}}): +Query caching works for all [Enterprise data sources](https://grafana.com/grafana/plugins/?type=datasource&enterprise=1) as well as the following [built-in data sources]({{< relref "../datasources/_index.md" >}}): - CloudWatch Metrics - Google Cloud Monitoring diff --git a/docs/sources/enterprise/recorded-queries.md b/docs/sources/enterprise/recorded-queries.md index 599e1c1255b..74b55ee45cf 100644 --- a/docs/sources/enterprise/recorded-queries.md +++ b/docs/sources/enterprise/recorded-queries.md @@ -17,6 +17,8 @@ Recorded queries allow you to see trends over time by taking a snapshot of a dat For our plugins that do not return time series, it might be useful to plot historical data. For example, you might want to query ServiceNow to see a history of request response times but it can only return current point-in-time metrics. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}). + ## How recorded queries work > **Note:** An administrator must configure a Prometheus data source and associate it with a [Remote write target](#remote-write-target) before recorded queries can be used. diff --git a/docs/sources/enterprise/reporting.md b/docs/sources/enterprise/reporting.md index 4fddbd4052f..e29903df652 100644 --- a/docs/sources/enterprise/reporting.md +++ b/docs/sources/enterprise/reporting.md @@ -14,7 +14,7 @@ weight: 800 Reporting allows you to automatically generate PDFs from any of your dashboards and have Grafana email them to interested parties on a schedule. This is available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. -> If you have [Role-based access control]({{< relref "access-control/_index.md" >}}) enabled, for some actions you would need to have relevant permissions. +> If you enabled [Role-based access control]({{< relref "access-control/_index.md" >}}), for some actions users would need to have relevant permissions. > Refer to specific guides to understand what permissions are required. {{< figure src="/static/img/docs/enterprise/reports_list_8.1.png" max-width="500px" class="docs-image--no-shadow" >}} @@ -55,7 +55,7 @@ Only organization admins can create reports by default. You can customize who ca ### Choose template variables -> **Note:** Available in Grafana Enterprise version 7.5+ (behind `reportVariables` feature flag) and Grafana Enterprise version 8+ without a feature flag. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.5 and later behind the `reportVariables` feature flag, Grafana Enterprise version 8.0 and later without a feature flag, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). You can configure report-specific template variables for the dashboard on the report page. The variables that you select will override the variables from the dashboard, and they are used when rendering a PDF file of the report. For detailed information about using template variables, refer to the [Templates and variables]({{< relref "../variables/_index.md" >}}) section. @@ -63,7 +63,7 @@ You can configure report-specific template variables for the dashboard on the re ### Render a report with panels or rows set to repeat by a variable -> **Note:** Available in Grafana Enterprise v8+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). You can include dynamic dashboards with panels or rows, set to repeat by a variable, into reports. For detailed information about setting up repeating panels or rows in dashboards, refer to the [Repeat panels or rows]({{< relref "../panels/add-panels-dynamically/" >}}) section. @@ -75,7 +75,7 @@ You can include dynamic dashboards with panels or rows, set to repeat by a varia ### Report time range -> Setting custom report time range is available in Grafana Enterprise v7.2+. +> **Note:** You can set custom report time ranges in [Grafana Enterprise]({{< relref "../enterprise" >}}) 7.2+ and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). By default, reports use the saved time range of the dashboard. Changing the time range of the report can be done by: @@ -88,7 +88,7 @@ If the time zone is set differently between your Grafana server and its remote i ### Layout and orientation -> We're actively working on developing new report layout options. [Contact us](https://grafana.com/contact?about=grafana-enterprise&topic=design-process&value=reporting) if you would like to get involved in the design process. +> We're actively developing new report layout options. [Contact us](https://grafana.com/contact?about=grafana-enterprise&topic=design-process&value=reporting) to get involved in the design process. | Layout | Orientation | Support | Description | Preview | | ------ | ----------- | ------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -99,7 +99,7 @@ If the time zone is set differently between your Grafana server and its remote i ### CSV export -> **Note:** Only available in Grafana Enterprise v8.0+, with the [Grafana image renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer) v3.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) 8+ with the [Grafana image renderer plugin](https://grafana.com/grafana/plugins/grafana-image-renderer) v3.0+, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). You can attach a CSV file to the report email for each table panel on the selected dashboard, along with the PDF report. By default, CSVs larger than 10Mb won't be sent to avoid email servers to reject the email. You can increase or decrease this limit in the [reporting configuration]({{< relref "#rendering-configuration" >}}). @@ -111,9 +111,10 @@ A background job runs every 10 minutes and removes temporary CSV files. You can ### Scheduling -> Note: Scheduler has been significantly changed in Grafana Enterprise v8.1. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). +> The scheduler was significantly changed in Grafana Enterprise version 8.1. -Scheduled reports can be sent once or repeatedly on an hourly, daily, weekly, or monthly basis, or at custom intervals. You can also disable scheduling by selecting **Never**: for example, if you want to send the report via the API. +Scheduled reports can be sent once, or repeated on an hourly, daily, weekly, or monthly basis, or sent at custom intervals. You can also disable scheduling by selecting **Never**, for example to send the report via the API. {{< figure src="/static/img/docs/enterprise/reports_scheduler_8.1.png" max-width="500px" class="docs-image--no-shadow" >}} @@ -133,7 +134,7 @@ When you schedule a report with a monthly frequency, and set the start date betw ### Send test email -> Only available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). 1. In the report, click **Send test email**. 1. In the Email field, enter the email address or addresses that you want to test, separated by semicolon. @@ -146,7 +147,7 @@ The last saved version of the report will be sent to selected emails. You can us ## Pause report -> **Note:** Available in Grafana Enterprise v8+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 8.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). You can pause sending of reports from the report list view by clicking the pause icon. The report will not be sent according to its schedule until it is resumed by clicking the resume button on the report row. @@ -187,14 +188,15 @@ font_italic = DejaVuSansCondensed-Oblique.ttf ## Reports settings -> **Note:** Available in Grafana Enterprise v7.2+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.2 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). You can configure organization-wide report settings in the **Settings** tab on the **Reporting** page. Settings are applied to all the reports for current organization. You can customize the branding options. Report branding: -**Company logo URL** - Company logo displayed in the report PDF. Defaults to the Grafana logo. + +- **Company logo URL** - Company logo displayed in the report PDF. Defaults to the Grafana logo. Email branding: diff --git a/docs/sources/enterprise/request-security.md b/docs/sources/enterprise/request-security.md index d02ded36429..4b307941d22 100644 --- a/docs/sources/enterprise/request-security.md +++ b/docs/sources/enterprise/request-security.md @@ -12,17 +12,12 @@ weight: 400 # Request security -> **Note:** Available in Grafana Enterprise v7.4 and later versions. - -Request security makes it possible to limit requests from the Grafana server, and it targets requests that are generated by users. - -For example: - -- Data source metric queries -- Alert notifications +Request security allows you to limit requests from the Grafana server by targeting requests generated by users, such as data source metric queries and alert notifications. This can be used to limit access to internal systems that the server Grafana runs on can access but that users of Grafana should not be able to access. This feature does not affect traffic from the Grafana users browser. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) version 7.4 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). + > **Note:** Although request security works with backend plugins, you can create a backend plugin that bypasses this security. ## IP and hostname blocking diff --git a/docs/sources/enterprise/saml/_index.md b/docs/sources/enterprise/saml/_index.md index a343bffb002..3a4cba3b96e 100644 --- a/docs/sources/enterprise/saml/_index.md +++ b/docs/sources/enterprise/saml/_index.md @@ -17,6 +17,6 @@ weight: 10 SAML authentication integration enables your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. -> Only available in Grafana Enterprise v6.3+. If you experience any issues with our implementation, contact our [Technical Support team](https://grafana.com/contact?plcmt=top-nav&cta=contactus) +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). {{< section >}} diff --git a/docs/sources/enterprise/saml/about-saml.md b/docs/sources/enterprise/saml/about-saml.md index b42bd19a054..75f02d25b32 100644 --- a/docs/sources/enterprise/saml/about-saml.md +++ b/docs/sources/enterprise/saml/about-saml.md @@ -20,7 +20,7 @@ SAML authentication integration allows your Grafana users to log in by using an The SAML single sign-on (SSO) standard is varied and flexible. Our implementation contains a subset of features needed to provide a smooth authentication experience into Grafana. -> Only available in Grafana Enterprise v6.3+. If you encounter any problems with our implementation, please don't hesitate to contact us. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). ## Supported SAML diff --git a/docs/sources/enterprise/settings-updates.md b/docs/sources/enterprise/settings-updates.md index a33c6c573a5..44fc259aeef 100644 --- a/docs/sources/enterprise/settings-updates.md +++ b/docs/sources/enterprise/settings-updates.md @@ -12,9 +12,9 @@ weight: 500 # Settings updates at runtime -> **Note:** Available in Grafana Enterprise v8.0+. +> **Note:** Available in Grafana Enterprise version 8.0 and later. -Settings updates at runtime allows you to update Grafana settings with no need to restart the Grafana server. +By updating settings at runtime, you can update Grafana settings without needing to restart the Grafana server. Updates that happen at runtime are stored in the database and override [settings from the other sources](https://grafana.com/docs/grafana/latest/administration/configuration/) @@ -92,5 +92,5 @@ HTTP API, then the other instances are synchronized through the database and the ## Control access with role-based access control -If you have [Role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, you can control who can read or update settings. +If you have [role-based access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, you can control who can read or update settings. Refer to the [Admin API]({{< relref "../developers/http_api/admin.md#update-settings" >}}) for more information. diff --git a/docs/sources/enterprise/team-sync.md b/docs/sources/enterprise/team-sync.md index aa844d58e4d..59792b01bf3 100644 --- a/docs/sources/enterprise/team-sync.md +++ b/docs/sources/enterprise/team-sync.md @@ -17,7 +17,7 @@ weight: 1000 Team sync lets you set up synchronization between your auth providers teams and teams in Grafana. This enables LDAP, OAuth, or SAML users who are members of certain teams or groups to automatically be added or removed as members of certain teams in Grafana. -> Available in Grafana Cloud Pro and Advanced and in Grafana Enterprise. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< ref "/docs/grafana-cloud" >}}). Grafana keeps track of all synchronized users in teams, and you can see which users have been synchronized in the team members list, see `LDAP` label in screenshot. This mechanism allows Grafana to remove an existing synchronized user from a team when its group membership changes. This mechanism also enables you to manually add a user as member of a team, and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. diff --git a/docs/sources/enterprise/usage-insights/_index.md b/docs/sources/enterprise/usage-insights/_index.md index 608689eef55..abb1c085024 100644 --- a/docs/sources/enterprise/usage-insights/_index.md +++ b/docs/sources/enterprise/usage-insights/_index.md @@ -14,6 +14,8 @@ weight: 200 Usage insights allow you to have a better understanding of how your Grafana instance is used. +> **Note:** Available in [Grafana Enterprise]({{< relref "../../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). + The usage insights feature collects a number of aggregated data and stores them in the database: - Dashboard views (aggregated and per user) diff --git a/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md b/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md index e284d0a016b..2a6bff4b413 100644 --- a/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md +++ b/docs/sources/enterprise/usage-insights/dashboard-datasource-insights.md @@ -16,7 +16,7 @@ For every dashboard and data source, you can access usage information. ## Dashboard insights -> **Note:** Available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). To see dashboard usage information, go to the top bar and click **Dashboard insights**. @@ -31,7 +31,7 @@ Dashboard insights show the following information: ## Data source insights -> **Note:** Available in Grafana Enterprise v7.3+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../../enterprise" >}}) version 7.3 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). Data source insights give you information about how a data source has been used in the past 30 days, such as: diff --git a/docs/sources/enterprise/usage-insights/export-logs.md b/docs/sources/enterprise/usage-insights/export-logs.md index dcd7cef2316..7b569afc2a5 100644 --- a/docs/sources/enterprise/usage-insights/export-logs.md +++ b/docs/sources/enterprise/usage-insights/export-logs.md @@ -13,7 +13,7 @@ weight: 500 # Export logs of usage insights -> **Note:** Available in Grafana Enterprise v7.4+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../../enterprise" >}}) version 7.4 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). By exporting usage logs to Loki, you can directly query them and create dashboards of the information that matters to you most, such as dashboard errors, most active organizations, or your top-10 most-used queries. diff --git a/docs/sources/enterprise/usage-insights/improved-search.md b/docs/sources/enterprise/usage-insights/improved-search.md index 14b873be8c7..24b0ee175e3 100644 --- a/docs/sources/enterprise/usage-insights/improved-search.md +++ b/docs/sources/enterprise/usage-insights/improved-search.md @@ -13,7 +13,7 @@ weight: 400 # Sort dashboards by using insights data -> **Note:** Available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). In the search view, you can sort dashboards by using insights data. Doing so helps you find unused or broken dashboards or discover those that are most viewed. diff --git a/docs/sources/enterprise/usage-insights/presence-indicator.md b/docs/sources/enterprise/usage-insights/presence-indicator.md index a84f0245839..499fcb727a7 100644 --- a/docs/sources/enterprise/usage-insights/presence-indicator.md +++ b/docs/sources/enterprise/usage-insights/presence-indicator.md @@ -12,7 +12,7 @@ weight: 300 # Presence indicator -> **Note:** Available in Grafana Enterprise v7.0+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../../enterprise" >}}) version 7.0 and later, and [Grafana Cloud Pro and Advanced]({{< ref "/docs/grafana-cloud" >}}). When you are signed in and looking at any given dashboard, you can know who is looking at the same dashboard as you are via a presence indicator, which displays avatars of users who have interacted with the dashboard recently. The default time frame is within the past 10 minutes. To see the user's name, hover over the user's avatar. The avatars come from [Gravatar](https://gravatar.com) based on the user's email. diff --git a/docs/sources/enterprise/vault.md b/docs/sources/enterprise/vault.md index de6a7574096..592e09462a0 100644 --- a/docs/sources/enterprise/vault.md +++ b/docs/sources/enterprise/vault.md @@ -12,10 +12,9 @@ weight: 1200 # Vault integration -> Only available in Grafana Enterprise v7.1+. +If you manage your secrets with [Hashicorp Vault](https://www.hashicorp.com/products/vault), you can use them for [Configuration]({{< relref "../administration/configuration.md" >}}) and [Provisioning]({{< relref "../administration/provisioning.md" >}}). -If you manage your secrets with [Hashicorp Vault](https://www.hashicorp.com/products/vault), you can use them for [Configuration]({{< relref "../administration/configuration.md" >}}) -and [Provisioning]({{< relref "../administration/provisioning.md" >}}). +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< ref "/docs/grafana-cloud" >}}). > **Note:** If you have Grafana [set up for high availability]({{< relref "../administration/set-up-for-high-availability.md" >}}), then we advise not to use dynamic secrets for provisioning files. > Each Grafana instance is responsible for renewing its own leases. Your data source leases might expire when one of your Grafana servers shuts down. diff --git a/docs/sources/enterprise/white-labeling.md b/docs/sources/enterprise/white-labeling.md index 61620ab30f9..244ed3c7e32 100644 --- a/docs/sources/enterprise/white-labeling.md +++ b/docs/sources/enterprise/white-labeling.md @@ -14,7 +14,7 @@ weight: 1300 White labeling allows you to replace the Grafana brand and logo with your own corporate brand and logo. -> Only available in Grafana Enterprise v6.6+. +> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Advanced]({{< ref "/docs/grafana-cloud" >}}). Grafana Enterprise has white labeling options in the `grafana.ini` file. As with all configuration options, you can also set them with environment variables. From b8ec4346aad6c514e3eacb2e52c8cfb50415807b Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Tue, 31 May 2022 10:14:35 -0500 Subject: [PATCH 05/95] Docs/fixes relrefs (#49754) (#49933) * fixes http-api link * removes old saml docs * fixes broken links to enterprise docs landing page (cherry picked from commit 0f40d2a79d7851b06d29f224e6bef4c7998aca00) --- docs/sources/enterprise/_index.md | 2 +- docs/sources/enterprise/saml/_index.md | 22 ----- docs/sources/enterprise/saml/about-saml.md | 51 ------------ .../enterprise/saml/set-up-saml-with-okta.md | 82 ------------------- 4 files changed, 1 insertion(+), 156 deletions(-) delete mode 100644 docs/sources/enterprise/saml/_index.md delete mode 100644 docs/sources/enterprise/saml/about-saml.md delete mode 100644 docs/sources/enterprise/saml/set-up-saml-with-okta.md diff --git a/docs/sources/enterprise/_index.md b/docs/sources/enterprise/_index.md index d3dd95c64cf..4f04f561e4e 100644 --- a/docs/sources/enterprise/_index.md +++ b/docs/sources/enterprise/_index.md @@ -52,7 +52,7 @@ With [enhanced LDAP integration]({{< relref "enhanced_ldap.md" >}}), you can set ### SAML authentication -[SAML authentication]({{< relref "./saml" >}}) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML). +[SAML authentication]({{< relref "./configure-saml" >}}) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML). ## Enterprise features diff --git a/docs/sources/enterprise/saml/_index.md b/docs/sources/enterprise/saml/_index.md deleted file mode 100644 index 3a4cba3b96e..00000000000 --- a/docs/sources/enterprise/saml/_index.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -aliases: - - /docs/grafana/latest/auth/saml/ - - /docs/grafana/latest/enterprise/saml/ -description: Grafana SAML authentication -keywords: - - grafana - - saml - - documentation - - saml-auth - - enterprise -title: SAML authentication -weight: 10 ---- - -# SAML authentication - -SAML authentication integration enables your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. - -> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). - -{{< section >}} diff --git a/docs/sources/enterprise/saml/about-saml.md b/docs/sources/enterprise/saml/about-saml.md deleted file mode 100644 index 75f02d25b32..00000000000 --- a/docs/sources/enterprise/saml/about-saml.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -aliases: - - /docs/grafana/latest/auth/saml/ - - /docs/grafana/latest/enterprise/saml/about-saml/ -description: SAML authentication -keywords: - - grafana - - saml - - documentation - - saml-auth - - enterprise -menuTitle: About SAML authentication -title: About SAML authentication in Grafana -weight: 20 ---- - -# About SAML authentication - -SAML authentication integration allows your Grafana users to log in by using an external SAML 2.0 Identity Provider (IdP). To enable this, Grafana becomes a Service Provider (SP) in the authentication flow, interacting with the IdP to exchange user information. - -The SAML single sign-on (SSO) standard is varied and flexible. Our implementation contains a subset of features needed to provide a smooth authentication experience into Grafana. - -> **Note:** Available in [Grafana Enterprise]({{< relref "../enterprise" >}}) and [Grafana Cloud Pro and Advanced]({{< relref "/grafana-cloud" >}}). - -## Supported SAML - -Grafana supports the following SAML 2.0 bindings: - -- From the Service Provider (SP) to the Identity Provider (IdP): - - - `HTTP-POST` binding - - `HTTP-Redirect` binding - -- From the Identity Provider (IdP) to the Service Provider (SP): - - `HTTP-POST` binding - -In terms of security: - -- Grafana supports signed and encrypted assertions. -- Grafana does not support signed or encrypted requests. - -In terms of initiation, Grafana supports: - -- SP-initiated requests -- IdP-initiated requests - -By default, SP-initiated requests are enabled. For instructions on how to enable IdP-initiated logins, refer to [IdP-initiated]({{< relref "./configure-saml/#idp-initiated-single-sign-on-sso" >}}) to get more information. - -### Edit SAML options in the Grafana config file - -Once you have enabled saml, you can configure Grafana to use it for SAML authentication. Refer to [Configure SAML Authentication]({{< relref "./configure-saml.md#" >}}) to get more information about how to configure Grafana. diff --git a/docs/sources/enterprise/saml/set-up-saml-with-okta.md b/docs/sources/enterprise/saml/set-up-saml-with-okta.md deleted file mode 100644 index 9de28106e78..00000000000 --- a/docs/sources/enterprise/saml/set-up-saml-with-okta.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -aliases: - - /docs/grafana/latest/enterprise/saml/set-up-saml-with-okta/ -description: This is a guide to set up SAML authentication with Okta in Grafana -keywords: - - grafana - - saml - - documentation - - saml-auth - - enterprise -menuTitle: SAML authentication with Okta -title: Set up SAML authentication with Okta in Grafana -weight: 30 ---- - -# Set up SAML with Okta - -Grafana supports user authentication through Okta, which is useful when you want your users to access Grafana using single sign on. This guide will follow you through the steps of configuring SAML authentication in Grafana with [Okta](https://okta.com/). You need to be an admin in your Okta organization to access Admin Console and create SAML integration. You also need permissions to edit Grafana config file and restart Grafana server. - -## Before you begin - -- To configure SAML integration with Okta, create integration inside the Okta organization first. [Add integration in Okta](https://help.okta.com/en/prod/Content/Topics/Apps/apps-overview-add-apps.htm) -- Ensure you have permission to administer SAML authentication. For more information about permissions, refer to [About users and permissions]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#" >}}). - -**To set up SAML with Okta:** - -1. Log in to the [Okta portal](https://login.okta.com/). -1. Go to the Admin Console in your Okta organization by clicking **Admin** in the upper-right corner. If you are in the Developer Console, then click **Developer Console** in the upper-left corner and then click **Classic UI** to switch over to the Admin Console. -1. In the Admin Console, navigate to **Applications** > **Applications**. -1. Click **Add Application**. -1. Click **Create New App** to start the Application Integration Wizard. -1. Choose **Web** as a platform. -1. Select **SAML 2.0** in the Sign on method section. -1. Click **Create**. -1. On the **General Settings** tab, enter a name for your Grafana integration. You can also upload a logo. -1. On the **Configure SAML** tab, enter the SAML information related to your Grafana instance: - - - In the **Single sign on URL** field, use the `/saml/acs` endpoint URL of your Grafana instance, for example, `https://grafana.example.com/saml/acs`. - - In the **Audience URI (SP Entity ID)** field, use the `/saml/metadata` endpoint URL, for example, `https://grafana.example.com/saml/metadata`. - - Leave the default values for **Name ID format** and **Application username**. - - In the **ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter the SAML attributes to be shared with Grafana, for example: - - | Attribute name (in Grafana) | Value (in Okta profile) | - | --------------------------- | -------------------------------------- | - | Login | `user.login` | - | Email | `user.email` | - | DisplayName | `user.firstName + " " + user.lastName` | - - - In the **GROUP ATTRIBUTE STATEMENTS (OPTIONAL)** section, enter a group attribute name (for example, `Group`) and set filter to `Matches regex .*` to return all user groups. - -1. Click **Next**. -1. On the final Feedback tab, fill out the form and then click **Finish**. - -**Edit SAML options for Okta in Grafana config file:** - -1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`]({{< relref ".././enterprise-configuration.md#enabled" >}}) to `true`. -1. Configure the [certificate and private key]({{< relref "#certificate-and-private-key" >}}). -1. On the Okta application page where you have been redirected after application created, navigate to the **Sign On** tab and find **Identity Provider metadata** link in the **Settings** section. -1. Set the [`idp_metadata_url`]({{< relref ".././enterprise-configuration.md#idp-metadata-url" >}}) to the URL obtained from the previous step. The URL should look like `https://.okta.com/app//sso/saml/metadata`. -1. Set the following options to the attribute names configured at the **step 10** of the SAML integration setup. You can find this attributes on the **General** tab of the application page (**ATTRIBUTE STATEMENTS** and **GROUP ATTRIBUTE STATEMENTS** in the **SAML Settings** section). - - [`assertion_attribute_login`]({{< relref ".././enterprise-configuration.md#assertion-attribute-login" >}}) - - [`assertion_attribute_email`]({{< relref ".././enterprise-configuration.md#assertion-attribute-email" >}}) - - [`assertion_attribute_name`]({{< relref ".././enterprise-configuration.md#assertion-attribute-name" >}}) - - [`assertion_attribute_groups`]({{< relref ".././enterprise-configuration.md#assertion-attribute-groups" >}}) -1. Save the configuration file and and then restart the Grafana server. - -When you are finished, the Grafana configuration might look like this example: - -```bash -[server] -root_url = https://grafana.example.com - -[auth.saml] -enabled = true -private_key_path = "/path/to/private_key.pem" -certificate_path = "/path/to/certificate.cert" -idp_metadata_url = "https://my-org.okta.com/app/my-application/sso/saml/metadata" -assertion_attribute_name = DisplayName -assertion_attribute_login = Login -assertion_attribute_email = Email -assertion_attribute_groups = Group -``` From e3127d40707317304c6c693a09132adf0239b7e1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 11:17:55 -0400 Subject: [PATCH 06/95] Datasource: Fix allowed cookies to be forwarded as header to backend datasources (#49541) (#49935) Co-authored-by: Will Browne (cherry picked from commit 1196b4a60992585bbbb8cb82e52dbc56ecbd81cc) Co-authored-by: Marcus Efraimsson --- pkg/api/dtos/models.go | 3 +++ pkg/api/metrics.go | 2 ++ pkg/services/query/query.go | 20 +++++++++++++++++ pkg/services/query/query_test.go | 37 ++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index f0ceb354802..dbed74c01b7 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -3,6 +3,7 @@ package dtos import ( "crypto/md5" "fmt" + "net/http" "regexp" "strings" @@ -68,6 +69,8 @@ type MetricRequest struct { Queries []*simplejson.Json `json:"queries"` // required: false Debug bool `json:"debug"` + + HTTPRequest *http.Request `json:"-"` } func GetGravatarUrl(text string) string { diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 3b6649ba7c8..46929748a92 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -44,6 +44,8 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } + reqDTO.HTTPRequest = c.Req + resp, err := hs.queryDataService.QueryData(c.Req.Context(), c.SignedInUser, c.SkipCache, reqDTO, true) if err != nil { return hs.handleQueryMetricsError(err) diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index b449648ca22..2bd50694b59 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -3,6 +3,7 @@ package query import ( "context" "fmt" + "net/http" "strings" "time" @@ -18,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafanads" "github.com/grafana/grafana/pkg/tsdb/legacydata" + "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana-plugin-sdk-go/backend" ) @@ -149,6 +151,19 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser req.Headers[k] = v } + if parsedReq.httpRequest != nil && parsedReq.httpRequest.Header.Get("Cookie") != "" && ds.JsonData != nil { + keepCookieNames := []string{} + + if keepCookies := ds.JsonData.Get("keepCookies"); keepCookies != nil { + keepCookieNames = keepCookies.MustStringArray() + } + + proxyutil.ClearCookieHeader(parsedReq.httpRequest, keepCookieNames) + if cookieStr := parsedReq.httpRequest.Header.Get("Cookie"); cookieStr != "" { + req.Headers["Cookie"] = cookieStr + } + } + for _, q := range parsedReq.parsedQueries { req.Queries = append(req.Queries, q.query) } @@ -164,6 +179,7 @@ type parsedQuery struct { type parsedRequest struct { hasExpression bool parsedQueries []parsedQuery + httpRequest *http.Request } func customHeaders(jsonData *simplejson.Json, decryptedJsonData map[string]string) map[string]string { @@ -243,6 +259,10 @@ func (s *Service) parseMetricRequest(ctx context.Context, user *models.SignedInU } } + if reqDTO.HTTPRequest != nil { + req.httpRequest = reqDTO.HTTPRequest + } + return req, nil } diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index d4f1d7b87ad..27714d41316 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -61,6 +61,43 @@ func TestQueryData(t *testing.T) { } require.Equal(t, expected, tc.pluginContext.req.Headers) }) + + t.Run("it doesn't add cookie header to the request when keepCookies configured and no cookies provided", func(t *testing.T) { + tc := setup(t) + json, err := simplejson.NewJson([]byte(`{"keepCookies": [ "foo", "bar" ]}`)) + require.NoError(t, err) + tc.dataSourceCache.ds.JsonData = json + + metricReq := metricRequest() + httpReq, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + metricReq.HTTPRequest = httpReq + _, err = tc.queryService.QueryData(context.Background(), nil, true, metricReq, false) + require.NoError(t, err) + + require.Empty(t, tc.pluginContext.req.Headers) + }) + + t.Run("it adds cookie header to the request when keepCookies configured and cookie provided", func(t *testing.T) { + tc := setup(t) + json, err := simplejson.NewJson([]byte(`{"keepCookies": [ "foo", "bar" ]}`)) + require.NoError(t, err) + tc.dataSourceCache.ds.JsonData = json + + metricReq := metricRequest() + httpReq, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + httpReq.AddCookie(&http.Cookie{Name: "a"}) + httpReq.AddCookie(&http.Cookie{Name: "bar", Value: "rab"}) + httpReq.AddCookie(&http.Cookie{Name: "b"}) + httpReq.AddCookie(&http.Cookie{Name: "foo", Value: "oof"}) + httpReq.AddCookie(&http.Cookie{Name: "c"}) + metricReq.HTTPRequest = httpReq + _, err = tc.queryService.QueryData(context.Background(), nil, true, metricReq, false) + require.NoError(t, err) + + require.Equal(t, map[string]string{"Cookie": "bar=rab; foo=oof"}, tc.pluginContext.req.Headers) + }) } func setup(t *testing.T) *testContext { From 445909a75987a3ed76aa5139c8abf9cb6b2764e9 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 11:48:31 -0400 Subject: [PATCH 07/95] InlineLabel: Remove deprecated props (#49929) (#49943) (cherry picked from commit 1595cc96e6e7e6181c666088b01f4d6651aeccaf) Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> --- packages/grafana-ui/src/components/Forms/InlineLabel.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/grafana-ui/src/components/Forms/InlineLabel.tsx b/packages/grafana-ui/src/components/Forms/InlineLabel.tsx index 32e21247969..33ffc2b660f 100644 --- a/packages/grafana-ui/src/components/Forms/InlineLabel.tsx +++ b/packages/grafana-ui/src/components/Forms/InlineLabel.tsx @@ -17,12 +17,6 @@ export interface Props extends Omit Date: Tue, 31 May 2022 12:22:35 -0400 Subject: [PATCH 08/95] Plugins: Support headers field for check health (#49930) (#49949) (cherry picked from commit a7813275a5fe2867f5a81690eb406480ea10a96d) Co-authored-by: Marcus Efraimsson --- ...d-authentication-for-data-source-plugins.md | 18 ++++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- pkg/api/datasources.go | 17 +++++++++++++++++ pkg/api/pluginproxy/ds_proxy.go | 9 +-------- pkg/api/plugins.go | 1 + pkg/models/datasource.go | 12 ++++++++++++ .../backendplugin/grpcplugin/client_v2.go | 2 +- pkg/services/query/query.go | 10 ++-------- 9 files changed, 55 insertions(+), 20 deletions(-) diff --git a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md index 4385945f1a7..a0a33d47341 100644 --- a/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md +++ b/docs/sources/developers/plugins/add-authentication-for-data-source-plugins.md @@ -293,6 +293,17 @@ To allow Grafana to pass the access token to the plugin, update the data source When configured, Grafana will pass the user's token to the plugin in an Authorization header, available on the `QueryDataRequest` object on the `QueryData` request in your backend data source. ```go +func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + token := strings.Fields(req.Headers["Authorization"]) + var ( + tokenType = token[0] + accessToken = token[1] + ) + + // ... + return &backend.CheckHealthResult{Status: backend.HealthStatusOk}, nil +} + func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { token := strings.Fields(req.Headers["Authorization"]) var ( @@ -309,6 +320,13 @@ func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataReque In addition, if the user's token includes an ID token, Grafana will pass the user's ID token to the plugin in an `X-ID-Token` header, available on the `QueryDataRequest` object on the `QueryData` request in your backend data source. ```go +func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + idToken := req.Headers["X-ID-Token"] + + // ... + return &backend.CheckHealthResult{Status: backend.HealthStatusOk}, nil +} + func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { idToken := req.Headers["X-ID-Token"] diff --git a/go.mod b/go.mod index 3900b7e23b5..9175b472c39 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,7 @@ require ( github.com/grafana/cuetsy v0.0.1 github.com/grafana/grafana-aws-sdk v0.10.3 github.com/grafana/grafana-azure-sdk-go v1.2.0 - github.com/grafana/grafana-plugin-sdk-go v0.134.0 + github.com/grafana/grafana-plugin-sdk-go v0.135.0 github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.0.0 diff --git a/go.sum b/go.sum index 0f8a1421224..20ae68b9b25 100644 --- a/go.sum +++ b/go.sum @@ -1451,8 +1451,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW3 github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= github.com/grafana/grafana-plugin-sdk-go v0.125.0/go.mod h1:9YiJ5GUxIsIEUC0qR9+BJVP5M7mCSP6uc6Ne62YKkgc= github.com/grafana/grafana-plugin-sdk-go v0.129.0/go.mod h1:4edtosZepfQF9jkQwRywJsNSJzXTHmzbmcVcAl8MEQc= -github.com/grafana/grafana-plugin-sdk-go v0.134.0 h1:8j8vsvhU3GabRPWB1EnFKSWt60yQVFPEE/5P1QV2gFw= -github.com/grafana/grafana-plugin-sdk-go v0.134.0/go.mod h1:jmrxelOJKrIK0yrsIzcotS8pbqPZozbmJgGy7k3hK1k= +github.com/grafana/grafana-plugin-sdk-go v0.135.0 h1:IQrwA/RCPr5IhE3lVxIpgEXwZohx7gp3rJ1KJa0KT5g= +github.com/grafana/grafana-plugin-sdk-go v0.135.0/go.mod h1:jmrxelOJKrIK0yrsIzcotS8pbqPZozbmJgGy7k3hK1k= github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa h1:+pXjAxavVR2FKKNsuuCXGCWEj8XGc1Af6SPiyBpzU2A= github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa/go.mod h1:0O8o/juxNSKN/e+DzWDTRkl7Zm8CkZcz0NDqEdojlrk= github.com/grafana/saml v0.0.0-20211007135653-aed1b2edd86b h1:YiSGp34F4V0G08HHx1cJBf2GVgwYAkXQjzuVs1t8jYk= diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index f6a87092e92..1c43d9f7263 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/proxyutil" "github.com/grafana/grafana/pkg/web" ) @@ -568,6 +569,7 @@ func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *models.Dat PluginID: plugin.ID, DataSourceInstanceSettings: dsInstanceSettings, }, + Headers: map[string]string{}, } var dsURL string @@ -580,6 +582,21 @@ func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *models.Dat return response.Error(http.StatusForbidden, "Access denied", err) } + if hs.DataProxy.OAuthTokenService.IsOAuthPassThruEnabled(ds) { + if token := hs.DataProxy.OAuthTokenService.GetCurrentOAuthToken(c.Req.Context(), c.SignedInUser); token != nil { + req.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken) + idToken, ok := token.Extra("id_token").(string) + if ok && idToken != "" { + req.Headers["X-ID-Token"] = idToken + } + } + } + + proxyutil.ClearCookieHeader(c.Req, ds.AllowedCookies()) + if cookieStr := c.Req.Header.Get("Cookie"); cookieStr != "" { + req.Headers["Cookie"] = cookieStr + } + resp, err := hs.pluginClient.CheckHealth(c.Req.Context(), req) if err != nil { return translatePluginRequestErrorToAPIError(err) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index d17ddee68f9..a2feb41535d 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -222,14 +222,7 @@ func (proxy *DataSourceProxy) director(req *http.Request) { applyUserHeader(proxy.cfg.SendUserHeader, req, proxy.ctx.SignedInUser) - keepCookieNames := []string{} - if proxy.ds.JsonData != nil { - if keepCookies := proxy.ds.JsonData.Get("keepCookies"); keepCookies != nil { - keepCookieNames = keepCookies.MustStringArray() - } - } - - proxyutil.ClearCookieHeader(req, keepCookieNames) + proxyutil.ClearCookieHeader(req, proxy.ds.AllowedCookies()) req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) jsonData := make(map[string]interface{}) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index b3e973c628d..b502ccd91b6 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -318,6 +318,7 @@ func (hs *HTTPServer) CheckHealth(c *models.ReqContext) response.Response { resp, err := hs.pluginClient.CheckHealth(c.Req.Context(), &backend.CheckHealthRequest{ PluginContext: pCtx, + Headers: map[string]string{}, }) if err != nil { return translatePluginRequestErrorToAPIError(err) diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 377a07fe14f..58d1f391c8e 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -67,6 +67,18 @@ type DataSource struct { Updated time.Time `json:"updated"` } +// AllowedCookies parses the jsondata.keepCookies and returns a list of +// allowed cookies, otherwise an empty list. +func (ds DataSource) AllowedCookies() []string { + if ds.JsonData != nil { + if keepCookies := ds.JsonData.Get("keepCookies"); keepCookies != nil { + return keepCookies.MustStringArray() + } + } + + return []string{} +} + // ---------------------- // COMMANDS diff --git a/pkg/plugins/backendplugin/grpcplugin/client_v2.go b/pkg/plugins/backendplugin/grpcplugin/client_v2.go index 6d44516bda3..f6063217c6e 100644 --- a/pkg/plugins/backendplugin/grpcplugin/client_v2.go +++ b/pkg/plugins/backendplugin/grpcplugin/client_v2.go @@ -115,7 +115,7 @@ func (c *ClientV2) CheckHealth(ctx context.Context, req *backend.CheckHealthRequ } protoContext := backend.ToProto().PluginContext(req.PluginContext) - protoResp, err := c.DiagnosticsClient.CheckHealth(ctx, &pluginv2.CheckHealthRequest{PluginContext: protoContext}) + protoResp, err := c.DiagnosticsClient.CheckHealth(ctx, &pluginv2.CheckHealthRequest{PluginContext: protoContext, Headers: req.Headers}) if err != nil { if status.Code(err) == codes.Unimplemented { diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index 2bd50694b59..082696f6691 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -151,14 +151,8 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser req.Headers[k] = v } - if parsedReq.httpRequest != nil && parsedReq.httpRequest.Header.Get("Cookie") != "" && ds.JsonData != nil { - keepCookieNames := []string{} - - if keepCookies := ds.JsonData.Get("keepCookies"); keepCookies != nil { - keepCookieNames = keepCookies.MustStringArray() - } - - proxyutil.ClearCookieHeader(parsedReq.httpRequest, keepCookieNames) + if parsedReq.httpRequest != nil { + proxyutil.ClearCookieHeader(parsedReq.httpRequest, ds.AllowedCookies()) if cookieStr := parsedReq.httpRequest.Header.Get("Cookie"); cookieStr != "" { req.Headers["Cookie"] = cookieStr } From 8a4eed5fb6666432d2612e51a290954f98381480 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 12:38:54 -0400 Subject: [PATCH 09/95] Datasource: Remove deprecated max_idle_connections_per_host setting (#49948) (#49951) (cherry picked from commit b03657b0e04697fe384fd41d7ad82066f5bf58e3) Co-authored-by: Marcus Efraimsson --- docs/sources/administration/configuration.md | 6 ------ pkg/setting/setting_data_proxy.go | 5 ----- 2 files changed, 11 deletions(-) diff --git a/docs/sources/administration/configuration.md b/docs/sources/administration/configuration.md index 857fd5bd37d..e0a5eb71766 100644 --- a/docs/sources/administration/configuration.md +++ b/docs/sources/administration/configuration.md @@ -434,12 +434,6 @@ For more details check the [Transport.MaxConnsPerHost](https://golang.org/pkg/ne The maximum number of idle connections that Grafana will maintain. Default is `100`. For more details check the [Transport.MaxIdleConns](https://golang.org/pkg/net/http/#Transport.MaxIdleConns) documentation. -### max_idle_connections_per_host - -[Deprecated - use max_idle_connections instead] - -The maximum number of idle connections per host that Grafana will maintain. Default is `2`. For more details check the [Transport.MaxIdleConnsPerHost](https://golang.org/pkg/net/http/#Transport.MaxIdleConnsPerHost) documentation. - ### idle_conn_timeout_seconds The length of time that Grafana maintains idle connections before closing them. Default is `90` seconds. For more details check the [Transport.IdleConnTimeout](https://golang.org/pkg/net/http/#Transport.IdleConnTimeout) documentation. diff --git a/pkg/setting/setting_data_proxy.go b/pkg/setting/setting_data_proxy.go index f879b4c9093..580eb8eadd4 100644 --- a/pkg/setting/setting_data_proxy.go +++ b/pkg/setting/setting_data_proxy.go @@ -23,10 +23,5 @@ func readDataProxySettings(iniFile *ini.File, cfg *Cfg) error { cfg.DataProxyRowLimit = defaultDataProxyRowLimit } - if val, err := dataproxy.Key("max_idle_connections_per_host").Int(); err == nil { - cfg.Logger.Warn("[Deprecated] the configuration setting 'max_idle_connections_per_host' is deprecated, please use 'max_idle_connections' instead") - cfg.DataProxyMaxIdleConns = val - } - return nil } From d71e30a6922096a524cc64a7ab21f2c73b3adfd9 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 12:52:59 -0400 Subject: [PATCH 10/95] [v9.0.x] Docs: integration tests for using sqllite (#49784) * Docs: integration tests for using sqllite (#49455) * Update developer-guide.md * prettier formatting (cherry picked from commit f5d25c91f6b5de8d78298ed30a25767e3359b5bd) * Update contribute/developer-guide.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update contribute/developer-guide.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> Co-authored-by: Eric Leijonmarck Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- contribute/developer-guide.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 9eb31be1f00..33abb37f798 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -136,17 +136,23 @@ Running the backend tests on Windows currently needs some tweaking, so use the b go run build.go test ``` -### Run PostgreSQL and MySQL integration tests +### Run SQLLite, PostgreSQL and MySQL integration tests -To run PostgreSQL and MySQL integration tests locally, you need to start the docker blocks for MySQL and/or PostgreSQL test data sources by running `make devenv sources=mysql_tests,postgres_tests`. When your test data sources are running, you can execute integration tests by running: +By default, Grafana runs SQLite to run tests with SQLite. +```bash +go test -covermode=atomic -tags=integration ./pkg/... ``` + +To run PostgreSQL and MySQL integration tests locally, start the Docker blocks for MySQL and/or PostgreSQL test data sources by running `make devenv sources=mysql_tests,postgres_tests`. When your test data sources are running, you can execute integration tests by running: + +```bash GRAFANA_TEST_DB=mysql go test -covermode=atomic -tags=integration ./pkg/... ``` and/or -``` +```bash GRAFANA_TEST_DB=postgres go test -covermode=atomic -tags=integration ./pkg/... ``` From 2508cb4751dbeb7dbdd0dae988ad7ef1d454eabf Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 14:49:24 -0400 Subject: [PATCH 11/95] "Release: Updated versions in package to 9.0.0-beta.2" (#49959) --- 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-runtime/package.json | 8 ++-- packages/grafana-schema/package.json | 2 +- packages/grafana-toolkit/package.json | 6 +-- packages/grafana-ui/package.json | 8 ++-- packages/jaeger-ui-components/package.json | 8 ++-- .../internal/input-datasource/package.json | 8 ++-- yarn.lock | 42 +++++++++---------- 12 files changed, 48 insertions(+), 48 deletions(-) diff --git a/lerna.json b/lerna.json index 56b8d14068f..8fb5a4d9628 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ "packages": [ "packages/*" ], - "version": "9.0.0-beta.1" + "version": "9.0.0-beta.2" } diff --git a/package.json b/package.json index 6c7227825ee..0678edb2fd3 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "license": "AGPL-3.0-only", "private": true, "name": "grafana", - "version": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "repository": "github:grafana/grafana", "scripts": { "api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index e730ac53804..12f6319ac00 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Grafana Data Library", "keywords": [ "typescript" @@ -22,7 +22,7 @@ }, "dependencies": { "@braintree/sanitize-url": "6.0.0", - "@grafana/schema": "9.0.0-beta.1", + "@grafana/schema": "9.0.0-beta.2", "@types/d3-interpolate": "^1.4.0", "d3-interpolate": "1.4.0", "date-fns": "2.28.0", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 88c7dc933a5..8f741c15e46 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "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 e358b03e99e..4968ce642e6 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Grafana End-to-End Test Library", "keywords": [ "cli", @@ -48,7 +48,7 @@ "@babel/core": "7.17.8", "@babel/preset-env": "7.17.10", "@cypress/webpack-preprocessor": "5.11.1", - "@grafana/e2e-selectors": "9.0.0-beta.1", + "@grafana/e2e-selectors": "9.0.0-beta.2", "@grafana/tsconfig": "^1.2.0-rc1", "@mochajs/json-file-reporter": "^1.2.0", "babel-loader": "8.2.5", diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index 8c9d0934497..7a3417820e7 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Grafana Runtime Library", "keywords": [ "grafana", @@ -22,9 +22,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@grafana/data": "9.0.0-beta.1", - "@grafana/e2e-selectors": "9.0.0-beta.1", - "@grafana/ui": "9.0.0-beta.1", + "@grafana/data": "9.0.0-beta.2", + "@grafana/e2e-selectors": "9.0.0-beta.2", + "@grafana/ui": "9.0.0-beta.2", "@sentry/browser": "6.19.7", "history": "4.10.1", "lodash": "4.17.21", diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 2829323c8c0..2a9f4cf59e1 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Grafana Schema Library", "keywords": [ "typescript" diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index 1dd3e2824aa..a9e06e26c90 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Grafana Toolkit", "keywords": [ "grafana", @@ -38,10 +38,10 @@ "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@babel/preset-typescript": "^7.16.7", - "@grafana/data": "9.0.0-beta.1", + "@grafana/data": "9.0.0-beta.2", "@grafana/eslint-config": "^3.0.0", "@grafana/tsconfig": "^1.2.0-rc1", - "@grafana/ui": "9.0.0-beta.1", + "@grafana/ui": "9.0.0-beta.2", "@jest/core": "27.5.1", "@types/command-exists": "^1.2.0", "@types/eslint": "8.4.1", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 306625e0ae8..7d3ddc2317d 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Grafana Components Library", "keywords": [ "grafana", @@ -33,9 +33,9 @@ "@emotion/css": "11.9.0", "@emotion/react": "11.9.0", "@grafana/aws-sdk": "0.0.36", - "@grafana/data": "9.0.0-beta.1", - "@grafana/e2e-selectors": "9.0.0-beta.1", - "@grafana/schema": "9.0.0-beta.1", + "@grafana/data": "9.0.0-beta.2", + "@grafana/e2e-selectors": "9.0.0-beta.2", + "@grafana/schema": "9.0.0-beta.2", "@grafana/slate-react": "0.22.10-grafana", "@monaco-editor/react": "4.3.1", "@popperjs/core": "2.11.5", diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index 65b25627557..db39a29dc82 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@jaegertracing/jaeger-ui-components", - "version": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -28,9 +28,9 @@ }, "dependencies": { "@emotion/css": "11.9.0", - "@grafana/data": "9.0.0-beta.1", - "@grafana/e2e-selectors": "9.0.0-beta.1", - "@grafana/ui": "9.0.0-beta.1", + "@grafana/data": "9.0.0-beta.2", + "@grafana/e2e-selectors": "9.0.0-beta.2", + "@grafana/ui": "9.0.0-beta.2", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", diff --git a/plugins-bundled/internal/input-datasource/package.json b/plugins-bundled/internal/input-datasource/package.json index 9e6c4ce8eeb..4ccb187e11d 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": "9.0.0-beta.1", + "version": "9.0.0-beta.2", "description": "Input Datasource", "private": true, "repository": { @@ -15,15 +15,15 @@ }, "author": "Grafana Labs", "devDependencies": { - "@grafana/toolkit": "9.0.0-beta.1", + "@grafana/toolkit": "9.0.0-beta.2", "@types/jest": "26.0.15", "@types/lodash": "4.14.149", "@types/react": "17.0.30", "lodash": "4.17.21" }, "dependencies": { - "@grafana/data": "9.0.0-beta.1", - "@grafana/ui": "9.0.0-beta.1", + "@grafana/data": "9.0.0-beta.2", + "@grafana/ui": "9.0.0-beta.2", "jquery": "3.5.1", "react": "17.0.1", "react-dom": "17.0.1", diff --git a/yarn.lock b/yarn.lock index 7899f442819..d8cce474e8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3815,9 +3815,9 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana-plugins/input-datasource@workspace:plugins-bundled/internal/input-datasource" dependencies: - "@grafana/data": 9.0.0-beta.1 - "@grafana/toolkit": 9.0.0-beta.1 - "@grafana/ui": 9.0.0-beta.1 + "@grafana/data": 9.0.0-beta.2 + "@grafana/toolkit": 9.0.0-beta.2 + "@grafana/ui": 9.0.0-beta.2 "@types/jest": 26.0.15 "@types/lodash": 4.14.149 "@types/react": 17.0.30 @@ -3855,12 +3855,12 @@ __metadata: languageName: node linkType: hard -"@grafana/data@9.0.0-beta.1, @grafana/data@workspace:*, @grafana/data@workspace:packages/grafana-data": +"@grafana/data@9.0.0-beta.2, @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.0 - "@grafana/schema": 9.0.0-beta.1 + "@grafana/schema": 9.0.0-beta.2 "@grafana/tsconfig": ^1.2.0-rc1 "@rollup/plugin-commonjs": 22.0.0 "@rollup/plugin-json": 4.1.0 @@ -3913,7 +3913,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/e2e-selectors@9.0.0-beta.1, @grafana/e2e-selectors@workspace:*, @grafana/e2e-selectors@workspace:packages/grafana-e2e-selectors": +"@grafana/e2e-selectors@9.0.0-beta.2, @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: @@ -3937,7 +3937,7 @@ __metadata: "@babel/core": 7.17.8 "@babel/preset-env": 7.17.10 "@cypress/webpack-preprocessor": 5.11.1 - "@grafana/e2e-selectors": 9.0.0-beta.1 + "@grafana/e2e-selectors": 9.0.0-beta.2 "@grafana/tsconfig": ^1.2.0-rc1 "@mochajs/json-file-reporter": ^1.2.0 "@rollup/plugin-commonjs": 22.0.0 @@ -4026,10 +4026,10 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/runtime@workspace:packages/grafana-runtime" dependencies: - "@grafana/data": 9.0.0-beta.1 - "@grafana/e2e-selectors": 9.0.0-beta.1 + "@grafana/data": 9.0.0-beta.2 + "@grafana/e2e-selectors": 9.0.0-beta.2 "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 9.0.0-beta.1 + "@grafana/ui": 9.0.0-beta.2 "@rollup/plugin-commonjs": 22.0.0 "@rollup/plugin-node-resolve": 13.3.0 "@sentry/browser": 6.19.7 @@ -4058,7 +4058,7 @@ __metadata: languageName: unknown linkType: soft -"@grafana/schema@9.0.0-beta.1, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": +"@grafana/schema@9.0.0-beta.2, @grafana/schema@workspace:*, @grafana/schema@workspace:packages/grafana-schema": version: 0.0.0-use.local resolution: "@grafana/schema@workspace:packages/grafana-schema" dependencies: @@ -4105,7 +4105,7 @@ __metadata: languageName: node linkType: hard -"@grafana/toolkit@9.0.0-beta.1, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": +"@grafana/toolkit@9.0.0-beta.2, @grafana/toolkit@workspace:*, @grafana/toolkit@workspace:packages/grafana-toolkit": version: 0.0.0-use.local resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit" dependencies: @@ -4121,10 +4121,10 @@ __metadata: "@babel/preset-env": ^7.16.11 "@babel/preset-react": ^7.16.7 "@babel/preset-typescript": ^7.16.7 - "@grafana/data": 9.0.0-beta.1 + "@grafana/data": 9.0.0-beta.2 "@grafana/eslint-config": ^3.0.0 "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 9.0.0-beta.1 + "@grafana/ui": 9.0.0-beta.2 "@jest/core": 27.5.1 "@types/command-exists": ^1.2.0 "@types/eslint": 8.4.1 @@ -4208,7 +4208,7 @@ __metadata: languageName: node linkType: hard -"@grafana/ui@9.0.0-beta.1, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": +"@grafana/ui@9.0.0-beta.2, @grafana/ui@workspace:*, @grafana/ui@workspace:packages/grafana-ui": version: 0.0.0-use.local resolution: "@grafana/ui@workspace:packages/grafana-ui" dependencies: @@ -4216,9 +4216,9 @@ __metadata: "@emotion/css": 11.9.0 "@emotion/react": 11.9.0 "@grafana/aws-sdk": 0.0.36 - "@grafana/data": 9.0.0-beta.1 - "@grafana/e2e-selectors": 9.0.0-beta.1 - "@grafana/schema": 9.0.0-beta.1 + "@grafana/data": 9.0.0-beta.2 + "@grafana/e2e-selectors": 9.0.0-beta.2 + "@grafana/schema": 9.0.0-beta.2 "@grafana/slate-react": 0.22.10-grafana "@grafana/tsconfig": ^1.2.0-rc1 "@mdx-js/react": 1.6.22 @@ -4454,10 +4454,10 @@ __metadata: resolution: "@jaegertracing/jaeger-ui-components@workspace:packages/jaeger-ui-components" dependencies: "@emotion/css": 11.9.0 - "@grafana/data": 9.0.0-beta.1 - "@grafana/e2e-selectors": 9.0.0-beta.1 + "@grafana/data": 9.0.0-beta.2 + "@grafana/e2e-selectors": 9.0.0-beta.2 "@grafana/tsconfig": ^1.2.0-rc1 - "@grafana/ui": 9.0.0-beta.1 + "@grafana/ui": 9.0.0-beta.2 "@testing-library/react": 12.1.4 "@testing-library/user-event": 14.2.0 "@types/classnames": ^2.2.7 From cc536ea6e9e1211999b1472a49854e713948fd79 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Tue, 31 May 2022 16:46:53 -0300 Subject: [PATCH 12/95] ReleaseNotes: Updated changelog and release notes for 9.0.0-beta2 (#49960) (#49963) (cherry picked from commit 1e4ebf876b8dc58a87f837c5f98a5bf7d72af1fd) Co-authored-by: Grot (@grafanabot) <43478413+grafanabot@users.noreply.github.com> --- CHANGELOG.md | 228 ++++++++++++++++++ docs/sources/release-notes/_index.md | 3 + .../release-notes-9-0-0-beta2.md | 135 +++++++++++ 3 files changed, 366 insertions(+) create mode 100644 docs/sources/release-notes/release-notes-9-0-0-beta2.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bf240475032..d8926a3aede 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,231 @@ + + +# 9.0.0-beta2 (2022-05-31) + +### Features and enhancements + +- **Alerting:** Add legacy indicator to navbar. [#49511](https://github.com/grafana/grafana/pull/49511), [@peterholmberg](https://github.com/peterholmberg) +- **Alerting:** Add templated subject config to email notifier. [#49742](https://github.com/grafana/grafana/pull/49742), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Enable Unified Alerting for open source and enterprise. [#49834](https://github.com/grafana/grafana/pull/49834), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** Make alertmanager datasource stable. [#49485](https://github.com/grafana/grafana/pull/49485), [@gillesdemey](https://github.com/gillesdemey) +- **Angular:** Remove deprecated angular modal support and libs. [#49781](https://github.com/grafana/grafana/pull/49781), [@torkelo](https://github.com/torkelo) +- **AuthProxy:** Remove deprecated ldap_sync_ttl setting. [#49902](https://github.com/grafana/grafana/pull/49902), [@kalleep](https://github.com/kalleep) +- **Build:** Enable long term caching for frontend assets. [#47625](https://github.com/grafana/grafana/pull/47625), [@jackw](https://github.com/jackw) +- **Chore:** Remove deprecated TextDisplayOptions export. [#49705](https://github.com/grafana/grafana/pull/49705), [@kaydelaney](https://github.com/kaydelaney) +- **Chore:** Remove deprecated `surface` prop from IconButton. [#49715](https://github.com/grafana/grafana/pull/49715), [@kaydelaney](https://github.com/kaydelaney) +- **Chore:** Remove usage of deprecated getColorForTheme function. [#49519](https://github.com/grafana/grafana/pull/49519), [@kaydelaney](https://github.com/kaydelaney) +- **DatePicker:** Add minDate prop. [#49503](https://github.com/grafana/grafana/pull/49503), [@alexanderzobnin](https://github.com/alexanderzobnin) +- **Notification history:** Enable by default. [#49502](https://github.com/grafana/grafana/pull/49502), [@ashharrison90](https://github.com/ashharrison90) +- **Prometheus:** Add pluginVersion to query. [#49414](https://github.com/grafana/grafana/pull/49414), [@toddtreece](https://github.com/toddtreece) +- **Prometheus:** Enable prometheusStreamingJSONParser by default. [#49475](https://github.com/grafana/grafana/pull/49475), [@toddtreece](https://github.com/toddtreece) +- **Prometheus:** Predefined scopes for Azure authentication. [#49557](https://github.com/grafana/grafana/pull/49557), [@kostrse](https://github.com/kostrse) +- **Prometheus:** Streaming JSON parser performance improvements. [#48792](https://github.com/grafana/grafana/pull/48792), [@toddtreece](https://github.com/toddtreece) +- **ValueMapping:** Add support for regex replacement over multiple lines. [#49607](https://github.com/grafana/grafana/pull/49607), [@ashharrison90](https://github.com/ashharrison90) + +### Bug fixes + +- **Accessibility:** Pressing escape in a Modal or DashboardSettings correctly closes the overlay. [#49500](https://github.com/grafana/grafana/pull/49500), [@ashharrison90](https://github.com/ashharrison90) +- **Alerting:** Validate alert notification UID length. [#45546](https://github.com/grafana/grafana/pull/45546), [@wbrowne](https://github.com/wbrowne) +- **BackendSrv:** Throw an error when fetching an invalid JSON. [#47493](https://github.com/grafana/grafana/pull/47493), [@leventebalogh](https://github.com/leventebalogh) +- **Fix:** Timeseries migration regex override. [#49629](https://github.com/grafana/grafana/pull/49629), [@zoltanbedi](https://github.com/zoltanbedi) +- **Loki:** Fix unwrap parsing in query builder. [#49732](https://github.com/grafana/grafana/pull/49732), [@ivanahuckova](https://github.com/ivanahuckova) +- **Navigation:** Position hamburger menu correctly in mobile view. [#49603](https://github.com/grafana/grafana/pull/49603), [@ashharrison90](https://github.com/ashharrison90) +- **PanelEditor:** Fixes issue with Table view and multi data frames. [#49854](https://github.com/grafana/grafana/pull/49854), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana) +- **Preferences:** Fix updating of preferences for Navbar and Query History. [#49677](https://github.com/grafana/grafana/pull/49677), [@ivanahuckova](https://github.com/ivanahuckova) +- **TimeRange:** Fixes issue when zooming out on a timerange with timespan 0. [#49622](https://github.com/grafana/grafana/pull/49622), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana) +- **Variables:** Fixes DS variables not being correctly used in panel queries. [#49323](https://github.com/grafana/grafana/pull/49323), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana) + +### Breaking changes + +Drop support for deprecated setting ldap_sync_ttl under [auth.proxy] +Only sync_ttl will work from now on Issue [#49902](https://github.com/grafana/grafana/issues/49902) + +Removes support for deprecated `heading` and `description` props. Moving forward, the `Card.Heading` and `Card.Description` components should be used. Issue [#49885](https://github.com/grafana/grafana/issues/49885) + +Removes the deprecated `link` variant from the `Button` component. +To migrate, replace any usage of `variant="link"` with `fill="text"`. Issue [#49843](https://github.com/grafana/grafana/issues/49843) + +Removes the deprecated `surface` prop from the `IconButton` component. This prop hasn't actually done anything for a while, so it should be safe to just remove any instances of its usage. +Issue [#49715](https://github.com/grafana/grafana/issues/49715) + +Removes the deprecated `TextDisplayOptions` export from `@grafana/data` in favor of `VizTextDisplayOptions` from `@grafana/schema`. To migrate, just replace usage of `TextDisplayOptions` with `VizTextDisplayOptions`. Issue [#49705](https://github.com/grafana/grafana/issues/49705) + +Removed support for the deprecated `getColorForTheme(color: string, theme: GrafanaTheme)` function in favor of the +`theme.visualization.getColorByName(color: string)` method. The output of this method is identical to the removed function, so migration should just be a matter of rewriting calls of `getColorForTheme(myColor, myTheme)` to `myTheme.visualization.getColorByName(myColor)`. +Issue [#49519](https://github.com/grafana/grafana/issues/49519) + +In the Prometheus data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Prometheus. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#49475](https://github.com/grafana/grafana/issues/49475) + +Plugins using custom Webpack configs could potentially break due to the changes between webpack@4 and webpack@5. Please refer to the [official migration guide](https://webpack.js.org/migrate/5/) for assistance. + +Webpack 5 does not include polyfills for node.js core modules by default (e.g. `buffer`, `stream`, `os`). This can result in failed builds for plugins. If polyfills are required it is recommended to create a custom webpack config in the root of the plugin repo and add the required fallbacks: + +```js +// webpack.config.js + +module.exports.getWebpackConfig = (config, options) => ({ + ...config, + resolve: { + ...config.resolve, + fallback: { + os: require.resolve('os-browserify/browser'), + stream: require.resolve('stream-browserify'), + timers: require.resolve('timers-browserify'), + }, + }, +}); +``` + +Please refer to the webpack build error messages or the [official migration guide](https://webpack.js.org/migrate/5/) for assistance with fallbacks. + +**Which issue(s) this PR fixes**: + + + +Fixes # + +**Special notes for your reviewer**: + +It does not bump the following dependencies to the very latest due to the latest versions being ES modules: + +- ora +- globby +- execa +- chalk + Issue [#47826](https://github.com/grafana/grafana/issues/47826) + +We have changed the internals of `backendSrv.fetch()` to throw an error when the response is an incorrect JSON. + +```javascript +// PREVIOUSLY: this was returning with an empty object {} - in case the response is an invalid JSON +return await getBackendSrv().post(`${API_ROOT}/${id}/install`); + +// AFTER THIS CHANGE: the following will throw an error - in case the response is an invalid JSON +return await getBackendSrv().post(`${API_ROOT}/${id}/install`); +``` + +**When is the response handled as JSON?** + +- If the response has the `"Content-Type: application/json"` header, OR +- If the backendSrv options ([`BackendSrvRequest`](https://github.com/grafana/grafana/blob/e237ff20a996c7313632b2e28f38032012f0e340/packages/grafana-runtime/src/services/backendSrv.ts#L8)) specify the response as JSON: `{ responseType: 'json' }` + +**How does it work after this change?** + +- In case it is recognised as a JSON response and the response is empty, it returns an empty object `{}` +- In case it is recognised as a JSON response and it has formatting errors, it throws an error + +**How to migrate?** +Make sure to handle possible errors on the callsite where using `backendSrv.fetch()` (or any other `backendSrv` methods). Issue [#47493](https://github.com/grafana/grafana/issues/47493) + +### Plugin development fixes & changes + +- **UI/Card:** Remove deprecated props. [#49885](https://github.com/grafana/grafana/pull/49885), [@kaydelaney](https://github.com/kaydelaney) +- **UI/Button:** Remove deprecated "link" variant. [#49843](https://github.com/grafana/grafana/pull/49843), [@kaydelaney](https://github.com/kaydelaney) +- **Toolkit:** Bump dependencies. [#47826](https://github.com/grafana/grafana/pull/47826), [@jackw](https://github.com/jackw) + + + + + +# 9.0.0-beta1 (2022-05-24) + +### Features and enhancements + +- **AccessControl:** Add setting for permission cache. (Enterprise) +- **AccessControl:** Check dashboard permissions for reports. (Enterprise) +- **Auth:** Remove grafana ui dependency to the aws sdk. [#43559](https://github.com/grafana/grafana/pull/43559), [@sunker](https://github.com/sunker) +- **BasicRoles:** Add API endpoint to reset basic roles permissions to factory. (Enterprise) +- **LDAP Mapping:** Allow Grafana Admin mapping without org role. [#37189](https://github.com/grafana/grafana/pull/37189), [@krzysdabro](https://github.com/krzysdabro) +- **Licensing:** Only enforce total number of users. (Enterprise) +- **Loki:** do not convert NaN to null. [#45389](https://github.com/grafana/grafana/pull/45389), [@gabor](https://github.com/gabor) +- **Report:** API support for multiple dashboards. (Enterprise) +- **Report:** Support sending embedded image in the report email. (Enterprise) +- **Report:** UI for multiple dashboards. (Enterprise) +- **Reporting:** Remove redundant empty attachment when export to CSV is enabled. (Enterprise) +- **SAML:** Implement Name Templates for assertion_attribute_name option. (Enterprise) +- **SSE/Alerting:** Support prom instant vector responses. [#44865](https://github.com/grafana/grafana/pull/44865), [@kylebrandt](https://github.com/kylebrandt) +- **Tracing:** Add trace to metrics config behind feature toggle. [#46298](https://github.com/grafana/grafana/pull/46298), [@connorlindsey](https://github.com/connorlindsey) + +### Bug fixes + +- **Fix:** Prevent automatic parsing of string data types to numbers. [#46035](https://github.com/grafana/grafana/pull/46035), [@joshhunt](https://github.com/joshhunt) +- **Prometheus:** Fix inconsistent labels in exemplars resulting in marshal json error. [#46135](https://github.com/grafana/grafana/pull/46135), [@hanjm](https://github.com/hanjm) + +### Breaking changes + +In the Loki data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Loki. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#45389](https://github.com/grafana/grafana/issues/45389) + +The dependency to [grafana/aws-sdk](https://github.com/grafana/grafana-aws-sdk-react) is moved from [grafana/ui](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/package.json) to the plugin. This means that any plugin that use SIGV4 auth need to pass a SIGV4 editor component as a prop to the `DataSourceHttpSettings` component. Issue [#43559](https://github.com/grafana/grafana/issues/43559) + + + + +# 8.5.4 (2022-05-30) + +### Features and enhancements + +- **Alerting:** Remove disabled flag for data source when migrating alerts. [#48559](https://github.com/grafana/grafana/pull/48559), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Show notification tab of legacy alerting only to editor. [#49624](https://github.com/grafana/grafana/pull/49624), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Update migration to migrate only alerts that belong to existing org\dashboard. [#49192](https://github.com/grafana/grafana/pull/49192), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **AzureMonitor:** Do not quote variables when a custom "All" variable option is used. [#49428](https://github.com/grafana/grafana/pull/49428), [@andresmgot](https://github.com/andresmgot) +- **AzureMonitor:** Update allowed namespaces. [#48468](https://github.com/grafana/grafana/pull/48468), [@jcolladokuri](https://github.com/jcolladokuri) +- **CloudMonitor:** Correctly encode default project response. [#49510](https://github.com/grafana/grafana/pull/49510), [@aangelisc](https://github.com/aangelisc) +- **Cloudwatch:** Add support for new AWS/RDS EBS\* metrics. [#48798](https://github.com/grafana/grafana/pull/48798), [@szymonpk](https://github.com/szymonpk) +- **InfluxDB:** Use backend for influxDB by default via feature toggle. [#48453](https://github.com/grafana/grafana/pull/48453), [@yesoreyeram](https://github.com/yesoreyeram) +- **Legend:** Use correct unit for percent and count calculations. [#49004](https://github.com/grafana/grafana/pull/49004), [@dprokop](https://github.com/dprokop) +- **LokI:** use millisecond steps in Grafana 8.5.x. [#48630](https://github.com/grafana/grafana/pull/48630), [@gabor](https://github.com/gabor) +- **Plugins:** Introduce HTTP 207 Multi Status response to api/ds/query. [#48550](https://github.com/grafana/grafana/pull/48550), [@wbrowne](https://github.com/wbrowne) +- **Reporting:** Improve PDF file size using grid layout. (Enterprise) +- **Transformations:** Add an All Unique Values Reducer. [#48653](https://github.com/grafana/grafana/pull/48653), [@josiahg](https://github.com/josiahg) +- **Transformers:** avoid error when the ExtractFields source field is missing. [#49368](https://github.com/grafana/grafana/pull/49368), [@wardbekker](https://github.com/wardbekker) +- **[v8.5.x] Alerting:** Update migration to migrate only alerts that belong to existing org\dashboard. [#49199](https://github.com/grafana/grafana/pull/49199), [@grafanabot](https://github.com/grafanabot) +- **[v8.5.x] Reporting:** Improve PDF file size using grid layout. (Enterprise) + +### Bug fixes + +- **Alerting:** Allow disabling override timings for notification policies. [#48648](https://github.com/grafana/grafana/pull/48648), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Allow serving images from custom url path. [#49022](https://github.com/grafana/grafana/pull/49022), [@gillesdemey](https://github.com/gillesdemey) +- **Alerting:** Apply Custom Headers to datasource queries. [#47860](https://github.com/grafana/grafana/pull/47860), [@joeblubaugh](https://github.com/joeblubaugh) +- **Alerting:** Fix RBAC actions for notification policies. [#49185](https://github.com/grafana/grafana/pull/49185), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Fix access to alerts for viewer with editor permissions when RBAC is disabled. [#49270](https://github.com/grafana/grafana/pull/49270), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** Fix anonymous access to alerting. [#49203](https://github.com/grafana/grafana/pull/49203), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **Alerting:** correctly show all alerts in a folder. [#48684](https://github.com/grafana/grafana/pull/48684), [@gillesdemey](https://github.com/gillesdemey) +- **AzureMonitor:** Fixes metric definition for Azure Storage queue/file/blob/table resources. [#49101](https://github.com/grafana/grafana/pull/49101), [@aangelisc](https://github.com/aangelisc) +- **Dashboard:** Fix dashboard update permission check. [#48746](https://github.com/grafana/grafana/pull/48746), [@IevaVasiljeva](https://github.com/IevaVasiljeva) +- **DashboardExport:** Fix exporting and importing dashboards where query data source ended up as incorrect. [#48410](https://github.com/grafana/grafana/pull/48410), [@torkelo](https://github.com/torkelo) +- **FileUpload:** clicking the `Upload file` button now opens the modal correctly. [#48766](https://github.com/grafana/grafana/pull/48766), [@ashharrison90](https://github.com/ashharrison90) +- **GrafanaUI:** Fix color of links in error Tooltips in light theme. [#49327](https://github.com/grafana/grafana/pull/49327), [@joshhunt](https://github.com/joshhunt) +- **LibraryPanels:** Fix library panels not connecting properly in imported dashboards. [#49161](https://github.com/grafana/grafana/pull/49161), [@joshhunt](https://github.com/joshhunt) +- **Loki:** Improve unpack parser handling. [#49074](https://github.com/grafana/grafana/pull/49074), [@gabor](https://github.com/gabor) +- **RolePicker:** Fix menu position on smaller screens. [#48429](https://github.com/grafana/grafana/pull/48429), [@Clarity-89](https://github.com/Clarity-89) +- **TimeRange:** Fixes updating time range from url and browser history. [#48657](https://github.com/grafana/grafana/pull/48657), [@torkelo](https://github.com/torkelo) +- **TimeSeries:** Fix detection & rendering of sparse datapoints. [#48841](https://github.com/grafana/grafana/pull/48841), [@leeoniya](https://github.com/leeoniya) +- **Timeseries:** Fix outside range stale state. [#49633](https://github.com/grafana/grafana/pull/49633), [@ryantxu](https://github.com/ryantxu) +- **Tooltip:** Fix links not legible in Tooltips when using light theme. [#48748](https://github.com/grafana/grafana/pull/48748), [@joshhunt](https://github.com/joshhunt) +- **Tooltip:** Sort decimals using standard numeric compare. [#49084](https://github.com/grafana/grafana/pull/49084), [@dprokop](https://github.com/dprokop) +- **Transforms:** Labels to fields, fix label picker layout. [#49304](https://github.com/grafana/grafana/pull/49304), [@torkelo](https://github.com/torkelo) +- **Variables:** Fixes issue with data source variables not updating queries with variable. [#49478](https://github.com/grafana/grafana/pull/49478), [@torkelo](https://github.com/torkelo) +- **[v8.5.x] Alerting:** Fix RBAC actions for notification policies (#49185). [#49348](https://github.com/grafana/grafana/pull/49348), [@yuri-tceretian](https://github.com/yuri-tceretian) +- **[v8.5.x] Alerting:** Fix access to alerts for viewer with editor permissions when RBAC is disabled. [#49427](https://github.com/grafana/grafana/pull/49427), [@konrad147](https://github.com/konrad147) +- **[v8.5.x] Alerting:** Fix anonymous access to alerting. [#49268](https://github.com/grafana/grafana/pull/49268), [@yuri-tceretian](https://github.com/yuri-tceretian) + +### Breaking changes + +For a data source query made via /api/ds/query : + +- If the `DatasourceQueryMultiStatus` feature is enabled and + - The data source response has an error set as part of the `DataResponse`, the resulting HTTP status code is now `207 Multi Status` instead of `400 Bad gateway` +- If the `DatasourceQueryMultiStatus` feature is **not** enabled and + - The data source response has an error set as part of the `DataResponse`, the resulting HTTP status code is `400 Bad Request` (no breaking change) + --> Issue [#48550](https://github.com/grafana/grafana/issues/48550) + + # 8.5.3 diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index e3ee8dc7b04..7ff30fb5e4f 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -10,6 +10,9 @@ weight: 10000 Here you can find detailed release notes that list everything that is included in every release as well as notices about deprecations, breaking changes as well as changes that relate to plugin development. +- [Release notes for 9.0.0-beta2]({{< relref "release-notes-9-0-0-beta2" >}}) +- [Release notes for 9.0.0-beta1]({{< relref "release-notes-9-0-0-beta1/" >}}) +- [Release notes for 8.5.4]({{< relref "release-notes-8-5-4" >}}) - [Release notes for 8.5.3]({{< relref "release-notes-8-5-3/" >}}) - [Release notes for 8.5.2]({{< relref "release-notes-8-5-2/" >}}) - [Release notes for 8.5.1]({{< relref "release-notes-8-5-1/" >}}) diff --git a/docs/sources/release-notes/release-notes-9-0-0-beta2.md b/docs/sources/release-notes/release-notes-9-0-0-beta2.md new file mode 100644 index 00000000000..44e9e6b1080 --- /dev/null +++ b/docs/sources/release-notes/release-notes-9-0-0-beta2.md @@ -0,0 +1,135 @@ ++++ +title = "Release notes for Grafana 9.0.0-beta2" +hide_menu = true ++++ + + + +# Release notes for Grafana 9.0.0-beta2 + +### Features and enhancements + +- **Alerting:** Add legacy indicator to navbar. [#49511](https://github.com/grafana/grafana/pull/49511), [@peterholmberg](https://github.com/peterholmberg) +- **Alerting:** Add templated subject config to email notifier. [#49742](https://github.com/grafana/grafana/pull/49742), [@JacobsonMT](https://github.com/JacobsonMT) +- **Alerting:** Enable Unified Alerting for open source and enterprise. [#49834](https://github.com/grafana/grafana/pull/49834), [@grobinson-grafana](https://github.com/grobinson-grafana) +- **Alerting:** Make alertmanager datasource stable. [#49485](https://github.com/grafana/grafana/pull/49485), [@gillesdemey](https://github.com/gillesdemey) +- **Angular:** Remove deprecated angular modal support and libs. [#49781](https://github.com/grafana/grafana/pull/49781), [@torkelo](https://github.com/torkelo) +- **AuthProxy:** Remove deprecated ldap_sync_ttl setting. [#49902](https://github.com/grafana/grafana/pull/49902), [@kalleep](https://github.com/kalleep) +- **Build:** Enable long term caching for frontend assets. [#47625](https://github.com/grafana/grafana/pull/47625), [@jackw](https://github.com/jackw) +- **Chore:** Remove deprecated TextDisplayOptions export. [#49705](https://github.com/grafana/grafana/pull/49705), [@kaydelaney](https://github.com/kaydelaney) +- **Chore:** Remove deprecated `surface` prop from IconButton. [#49715](https://github.com/grafana/grafana/pull/49715), [@kaydelaney](https://github.com/kaydelaney) +- **Chore:** Remove usage of deprecated getColorForTheme function. [#49519](https://github.com/grafana/grafana/pull/49519), [@kaydelaney](https://github.com/kaydelaney) +- **DatePicker:** Add minDate prop. [#49503](https://github.com/grafana/grafana/pull/49503), [@alexanderzobnin](https://github.com/alexanderzobnin) +- **Notification history:** Enable by default. [#49502](https://github.com/grafana/grafana/pull/49502), [@ashharrison90](https://github.com/ashharrison90) +- **Prometheus:** Add pluginVersion to query. [#49414](https://github.com/grafana/grafana/pull/49414), [@toddtreece](https://github.com/toddtreece) +- **Prometheus:** Enable prometheusStreamingJSONParser by default. [#49475](https://github.com/grafana/grafana/pull/49475), [@toddtreece](https://github.com/toddtreece) +- **Prometheus:** Predefined scopes for Azure authentication. [#49557](https://github.com/grafana/grafana/pull/49557), [@kostrse](https://github.com/kostrse) +- **Prometheus:** Streaming JSON parser performance improvements. [#48792](https://github.com/grafana/grafana/pull/48792), [@toddtreece](https://github.com/toddtreece) +- **ValueMapping:** Add support for regex replacement over multiple lines. [#49607](https://github.com/grafana/grafana/pull/49607), [@ashharrison90](https://github.com/ashharrison90) + +### Bug fixes + +- **Accessibility:** Pressing escape in a Modal or DashboardSettings correctly closes the overlay. [#49500](https://github.com/grafana/grafana/pull/49500), [@ashharrison90](https://github.com/ashharrison90) +- **Alerting:** Validate alert notification UID length. [#45546](https://github.com/grafana/grafana/pull/45546), [@wbrowne](https://github.com/wbrowne) +- **BackendSrv:** Throw an error when fetching an invalid JSON. [#47493](https://github.com/grafana/grafana/pull/47493), [@leventebalogh](https://github.com/leventebalogh) +- **Fix:** Timeseries migration regex override. [#49629](https://github.com/grafana/grafana/pull/49629), [@zoltanbedi](https://github.com/zoltanbedi) +- **Loki:** Fix unwrap parsing in query builder. [#49732](https://github.com/grafana/grafana/pull/49732), [@ivanahuckova](https://github.com/ivanahuckova) +- **Navigation:** Position hamburger menu correctly in mobile view. [#49603](https://github.com/grafana/grafana/pull/49603), [@ashharrison90](https://github.com/ashharrison90) +- **PanelEditor:** Fixes issue with Table view and multi data frames. [#49854](https://github.com/grafana/grafana/pull/49854), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana) +- **Preferences:** Fix updating of preferences for Navbar and Query History. [#49677](https://github.com/grafana/grafana/pull/49677), [@ivanahuckova](https://github.com/ivanahuckova) +- **TimeRange:** Fixes issue when zooming out on a timerange with timespan 0. [#49622](https://github.com/grafana/grafana/pull/49622), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana) +- **Variables:** Fixes DS variables not being correctly used in panel queries. [#49323](https://github.com/grafana/grafana/pull/49323), [@JoaoSilvaGrafana](https://github.com/JoaoSilvaGrafana) + +### Breaking changes + +Drop support for deprecated setting ldap_sync_ttl under [auth.proxy] +Only sync_ttl will work from now on Issue [#49902](https://github.com/grafana/grafana/issues/49902) + +Removes support for deprecated `heading` and `description` props. Moving forward, the `Card.Heading` and `Card.Description` components should be used. Issue [#49885](https://github.com/grafana/grafana/issues/49885) + +Removes the deprecated `link` variant from the `Button` component. +To migrate, replace any usage of `variant="link"` with `fill="text"`. Issue [#49843](https://github.com/grafana/grafana/issues/49843) + +Removes the deprecated `surface` prop from the `IconButton` component. This prop hasn't actually done anything for a while, so it should be safe to just remove any instances of its usage. +Issue [#49715](https://github.com/grafana/grafana/issues/49715) + +Removes the deprecated `TextDisplayOptions` export from `@grafana/data` in favor of `VizTextDisplayOptions` from `@grafana/schema`. To migrate, just replace usage of `TextDisplayOptions` with `VizTextDisplayOptions`. Issue [#49705](https://github.com/grafana/grafana/issues/49705) + +Removed support for the deprecated `getColorForTheme(color: string, theme: GrafanaTheme)` function in favor of the +`theme.visualization.getColorByName(color: string)` method. The output of this method is identical to the removed function, so migration should just be a matter of rewriting calls of `getColorForTheme(myColor, myTheme)` to `myTheme.visualization.getColorByName(myColor)`. +Issue [#49519](https://github.com/grafana/grafana/issues/49519) + +In the Prometheus data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Prometheus. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#49475](https://github.com/grafana/grafana/issues/49475) + +Plugins using custom Webpack configs could potentially break due to the changes between webpack@4 and webpack@5. Please refer to the [official migration guide](https://webpack.js.org/migrate/5/) for assistance. + +Webpack 5 does not include polyfills for node.js core modules by default (e.g. `buffer`, `stream`, `os`). This can result in failed builds for plugins. If polyfills are required it is recommended to create a custom webpack config in the root of the plugin repo and add the required fallbacks: + +```js +// webpack.config.js + +module.exports.getWebpackConfig = (config, options) => ({ + ...config, + resolve: { + ...config.resolve, + fallback: { + os: require.resolve('os-browserify/browser'), + stream: require.resolve('stream-browserify'), + timers: require.resolve('timers-browserify'), + }, + }, +}); +``` + +Please refer to the webpack build error messages or the [official migration guide](https://webpack.js.org/migrate/5/) for assistance with fallbacks. + +**Which issue(s) this PR fixes**: + + + +Fixes # + +**Special notes for your reviewer**: + +It does not bump the following dependencies to the very latest due to the latest versions being ES modules: + +- ora +- globby +- execa +- chalk + Issue [#47826](https://github.com/grafana/grafana/issues/47826) + +We have changed the internals of `backendSrv.fetch()` to throw an error when the response is an incorrect JSON. + +```javascript +// PREVIOUSLY: this was returning with an empty object {} - in case the response is an invalid JSON +return await getBackendSrv().post(`${API_ROOT}/${id}/install`); + +// AFTER THIS CHANGE: the following will throw an error - in case the response is an invalid JSON +return await getBackendSrv().post(`${API_ROOT}/${id}/install`); +``` + +**When is the response handled as JSON?** + +- If the response has the `"Content-Type: application/json"` header, OR +- If the backendSrv options ([`BackendSrvRequest`](https://github.com/grafana/grafana/blob/e237ff20a996c7313632b2e28f38032012f0e340/packages/grafana-runtime/src/services/backendSrv.ts#L8)) specify the response as JSON: `{ responseType: 'json' }` + +**How does it work after this change?** + +- In case it is recognised as a JSON response and the response is empty, it returns an empty object `{}` +- In case it is recognised as a JSON response and it has formatting errors, it throws an error + +**How to migrate?** +Make sure to handle possible errors on the callsite where using `backendSrv.fetch()` (or any other `backendSrv` methods). Issue [#47493](https://github.com/grafana/grafana/issues/47493) + +### Plugin development fixes & changes + +- **UI/Card:** Remove deprecated props. [#49885](https://github.com/grafana/grafana/pull/49885), [@kaydelaney](https://github.com/kaydelaney) +- **UI/Button:** Remove deprecated "link" variant. [#49843](https://github.com/grafana/grafana/pull/49843), [@kaydelaney](https://github.com/kaydelaney) +- **Toolkit:** Bump dependencies. [#47826](https://github.com/grafana/grafana/pull/47826), [@jackw](https://github.com/jackw) From 849a0932e81addaf8c0702fbb2bf0bcfbb877f2c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 22:14:56 -0400 Subject: [PATCH 13/95] Alerting: Add templated subject config to email notifier (#49742) (#49846) * Add subject templating to email notifier * Fix linting (cherry picked from commit d92625125bf0cbb4c51158cf7dcdd40efac49dfc) Co-authored-by: Matthew Jacobson --- .../ngalert/notifier/available_channels.go | 8 +++++++ .../ngalert/notifier/channels/email.go | 10 +++++--- .../ngalert/notifier/channels/email_test.go | 24 +++++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/pkg/services/ngalert/notifier/available_channels.go b/pkg/services/ngalert/notifier/available_channels.go index 7c197f68a72..1ba56d6dbcd 100644 --- a/pkg/services/ngalert/notifier/available_channels.go +++ b/pkg/services/ngalert/notifier/available_channels.go @@ -190,6 +190,14 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { Element: alerting.ElementTypeTextArea, PropertyName: "message", }, + { // New in 9.0. + Label: "Subject", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Description: "Templated subject of the email", + PropertyName: "subject", + Placeholder: `{{ template "default.title" . }}`, + }, }, }, { diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index bbbc6de13e0..db3861f781c 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -24,6 +24,7 @@ type EmailNotifier struct { Addresses []string SingleEmail bool Message string + Subject string log log.Logger ns notifications.EmailSender images ImageStore @@ -35,6 +36,7 @@ type EmailConfig struct { SingleEmail bool Addresses []string Message string + Subject string } func EmailFactory(fc FactoryConfig) (NotificationChannel, error) { @@ -59,6 +61,7 @@ func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) { NotificationChannelConfig: config, SingleEmail: config.Settings.Get("singleEmail").MustBool(false), Message: config.Settings.Get("message").MustString(), + Subject: config.Settings.Get("subject").MustString(DefaultMessageTitleEmbed), Addresses: addresses, }, nil } @@ -77,6 +80,7 @@ func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, images Addresses: config.Addresses, SingleEmail: config.SingleEmail, Message: config.Message, + Subject: config.Subject, log: log.New("alerting.notifier.email"), ns: ns, images: images, @@ -89,7 +93,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, var tmplErr error tmpl, data := TmplText(ctx, en.tmpl, as, en.log, &tmplErr) - title := tmpl(DefaultMessageTitleEmbed) + subject := tmpl(en.Subject) alertPageURL := en.tmpl.ExternalURL.String() ruleURL := en.tmpl.ExternalURL.String() @@ -106,9 +110,9 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, cmd := &models.SendEmailCommandSync{ SendEmailCommand: models.SendEmailCommand{ - Subject: title, + Subject: subject, Data: map[string]interface{}{ - "Title": title, + "Title": subject, "Message": tmpl(en.Message), "Status": data.Status, "Alerts": data.Alerts, diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 35954447f7e..7761129cb67 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -117,6 +117,7 @@ func TestEmailNotifierIntegration(t *testing.T) { name string alerts []*types.Alert messageTmpl string + subjectTmpl string expSubject string expSnippets []string }{ @@ -220,11 +221,25 @@ func TestEmailNotifierIntegration(t *testing.T) { "<li>Firing: AlwaysFiring at warning </li>", }, }, + { + name: "single alert with templated subject", + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "AlwaysFiring", "severity": "warning"}, + Annotations: model.LabelSet{"runbook_url": "http://fix.me", "__dashboardUid__": "abc", "__panelId__": "5"}, + }, + }, + }, + subjectTmpl: `This notification is {{ .Status }}!`, + expSubject: "This notification is firing!", + expSnippets: []string{}, + }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { - emailNotifier := createSut(t, c.messageTmpl, emailTmpl, ns) + emailNotifier := createSut(t, c.messageTmpl, c.subjectTmpl, emailTmpl, ns) ok, err := emailNotifier.Notify(context.Background(), c.alerts...) require.NoError(t, err) @@ -271,7 +286,7 @@ func createCoreEmailService(t *testing.T) *notifications.NotificationService { return ns } -func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, ns notifications.EmailSender) *EmailNotifier { +func createSut(t *testing.T, messageTmpl string, subjectTmpl string, emailTmpl *template.Template, ns notifications.EmailSender) *EmailNotifier { t.Helper() json := `{ @@ -282,6 +297,11 @@ func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, n if messageTmpl != "" { settingsJSON.Set("message", messageTmpl) } + + if subjectTmpl != "" { + settingsJSON.Set("subject", subjectTmpl) + } + require.NoError(t, err) cfg, err := NewEmailConfig(&NotificationChannelConfig{ Name: "ops", From 5c19011dc14a2207a7b2b92db54e285a3ac2099a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Tue, 31 May 2022 23:10:03 -0400 Subject: [PATCH 14/95] Alerting: Add GetImages to ImageStore (#49717) (#49791) GetImages does a `TOKEN IN` query for each token in the argument. (cherry picked from commit 47a3ddd968eda8d2cd09edd306cbb78ab8966590) Co-authored-by: George Robinson --- pkg/services/ngalert/notifier/testing.go | 4 ++ pkg/services/ngalert/store/image.go | 21 +++++++++- pkg/services/ngalert/store/image_test.go | 50 ++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/notifier/testing.go b/pkg/services/ngalert/notifier/testing.go index a1fc49a03e5..a7cd451b11c 100644 --- a/pkg/services/ngalert/notifier/testing.go +++ b/pkg/services/ngalert/notifier/testing.go @@ -27,6 +27,10 @@ func (f *FakeConfigStore) GetImage(ctx context.Context, token string) (*models.I return nil, models.ErrImageNotFound } +func (f *FakeConfigStore) GetImages(ctx context.Context, tokens []string) ([]models.Image, error) { + return nil, models.ErrImageNotFound +} + func NewFakeConfigStore(t *testing.T, configs map[int64]*models.AlertConfiguration) FakeConfigStore { t.Helper() diff --git a/pkg/services/ngalert/store/image.go b/pkg/services/ngalert/store/image.go index 4d0f94b0ee0..3f707cd463e 100644 --- a/pkg/services/ngalert/store/image.go +++ b/pkg/services/ngalert/store/image.go @@ -12,10 +12,14 @@ import ( ) type ImageStore interface { - // Get returns the image with the token or ErrImageNotFound. + // GetImage returns the image with the token or ErrImageNotFound. GetImage(ctx context.Context, token string) (*models.Image, error) - // Saves the image or returns an error. + // GetImages returns all images that match the tokens. If one or more + // tokens does not exist then it also returns ErrImageNotFound. + GetImages(ctx context.Context, tokens []string) ([]models.Image, error) + + // SaveImage saves the image or returns an error. SaveImage(ctx context.Context, img *models.Image) error } @@ -36,6 +40,19 @@ func (st DBstore) GetImage(ctx context.Context, token string) (*models.Image, er return &img, nil } +func (st DBstore) GetImages(ctx context.Context, tokens []string) ([]models.Image, error) { + var imgs []models.Image + if err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + return sess.In("token", tokens).Find(&imgs) + }); err != nil { + return nil, err + } + if len(imgs) < len(tokens) { + return imgs, models.ErrImageNotFound + } + return imgs, nil +} + func (st DBstore) SaveImage(ctx context.Context, img *models.Image) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { // TODO: Is this a good idea? Do we actually want to automatically expire diff --git a/pkg/services/ngalert/store/image_test.go b/pkg/services/ngalert/store/image_test.go index bd607a9137f..52d6772c6c8 100644 --- a/pkg/services/ngalert/store/image_test.go +++ b/pkg/services/ngalert/store/image_test.go @@ -93,6 +93,56 @@ func TestIntegrationSaveAndGetImage(t *testing.T) { } } +func TestIntegrationGetImages(t *testing.T) { + mockTimeNow() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, dbstore := tests.SetupTestEnv(t, baseIntervalSeconds) + + // create an image foo.png + img1 := models.Image{Path: "foo.png"} + require.NoError(t, dbstore.SaveImage(ctx, &img1)) + + // GetImages should return the first image + imgs, err := dbstore.GetImages(ctx, []string{img1.Token}) + require.NoError(t, err) + assert.Equal(t, []models.Image{img1}, imgs) + + // create another image bar.png + img2 := models.Image{Path: "bar.png"} + require.NoError(t, dbstore.SaveImage(ctx, &img2)) + + // GetImages should return both images + imgs, err = dbstore.GetImages(ctx, []string{img1.Token, img2.Token}) + require.NoError(t, err) + assert.ElementsMatch(t, []models.Image{img1, img2}, imgs) + + // GetImages should return the first image + imgs, err = dbstore.GetImages(ctx, []string{img1.Token}) + require.NoError(t, err) + assert.Equal(t, []models.Image{img1}, imgs) + + // GetImages should return the second image + imgs, err = dbstore.GetImages(ctx, []string{img2.Token}) + require.NoError(t, err) + assert.Equal(t, []models.Image{img2}, imgs) + + // GetImages should return the first image and an error + imgs, err = dbstore.GetImages(ctx, []string{img1.Token, "unknown"}) + assert.EqualError(t, err, "image not found") + assert.Equal(t, []models.Image{img1}, imgs) + + // GetImages should return no images for no tokens + imgs, err = dbstore.GetImages(ctx, []string{}) + require.NoError(t, err) + assert.Len(t, imgs, 0) + + // GetImages should return no images for nil tokens + imgs, err = dbstore.GetImages(ctx, nil) + require.NoError(t, err) + assert.Len(t, imgs, 0) +} + func TestIntegrationDeleteExpiredImages(t *testing.T) { mockTimeNow() ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) From a51f51f7a86e4bacba71f3ad80880546d59ddffa Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 02:29:13 -0400 Subject: [PATCH 15/95] grafana/ui: Move panel-container styles to component (#49566) (#49915) * Chore: transfer styling to Explore.tsx and convert to Emotion * feat: create a component and export it * Chore: replace by new component * Chore: replace by new component * Feat: create a story * Chore: clean up * Chore: clean up (cherry picked from commit 94375592c88632f0976d52203a89f90d10dc5c9f) Co-authored-by: Laura <48948963+L-M-K-B@users.noreply.github.com> --- .../PanelContainer/PanelContainer.mdx | 8 ++++++ .../PanelContainer/PanelContainer.story.tsx | 26 +++++++++++++++++++ .../PanelContainer/PanelContainer.tsx | 24 +++++++++++++++++ packages/grafana-ui/src/components/index.ts | 1 + public/app/features/explore/Explore.tsx | 6 ++--- public/app/features/explore/NoData.tsx | 10 +++---- 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 packages/grafana-ui/src/components/PanelContainer/PanelContainer.mdx create mode 100644 packages/grafana-ui/src/components/PanelContainer/PanelContainer.story.tsx create mode 100644 packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx diff --git a/packages/grafana-ui/src/components/PanelContainer/PanelContainer.mdx b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.mdx new file mode 100644 index 00000000000..29c6eeca90e --- /dev/null +++ b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.mdx @@ -0,0 +1,8 @@ +import { Meta, Preview, Props } from '@storybook/addon-docs/blocks'; +import { PanelContainer } from './PanelContainer'; + + + +# PanelContainer + +The PanelContainer is used as a simple background for storing other components. diff --git a/packages/grafana-ui/src/components/PanelContainer/PanelContainer.story.tsx b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.story.tsx new file mode 100644 index 00000000000..4ace28bd6b0 --- /dev/null +++ b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.story.tsx @@ -0,0 +1,26 @@ +import { Meta, Story } from '@storybook/react'; +import React from 'react'; + +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; + +import { PanelContainer } from './PanelContainer'; +import mdx from './PanelContainer.mdx'; + +export default { + title: 'General/PanelContainer', + component: PanelContainer, + decorators: [withCenteredStory], + parameters: { + docs: { + page: mdx, + }, + }, +} as Meta; + +export const Basic: Story = () => { + return ( + +

Here could be your component

+
+ ); +}; diff --git a/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx new file mode 100644 index 00000000000..d39725ec91a --- /dev/null +++ b/packages/grafana-ui/src/components/PanelContainer/PanelContainer.tsx @@ -0,0 +1,24 @@ +import { css, cx } from '@emotion/css'; +import React, { DetailedHTMLProps, HTMLAttributes } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../themes'; + +type Props = DetailedHTMLProps, HTMLDivElement>; + +export const PanelContainer = ({ children, className, ...props }: Props) => { + const styles = useStyles2(getStyles); + return ( +
+ {children} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => + css` + background-color: ${theme.components.panel.background}; + border: 1px solid ${theme.components.panel.borderColor}; + border-radius: 3px; + `; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 4c32405d854..6545f93dbee 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -269,3 +269,4 @@ export { GraphNGLegendEvent } from './GraphNG/types'; export * from './PanelChrome/types'; export { EmotionPerfTest } from './ThemeDemos/EmotionPerfTest'; export { Label as BrowserLabel } from './BrowserLabel/Label'; +export { PanelContainer } from './PanelContainer/PanelContainer'; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 0a25babfd79..3dbe10a8c19 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -8,7 +8,7 @@ import { Unsubscribable } from 'rxjs'; import { AbsoluteTimeRange, DataQuery, GrafanaTheme2, LoadingState, RawTimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2 } from '@grafana/ui'; +import { Collapse, CustomScrollbar, ErrorBoundaryAlert, Themeable2, withTheme2, PanelContainer } from '@grafana/ui'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, FilterItem } from '@grafana/ui/src/components/Table/types'; import appEvents from 'app/core/app_events'; import { getNodeGraphDataFrames } from 'app/plugins/panel/nodeGraph/utils'; @@ -368,7 +368,7 @@ export class Explore extends React.PureComponent { {datasourceMissing ? this.renderEmptyState(styles.exploreContainer) : null} {datasourceInstance && (
-
+ { onClickQueryInspectorButton={this.toggleShowQueryInspector} /> -
+ {({ width }) => { if (width === 0) { diff --git a/public/app/features/explore/NoData.tsx b/public/app/features/explore/NoData.tsx index 791505c6a57..a8c73dd2c2c 100644 --- a/public/app/features/explore/NoData.tsx +++ b/public/app/features/explore/NoData.tsx @@ -1,16 +1,16 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data/src'; -import { useStyles2 } from '@grafana/ui'; +import { useStyles2, PanelContainer } from '@grafana/ui'; export const NoData = () => { const css = useStyles2(getStyles); return ( <> -
- {'No data'} -
+ + {'No data'} + ); }; From c14d7aa4a3752957eae762dd5e5e7ec6798b63f1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 05:13:12 -0400 Subject: [PATCH 16/95] UI: Remove deprecated getFormStyles function (#49945) (#49981) (cherry picked from commit 05e501c64195711599c1accb669f6ecc823159fb) Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> --- .../src/components/Forms/getFormStyles.ts | 31 ------------------- packages/grafana-ui/src/components/index.ts | 1 - 2 files changed, 32 deletions(-) delete mode 100644 packages/grafana-ui/src/components/Forms/getFormStyles.ts diff --git a/packages/grafana-ui/src/components/Forms/getFormStyles.ts b/packages/grafana-ui/src/components/Forms/getFormStyles.ts deleted file mode 100644 index a730ab50efd..00000000000 --- a/packages/grafana-ui/src/components/Forms/getFormStyles.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { GrafanaTheme2 } from '@grafana/data'; - -import { stylesFactory } from '../../themes'; -import { ComponentSize } from '../../types/size'; -import { getButtonStyles, ButtonVariant } from '../Button'; -import { getInputStyles } from '../Input/Input'; - -import { getCheckboxStyles } from './Checkbox'; -import { getFieldValidationMessageStyles } from './FieldValidationMessage'; -import { getLabelStyles } from './Label'; -import { getLegendStyles } from './Legend'; - -/** @deprecated */ -export const getFormStyles = stylesFactory( - (theme: GrafanaTheme2, options: { variant: ButtonVariant; size: ComponentSize; invalid: boolean }) => { - console.warn('getFormStyles is deprecated'); - - return { - label: getLabelStyles(theme), - legend: getLegendStyles(theme.v1), - fieldValidationMessage: getFieldValidationMessageStyles(theme), - button: getButtonStyles({ - theme, - variant: options.variant, - size: options.size, - }), - input: getInputStyles({ theme, invalid: options.invalid }), - checkbox: getCheckboxStyles(theme), - }; - } -); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 6545f93dbee..2e089ad834e 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -190,7 +190,6 @@ export { InputControl } from './InputControl'; export { Button, LinkButton, ButtonVariant, ToolbarButton, ButtonGroup, ToolbarButtonRow, ButtonProps } from './Button'; export { ValuePicker } from './ValuePicker/ValuePicker'; export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI'; -export { getFormStyles } from './Forms/getFormStyles'; export { Link } from './Link/Link'; export { Label } from './Forms/Label'; From f5ede594f4db6d99d553842d23e93dd695eab8c1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 06:07:29 -0400 Subject: [PATCH 17/95] Alerting: do not overwrite existing alert rule condition (#49920) (#49984) (cherry picked from commit 82e9f4e7e7fb7aa1d55638c12ad9c29378a3b5e3) Co-authored-by: Gilles De Mey --- .../rule-editor/ConditionField.test.tsx | 39 +++++++++++++++++++ .../components/rule-editor/ConditionField.tsx | 10 +++-- .../QueryAndAlertConditionStep.tsx | 2 +- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 public/app/features/alerting/unified/components/rule-editor/ConditionField.test.tsx diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionField.test.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionField.test.tsx new file mode 100644 index 00000000000..440d155f385 --- /dev/null +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionField.test.tsx @@ -0,0 +1,39 @@ +import { render, screen } from '@testing-library/react'; +import React, { FC } from 'react'; +import { FormProvider, useForm, UseFormProps } from 'react-hook-form'; + +import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; + +import { RuleFormValues } from '../../types/rule-form'; + +import { ConditionField } from './ConditionField'; + +const FormProviderWrapper: FC = ({ children, ...props }) => { + const methods = useForm({ ...props }); + return {children}; +}; + +describe('ConditionField', () => { + it('should render the correct condition when editing existing rule', () => { + const existingRule = { + name: 'ConditionsTest', + condition: 'B', + queries: [ + { refId: 'A' }, + { refId: 'B', datasourceUid: ExpressionDatasourceUID }, + { refId: 'C', datasourceUid: ExpressionDatasourceUID }, + ], + } as RuleFormValues; + + const form = ( + + + + ); + + render(form); + expect(screen.getByLabelText(/^A/)).not.toBeChecked(); + expect(screen.getByLabelText(/^B/)).toBeChecked(); + expect(screen.getByLabelText(/^C/)).not.toBeChecked(); + }); +}); diff --git a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx index 5ec13955b0e..a07c9ba8285 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ConditionField.tsx @@ -9,7 +9,11 @@ import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionData import { RuleFormValues } from '../../types/rule-form'; -export const ConditionField: FC = () => { +interface Props { + existing?: boolean; +} + +export const ConditionField: FC = ({ existing = false }) => { const { watch, setValue, @@ -37,10 +41,10 @@ export const ConditionField: FC = () => { // automatically use the last expression when new expressions have been added useEffect(() => { const lastExpression = last(expressions); - if (lastExpression) { + if (lastExpression && !existing) { setValue('condition', lastExpression.refId, { shouldValidate: true }); } - }, [expressions, setValue]); + }, [expressions, setValue, existing]); // reset condition if option no longer exists or if it is unset, but there are options available useEffect(() => { diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndAlertConditionStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndAlertConditionStep.tsx index 4e61cda9024..c4031f8762d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndAlertConditionStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndAlertConditionStep.tsx @@ -22,7 +22,7 @@ export const QueryAndAlertConditionStep: FC = ({ editingExistingRule }) = {type && } - {isGrafanaManagedType && } + {isGrafanaManagedType && } ); }; From 3061d572206f732ddc9d2ea4308ac29ed16636df Mon Sep 17 00:00:00 2001 From: Will Browne Date: Wed, 1 Jun 2022 12:37:49 +0200 Subject: [PATCH 18/95] regenerate swagger spec (#49942) --- public/api-merged.json | 134 +++++++++++++++++++---------------------- public/api-spec.json | 100 ++++++++++++++++-------------- 2 files changed, 116 insertions(+), 118 deletions(-) diff --git a/public/api-merged.json b/public/api-merged.json index 5fdc053a4d9..e1af3800385 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -506,14 +506,6 @@ "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -522,6 +514,14 @@ "schema": { "$ref": "#/definitions/AddUserRoleCommand" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -547,14 +547,6 @@ "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "type": "string", "x-go-name": "RoleUID", @@ -568,6 +560,14 @@ "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.", "name": "global", "in": "query" + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -3926,15 +3926,15 @@ "parameters": [ { "type": "string", - "x-go-name": "DatasourceID", - "name": "id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "id", "in": "path", "required": true } @@ -7055,6 +7055,14 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -7063,14 +7071,6 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true } ], "responses": { @@ -7104,16 +7104,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true } @@ -9379,6 +9379,13 @@ "type": "boolean", "x-go-name": "EnableDashboardURL" }, + "formats": { + "type": "array", + "items": { + "$ref": "#/definitions/Type" + }, + "x-go-name": "Formats" + }, "id": { "type": "integer", "format": "int64", @@ -9597,6 +9604,13 @@ "type": "boolean", "x-go-name": "EnableDashboardURL" }, + "formats": { + "type": "array", + "items": { + "$ref": "#/definitions/Type" + }, + "x-go-name": "Formats" + }, "message": { "type": "string", "x-go-name": "Message" @@ -12349,27 +12363,10 @@ }, "x-go-package": "github.com/prometheus/alertmanager/config" }, - "MuteTiming": { - "type": "object", - "properties": { - "name": { - "type": "string", - "x-go-name": "Name" - }, - "time_intervals": { - "type": "array", - "items": { - "$ref": "#/definitions/TimeInterval" - }, - "x-go-name": "TimeIntervals" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - }, "MuteTimings": { "type": "array", "items": { - "$ref": "#/definitions/MuteTiming" + "$ref": "#/definitions/MuteTimeInterval" }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, @@ -13504,7 +13501,7 @@ "x-go-name": "HomeTab" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "Receiver": { "type": "object", @@ -14997,7 +14994,7 @@ }, "details_url": { "type": "string", - "x-go-name": "DetailsUrl" + "x-go-name": "DetailsURL" }, "exp": { "type": "integer", @@ -15009,28 +15006,18 @@ "format": "int64", "x-go-name": "Issued" }, - "included_admins": { - "type": "integer", - "format": "int64", - "x-go-name": "IncludedAdmins" - }, "included_users": { "type": "integer", "format": "int64", "x-go-name": "IncludedUsers" }, - "included_viewers": { - "type": "integer", - "format": "int64", - "x-go-name": "IncludedViewers" - }, "iss": { "type": "string", "x-go-name": "Issuer" }, "jti": { "type": "string", - "x-go-name": "Id" + "x-go-name": "ID" }, "lexp": { "type": "integer", @@ -15044,7 +15031,7 @@ }, "lid": { "type": "string", - "x-go-name": "LicenseId" + "x-go-name": "LicenseID" }, "limit_by": { "type": "string", @@ -15133,6 +15120,10 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, + "Type": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/models" + }, "URL": { "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", @@ -16252,7 +16243,6 @@ } }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt", "id", "status", "updatedAt"], "properties": { @@ -16295,7 +16285,9 @@ "format": "date-time", "x-go-name": "UpdatedAt" } - } + }, + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilences": { "type": "array", @@ -16423,6 +16415,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": ["comment", "createdBy", "endsAt", "matchers", "startsAt"], "properties": { @@ -16456,11 +16449,10 @@ "format": "date-time", "x-go-name": "StartsAt" } - }, - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + } }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": ["name"], "properties": { @@ -16469,9 +16461,7 @@ "type": "string", "x-go-name": "Name" } - }, - "x-go-name": "Receiver", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + } }, "silence": { "description": "Silence silence", diff --git a/public/api-spec.json b/public/api-spec.json index 69e4636ab54..9c2e2e2e20a 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -506,14 +506,6 @@ "summary": "Add a user role assignment.", "operationId": "addUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -522,6 +514,14 @@ "schema": { "$ref": "#/definitions/AddUserRoleCommand" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -547,14 +547,6 @@ "summary": "Remove a user role assignment.", "operationId": "removeUserRole", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "UserID", - "name": "user_id", - "in": "path", - "required": true - }, { "type": "string", "x-go-name": "RoleUID", @@ -568,6 +560,14 @@ "description": "A flag indicating if the assignment is global or not. If set to false, the default org ID of the authenticated user will be used from the request to remove assignment.", "name": "global", "in": "query" + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "UserID", + "name": "user_id", + "in": "path", + "required": true } ], "responses": { @@ -3926,15 +3926,15 @@ "parameters": [ { "type": "string", - "x-go-name": "DatasourceID", - "name": "id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "id", "in": "path", "required": true } @@ -7055,6 +7055,14 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true + }, { "x-go-name": "Body", "name": "body", @@ -7063,14 +7071,6 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } - }, - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true } ], "responses": { @@ -7104,16 +7104,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true } @@ -9136,6 +9136,13 @@ "type": "boolean", "x-go-name": "EnableDashboardURL" }, + "formats": { + "type": "array", + "items": { + "$ref": "#/definitions/Type" + }, + "x-go-name": "Formats" + }, "id": { "type": "integer", "format": "int64", @@ -9354,6 +9361,13 @@ "type": "boolean", "x-go-name": "EnableDashboardURL" }, + "formats": { + "type": "array", + "items": { + "$ref": "#/definitions/Type" + }, + "x-go-name": "Formats" + }, "message": { "type": "string", "x-go-name": "Message" @@ -11657,7 +11671,7 @@ "x-go-name": "HomeTab" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", @@ -12345,7 +12359,7 @@ }, "details_url": { "type": "string", - "x-go-name": "DetailsUrl" + "x-go-name": "DetailsURL" }, "exp": { "type": "integer", @@ -12357,28 +12371,18 @@ "format": "int64", "x-go-name": "Issued" }, - "included_admins": { - "type": "integer", - "format": "int64", - "x-go-name": "IncludedAdmins" - }, "included_users": { "type": "integer", "format": "int64", "x-go-name": "IncludedUsers" }, - "included_viewers": { - "type": "integer", - "format": "int64", - "x-go-name": "IncludedViewers" - }, "iss": { "type": "string", "x-go-name": "Issuer" }, "jti": { "type": "string", - "x-go-name": "Id" + "x-go-name": "ID" }, "lexp": { "type": "integer", @@ -12392,7 +12396,7 @@ }, "lid": { "type": "string", - "x-go-name": "LicenseId" + "x-go-name": "LicenseID" }, "limit_by": { "type": "string", @@ -12481,6 +12485,10 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, + "Type": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/extensions/report/models" + }, "UpdateAlertNotificationCommand": { "type": "object", "properties": { From 4371c45dffdcded6aea66efd16db9e6b06670039 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 06:43:02 -0400 Subject: [PATCH 19/95] Usage stats: Divide collection into multiple functions to isolate failures (#49928) (#49989) (cherry picked from commit d3ffb9e24541a791c54f8039149a37e1e5ac6339) Co-authored-by: Emil Tullstedt --- pkg/infra/usagestats/service/usage_stats.go | 5 + .../usagestats/service/usage_stats_test.go | 4 +- .../statscollector/concurrent_users.go | 21 +++ .../statscollector/concurrent_users_test.go | 2 +- .../statscollector/prometheus_flavor.go | 15 +- .../usagestats/statscollector/service.go | 162 ++++++++++-------- .../usagestats/statscollector/service_test.go | 136 +++++++++------ 7 files changed, 210 insertions(+), 135 deletions(-) diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go index 95df118a6c0..15dc764d2cb 100644 --- a/pkg/infra/usagestats/service/usage_stats.go +++ b/pkg/infra/usagestats/service/usage_stats.go @@ -48,10 +48,13 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e } func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]interface{}) { + totC, errC := 0, 0 for _, fn := range uss.externalMetrics { fnMetrics, err := fn(ctx) + totC++ if err != nil { uss.log.Error("Failed to fetch external metrics", "error", err) + errC++ continue } @@ -59,6 +62,8 @@ func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]int metrics[name] = value } } + metrics["stats.usagestats.debug.collect.total.count"] = totC + metrics["stats.usagestats.debug.collect.error.count"] = errC } func (uss *UsageStats) RegisterMetricsFunc(fn usagestats.MetricsFunc) { diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index 7c36eead42e..f68cd2c8c22 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -178,6 +178,7 @@ func TestRegisterMetrics(t *testing.T) { uss.gatherMetrics(context.Background(), metrics) assert.Equal(t, 1, metrics[goodMetricName]) + metricsCount := len(metrics) t.Run("do not add metrics that return an error when fetched", func(t *testing.T) { const badMetricName = "stats.test_external_metric_error.count" @@ -192,7 +193,8 @@ func TestRegisterMetrics(t *testing.T) { require.Nil(t, extErrorMetric, "Invalid metric should not be added") assert.Equal(t, 1, extMetric) - assert.Len(t, metrics, 3, "Expected only one available metric") + assert.Len(t, metrics, metricsCount, "Expected same number of metrics before and after collecting bad metric") + assert.EqualValues(t, 1, metrics["stats.usagestats.debug.collect.error.count"]) }) } diff --git a/pkg/infra/usagestats/statscollector/concurrent_users.go b/pkg/infra/usagestats/statscollector/concurrent_users.go index 55d23881ebc..23ae1e16874 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users.go @@ -56,3 +56,24 @@ FROM (select count(1) as tokens from user_auth_token group by user_id) uat;` s.concurrentUserStatsCache.memoized = time.Now() return s.concurrentUserStatsCache.stats, nil } + +func (s *Service) collectConcurrentUsers(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} + + // Get concurrent users stats as histogram + concurrentUsersStats, err := s.concurrentUsers(ctx) + if err != nil { + s.log.Error("Failed to get concurrent users stats", "error", err) + return nil, err + } + + // Histogram is cumulative and metric name has a postfix of le_"" + m["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3 + m["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6 + m["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9 + m["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12 + m["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15 + m["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf + + return m, nil +} diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go index dfa027d0b85..c74eb0e11cf 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -23,7 +23,7 @@ func TestConcurrentUsersMetrics(t *testing.T) { createConcurrentTokens(t, sqlStore) - stats, err := s.collect(context.Background()) + stats, err := s.collectConcurrentUsers(context.Background()) require.NoError(t, err) assert.Equal(t, int32(1), stats["stats.auth_token_per_user_le_3"]) diff --git a/pkg/infra/usagestats/statscollector/prometheus_flavor.go b/pkg/infra/usagestats/statscollector/prometheus_flavor.go index 652b312ea6a..09ba3a2171c 100644 --- a/pkg/infra/usagestats/statscollector/prometheus_flavor.go +++ b/pkg/infra/usagestats/statscollector/prometheus_flavor.go @@ -18,6 +18,19 @@ type memoPrometheusFlavor struct { memoized time.Time } +func (s *Service) collectPrometheusFlavors(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} + variants, err := s.detectPrometheusVariants(ctx) + if err != nil { + return nil, err + } + + for variant, count := range variants { + m["stats.ds.prometheus.flavor."+variant+".count"] = count + } + return m, nil +} + func (s *Service) detectPrometheusVariants(ctx context.Context) (map[string]int64, error) { if s.promFlavorCache.memoized.Add(promFlavorCacheLifetime).After(time.Now()) && s.promFlavorCache.variants != nil { @@ -77,7 +90,7 @@ func (s *Service) detectPrometheusVariant(ctx context.Context, ds *models.DataSo // Possibly configuration error, the risk of a false positive is // too high. s.log.Debug("Failed to send Prometheus build info request", "error", err) - return "", nil + return "unreachable", nil } defer func() { err := resp.Body.Close() diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index b2c930c2107..c5326b1491a 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -18,7 +20,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" ) type Service struct { @@ -40,7 +41,7 @@ type Service struct { } func ProvideService( - usagestats usagestats.Service, + us usagestats.Service, cfg *setting.Cfg, store sqlstore.Store, social social.Service, @@ -54,7 +55,7 @@ func ProvideService( sqlstore: store, plugins: plugins, social: social, - usageStats: usagestats, + usageStats: us, features: features, datasources: datasourceService, httpClientProvider: httpClientProvider, @@ -63,7 +64,19 @@ func ProvideService( log: log.New("infra.usagestats.collector"), } - usagestats.RegisterMetricsFunc(s.collect) + collectors := []usagestats.MetricsFunc{ + s.collectSystemStats, + s.collectConcurrentUsers, + s.collectDatasourceStats, + s.collectDatasourceAccess, + s.collectElasticStats, + s.collectAlertNotifierStats, + s.collectPrometheusFlavors, + s.collectAdditionalMetrics, + } + for _, c := range collectors { + us.RegisterMetricsFunc(c) + } return s } @@ -89,7 +102,7 @@ func (s *Service) Run(ctx context.Context) error { } } -func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { +func (s *Service) collectSystemStats(ctx context.Context) (map[string]interface{}, error) { m := map[string]interface{}{} statsQuery := models.GetSystemStatsQuery{} @@ -158,7 +171,66 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { } m["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser + m["stats.packaging."+s.cfg.Packaging+".count"] = 1 + m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1 + // Add stats about auth configuration + authTypes := map[string]bool{} + authTypes["anonymous"] = s.cfg.AnonymousEnabled + authTypes["basic_auth"] = s.cfg.BasicAuthEnabled + authTypes["ldap"] = s.cfg.LDAPEnabled + authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled + + for provider, enabled := range s.social.GetOAuthProviders() { + authTypes["oauth_"+provider] = enabled + } + + for authType, enabled := range authTypes { + enabledValue := 0 + if enabled { + enabledValue = 1 + } + m["stats.auth_enabled."+authType+".count"] = enabledValue + } + + m["stats.uptime"] = int64(time.Since(s.startTime).Seconds()) + + featureUsageStats := s.features.GetUsageStats(ctx) + for k, v := range featureUsageStats { + m[k] = v + } + + return m, nil +} + +func (s *Service) collectAdditionalMetrics(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} + for _, usageStatProvider := range s.usageStatProviders { + stats := usageStatProvider.GetUsageStats(ctx) + for k, v := range stats { + m[k] = v + } + } + return m, nil +} + +func (s *Service) collectAlertNotifierStats(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} + // get stats about alert notifier usage + anStats := models.GetAlertNotifierUsageStatsQuery{} + if err := s.sqlstore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { + s.log.Error("Failed to get alert notification stats", "error", err) + return nil, err + } + + for _, stats := range anStats.Result { + m["stats.alert_notifiers."+stats.Type+".count"] = stats.Count + } + return m, nil +} + +func (s *Service) collectDatasourceStats(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} dsStats := models.GetDataSourceStatsQuery{} if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil { s.log.Error("Failed to get datasource stats", "error", err) @@ -178,6 +250,11 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { } m["stats.ds.other.count"] = dsOtherCount + return m, nil +} + +func (s *Service) collectElasticStats(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES} if err := s.sqlstore.GetDataSourcesByType(ctx, &esDataSourcesQuery); err != nil { s.log.Error("Failed to get elasticsearch json data", "error", err) @@ -196,9 +273,11 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { m[statName] = count + 1 } + return m, nil +} - m["stats.packaging."+s.cfg.Packaging+".count"] = 1 - m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1 +func (s *Service) collectDatasourceAccess(ctx context.Context) (map[string]interface{}, error) { + m := map[string]interface{}{} // fetch datasource access stats dsAccessStats := models.GetDataSourceAccessStatsQuery{} @@ -207,15 +286,6 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { return nil, err } - variants, err := s.detectPrometheusVariants(ctx) - if err != nil { - return nil, err - } - - for variant, count := range variants { - m["stats.ds.prometheus.flavor."+variant+".count"] = count - } - // send access counters for each data source // but ignore any custom data sources // as sending that name could be sensitive information @@ -238,66 +308,6 @@ func (s *Service) collect(ctx context.Context) (map[string]interface{}, error) { for access, count := range dsAccessOtherCount { m["stats.ds_access.other."+access+".count"] = count } - - // get stats about alert notifier usage - anStats := models.GetAlertNotifierUsageStatsQuery{} - if err := s.sqlstore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { - s.log.Error("Failed to get alert notification stats", "error", err) - return nil, err - } - - for _, stats := range anStats.Result { - m["stats.alert_notifiers."+stats.Type+".count"] = stats.Count - } - - // Add stats about auth configuration - authTypes := map[string]bool{} - authTypes["anonymous"] = s.cfg.AnonymousEnabled - authTypes["basic_auth"] = s.cfg.BasicAuthEnabled - authTypes["ldap"] = s.cfg.LDAPEnabled - authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled - - for provider, enabled := range s.social.GetOAuthProviders() { - authTypes["oauth_"+provider] = enabled - } - - for authType, enabled := range authTypes { - enabledValue := 0 - if enabled { - enabledValue = 1 - } - m["stats.auth_enabled."+authType+".count"] = enabledValue - } - - // Get concurrent users stats as histogram - concurrentUsersStats, err := s.concurrentUsers(ctx) - if err != nil { - s.log.Error("Failed to get concurrent users stats", "error", err) - return nil, err - } - - // Histogram is cumulative and metric name has a postfix of le_"" - m["stats.auth_token_per_user_le_3"] = concurrentUsersStats.BucketLE3 - m["stats.auth_token_per_user_le_6"] = concurrentUsersStats.BucketLE6 - m["stats.auth_token_per_user_le_9"] = concurrentUsersStats.BucketLE9 - m["stats.auth_token_per_user_le_12"] = concurrentUsersStats.BucketLE12 - m["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15 - m["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf - - m["stats.uptime"] = int64(time.Since(s.startTime).Seconds()) - - featureUsageStats := s.features.GetUsageStats(ctx) - for k, v := range featureUsageStats { - m[k] = v - } - - for _, usageStatProvider := range s.usageStatProviders { - stats := usageStatProvider.GetUsageStats(ctx) - for k, v := range stats { - m[k] = v - } - } - return m, nil } diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 987d417978d..1a3d2c3f8e8 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -7,12 +7,13 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/components/simplejson" + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/login/social" @@ -97,7 +98,7 @@ func TestUsageStatsProviders(t *testing.T) { s := createService(t, setting.NewCfg(), store) s.RegisterProviders([]registry.ProvidesUsageStats{provider1, provider2}) - m, err := s.collect(context.Background()) + m, err := s.collectAdditionalMetrics(context.Background()) require.NoError(t, err, "Expected no error") assert.Equal(t, "val1", m["my_stat_1"]) @@ -111,7 +112,7 @@ func TestFeatureUsageStats(t *testing.T) { mockSystemStats(store) s := createService(t, setting.NewCfg(), store) - m, err := s.collect(context.Background()) + m, err := s.collectSystemStats(context.Background()) require.NoError(t, err, "Expected no error") assert.Equal(t, 1, m["stats.features.feature_1.count"]) @@ -134,6 +135,56 @@ func TestCollectingUsageStats(t *testing.T) { s.startTime = time.Now().Add(-1 * time.Minute) mockSystemStats(sqlStore) + + createConcurrentTokens(t, sqlStore) + + s.social = &mockSocial{ + OAuthProviders: map[string]bool{ + "github": true, + "gitlab": true, + "azuread": true, + "google": true, + "generic_oauth": true, + "grafana_com": true, + }, + } + + metrics, err := s.collectSystemStats(context.Background()) + require.NoError(t, err) + + assert.EqualValues(t, 15, metrics["stats.total_auth_token.count"]) + assert.EqualValues(t, 2, metrics["stats.api_keys.count"]) + assert.EqualValues(t, 5, metrics["stats.avg_auth_token_per_user.count"]) + assert.EqualValues(t, 16, metrics["stats.dashboard_versions.count"]) + assert.EqualValues(t, 17, metrics["stats.annotations.count"]) + assert.EqualValues(t, 18, metrics["stats.alert_rules.count"]) + assert.EqualValues(t, 19, metrics["stats.library_panels.count"]) + assert.EqualValues(t, 20, metrics["stats.library_variables.count"]) + + assert.EqualValues(t, 1, metrics["stats.auth_enabled.anonymous.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.basic_auth.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.ldap.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.auth_proxy.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_github.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_gitlab.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_google.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_azuread.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_generic_oauth.count"]) + assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_grafana_com.count"]) + + assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"]) + assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"]) + + assert.EqualValues(t, 11, metrics["stats.data_keys.count"]) + assert.EqualValues(t, 3, metrics["stats.active_data_keys.count"]) + + assert.InDelta(t, int64(65), metrics["stats.uptime"], 6) +} + +func TestDatasourceStats(t *testing.T) { + sqlStore := mockstore.NewSQLStoreMock() + s := createService(t, &setting.Cfg{}, sqlStore) + setupSomeDataSourcePlugins(t, s) sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{ @@ -216,6 +267,31 @@ func TestCollectingUsageStats(t *testing.T) { }, } + { + db, err := s.collectDatasourceStats(context.Background()) + require.NoError(t, err) + + assert.EqualValues(t, 9, db["stats.ds."+models.DS_ES+".count"]) + assert.EqualValues(t, 10, db["stats.ds."+models.DS_PROMETHEUS+".count"]) + assert.EqualValues(t, 11+12, db["stats.ds.other.count"]) + } + + { + dba, err := s.collectDatasourceAccess(context.Background()) + require.NoError(t, err) + + assert.EqualValues(t, 1, dba["stats.ds_access."+models.DS_ES+".direct.count"]) + assert.EqualValues(t, 2, dba["stats.ds_access."+models.DS_ES+".proxy.count"]) + assert.EqualValues(t, 3, dba["stats.ds_access."+models.DS_PROMETHEUS+".proxy.count"]) + assert.EqualValues(t, 6+7, dba["stats.ds_access.other.direct.count"]) + assert.EqualValues(t, 4+8, dba["stats.ds_access.other.proxy.count"]) + } +} + +func TestAlertNotifiersStats(t *testing.T) { + sqlStore := mockstore.NewSQLStoreMock() + s := createService(t, &setting.Cfg{}, sqlStore) + sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{ { Type: "slack", @@ -227,63 +303,11 @@ func TestCollectingUsageStats(t *testing.T) { }, } - createConcurrentTokens(t, sqlStore) - - s.social = &mockSocial{ - OAuthProviders: map[string]bool{ - "github": true, - "gitlab": true, - "azuread": true, - "google": true, - "generic_oauth": true, - "grafana_com": true, - }, - } - - metrics, err := s.collect(context.Background()) + metrics, err := s.collectAlertNotifierStats(context.Background()) require.NoError(t, err) - assert.EqualValues(t, 15, metrics["stats.total_auth_token.count"]) - assert.EqualValues(t, 2, metrics["stats.api_keys.count"]) - assert.EqualValues(t, 5, metrics["stats.avg_auth_token_per_user.count"]) - assert.EqualValues(t, 16, metrics["stats.dashboard_versions.count"]) - assert.EqualValues(t, 17, metrics["stats.annotations.count"]) - assert.EqualValues(t, 18, metrics["stats.alert_rules.count"]) - assert.EqualValues(t, 19, metrics["stats.library_panels.count"]) - assert.EqualValues(t, 20, metrics["stats.library_variables.count"]) - - assert.EqualValues(t, 9, metrics["stats.ds."+models.DS_ES+".count"]) - assert.EqualValues(t, 10, metrics["stats.ds."+models.DS_PROMETHEUS+".count"]) - - assert.EqualValues(t, 11+12, metrics["stats.ds.other.count"]) - - assert.EqualValues(t, 1, metrics["stats.ds_access."+models.DS_ES+".direct.count"]) - assert.EqualValues(t, 2, metrics["stats.ds_access."+models.DS_ES+".proxy.count"]) - assert.EqualValues(t, 3, metrics["stats.ds_access."+models.DS_PROMETHEUS+".proxy.count"]) - assert.EqualValues(t, 6+7, metrics["stats.ds_access.other.direct.count"]) - assert.EqualValues(t, 4+8, metrics["stats.ds_access.other.proxy.count"]) - assert.EqualValues(t, 1, metrics["stats.alert_notifiers.slack.count"]) assert.EqualValues(t, 2, metrics["stats.alert_notifiers.webhook.count"]) - - assert.EqualValues(t, 1, metrics["stats.auth_enabled.anonymous.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.basic_auth.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.ldap.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.auth_proxy.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_github.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_gitlab.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_google.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_azuread.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_generic_oauth.count"]) - assert.EqualValues(t, 1, metrics["stats.auth_enabled.oauth_grafana_com.count"]) - - assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"]) - assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"]) - - assert.EqualValues(t, 11, metrics["stats.data_keys.count"]) - assert.EqualValues(t, 3, metrics["stats.active_data_keys.count"]) - - assert.InDelta(t, int64(65), metrics["stats.uptime"], 6) } func mockSystemStats(sqlStore *mockstore.SQLStoreMock) { From a4431a7cbf21a577e85b548a690b99c0aa068d2e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 06:46:25 -0400 Subject: [PATCH 20/95] Settings: Sunset non-duration based login lifetime config (#49944) (#49990) (cherry picked from commit 39096208edee1915f20f82b2786ac4b63999de77) Co-authored-by: Emil Tullstedt --- docs/sources/auth/auth-proxy.md | 2 +- docs/sources/installation/upgrading.md | 8 +++++--- pkg/setting/setting.go | 21 +++++---------------- pkg/setting/setting_test.go | 10 ++-------- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/docs/sources/auth/auth-proxy.md b/docs/sources/auth/auth-proxy.md index 70fff8ee09c..3a2440ba18b 100644 --- a/docs/sources/auth/auth-proxy.md +++ b/docs/sources/auth/auth-proxy.md @@ -306,5 +306,5 @@ With `enable_login_token` set to `true` Grafana will, after successful auth prox a login token and cookie. You only have to configure your auth proxy to provide headers for the /login route. Requests via other routes will be authenticated using the cookie. -Use settings `login_maximum_inactive_lifetime_days` and `login_maximum_lifetime_days` under `[auth]` to control session +Use settings `login_maximum_inactive_lifetime_duration` and `login_maximum_lifetime_duration` under `[auth]` to control session lifetime. [Read more about login tokens]({{< relref "overview/#login-and-short-lived-tokens" >}}) diff --git a/docs/sources/installation/upgrading.md b/docs/sources/installation/upgrading.md index 7e79b328496..87150534303 100644 --- a/docs/sources/installation/upgrading.md +++ b/docs/sources/installation/upgrading.md @@ -157,14 +157,16 @@ cookie_secure = true cookie_secure = true ``` -The `login_remember_days`, `cookie_username` and `cookie_remember_name` settings in the `security` section are no longer being used so they're safe to remove. +The `login_remember_days`, `login_maximum_inactive_lifetime_days`, `login_maximum_lifetime_days`, `cookie_username` and `cookie_remember_name` settings in the `security` section are no longer being used so they're safe to remove. + +If you have `login_maximum_lifetime_days` or `login_maximum_inactive_lifetime_days` configured, you need to change it to `login_maximum_lifetime_duration` or `login_maximum_inactive_lifetime_duration` and append `d` to the configuration value to retain the previous behavior. If you have `login_remember_days` configured to 0 (zero) you should change your configuration to this to accomplish similar behavior, i.e. a logged in user will maximum be logged in for 1 day until being forced to login again: ```ini [auth] -login_maximum_inactive_lifetime_days = 1 -login_maximum_lifetime_days = 1 +login_maximum_inactive_lifetime_duration = 1d +login_maximum_lifetime_duration = 1d ``` The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings. diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 861fe1358a1..c6f0b815376 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -1246,27 +1246,16 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { auth := iniFile.Section("auth") cfg.LoginCookieName = valueAsString(auth, "login_cookie_name", "grafana_session") - maxInactiveDaysVal := auth.Key("login_maximum_inactive_lifetime_days").MustString("") - if maxInactiveDaysVal != "" { - maxInactiveDaysVal = fmt.Sprintf("%sd", maxInactiveDaysVal) - cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_inactive_lifetime_days' is deprecated, please use 'login_maximum_inactive_lifetime_duration' instead") - } else { - maxInactiveDaysVal = "7d" - } - maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", maxInactiveDaysVal) + + const defaultMaxInactiveLifetime = "7d" + maxInactiveDurationVal := valueAsString(auth, "login_maximum_inactive_lifetime_duration", defaultMaxInactiveLifetime) cfg.LoginMaxInactiveLifetime, err = gtime.ParseDuration(maxInactiveDurationVal) if err != nil { return err } - maxLifetimeDaysVal := auth.Key("login_maximum_lifetime_days").MustString("") - if maxLifetimeDaysVal != "" { - maxLifetimeDaysVal = fmt.Sprintf("%sd", maxLifetimeDaysVal) - cfg.Logger.Warn("[Deprecated] the configuration setting 'login_maximum_lifetime_days' is deprecated, please use 'login_maximum_lifetime_duration' instead") - } else { - maxLifetimeDaysVal = "30d" - } - maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", maxLifetimeDaysVal) + const defaultMaxLifetime = "30d" + maxLifetimeDurationVal := valueAsString(auth, "login_maximum_lifetime_duration", defaultMaxLifetime) cfg.LoginMaxLifetime, err = gtime.ParseDuration(maxLifetimeDurationVal) if err != nil { return err diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 59f8033a5c9..826e6ad4e3b 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -301,9 +301,7 @@ func TestAuthDurationSettings(t *testing.T) { cfg := NewCfg() sec, err := f.NewSection("auth") require.NoError(t, err) - _, err = sec.NewKey("login_maximum_inactive_lifetime_days", "10") - require.NoError(t, err) - _, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "") + _, err = sec.NewKey("login_maximum_inactive_lifetime_duration", "10d") require.NoError(t, err) err = readAuthSettings(f, cfg) require.NoError(t, err) @@ -323,9 +321,7 @@ func TestAuthDurationSettings(t *testing.T) { f = ini.Empty() sec, err = f.NewSection("auth") require.NoError(t, err) - _, err = sec.NewKey("login_maximum_lifetime_days", "24") - require.NoError(t, err) - _, err = sec.NewKey("login_maximum_lifetime_duration", "") + _, err = sec.NewKey("login_maximum_lifetime_duration", "24d") require.NoError(t, err) maxLifetimeDaysTest, err := time.ParseDuration("576h") require.NoError(t, err) @@ -347,8 +343,6 @@ func TestAuthDurationSettings(t *testing.T) { f = ini.Empty() sec, err = f.NewSection("auth") require.NoError(t, err) - _, err = sec.NewKey("login_maximum_lifetime_days", "") - require.NoError(t, err) _, err = sec.NewKey("login_maximum_lifetime_duration", "") require.NoError(t, err) maxLifetimeDurationTest, err = time.ParseDuration("720h") From e58aac1b777dfb2d6010a6e26d24bf89237e6803 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 06:48:31 -0400 Subject: [PATCH 21/95] Prometheus: Fix aligning of labels of exemplars after backend migration (#49924) (#49982) * Fix normalization of labels * Move sorting so it actually has an effect * fix lint error Co-authored-by: Todd Treece (cherry picked from commit d2fefec306d6fc04fefdacba67f834d6949103cf) Co-authored-by: Andrej Ocenas --- .../prometheus/buffered/time_series_query.go | 71 ++++++++++++++----- .../buffered/time_series_query_test.go | 29 ++++++-- .../datasource/prometheus/datasource.tsx | 2 +- 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/pkg/tsdb/prometheus/buffered/time_series_query.go b/pkg/tsdb/prometheus/buffered/time_series_query.go index e79a2767d13..40db2d9100d 100644 --- a/pkg/tsdb/prometheus/buffered/time_series_query.go +++ b/pkg/tsdb/prometheus/buffered/time_series_query.go @@ -434,20 +434,39 @@ func vectorToDataFrames(vector model.Vector, query *PrometheusQuery, frames data return frames } -func exemplarToDataFrames(response []apiv1.ExemplarQueryResult, query *PrometheusQuery, frames data.Frames) data.Frames { +// normalizeExemplars transforms the exemplar results into a single list of events. At the same time we make sure +// that all exemplar events have the same labels which is important when converting to dataFrames so that we have +// the same length of each field (each label will be a separate field). Exemplars can have different label either +// because the exemplar event have different labels or because they are from different series. +// Reason why we merge exemplars into single list even if they are from different series is that for example in case +// of a histogram query, like histogram_quantile(0.99, sum(rate(traces_spanmetrics_duration_seconds_bucket[15s])) by (le)) +// Prometheus still returns all the exemplars for all the series of metric traces_spanmetrics_duration_seconds_bucket. +// Which makes sense because each histogram bucket is separate series but we still want to show all the exemplars for +// the metric and we don't specifically care which buckets they are from. +// For non histogram queries or if you split by some label it would probably be nicer to then split also exemplars to +// multiple frames (so they will have different symbols in the UI) but that would require understanding the query so it +// is not implemented now. +func normalizeExemplars(response []apiv1.ExemplarQueryResult) []ExemplarEvent { // TODO: this preallocation is very naive. // We should figure out a better approximation here. events := make([]ExemplarEvent, 0, len(response)*2) - // Prometheus treats empty value as same as null, so `event.Labels` may not be consistent across `events`, - // leading errors like "frame has different field lengths, field 0 is len 5 but field 14 is len 2", need a fix. + + // Get all the labels across all exemplars both from the examplars and their series labels. We will use this to make + // sure the resulting data frame has consistent number of values in each column. eventLabels := make(map[string]struct{}) for _, exemplarData := range response { + // Check each exemplar labels as there isn't a guarantee they are consistent for _, exemplar := range exemplarData.Exemplars { for label := range exemplar.Labels { eventLabels[string(label)] = struct{}{} } } + + for label := range exemplarData.SeriesLabels { + eventLabels[string(label)] = struct{}{} + } } + for _, exemplarData := range response { for _, exemplar := range exemplarData.Exemplars { event := ExemplarEvent{} @@ -456,26 +475,26 @@ func exemplarToDataFrames(response []apiv1.ExemplarQueryResult, query *Prometheu event.Value = float64(exemplar.Value) event.Labels = make(map[string]string) - for label, value := range exemplar.Labels { - event.Labels[string(label)] = string(value) - } - - for seriesLabel, seriesValue := range exemplarData.SeriesLabels { - event.Labels[string(seriesLabel)] = string(seriesValue) - } - - if len(event.Labels) != len(eventLabels) { - // Fill event labels with empty value. - for label := range eventLabels { - if _, ok := event.Labels[label]; !ok { - event.Labels[label] = "" - } + // Fill in all the labels from eventLabels with values from exemplar labels or series labels or fill with + // empty string + for label := range eventLabels { + if _, ok := exemplar.Labels[model.LabelName(label)]; ok { + event.Labels[label] = string(exemplar.Labels[model.LabelName(label)]) + } else if _, ok := exemplarData.SeriesLabels[model.LabelName(label)]; ok { + event.Labels[label] = string(exemplarData.SeriesLabels[model.LabelName(label)]) + } else { + event.Labels[label] = "" } } events = append(events, event) } } + return events +} + +func exemplarToDataFrames(response []apiv1.ExemplarQueryResult, query *PrometheusQuery, frames data.Frames) data.Frames { + events := normalizeExemplars(response) // Sampling of exemplars bucketedExemplars := make(map[string][]ExemplarEvent) @@ -562,13 +581,27 @@ func exemplarToDataFrames(response []apiv1.ExemplarQueryResult, query *Prometheu dataFields := make([]*data.Field, 0, len(labelsVector)+2) dataFields = append(dataFields, timeField, valueField) - for label, vector := range labelsVector { - dataFields = append(dataFields, data.NewField(label, nil, vector)) + + // Sort the labels/fields so that it is consistent (mainly for easier testing) + allLabels := sortedLabels(labelsVector) + for _, label := range allLabels { + dataFields = append(dataFields, data.NewField(label, nil, labelsVector[label])) } return append(frames, newDataFrame("exemplar", "exemplar", dataFields...)) } +func sortedLabels(labelsVector map[string][]string) []string { + allLabels := make([]string, len(labelsVector)) + i := 0 + for key := range labelsVector { + allLabels[i] = key + i++ + } + sort.Strings(allLabels) + return allLabels +} + func deviation(values []float64) float64 { var sum, mean, sd float64 valuesLen := float64(len(values)) diff --git a/pkg/tsdb/prometheus/buffered/time_series_query_test.go b/pkg/tsdb/prometheus/buffered/time_series_query_test.go index 07b4c12f237..d0660c27624 100644 --- a/pkg/tsdb/prometheus/buffered/time_series_query_test.go +++ b/pkg/tsdb/prometheus/buffered/time_series_query_test.go @@ -5,7 +5,9 @@ import ( "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/tsdb/intervalv2" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" p "github.com/prometheus/common/model" @@ -601,29 +603,28 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { SeriesLabels: p.LabelSet{ "__name__": "tns_request_duration_seconds_bucket", "instance": "app:80", - "job": "tns/app", "service": "example", }, Exemplars: []apiv1.Exemplar{ { Labels: p.LabelSet{"traceID": "test1"}, Value: 0.003535405, - Timestamp: p.TimeFromUnixNano(time.Now().Add(-2 * time.Minute).UnixNano()), + Timestamp: 1, }, }, }, { SeriesLabels: p.LabelSet{ - "__name__": "tns_request_duration_seconds_bucket", - "instance": "app:80", - "job": "tns/app", - "service": "example", + "__name__": "tns_request_duration_seconds_bucket", + "instance": "app:80", + "service": "example2", + "additional_label": "foo", }, Exemplars: []apiv1.Exemplar{ { Labels: p.LabelSet{"traceID": "test2", "userID": "test3"}, Value: 0.003535405, - Timestamp: p.TimeFromUnixNano(time.Now().Add(-2 * time.Minute).UnixNano()), + Timestamp: 10, }, }, }, @@ -639,6 +640,20 @@ func TestPrometheus_parseTimeSeriesResponse(t *testing.T) { // Test frame marshal json no error. _, err = res[0].MarshalJSON() require.NoError(t, err) + + fields := []*data.Field{ + data.NewField("Time", map[string]string{}, []time.Time{time.UnixMilli(1), time.UnixMilli(10)}), + data.NewField("Value", map[string]string{}, []float64{0.003535405, 0.003535405}), + data.NewField("__name__", map[string]string{}, []string{"tns_request_duration_seconds_bucket", "tns_request_duration_seconds_bucket"}), + data.NewField("additional_label", map[string]string{}, []string{"", "foo"}), + data.NewField("instance", map[string]string{}, []string{"app:80", "app:80"}), + data.NewField("service", map[string]string{}, []string{"example", "example2"}), + data.NewField("traceID", map[string]string{}, []string{"test1", "test2"}), + data.NewField("userID", map[string]string{}, []string{"", "test3"}), + } + if diff := cmp.Diff(newDataFrame("exemplar", "exemplar", fields...), res[0], data.FrameTestCompareOptions()...); diff != "" { + t.Errorf("Result mismatch (-want +got):\n%s", diff) + } }) t.Run("matrix response should be parsed normally", func(t *testing.T) { diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 9c5426548d7..6877bc011ae 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -336,7 +336,7 @@ export class PrometheusDatasource const metricName = this.languageProvider.histogramMetrics.find((m) => target.expr.includes(m)); // Remove targets that weren't processed yet (in targets array they are after current target) const currentTargetIdx = request.targets.findIndex((t) => t.refId === target.refId); - const targets = request.targets.slice(0, currentTargetIdx); + const targets = request.targets.slice(0, currentTargetIdx).filter((t) => !t.hide); if (!metricName || (metricName && !targets.some((t) => t.expr.includes(metricName)))) { return true; From d344b69fbb0832f342a15837675cadbbbedb33ee Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 07:22:53 -0400 Subject: [PATCH 22/95] Plugins: Remove deprecated /api/tsdb/query metrics endpoint (#49916) (#49996) * remove /api/tsdb/query * revert changes to alert rules * regenerate spec based on 9.0.x (cherry picked from commit abfc711c53166e8a3ef85c38225090433dd1a3ca) Co-authored-by: Will Browne --- .../loadtest/annotations_by_tag_test.js | 2 +- devenv/docker/loadtest/auth_key_test.js | 2 +- .../docker/loadtest/auth_token_slow_test.js | 2 +- devenv/docker/loadtest/auth_token_test.js | 2 +- .../developers/http_api/data_source.md | 114 ---- pkg/api/api.go | 3 - pkg/api/docs/definitions/datasources.go | 39 -- pkg/api/metrics.go | 49 -- public/api-merged.json | 588 ++++++++++++----- public/api-spec.json | 596 +++++++++++++----- public/app/core/utils/query.ts | 6 +- .../elasticsearch/datasource.test.ts | 8 +- public/test/helpers/createFetchResponse.ts | 4 +- 13 files changed, 869 insertions(+), 546 deletions(-) diff --git a/devenv/docker/loadtest/annotations_by_tag_test.js b/devenv/docker/loadtest/annotations_by_tag_test.js index 7f722757caa..39d376b85f7 100644 --- a/devenv/docker/loadtest/annotations_by_tag_test.js +++ b/devenv/docker/loadtest/annotations_by_tag_test.js @@ -57,7 +57,7 @@ export default (data) => { requests.push({ method: 'GET', url: '/api/annotations?from=1580825186534&to=1580846786535' }); for (let n = 0; n < batchCount; n++) { - requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload }); + requests.push({ method: 'POST', url: '/api/ds/query', body: payload }); } let responses = client.batch(requests); diff --git a/devenv/docker/loadtest/auth_key_test.js b/devenv/docker/loadtest/auth_key_test.js index b13b97dd17c..a3029226a64 100644 --- a/devenv/docker/loadtest/auth_key_test.js +++ b/devenv/docker/loadtest/auth_key_test.js @@ -62,7 +62,7 @@ export default (data) => { requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' }); for (let n = 0; n < batchCount; n++) { - requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload }); + requests.push({ method: 'POST', url: '/api/ds/query', body: payload }); } let responses = client.batch(requests); diff --git a/devenv/docker/loadtest/auth_token_slow_test.js b/devenv/docker/loadtest/auth_token_slow_test.js index 7e80b1017c2..38a3a6d8171 100644 --- a/devenv/docker/loadtest/auth_token_slow_test.js +++ b/devenv/docker/loadtest/auth_token_slow_test.js @@ -59,7 +59,7 @@ export default (data) => { requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' }); for (let n = 0; n < batchCount; n++) { - requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload }); + requests.push({ method: 'POST', url: '/api/ds/query', body: payload }); } let responses = client.batch(requests); diff --git a/devenv/docker/loadtest/auth_token_test.js b/devenv/docker/loadtest/auth_token_test.js index 40570fdef57..0626377a29a 100644 --- a/devenv/docker/loadtest/auth_token_test.js +++ b/devenv/docker/loadtest/auth_token_test.js @@ -58,7 +58,7 @@ export default (data) => { requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' }); for (let n = 0; n < batchCount; n++) { - requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload }); + requests.push({ method: 'POST', url: '/api/ds/query', body: payload }); } let responses = client.batch(requests); diff --git a/docs/sources/developers/http_api/data_source.md b/docs/sources/developers/http_api/data_source.md index 008ac9f43d7..8f4a9da2147 100644 --- a/docs/sources/developers/http_api/data_source.md +++ b/docs/sources/developers/http_api/data_source.md @@ -960,117 +960,3 @@ In addition, specific properties of each data source should be added in a reques | 403 | Access denied. | | 404 | Either the data source or plugin required to fulfil the request could not be found. | | 500 | Unexpected error. Refer to the body and/or server logs for more details. | - -## Deprecated resources - -The following resources have been deprecated. They will be removed in a future release. - -### Query a data source by id - -> **Warning:** This API is deprecated since Grafana v8.5.0 and will be removed in a future release. Refer to the [new data source query API](#query-a-data-source). - -Queries a data source having a backend implementation. - -`POST /api/tsdb/query` - -> **Note:** Grafana's built-in data sources usually have a backend implementation. - -**Example Request**: - -```http -POST /api/tsdb/query HTTP/1.1 -Accept: application/json -Content-Type: application/json - -{ - "from": "1420066800000", - "to": "1575845999999", - "queries": [ - { - "refId": "A", - "intervalMs": 86400000, - "maxDataPoints": 1092, - "datasourceId": 86, - "rawSql": "SELECT 1 as valueOne, 2 as valueTwo", - "format": "table" - } - ] -} -``` - -JSON Body schema: - -- **from/to** – Specifies the time range for the queries. The time can be either epoch timestamps in milliseconds or relative using Grafana time units. For example, `now-5m`. -- **queries.refId** – Specifies an identifier of the query. Defaults to "A". -- **queries.format** – Specifies the format the data should be returned in. Valid options are `time_series` or `table` depending on the data source. -- **queries.datasourceId** – Specifies the data source to be queried. Each `query` in the request must have a unique `datasourceId`. -- **queries.maxDataPoints** - Species the maximum amount of data points that a dashboard panel can render. Defaults to 100. -- **queries.intervalMs** - Specifies the time series time interval in milliseconds. Defaults to 1000. - -In addition, specific properties of each data source should be added in a request. To better understand how to form a query for a certain data source, use the Developer Tools in your browser of choice and inspect the HTTP requests being made to `/api/tsdb/query`. - -**Example request for the MySQL data source:** - -```http -POST /api/tsdb/query HTTP/1.1 -Accept: application/json -Content-Type: application/json - -{ - "from": "1420066800000", - "to": "1575845999999", - "queries": [ - { - "refId": "A", - "intervalMs": 86400000, - "maxDataPoints": 1092, - "datasourceId": 86, - "rawSql": "SELECT\n time,\n sum(opened) AS \"Opened\",\n sum(closed) AS \"Closed\"\nFROM\n issues_activity\nWHERE\n $__unixEpochFilter(time) AND\n period = 'm' AND\n repo IN('grafana/grafana') AND\n opened_by IN('Contributor','Grafana Labs')\nGROUP BY 1\nORDER BY 1\n", - "format": "time_series" - } - ] -} -``` - -**Example MySQL time series query response:** - -```http -HTTP/1.1 200 -Content-Type: application/json - -{ - "results": { - "A": { - "refId": "A", - "meta": { - "rowCount": 0, - "sql": "SELECT\n time,\n sum(opened) AS \"Opened\",\n sum(closed) AS \"Closed\"\nFROM\n issues_activity\nWHERE\n time >= 1420066800 AND time <= 1575845999 AND\n period = 'm' AND\n repo IN('grafana/grafana') AND\n opened_by IN('Contributor','Grafana Labs')\nGROUP BY 1\nORDER BY 1\n" - }, - "series": [ - { - "name": "Opened", - "points": [ - [ - 109, - 1420070400000 - ], - [ - 122, - 1422748800000 - ] - ] - }, - { - "name": "Closed", - "points": [ - [ - 89, - 1420070400000 - ] - ] - } - ] - } - } -} -``` diff --git a/pkg/api/api.go b/pkg/api/api.go index 5f691bbaf08..9e755fcffe1 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -444,9 +444,6 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/search/", routing.Wrap(hs.Search)) // metrics - // Deprecated: use /ds/query API instead. - apiRoute.Post("/tsdb/query", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.QueryMetrics)) - // DataSource w/ expressions apiRoute.Post("/ds/query", authorize(reqSignedIn, ac.EvalPermission(datasources.ActionQuery)), routing.Wrap(hs.QueryMetricsV2)) diff --git a/pkg/api/docs/definitions/datasources.go b/pkg/api/docs/definitions/datasources.go index c3afa5bcf4b..728dd8c813d 100644 --- a/pkg/api/docs/definitions/datasources.go +++ b/pkg/api/docs/definitions/datasources.go @@ -3,7 +3,6 @@ package definitions import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/tsdb/legacydata" ) // swagger:route GET /datasources datasources getDatasources @@ -334,29 +333,6 @@ import ( // 404: notFoundError // 500: internalServerError -// swagger:route POST /tsdb/query datasources queryDatasource -// -// Query metrics. -// -// Please refer to [updated API](#/ds/queryMetricsWithExpressions) instead -// -// Queries a data source having backend implementation. -// -// Most of Grafana’s builtin data sources have backend implementation. -// -// If you are running Grafana Enterprise and have Fine-grained access control enabled -// you need to have a permission with action: `datasources:query`. -// -// Deprecated: true -// -// Responses: -// 200: queryDatasourceResponse -// 401: unauthorisedError -// 400: badRequestError -// 403: forbiddenError -// 404: notFoundError -// 500: internalServerError - // swagger:parameters updateDatasourceByID deleteDatasourceByID getDatasourceByID datasourceProxyGETcalls datasourceProxyPOSTcalls datasourceProxyDELETEcalls // swagger:parameters enablePermissions disablePermissions getPermissions deletePermissions // swagger:parameters checkDatasourceHealthByID fetchDatasourceResourcesByID @@ -411,13 +387,6 @@ type UpdateDatasource struct { Body models.UpdateDataSourceCommand } -// swagger:parameters queryDatasource -type QueryDatasource struct { - // in:body - // required:true - Body dtos.MetricRequest -} - // swagger:response getDatasourcesResponse type GetDatasourcesResponse struct { // The response message @@ -486,11 +455,3 @@ type DeleteDatasourceByNameResponse struct { Message string `json:"message"` } `json:"body"` } - -// swagger:response queryDatasourceResponse -type QueryDatasourceResponse struct { - // The response message - // in: body - //nolint: staticcheck // plugins.DataResponse deprecated - Body legacydata.DataResponse `json:"body"` -} diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 46929748a92..7cb475c8940 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -12,7 +12,6 @@ import ( "github.com/grafana/grafana/pkg/plugins/backendplugin" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/query" - "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) @@ -53,54 +52,6 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { return hs.toJsonStreamingResponse(resp) } -// QueryMetrics returns query metrics -// POST /api/tsdb/query -//nolint: staticcheck // legacydata.DataResponse deprecated -//nolint: staticcheck // legacydata.DataQueryResult deprecated -// Deprecated: use QueryMetricsV2 instead. -func (hs *HTTPServer) QueryMetrics(c *models.ReqContext) response.Response { - reqDto := dtos.MetricRequest{} - if err := web.Bind(c.Req, &reqDto); err != nil { - return response.Error(http.StatusBadRequest, "bad request data", err) - } - - sdkResp, err := hs.queryDataService.QueryData(c.Req.Context(), c.SignedInUser, c.SkipCache, reqDto, false) - if err != nil { - return hs.handleQueryMetricsError(err) - } - - legacyResp := legacydata.DataResponse{ - Results: map[string]legacydata.DataQueryResult{}, - } - - for refID, res := range sdkResp.Responses { - dqr := legacydata.DataQueryResult{ - RefID: refID, - } - - if res.Error != nil { - dqr.Error = res.Error - } - - if res.Frames != nil { - dqr.Dataframes = legacydata.NewDecodedDataFrames(res.Frames) - } - - legacyResp.Results[refID] = dqr - } - - statusCode := http.StatusOK - for _, res := range legacyResp.Results { - if res.Error != nil { - res.ErrorString = res.Error.Error() - legacyResp.Message = res.ErrorString - statusCode = http.StatusBadRequest - } - } - - return response.JSON(statusCode, &legacyResp) -} - func (hs *HTTPServer) toJsonStreamingResponse(qdr *backend.QueryDataResponse) response.Response { statusWhenError := http.StatusBadRequest if hs.Features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) { diff --git a/public/api-merged.json b/public/api-merged.json index e1af3800385..8fc1c36e0ea 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -7464,45 +7464,6 @@ } } }, - "/tsdb/query": { - "post": { - "description": "Please refer to [updated API](#/ds/queryMetricsWithExpressions) instead\n\nQueries a data source having backend implementation.\n\nMost of Grafana’s builtin data sources have backend implementation.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:query`.", - "tags": ["datasources"], - "summary": "Query metrics.", - "operationId": "queryDatasource", - "deprecated": true, - "parameters": [ - { - "name": "Body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/MetricRequest" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/queryDatasourceResponse" - }, - "400": { - "$ref": "#/responses/badRequestError" - }, - "401": { - "$ref": "#/responses/unauthorisedError" - }, - "403": { - "$ref": "#/responses/forbiddenError" - }, - "404": { - "$ref": "#/responses/notFoundError" - }, - "500": { - "$ref": "#/responses/internalServerError" - } - } - } - }, "/user": { "get": { "tags": ["signed_in_user"], @@ -9308,6 +9269,12 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/models" }, + "ConfFloat64": { + "description": "ConfFloat64 is a float64. It Marshals float64 values of NaN of Inf\nto null.", + "type": "number", + "format": "double", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "Config": { "type": "object", "title": "Config is the top-level configuration for Alertmanager's config files.", @@ -10296,67 +10263,39 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/models" }, - "DataFrames": { - "description": "See NewDecodedDataFrames and NewEncodedDataFrames for more information.", - "type": "object", - "title": "DataFrames is an interface for retrieving encoded and decoded data frames.", - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataQueryResult": { - "description": "Deprecated: DataQueryResult should use backend.QueryDataResponse", + "DataLink": { + "description": "DataLink define what", "type": "object", "properties": { - "dataframes": { - "$ref": "#/definitions/DataFrames" + "targetBlank": { + "type": "boolean", + "x-go-name": "TargetBlank" }, - "error": { + "title": { "type": "string", - "x-go-name": "ErrorString" + "x-go-name": "Title" }, - "meta": { - "$ref": "#/definitions/Json" - }, - "refId": { + "url": { "type": "string", - "x-go-name": "RefID" - }, - "series": { - "$ref": "#/definitions/DataTimeSeriesSlice" - }, - "tables": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTable" - }, - "x-go-name": "Tables" + "x-go-name": "URL" } }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" }, "DataResponse": { - "description": "Deprecated: DataResponse -- this structure is deprecated, all new work should use backend.QueryDataResponse", + "description": "A map of RefIDs (unique query identifers) to this type makes up the Responses property of a QueryDataResponse.\nThe Error property is used to allow for partial success responses from the containing QueryDataResponse.", "type": "object", + "title": "DataResponse contains the results from a DataQuery.", "properties": { - "message": { - "type": "string", - "x-go-name": "Message" + "Error": { + "description": "Error is a property to be set if the the corresponding DataQuery has an error.", + "type": "string" }, - "results": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/DataQueryResult" - }, - "x-go-name": "Results" + "Frames": { + "$ref": "#/definitions/Frames" } }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataRowValues": { - "type": "array", - "items": { - "type": "object" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/backend" }, "DataSource": { "type": "object", @@ -10528,78 +10467,6 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, - "DataTable": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTableColumn" - }, - "x-go-name": "Columns" - }, - "rows": { - "type": "array", - "items": { - "$ref": "#/definitions/DataRowValues" - }, - "x-go-name": "Rows" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTableColumn": { - "type": "object", - "properties": { - "text": { - "type": "string", - "x-go-name": "Text" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimePoint": { - "type": "array", - "items": { - "$ref": "#/definitions/Float" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimeSeries": { - "description": "DataTimeSeries -- this structure is deprecated, all new work should use DataFrames from the SDK", - "type": "object", - "properties": { - "name": { - "type": "string", - "x-go-name": "Name" - }, - "points": { - "$ref": "#/definitions/DataTimeSeriesPoints" - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Tags" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimeSeriesPoints": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTimePoint" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimeSeriesSlice": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTimeSeries" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, "DateTime": { "description": "DateTime is a time but it serializes to ISO8601 format with millis\nIt knows how to read 3 different variations of a RFC3339 date time.\nMost APIs we encounter want either millisecond or second precision times.\nThis just tries to make it worry-free.", "type": "string", @@ -10897,6 +10764,119 @@ "Failure": { "$ref": "#/definitions/ResponseDetails" }, + "Field": { + "description": "A Field is essentially a slice of various types with extra properties and methods.\nSee NewField() for supported types.\n\nThe slice data in the Field is a not exported, so methods on the Field are used to to manipulate its data.", + "type": "object", + "title": "Field represents a typed column of data within a Frame.", + "properties": { + "config": { + "$ref": "#/definitions/FieldConfig" + }, + "labels": { + "$ref": "#/definitions/Labels" + }, + "name": { + "description": "Name is default identifier of the field. The name does not have to be unique, but the combination\nof name and Labels should be unique for proper behavior in all situations.", + "type": "string", + "x-go-name": "Name" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "FieldConfig": { + "type": "object", + "title": "FieldConfig represents the display properties for a Field.", + "properties": { + "color": { + "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Color" + }, + "custom": { + "description": "Panel Specific Values", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Custom" + }, + "decimals": { + "type": "integer", + "format": "uint16", + "x-go-name": "Decimals" + }, + "description": { + "description": "Description is human readable field metadata", + "type": "string", + "x-go-name": "Description" + }, + "displayName": { + "description": "DisplayName overrides Grafana default naming, should not be used from a data source", + "type": "string", + "x-go-name": "DisplayName" + }, + "displayNameFromDS": { + "description": "DisplayNameFromDS overrides Grafana default naming in a better way that allows users to override it easily.", + "type": "string", + "x-go-name": "DisplayNameFromDS" + }, + "filterable": { + "description": "Filterable indicates if the Field's data can be filtered by additional calls.", + "type": "boolean", + "x-go-name": "Filterable" + }, + "interval": { + "description": "Interval indicates the expected regular step between values in the series.\nWhen an interval exists, consumers can identify \"missing\" values when the expected value is not present.\nThe grafana timeseries visualization will render disconnected values when missing values are found it the time field.\nThe interval uses the same units as the values. For time.Time, this is defined in milliseconds.", + "type": "number", + "format": "double", + "x-go-name": "Interval" + }, + "links": { + "description": "The behavior when clicking on a result", + "type": "array", + "items": { + "$ref": "#/definitions/DataLink" + }, + "x-go-name": "Links" + }, + "mappings": { + "$ref": "#/definitions/ValueMappings" + }, + "max": { + "$ref": "#/definitions/ConfFloat64" + }, + "min": { + "$ref": "#/definitions/ConfFloat64" + }, + "noValue": { + "description": "Alternative to empty string", + "type": "string", + "x-go-name": "NoValue" + }, + "path": { + "description": "Path is an explicit path to the field in the datasource. When the frame meta includes a path,\nthis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used as an identifier to update values in a subsequent request", + "type": "string", + "x-go-name": "Path" + }, + "thresholds": { + "$ref": "#/definitions/ThresholdsConfig" + }, + "unit": { + "description": "Numeric Options", + "type": "string", + "x-go-name": "Unit" + }, + "writeable": { + "description": "Writeable indicates that the datasource knows how to update this value", + "type": "boolean", + "x-go-name": "Writeable" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "FindTagsResult": { "type": "object", "title": "FindTagsResult is the result of a tags search.", @@ -11011,6 +10991,101 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, + "Frame": { + "description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.", + "type": "object", + "title": "Frame is a columnar data structure where each column is a Field.", + "properties": { + "Fields": { + "description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.", + "type": "array", + "items": { + "$ref": "#/definitions/Field" + } + }, + "Meta": { + "$ref": "#/definitions/FrameMeta" + }, + "Name": { + "description": "Name is used in some Grafana visualizations.", + "type": "string" + }, + "RefID": { + "description": "RefID is a property that can be set to match a Frame to its originating query.", + "type": "string" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "FrameMeta": { + "description": "https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L11\nNOTE -- in javascript this can accept any `[key: string]: any;` however\nthis interface only exposes the values we want to be exposed", + "type": "object", + "title": "FrameMeta matches:", + "properties": { + "channel": { + "description": "Channel is the path to a stream in grafana live that has real-time updates for this data.", + "type": "string", + "x-go-name": "Channel" + }, + "custom": { + "description": "Custom datasource specific values.", + "type": "object", + "x-go-name": "Custom" + }, + "executedQueryString": { + "description": "ExecutedQueryString is the raw query sent to the underlying system. All macros and templating\nhave been applied. When metadata contains this value, it will be shown in the query inspector.", + "type": "string", + "x-go-name": "ExecutedQueryString" + }, + "notices": { + "description": "Notices provide additional information about the data in the Frame that\nGrafana can display to the user in the user interface.", + "type": "array", + "items": { + "$ref": "#/definitions/Notice" + }, + "x-go-name": "Notices" + }, + "path": { + "description": "Path is a browsable path on the datasource.", + "type": "string", + "x-go-name": "Path" + }, + "pathSeparator": { + "description": "PathSeparator defines the separator pattern to decode a hiearchy. The default separator is '/'.", + "type": "string", + "x-go-name": "PathSeparator" + }, + "preferredVisualisationType": { + "$ref": "#/definitions/VisType" + }, + "stats": { + "description": "Stats is an array of query result statistics.", + "type": "array", + "items": { + "$ref": "#/definitions/QueryStat" + }, + "x-go-name": "Stats" + }, + "type": { + "$ref": "#/definitions/FrameType" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "FrameType": { + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "type": "string", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "Frames": { + "description": "It is the main data container within a backend.DataResponse.", + "type": "array", + "title": "Frames is a slice of Frame pointers.", + "items": { + "$ref": "#/definitions/Frame" + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "GetAnnotationTagsResponse": { "type": "object", "title": "GetAnnotationTagsResponse is a response struct for FindTagsResult.", @@ -11827,6 +11902,12 @@ }, "x-go-package": "github.com/prometheus/alertmanager/config" }, + "InspectType": { + "type": "integer", + "format": "int64", + "title": "InspectType is a type for the Inspect property of a Notice.", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "ItemDTO": { "type": "object", "properties": { @@ -12438,6 +12519,35 @@ "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "Notice": { + "type": "object", + "title": "Notice provides a structure for presenting notifications in Grafana's user interface.", + "properties": { + "inspect": { + "$ref": "#/definitions/InspectType" + }, + "link": { + "description": "Link is an optional link for display in the user interface and can be an\nabsolute URL or a path relative to Grafana's root url.", + "type": "string", + "x-go-name": "Link" + }, + "severity": { + "$ref": "#/definitions/NoticeSeverity" + }, + "text": { + "description": "Text is freeform descriptive text for the notice.", + "type": "string", + "x-go-name": "Text" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "NoticeSeverity": { + "type": "integer", + "format": "int64", + "title": "NoticeSeverity is a type for the Severity property of a Notice.", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "NotificationTestCommand": { "type": "object", "properties": { @@ -13503,6 +13613,106 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, + "QueryStat": { + "description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).", + "type": "object", + "title": "QueryStat is used for storing arbitrary statistics metadata related to a query and its result, e.g. total request time, data processing time.", + "properties": { + "color": { + "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Color" + }, + "custom": { + "description": "Panel Specific Values", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Custom" + }, + "decimals": { + "type": "integer", + "format": "uint16", + "x-go-name": "Decimals" + }, + "description": { + "description": "Description is human readable field metadata", + "type": "string", + "x-go-name": "Description" + }, + "displayName": { + "description": "DisplayName overrides Grafana default naming, should not be used from a data source", + "type": "string", + "x-go-name": "DisplayName" + }, + "displayNameFromDS": { + "description": "DisplayNameFromDS overrides Grafana default naming in a better way that allows users to override it easily.", + "type": "string", + "x-go-name": "DisplayNameFromDS" + }, + "filterable": { + "description": "Filterable indicates if the Field's data can be filtered by additional calls.", + "type": "boolean", + "x-go-name": "Filterable" + }, + "interval": { + "description": "Interval indicates the expected regular step between values in the series.\nWhen an interval exists, consumers can identify \"missing\" values when the expected value is not present.\nThe grafana timeseries visualization will render disconnected values when missing values are found it the time field.\nThe interval uses the same units as the values. For time.Time, this is defined in milliseconds.", + "type": "number", + "format": "double", + "x-go-name": "Interval" + }, + "links": { + "description": "The behavior when clicking on a result", + "type": "array", + "items": { + "$ref": "#/definitions/DataLink" + }, + "x-go-name": "Links" + }, + "mappings": { + "$ref": "#/definitions/ValueMappings" + }, + "max": { + "$ref": "#/definitions/ConfFloat64" + }, + "min": { + "$ref": "#/definitions/ConfFloat64" + }, + "noValue": { + "description": "Alternative to empty string", + "type": "string", + "x-go-name": "NoValue" + }, + "path": { + "description": "Path is an explicit path to the field in the datasource. When the frame meta includes a path,\nthis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used as an identifier to update values in a subsequent request", + "type": "string", + "x-go-name": "Path" + }, + "thresholds": { + "$ref": "#/definitions/ThresholdsConfig" + }, + "unit": { + "description": "Numeric Options", + "type": "string", + "x-go-name": "Unit" + }, + "value": { + "type": "number", + "format": "double", + "x-go-name": "Value" + }, + "writeable": { + "description": "Writeable indicates that the datasource knows how to update this value", + "type": "boolean", + "x-go-name": "Writeable" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "Receiver": { "type": "object", "title": "Receiver configuration provides configuration on how to contact a receiver.", @@ -14909,6 +15119,47 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "Threshold": { + "description": "Threshold a single step on the threshold list", + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "state": { + "type": "string", + "x-go-name": "State" + }, + "value": { + "$ref": "#/definitions/ConfFloat64" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "ThresholdsConfig": { + "description": "ThresholdsConfig setup thresholds", + "type": "object", + "properties": { + "mode": { + "$ref": "#/definitions/ThresholdsMode" + }, + "steps": { + "description": "Must be sorted by 'value', first value is always -Infinity", + "type": "array", + "items": { + "$ref": "#/definitions/Threshold" + }, + "x-go-name": "Steps" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "ThresholdsMode": { + "description": "ThresholdsMode absolute or percentage", + "type": "string", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "TimeInterval": { "description": "TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained\nwithin the interval.", "type": "object", @@ -15875,6 +16126,18 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "ValueMapping": { + "description": "ValueMapping allows mapping input values to text and color", + "type": "object", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "ValueMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/ValueMapping" + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "Vector": { "description": "Vector is basically only an alias for model.Samples, but the\ncontract is that in a Vector, all Samples have the same timestamp.", "type": "array", @@ -15933,6 +16196,11 @@ }, "x-go-package": "github.com/prometheus/alertmanager/config" }, + "VisType": { + "type": "string", + "title": "VisType is used to indicate how the data should be visualized in explore.", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "WebhookConfig": { "type": "object", "title": "WebhookConfig configures notifications via a generic webhook.", @@ -17437,12 +17705,6 @@ "$ref": "#/definitions/QueryDataResponse" } }, - "queryDatasourceResponse": { - "description": "", - "schema": { - "$ref": "#/definitions/DataResponse" - } - }, "recordingRuleResponse": { "description": "", "schema": { diff --git a/public/api-spec.json b/public/api-spec.json index 9c2e2e2e20a..b05dfd79a99 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -7464,45 +7464,6 @@ } } }, - "/tsdb/query": { - "post": { - "description": "Please refer to [updated API](#/ds/queryMetricsWithExpressions) instead\n\nQueries a data source having backend implementation.\n\nMost of Grafana’s builtin data sources have backend implementation.\n\nIf you are running Grafana Enterprise and have Fine-grained access control enabled\nyou need to have a permission with action: `datasources:query`.", - "tags": ["datasources"], - "summary": "Query metrics.", - "operationId": "queryDatasource", - "deprecated": true, - "parameters": [ - { - "name": "Body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/MetricRequest" - } - } - ], - "responses": { - "200": { - "$ref": "#/responses/queryDatasourceResponse" - }, - "400": { - "$ref": "#/responses/badRequestError" - }, - "401": { - "$ref": "#/responses/unauthorisedError" - }, - "403": { - "$ref": "#/responses/forbiddenError" - }, - "404": { - "$ref": "#/responses/notFoundError" - }, - "500": { - "$ref": "#/responses/internalServerError" - } - } - } - }, "/user": { "get": { "tags": ["signed_in_user"], @@ -9099,6 +9060,12 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/models" }, + "ConfFloat64": { + "description": "ConfFloat64 is a float64. It Marshals float64 values of NaN of Inf\nto null.", + "type": "number", + "format": "double", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "ConfigDTO": { "description": "ConfigDTO is model representation in transfer", "type": "object", @@ -10053,67 +10020,39 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/models" }, - "DataFrames": { - "description": "See NewDecodedDataFrames and NewEncodedDataFrames for more information.", - "type": "object", - "title": "DataFrames is an interface for retrieving encoded and decoded data frames.", - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataQueryResult": { - "description": "Deprecated: DataQueryResult should use backend.QueryDataResponse", + "DataLink": { + "description": "DataLink define what", "type": "object", "properties": { - "dataframes": { - "$ref": "#/definitions/DataFrames" + "targetBlank": { + "type": "boolean", + "x-go-name": "TargetBlank" }, - "error": { + "title": { "type": "string", - "x-go-name": "ErrorString" + "x-go-name": "Title" }, - "meta": { - "$ref": "#/definitions/Json" - }, - "refId": { + "url": { "type": "string", - "x-go-name": "RefID" - }, - "series": { - "$ref": "#/definitions/DataTimeSeriesSlice" - }, - "tables": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTable" - }, - "x-go-name": "Tables" + "x-go-name": "URL" } }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" }, "DataResponse": { - "description": "Deprecated: DataResponse -- this structure is deprecated, all new work should use backend.QueryDataResponse", + "description": "A map of RefIDs (unique query identifers) to this type makes up the Responses property of a QueryDataResponse.\nThe Error property is used to allow for partial success responses from the containing QueryDataResponse.", "type": "object", + "title": "DataResponse contains the results from a DataQuery.", "properties": { - "message": { - "type": "string", - "x-go-name": "Message" + "Error": { + "description": "Error is a property to be set if the the corresponding DataQuery has an error.", + "type": "string" }, - "results": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/DataQueryResult" - }, - "x-go-name": "Results" + "Frames": { + "$ref": "#/definitions/Frames" } }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataRowValues": { - "type": "array", - "items": { - "type": "object" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/backend" }, "DataSource": { "type": "object", @@ -10285,78 +10224,6 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, - "DataTable": { - "type": "object", - "properties": { - "columns": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTableColumn" - }, - "x-go-name": "Columns" - }, - "rows": { - "type": "array", - "items": { - "$ref": "#/definitions/DataRowValues" - }, - "x-go-name": "Rows" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTableColumn": { - "type": "object", - "properties": { - "text": { - "type": "string", - "x-go-name": "Text" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimePoint": { - "type": "array", - "items": { - "$ref": "#/definitions/Float" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimeSeries": { - "description": "DataTimeSeries -- this structure is deprecated, all new work should use DataFrames from the SDK", - "type": "object", - "properties": { - "name": { - "type": "string", - "x-go-name": "Name" - }, - "points": { - "$ref": "#/definitions/DataTimeSeriesPoints" - }, - "tags": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "x-go-name": "Tags" - } - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimeSeriesPoints": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTimePoint" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, - "DataTimeSeriesSlice": { - "type": "array", - "items": { - "$ref": "#/definitions/DataTimeSeries" - }, - "x-go-package": "github.com/grafana/grafana/pkg/tsdb/legacydata" - }, "DeleteTokenCommand": { "type": "object", "properties": { @@ -10438,6 +10305,119 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/extensions/ldapsync" }, + "Field": { + "description": "A Field is essentially a slice of various types with extra properties and methods.\nSee NewField() for supported types.\n\nThe slice data in the Field is a not exported, so methods on the Field are used to to manipulate its data.", + "type": "object", + "title": "Field represents a typed column of data within a Frame.", + "properties": { + "config": { + "$ref": "#/definitions/FieldConfig" + }, + "labels": { + "$ref": "#/definitions/Labels" + }, + "name": { + "description": "Name is default identifier of the field. The name does not have to be unique, but the combination\nof name and Labels should be unique for proper behavior in all situations.", + "type": "string", + "x-go-name": "Name" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "FieldConfig": { + "type": "object", + "title": "FieldConfig represents the display properties for a Field.", + "properties": { + "color": { + "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Color" + }, + "custom": { + "description": "Panel Specific Values", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Custom" + }, + "decimals": { + "type": "integer", + "format": "uint16", + "x-go-name": "Decimals" + }, + "description": { + "description": "Description is human readable field metadata", + "type": "string", + "x-go-name": "Description" + }, + "displayName": { + "description": "DisplayName overrides Grafana default naming, should not be used from a data source", + "type": "string", + "x-go-name": "DisplayName" + }, + "displayNameFromDS": { + "description": "DisplayNameFromDS overrides Grafana default naming in a better way that allows users to override it easily.", + "type": "string", + "x-go-name": "DisplayNameFromDS" + }, + "filterable": { + "description": "Filterable indicates if the Field's data can be filtered by additional calls.", + "type": "boolean", + "x-go-name": "Filterable" + }, + "interval": { + "description": "Interval indicates the expected regular step between values in the series.\nWhen an interval exists, consumers can identify \"missing\" values when the expected value is not present.\nThe grafana timeseries visualization will render disconnected values when missing values are found it the time field.\nThe interval uses the same units as the values. For time.Time, this is defined in milliseconds.", + "type": "number", + "format": "double", + "x-go-name": "Interval" + }, + "links": { + "description": "The behavior when clicking on a result", + "type": "array", + "items": { + "$ref": "#/definitions/DataLink" + }, + "x-go-name": "Links" + }, + "mappings": { + "$ref": "#/definitions/ValueMappings" + }, + "max": { + "$ref": "#/definitions/ConfFloat64" + }, + "min": { + "$ref": "#/definitions/ConfFloat64" + }, + "noValue": { + "description": "Alternative to empty string", + "type": "string", + "x-go-name": "NoValue" + }, + "path": { + "description": "Path is an explicit path to the field in the datasource. When the frame meta includes a path,\nthis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used as an identifier to update values in a subsequent request", + "type": "string", + "x-go-name": "Path" + }, + "thresholds": { + "$ref": "#/definitions/ThresholdsConfig" + }, + "unit": { + "description": "Numeric Options", + "type": "string", + "x-go-name": "Unit" + }, + "writeable": { + "description": "Writeable indicates that the datasource knows how to update this value", + "type": "boolean", + "x-go-name": "Writeable" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "FindTagsResult": { "type": "object", "title": "FindTagsResult is the result of a tags search.", @@ -10552,6 +10532,101 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, + "Frame": { + "description": "Each Field is well typed by its FieldType and supports optional Labels.\n\nA Frame is a general data container for Grafana. A Frame can be table data\nor time series data depending on its content and field types.", + "type": "object", + "title": "Frame is a columnar data structure where each column is a Field.", + "properties": { + "Fields": { + "description": "Fields are the columns of a frame.\nAll Fields must be of the same the length when marshalling the Frame for transmission.", + "type": "array", + "items": { + "$ref": "#/definitions/Field" + } + }, + "Meta": { + "$ref": "#/definitions/FrameMeta" + }, + "Name": { + "description": "Name is used in some Grafana visualizations.", + "type": "string" + }, + "RefID": { + "description": "RefID is a property that can be set to match a Frame to its originating query.", + "type": "string" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "FrameMeta": { + "description": "https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L11\nNOTE -- in javascript this can accept any `[key: string]: any;` however\nthis interface only exposes the values we want to be exposed", + "type": "object", + "title": "FrameMeta matches:", + "properties": { + "channel": { + "description": "Channel is the path to a stream in grafana live that has real-time updates for this data.", + "type": "string", + "x-go-name": "Channel" + }, + "custom": { + "description": "Custom datasource specific values.", + "type": "object", + "x-go-name": "Custom" + }, + "executedQueryString": { + "description": "ExecutedQueryString is the raw query sent to the underlying system. All macros and templating\nhave been applied. When metadata contains this value, it will be shown in the query inspector.", + "type": "string", + "x-go-name": "ExecutedQueryString" + }, + "notices": { + "description": "Notices provide additional information about the data in the Frame that\nGrafana can display to the user in the user interface.", + "type": "array", + "items": { + "$ref": "#/definitions/Notice" + }, + "x-go-name": "Notices" + }, + "path": { + "description": "Path is a browsable path on the datasource.", + "type": "string", + "x-go-name": "Path" + }, + "pathSeparator": { + "description": "PathSeparator defines the separator pattern to decode a hiearchy. The default separator is '/'.", + "type": "string", + "x-go-name": "PathSeparator" + }, + "preferredVisualisationType": { + "$ref": "#/definitions/VisType" + }, + "stats": { + "description": "Stats is an array of query result statistics.", + "type": "array", + "items": { + "$ref": "#/definitions/QueryStat" + }, + "x-go-name": "Stats" + }, + "type": { + "$ref": "#/definitions/FrameType" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "FrameType": { + "description": "A FrameType string, when present in a frame's metadata, asserts that the\nframe's structure conforms to the FrameType's specification.\nThis property is currently optional, so FrameType may be FrameTypeUnknown even if the properties of\nthe Frame correspond to a defined FrameType.", + "type": "string", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "Frames": { + "description": "It is the main data container within a backend.DataResponse.", + "type": "array", + "title": "Frames is a slice of Frame pointers.", + "items": { + "$ref": "#/definitions/Frame" + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "GetAnnotationTagsResponse": { "type": "object", "title": "GetAnnotationTagsResponse is a response struct for FindTagsResult.", @@ -10799,6 +10874,12 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/dashboardimport" }, + "InspectType": { + "type": "integer", + "format": "int64", + "title": "InspectType is a type for the Inspect property of a Notice.", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "ItemDTO": { "type": "object", "properties": { @@ -10896,6 +10977,14 @@ "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/components/simplejson" }, + "Labels": { + "description": "Labels are used to add metadata to an object. The JSON will always be sorted keys", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "LibraryElementConnectionDTO": { "type": "object", "title": "LibraryElementConnectionDTO is the frontend DTO for element connections.", @@ -11239,6 +11328,35 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, + "Notice": { + "type": "object", + "title": "Notice provides a structure for presenting notifications in Grafana's user interface.", + "properties": { + "inspect": { + "$ref": "#/definitions/InspectType" + }, + "link": { + "description": "Link is an optional link for display in the user interface and can be an\nabsolute URL or a path relative to Grafana's root url.", + "type": "string", + "x-go-name": "Link" + }, + "severity": { + "$ref": "#/definitions/NoticeSeverity" + }, + "text": { + "description": "Text is freeform descriptive text for the notice.", + "type": "string", + "x-go-name": "Text" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "NoticeSeverity": { + "type": "integer", + "format": "int64", + "title": "NoticeSeverity is a type for the Severity property of a Notice.", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "NotificationTestCommand": { "type": "object", "properties": { @@ -11673,6 +11791,106 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, + "QueryStat": { + "description": "The embedded FieldConfig's display name must be set.\nIt corresponds to the QueryResultMetaStat on the frontend (https://github.com/grafana/grafana/blob/master/packages/grafana-data/src/types/data.ts#L53).", + "type": "object", + "title": "QueryStat is used for storing arbitrary statistics metadata related to a query and its result, e.g. total request time, data processing time.", + "properties": { + "color": { + "description": "Map values to a display color\nNOTE: this interface is under development in the frontend... so simple map for now", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Color" + }, + "custom": { + "description": "Panel Specific Values", + "type": "object", + "additionalProperties": { + "type": "object" + }, + "x-go-name": "Custom" + }, + "decimals": { + "type": "integer", + "format": "uint16", + "x-go-name": "Decimals" + }, + "description": { + "description": "Description is human readable field metadata", + "type": "string", + "x-go-name": "Description" + }, + "displayName": { + "description": "DisplayName overrides Grafana default naming, should not be used from a data source", + "type": "string", + "x-go-name": "DisplayName" + }, + "displayNameFromDS": { + "description": "DisplayNameFromDS overrides Grafana default naming in a better way that allows users to override it easily.", + "type": "string", + "x-go-name": "DisplayNameFromDS" + }, + "filterable": { + "description": "Filterable indicates if the Field's data can be filtered by additional calls.", + "type": "boolean", + "x-go-name": "Filterable" + }, + "interval": { + "description": "Interval indicates the expected regular step between values in the series.\nWhen an interval exists, consumers can identify \"missing\" values when the expected value is not present.\nThe grafana timeseries visualization will render disconnected values when missing values are found it the time field.\nThe interval uses the same units as the values. For time.Time, this is defined in milliseconds.", + "type": "number", + "format": "double", + "x-go-name": "Interval" + }, + "links": { + "description": "The behavior when clicking on a result", + "type": "array", + "items": { + "$ref": "#/definitions/DataLink" + }, + "x-go-name": "Links" + }, + "mappings": { + "$ref": "#/definitions/ValueMappings" + }, + "max": { + "$ref": "#/definitions/ConfFloat64" + }, + "min": { + "$ref": "#/definitions/ConfFloat64" + }, + "noValue": { + "description": "Alternative to empty string", + "type": "string", + "x-go-name": "NoValue" + }, + "path": { + "description": "Path is an explicit path to the field in the datasource. When the frame meta includes a path,\nthis will default to `${frame.meta.path}/${field.name}\n\nWhen defined, this value can be used as an identifier within the datasource scope, and\nmay be used as an identifier to update values in a subsequent request", + "type": "string", + "x-go-name": "Path" + }, + "thresholds": { + "$ref": "#/definitions/ThresholdsConfig" + }, + "unit": { + "description": "Numeric Options", + "type": "string", + "x-go-name": "Unit" + }, + "value": { + "type": "number", + "format": "double", + "x-go-name": "Value" + }, + "writeable": { + "description": "Writeable indicates that the datasource knows how to update this value", + "type": "boolean", + "x-go-name": "Writeable" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", "type": "object", @@ -12332,6 +12550,47 @@ "type": "string", "x-go-package": "github.com/grafana/grafana/pkg/models" }, + "Threshold": { + "description": "Threshold a single step on the threshold list", + "type": "object", + "properties": { + "color": { + "type": "string", + "x-go-name": "Color" + }, + "state": { + "type": "string", + "x-go-name": "State" + }, + "value": { + "$ref": "#/definitions/ConfFloat64" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "ThresholdsConfig": { + "description": "ThresholdsConfig setup thresholds", + "type": "object", + "properties": { + "mode": { + "$ref": "#/definitions/ThresholdsMode" + }, + "steps": { + "description": "Must be sorted by 'value', first value is always -Infinity", + "type": "array", + "items": { + "$ref": "#/definitions/Threshold" + }, + "x-go-name": "Steps" + } + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "ThresholdsMode": { + "description": "ThresholdsMode absolute or percentage", + "type": "string", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, "TimeRangeDTO": { "type": "object", "properties": { @@ -13186,6 +13445,23 @@ } }, "x-go-package": "github.com/grafana/grafana/pkg/models" + }, + "ValueMapping": { + "description": "ValueMapping allows mapping input values to text and color", + "type": "object", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "ValueMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/ValueMapping" + }, + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" + }, + "VisType": { + "type": "string", + "title": "VisType is used to indicate how the data should be visualized in explore.", + "x-go-package": "github.com/grafana/grafana-plugin-sdk-go/data" } }, "responses": { @@ -14073,12 +14349,6 @@ "$ref": "#/definitions/QueryDataResponse" } }, - "queryDatasourceResponse": { - "description": "", - "schema": { - "$ref": "#/definitions/DataResponse" - } - }, "recordingRuleResponse": { "description": "", "schema": { diff --git a/public/app/core/utils/query.ts b/public/app/core/utils/query.ts index 166a98a1857..4a771ca64e3 100644 --- a/public/app/core/utils/query.ts +++ b/public/app/core/utils/query.ts @@ -22,11 +22,7 @@ export function addQuery(queries: DataQuery[], query?: Partial, datas } export function isDataQuery(url: string): boolean { - if ( - url.indexOf('api/datasources/proxy') !== -1 || - url.indexOf('api/tsdb/query') !== -1 || - url.indexOf('api/ds/query') !== -1 - ) { + if (url.indexOf('api/datasources/proxy') !== -1 || url.indexOf('api/ds/query') !== -1) { return true; } diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index fd473ab6083..887ff469840 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -340,7 +340,7 @@ describe('ElasticDatasource', function (this: any) { data: '{\n "reason": "all shards failed"\n}', message: 'all shards failed', config: { - url: 'http://localhost:3000/api/tsdb/query', + url: 'http://localhost:3000/api/ds/query', }, }; @@ -357,8 +357,8 @@ describe('ElasticDatasource', function (this: any) { message: 'Authentication to data source failed', }, status: 400, - url: 'http://localhost:3000/api/tsdb/query', - config: { url: 'http://localhost:3000/api/tsdb/query' }, + url: 'http://localhost:3000/api/ds/query', + config: { url: 'http://localhost:3000/api/ds/query' }, type: 'basic', statusText: 'Bad Request', redirected: false, @@ -401,7 +401,7 @@ describe('ElasticDatasource', function (this: any) { data: '{}', message: 'Unknown elastic error response', config: { - url: 'http://localhost:3000/api/tsdb/query', + url: 'http://localhost:3000/api/ds/query', }, }; diff --git a/public/test/helpers/createFetchResponse.ts b/public/test/helpers/createFetchResponse.ts index f69908ec0ea..1f75026ed71 100644 --- a/public/test/helpers/createFetchResponse.ts +++ b/public/test/helpers/createFetchResponse.ts @@ -4,8 +4,8 @@ export function createFetchResponse(data: T): FetchResponse { return { data, status: 200, - url: 'http://localhost:3000/api/tsdb/query', - config: { url: 'http://localhost:3000/api/tsdb/query' }, + url: 'http://localhost:3000/api/ds/query', + config: { url: 'http://localhost:3000/api/ds/query' }, type: 'basic', statusText: 'Ok', redirected: false, From f7e880976318acb2340ceea5c77e402e271d1be1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 07:24:22 -0400 Subject: [PATCH 23/95] [v9.0.x] Alerting: Re-render panel's tabs on variables change (#49995) Co-authored-by: Konrad Lalik --- .../unified/PanelAlertTabContent.test.tsx | 33 +++++++++++++++++-- .../NewRuleFromPanelButton.tsx | 16 +++++++-- .../PanelEditor/PanelEditorTabs.tsx | 2 +- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index 88bb90929fa..cd0170429f1 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -1,4 +1,4 @@ -import { render, act } from '@testing-library/react'; +import { render, act, waitFor } from '@testing-library/react'; import React from 'react'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; @@ -9,6 +9,8 @@ import { locationService, setDataSourceSrv } from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { toggleOption } from 'app/features/variables/pickers/OptionsPicker/reducer'; +import { toKeyedAction } from 'app/features/variables/state/keyedVariablesReducer'; import { PrometheusDatasource } from 'app/plugins/datasource/prometheus/datasource'; import { PromOptions } from 'app/plugins/datasource/prometheus/types'; import { configureStore } from 'app/store/configureStore'; @@ -28,6 +30,7 @@ import { import { getAllDataSources } from './utils/config'; import { Annotation } from './utils/constants'; import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; +import * as ruleFormUtils from './utils/rule-form'; jest.mock('./api/prometheus'); jest.mock('./api/ruler'); @@ -56,8 +59,12 @@ const mocks = { }, }; -const renderAlertTabContent = (dashboard: DashboardModel, panel: PanelModel) => { - const store = configureStore(); +const renderAlertTabContent = ( + dashboard: DashboardModel, + panel: PanelModel, + initialStore?: ReturnType +) => { + const store = initialStore ?? configureStore(); return act(async () => { render( @@ -349,4 +356,24 @@ describe('PanelAlertTabContent', () => { panelId: panel.id, }); }); + + it('Update NewRuleFromPanel button url when template changes', async () => { + const panelToRuleValuesSpy = jest.spyOn(ruleFormUtils, 'panelToRuleFormValues'); + + const store = configureStore(); + await renderAlertTabContent(dashboard, panel, store); + + store.dispatch( + toKeyedAction( + 'optionKey', + toggleOption({ + option: { value: 'optionValue', selected: true, text: 'Option' }, + clearOthers: false, + forceSelect: false, + }) + ) + ); + + await waitFor(() => expect(panelToRuleValuesSpy).toHaveBeenCalledTimes(2)); + }); }); diff --git a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx index 6b500e8ab46..608f830987d 100644 --- a/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx +++ b/public/app/features/alerting/unified/components/panel-alerts-tab/NewRuleFromPanelButton.tsx @@ -1,10 +1,12 @@ import React, { FC } from 'react'; +import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { useAsync } from 'react-use'; import { urlUtil } from '@grafana/data'; -import { Alert, LinkButton, Button } from '@grafana/ui'; +import { Alert, Button, LinkButton } from '@grafana/ui'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; +import { StoreState } from 'app/types'; import { panelToRuleFormValues } from '../../utils/rule-form'; @@ -15,8 +17,18 @@ interface Props { } export const NewRuleFromPanelButton: FC = ({ dashboard, panel, className }) => { - const { loading, value: formValues } = useAsync(() => panelToRuleFormValues(panel, dashboard), [panel, dashboard]); + const templating = useSelector((state: StoreState) => { + return state.templating; + }); + const location = useLocation(); + + const { loading, value: formValues } = useAsync( + () => panelToRuleFormValues(panel, dashboard), + // Templating variables are required to update formValues on each variable's change. It's used implicitly by the templating engine + [panel, dashboard, templating] + ); + if (loading) { return ; } diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx index e9c4ed1a4dc..3d8be41e1c4 100644 --- a/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx +++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorTabs.tsx @@ -31,7 +31,7 @@ export const PanelEditorTabs: FC = React.memo(({ panel, da eventSubs.add(panel.events.subscribe(PanelQueriesChangedEvent, forceUpdate)); eventSubs.add(panel.events.subscribe(PanelTransformationsChangedEvent, forceUpdate)); return () => eventSubs.unsubscribe(); - }, [panel, forceUpdate]); + }, [panel, dashboard, forceUpdate]); const activeTab = tabs.find((item) => item.active)!; From c011b15d1cf16f29ce1596fdade6ea99ceb8174e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 07:33:14 -0400 Subject: [PATCH 24/95] Table: Reorder panel options (#49983) (#49998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Table: Reorder panel options * Fix e2e selector (cherry picked from commit f566958555856cfe12bfde30e37bd867452d2044) Co-authored-by: Zoltán Bedi --- e2e/panels-suite/panelEdit_base.spec.ts | 2 +- public/app/plugins/panel/table/module.tsx | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/e2e/panels-suite/panelEdit_base.spec.ts b/e2e/panels-suite/panelEdit_base.spec.ts index 1436fc457ac..8eb18531ddd 100644 --- a/e2e/panels-suite/panelEdit_base.spec.ts +++ b/e2e/panels-suite/panelEdit_base.spec.ts @@ -102,7 +102,7 @@ e2e.scenario({ e2e.components.PanelEditor.DataPane.content().should('be.visible'); // Field & Overrides tabs (need to switch to React based vis, i.e. Table) - e2e.components.PanelEditor.OptionsPane.fieldLabel('Header and footer Show header').should('be.visible'); + e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show table header').should('be.visible'); e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible'); }, }); diff --git a/public/app/plugins/panel/table/module.tsx b/public/app/plugins/panel/table/module.tsx index 655597ec4c0..ad19eba30a9 100644 --- a/public/app/plugins/panel/table/module.tsx +++ b/public/app/plugins/panel/table/module.tsx @@ -15,6 +15,8 @@ import { tableMigrationHandler, tablePanelChangedHandler } from './migrations'; import { PanelOptions, defaultPanelOptions, defaultPanelFieldConfig } from './models.gen'; import { TableSuggestionsSupplier } from './suggestions'; +const footerCategory = 'Table footer'; + export const plugin = new PanelPlugin(TablePanel) .setPanelChangeHandler(tablePanelChangedHandler) .setMigrationHandler(tableMigrationHandler) @@ -110,21 +112,18 @@ export const plugin = new PanelPlugin(TablePane builder .addBooleanSwitch({ path: 'showHeader', - category: ['Header and footer'], - name: 'Show header', - description: "To display table's header or not to display", + name: 'Show table header', defaultValue: defaultPanelOptions.showHeader, }) .addBooleanSwitch({ path: 'footer.show', - category: ['Header and footer'], - name: 'Show Footer', - description: "To display table's footer or not to display", + category: [footerCategory], + name: 'Show table footer', defaultValue: defaultPanelOptions.footer?.show, }) .addCustomEditor({ id: 'footer.reducer', - category: ['Header and footer'], + category: [footerCategory], path: 'footer.reducer', name: 'Calculation', description: 'Choose a reducer function / calculation', @@ -134,7 +133,7 @@ export const plugin = new PanelPlugin(TablePane }) .addMultiSelect({ path: 'footer.fields', - category: ['Header and footer'], + category: [footerCategory], name: 'Fields', description: 'Select the fields that should be calculated', settings: { @@ -161,7 +160,6 @@ export const plugin = new PanelPlugin(TablePane }) .addCustomEditor({ id: 'footer.enablePagination', - category: ['Header and footer'], path: 'footer.enablePagination', name: 'Enable pagination', editor: PaginationEditor, From b2e73b866fcfe9ff7c79ea70a6bc5eebd9493da2 Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Wed, 1 Jun 2022 05:30:16 -0700 Subject: [PATCH 25/95] Azure Monitor: Include datasource ref when interpolating variables (#49543) (#49957) --- .../__mocks__/query.ts | 22 ++++++++--- .../datasource.test.ts | 37 +++++++++++++++++++ .../datasource.ts | 5 ++- 3 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts index cac782301ec..9da3bf31761 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query.ts @@ -1,16 +1,30 @@ import { AzureMonitorQuery, AzureQueryType } from '../types'; -export default function createMockQuery(): AzureMonitorQuery { +export default function createMockQuery(overrides?: Partial): AzureMonitorQuery { return { + queryType: AzureQueryType.AzureMonitor, + refId: 'A', + subscription: '99999999-cccc-bbbb-aaaa-9106972f9572', + subscriptions: ['99999999-cccc-bbbb-aaaa-9106972f9572'], + datasource: { + type: 'grafana-azure-monitor-datasource', + uid: 'AAAAA11111BBBBB22222CCCC', + }, + ...overrides, + azureLogAnalytics: { query: '//change this example to create your own time series query\n //the table to query (e.g. Usage, Heartbeat, Perf)\n| where $__timeFilter(TimeGenerated) //this is a macro used to show the full chart’s time range, choose the datetime column here\n| summarize count() by , bin(TimeGenerated, $__interval) //change “group by column” to a column in your table, such as “Computer”. The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.\n| order by TimeGenerated asc', resultFormat: 'time_series', workspace: 'e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2', + resource: 'test-resource', + ...overrides?.azureLogAnalytics, }, azureResourceGraph: { query: 'Resources | summarize count()', + resultFormat: 'table', + ...overrides?.azureResourceGraph, }, azureMonitor: { @@ -30,11 +44,7 @@ export default function createMockQuery(): AzureMonitorQuery { alias: '', // timeGrains: [], top: '10', + ...overrides?.azureMonitor, }, - - queryType: AzureQueryType.AzureMonitor, - refId: 'A', - subscription: '99999999-cccc-bbbb-aaaa-9106972f9572', - subscriptions: ['99999999-cccc-bbbb-aaaa-9106972f9572'], }; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts new file mode 100644 index 00000000000..f0677e6a1a8 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.test.ts @@ -0,0 +1,37 @@ +import { createMockInstanceSetttings } from './__mocks__/instanceSettings'; +import createMockQuery from './__mocks__/query'; +import Datasource from './datasource'; + +describe('Azure Monitor Datasource', () => { + describe('interpolateVariablesInQueries()', () => { + it('should interpolate variables in the queries', () => { + const ds = new Datasource(createMockInstanceSetttings()); + const queries = [createMockQuery({ azureMonitor: { resourceGroup: '$resourceGroup' } })]; + + const interpolatedQueries = ds.interpolateVariablesInQueries(queries, { + resourceGroup: { text: 'the-resource-group', value: 'the-resource-group' }, + }); + + expect(interpolatedQueries).toContainEqual( + expect.objectContaining({ + azureMonitor: expect.objectContaining({ resourceGroup: 'the-resource-group' }), + }) + ); + }); + + it('should include a datasource ref when interpolating queries', () => { + const ds = new Datasource(createMockInstanceSetttings()); + const query = createMockQuery(); + delete query.datasource; + const queries = [query]; + + const interpolatedQueries = ds.interpolateVariablesInQueries(queries, {}); + + expect(interpolatedQueries).toContainEqual( + expect.objectContaining({ + datasource: expect.objectContaining({ type: 'azuremonitor', uid: 'abc' }), + }) + ); + }); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts index 59ebd9d1bc0..f5adb0410cd 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts @@ -203,7 +203,10 @@ export default class Datasource extends DataSourceApi Date: Wed, 1 Jun 2022 08:49:37 -0400 Subject: [PATCH 26/95] Metrics: Remove support for using summaries instead of histogram for HTTP instrumentation (#49985) (#50003) Signed-off-by: bergquist (cherry picked from commit 9562fb389fcca26a927c86253a689a85292e4dcc) Co-authored-by: Carl Bergquist --- .../src/types/featureToggles.gen.ts | 1 - pkg/infra/metrics/metrics.go | 25 ------------ pkg/middleware/request_metrics.go | 39 +++++++------------ pkg/services/featuremgmt/registry.go | 5 --- pkg/services/featuremgmt/toggles_gen.go | 4 -- pkg/services/featuremgmt/toggles_gen_test.go | 15 ++++--- 6 files changed, 20 insertions(+), 69 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2d990c58dda..1d6dbe5f7a6 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,7 +36,6 @@ export interface FeatureToggles { influxdbBackendMigration?: boolean; newNavigation?: boolean; showFeatureFlagsInUI?: boolean; - disable_http_request_histogram?: boolean; publicDashboards?: boolean; lokiLive?: boolean; swaggerUi?: boolean; diff --git a/pkg/infra/metrics/metrics.go b/pkg/infra/metrics/metrics.go index d35115ee899..3eac92d8987 100644 --- a/pkg/infra/metrics/metrics.go +++ b/pkg/infra/metrics/metrics.go @@ -24,12 +24,6 @@ var ( // MProxyStatus is a metric proxy http response status MProxyStatus *prometheus.CounterVec - // MHttpRequestTotal is a metric http request counter - MHttpRequestTotal *prometheus.CounterVec - - // MHttpRequestSummary is a metric http request summary - MHttpRequestSummary *prometheus.SummaryVec - // MApiUserSignUpStarted is a metric amount of users who started the signup flow MApiUserSignUpStarted prometheus.Counter @@ -226,23 +220,6 @@ func init() { Namespace: ExporterName, }, []string{"code"}, httpStatusCodes...) - MHttpRequestTotal = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "http_request_total", - Help: "http request counter", - }, - []string{"handler", "statuscode", "method"}, - ) - - MHttpRequestSummary = prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Name: "http_request_duration_milliseconds", - Help: "http request summary", - Objectives: objectiveMap, - }, - []string{"handler", "statuscode", "method"}, - ) - MApiUserSignUpStarted = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_user_signup_started_total", Help: "amount of users who started the signup flow", @@ -615,8 +592,6 @@ func initMetricVars() { MPageStatus, MApiStatus, MProxyStatus, - MHttpRequestTotal, - MHttpRequestSummary, MApiUserSignUpStarted, MApiUserSignUpCompleted, MApiUserSignUpInvite, diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index 6f9b2618993..a91b6849f1b 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -66,31 +66,22 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler { } status := rw.Status() - code := sanitizeCode(status) - method := sanitizeMethod(req.Method) - // enable histogram and disable summaries + counters for http requests. - if features.IsEnabled(featuremgmt.FlagDisableHttpRequestHistogram) { - duration := time.Since(now).Nanoseconds() / int64(time.Millisecond) - metrics.MHttpRequestTotal.WithLabelValues(handler, code, method).Inc() - metrics.MHttpRequestSummary.WithLabelValues(handler, code, method).Observe(float64(duration)) - } else { - // avoiding the sanitize functions for in the new instrumentation - // since they dont make much sense. We should remove them later. - histogram := httpRequestDurationHistogram. - WithLabelValues(handler, code, req.Method) - if traceID := tracing.TraceIDFromContext(c.Req.Context(), true); traceID != "" { - // Need to type-convert the Observer to an - // ExemplarObserver. This will always work for a - // HistogramVec. - histogram.(prometheus.ExemplarObserver).ObserveWithExemplar( - time.Since(now).Seconds(), prometheus.Labels{"traceID": traceID}, - ) - return - } - histogram.Observe(time.Since(now).Seconds()) + // avoiding the sanitize functions for in the new instrumentation + // since they dont make much sense. We should remove them later. + histogram := httpRequestDurationHistogram. + WithLabelValues(handler, code, req.Method) + if traceID := tracing.TraceIDFromContext(c.Req.Context(), true); traceID != "" { + // Need to type-convert the Observer to an + // ExemplarObserver. This will always work for a + // HistogramVec. + histogram.(prometheus.ExemplarObserver).ObserveWithExemplar( + time.Since(now).Seconds(), prometheus.Labels{"traceID": traceID}, + ) + return } + histogram.Observe(time.Since(now).Seconds()) switch { case strings.HasPrefix(req.RequestURI, "/api/datasources/proxy"): @@ -142,10 +133,6 @@ func countProxyRequests(status int) { } } -func sanitizeMethod(m string) string { - return strings.ToLower(m) -} - // If the wrapped http.Handler has not set a status code, i.e. the value is // currently 0, sanitizeCode will return 200, for consistency with behavior in // the stdlib. diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 69d4781256f..3182c53c179 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -117,11 +117,6 @@ var ( State: FeatureStateAlpha, RequiresDevMode: true, }, - { - Name: "disable_http_request_histogram", - Description: "Do not create histograms for http requests", - State: FeatureStateAlpha, - }, { Name: "publicDashboards", Description: "enables public access to dashboards", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f41fbacd7cb..13ad09f4067 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -87,10 +87,6 @@ const ( // Show feature flags in the settings UI FlagShowFeatureFlagsInUI = "showFeatureFlagsInUI" - // FlagDisableHttpRequestHistogram - // Do not create histograms for http requests - FlagDisableHttpRequestHistogram = "disable_http_request_histogram" - // FlagPublicDashboards // enables public access to dashboards FlagPublicDashboards = "publicDashboards" diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 423057d6f3a..dc72fa4aaaa 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -18,14 +18,13 @@ import ( func TestFeatureToggleFiles(t *testing.T) { legacyNames := map[string]bool{ - "httpclientprovider_azure_auth": true, - "service-accounts": true, - "database_metrics": true, - "live-config": true, - "live-pipeline": true, - "live-service-web-worker": true, - "prometheus_azure_auth": true, - "disable_http_request_histogram": true, + "httpclientprovider_azure_auth": true, + "service-accounts": true, + "database_metrics": true, + "live-config": true, + "live-pipeline": true, + "live-service-web-worker": true, + "prometheus_azure_auth": true, } t.Run("verify files", func(t *testing.T) { From 338f6797b220adcf68e260f428abd44e9b0ec103 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 09:41:07 -0400 Subject: [PATCH 27/95] RBAC: Include alert.rules action when setting folder permissions (#49946) (#50006) (cherry picked from commit bdff63d4a8c25b6e2e400d278b11eb510db030e7) Co-authored-by: Karl Persson --- .../ossaccesscontrol/permissions_services.go | 11 +- .../accesscontrol/dashboard_permissions.go | 124 ++++++++++++++++++ .../sqlstore/migrations/migrations.go | 1 + 3 files changed, 134 insertions(+), 2 deletions(-) diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index e9b704cf548..5e697e9e5b6 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -175,8 +175,15 @@ type FolderPermissionsService struct { *resourcepermissions.Service } -var FolderViewActions = []string{dashboards.ActionFoldersRead} -var FolderEditActions = append(FolderViewActions, []string{dashboards.ActionFoldersWrite, dashboards.ActionFoldersDelete, dashboards.ActionDashboardsCreate}...) +var FolderViewActions = []string{dashboards.ActionFoldersRead, accesscontrol.ActionAlertingRuleRead} +var FolderEditActions = append(FolderViewActions, []string{ + dashboards.ActionFoldersWrite, + dashboards.ActionFoldersDelete, + dashboards.ActionDashboardsCreate, + accesscontrol.ActionAlertingRuleCreate, + accesscontrol.ActionAlertingRuleUpdate, + accesscontrol.ActionAlertingRuleDelete, +}...) var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...) func ProvideFolderPermissions( diff --git a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go index 7ba629bfd80..c11711ed0e7 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/dashboard_permissions.go @@ -294,3 +294,127 @@ func (m *managedFolderCreateAction) Exec(sess *xorm.Session, migrator *migrator. } return nil } + +const managedFolderAlertActionsMigratorID = "managed folder permissions alert actions migration" + +func AddManagedFolderAlertActionsMigration(mg *migrator.Migrator) { + mg.AddMigration(managedFolderAlertActionsMigratorID, &managedFolderAlertActionsMigrator{}) +} + +type managedFolderAlertActionsMigrator struct { + migrator.MigrationBase +} + +func (m *managedFolderAlertActionsMigrator) SQL(dialect migrator.Dialect) string { + return CodeMigrationSQL +} + +func (m *managedFolderAlertActionsMigrator) Exec(sess *xorm.Session, mg *migrator.Migrator) error { + var ids []interface{} + if err := sess.SQL("SELECT id FROM role WHERE name LIKE 'managed:%'").Find(&ids); err != nil { + return err + } + + if len(ids) == 0 { + return nil + } + + var permissions []ac.Permission + if err := sess.SQL("SELECT role_id, action, scope FROM permission WHERE role_id IN(?"+strings.Repeat(" ,?", len(ids)-1)+") AND scope LIKE 'folders:%'", ids...).Find(&permissions); err != nil { + return err + } + + mapped := make(map[int64]map[string][]ac.Permission, len(ids)-1) + for _, p := range permissions { + if mapped[p.RoleID] == nil { + mapped[p.RoleID] = make(map[string][]ac.Permission) + } + mapped[p.RoleID][p.Scope] = append(mapped[p.RoleID][p.Scope], p) + } + + var toAdd []ac.Permission + now := time.Now() + + for id, a := range mapped { + for scope, p := range a { + if hasFolderView(p) { + toAdd = append(toAdd, ac.Permission{ + RoleID: id, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAlertingRuleRead, + }) + } + + if hasFolderAdmin(p) || hasFolderEdit(p) { + toAdd = append( + toAdd, + ac.Permission{ + RoleID: id, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAlertingRuleCreate, + }, + ac.Permission{ + RoleID: id, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAlertingRuleDelete, + }, + ac.Permission{ + RoleID: id, + Updated: now, + Created: now, + Scope: scope, + Action: ac.ActionAlertingRuleUpdate, + }, + ) + } + } + } + + if len(toAdd) == 0 { + return nil + } + + err := batch(len(toAdd), batchSize, func(start, end int) error { + if _, err := sess.InsertMulti(toAdd[start:end]); err != nil { + return err + } + return nil + }) + + if err != nil { + return err + } + + return nil +} + +func hasFolderAdmin(permissions []ac.Permission) bool { + return hasActions(folderPermissionTranslation[models.PERMISSION_ADMIN], permissions) +} + +func hasFolderEdit(permissions []ac.Permission) bool { + return hasActions(folderPermissionTranslation[models.PERMISSION_EDIT], permissions) +} + +func hasFolderView(permissions []ac.Permission) bool { + return hasActions(folderPermissionTranslation[models.PERMISSION_VIEW], permissions) +} + +func hasActions(actions []string, permissions []ac.Permission) bool { + var contains int + for _, action := range actions { + for _, p := range permissions { + if action == p.Action { + contains++ + break + } + } + } + return contains >= len(actions) +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 3e60ddff3c2..5804bce53a1 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -89,6 +89,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { addDbFileStorageMigration(mg) accesscontrol.AddManagedPermissionsMigration(mg) + accesscontrol.AddManagedFolderAlertActionsMigration(mg) } func addMigrationLogMigrations(mg *Migrator) { From 3b5511db2bf56c2f5348044cdbcd71f88a9fe7b8 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 10:08:09 -0400 Subject: [PATCH 28/95] Loki: Run query when pressing Enter on line-filters (#49913) (#50004) * changed `onBlur` and `onKeyDown` handling - `onCommitChange` is only called if `onBlur` or `onKeyDown` are not set * added `runQueryOnEnter` flag to OperationParamDef * only run query if `runQueryOnEnter` is configured * changed `evt.type` check to `keydown` (cherry picked from commit b355adac6fccaa573c2f3101f2913b716166a844) Co-authored-by: svennergr --- .../loki/querybuilder/operations.ts | 4 ++ .../shared/AutoSizeInput.test.tsx | 48 +++++++++++++++++++ .../querybuilder/shared/AutoSizeInput.tsx | 10 ++-- .../shared/OperationParamEditor.tsx | 3 ++ .../prometheus/querybuilder/shared/types.ts | 1 + 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/loki/querybuilder/operations.ts b/public/app/plugins/datasource/loki/querybuilder/operations.ts index b0c7f481406..31cf3aca86c 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operations.ts @@ -202,6 +202,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { placeholder: 'Text to find', description: 'Find log lines that contains this text', minWidth: 20, + runQueryOnEnter: true, }, ], defaultParams: [''], @@ -223,6 +224,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { placeholder: 'Text to exclude', description: 'Find log lines that does not contain this text', minWidth: 26, + runQueryOnEnter: true, }, ], defaultParams: [''], @@ -244,6 +246,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { placeholder: 'Pattern to match', description: 'Find log lines that match this regex pattern', minWidth: 30, + runQueryOnEnter: true, }, ], defaultParams: [''], @@ -265,6 +268,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { placeholder: 'Pattern to exclude', description: 'Find log lines that does not match this regex pattern', minWidth: 30, + runQueryOnEnter: true, }, ], defaultParams: [''], diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx index 1bd9458fb50..031192b7cfc 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.test.tsx @@ -49,4 +49,52 @@ describe('AutoSizeInput', () => { fireEvent.change(input, { target: { value: 'very very long value' } }); expect(getComputedStyle(inputWrapper).width).toBe('304px'); }); + + it('should call onBlur if set when blurring', () => { + const onBlur = jest.fn(); + const onCommitChange = jest.fn(); + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + + fireEvent.blur(input); + + expect(onBlur).toHaveBeenCalled(); + expect(onCommitChange).not.toHaveBeenCalled(); + }); + + it('should call onCommitChange if not set when blurring', () => { + const onCommitChange = jest.fn(); + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + + fireEvent.blur(input); + + expect(onCommitChange).toHaveBeenCalled(); + }); + + it('should call onKeyDown if set when keydown', () => { + const onKeyDown = jest.fn(); + const onCommitChange = jest.fn(); + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(onKeyDown).toHaveBeenCalled(); + expect(onCommitChange).not.toHaveBeenCalled(); + }); + + it('should call onCommitChange if not set when keydown', () => { + const onCommitChange = jest.fn(); + render(); + + const input: HTMLInputElement = screen.getByTestId('autosize-input'); + + fireEvent.keyDown(input, { key: 'Enter' }); + + expect(onCommitChange).toHaveBeenCalled(); + }); }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx index a81e3c91010..639dcc8a781 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/AutoSizeInput.tsx @@ -30,19 +30,17 @@ export const AutoSizeInput = React.forwardRef((props, r }} width={inputWidth} onBlur={(event) => { - if (onCommitChange) { - onCommitChange(event); - } if (onBlur) { onBlur(event); + } else if (onCommitChange) { + onCommitChange(event); } }} onKeyDown={(event) => { - if (event.key === 'Enter' && onCommitChange) { - onCommitChange(event); - } if (onKeyDown) { onKeyDown(event); + } else if (event.key === 'Enter' && onCommitChange) { + onCommitChange(event); } }} data-testid={'autosize-input'} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx index 2ebe1431e34..0c375360522 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationParamEditor.tsx @@ -39,6 +39,9 @@ function SimpleInputParamEditor(props: QueryBuilderOperationParamEditorProps) { title={props.paramDef.description} onCommitChange={(evt) => { props.onChange(props.index, evt.currentTarget.value); + if (props.paramDef.runQueryOnEnter && evt.type === 'keydown') { + props.onRunQuery(); + } }} /> ); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts index 1b79b22a5a3..8839158c50f 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts @@ -70,6 +70,7 @@ export interface QueryBuilderOperationParamDef { description?: string; minWidth?: number; editor?: ComponentType; + runQueryOnEnter?: boolean; } export interface QueryBuilderOperationEditorProps { From 382eaaa773154536ae0dede7bb0febcd3fed779a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 10:10:02 -0400 Subject: [PATCH 29/95] Secrets: Fix unified secrets backwards compatibility (#49719) (#50009) * Fix unified secrets backwards compatibility * Add compatibility fix to AddDataSource function * Allow updating password on fail to decrypt secrets * If unified secret is corrupt try migrating (cherry picked from commit 470be98588f4da6509ea130324a210627dc8ee67) Co-authored-by: Guilherme Caulada --- .../datasources/service/datasource_service.go | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go index c95c6294fdf..b17ccb618fe 100644 --- a/pkg/services/datasources/service/datasource_service.go +++ b/pkg/services/datasources/service/datasource_service.go @@ -146,6 +146,12 @@ func (s *Service) GetDataSourcesByType(ctx context.Context, query *models.GetDat func (s *Service) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error { var err error + // this is here for backwards compatibility + cmd.EncryptedSecureJsonData, err = s.SecretsService.EncryptJsonData(ctx, cmd.SecureJsonData, secrets.WithoutScope()) + if err != nil { + return err + } + if err := s.SQLStore.AddDataSource(ctx, cmd); err != nil { return err } @@ -287,11 +293,10 @@ func (s *Service) DecryptedValues(ctx context.Context, ds *models.DataSource) (m } if exist { - err := json.Unmarshal([]byte(secret), &decryptedValues) - if err != nil { - return nil, err - } - } else if len(ds.SecureJsonData) > 0 { + err = json.Unmarshal([]byte(secret), &decryptedValues) + } + + if (!exist || err != nil) && len(ds.SecureJsonData) > 0 { decryptedValues, err = s.MigrateSecrets(ctx, ds) if err != nil { return nil, err @@ -302,9 +307,13 @@ func (s *Service) DecryptedValues(ctx context.Context, ds *models.DataSource) (m } func (s *Service) MigrateSecrets(ctx context.Context, ds *models.DataSource) (map[string]string, error) { - secureJsonData, err := s.SecretsService.DecryptJsonData(ctx, ds.SecureJsonData) - if err != nil { - return nil, err + secureJsonData := make(map[string]string) + for k, v := range ds.SecureJsonData { + decrypted, err := s.SecretsService.Decrypt(ctx, v) + if err != nil { + return nil, err + } + secureJsonData[k] = string(decrypted) } jsonData, err := json.Marshal(secureJsonData) @@ -579,5 +588,11 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *models.Update } } + // this is here for backwards compatibility + cmd.EncryptedSecureJsonData, err = s.SecretsService.EncryptJsonData(ctx, cmd.SecureJsonData, secrets.WithoutScope()) + if err != nil { + return err + } + return nil } From 6e93f497b2b64e0709ca9912735644da3c796f10 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 10:36:38 -0400 Subject: [PATCH 30/95] Alerting: Fix external alertmanager duplication (#49980) (#50008) * Fix external alertmanager duplication * Add tests (cherry picked from commit 9da41140aa08fed89b452d978c600148056f0759) Co-authored-by: Konrad Lalik --- .../hooks/useExternalAMSelector.test.ts | 37 +++++++++++++++++++ .../unified/hooks/useExternalAmSelector.ts | 30 +++++++-------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.ts b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.ts index bf613686bf1..2a30a8a757b 100644 --- a/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.ts +++ b/public/app/features/alerting/unified/hooks/useExternalAMSelector.test.ts @@ -127,4 +127,41 @@ describe('useExternalAmSelector', () => { }, ]); }); + + it('The number of alert managers should match config entries when there are multiple entries of the same url', () => { + useSelectorMock.mockImplementation((callback) => { + return callback( + createMockStoreState( + [ + { url: 'same/url/to/am/api/v2/alerts' }, + { url: 'same/url/to/am/api/v2/alerts' }, + { url: 'same/url/to/am/api/v2/alerts' }, + ], + [], + ['same/url/to/am', 'same/url/to/am', 'same/url/to/am'] + ) + ); + }); + + const alertmanagers = useExternalAmSelector(); + + expect(alertmanagers.length).toBe(3); + expect(alertmanagers).toEqual([ + { + url: 'same/url/to/am', + actualUrl: 'same/url/to/am/api/v2/alerts', + status: 'active', + }, + { + url: 'same/url/to/am', + actualUrl: 'same/url/to/am/api/v2/alerts', + status: 'active', + }, + { + url: 'same/url/to/am', + actualUrl: 'same/url/to/am/api/v2/alerts', + status: 'active', + }, + ]); + }); }); diff --git a/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts b/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts index 0a87417fefc..51a58075a13 100644 --- a/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts +++ b/public/app/features/alerting/unified/hooks/useExternalAmSelector.ts @@ -18,7 +18,7 @@ export function useExternalAmSelector(): AlertmanagerConfig[] | [] { } const enabledAlertmanagers: AlertmanagerConfig[] = []; - const droppedAlertmanagers: AlertmanagerConfig[] = discoveredAlertmanagers?.droppedAlertManagers.map((am) => ({ + const droppedAlertmanagers: AlertmanagerConfig[] = discoveredAlertmanagers.droppedAlertManagers.map((am) => ({ url: am.url.replace(SUFFIX_REGEX, ''), status: 'dropped', actualUrl: am.url, @@ -32,24 +32,20 @@ export function useExternalAmSelector(): AlertmanagerConfig[] | [] { actualUrl: '', }); } else { - let found = false; - for (const activeAM of discoveredAlertmanagers.activeAlertManagers) { - if (activeAM.url === `${url}/api/v2/alerts`) { - found = true; - enabledAlertmanagers.push({ - url: activeAM.url.replace(SUFFIX_REGEX, ''), + const matchingActiveAM = discoveredAlertmanagers.activeAlertManagers.find( + (am) => am.url === `${url}/api/v2/alerts` + ); + matchingActiveAM + ? enabledAlertmanagers.push({ + url: matchingActiveAM.url.replace(SUFFIX_REGEX, ''), status: 'active', - actualUrl: activeAM.url, + actualUrl: matchingActiveAM.url, + }) + : enabledAlertmanagers.push({ + url: url, + status: 'pending', + actualUrl: '', }); - } - } - if (!found) { - enabledAlertmanagers.push({ - url: url, - status: 'pending', - actualUrl: '', - }); - } } } From 56818c16fd0245c1ac204c2afbc35854953db128 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 11:13:12 -0400 Subject: [PATCH 31/95] SearchV2: Fix scroll issue in folder folder view page (#50010) (#50017) (cherry picked from commit 07bfa137708176bcbd71ac7215be8e3061f5598a) Co-authored-by: Maria Alexandra <239999+axelavargas@users.noreply.github.com> --- public/app/features/search/page/components/FolderSection.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index 167cc37afbc..f3adfc0b4d6 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -151,7 +151,7 @@ export const FolderSection: FC = ({ // Skip the folder wrapper if (renderStandaloneBody) { - return
{renderResults()}
; + return
{renderResults()}
; } return ( @@ -227,6 +227,9 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa icon: css` padding: 0 ${sm} 0 ${editable ? 0 : sm}; `, + folderViewResults: css` + overflow: auto; + `, text: css` flex-grow: 1; line-height: 24px; From ffa587352931137515adf1297d12da50822cbd06 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:47:59 -0400 Subject: [PATCH 32/95] DashList: Remove star z-index (#50029) Closes #49796 (cherry picked from commit 6ceb40e20e49f983f4a56c2556c54b62ad5f9d54) Co-authored-by: Alexander Kubyshkin --- public/app/plugins/panel/dashlist/DashList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/panel/dashlist/DashList.tsx b/public/app/plugins/panel/dashlist/DashList.tsx index 255ecffdb78..60fc7c569ee 100644 --- a/public/app/plugins/panel/dashlist/DashList.tsx +++ b/public/app/plugins/panel/dashlist/DashList.tsx @@ -232,7 +232,6 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => { display: 'flex', alignSelf: 'center', cursor: 'pointer', - zIndex: 100, }), checkBox: css({ appearance: 'none', From 835928cd01442bb72ef9ae58916403742376e98d Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 13:17:29 -0400 Subject: [PATCH 33/95] license: Make coremodels all Apache v2 (#49731) (#50032) (cherry picked from commit e5fab2dec8b503c049e06529dad667ef284c5ce2) Co-authored-by: sam boyer --- LICENSING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LICENSING.md b/LICENSING.md index 7506fcdc946..510ff42a32b 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -17,6 +17,8 @@ packages/grafana-toolkit/ packages/grafana-ui/ packages/jaeger-ui-components/ packaging/ +pkg/coremodel/ +pkg/framework/coremodel/ grafana-mixin/ cue/ ``` From a96aac39f3e56602d73608cb2e3739d4b05bbee2 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:35:07 -0500 Subject: [PATCH 34/95] Docs: intro docs refactor (#49545) (#50033) * intro docs refactor * adds content to the index file; incorporates feedback Signed-off-by: Jack Baldry * Fix alerting opt out relref Signed-off-by: Jack Baldry Co-authored-by: Chris Moyer * Convert front matter to YAML and add current aliases Signed-off-by: Jack Baldry Co-authored-by: Chris Moyer Co-authored-by: Jack Baldry (cherry picked from commit 1e3e9f3c687613933d7b38ce949b1ec55f0aed34) --- docs/sources/introduction/_index.md | 67 ++++++++++++-- docs/sources/introduction/grafana-cloud.md | 12 +++ .../introduction/grafana-enterprise.md | 88 +++++++++++++++++++ docs/sources/introduction/oss-details.md | 63 ------------- 4 files changed, 162 insertions(+), 68 deletions(-) create mode 100644 docs/sources/introduction/grafana-cloud.md create mode 100644 docs/sources/introduction/grafana-enterprise.md delete mode 100644 docs/sources/introduction/oss-details.md diff --git a/docs/sources/introduction/_index.md b/docs/sources/introduction/_index.md index 510b142b772..c6cad428c99 100644 --- a/docs/sources/introduction/_index.md +++ b/docs/sources/introduction/_index.md @@ -2,16 +2,73 @@ aliases: - /docs/grafana/latest/guides/what-is-grafana/ - /docs/grafana/latest/introduction/ + - /docs/grafana/latest/introduction/oss-details/ title: Introduction to Grafana weight: 5 --- -# Introduction to Grafana +# Grafana OSS -Grafana is a complete observability stack that allows you to monitor and analyze metrics, logs and traces. It allows you to query, visualize, alert on and understand your data no matter where it is stored. Create, explore, and share beautiful dashboards with your team and foster a data driven culture. For more information, refer to [Grafana overview](https://grafana.com/grafana/). Our observability stack has the following products and components. +[Grafana open source software](https://grafana.com/oss/) enables you to query, visualize, alert on, and explore your metrics, logs, and traces wherever they are stored. Grafana OSS provides you with tools to turn your time-series database (TSDB) data into insightful graphs and visualizations. -{{< docs/shared "basics/what-is-grafana.md" >}} +After you have [installed Grafana]({{< relref "../installation/_index.md" >}}) and set up your first dashboard using instructions in [Getting started with Grafana]({{< relref "../getting-started/build-first-dashboard.md" >}}), you will have many options to choose from depending on your requirements. For example, if you want to view weather data and statistics about your smart home, then you can create a [playlist]({{< relref "../dashboards/playlist.md" >}}). If you are the administrator for an enterprise and are managing Grafana for multiple teams, then you can set up [provisioning]({{< relref "../administration/provisioning.md" >}}) and [authentication]({{< relref "../auth/_index.md" >}}). -{{< docs/shared "basics/grafana-cloud.md" >}} +The following sections provide an overview of Grafana features and links to product documentation to help you learn more. For more guidance and ideas, check out our [Grafana Community forums](https://community.grafana.com/). -{{< docs/shared "basics/grafana-enterprise.md" >}} +## Explore metrics, logs, and traces + +Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. Refer to [Explore]({{< relref "../explore/_index.md" >}}) for more information. + +## Alerts + +If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. + +Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics. + +## Annotations + +Annotate graphs with rich events from different data sources. Hover over events to see the full event metadata and tags. + +This feature, which shows up as a graph marker in Grafana, is useful for correlating data in case something goes wrong. You can create the annotations manually—just control-click on a graph and input some text—or you can fetch data from any data source. Refer to [Annotations]({{< relref "../dashboards/annotations.md" >}}) for more information. + +## Dashboard variables + +[Template variables]({{< relref "../variables/_index.md" >}}) allow you to create dashboards that can be reused for lots of different use cases. Values aren't hard-coded with these templates, so for instance, if you have a production server and a test server, you can use the same dashboard for both. + +Templating allows you to drill down into your data, say, from all data to North America data, down to Texas data, and beyond. You can also share these dashboards across teams within your organization—or if you create a great dashboard template for a popular data source, you can contribute it to the whole community to customize and use. + +## Configure Grafana + +If you're a Grafana administrator, then you'll want to thoroughly familiarize yourself with [Grafana configuration options]({{< relref "../administration/configuration.md" >}}) and the [Grafana CLI]({{< relref "../administration/cli.md" >}}). + +Configuration covers both config files and environment variables. You can set up default ports, logging levels, email IP addresses, security, and more. + +## Import dashboards and plugins + +Discover hundreds of [dashboards](https://grafana.com/grafana/dashboards) and [plugins](https://grafana.com/grafana/plugins) in the official library. Thanks to the passion and momentum of community members, new ones are added every week. + +## Authentication + +Grafana supports different authentication methods, such as LDAP and OAuth, and allows you to map users to organizations. Refer to the [User authentication overview]({{< relref "../auth/overview.md" >}}) for more information. + +In Grafana Enterprise, you can also map users to teams: If your company has its own authentication system, Grafana allows you to map the teams in your internal systems to teams in Grafana. That way, you can automatically give people access to the dashboards designated for their teams. Refer to [Grafana Enterprise]({{< relref "../enterprise/_index.md" >}}) for more information. + +## Provisioning + +While it's easy to click, drag, and drop to create a single dashboard, power users in need of many dashboards will want to automate the setup with a script. You can script anything in Grafana. + +For example, if you're spinning up a new Kubernetes cluster, you can also spin up a Grafana automatically with a script that would have the right server, IP address, and data sources preset and locked in so users cannot change them. It's also a way of getting control over a lot of dashboards. Refer to [Provisioning]({{< relref "../administration/provisioning.md" >}}) for more information. + +## Permissions + +When organizations have one Grafana and multiple teams, they often want the ability to both keep things separate and share dashboards. You can create a team of users and then set permissions on [folders and dashboards]({{< relref "../administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md" >}}), and down to the [data source level]({{< relref "../enterprise/datasource_permissions.md" >}}) if you're using [Grafana Enterprise]({{< relref "../enterprise/_index.md" >}}). + +## Other Grafana Labs OSS Projects + +In addition to Grafana, Grafana Labs also provides the following open source projects: + +**Grafana Loki:** Grafana Loki is an open source, set of components that can be composed into a fully featured logging stack. For more information, refer to [Grafana Loki documentation](https://grafana.com/docs/loki/latest/). + +**Grafana Tempo:** Grafana Tempo is an open source, easy-to-use and high-volume distributed tracing backend. For more information, refer to [Grafana Tempo documentation](https://grafana.com/docs/tempo/latest/?pg=oss-tempo&plcmt=hero-txt/). + +**Grafana Mimir:** Grafana Mimir is an open source software project that provides a scalable long-term storage for Prometheus. For more information about Grafana Mimir, refer to [Grafana Mimir documentation](https://grafana.com/docs/mimir/latest/). diff --git a/docs/sources/introduction/grafana-cloud.md b/docs/sources/introduction/grafana-cloud.md new file mode 100644 index 00000000000..fae75e88c13 --- /dev/null +++ b/docs/sources/introduction/grafana-cloud.md @@ -0,0 +1,12 @@ +--- +aliases: + - /docs/grafana/latest/introduction/grafana-cloud/ +title: Grafana Cloud +weight: 300 +--- + +# Grafana Cloud + +Grafana Cloud is a highly available, fast, fully-managed OpenSaaS logging and metrics platform. It is everything you love about Grafana, hosted by Grafana Labs. + +[Learn more about Grafana Cloud](https://grafana.com/cloud/) and get started with your [free account with Grafana Cloud](https://grafana.com/signup/cloud/connect-account?pg=gsdocs) that includes a robust free tier with access to 10k metrics, 50GB logs, 50GB traces, two-week data retention, and three users. diff --git a/docs/sources/introduction/grafana-enterprise.md b/docs/sources/introduction/grafana-enterprise.md new file mode 100644 index 00000000000..20edde2eaa0 --- /dev/null +++ b/docs/sources/introduction/grafana-enterprise.md @@ -0,0 +1,88 @@ +--- +aliases: + - /docs/grafana/latest/enterprise/ + - /docs/grafana/latest/introduction/grafana-enterprise/ +description: Grafana Enterprise overview +title: Grafana Enterprise +weight: 200 +--- + +# Grafana Enterprise + +Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version. + +Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive datasource plugins]({{< relref "#enterprise-plugins">}}) and [additional features]({{< relref "#enterprise-features">}}). You also get 24x7x365 support and training from the core Grafana team. + +To learn more about Grafana Enterprise, refer to [our product page](https://grafana.com/enterprise). + +## Enterprise features in Grafana Cloud + +Many Grafana Enterprise features are also available in [Grafana Cloud]({{< ref "/docs/grafana-cloud" >}}) Pro and Advanced accounts. For details, refer to [the Grafana Cloud features table](https://grafana.com/pricing/#featuresTable) and [Enterprise features available to Grafana Cloud Pro and Advanced accounts]({{< ref "/docs/grafana-cloud/reference/enterprise-features" >}}). + +## Authentication + +Grafana Enterprise includes integrations with more ways to authenticate your users and enhanced authentication capabilities. + +### Team sync + +[Team sync]({{< relref "../enterprise/team-sync.md" >}}) allows you to set up synchronization between teams in Grafana and teams in your auth provider so that your users automatically end up in the right team. + +Supported auth providers: + +- [Auth Proxy]({{< relref "../auth/auth-proxy.md#team-sync-enterprise-only" >}}) +- [Azure AD OAuth]({{< relref "../auth/azuread.md#team-sync-enterprise-only" >}}) +- [GitHub OAuth]({{< relref "../auth/github.md#team-sync-enterprise-only" >}}) +- [GitLab OAuth]({{< relref "../auth/gitlab.md#team-sync-enterprise-only" >}}) +- [LDAP]({{< relref "../enterprise/enhanced_ldap.md#ldap-group-synchronization-for-teams" >}}) +- [Okta]({{< relref "../auth/okta.md#team-sync-enterprise-only" >}}) +- [SAML]({{< relref "../enterprise/configure-saml.md#configure-team-sync" >}}) + +### Enhanced LDAP integration + +With [enhanced LDAP integration]({{< relref "../enterprise/enhanced_ldap.md" >}}), you can set up active LDAP synchronization. + +### SAML authentication + +[SAML authentication]({{< relref "../enterprise/configure-saml" >}}) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML). + +## Enterprise features + +Grafana Enterprise adds the following features: + +- [Role-based access control]({{< relref "../enterprise/access-control/" >}}) to control access with role-based permissions. +- [Data source permissions]({{< relref "../enterprise/datasource_permissions.md" >}}) to restrict query access to specific teams and users. +- [Data source query caching]({{< relref "../enterprise/query-caching.md" >}}) to temporarily store query results in Grafana to reduce data source load and rate limiting. +- [Reporting]({{< relref "../enterprise/reporting.md" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whoever you choose. +- [Export dashboard as PDF]({{< relref "../enterprise/export-pdf.md" >}}) +- [White labeling]({{< relref "../enterprise/white-labeling.md" >}}) to customize Grafana from the brand and logo to the footer links. +- [Usage insights]({{< relref "../enterprise/usage-insights/" >}}) to understand how your Grafana instance is used. +- [Vault integration]({{< relref "../enterprise/vault.md" >}}) to manage your configuration or provisioning secrets with Vault. +- [Auditing]({{< relref "../enterprise/auditing.md" >}}) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements. +- [Request security]({{< relref "../enterprise/request-security.md" >}}) makes it possible to restrict outgoing requests from the Grafana server. +- [Settings updates at runtime]({{< relref "../enterprise/settings-updates.md" >}}) allows you to update Grafana settings at runtime without requiring a restart. + +## Enterprise data sources + +With a Grafana Enterprise license, you also get access to premium data sources, including: + +- [AppDynamics](https://grafana.com/grafana/plugins/dlopes7-appdynamics-datasource) +- [Azure Devops](https://grafana.com/grafana/plugins/grafana-azuredevops-datasource) +- [DataDog](https://grafana.com/grafana/plugins/grafana-datadog-datasource) +- [Dynatrace](https://grafana.com/grafana/plugins/grafana-dynatrace-datasource) +- [Gitlab](https://grafana.com/grafana/plugins/grafana-gitlab-datasource) +- [Honeycomb](https://grafana.com/grafana/plugins/grafana-honeycomb-datasource) +- [Jira](https://grafana.com/grafana/plugins/grafana-jira-datasource) +- [MongoDB](https://grafana.com/grafana/plugins/grafana-mongodb-datasource) +- [New Relic](https://grafana.com/grafana/plugins/grafana-newrelic-datasource) +- [Oracle Database](https://grafana.com/grafana/plugins/grafana-oracle-datasource) +- [Salesforce](https://grafana.com/grafana/plugins/grafana-salesforce-datasource) +- [SAP HANA®](https://grafana.com/grafana/plugins/grafana-saphana-datasource) +- [ServiceNow](https://grafana.com/grafana/plugins/grafana-servicenow-datasource) +- [Snowflake](https://grafana.com/grafana/plugins/grafana-snowflake-datasource) +- [Splunk](https://grafana.com/grafana/plugins/grafana-splunk-datasource) +- [Splunk Infrastructure monitoring (SignalFx)](https://grafana.com/grafana/plugins/grafana-splunk-monitoring-datasource) +- [Wavefront](https://grafana.com/grafana/plugins/grafana-wavefront-datasource) + +## Try Grafana Enterprise + +To purchase or obtain a trial license, contact the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). diff --git a/docs/sources/introduction/oss-details.md b/docs/sources/introduction/oss-details.md deleted file mode 100644 index cd4452e388b..00000000000 --- a/docs/sources/introduction/oss-details.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -aliases: - - /docs/grafana/latest/guides/what-is-grafana/ - - /docs/grafana/latest/introduction/oss-details/ -title: What is Grafana OSS -weight: 5 ---- - -# What is Grafana OSS - -[Grafana open source software](https://grafana.com/oss/) allows you to query, visualize, alert on, and understand your data no matter where it’s stored. With Grafana you can create, explore and share all of your data through elegant, flexible dashboards. - -After you have [installed Grafana]({{< relref "../installation/_index.md" >}}) and set up your first dashboard using instructions in [Getting started with Grafana]({{< relref "../getting-started/getting-started.md" >}}), you will have many options to choose from depending on your requirements. For example, if you want to view weather data and statistics about your smart home, then you can create a [playlist]({{< relref "../dashboards/playlist.md" >}}). If you are the administrator for an enterprise and are managing Grafana for multiple teams, then you can set up [provisioning]({{< relref "../administration/provisioning.md" >}}) and [authentication]({{< relref "../auth/_index.md" >}}). - -In the following sections, you can get an overview of the capabilities of Grafana features as well as links to the product documentation to help you learn more. For more guidance and ideas, check out our [Grafana Community forums](https://community.grafana.com/). - -## Explore metrics, logs, and traces - -Explore your data through ad-hoc queries and dynamic drilldown. Split view and compare different time ranges, queries and data sources side by side. Refer to [Explore]({{< relref "../explore/_index.md" >}}) for more information. - -## Alerts - -If you're using Grafana alerting, then you can have alerts sent through a number of different [alert notifiers]({{< relref "../alerting/contact-points/_index.md#list-of-notifiers-supported-by-grafana" >}}), including PagerDuty, SMS, email, VictorOps, OpsGenie, or Slack. - -Alert hooks allow you to create different notifiers with a bit of code if you prefer some other channels of communication. Visually define [alert rules]({{< relref "../alerting/alerting-rules/_index.md" >}}) for your most important metrics. - -## Annotations - -Annotate graphs with rich events from different data sources. Hover over events to see the full event metadata and tags. - -This feature, which shows up as a graph marker in Grafana, is useful for correlating data in case something goes wrong. You can create the annotations manually—just control-click on a graph and input some text—or you can fetch data from any data source. Refer to [Annotations]({{< relref "../dashboards/annotations.md" >}}) for more information. - -## Dashboard variables - -[Template variables]({{< relref "../variables/_index.md" >}}) allow you to create dashboards that can be reused for lots of different use cases. Values aren't hard-coded with these templates, so for instance, if you have a production server and a test server, you can use the same dashboard for both. - -Templating allows you to drill down into your data, say, from all data to North America data, down to Texas data, and beyond. You can also share these dashboards across teams within your organization—or if you create a great dashboard template for a popular data source, you can contribute it to the whole community to customize and use. - -## Configure Grafana - -If you're a Grafana administrator, then you'll want to thoroughly familiarize yourself with [Grafana configuration options]({{< relref "../administration/configuration.md" >}}) and the [Grafana CLI]({{< relref "../administration/cli.md" >}}). - -Configuration covers both config files and environment variables. You can set up default ports, logging levels, email IP addresses, security, and more. - -## Import dashboards and plugins - -Discover hundreds of [dashboards](https://grafana.com/grafana/dashboards) and [plugins](https://grafana.com/grafana/plugins) in the official library. Thanks to the passion and momentum of community members, new ones are added every week. - -## Authentication - -Grafana supports different authentication methods, such as LDAP and OAuth, and allows you to map users to organizations. Refer to the [User authentication overview]({{< relref "../auth/overview.md" >}}) for more information. - -In Grafana Enterprise, you can also map users to teams: If your company has its own authentication system, Grafana allows you to map the teams in your internal systems to teams in Grafana. That way, you can automatically give people access to the dashboards designated for their teams. Refer to [Grafana Enterprise]({{< relref "../enterprise/_index.md" >}}) for more information. - -## Provisioning - -While it's easy to click, drag, and drop to create a single dashboard, power users in need of many dashboards will want to automate the setup with a script. You can script anything in Grafana. - -For example, if you're spinning up a new Kubernetes cluster, you can also spin up a Grafana automatically with a script that would have the right server, IP address, and data sources preset and locked in so users cannot change them. It's also a way of getting control over a lot of dashboards. Refer to [Provisioning]({{< relref "../administration/provisioning.md" >}}) for more information. - -## Permissions - -When organizations have one Grafana and multiple teams, they often want the ability to both keep things separate and share dashboards. You can create a team of users and then set permissions on [folders and dashboards]({{< relref "../administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md" >}}), and down to the [data source level]({{< relref "../enterprise/datasource_permissions.md" >}}) if you're using [Grafana Enterprise]({{< relref "../enterprise/_index.md" >}}). From 08b84c11cb819b6b6354bf9753915ab80d69f5e3 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 13:41:51 -0400 Subject: [PATCH 35/95] CloudWatch: Add multi-value template variable support for log group names in logs query builder (#49737) (#50037) * Add multi-value template variable support for log group names * add test for multi-value template variable for log group names * add test (cherry picked from commit dca0453c2e6692b6b09d7ee247f01ebedab611a0) Co-authored-by: Kevin Yu --- .betterer.results | 4 +- .../__mocks__/CloudWatchDataSource.ts | 42 +++++++++++++++++++ .../components/LogsQueryField.test.tsx | 33 +++++++++++++++ .../cloudwatch/components/LogsQueryField.tsx | 3 +- .../datasource/cloudwatch/datasource.test.ts | 29 +++++++++++++ .../datasource/cloudwatch/datasource.ts | 10 +++-- .../cloudwatch/utils/datalinks.test.ts | 1 + .../datasource/cloudwatch/utils/datalinks.ts | 9 ++-- 8 files changed, 122 insertions(+), 9 deletions(-) diff --git a/.betterer.results b/.betterer.results index 14981d93b13..8f3c4abfac8 100644 --- a/.betterer.results +++ b/.betterer.results @@ -212,8 +212,8 @@ exports[`no enzyme tests`] = { "public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:1224072551": [ [0, 19, 13, "RegExp match", "2409514259"] ], - "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:2097436158": [ - [1, 19, 13, "RegExp match", "2409514259"] + "public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:1501504663": [ + [2, 19, 13, "RegExp match", "2409514259"] ], "public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:3481855642": [ [0, 26, 13, "RegExp match", "2409514259"] diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts index 5b907055215..bcb8d3a7584 100644 --- a/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts +++ b/public/app/plugins/datasource/cloudwatch/__mocks__/CloudWatchDataSource.ts @@ -149,3 +149,45 @@ export const dimensionVariable: CustomVariableModel = { ], multi: false, }; + +export const logGroupNamesVariable: CustomVariableModel = { + ...initialCustomVariableModelState, + id: 'groups', + name: 'groups', + current: { + value: ['templatedGroup-1', 'templatedGroup-2'], + text: ['templatedGroup-1', 'templatedGroup-2'], + selected: true, + }, + options: [ + { value: 'templatedGroup-1', text: 'templatedGroup-1', selected: true }, + { value: 'templatedGroup-2', text: 'templatedGroup-2', selected: true }, + ], + multi: true, +}; + +export const regionVariable: CustomVariableModel = { + ...initialCustomVariableModelState, + id: 'region', + name: 'region', + current: { + value: 'templatedRegion', + text: 'templatedRegion', + selected: true, + }, + options: [{ value: 'templatedRegion', text: 'templatedRegion', selected: true }], + multi: false, +}; + +export const expressionVariable: CustomVariableModel = { + ...initialCustomVariableModelState, + id: 'fields', + name: 'fields', + current: { + value: 'templatedField', + text: 'templatedField', + selected: true, + }, + options: [{ value: 'templatedField', text: 'templatedField', selected: true }], + multi: false, +}; diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx index 0aed8959533..b179c1bd260 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx @@ -1,8 +1,10 @@ import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { shallow } from 'enzyme'; import _, { DebouncedFunc } from 'lodash'; // eslint-disable-line lodash/import-scope import React from 'react'; import { act } from 'react-dom/test-utils'; +import { openMenu, select } from 'react-select-event'; import { SelectableValue } from '@grafana/data'; @@ -69,6 +71,7 @@ describe('CloudWatchLogsQueryField', () => { return Promise.resolve(['log_group_2']); } }, + getVariables: jest.fn().mockReturnValue([]), } as any } query={{} as any} @@ -201,6 +204,7 @@ describe('CloudWatchLogsQueryField', () => { .slice(0, Math.max(params.limit ?? 50, 50)); return Promise.resolve(theLogGroups); }, + getVariables: jest.fn().mockReturnValue([]), } as any } query={{} as any} @@ -235,4 +239,33 @@ describe('CloudWatchLogsQueryField', () => { .concat(['WaterGroup', 'WaterGroup2', 'WaterGroup3', 'VelvetGroup', 'VelvetGroup2', 'VelvetGroup3']) ); }); + + it('should render template variables a selectable option', async () => { + const { datasource } = setupMockedDataSource(); + const onChange = jest.fn(); + + render( + {}} + onChange={onChange} + /> + ); + + const logGroupSelector = await screen.findByLabelText('Log Groups'); + expect(logGroupSelector).toBeInTheDocument(); + + await openMenu(logGroupSelector); + const templateVariableSelector = await screen.findByText('Template Variables'); + expect(templateVariableSelector).toBeInTheDocument(); + + userEvent.click(templateVariableSelector); + await select(await screen.findByLabelText('Select option'), 'test'); + + expect(await screen.findByText('test')).toBeInTheDocument(); + }); }); diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx index c981310dd34..8d2ef81e7c6 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsQueryField.tsx @@ -27,6 +27,7 @@ import { CloudWatchLanguageProvider } from '../language_provider'; import syntax from '../syntax'; import { CloudWatchJsonData, CloudWatchLogsQuery, CloudWatchQuery } from '../types'; import { getStatsGroups } from '../utils/query/getStatsGroups'; +import { appendTemplateVariables } from '../utils/utils'; import QueryHeader from './QueryHeader'; @@ -310,7 +311,7 @@ export class CloudWatchLogsQueryField extends React.PureComponent { this.setSelectedLogGroups(v); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 964a17a061d..494a54492c6 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -6,11 +6,14 @@ import { setDataSourceSrv } from '@grafana/runtime'; import { dimensionVariable, + expressionVariable, labelsVariable, limitVariable, + logGroupNamesVariable, metricVariable, namespaceVariable, setupMockedDataSource, + regionVariable, } from './__mocks__/CloudWatchDataSource'; import { CloudWatchLogsQuery, @@ -65,6 +68,32 @@ describe('datasource', () => { }); }); + it('should interpolate multi-value template variable for log group names in the query', async () => { + const { datasource, fetchMock } = setupMockedDataSource({ + variables: [expressionVariable, logGroupNamesVariable, regionVariable], + mockGetVariableName: false, + }); + await lastValueFrom( + datasource + .query({ + targets: [ + { + queryMode: 'Logs', + region: '$region', + expression: 'fields $fields', + logGroupNames: ['$groups'], + }, + ], + } as any) + .pipe(toArray()) + ); + expect(fetchMock.mock.calls[0][0].data.queries[0]).toMatchObject({ + queryString: 'fields templatedField', + logGroupNames: ['templatedGroup-1', 'templatedGroup-2'], + region: 'templatedRegion', + }); + }); + it('should add links to log queries', async () => { const { datasource } = setupForLogs(); const observable = datasource.query({ diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 582d3016959..0c80835037a 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -233,6 +233,7 @@ export class CloudWatchDatasource options, this.timeSrv.timeRange(), this.replace.bind(this), + this.getVariableValue.bind(this), this.getActualRegion.bind(this), this.tracingDataSourceUid ); @@ -648,9 +649,12 @@ export class CloudWatchDatasource for (const fieldName of fieldsToReplace) { if (query.hasOwnProperty(fieldName)) { if (Array.isArray(anyQuery[fieldName])) { - anyQuery[fieldName] = anyQuery[fieldName].map((val: string) => - this.replace(val, options.scopedVars, true, fieldName) - ); + anyQuery[fieldName] = anyQuery[fieldName].flatMap((val: string) => { + if (fieldName === 'logGroupNames') { + return this.getVariableValue(val, options.scopedVars || {}); + } + return this.replace(val, options.scopedVars, true, fieldName); + }); } else { anyQuery[fieldName] = this.replace(anyQuery[fieldName], options.scopedVars, true, fieldName); } diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts index 25edfd33b16..bc0fa5e7380 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.test.ts @@ -52,6 +52,7 @@ describe('addDataLinksToLogsResponse', () => { mockOptions, { ...time, raw: time }, (s) => s ?? '', + (v) => [v] ?? [], (r) => r, 'xrayUid' ); diff --git a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts index 05f19e61925..e0f8f05c6a7 100644 --- a/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts +++ b/public/app/plugins/datasource/cloudwatch/utils/datalinks.ts @@ -16,10 +16,12 @@ export async function addDataLinksToLogsResponse( request: DataQueryRequest, range: TimeRange, replaceFn: ReplaceFn, + getVariableValueFn: (value: string, scopedVars: ScopedVars) => string[], getRegion: (region: string) => string, tracingDatasourceUid?: string ): Promise { const replace = (target: string, fieldName?: string) => replaceFn(target, request.scopedVars, true, fieldName); + const getVariableValue = (target: string) => getVariableValueFn(target, request.scopedVars); for (const dataFrame of response.data as DataFrame[]) { const curTarget = request.targets.find((target) => target.refId === dataFrame.refId) as CloudWatchLogsQuery; @@ -35,7 +37,7 @@ export async function addDataLinksToLogsResponse( } else { // Right now we add generic link to open the query in xray console to every field so it shows in the logs row // details. Unfortunately this also creates link for all values inside table which look weird. - field.config.links = [createAwsConsoleLink(curTarget, range, interpolatedRegion, replace)]; + field.config.links = [createAwsConsoleLink(curTarget, range, interpolatedRegion, replace, getVariableValue)]; } } } @@ -65,10 +67,11 @@ function createAwsConsoleLink( target: CloudWatchLogsQuery, range: TimeRange, region: string, - replace: (target: string, fieldName?: string) => string + replace: (target: string, fieldName?: string) => string, + getVariableValue: (value: string) => string[] ) { const interpolatedExpression = target.expression ? replace(target.expression) : ''; - const interpolatedGroups = target.logGroupNames?.map((logGroup: string) => replace(logGroup, 'log groups')) ?? []; + const interpolatedGroups = target.logGroupNames?.flatMap(getVariableValue) ?? []; const urlProps: AwsUrl = { end: range.to.toISOString(), From 1c335d1da723373434fa7ac7864b85848c591b84 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Wed, 1 Jun 2022 13:50:12 -0400 Subject: [PATCH 36/95] Tracing: Fix trace links in traces panel (#50028) (#50039) (cherry picked from commit bb94681d5a186982e49ce757097375db85bfb069) Co-authored-by: Connor Lindsey --- .../src/TraceTimelineViewer/SpanLinks.tsx | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx index b477c6e33e1..36b224e16b5 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanLinks.tsx @@ -18,12 +18,15 @@ const renderMenuItems = (links: SpanLinks, styles: ReturnType, { - if (link.onClick) { - link.onClick(e); - } - closeMenu(); - }} + onClick={ + link.onClick + ? (event) => { + event?.preventDefault(); + link.onClick!(event); + closeMenu(); + } + : undefined + } url={link.href} className={styles.menuItem} /> @@ -36,12 +39,15 @@ const renderMenuItems = (links: SpanLinks, styles: ReturnType, { - if (link.onClick) { - link.onClick(e); - } - closeMenu(); - }} + onClick={ + link.onClick + ? (event) => { + event?.preventDefault(); + link.onClick!(event); + closeMenu(); + } + : undefined + } url={link.href} className={styles.menuItem} /> @@ -54,12 +60,15 @@ const renderMenuItems = (links: SpanLinks, styles: ReturnType, { - if (link.onClick) { - link.onClick(e); - } - closeMenu(); - }} + onClick={ + link.onClick + ? (event) => { + event?.preventDefault(); + link.onClick!(event); + closeMenu(); + } + : undefined + } url={link.href} className={styles.menuItem} /> From 8efd4350b43c358625ab48a0e22ebdc1166d311a Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Wed, 1 Jun 2022 16:52:07 -0500 Subject: [PATCH 37/95] Alerting: Remove double quotes from matchers (#50038) (#50046) * Alerting: Remove double quotes from matchers With #38629 a new Alertmanager configuration object was introduced with `object_matchers`, it was meant to circumvent around the fact that Prometheus label names don't support a set of characters that Grafana needs to support for alerts, silences, matchers, etc. (with a common example being elasticsearch's `.`). This new object does not include the label of sanitzation or validation that its Prometheus equivalent supports in `matchers` and therefore are semantically not equivalent. This triggered the problem that when the migration is run, we use `matchers` as the object to populate in configuration for routing policies, but when the UI does its first save this object is transformed to `object_matchers`. Matchers that were previously running just fine would immediately stop working as soon as the configuration is saved. This problem surfaced with the introduction of #49952 where we stopped stripping double quotes from matchers (not just regex but _all_ of them). * Add comment explaining rationale and future removal Co-authored-by: Alex Weaver (cherry picked from commit 1a50b0dbb733399eed729850140fe9b080373568) Co-authored-by: gotjosh --- pkg/services/ngalert/CHANGELOG.md | 3 + .../api/tooling/definitions/alertmanager.go | 24 ++++- .../tooling/definitions/alertmanager_test.go | 101 ++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index c4d874552ce..179e1b035e1 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -54,11 +54,14 @@ Scopes must have an order to ensure consistency and ease of search, this helps u - [FEATURE] Indicate whether routes are provisioned when GETting Alertmanager configuration #47857 - [FEATURE] Indicate whether contact point is provisioned when GETting Alertmanager configuration #48323 - [FEATURE] Indicate whether alert rule is provisioned when GETting the rule #48458 +- [FEATURE] Alert rules with associated panels will take screenshots. #49293 #49338 #49374 #49377 #49378 #49379 #49381 #49385 #49439 #49445 - [BUGFIX] Migration: ignore alerts that do not belong to any existing organization\dashboard #49192 - [BUGFIX] Allow anonymous access to alerts #49203 - [BUGFIX] RBAC: replace create\update\delete actions for notification policies by alert.notifications:write #49185 - [BUGFIX] Fix access to alerts for Viewer role with editor permissions in folder #49270 - [FEATURE] Alert rules with associated panels will take screenshots. #49293 #49338 #49374 #49377 #49378 #49379 #49381 #49385 #49439 #49445 +- [BUGFIX] Alerting: Remove double quotes from double quoted matchers #50038 +- [ENHANCEMENT] Scheduler: ticker to support stopping #48142 ## 8.5.3 diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go index fa27afd52f3..3ef36cf7dc0 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "sort" + "strings" "time" "github.com/go-openapi/strfmt" @@ -727,7 +728,7 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error { return r.validateChild() } -// Return an alertmanager route from a Grafana route. The ObjectMatchers are converted to Matchers. +// AsAMRoute returns an Alertmanager route from a Grafana route. The ObjectMatchers are converted to Matchers. func (r *Route) AsAMRoute() *config.Route { amRoute := &config.Route{ Receiver: r.Receiver, @@ -753,7 +754,7 @@ func (r *Route) AsAMRoute() *config.Route { return amRoute } -// Return a Grafana route from an alertmanager route. The Matchers are converted to ObjectMatchers. +// AsGrafanaRoute returns a Grafana route from an Alertmanager route. The Matchers are converted to ObjectMatchers. func AsGrafanaRoute(r *config.Route) *Route { gRoute := &Route{ Receiver: r.Receiver, @@ -1226,6 +1227,22 @@ func (m *ObjectMatchers) UnmarshalYAML(unmarshal func(interface{}) error) error return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) } + // When Prometheus serializes a matcher, the value gets wrapped in quotes: + // https://github.com/prometheus/alertmanager/blob/main/pkg/labels/matcher.go#L77 + // Remove these quotes so that we are matching against the right value. + // + // This is a stop-gap solution which will be superceded by https://github.com/grafana/grafana/issues/50040. + // + // The ngalert migration converts matchers into the Prom-style, quotes included. + // The UI then stores the quotes into ObjectMatchers without removing them. + // This approach allows these extra quotes to be stored in the database, and fixes them at read time. + // This works because the database stores matchers as JSON text. + // + // There is a subtle bug here, where users might intentionally add quotes to matchers. This method can remove such quotes. + // Since ObjectMatchers will be deprecated entirely, this bug will go away naturally with time. + rawMatcher[2] = strings.TrimPrefix(rawMatcher[2], "\"") + rawMatcher[2] = strings.TrimSuffix(rawMatcher[2], "\"") + matcher, err := labels.NewMatcher(matchType, rawMatcher[0], rawMatcher[2]) if err != nil { return err @@ -1257,6 +1274,9 @@ func (m *ObjectMatchers) UnmarshalJSON(data []byte) error { return fmt.Errorf("unsupported match type %q in matcher", rawMatcher[1]) } + rawMatcher[2] = strings.TrimPrefix(rawMatcher[2], "\"") + rawMatcher[2] = strings.TrimSuffix(rawMatcher[2], "\"") + matcher, err := labels.NewMatcher(matchType, rawMatcher[0], rawMatcher[2]) if err != nil { return err diff --git a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go index a5c88d01621..726cea1d75c 100644 --- a/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go +++ b/pkg/services/ngalert/api/tooling/definitions/alertmanager_test.go @@ -939,6 +939,107 @@ func Test_ReceiverMatchesBackend(t *testing.T) { } } +func TestObjectMatchers_UnmarshalJSON(t *testing.T) { + j := `{ + "receiver": "autogen-contact-point-default", + "routes": [{ + "receiver": "autogen-contact-point-1", + "object_matchers": [ + [ + "a", + "=", + "MFR3Gxrnk" + ], + [ + "b", + "=", + "\"MFR3Gxrnk\"" + ], + [ + "c", + "=~", + "^[a-z0-9-]{1}[a-z0-9-]{0,30}$" + ], + [ + "d", + "=~", + "\"^[a-z0-9-]{1}[a-z0-9-]{0,30}$\"" + ] + ], + "group_interval": "3s", + "repeat_interval": "10s" + }] +}` + var r Route + if err := json.Unmarshal([]byte(j), &r); err != nil { + require.NoError(t, err) + } + + matchers := r.Routes[0].ObjectMatchers + + // Without quotes. + require.Equal(t, matchers[0].Name, "a") + require.Equal(t, matchers[0].Value, "MFR3Gxrnk") + + // With double quotes. + require.Equal(t, matchers[1].Name, "b") + require.Equal(t, matchers[1].Value, "MFR3Gxrnk") + + // Regexp without quotes. + require.Equal(t, matchers[2].Name, "c") + require.Equal(t, matchers[2].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") + + // Regexp with quotes. + require.Equal(t, matchers[3].Name, "d") + require.Equal(t, matchers[3].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") +} + +func TestObjectMatchers_UnmarshalYAML(t *testing.T) { + y := `--- +receiver: autogen-contact-point-default +routes: +- receiver: autogen-contact-point-1 + object_matchers: + - - a + - "=" + - MFR3Gxrnk + - - b + - "=" + - '"MFR3Gxrnk"' + - - c + - "=~" + - "^[a-z0-9-]{1}[a-z0-9-]{0,30}$" + - - d + - "=~" + - '"^[a-z0-9-]{1}[a-z0-9-]{0,30}$"' + group_interval: 3s + repeat_interval: 10s +` + + var r Route + if err := yaml.Unmarshal([]byte(y), &r); err != nil { + require.NoError(t, err) + } + + matchers := r.Routes[0].ObjectMatchers + + // Without quotes. + require.Equal(t, matchers[0].Name, "a") + require.Equal(t, matchers[0].Value, "MFR3Gxrnk") + + // With double quotes. + require.Equal(t, matchers[1].Name, "b") + require.Equal(t, matchers[1].Value, "MFR3Gxrnk") + + // Regexp without quotes. + require.Equal(t, matchers[2].Name, "c") + require.Equal(t, matchers[2].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") + + // Regexp with quotes. + require.Equal(t, matchers[3].Name, "d") + require.Equal(t, matchers[3].Value, "^[a-z0-9-]{1}[a-z0-9-]{0,30}$") +} + func Test_Marshaling_Validation(t *testing.T) { jsonEncoded, err := ioutil.ReadFile("alertmanager_test_artifact.json") require.Nil(t, err) From f591c6466130d637d30a57986cf9681119a43a0e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 03:03:55 -0400 Subject: [PATCH 38/95] etcd: Fix vuln CVE-2018-1098 (#49976) (#49977) * Update etcd * Update go.sum * Replace etcd with etcd/v3 (cherry picked from commit 0d7a3209e775398e5047d347e9f2e2843b06d44e) Co-authored-by: Dimitris Sotirakis --- go.mod | 5 +---- go.sum | 25 ++++++++++++++++++++-- pkg/infra/tracing/opentelemetry_tracing.go | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9175b472c39..6b52c350be9 100644 --- a/go.mod +++ b/go.mod @@ -11,9 +11,6 @@ replace github.com/denisenkom/go-mssqldb => github.com/grafana/go-mssqldb v0.0.0 // It's also present on grafana/loki's go.mod so we'll need till it gets updated. replace k8s.io/client-go => k8s.io/client-go v0.22.1 -// Github issue https://github.com/etcd-io/etcd/issues/11154 -replace go.etcd.io/etcd => go.etcd.io/etcd v0.0.0-20200520232829-54ba9589114f - replace github.com/russellhaering/goxmldsig@v1.1.0 => github.com/russellhaering/goxmldsig v1.1.1 require ( @@ -251,7 +248,7 @@ require ( github.com/golang-migrate/migrate/v4 v4.7.0 github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f github.com/grafana/thema v0.0.0-20220523183731-72aebd14e751 - go.etcd.io/etcd v3.3.25+incompatible + go.etcd.io/etcd/api/v3 v3.5.4 go.opentelemetry.io/contrib/propagators/jaeger v1.6.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.6.3 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.6.3 diff --git a/go.sum b/go.sum index 20ae68b9b25..bf1e8e0649e 100644 --- a/go.sum +++ b/go.sum @@ -585,6 +585,7 @@ github.com/cockroachdb/apd/v2 v2.0.1/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOG github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= +github.com/cockroachdb/datadriven v0.0.0-20190531201743-edce55837238/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= @@ -1217,6 +1218,7 @@ github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/ github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -1297,6 +1299,7 @@ github.com/gomodule/redigo v1.8.4/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUz github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= @@ -1473,6 +1476,7 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.2.0.20201207153454-9f6 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc= +github.com/grpc-ecosystem/grpc-gateway v1.4.1/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.4/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -1774,6 +1778,7 @@ github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+ github.com/jsimonetti/rtnetlink v0.0.0-20190830100107-3784a6c7c552/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -1844,6 +1849,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.0.0/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= @@ -1968,6 +1974,7 @@ github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mattn/go-xmlrpc v0.0.3/go.mod h1:mqc2dz7tP5x5BKlCahN/n+hs7OSZKJkS9JsHNBRlrxA= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/matttproud/golang_protobuf_extensions v1.0.0/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -2143,6 +2150,7 @@ github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -2275,6 +2283,7 @@ github.com/prometheus/alertmanager v0.23.1-0.20210914172521-e35efbddb66a/go.mod github.com/prometheus/alertmanager v0.23.1-0.20211116083607-e2a10119aaf7 h1:OMwDo53awRp+UzaBrwmVC7HJiAMYP/niBJfKcGpPiac= github.com/prometheus/alertmanager v0.23.1-0.20211116083607-e2a10119aaf7/go.mod h1:1UH4XA4DAXzsvofKVzcXmC0mqt6Y8BZP9JcQWKDmbFc= github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -2295,6 +2304,7 @@ github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3e github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20170216185247-6f3806018612/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= @@ -2304,6 +2314,7 @@ github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20180518154759-7600349dcfe1/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -2339,6 +2350,7 @@ github.com/prometheus/exporter-toolkit v0.7.0/go.mod h1:ZUBIj498ePooX9t/2xtDjeQY github.com/prometheus/node_exporter v1.0.0-rc.0.0.20200428091818-01054558c289 h1:dTUS1vaLWq+Y6XKOTnrFpoVsQKLCbCp1OLj24TDi7oM= github.com/prometheus/node_exporter v1.0.0-rc.0.0.20200428091818-01054558c289/go.mod h1:FGbBv5OPKjch+jNUJmEQpMZytIdyW0NdBtWFcfSKusc= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20180612222113-7d6f385de8be/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -2488,6 +2500,7 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546 h1:pXY9qYc/MP5zdvq github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/siebenmann/go-kstat v0.0.0-20160321171754-d34789b79745/go.mod h1:G81aIFAMS9ECrwBYR9YxhlPjWgrItd+Kje78O6+uqm8= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -2751,11 +2764,17 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd v0.0.0-20200520232829-54ba9589114f h1:uj/Xzadu0dyweesgOo+HghLYs0ssb9IRDvMp1UbAwJU= -go.etcd.io/etcd v0.0.0-20200520232829-54ba9589114f/go.mod h1:skWido08r9w6Lq/w70DO5XYIKMu4QFu1+4VsqLQuJy8= +go.etcd.io/etcd v0.0.0-20190709142735-eb7dd97135a5/go.mod h1:N0RPWo9FXJYZQI4BTkDtQylrstIigYHeR18ONnyTufk= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200520232829-54ba9589114f/go.mod h1:skWido08r9w6Lq/w70DO5XYIKMu4QFu1+4VsqLQuJy8= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd v3.3.25+incompatible h1:V1RzkZJj9LqsJRy+TUBgpWSbZXITLB819lstuTFoZOY= +go.etcd.io/etcd v3.3.25+incompatible/go.mod h1:yaeTdrJi5lOmYerz05bd8+V7KubZs8YSFZfzsF9A6aI= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= @@ -2898,6 +2917,7 @@ gocloud.dev v0.25.0 h1:Y7vDq8xj7SyM848KXf32Krda2e6jQ4CLh/mTeCSqXtk= gocloud.dev v0.25.0/go.mod h1:7HegHVCYZrMiU3IE1qtnzf/vRrDwLYnRNR3EhWX8x9Y= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180505025534-4ec37c66abab/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180608092829-8ac0e0d97ce4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -3570,6 +3590,7 @@ google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= 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-20180608181217-32ee49c4dd80/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= diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index 2fdd5724ce0..8d8e401c17d 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/level" "github.com/grafana/grafana/pkg/setting" - "go.etcd.io/etcd/version" + "go.etcd.io/etcd/api/v3/version" jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" From 77dccd54ca0c5f8ceed9fc53392f94759ffdcf29 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 03:23:44 -0400 Subject: [PATCH 39/95] Azure OAuth: silent fail on getting groups (#49909) (#50022) (cherry picked from commit 3049534c405e1f59a718c2f6b8d114cafa28bb4c) Co-authored-by: Gabriel MABILLE --- pkg/login/social/azuread_oauth.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index 774e7bdfa55..7296a90ce58 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "strings" @@ -214,9 +215,11 @@ func extractGroups(client *http.Client, claims azureClaims, token *oauth2.Token) if res.StatusCode != http.StatusOK { if res.StatusCode == http.StatusForbidden { logger.Warn("AzureAD OAuh: Token need GroupMember.Read.All permission to fetch all groups") - return []string{}, nil + } else { + body, _ := io.ReadAll(res.Body) + logger.Warn("AzureAD OAuh: could not fetch user groups", "code", res.StatusCode, "body", string(body)) } - return nil, errors.New("error fetching groups") + return []string{}, nil } var body getAzureGroupResponse From 63c1a2706fad5a73a5e7b01752b70174a73d951e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 04:09:39 -0400 Subject: [PATCH 40/95] API: Fix swagger specification (#50034) (#50060) * API: Fix swagger specification * Validate specification after generation (cherry picked from commit 6112bd0c63c6a97ae4f77b3a78b81377c3aefbd9) Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> --- Makefile | 4 ++-- pkg/api/docs/definitions/annotations.go | 2 +- pkg/api/docs/definitions/datasources.go | 1 - public/api-merged.json | 31 ++++++++++++++++--------- public/api-spec.json | 31 ++++++++++++++++--------- 5 files changed, 43 insertions(+), 26 deletions(-) diff --git a/Makefile b/Makefile index e4881487e56..12cd5e7f900 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ $(SPEC_TARGET): $(API_DEFINITION_FILES) ## Generate API spec -x "github.com/prometheus/alertmanager" \ -i /grafana/pkg/api/docs/tags.json -swagger-api-spec: gen-go $(SPEC_TARGET) $(MERGED_SPEC_TARGET) +swagger-api-spec: gen-go $(SPEC_TARGET) $(MERGED_SPEC_TARGET) validate-api-spec $(NGALERT_SPEC_TARGET): +$(MAKE) -C pkg/services/ngalert/api/tooling api.json @@ -67,7 +67,7 @@ ensure_go-swagger_mac: -x "github.com/prometheus/alertmanager" \ -i pkg/api/docs/tags.json -swagger-api-spec-mac: gen-go --swagger-api-spec-mac $(MERGED_SPEC_TARGET) +swagger-api-spec-mac: gen-go --swagger-api-spec-mac $(MERGED_SPEC_TARGET) validate-api-spec validate-api-spec: $(MERGED_SPEC_TARGET) ## Validate API spec docker run --rm -it \ diff --git a/pkg/api/docs/definitions/annotations.go b/pkg/api/docs/definitions/annotations.go index 019a02acd52..85a1e60333b 100644 --- a/pkg/api/docs/definitions/annotations.go +++ b/pkg/api/docs/definitions/annotations.go @@ -113,7 +113,7 @@ import ( // 401: unauthorisedError // 500: internalServerError -// swagger:parameters updateAnnotation patchAnnotation deleteAnnotation +// swagger:parameters getAnnotation updateAnnotation patchAnnotation deleteAnnotation type AnnotationIDParam struct { // in:path // required:true diff --git a/pkg/api/docs/definitions/datasources.go b/pkg/api/docs/definitions/datasources.go index 728dd8c813d..a7262cf64a9 100644 --- a/pkg/api/docs/definitions/datasources.go +++ b/pkg/api/docs/definitions/datasources.go @@ -334,7 +334,6 @@ import ( // 500: internalServerError // swagger:parameters updateDatasourceByID deleteDatasourceByID getDatasourceByID datasourceProxyGETcalls datasourceProxyPOSTcalls datasourceProxyDELETEcalls -// swagger:parameters enablePermissions disablePermissions getPermissions deletePermissions // swagger:parameters checkDatasourceHealthByID fetchDatasourceResourcesByID type DatasourceID struct { // in:path diff --git a/public/api-merged.json b/public/api-merged.json index 8fc1c36e0ea..34c267f27d9 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -2258,6 +2258,15 @@ "tags": ["annotations"], "summary": "Get Annotation by Id.", "operationId": "getAnnotation", + "parameters": [ + { + "type": "string", + "x-go-name": "AnnotationID", + "name": "annotation_id", + "in": "path", + "required": true + } + ], "responses": { "200": { "$ref": "#/responses/getAnnotationResponse" @@ -3809,7 +3818,7 @@ } } }, - "/datasources/{datasource_id}/disable-permissions": { + "/datasources/{datasourceId}/disable-permissions": { "post": { "description": "Disables permissions for the data source with the given id. All existing permissions will be removed and anyone will be able to query the data source.\n\nYou need to have a permission with action `datasources.permissions:toggle` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3819,7 +3828,7 @@ { "type": "string", "x-go-name": "DatasourceID", - "name": "id", + "name": "datasourceId", "in": "path", "required": true } @@ -3846,7 +3855,7 @@ } } }, - "/datasources/{datasource_id}/enable-permissions": { + "/datasources/{datasourceId}/enable-permissions": { "post": { "description": "Enables permissions for the data source with the given id.\nNo one except Org Admins will be able to query the data source until permissions have been added\nwhich permit certain users or teams to query the data source.\n\nYou need to have a permission with action `datasources.permissions:toggle` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3856,7 +3865,7 @@ { "type": "string", "x-go-name": "DatasourceID", - "name": "id", + "name": "datasourceId", "in": "path", "required": true } @@ -3883,7 +3892,7 @@ } } }, - "/datasources/{datasource_id}/permissions": { + "/datasources/{datasourceId}/permissions": { "get": { "description": "Gets all existing permissions for the data source with the given id.\n\nYou need to have a permission with action `datasources.permissions:read` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3893,7 +3902,7 @@ { "type": "string", "x-go-name": "DatasourceID", - "name": "id", + "name": "datasourceId", "in": "path", "required": true } @@ -3917,7 +3926,7 @@ } } }, - "/datasources/{datasource_id}/permissions/{permissionId}": { + "/datasources/{datasourceId}/permissions/{permissionId}": { "delete": { "description": "Removes the permission with the given permissionId for the data source with the given id.\n\nYou need to have a permission with action `datasources.permissions:delete` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3926,15 +3935,15 @@ "parameters": [ { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasourceId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "DatasourceID", - "name": "id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true } diff --git a/public/api-spec.json b/public/api-spec.json index b05dfd79a99..0f8f26e51fc 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -2258,6 +2258,15 @@ "tags": ["annotations"], "summary": "Get Annotation by Id.", "operationId": "getAnnotation", + "parameters": [ + { + "type": "string", + "x-go-name": "AnnotationID", + "name": "annotation_id", + "in": "path", + "required": true + } + ], "responses": { "200": { "$ref": "#/responses/getAnnotationResponse" @@ -3809,7 +3818,7 @@ } } }, - "/datasources/{datasource_id}/disable-permissions": { + "/datasources/{datasourceId}/disable-permissions": { "post": { "description": "Disables permissions for the data source with the given id. All existing permissions will be removed and anyone will be able to query the data source.\n\nYou need to have a permission with action `datasources.permissions:toggle` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3819,7 +3828,7 @@ { "type": "string", "x-go-name": "DatasourceID", - "name": "id", + "name": "datasourceId", "in": "path", "required": true } @@ -3846,7 +3855,7 @@ } } }, - "/datasources/{datasource_id}/enable-permissions": { + "/datasources/{datasourceId}/enable-permissions": { "post": { "description": "Enables permissions for the data source with the given id.\nNo one except Org Admins will be able to query the data source until permissions have been added\nwhich permit certain users or teams to query the data source.\n\nYou need to have a permission with action `datasources.permissions:toggle` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3856,7 +3865,7 @@ { "type": "string", "x-go-name": "DatasourceID", - "name": "id", + "name": "datasourceId", "in": "path", "required": true } @@ -3883,7 +3892,7 @@ } } }, - "/datasources/{datasource_id}/permissions": { + "/datasources/{datasourceId}/permissions": { "get": { "description": "Gets all existing permissions for the data source with the given id.\n\nYou need to have a permission with action `datasources.permissions:read` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3893,7 +3902,7 @@ { "type": "string", "x-go-name": "DatasourceID", - "name": "id", + "name": "datasourceId", "in": "path", "required": true } @@ -3917,7 +3926,7 @@ } } }, - "/datasources/{datasource_id}/permissions/{permissionId}": { + "/datasources/{datasourceId}/permissions/{permissionId}": { "delete": { "description": "Removes the permission with the given permissionId for the data source with the given id.\n\nYou need to have a permission with action `datasources.permissions:delete` and scopes `datasources:*`, `datasources:id:*`, `datasources:id:1` (single data source).", "tags": ["datasource_permissions", "enterprise"], @@ -3926,15 +3935,15 @@ "parameters": [ { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasourceId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "DatasourceID", - "name": "id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true } From 063c5095eb125e125ece366363e300a9d60c1e7a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 05:26:13 -0400 Subject: [PATCH 41/95] Loki: do not produce histogram for instant queries (#50019) (#50065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * loki: no histogram for instant queries * added test (cherry picked from commit b0925ed4ee822645729ddc294d07b0cf54f04346) Co-authored-by: Gábor Farkas --- .../datasource/loki/datasource.test.ts | 11 +++++++- .../app/plugins/datasource/loki/datasource.ts | 28 +++++++++++-------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index 1919f610ee2..515e773d9e2 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -25,7 +25,7 @@ import { CustomVariableModel } from '../../../features/variables/types'; import { isMetricsQuery, LokiDatasource, RangeQueryOptions } from './datasource'; import { makeMockLokiDatasource } from './mocks'; -import { LokiQuery, LokiResponse, LokiResultType } from './types'; +import { LokiQuery, LokiQueryType, LokiResponse, LokiResultType } from './types'; jest.mock('@grafana/runtime', () => ({ // @ts-ignore @@ -997,6 +997,15 @@ describe('LokiDatasource', () => { expect(ds.getLogsVolumeDataProvider(options)).toBeDefined(); }); + + it('does not create provider if there is only an instant logs query', () => { + const ds = createLokiDSForTests(); + const options = getQueryOptions({ + targets: [{ expr: '{label=value', refId: 'A', queryType: LokiQueryType.Instant }], + }); + + expect(ds.getLogsVolumeDataProvider(options)).not.toBeDefined(); + }); }); describe('importing queries', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 828b3be499b..f051816185e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -141,22 +141,28 @@ export class LokiDatasource } getLogsVolumeDataProvider(request: DataQueryRequest): Observable | undefined { - const isLogsVolumeAvailable = request.targets.some((target) => target.expr && !isMetricsQuery(target.expr)); + const isQuerySuitable = (query: LokiQuery) => { + const normalized = getNormalizedLokiQuery(query); + const { expr } = normalized; + // it has to be a logs-producing range-query + return expr && !isMetricsQuery(expr) && normalized.queryType === LokiQueryType.Range; + }; + + const isLogsVolumeAvailable = request.targets.some(isQuerySuitable); + if (!isLogsVolumeAvailable) { return undefined; } const logsVolumeRequest = cloneDeep(request); - logsVolumeRequest.targets = logsVolumeRequest.targets - .filter((target) => target.expr && !isMetricsQuery(target.expr)) - .map((target) => { - return { - ...target, - instant: false, - volumeQuery: true, - expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`, - }; - }); + logsVolumeRequest.targets = logsVolumeRequest.targets.filter(isQuerySuitable).map((target) => { + return { + ...target, + instant: false, + volumeQuery: true, + expr: `sum by (level) (count_over_time(${target.expr}[$__interval]))`, + }; + }); return queryLogsVolume(this, logsVolumeRequest, { extractLevel, From cac5a1945f52d4f54d60cb245fde0b0afdda046a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 05:26:27 -0400 Subject: [PATCH 42/95] Cloudwatch: Dynamic labels autocomplete (#49794) (#50063) * add completeable interface * add basic labels language * render monaco editor for label field * align styling in math expression field * add unit tests * fix broken test * remove unused import * use theme * remove comment * pr feedback * fix broken imports * improve test * make it possible to override code editor styles * use input styles and align border styles (cherry picked from commit 467e375fe6c3de0309a69664b32301e22c0f5f7e) Co-authored-by: Erik Sundell --- .../src/components/Monaco/CodeEditor.tsx | 4 +- .../grafana-ui/src/components/Monaco/types.ts | 2 + .../afterLabelValue.ts | 23 ++++++ .../dynamic-label-test-data/index.ts | 2 + .../insideLabelValue.ts | 23 ++++++ .../cloudwatch/__mocks__/monarch/Monaco.ts | 9 ++ .../components/DynamicLabelsField.tsx | 82 +++++++++++++++++++ .../components/MathExpressionQueryField.tsx | 5 +- .../MetricsQueryEditor.test.tsx | 12 ++- .../MetricsQueryEditor/MetricsQueryEditor.tsx | 15 ++-- .../CompletionItemProvider.test.ts | 46 +++++++++++ .../dynamic-labels/CompletionItemProvider.ts | 71 ++++++++++++++++ .../cloudwatch/dynamic-labels/definition.ts | 10 +++ .../cloudwatch/dynamic-labels/language.ts | 52 ++++++++++++ .../monarch/CompletionItemProvider.ts | 4 +- .../datasource/cloudwatch/monarch/register.ts | 4 +- .../datasource/cloudwatch/monarch/types.ts | 9 ++ 17 files changed, 355 insertions(+), 18 deletions(-) create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/dynamic-label-test-data/afterLabelValue.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/dynamic-label-test-data/index.ts create mode 100644 public/app/plugins/datasource/cloudwatch/__mocks__/dynamic-label-test-data/insideLabelValue.ts create mode 100644 public/app/plugins/datasource/cloudwatch/components/DynamicLabelsField.tsx create mode 100644 public/app/plugins/datasource/cloudwatch/dynamic-labels/CompletionItemProvider.test.ts create mode 100644 public/app/plugins/datasource/cloudwatch/dynamic-labels/CompletionItemProvider.ts create mode 100644 public/app/plugins/datasource/cloudwatch/dynamic-labels/definition.ts create mode 100644 public/app/plugins/datasource/cloudwatch/dynamic-labels/language.ts diff --git a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx index c6f8dad7157..ada037d8b66 100644 --- a/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx +++ b/packages/grafana-ui/src/components/Monaco/CodeEditor.tsx @@ -111,7 +111,7 @@ class UnthemedCodeEditor extends React.PureComponent { const value = this.props.value ?? ''; const longText = value.length > 100; - const styles = getStyles(theme); + const containerStyles = this.props.containerStyles ?? getStyles(theme).container; const options: MonacoOptions = { wordWrap: 'off', @@ -143,7 +143,7 @@ class UnthemedCodeEditor extends React.PureComponent { } return ( -
+
void; + onRunQuery: () => void; + label: string; + width: number; +} + +export function DynamicLabelsField({ label, width, onChange, onRunQuery }: Props) { + const theme = useTheme2(); + const styles = getInputStyles({ theme, width }); + const containerRef = useRef(null); + const onEditorMount = useCallback( + (editor: monacoType.editor.IStandaloneCodeEditor, monaco: Monaco) => { + editor.onDidFocusEditorText(() => editor.trigger(TRIGGER_SUGGEST.id, TRIGGER_SUGGEST.id, {})); + editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { + const text = editor.getValue(); + onChange(text); + onRunQuery(); + }); + + const containerDiv = containerRef.current; + containerDiv !== null && editor.layout({ width: containerDiv.clientWidth, height: containerDiv.clientHeight }); + }, + [onChange, onRunQuery] + ); + + return ( +
+ { + if (value !== label) { + onChange(value); + onRunQuery(); + } + }} + onBeforeEditorMount={(monaco: Monaco) => + registerLanguage(monaco, language, dynamicLabelsCompletionItemProvider) + } + onEditorDidMount={onEditorMount} + /> +
+ ); +} diff --git a/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx index b9e89f2daa1..4a8964fbb44 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MathExpressionQueryField.tsx @@ -37,7 +37,7 @@ export function MathExpressionQueryField({ const updateElementHeight = () => { const containerDiv = containerRef.current; if (containerDiv !== null && editor.getContentHeight() < 200) { - const pixelHeight = editor.getContentHeight(); + const pixelHeight = Math.max(32, editor.getContentHeight()); containerDiv.style.height = `${pixelHeight}px`; containerDiv.style.width = '100%'; const pixelWidth = containerDiv.clientWidth; @@ -68,6 +68,9 @@ export function MathExpressionQueryField({ }, suggestFontSize: 12, wordWrap: 'on', + padding: { + top: 6, + }, }} language={language.id} value={query} diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx index 92349bb29e6..f99d0289837 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.test.tsx @@ -4,6 +4,7 @@ import selectEvent from 'react-select-event'; import { DataSourceInstanceSettings } from '@grafana/data'; import { config } from '@grafana/runtime'; +import * as ui from '@grafana/ui'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { CustomVariableModel, initialVariableModelState } from '../../../../../features/variables/types'; @@ -12,6 +13,13 @@ import { CloudWatchJsonData, MetricEditorMode, MetricQueryType } from '../../typ import { MetricsQueryEditor, Props } from './MetricsQueryEditor'; +jest.mock('@grafana/ui', () => ({ + ...jest.requireActual('@grafana/ui'), + CodeEditor: function CodeEditor({ value }: { value: string }) { + return
{value}
; + }, +})); + const setup = () => { const instanceSettings = { jsonData: { defaultRegion: 'us-east-1' }, @@ -173,9 +181,7 @@ describe('QueryEditor', () => { expect(screen.getByText('Label')).toBeInTheDocument(); expect(screen.queryByText('Alias')).toBeNull(); - expect(screen.getByLabelText('Label - optional')).toHaveValue( - "Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}" - ); + expect(screen.getByText("Period: ${PROP('Period')} InstanceId: ${PROP('Dim.InstanceId')}")); config.featureToggles.cloudWatchDynamicLabels = originalValue; }); diff --git a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 6f61af7724f..7b8b1e39149 100644 --- a/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -16,6 +16,7 @@ import { MetricQueryType, MetricStat, } from '../../types'; +import { DynamicLabelsField } from '../DynamicLabelsField'; import QueryHeader from '../QueryHeader'; import { Alias } from './Alias'; @@ -138,14 +139,12 @@ export const MetricsQueryEditor = (props: Props) => { optional tooltip="Change time series legend name using Dynamic labels. See documentation for details." > - ) => - onChange({ ...preparedQuery, label: event.target.value }) - } - /> + props.onChange({ ...query, label })} + > ) : ( { + const setup = new DynamicLabelsCompletionItemProvider(); + const monaco = MonacoMock as Monaco; + const provider = setup.getCompletionProvider(monaco, cloudWatchDynamicLabelsLanguageDefinition); + const { suggestions } = await provider.provideCompletionItems( + TextModel(value) as monacoTypes.editor.ITextModel, + position + ); + return suggestions; +}; + +describe('Dynamic labels: CompletionItemProvider', () => { + describe('getSuggestions', () => { + it('returns all dynamic labels in case current token is a whitespace', async () => { + const { query, position } = afterLabelValue; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toEqual(DYNAMIC_LABEL_PATTERNS.length + 1); // + 1 for the dimension suggestions + }); + + it('should return suggestion for dimension label that has high prio', async () => { + const { query, position } = afterLabelValue; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toBeTruthy(); + const highPrioSuggestsions = suggestions.filter((s) => s.sortText === CompletionItemPriority.High); + expect(highPrioSuggestsions.length).toBe(1); + expect(highPrioSuggestsions[0].label).toBe("${PROP('Dim.')}"); + }); + + it('doesnt return suggestions if cursor is inside a dynamic label', async () => { + const { query, position } = insideLabelValue; + const suggestions = await getSuggestions(query, position); + expect(suggestions.length).toBe(0); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/dynamic-labels/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/dynamic-labels/CompletionItemProvider.ts new file mode 100644 index 00000000000..043d607e52c --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/dynamic-labels/CompletionItemProvider.ts @@ -0,0 +1,71 @@ +import type { Monaco, monacoTypes } from '@grafana/ui'; + +import { linkedTokenBuilder } from '../monarch/linkedTokenBuilder'; +import { LanguageDefinition } from '../monarch/register'; +import { Completeable, CompletionItemPriority, TokenTypes } from '../monarch/types'; + +import { DYNAMIC_LABEL_PATTERNS } from './language'; + +type CompletionItem = monacoTypes.languages.CompletionItem; + +export class DynamicLabelsCompletionItemProvider implements Completeable { + tokenTypes: TokenTypes; + + constructor() { + this.tokenTypes = { + Parenthesis: 'delimiter.parenthesis.cloudwatch-dynamicLabels', + Whitespace: 'white.cloudwatch-dynamicLabels', + Keyword: 'keyword.cloudwatch-dynamicLabels', + Delimiter: 'delimiter.cloudwatch-dynamicLabels', + Operator: 'operator.cloudwatch-dynamicLabels', + Identifier: 'identifier.cloudwatch-dynamicLabels', + Type: 'type.cloudwatch-dynamicLabels', + Function: 'predefined.cloudwatch-dynamicLabels', + Number: 'number.cloudwatch-dynamicLabels', + String: 'string.cloudwatch-dynamicLabels', + Variable: 'variable.cloudwatch-dynamicLabels', + }; + } + + // called by registerLanguage and passed to monaco with registerCompletionItemProvider + // returns an object that implements https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.CompletionItemProvider.html + getCompletionProvider(monaco: Monaco, languageDefinition: LanguageDefinition) { + return { + triggerCharacters: [' ', '$', ',', '(', "'"], // one of these characters indicates that it is time to look for a suggestion + provideCompletionItems: async (model: monacoTypes.editor.ITextModel, position: monacoTypes.IPosition) => { + const currentToken = linkedTokenBuilder(monaco, languageDefinition, model, position, this.tokenTypes); + const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis(); + const range = + invalidRangeToken || !currentToken?.range ? monaco.Range.fromPositions(position) : currentToken?.range; + const toCompletionItem = (value: string, rest: Partial = {}) => { + const item: CompletionItem = { + label: value, + insertText: value, + kind: monaco.languages.CompletionItemKind.Field, + range, + sortText: CompletionItemPriority.Medium, + ...rest, + }; + return item; + }; + let suggestions: CompletionItem[] = []; + const next = currentToken?.next; + if (!currentToken?.isFunction() && (!next || next.isWhiteSpace())) { + suggestions = DYNAMIC_LABEL_PATTERNS.map((val) => toCompletionItem(val)); + // always insert suggestion for dimension value and allow user to complete pattern by providing the dimension name + suggestions.push( + toCompletionItem("${PROP('Dim.')}", { + sortText: CompletionItemPriority.High, + insertText: `\${PROP('Dim.$0')} `, + insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + }) + ); + } + + return { + suggestions, + }; + }, + }; + } +} diff --git a/public/app/plugins/datasource/cloudwatch/dynamic-labels/definition.ts b/public/app/plugins/datasource/cloudwatch/dynamic-labels/definition.ts new file mode 100644 index 00000000000..3791b9c2b2e --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/dynamic-labels/definition.ts @@ -0,0 +1,10 @@ +import { LanguageDefinition } from '../monarch/register'; + +const cloudWatchDynamicLabelsLanguageDefinition: LanguageDefinition = { + id: 'cloudwatch-dynamicLabels', + extensions: [], + aliases: [], + mimetypes: [], + loader: () => import('./language'), +}; +export default cloudWatchDynamicLabelsLanguageDefinition; diff --git a/public/app/plugins/datasource/cloudwatch/dynamic-labels/language.ts b/public/app/plugins/datasource/cloudwatch/dynamic-labels/language.ts new file mode 100644 index 00000000000..690334c5b6a --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/dynamic-labels/language.ts @@ -0,0 +1,52 @@ +import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; + +// Dynamic labels: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/graph-dynamic-labels.html +export const DYNAMIC_LABEL_PATTERNS = [ + '${DATAPOINT_COUNT}', + '${FIRST}', + '${FIRST_LAST_RANGE}', + '${FIRST_LAST_TIME_RANGE}', + '${FIRST_TIME}', + '${FIRST_TIME_RELATIVE}', + '${LABEL}', + '${LAST}', + '${LAST_TIME}', + '${LAST_TIME_RELATIVE}', + '${MAX}', + '${MAX_TIME}', + '${MAX_TIME_RELATIVE}', + '${MIN}', + '${MIN_MAX_RANGE}', + '${MIN_MAX_TIME_RANGE}', + '${MIN_TIME}', + '${MIN_TIME_RELATIVE}', + "${PROP('AccountId')}", + "${PROP('MetricName')}", + "${PROP('Namespace')}", + "${PROP('Period')}", + "${PROP('Region')}", + "${PROP('Stat')}", + '${SUM}', +]; + +export const language: monacoType.languages.IMonarchLanguage = { + id: 'dynamicLabels', + ignoreCase: false, + tokenizer: { + root: [ + { include: '@whitespace' }, + { include: '@builtInFunctions' }, + { include: '@string' }, + [/\$\{PROP\('Dim.[a-zA-Z0-9-_]?.*'\)\}+/, 'predefined'], //custom handling for dimension patterns + ], + builtInFunctions: [[DYNAMIC_LABEL_PATTERNS.map(escapeRegExp).join('|'), 'predefined']], + whitespace: [[/\s+/, 'white']], + string: [], + }, +}; + +export const conf: monacoType.languages.LanguageConfiguration = {}; + +function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} diff --git a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts index cf2d1dfed30..6f1b1bcbfa2 100644 --- a/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/CompletionItemProvider.ts @@ -6,7 +6,7 @@ import { CloudWatchDatasource } from '../datasource'; import { LinkedToken } from './LinkedToken'; import { linkedTokenBuilder } from './linkedTokenBuilder'; import { LanguageDefinition } from './register'; -import { StatementPosition, SuggestionKind, TokenTypes } from './types'; +import { Completeable, StatementPosition, SuggestionKind, TokenTypes } from './types'; type CompletionItem = monacoTypes.languages.CompletionItem; @@ -17,7 +17,7 @@ CompletionItemProvider is an extendable class which needs to implement : - getSuggestionKinds - getSuggestions */ -export class CompletionItemProvider { +export class CompletionItemProvider implements Completeable { templateVariables: string[]; datasource: CloudWatchDatasource; templateSrv: TemplateSrv; diff --git a/public/app/plugins/datasource/cloudwatch/monarch/register.ts b/public/app/plugins/datasource/cloudwatch/monarch/register.ts index 3a3f7b4e2d6..4b552a4763a 100644 --- a/public/app/plugins/datasource/cloudwatch/monarch/register.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/register.ts @@ -2,7 +2,7 @@ import type * as monacoType from 'monaco-editor/esm/vs/editor/editor.api'; import { Monaco } from '@grafana/ui'; -import { CompletionItemProvider } from './CompletionItemProvider'; +import { Completeable } from './types'; export type LanguageDefinition = { id: string; @@ -18,7 +18,7 @@ export type LanguageDefinition = { export const registerLanguage = ( monaco: Monaco, language: LanguageDefinition, - completionItemProvider: CompletionItemProvider + completionItemProvider: Completeable ) => { const { id, loader } = language; diff --git a/public/app/plugins/datasource/cloudwatch/monarch/types.ts b/public/app/plugins/datasource/cloudwatch/monarch/types.ts index 5ca960e034e..c10424b78f6 100644 --- a/public/app/plugins/datasource/cloudwatch/monarch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/monarch/types.ts @@ -1,5 +1,7 @@ import { monacoTypes } from '@grafana/ui'; +import { LanguageDefinition } from './register'; + export interface TokenTypes { Parenthesis: string; Whitespace: string; @@ -98,3 +100,10 @@ export interface Monaco { Range: Range; languages: Languages; } + +export interface Completeable { + getCompletionProvider( + monaco: Monaco, + languageDefinition: LanguageDefinition + ): monacoTypes.languages.CompletionItemProvider; +} From 2fc2a15fabede0f8cc7a9889651aeb1839099522 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 05:44:30 -0400 Subject: [PATCH 43/95] Alerting: Remove Image Upload code from Slack notifier. (#50062) (#50066) The image file upload code as it is now simply doesn't work - it's missing several important steps in the file upload process. There is more information in the fixed issue as to the steps required. After this change, screenshots will still be attached to slack messages when external image storage is used with Grafana (an S3 bucket, for example). Fixes #50056 (cherry picked from commit 9759eeda17158e6784c075c5b19c2fade4558535) Co-authored-by: Joe Blubaugh --- .../ngalert/notifier/channels/slack.go | 107 ++-------------- .../ngalert/notifier/channels/slack_test.go | 117 ------------------ 2 files changed, 8 insertions(+), 216 deletions(-) diff --git a/pkg/services/ngalert/notifier/channels/slack.go b/pkg/services/ngalert/notifier/channels/slack.go index e1132d3c819..d6b76f8dbfd 100644 --- a/pkg/services/ngalert/notifier/channels/slack.go +++ b/pkg/services/ngalert/notifier/channels/slack.go @@ -8,8 +8,6 @@ import ( "errors" "fmt" "io" - "math/rand" - "mime/multipart" "net" "net/http" "net/url" @@ -27,7 +25,6 @@ import ( ) var SlackAPIEndpoint = "https://slack.com/api/chat.postMessage" -var SlackImageAPIEndpoint = "https://slack.com/api/files.upload" // SlackNotifier is responsible for sending // alert notification to Slack. @@ -39,7 +36,6 @@ type SlackNotifier struct { webhookSender notifications.WebhookSender URL *url.URL - ImageUploadURL string Username string IconEmoji string IconURL string @@ -55,7 +51,6 @@ type SlackNotifier struct { type SlackConfig struct { *NotificationChannelConfig URL *url.URL - ImageUploadURL string Username string IconEmoji string IconURL string @@ -83,7 +78,6 @@ func NewSlackConfig(factoryConfig FactoryConfig) (*SlackConfig, error) { channelConfig := factoryConfig.Config decryptFunc := factoryConfig.DecryptFunc endpointURL := channelConfig.Settings.Get("endpointUrl").MustString(SlackAPIEndpoint) - imageUploadURL := channelConfig.Settings.Get("imageUploadUrl").MustString(SlackImageAPIEndpoint) slackURL := decryptFunc(context.Background(), channelConfig.SecureSettings, "url", channelConfig.Settings.Get("url").MustString()) if slackURL == "" { slackURL = endpointURL @@ -127,7 +121,6 @@ func NewSlackConfig(factoryConfig FactoryConfig) (*SlackConfig, error) { MentionUsers: mentionUsers, MentionGroups: mentionGroups, URL: apiURL, - ImageUploadURL: imageUploadURL, Username: channelConfig.Settings.Get("username").MustString("Grafana"), IconEmoji: channelConfig.Settings.Get("icon_emoji").MustString(), IconURL: channelConfig.Settings.Get("icon_url").MustString(), @@ -152,7 +145,6 @@ func NewSlackNotifier(config *SlackConfig, Settings: config.Settings, }), URL: config.URL, - ImageUploadURL: config.ImageUploadURL, Recipient: config.Recipient, MentionUsers: config.MentionUsers, MentionGroups: config.MentionGroups, @@ -196,6 +188,7 @@ type attachment struct { // Notify sends an alert notification to Slack. func (sn *SlackNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, error) { + sn.log.Debug("building slack message", "alerts", len(alerts)) msg, err := sn.buildSlackMessage(ctx, alerts) if err != nil { return false, fmt.Errorf("build slack message: %w", err) @@ -227,46 +220,18 @@ func (sn *SlackNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo return false, err } - var imgData io.ReadCloser - - // Try to upload if we have an image path but no image URL. This uploads the file - // immediately after the message. A bit of a hack, but it doesn't require the - // user to have an image host set up. - // TODO: We need a refactoring so we don't do two database reads for the same data. - if len(msg.Attachments[0].ImageURL) == 0 { - _ = withStoredImage(ctx, sn.log, sn.images, - func(index int, image *ngmodels.Image) error { - if image == nil || len(image.Path) == 0 { - return nil - } - - imgData, err = openImage(image.Path) - if err != nil { - imgData = nil - } - - return nil - }, - 0, alerts...) - - if imgData != nil { - defer func() { - _ = imgData.Close() - }() - - err = sn.slackFileUpload(ctx, imgData, sn.Recipient, sn.Token) - if err != nil { - sn.log.Warn("Error reading screenshot data from ImageStore: %v", err) - } - } - } - return true, nil } // sendSlackRequest sends a request to the Slack API. // Stubbable by tests. -var sendSlackRequest = func(request *http.Request, logger log.Logger) error { +var sendSlackRequest = func(request *http.Request, logger log.Logger) (retErr error) { + defer func() { + if retErr != nil { + logger.Warn("failed to send slack request", "err", retErr) + } + }() + netTransport := &http.Transport{ TLSClientConfig: &tls.Config{ Renegotiation: tls.RenegotiateFreelyAsClient, @@ -407,59 +372,3 @@ func (sn *SlackNotifier) buildSlackMessage(ctx context.Context, alrts []*types.A func (sn *SlackNotifier) SendResolved() bool { return !sn.GetDisableResolveMessage() } - -func (sn *SlackNotifier) slackFileUpload(ctx context.Context, data io.Reader, recipient, token string) error { - sn.log.Info("Uploading to slack via file.upload API") - headers, uploadBody, err := sn.generateFileUploadBody(data, token, recipient) - if err != nil { - return err - } - cmd := &models.SendWebhookSync{ - Url: sn.ImageUploadURL, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST", - } - if err := sn.webhookSender.SendWebhookSync(ctx, cmd); err != nil { - sn.log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") - return err - } - return nil -} - -func (sn *SlackNotifier) generateFileUploadBody(data io.Reader, token string, recipient string) (map[string]string, bytes.Buffer, error) { - // Slack requires all POSTs to files.upload to present - // an "application/x-www-form-urlencoded" encoded querystring - // See https://api.slack.com/methods/files.upload - var b bytes.Buffer - w := multipart.NewWriter(&b) - defer func() { - if err := w.Close(); err != nil { - // Shouldn't matter since we already close w explicitly on the non-error path - sn.log.Warn("Failed to close multipart writer", "err", err) - } - }() - - // TODO: perhaps we should pass the filename through to here to use the local name. - // https://github.com/grafana/grafana/issues/49375 - fw, err := w.CreateFormFile("file", fmt.Sprintf("screenshot-%v", rand.Intn(2e6))) - if err != nil { - return nil, b, err - } - if _, err := io.Copy(fw, data); err != nil { - return nil, b, err - } - // Add the authorization token - if err := w.WriteField("token", token); err != nil { - return nil, b, err - } - // Add the channel(s) to POST to - if err := w.WriteField("channels", recipient); err != nil { - return nil, b, err - } - if err := w.Close(); err != nil { - return nil, b, fmt.Errorf("failed to close multipart writer: %w", err) - } - headers := map[string]string{ - "Content-Type": w.FormDataContentType(), - "Authorization": "auth_token=\"" + token + "\"", - } - return headers, b, nil -} diff --git a/pkg/services/ngalert/notifier/channels/slack_test.go b/pkg/services/ngalert/notifier/channels/slack_test.go index 82fc387a986..83899b44f59 100644 --- a/pkg/services/ngalert/notifier/channels/slack_test.go +++ b/pkg/services/ngalert/notifier/channels/slack_test.go @@ -39,14 +39,6 @@ func TestSlackNotifier(t *testing.T) { Token: "test-with-url", URL: "https://www.example.com/image.jpg", }, - { - Token: "test-with-path-not-found", - Path: "usr/home/nouser/noway.jpg", - }, - { - Token: "test-with-path-found", - Path: f.Name(), // Has the full path because of how CreateTemp works. - }, }, } @@ -172,78 +164,6 @@ func TestSlackNotifier(t *testing.T) { }, expMsgError: nil, }, - { - name: "Image URL with path but no file creates message with no error", - settings: `{ - "token": "1234", - "image_upload_url": "https://www.webhook.com", - "recipient": "#testchannel", - "icon_emoji": ":emoji:" - }`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-path-not-found"}, - }, - }, - }, - expMsg: &slackMessage{ - Channel: "#testchannel", - Username: "Grafana", - IconEmoji: ":emoji:", - Attachments: []attachment{ - { - Title: "[FIRING:1] (val1)", - TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", - Fallback: "[FIRING:1] (val1)", - Fields: nil, - Footer: "Grafana v" + setting.BuildVersion, - FooterIcon: "https://grafana.com/assets/img/fav32.png", - Color: "#D63232", - Ts: 0, - }, - }, - }, - expMsgError: nil, - }, - { - name: "Image URL with path and file creates message and uploads image", - settings: `{ - "token": "1234", - "recipient": "#testchannel", - "icon_emoji": ":emoji:" - }`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-path-found"}, - }, - }, - }, - expMsg: &slackMessage{ - Channel: "#testchannel", - Username: "Grafana", - IconEmoji: ":emoji:", - Attachments: []attachment{ - { - Title: "[FIRING:1] (val1)", - TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", - Fallback: "[FIRING:1] (val1)", - Fields: nil, - Footer: "Grafana v" + setting.BuildVersion, - FooterIcon: "https://grafana.com/assets/img/fav32.png", - Color: "#D63232", - Ts: 0, - }, - }, - }, - expMsgError: nil, - expWebhookURL: SlackImageAPIEndpoint, - }, { name: "Correct config with multiple alerts and template", settings: `{ @@ -334,43 +254,6 @@ func TestSlackNotifier(t *testing.T) { }, expMsgError: nil, }, - { - name: "Custom image upload URL", - settings: `{ - "token": "1234", - "recipient": "#testchannel", - "icon_emoji": ":emoji:", - "imageUploadUrl": "https://custom-domain.upload" - }`, - alerts: []*types.Alert{ - { - Alert: model.Alert{ - Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, - Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-with-path-found"}, - }, - }, - }, - expMsg: &slackMessage{ - Channel: "#testchannel", - Username: "Grafana", - IconEmoji: ":emoji:", - Attachments: []attachment{ - { - Title: "[FIRING:1] (val1)", - TitleLink: "http://localhost/alerting/list", - Text: "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", - Fallback: "[FIRING:1] (val1)", - Fields: nil, - Footer: "Grafana v" + setting.BuildVersion, - FooterIcon: "https://grafana.com/assets/img/fav32.png", - Color: "#D63232", - Ts: 0, - }, - }, - }, - expMsgError: nil, - expWebhookURL: "https://custom-domain.upload", - }, } for _, c := range cases { From 0ba2bf265379e52ca81eed86f161dc0d0f5f7e6e Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 08:13:57 -0400 Subject: [PATCH 44/95] [v9.0.x] Draggable: only set drag handle props on the drag handle itself (#50076) Co-authored-by: Ashley Harrison --- .betterer.results | 2 +- .../QueryOperationRow.test.tsx | 8 +- .../QueryOperationRow/QueryOperationRow.tsx | 110 ++++------------ .../QueryOperationRowHeader.tsx | 123 ++++++++++++++++++ .../OrganizeFieldsTransformerEditor.tsx | 15 ++- 5 files changed, 162 insertions(+), 96 deletions(-) create mode 100644 public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx diff --git a/.betterer.results b/.betterer.results index 8f3c4abfac8..eddcb083542 100644 --- a/.betterer.results +++ b/.betterer.results @@ -125,7 +125,7 @@ exports[`no enzyme tests`] = { "public/app/core/components/QueryOperationRow/QueryOperationAction.test.tsx:3032694716": [ [0, 19, 13, "RegExp match", "2409514259"] ], - "public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx:2026575657": [ + "public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx:3743889097": [ [0, 26, 13, "RegExp match", "2409514259"] ], "public/app/core/components/Select/FolderPicker.test.tsx:993468764": [ diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx index afcccbf93fb..b046539d99f 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRow.test.tsx @@ -60,7 +60,7 @@ describe('QueryOperationRow', () => { describe('headerElement rendering', () => { it('should render headerElement provided as element', () => { const title =
Test
; - const wrapper = shallow( + const wrapper = mount(
Test
@@ -72,7 +72,7 @@ describe('QueryOperationRow', () => { it('should render headerElement provided as function', () => { const title = () =>
Test
; - const wrapper = shallow( + const wrapper = mount(
Test
@@ -101,7 +101,7 @@ describe('QueryOperationRow', () => { describe('actions rendering', () => { it('should render actions provided as element', () => { const actions =
Test
; - const wrapper = shallow( + const wrapper = mount(
Test
@@ -112,7 +112,7 @@ describe('QueryOperationRow', () => { }); it('should render actions provided as function', () => { const actions = () =>
Test
; - const wrapper = shallow( + const wrapper = mount(
Test
diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx index 85f32fdfa08..331e189e976 100644 --- a/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx +++ b/public/app/core/components/QueryOperationRow/QueryOperationRow.tsx @@ -1,11 +1,13 @@ -import { css, cx } from '@emotion/css'; +import { css } from '@emotion/css'; import React, { useCallback, useState } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { useUpdateEffect } from 'react-use'; import { GrafanaTheme } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; -import { Icon, ReactUtils, stylesFactory, useTheme } from '@grafana/ui'; +import { ReactUtils, stylesFactory, useTheme } from '@grafana/ui'; + +import { QueryOperationRowHeader } from './QueryOperationRowHeader'; interface QueryOperationRowProps { index: number; @@ -93,41 +95,25 @@ export const QueryOperationRow: React.FC = ({ const actionsElement = actions && ReactUtils.renderOrCallToRender(actions, renderPropArgs); const headerElementRendered = headerElement && ReactUtils.renderOrCallToRender(headerElement, renderPropArgs); - const rowHeader = ( -
-
- - {title && ( -
-
{titleElement}
-
- )} - {headerElementRendered} -
- -
- {actionsElement} - {draggable && ( - - )} -
-
- ); - if (draggable) { return ( {(provided) => { - const dragHandleProps = { ...provided.dragHandleProps, role: 'group' }; // replace the role="button" because it causes https://dequeuniversity.com/rules/axe/4.3/nested-interactive?application=msftAI return ( <>
-
- {rowHeader} +
+
{isContentVisible &&
{children}
}
@@ -140,7 +126,16 @@ export const QueryOperationRow: React.FC = ({ return (
- {rowHeader} + {isContentVisible &&
{children}
}
); @@ -151,63 +146,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => { wrapper: css` margin-bottom: ${theme.spacing.md}; `, - header: css` - label: Header; - padding: ${theme.spacing.xs} ${theme.spacing.sm}; - border-radius: ${theme.border.radius.sm}; - background: ${theme.colors.bg2}; - min-height: ${theme.spacing.formInputHeight}px; - display: grid; - grid-template-columns: minmax(100px, max-content) min-content; - align-items: center; - justify-content: space-between; - white-space: nowrap; - - &:focus { - outline: none; - } - `, - column: css` - label: Column; - display: flex; - align-items: center; - `, - dragIcon: css` - cursor: grab; - color: ${theme.colors.textWeak}; - &:hover { - color: ${theme.colors.text}; - } - `, - collapseIcon: css` - color: ${theme.colors.textWeak}; - cursor: pointer; - &:hover { - color: ${theme.colors.text}; - } - `, - titleWrapper: css` - display: flex; - align-items: center; - flex-grow: 1; - cursor: pointer; - overflow: hidden; - margin-right: ${theme.spacing.sm}; - `, - title: css` - font-weight: ${theme.typography.weight.semibold}; - color: ${theme.colors.textBlue}; - margin-left: ${theme.spacing.sm}; - overflow: hidden; - text-overflow: ellipsis; - `, content: css` margin-top: ${theme.spacing.inlineFormMargin}; margin-left: ${theme.spacing.lg}; `, - disabled: css` - color: ${theme.colors.textWeak}; - `, }; }); diff --git a/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx new file mode 100644 index 00000000000..9b71a51a226 --- /dev/null +++ b/public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx @@ -0,0 +1,123 @@ +import { css, cx } from '@emotion/css'; +import React, { MouseEventHandler } from 'react'; +import { DraggableProvidedDragHandleProps } from 'react-beautiful-dnd'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Icon, useStyles2 } from '@grafana/ui'; + +interface QueryOperationRowHeaderProps { + actionsElement?: React.ReactNode; + disabled?: boolean; + draggable: boolean; + dragHandleProps?: DraggableProvidedDragHandleProps; + headerElement?: React.ReactNode; + isContentVisible: boolean; + onRowToggle: () => void; + reportDragMousePosition: MouseEventHandler; + titleElement?: React.ReactNode; +} + +export const QueryOperationRowHeader: React.FC = ({ + actionsElement, + disabled, + draggable, + dragHandleProps, + headerElement, + isContentVisible, + onRowToggle, + reportDragMousePosition, + titleElement, +}: QueryOperationRowHeaderProps) => { + const styles = useStyles2(getStyles); + + return ( +
+
+ + {titleElement && ( +
+
{titleElement}
+
+ )} + {headerElement} +
+ +
+ {actionsElement} + {draggable && ( + + )} +
+
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + header: css` + label: Header; + padding: ${theme.spacing(0.5, 0.5)}; + border-radius: ${theme.shape.borderRadius(1)}; + background: ${theme.colors.background.secondary}; + min-height: ${theme.spacing(4)}; + display: grid; + grid-template-columns: minmax(100px, max-content) min-content; + align-items: center; + justify-content: space-between; + white-space: nowrap; + + &:focus { + outline: none; + } + `, + column: css` + label: Column; + display: flex; + align-items: center; + `, + dragIcon: css` + cursor: grab; + color: ${theme.colors.text.disabled}; + margin: ${theme.spacing(0, 0.5)}; + &:hover { + color: ${theme.colors.text}; + } + `, + collapseIcon: css` + color: ${theme.colors.text.disabled}; + cursor: pointer; + &:hover { + color: ${theme.colors.text}; + } + `, + titleWrapper: css` + display: flex; + align-items: center; + flex-grow: 1; + cursor: pointer; + overflow: hidden; + margin-right: ${theme.spacing(0.5)}; + `, + title: css` + font-weight: ${theme.typography.fontWeightBold}; + color: ${theme.colors.text.link}; + margin-left: ${theme.spacing(0.5)}; + overflow: hidden; + text-overflow: ellipsis; + `, + disabled: css` + color: ${theme.colors.text.disabled}; + `, +}); + +QueryOperationRowHeader.displayName = 'QueryOperationRowHeader'; diff --git a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx index 1365aaf58dc..3fc97f52fa1 100644 --- a/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx +++ b/public/app/features/transformers/editors/OrganizeFieldsTransformerEditor.tsx @@ -131,15 +131,16 @@ const DraggableFieldName: React.FC = ({ return ( {(provided) => ( -
+
- + Date: Thu, 2 Jun 2022 08:23:03 -0400 Subject: [PATCH 45/95] Alerting: Fix notification policy "Override grouping" form save (#50031) (#50078) (cherry picked from commit ace5b2058d05112eafe63b1cd8834bd6281c3734) Co-authored-by: Matthew Jacobson --- .../components/amroutes/AmRootRouteForm.tsx | 2 +- .../amroutes/AmRoutesExpandedForm.tsx | 9 +- .../components/amroutes/AmRoutesTable.test.ts | 1 + .../components/amroutes/AmRoutesTable.tsx | 2 +- .../alerting/unified/types/amroutes.ts | 1 + .../alerting/unified/utils/amroutes.test.ts | 91 +++++++++++++++++++ .../alerting/unified/utils/amroutes.ts | 8 +- 7 files changed, 104 insertions(+), 10 deletions(-) create mode 100644 public/app/features/alerting/unified/utils/amroutes.test.ts diff --git a/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx b/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx index 5375476a99a..3a98e732783 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRootRouteForm.tsx @@ -36,7 +36,7 @@ export const AmRootRouteForm: FC = ({ const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(routes.groupBy)); return ( -
+ {({ control, errors, setValue }) => ( <> diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx index d2f28c173a9..d95f984a09c 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedForm.tsx @@ -44,7 +44,6 @@ export interface AmRoutesExpandedFormProps { export const AmRoutesExpandedForm: FC = ({ onCancel, onSave, receivers, routes }) => { const styles = useStyles2(getStyles); const formStyles = useStyles2(getFormStyles); - const [overrideGrouping, setOverrideGrouping] = useState(routes.groupBy.length > 0); const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(routes.groupBy)); const muteTimingOptions = useMuteTimingOptions(); @@ -159,13 +158,9 @@ export const AmRoutesExpandedForm: FC = ({ onCancel, - setOverrideGrouping((overrideGrouping) => !overrideGrouping)} - /> + - {overrideGrouping && ( + {watch().overrideGrouping && ( = ({ { id: 'groupBy', label: 'Group by', - renderCell: (item) => item.data.groupBy.join(', ') || '-', + renderCell: (item) => (item.data.overrideGrouping && item.data.groupBy.join(', ')) || '-', size: 5, }, { diff --git a/public/app/features/alerting/unified/types/amroutes.ts b/public/app/features/alerting/unified/types/amroutes.ts index 0e15feb8e77..b52b56e489c 100644 --- a/public/app/features/alerting/unified/types/amroutes.ts +++ b/public/app/features/alerting/unified/types/amroutes.ts @@ -5,6 +5,7 @@ export interface FormAmRoute { object_matchers: MatcherFieldValue[]; continue: boolean; receiver: string; + overrideGrouping: boolean; groupBy: string[]; overrideTimings: boolean; groupWaitValue: string; diff --git a/public/app/features/alerting/unified/utils/amroutes.test.ts b/public/app/features/alerting/unified/utils/amroutes.test.ts new file mode 100644 index 00000000000..20448a0a9de --- /dev/null +++ b/public/app/features/alerting/unified/utils/amroutes.test.ts @@ -0,0 +1,91 @@ +import { Route } from 'app/plugins/datasource/alertmanager/types'; + +import { FormAmRoute } from '../types/amroutes'; + +import { amRouteToFormAmRoute, emptyRoute, formAmRouteToAmRoute } from './amroutes'; + +const emptyAmRoute: Route = { + receiver: '', + group_by: [], + continue: false, + object_matchers: [], + matchers: [], + match: {}, + match_re: {}, + group_wait: '', + group_interval: '', + repeat_interval: '', + routes: [], + mute_time_intervals: [], +}; + +const buildAmRoute = (override: Partial = {}): Route => { + return { ...emptyAmRoute, ...override }; +}; + +const buildFormAmRoute = (override: Partial = {}): FormAmRoute => { + return { ...emptyRoute, ...override }; +}; + +describe('formAmRouteToAmRoute', () => { + describe('when called with overrideGrouping=false', () => { + it('Should not set groupBy', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ id: '1', overrideGrouping: false, groupBy: ['SHOULD NOT BE SET'] }); + + // Act + const amRoute = formAmRouteToAmRoute('test', route, {}); + + // Assert + expect(amRoute.group_by).toStrictEqual([]); + }); + }); + + describe('when called with overrideGrouping=true', () => { + it('Should set groupBy', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ id: '1', overrideGrouping: true, groupBy: ['SHOULD BE SET'] }); + + // Act + const amRoute = formAmRouteToAmRoute('test', route, {}); + + // Assert + expect(amRoute.group_by).toStrictEqual(['SHOULD BE SET']); + }); + }); +}); + +describe('amRouteToFormAmRoute', () => { + describe('when called with empty group_by', () => { + it.each` + group_by + ${[]} + ${null} + ${undefined} + `("when group_by is '$group_by', should set overrideGrouping false", ({ group_by }) => { + // Arrange + const amRoute: Route = buildAmRoute({ group_by: group_by }); + + // Act + const [formRoute] = amRouteToFormAmRoute(amRoute); + + // Assert + expect(formRoute.groupBy).toStrictEqual([]); + expect(formRoute.overrideGrouping).toBe(false); + }); + }); + + describe('when called with non-empty group_by', () => { + it('Should set overrideGrouping true and groupBy', () => { + // Arrange + const amRoute: Route = buildAmRoute({ group_by: ['SHOULD BE SET'] }); + + // Act + const [formRoute] = amRouteToFormAmRoute(amRoute); + + // Assert + expect(formRoute.groupBy).toStrictEqual(['SHOULD BE SET']); + expect(formRoute.overrideGrouping).toBe(true); + }); + }); +}); diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts index 58102b86367..7d5a2bb8e6e 100644 --- a/public/app/features/alerting/unified/utils/amroutes.ts +++ b/public/app/features/alerting/unified/utils/amroutes.ts @@ -61,6 +61,7 @@ export const emptyArrayFieldMatcher: MatcherFieldValue = { export const emptyRoute: FormAmRoute = { id: '', + overrideGrouping: false, groupBy: [], object_matchers: [], routes: [], @@ -114,6 +115,7 @@ export const amRouteToFormAmRoute = (route: Route | undefined): [FormAmRoute, Re ], continue: route.continue ?? false, receiver: route.receiver ?? '', + overrideGrouping: Array.isArray(route.group_by) && route.group_by.length !== 0, groupBy: route.group_by ?? [], overrideTimings: [groupWaitValue, groupIntervalValue, repeatIntervalValue].some(Boolean), groupWaitValue, @@ -137,6 +139,8 @@ export const formAmRouteToAmRoute = ( const existing: Route | undefined = id2ExistingRoute[formAmRoute.id]; const { + overrideGrouping, + groupBy, overrideTimings, groupWaitValue, groupWaitValueType, @@ -146,6 +150,8 @@ export const formAmRouteToAmRoute = ( repeatIntervalValueType, } = formAmRoute; + const group_by = overrideGrouping && groupBy ? groupBy : []; + const overrideGroupWait = overrideTimings && groupWaitValue; const group_wait = overrideGroupWait ? `${groupWaitValue}${groupWaitValueType}` : undefined; @@ -158,7 +164,7 @@ export const formAmRouteToAmRoute = ( const amRoute: Route = { ...(existing ?? {}), continue: formAmRoute.continue, - group_by: formAmRoute.groupBy, + group_by: group_by, object_matchers: formAmRoute.object_matchers.length ? formAmRoute.object_matchers.map((matcher) => [matcher.name, matcher.operator, matcher.value]) : undefined, From e61e26d77432e6abd2e3ab0fcee06acbe699325b Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 08:28:41 -0400 Subject: [PATCH 46/95] Prometheus/Loki: Show raw query by default in the builder(#50007) (#50080) (cherry picked from commit c63071f519356fc6634467b27971b8a8349207b9) Co-authored-by: Andrej Ocenas --- .../LokiQueryBuilderContainer.test.tsx | 1 + .../components/LokiQueryBuilderContainer.tsx | 5 ++-- .../LokiQueryEditorSelector.test.tsx | 29 ++++++++++--------- .../components/LokiQueryEditorSelector.tsx | 8 +++-- .../datasource/loki/querybuilder/state.ts | 27 +++++++++++++++++ public/app/plugins/datasource/loki/types.ts | 2 -- .../components/PromQueryBuilderContainer.tsx | 5 ++-- .../PromQueryEditorSelector.test.tsx | 29 ++++++++++--------- .../components/PromQueryEditorSelector.tsx | 8 +++-- .../prometheus/querybuilder/state.ts | 27 +++++++++++++++++ .../plugins/datasource/prometheus/types.ts | 2 -- 11 files changed, 103 insertions(+), 40 deletions(-) diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx index 170939451b6..24b391681d2 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -30,6 +30,7 @@ describe('LokiQueryBuilderContainer', () => { ), onChange: jest.fn(), onRunQuery: () => {}, + showRawQuery: true, }; render(); expect(screen.getByText('testjob')).toBeInTheDocument(); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx index 3e20f62bcf3..09a6fed59c2 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx @@ -15,6 +15,7 @@ export interface Props { datasource: LokiDatasource; onChange: (update: LokiQuery) => void; onRunQuery: () => void; + showRawQuery: boolean; } export interface State { @@ -26,7 +27,7 @@ export interface State { * This component is here just to contain the translation logic between string query and the visual query builder model. */ export function LokiQueryBuilderContainer(props: Props) { - const { query, onChange, onRunQuery, datasource } = props; + const { query, onChange, onRunQuery, datasource, showRawQuery } = props; const [state, dispatch] = useReducer(stateSlice.reducer, { expr: query.expr, // Use initial visual query only if query.expr is empty string @@ -62,7 +63,7 @@ export function LokiQueryBuilderContainer(props: Props) { onChange={onVisQueryChange} onRunQuery={onRunQuery} /> - {query.rawQuery && } + {showRawQuery && } ); } diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx index 1b483c53dd1..d105157ef0e 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx @@ -10,6 +10,18 @@ import { LokiQuery, LokiQueryType } from '../../types'; import { LokiQueryEditorSelector } from './LokiQueryEditorSelector'; +jest.mock('app/core/store', () => { + return { + get() { + return undefined; + }, + set() {}, + getObject(key: string, defaultValue: any) { + return defaultValue; + }, + }; +}); + const defaultQuery = { refId: 'A', expr: '{label1="foo", label2="bar"}', @@ -86,23 +98,14 @@ describe('LokiQueryEditorSelector', () => { }); it('Can enable raw query', async () => { - const { onChange } = renderWithMode(QueryEditorMode.Builder); - expect(screen.queryByLabelText('selector')).not.toBeInTheDocument(); - + renderWithMode(QueryEditorMode.Builder); + expect(screen.queryByLabelText('selector')).toBeInTheDocument(); screen.getByLabelText('Raw query').click(); - - expect(onChange).toBeCalledWith({ - refId: 'A', - expr: defaultQuery.expr, - queryType: 'range', - editorMode: QueryEditorMode.Builder, - rawQuery: true, - }); + expect(screen.queryByLabelText('selector')).not.toBeInTheDocument(); }); - it('Should show raw query', async () => { + it('Should show raw query by default', async () => { renderWithProps({ - rawQuery: true, editorMode: QueryEditorMode.Builder, expr: '{job="grafana"}', }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index ef7aceaf560..e374302a2b6 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -11,7 +11,7 @@ import { LokiQueryEditorProps } from '../../components/types'; import { LokiQuery } from '../../types'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { buildVisualQueryFromString } from '../parsing'; -import { changeEditorMode, getQueryWithDefaults } from '../state'; +import { changeEditorMode, getQueryWithDefaults, useRawQuery } from '../state'; import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer'; import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained'; @@ -24,6 +24,7 @@ export const LokiQueryEditorSelector = React.memo((props) const [dataIsStale, setDataIsStale] = useState(false); const query = getQueryWithDefaults(props.query); + const [rawQuery, setRawQuery] = useRawQuery(); // This should be filled in from the defaults by now. const editorMode = query.editorMode!; @@ -53,7 +54,7 @@ export const LokiQueryEditorSelector = React.memo((props) const onQueryPreviewChange = (event: SyntheticEvent) => { const isEnabled = event.currentTarget.checked; - onChange({ ...query, rawQuery: isEnabled }); + setRawQuery(isEnabled); }; return ( @@ -86,7 +87,7 @@ export const LokiQueryEditorSelector = React.memo((props) }} options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> - + )} @@ -110,6 +111,7 @@ export const LokiQueryEditorSelector = React.memo((props) query={query} onChange={onChangeInternal} onRunQuery={props.onRunQuery} + showRawQuery={rawQuery} /> )} {editorMode === QueryEditorMode.Explain && } diff --git a/public/app/plugins/datasource/loki/querybuilder/state.ts b/public/app/plugins/datasource/loki/querybuilder/state.ts index 741a0fbcd3e..94ab436ea20 100644 --- a/public/app/plugins/datasource/loki/querybuilder/state.ts +++ b/public/app/plugins/datasource/loki/querybuilder/state.ts @@ -1,3 +1,5 @@ +import { useCallback, useState } from 'react'; + import store from 'app/core/store'; import { QueryEditorMode } from '../../prometheus/querybuilder/shared/types'; @@ -53,3 +55,28 @@ export function getQueryWithDefaults(query: LokiQuery): LokiQuery { return result; } + +const queryEditorRawQueryLocalStorageKey = 'LokiQueryEditorRawQueryDefault'; + +function getRawQueryVisibility(): boolean { + const val = store.get(queryEditorRawQueryLocalStorageKey); + return val === undefined ? true : Boolean(parseInt(val, 10)); +} + +function setRawQueryVisibility(value: boolean) { + store.set(queryEditorRawQueryLocalStorageKey, value ? '1' : '0'); +} + +/** + * Use and store value of raw query switch in local storage. + * Needs to be a hook with local state to trigger rerenders. + */ +export function useRawQuery(): [boolean, (val: boolean) => void] { + const [rawQuery, setRawQuery] = useState(getRawQueryVisibility()); + const setter = useCallback((value: boolean) => { + setRawQueryVisibility(value); + setRawQuery(value); + }, []); + + return [rawQuery, setter]; +} diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index 345e2f23cbf..f84982d2e76 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -49,8 +49,6 @@ export interface LokiQuery extends DataQuery { /* @deprecated now use queryType */ instant?: boolean; editorMode?: QueryEditorMode; - /** Controls if the raw query text is shown */ - rawQuery?: boolean; } export interface LokiOptions extends DataSourceJsonData { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx index d7cc0ab0a32..cae0623fe73 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx @@ -18,6 +18,7 @@ export interface Props { onChange: (update: PromQuery) => void; onRunQuery: () => void; data?: PanelData; + showRawQuery?: boolean; } export interface State { @@ -29,7 +30,7 @@ export interface State { * This component is here just to contain the translation logic between string query and the visual query builder model. */ export function PromQueryBuilderContainer(props: Props) { - const { query, onChange, onRunQuery, datasource, data } = props; + const { query, onChange, onRunQuery, datasource, data, showRawQuery } = props; const [state, dispatch] = useReducer(stateSlice.reducer, { expr: query.expr }); // Only rebuild visual query if expr changes from outside @@ -56,7 +57,7 @@ export function PromQueryBuilderContainer(props: Props) { onRunQuery={onRunQuery} data={data} /> - {query.rawQuery && } + {showRawQuery && } ); } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx index 3d3bd9821b9..d01bbd40f77 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx @@ -20,6 +20,18 @@ jest.mock('../../components/monaco-query-field/MonacoQueryFieldWrapper', () => { }; }); +jest.mock('app/core/store', () => { + return { + get() { + return undefined; + }, + set() {}, + getObject(key: string, defaultValue: any) { + return defaultValue; + }, + }; +}); + jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), @@ -87,23 +99,14 @@ describe('PromQueryEditorSelector', () => { }); it('Can enable raw query', async () => { - const { onChange } = renderWithMode(QueryEditorMode.Builder); - expect(screen.queryByLabelText('selector')).not.toBeInTheDocument(); - + renderWithMode(QueryEditorMode.Builder); + expect(screen.queryByLabelText('selector')).toBeInTheDocument(); screen.getByLabelText('Raw query').click(); - - expect(onChange).toBeCalledWith({ - refId: 'A', - expr: defaultQuery.expr, - range: true, - editorMode: QueryEditorMode.Builder, - rawQuery: true, - }); + expect(screen.queryByLabelText('selector')).not.toBeInTheDocument(); }); - it('Should show raw query', async () => { + it('Should show raw query by default', async () => { renderWithProps({ - rawQuery: true, editorMode: QueryEditorMode.Builder, expr: 'my_metric', }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index cbb5f0801bc..ca58fe5219e 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -13,7 +13,7 @@ import { FeedbackLink } from '../shared/FeedbackLink'; import { QueryEditorModeToggle } from '../shared/QueryEditorModeToggle'; import { QueryHeaderSwitch } from '../shared/QueryHeaderSwitch'; import { QueryEditorMode } from '../shared/types'; -import { changeEditorMode, getQueryWithDefaults } from '../state'; +import { changeEditorMode, getQueryWithDefaults, useRawQuery } from '../state'; import { PromQueryBuilderContainer } from './PromQueryBuilderContainer'; import { PromQueryBuilderExplained } from './PromQueryBuilderExplained'; @@ -28,6 +28,7 @@ export const PromQueryEditorSelector = React.memo((props) => { const [dataIsStale, setDataIsStale] = useState(false); const query = getQueryWithDefaults(props.query, app); + const [rawQuery, setRawQuery] = useRawQuery(); // This should be filled in from the defaults by now. const editorMode = query.editorMode!; @@ -59,7 +60,7 @@ export const PromQueryEditorSelector = React.memo((props) => { const onQueryPreviewChange = (event: SyntheticEvent) => { const isEnabled = event.currentTarget.checked; - onChange({ ...query, rawQuery: isEnabled }); + setRawQuery(isEnabled); }; const onChangeInternal = (query: PromQuery) => { @@ -99,7 +100,7 @@ export const PromQueryEditorSelector = React.memo((props) => { }} options={promQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> - + )} {editorMode === QueryEditorMode.Builder && ( @@ -127,6 +128,7 @@ export const PromQueryEditorSelector = React.memo((props) => { onChange={onChangeInternal} onRunQuery={props.onRunQuery} data={data} + showRawQuery={rawQuery} /> )} {editorMode === QueryEditorMode.Explain && } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/state.ts b/public/app/plugins/datasource/prometheus/querybuilder/state.ts index 04a42555d40..a9c0b6e79d3 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/state.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/state.ts @@ -1,3 +1,5 @@ +import { useCallback, useState } from 'react'; + import { CoreApp } from '@grafana/data'; import store from 'app/core/store'; @@ -59,3 +61,28 @@ export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined) return result; } + +const queryEditorRawQueryLocalStorageKey = 'PrometheusQueryEditorRawQueryDefault'; + +function getRawQueryVisibility(): boolean { + const val = store.get(queryEditorRawQueryLocalStorageKey); + return val === undefined ? true : Boolean(parseInt(val, 10)); +} + +function setRawQueryVisibility(value: boolean) { + store.set(queryEditorRawQueryLocalStorageKey, value ? '1' : '0'); +} + +/** + * Use and store value of raw query switch in local storage. + * Needs to be a hook with local state to trigger rerenders. + */ +export function useRawQuery(): [boolean, (val: boolean) => void] { + const [rawQuery, setRawQuery] = useState(getRawQueryVisibility()); + const setter = useCallback((value: boolean) => { + setRawQueryVisibility(value); + setRawQuery(value); + }, []); + + return [rawQuery, setter]; +} diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts index e9f1776becd..7194bfcfb03 100644 --- a/public/app/plugins/datasource/prometheus/types.ts +++ b/public/app/plugins/datasource/prometheus/types.ts @@ -20,8 +20,6 @@ export interface PromQuery extends DataQuery { showingTable?: boolean; /** Code, Builder or Explain */ editorMode?: QueryEditorMode; - /** Controls if the raw query text is shown */ - rawQuery?: boolean; } export interface PromOptions extends DataSourceJsonData { From 1c870e99088941e96c27e862cd62441ab05880b3 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 08:29:12 -0400 Subject: [PATCH 47/95] Prometheus/Loki: Add raw query and syntax highlight in explain mode (#50070) (#50081) (cherry picked from commit cc90f9bb693e7fc4303a82d16c0b88bc85c22921) Co-authored-by: Andrej Ocenas --- .../components/LokiQueryBuilderExplained.tsx | 21 ++++++++--- .../querybuilder/components/QueryPreview.tsx | 24 ++----------- .../components/PromQueryBuilderExplained.tsx | 20 ++++++++--- .../querybuilder/components/QueryPreview.tsx | 26 ++------------ .../shared/OperationExplainedBox.tsx | 15 ++++---- .../shared/OperationListExplained.tsx | 22 ++++++++++-- .../querybuilder/shared/RawQuery.tsx | 36 +++++++++++++++++++ 7 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/querybuilder/shared/RawQuery.tsx diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx index 257ff64722c..236267c5163 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx @@ -3,25 +3,38 @@ import React from 'react'; import { Stack } from '@grafana/experimental'; import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox'; import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained'; +import { RawQuery } from 'app/plugins/datasource/prometheus/querybuilder/shared/RawQuery'; +import { lokiGrammar } from '../../syntax'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { buildVisualQueryFromString } from '../parsing'; import { LokiVisualQuery } from '../types'; export interface Props { query: string; - nested?: boolean; } -export const LokiQueryBuilderExplained = React.memo(({ query, nested }) => { +export const LokiQueryBuilderExplained = React.memo(({ query }) => { const visQuery = buildVisualQueryFromString(query || '').query; + const lang = { grammar: lokiGrammar, name: 'lokiql' }; return ( - + + + + } + > Fetch all log lines matching label filters. - stepNumber={2} queryModeller={lokiQueryModeller} query={visQuery} /> + + stepNumber={2} + queryModeller={lokiQueryModeller} + query={visQuery} + lang={lang} + /> ); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx index e7726df257c..64e5fe16d91 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx @@ -1,11 +1,8 @@ -import { css, cx } from '@emotion/css'; -import Prism from 'prismjs'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; -import { useTheme2 } from '@grafana/ui'; +import { RawQuery } from '../../../prometheus/querybuilder/shared/RawQuery'; import { lokiGrammar } from '../../syntax'; export interface Props { @@ -13,30 +10,13 @@ export interface Props { } export function QueryPreview({ query }: Props) { - const theme = useTheme2(); - const styles = getStyles(theme); - const highlighted = Prism.highlight(query, lokiGrammar, 'lokiql'); - return ( -
+ ); } - -const getStyles = (theme: GrafanaTheme2) => { - return { - editorField: css({ - fontFamily: theme.typography.fontFamilyMonospace, - fontSize: theme.typography.bodySmall.fontSize, - }), - }; -}; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx index 422f4b3b231..80d223d1406 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx @@ -2,29 +2,39 @@ import React from 'react'; import { Stack } from '@grafana/experimental'; +import promqlGrammar from '../../promql'; import { promQueryModeller } from '../PromQueryModeller'; import { buildVisualQueryFromString } from '../parsing'; import { OperationExplainedBox } from '../shared/OperationExplainedBox'; import { OperationListExplained } from '../shared/OperationListExplained'; +import { RawQuery } from '../shared/RawQuery'; import { PromVisualQuery } from '../types'; export interface Props { query: string; - nested?: boolean; } -export const PromQueryBuilderExplained = React.memo(({ query, nested }) => { +export const PromQueryBuilderExplained = React.memo(({ query }) => { const visQuery = buildVisualQueryFromString(query || '').query; + const lang = { grammar: promqlGrammar, name: 'promql' }; return ( - + + + + } > Fetch all series matching metric name and label filters. - stepNumber={2} queryModeller={promQueryModeller} query={visQuery} /> + + stepNumber={2} + queryModeller={promQueryModeller} + query={visQuery} + lang={lang} + /> ); }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx index 7ff6bfcb9f4..853185268ba 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx @@ -1,42 +1,22 @@ -import { css, cx } from '@emotion/css'; -import Prism from 'prismjs'; import React from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; -import { useTheme2 } from '@grafana/ui'; -import { promqlGrammar } from '../../promql'; +import promqlGrammar from '../../promql'; +import { RawQuery } from '../shared/RawQuery'; export interface Props { query: string; } export function QueryPreview({ query }: Props) { - const theme = useTheme2(); - const styles = getStyles(theme); - const highlighted = Prism.highlight(query, promqlGrammar, 'promql'); - return ( -
+ ); } - -const getStyles = (theme: GrafanaTheme2) => { - return { - editorField: css({ - fontFamily: theme.typography.fontFamilyMonospace, - fontSize: theme.typography.bodySmall.fontSize, - }), - }; -}; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox.tsx index 867c03b2cb2..302b4b7d744 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox.tsx @@ -5,10 +5,10 @@ import { GrafanaTheme2, renderMarkdown } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; export interface Props { - title: string; + title?: React.ReactNode; children?: React.ReactNode; markdown?: string; - stepNumber: number; + stepNumber?: number; } export function OperationExplainedBox({ title, stepNumber, markdown, children }: Props) { @@ -16,11 +16,13 @@ export function OperationExplainedBox({ title, stepNumber, markdown, children }: return (
-
{stepNumber}
+ {stepNumber !== undefined &&
{stepNumber}
}
-
- {title} -
+ {title && ( +
+ {title} +
+ )}
{markdown &&
} {children} @@ -37,7 +39,6 @@ const getStyles = (theme: GrafanaTheme2) => { padding: theme.spacing(1), borderRadius: theme.shape.borderRadius(), position: 'relative', - marginBottom: theme.spacing(0.5), }), boxInner: css({ marginLeft: theme.spacing(4), diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx index 1bae2ed76db..9e83131c174 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx @@ -1,6 +1,8 @@ +import { Grammar } from 'prismjs'; import React from 'react'; import { OperationExplainedBox } from './OperationExplainedBox'; +import { RawQuery } from './RawQuery'; import { QueryWithOperations, VisualQueryModeller } from './types'; export interface Props { @@ -8,9 +10,18 @@ export interface Props { queryModeller: VisualQueryModeller; explainMode?: boolean; stepNumber: number; + lang: { + grammar: Grammar; + name: string; + }; } -export function OperationListExplained({ query, queryModeller, stepNumber }: Props) { +export function OperationListExplained({ + query, + queryModeller, + stepNumber, + lang, +}: Props) { return ( <> {query.operations.map((op, index) => { @@ -21,7 +32,14 @@ export function OperationListExplained({ query, q const title = def.renderer(op, def, ''); const body = def.explainHandler ? def.explainHandler(op, def) : def.documentation ?? 'no docs'; - return ; + return ( + } + markdown={body} + /> + ); })} ); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/RawQuery.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/RawQuery.tsx new file mode 100644 index 00000000000..57151312947 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/RawQuery.tsx @@ -0,0 +1,36 @@ +import { css, cx } from '@emotion/css'; +import Prism, { Grammar } from 'prismjs'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data/src'; +import { useTheme2 } from '@grafana/ui/src'; + +export interface Props { + query: string; + lang: { + grammar: Grammar; + name: string; + }; +} +export function RawQuery({ query, lang }: Props) { + const theme = useTheme2(); + const styles = getStyles(theme); + const highlighted = Prism.highlight(query, lang.grammar, lang.name); + + return ( +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + return { + editorField: css({ + fontFamily: theme.typography.fontFamilyMonospace, + fontSize: theme.typography.bodySmall.fontSize, + }), + }; +}; From b342fe6e30bd264a0d904d20461805f29a7647fd Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:05:57 -0400 Subject: [PATCH 48/95] Alerting: Provisioning API - Alert rules (#47930) (#50086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 81d360529b18a977127a13c52250e1f82331d479) Co-authored-by: Jean-Philippe Quéméner --- pkg/services/ngalert/api/api.go | 2 + pkg/services/ngalert/api/api_provisioning.go | 66 +++- pkg/services/ngalert/api/api_ruler.go | 2 +- pkg/services/ngalert/api/authorization.go | 9 +- .../ngalert/api/authorization_test.go | 2 +- .../ngalert/api/forked_provisioning.go | 20 ++ .../api/generated_base_api_alertmanager.go | 24 -- .../api/generated_base_api_configuration.go | 4 - .../api/generated_base_api_prometheus.go | 4 - .../api/generated_base_api_provisioning.go | 97 +++++- .../ngalert/api/generated_base_api_ruler.go | 12 - .../ngalert/api/generated_base_api_testing.go | 3 - pkg/services/ngalert/api/tooling/Makefile | 8 +- .../definitions/provisioning_alert_rules.go | 144 +++++++++ pkg/services/ngalert/api/tooling/post.json | 298 +++++++++++++++++- pkg/services/ngalert/api/tooling/spec.json | 288 ++++++++++++++++- pkg/services/ngalert/ngalert.go | 2 + .../ngalert/provisioning/alert_rules.go | 151 +++++++++ .../ngalert/provisioning/alert_rules_test.go | 166 ++++++++++ pkg/services/ngalert/store/alert_rule.go | 88 ++++-- pkg/services/ngalert/store/testing.go | 29 +- pkg/services/ngalert/tests/util.go | 2 +- 22 files changed, 1295 insertions(+), 126 deletions(-) create mode 100644 pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go create mode 100644 pkg/services/ngalert/provisioning/alert_rules.go create mode 100644 pkg/services/ngalert/provisioning/alert_rules_test.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index c163b0f2836..feff31c52ab 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -81,6 +81,7 @@ type API struct { ContactPointService *provisioning.ContactPointService Templates *provisioning.TemplateService MuteTimings *provisioning.MuteTimingService + AlertRules *provisioning.AlertRuleService } // RegisterAPIEndpoints registers API handlers @@ -142,6 +143,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { contactPointService: api.ContactPointService, templates: api.Templates, muteTimings: api.MuteTimings, + alertRules: api.AlertRules, }), m) } } diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 1edcb98d555..ce43a3c34ed 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -16,8 +16,13 @@ import ( "github.com/grafana/grafana/pkg/web" ) -const namePathParam = ":name" -const idPathParam = ":ID" +const ( + namePathParam = ":name" + idPathParam = ":ID" + uidPathParam = ":UID" + groupPathParam = ":Group" + folderUIDPathParam = ":FolderUID" +) type ProvisioningSrv struct { log log.Logger @@ -25,6 +30,7 @@ type ProvisioningSrv struct { contactPointService ContactPointService templates TemplateService muteTimings MuteTimingService + alertRules AlertRuleService } type ContactPointService interface { @@ -52,6 +58,14 @@ type MuteTimingService interface { DeleteMuteTiming(ctx context.Context, name string, orgID int64) error } +type AlertRuleService interface { + GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (alerting_models.AlertRule, alerting_models.Provenance, error) + CreateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error) + UpdateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error) + DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error + UpdateAlertGroup(ctx context.Context, orgID int64, folderUID, rulegroup string, interval int64) error +} + func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response { policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgId) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { @@ -223,6 +237,54 @@ func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext) response return response.JSON(http.StatusNoContent, nil) } +func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext) response.Response { + uid := pathParam(c, uidPathParam) + rule, provenace, err := srv.alertRules.GetAlertRule(c.Req.Context(), c.OrgId, uid) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, apimodels.NewAlertRule(rule, provenace)) +} + +func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar apimodels.AlertRule) response.Response { + createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + ar.ID = createdAlertRule.ID + ar.UID = createdAlertRule.UID + ar.Updated = createdAlertRule.Updated + return response.JSON(http.StatusCreated, ar) +} + +func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar apimodels.AlertRule) response.Response { + updatedAlertRule, err := srv.alertRules.UpdateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + ar.Updated = updatedAlertRule.Updated + return response.JSON(http.StatusOK, ar) +} + +func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext) response.Response { + uid := pathParam(c, uidPathParam) + err := srv.alertRules.DeleteAlertRule(c.Req.Context(), c.OrgId, uid, alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusNoContent, "") +} + +func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag apimodels.AlertRuleGroup) response.Response { + rulegroup := pathParam(c, groupPathParam) + folderUID := pathParam(c, folderUIDPathParam) + err := srv.alertRules.UpdateAlertGroup(c.Req.Context(), c.OrgId, folderUID, rulegroup, ag.Interval) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, ag) +} + func pathParam(c *models.ReqContext, param string) string { return web.Params(c.Req)[param] } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 22238d85cab..104b5daef5c 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -429,7 +429,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod for _, rule := range finalChanges.New { inserts = append(inserts, *rule) } - err = srv.store.InsertAlertRules(tranCtx, inserts) + _, err = srv.store.InsertAlertRules(tranCtx, inserts) if err != nil { return fmt.Errorf("failed to add rules: %w", err) } diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index dc6725d0efc..fc53c9c3c0e 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -184,7 +184,8 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/provisioning/templates", http.MethodGet + "/api/provisioning/templates/{name}", http.MethodGet + "/api/provisioning/mute-timings", - http.MethodGet + "/api/provisioning/mute-timings/{name}": + http.MethodGet + "/api/provisioning/mute-timings/{name}", + http.MethodGet + "/api/provisioning/alert-rules/{UID}": return middleware.ReqSignedIn case http.MethodPut + "/api/provisioning/policies", @@ -195,7 +196,11 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodDelete + "/api/provisioning/templates/{name}", http.MethodPost + "/api/provisioning/mute-timings", http.MethodPut + "/api/provisioning/mute-timings/{name}", - http.MethodDelete + "/api/provisioning/mute-timings/{name}": + http.MethodDelete + "/api/provisioning/mute-timings/{name}", + http.MethodPost + "/api/provisioning/alert-rules", + http.MethodPut + "/api/provisioning/alert-rules/{UID}", + http.MethodDelete + "/api/provisioning/alert-rules/{UID}", + http.MethodPut + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}": return middleware.ReqEditorRole } diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index 8ad6cc55fa8..685e3671da2 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -46,7 +46,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 36) + require.Len(t, paths, 39) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 297147e7e65..a0a94c76e1e 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -78,3 +78,23 @@ func (f *ForkedProvisioningApi) forkRoutePutMuteTiming(ctx *models.ReqContext, m func (f *ForkedProvisioningApi) forkRouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { return f.svc.RouteDeleteMuteTiming(ctx) } + +func (f *ForkedProvisioningApi) forkRouteGetAlertRule(ctx *models.ReqContext) response.Response { + return f.svc.RouteRouteGetAlertRule(ctx) +} + +func (f *ForkedProvisioningApi) forkRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.AlertRule) response.Response { + return f.svc.RoutePostAlertRule(ctx, ar) +} + +func (f *ForkedProvisioningApi) forkRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.AlertRule) response.Response { + return f.svc.RoutePutAlertRule(ctx, ar) +} + +func (f *ForkedProvisioningApi) forkRouteDeleteAlertRule(ctx *models.ReqContext) response.Response { + return f.svc.RouteDeleteAlertRule(ctx) +} + +func (f *ForkedProvisioningApi) forkRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroup) response.Response { + return f.svc.RoutePutAlertRuleGroup(ctx, ag) +} diff --git a/pkg/services/ngalert/api/generated_base_api_alertmanager.go b/pkg/services/ngalert/api/generated_base_api_alertmanager.go index 5b744e66038..368c0d2d547 100644 --- a/pkg/services/ngalert/api/generated_base_api_alertmanager.go +++ b/pkg/services/ngalert/api/generated_base_api_alertmanager.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -53,7 +52,6 @@ func (f *ForkedAlertmanagerApi) RouteCreateGrafanaSilence(ctx *models.ReqContext } return f.forkRouteCreateGrafanaSilence(ctx, conf) } - func (f *ForkedAlertmanagerApi) RouteCreateSilence(ctx *models.ReqContext) response.Response { conf := apimodels.PostableSilence{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -61,71 +59,54 @@ func (f *ForkedAlertmanagerApi) RouteCreateSilence(ctx *models.ReqContext) respo } return f.forkRouteCreateSilence(ctx, conf) } - func (f *ForkedAlertmanagerApi) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteGrafanaAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteGrafanaSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteDeleteSilence(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { return f.forkRouteGetAMAlertGroups(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { return f.forkRouteGetAMAlerts(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAMStatus(ctx *models.ReqContext) response.Response { return f.forkRouteGetAMStatus(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAMAlertGroups(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAMAlerts(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAMStatus(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaSilences(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetSilence(ctx *models.ReqContext) response.Response { return f.forkRouteGetSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetSilences(ctx *models.ReqContext) response.Response { return f.forkRouteGetSilences(ctx) } - func (f *ForkedAlertmanagerApi) RoutePostAMAlerts(ctx *models.ReqContext) response.Response { conf := apimodels.PostableAlerts{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -133,7 +114,6 @@ func (f *ForkedAlertmanagerApi) RoutePostAMAlerts(ctx *models.ReqContext) respon } return f.forkRoutePostAMAlerts(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -141,7 +121,6 @@ func (f *ForkedAlertmanagerApi) RoutePostAlertingConfig(ctx *models.ReqContext) } return f.forkRoutePostAlertingConfig(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) response.Response { conf := apimodels.PostableAlerts{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -149,7 +128,6 @@ func (f *ForkedAlertmanagerApi) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) } return f.forkRoutePostGrafanaAMAlerts(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -157,7 +135,6 @@ func (f *ForkedAlertmanagerApi) RoutePostGrafanaAlertingConfig(ctx *models.ReqCo } return f.forkRoutePostGrafanaAlertingConfig(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response { conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -165,7 +142,6 @@ func (f *ForkedAlertmanagerApi) RoutePostTestGrafanaReceivers(ctx *models.ReqCon } return f.forkRoutePostTestGrafanaReceivers(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostTestReceivers(ctx *models.ReqContext) response.Response { conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_configuration.go b/pkg/services/ngalert/api/generated_base_api_configuration.go index 4be46dbc058..084c27dec82 100644 --- a/pkg/services/ngalert/api/generated_base_api_configuration.go +++ b/pkg/services/ngalert/api/generated_base_api_configuration.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -29,15 +28,12 @@ type ConfigurationApiForkingService interface { func (f *ForkedConfigurationApi) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteNGalertConfig(ctx) } - func (f *ForkedConfigurationApi) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response { return f.forkRouteGetAlertmanagers(ctx) } - func (f *ForkedConfigurationApi) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetNGalertConfig(ctx) } - func (f *ForkedConfigurationApi) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableNGalertConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_prometheus.go b/pkg/services/ngalert/api/generated_base_api_prometheus.go index fd4cadcb18e..32198a858bb 100644 --- a/pkg/services/ngalert/api/generated_base_api_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_prometheus.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -27,15 +26,12 @@ type PrometheusApiForkingService interface { func (f *ForkedPrometheusApi) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetAlertStatuses(ctx) } - func (f *ForkedPrometheusApi) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAlertStatuses(ctx) } - func (f *ForkedPrometheusApi) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaRuleStatuses(ctx) } - func (f *ForkedPrometheusApi) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetRuleStatuses(ctx) } diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index edde8b0afd1..b48c5212a9a 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -20,59 +19,68 @@ import ( ) type ProvisioningApiForkingService interface { + RouteDeleteAlertRule(*models.ReqContext) response.Response RouteDeleteContactpoints(*models.ReqContext) response.Response RouteDeleteMuteTiming(*models.ReqContext) response.Response RouteDeleteTemplate(*models.ReqContext) response.Response + RouteGetAlertRule(*models.ReqContext) response.Response RouteGetContactpoints(*models.ReqContext) response.Response RouteGetMuteTiming(*models.ReqContext) response.Response RouteGetMuteTimings(*models.ReqContext) response.Response RouteGetPolicyTree(*models.ReqContext) response.Response RouteGetTemplate(*models.ReqContext) response.Response RouteGetTemplates(*models.ReqContext) response.Response + RoutePostAlertRule(*models.ReqContext) response.Response RoutePostContactpoints(*models.ReqContext) response.Response RoutePostMuteTiming(*models.ReqContext) response.Response + RoutePutAlertRule(*models.ReqContext) response.Response + RoutePutAlertRuleGroup(*models.ReqContext) response.Response RoutePutContactpoint(*models.ReqContext) response.Response RoutePutMuteTiming(*models.ReqContext) response.Response RoutePutPolicyTree(*models.ReqContext) response.Response RoutePutTemplate(*models.ReqContext) response.Response } +func (f *ForkedProvisioningApi) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { + return f.forkRouteDeleteAlertRule(ctx) +} func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteContactpoints(ctx) } - func (f *ForkedProvisioningApi) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteMuteTiming(ctx) } - func (f *ForkedProvisioningApi) RouteDeleteTemplate(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteTemplate(ctx) } - +func (f *ForkedProvisioningApi) RouteGetAlertRule(ctx *models.ReqContext) response.Response { + return f.forkRouteGetAlertRule(ctx) +} func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response { return f.forkRouteGetContactpoints(ctx) } - func (f *ForkedProvisioningApi) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { return f.forkRouteGetMuteTiming(ctx) } - func (f *ForkedProvisioningApi) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { return f.forkRouteGetMuteTimings(ctx) } - func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { return f.forkRouteGetPolicyTree(ctx) } - func (f *ForkedProvisioningApi) RouteGetTemplate(ctx *models.ReqContext) response.Response { return f.forkRouteGetTemplate(ctx) } - func (f *ForkedProvisioningApi) RouteGetTemplates(ctx *models.ReqContext) response.Response { return f.forkRouteGetTemplates(ctx) } - +func (f *ForkedProvisioningApi) RoutePostAlertRule(ctx *models.ReqContext) response.Response { + conf := apimodels.AlertRule{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePostAlertRule(ctx, conf) +} func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -80,7 +88,6 @@ func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) r } return f.forkRoutePostContactpoints(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) response.Response { conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -88,7 +95,20 @@ func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) resp } return f.forkRoutePostMuteTiming(ctx, conf) } - +func (f *ForkedProvisioningApi) RoutePutAlertRule(ctx *models.ReqContext) response.Response { + conf := apimodels.AlertRule{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePutAlertRule(ctx, conf) +} +func (f *ForkedProvisioningApi) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response { + conf := apimodels.AlertRuleGroup{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePutAlertRuleGroup(ctx, conf) +} func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -96,7 +116,6 @@ func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) res } return f.forkRoutePutContactpoint(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) response.Response { conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -104,7 +123,6 @@ func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) respo } return f.forkRoutePutMuteTiming(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { conf := apimodels.Route{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -112,7 +130,6 @@ func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) respo } return f.forkRoutePutPolicyTree(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) response.Response { conf := apimodels.MessageTemplateContent{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -123,6 +140,16 @@ func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) respons func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Delete( + toMacaronPath("/api/provisioning/alert-rules/{UID}"), + api.authorize(http.MethodDelete, "/api/provisioning/alert-rules/{UID}"), + metrics.Instrument( + http.MethodDelete, + "/api/provisioning/alert-rules/{UID}", + srv.RouteDeleteAlertRule, + m, + ), + ) group.Delete( toMacaronPath("/api/provisioning/contact-points/{ID}"), api.authorize(http.MethodDelete, "/api/provisioning/contact-points/{ID}"), @@ -153,6 +180,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Get( + toMacaronPath("/api/provisioning/alert-rules/{UID}"), + api.authorize(http.MethodGet, "/api/provisioning/alert-rules/{UID}"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/alert-rules/{UID}", + srv.RouteGetAlertRule, + m, + ), + ) group.Get( toMacaronPath("/api/provisioning/contact-points"), api.authorize(http.MethodGet, "/api/provisioning/contact-points"), @@ -213,6 +250,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Post( + toMacaronPath("/api/provisioning/alert-rules"), + api.authorize(http.MethodPost, "/api/provisioning/alert-rules"), + metrics.Instrument( + http.MethodPost, + "/api/provisioning/alert-rules", + srv.RoutePostAlertRule, + m, + ), + ) group.Post( toMacaronPath("/api/provisioning/contact-points"), api.authorize(http.MethodPost, "/api/provisioning/contact-points"), @@ -233,6 +280,26 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Put( + toMacaronPath("/api/provisioning/alert-rules/{UID}"), + api.authorize(http.MethodPut, "/api/provisioning/alert-rules/{UID}"), + metrics.Instrument( + http.MethodPut, + "/api/provisioning/alert-rules/{UID}", + srv.RoutePutAlertRule, + m, + ), + ) + group.Put( + toMacaronPath("/api/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + api.authorize(http.MethodPut, "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + metrics.Instrument( + http.MethodPut, + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}", + srv.RoutePutAlertRuleGroup, + m, + ), + ) group.Put( toMacaronPath("/api/provisioning/contact-points/{ID}"), api.authorize(http.MethodPut, "/api/provisioning/contact-points/{ID}"), diff --git a/pkg/services/ngalert/api/generated_base_api_ruler.go b/pkg/services/ngalert/api/generated_base_api_ruler.go index 3d2979b2ca3..7b2c8967d39 100644 --- a/pkg/services/ngalert/api/generated_base_api_ruler.go +++ b/pkg/services/ngalert/api/generated_base_api_ruler.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -37,43 +36,33 @@ type RulerApiForkingService interface { func (f *ForkedRulerApi) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteGrafanaRuleGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteNamespaceGrafanaRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteNamespaceRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteRuleGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaRuleGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetNamespaceGrafanaRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetNamespaceRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetRulegGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetRulesConfig(ctx) } - func (f *ForkedRulerApi) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableRuleGroupConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -81,7 +70,6 @@ func (f *ForkedRulerApi) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) } return f.forkRoutePostNameGrafanaRulesConfig(ctx, conf) } - func (f *ForkedRulerApi) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableRuleGroupConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_testing.go b/pkg/services/ngalert/api/generated_base_api_testing.go index 9ba0dc975a4..1dd46c6eb81 100644 --- a/pkg/services/ngalert/api/generated_base_api_testing.go +++ b/pkg/services/ngalert/api/generated_base_api_testing.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -32,7 +31,6 @@ func (f *ForkedTestingApi) RouteEvalQueries(ctx *models.ReqContext) response.Res } return f.forkRouteEvalQueries(ctx, conf) } - func (f *ForkedTestingApi) RouteTestRuleConfig(ctx *models.ReqContext) response.Response { conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -40,7 +38,6 @@ func (f *ForkedTestingApi) RouteTestRuleConfig(ctx *models.ReqContext) response. } return f.forkRouteTestRuleConfig(ctx, conf) } - func (f *ForkedTestingApi) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response { conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/tooling/Makefile b/pkg/services/ngalert/api/tooling/Makefile index ed4fcfd094c..0e2821c8c67 100644 --- a/pkg/services/ngalert/api/tooling/Makefile +++ b/pkg/services/ngalert/api/tooling/Makefile @@ -35,7 +35,7 @@ api.json: spec-stable.json go run cmd/clean-swagger/main.go -if $(<) -of $@ swagger-codegen-api: - docker run --rm -v $$(pwd):/local --user $$(id -u):$$(id -g) swaggerapi/swagger-codegen-cli generate \ + docker run --rm -v $$(pwd):/local --user $$(id -u):$$(id -g) parsertongue/swagger-codegen-cli:3.0.32 generate \ -i /local/post.json \ -l go-server \ -Dapis \ @@ -49,9 +49,9 @@ copy-files: ls -1 go | xargs -n 1 -I {} mv go/{} ../generated_base_{} fix: - sed -i -e 's/apimodels\.\[\]PostableAlert/apimodels.PostableAlerts/' $(GENERATED_GO_MATCHERS) - sed -i -e 's/apimodels\.\[\]UpdateDashboardAclCommand/apimodels.Permissions/' $(GENERATED_GO_MATCHERS) - sed -i -e 's/apimodels\.\[\]PostableApiReceiver/apimodels.TestReceiversConfigParams/' $(GENERATED_GO_MATCHERS) + sed -i '' -e 's/apimodels\.\[\]PostableAlert/apimodels.PostableAlerts/' $(GENERATED_GO_MATCHERS) + sed -i '' -e 's/apimodels\.\[\]UpdateDashboardAclCommand/apimodels.Permissions/' $(GENERATED_GO_MATCHERS) + sed -i '' -e 's/apimodels\.\[\]PostableApiReceiver/apimodels.TestReceiversConfigParams/' $(GENERATED_GO_MATCHERS) goimports -w -v $(GENERATED_GO_MATCHERS) clean: diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go new file mode 100644 index 00000000000..fca432c43db --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -0,0 +1,144 @@ +package definitions + +import ( + "time" + + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +// swagger:route GET /api/provisioning/alert-rules/{UID} provisioning RouteGetAlertRule +// +// Get a specific alert rule by UID. +// +// Responses: +// 200: AlertRule +// 400: ValidationError + +// swagger:route POST /api/provisioning/alert-rules provisioning RoutePostAlertRule +// +// Create a new alert rule. +// +// Responses: +// 201: AlertRule +// 400: ValidationError + +// swagger:route PUT /api/provisioning/alert-rules/{UID} provisioning RoutePutAlertRule +// +// Update an existing alert rule. +// +// Consumes: +// - application/json +// +// Responses: +// 200: AlertRule +// 400: ValidationError + +// swagger:route DELETE /api/provisioning/alert-rules/{UID} provisioning RouteDeleteAlertRule +// +// Delete a specific alert rule by UID. +// +// Responses: +// 204: description: The alert rule was deleted successfully. +// 400: ValidationError + +// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule +type AlertRuleUIDReference struct { + // in:path + UID string +} + +// swagger:parameters RoutePostAlertRule RoutePutAlertRule +type AlertRulePayload struct { + // in:body + Body AlertRule +} + +type AlertRule struct { + ID int64 `json:"id"` + UID string `json:"uid"` + OrgID int64 `json:"orgID"` + FolderUID string `json:"folderUID"` + RuleGroup string `json:"ruleGroup"` + Title string `json:"title"` + Condition string `json:"condition"` + Data []models.AlertQuery `json:"data"` + Updated time.Time `json:"updated,omitempty"` + NoDataState models.NoDataState `json:"noDataState"` + ExecErrState models.ExecutionErrorState `json:"execErrState"` + For time.Duration `json:"for"` + Annotations map[string]string `json:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Provenance models.Provenance `json:"provenance,omitempty"` +} + +func (a *AlertRule) UpstreamModel() models.AlertRule { + return models.AlertRule{ + ID: a.ID, + UID: a.UID, + OrgID: a.OrgID, + NamespaceUID: a.FolderUID, + RuleGroup: a.RuleGroup, + Title: a.Title, + Condition: a.Condition, + Data: a.Data, + Updated: a.Updated, + NoDataState: a.NoDataState, + ExecErrState: a.ExecErrState, + For: a.For, + Annotations: a.Annotations, + Labels: a.Labels, + } +} + +func NewAlertRule(rule models.AlertRule, provenance models.Provenance) AlertRule { + return AlertRule{ + ID: rule.ID, + UID: rule.UID, + OrgID: rule.OrgID, + FolderUID: rule.NamespaceUID, + RuleGroup: rule.RuleGroup, + Title: rule.Title, + For: rule.For, + Condition: rule.Condition, + Data: rule.Data, + Updated: rule.Updated, + NoDataState: rule.NoDataState, + ExecErrState: rule.ExecErrState, + Annotations: rule.Annotations, + Labels: rule.Labels, + Provenance: provenance, + } +} + +// swagger:route PUT /api/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning RoutePutAlertRuleGroup +// +// Update the interval of a rule group. +// +// Consumes: +// - application/json +// +// Responses: +// 200: AlertRuleGroup +// 400: ValidationError + +// swagger:parameters RoutePutAlertRuleGroup +type FolderUIDPathParam struct { + // in:path + FolderUID string `json:"FolderUID"` +} + +// swagger:parameters RoutePutAlertRuleGroup +type RuleGroupPathParam struct { + // in:path + Group string `json:"Group"` +} + +// swagger:parameters RoutePutAlertRuleGroup +type AlertRuleGroupPayload struct { + // in:body + Body AlertRuleGroup +} + +type AlertRuleGroup struct { + Interval int64 `json:"interval"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index a4b6a1cae67..77c9d3f9ffb 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -194,6 +194,91 @@ "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "AlertRule": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "x-go-name": "Annotations" + }, + "condition": { + "type": "string", + "x-go-name": "Condition" + }, + "data": { + "items": { + "$ref": "#/definitions/AlertQuery" + }, + "type": "array", + "x-go-name": "Data" + }, + "execErrState": { + "$ref": "#/definitions/ExecutionErrorState" + }, + "folderUID": { + "type": "string", + "x-go-name": "FolderUID" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "x-go-name": "Labels" + }, + "noDataState": { + "$ref": "#/definitions/NoDataState" + }, + "orgID": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, + "ruleGroup": { + "type": "string", + "x-go-name": "RuleGroup" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + }, + "updated": { + "format": "date-time", + "type": "string", + "x-go-name": "Updated" + } + }, + "type": "object", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, + "AlertRuleGroup": { + "properties": { + "interval": { + "format": "int64", + "type": "integer", + "x-go-name": "Interval" + } + }, + "type": "object", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "AlertingRule": { "description": "adapted from cortex", "properties": { @@ -663,6 +748,10 @@ "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, "EvalQueriesResponse": {}, + "ExecutionErrorState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "ExtendedReceiver": { "properties": { "email_configs": { @@ -1456,6 +1545,10 @@ "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "NoDataState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "NotFound": { "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -2923,6 +3016,7 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2955,9 +3049,9 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object", - "x-go-package": "github.com/prometheus/common/config" + "x-go-package": "net/url" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -3179,12 +3273,11 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, - "type": "array", - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "array" }, "alertStatus": { "description": "AlertStatus alert status", @@ -3304,6 +3397,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3362,9 +3456,7 @@ "status", "updatedAt" ], - "type": "object", - "x-go-name": "GettableAlert", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "gettableAlerts": { "description": "GettableAlerts gettable alerts", @@ -3374,7 +3466,6 @@ "type": "array" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3426,7 +3517,9 @@ "status", "updatedAt" ], - "type": "object" + "type": "object", + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilences": { "items": { @@ -3563,6 +3656,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3602,11 +3696,10 @@ "matchers", "startsAt" ], - "type": "object", - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -3617,9 +3710,7 @@ "required": [ "name" ], - "type": "object", - "x-go-name": "Receiver", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "silence": { "description": "Silence silence", @@ -4838,6 +4929,134 @@ ] } }, + "/api/provisioning/alert-rules": { + "post": { + "operationId": "RoutePostAlertRule", + "parameters": [ + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "201": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Create a new alert rule.", + "tags": [ + "provisioning" + ] + } + }, + "/api/provisioning/alert-rules/{UID}": { + "delete": { + "operationId": "RouteDeleteAlertRule", + "parameters": [ + { + "in": "path", + "name": "UID", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": " The alert rule was deleted successfully." + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Delete a specific alert rule by UID.", + "tags": [ + "provisioning" + ] + }, + "get": { + "operationId": "RouteGetAlertRule", + "parameters": [ + { + "in": "path", + "name": "UID", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Get a specific alert rule by UID.", + "tags": [ + "provisioning" + ] + }, + "put": { + "consumes": [ + "application/json" + ], + "operationId": "RoutePutAlertRule", + "parameters": [ + { + "in": "path", + "name": "UID", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Update an existing alert rule.", + "tags": [ + "provisioning" + ] + } + }, "/api/provisioning/contact-points": { "get": { "operationId": "RouteGetContactpoints", @@ -4969,6 +5188,53 @@ ] } }, + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "put": { + "consumes": [ + "application/json" + ], + "operationId": "RoutePutAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + } + ], + "responses": { + "200": { + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Update the interval of an rule group.", + "tags": [ + "provisioning" + ] + } + }, "/api/provisioning/mute-timings": { "get": { "operationId": "RouteGetMuteTimings", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index eaec39f99c5..f6a58a5786c 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1122,6 +1122,134 @@ } } }, + "/api/provisioning/alert-rules": { + "post": { + "tags": [ + "provisioning" + ], + "summary": "Create a new alert rule.", + "operationId": "RoutePostAlertRule", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "201": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/api/provisioning/alert-rules/{UID}": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get a specific alert rule by UID.", + "operationId": "RouteGetAlertRule", + "parameters": [ + { + "type": "string", + "name": "UID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Update an existing alert rule.", + "operationId": "RoutePutAlertRule", + "parameters": [ + { + "type": "string", + "name": "UID", + "in": "path", + "required": true + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "delete": { + "tags": [ + "provisioning" + ], + "summary": "Delete a specific alert rule by UID.", + "operationId": "RouteDeleteAlertRule", + "parameters": [ + { + "type": "string", + "name": "UID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": " The alert rule was deleted successfully." + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/api/provisioning/contact-points": { "get": { "tags": [ @@ -1253,6 +1381,53 @@ } } }, + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Update the interval of an rule group.", + "operationId": "RoutePutAlertRuleGroup", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + } + ], + "responses": { + "200": { + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/api/provisioning/mute-timings": { "get": { "tags": [ @@ -2394,6 +2569,91 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "AlertRule": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Annotations" + }, + "condition": { + "type": "string", + "x-go-name": "Condition" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertQuery" + }, + "x-go-name": "Data" + }, + "execErrState": { + "$ref": "#/definitions/ExecutionErrorState" + }, + "folderUID": { + "type": "string", + "x-go-name": "FolderUID" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "noDataState": { + "$ref": "#/definitions/NoDataState" + }, + "orgID": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, + "ruleGroup": { + "type": "string", + "x-go-name": "RuleGroup" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + }, + "updated": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, + "AlertRuleGroup": { + "type": "object", + "properties": { + "interval": { + "type": "integer", + "format": "int64", + "x-go-name": "Interval" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -2866,6 +3126,10 @@ "EvalQueriesResponse": { "$ref": "#/definitions/EvalQueriesResponse" }, + "ExecutionErrorState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "ExtendedReceiver": { "type": "object", "properties": { @@ -3660,6 +3924,10 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "NoDataState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "NotFound": { "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -5127,8 +5395,9 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -5161,7 +5430,7 @@ "$ref": "#/definitions/Userinfo" } }, - "x-go-package": "github.com/prometheus/common/config" + "x-go-package": "net/url" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -5384,12 +5653,11 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" }, - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroups" }, "alertStatus": { @@ -5510,6 +5778,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -5569,8 +5838,6 @@ "x-go-name": "UpdatedAt" } }, - "x-go-name": "GettableAlert", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { @@ -5582,7 +5849,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5635,6 +5901,8 @@ "x-go-name": "UpdatedAt" } }, + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { @@ -5773,6 +6041,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -5813,11 +6082,10 @@ "x-go-name": "StartsAt" } }, - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/postableSilence" }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "name" @@ -5829,8 +6097,6 @@ "x-go-name": "Name" } }, - "x-go-name": "Receiver", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/receiver" }, "silence": { diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 6c152ea138b..1c077c4393d 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -156,6 +156,7 @@ func (ng *AlertNG) init() error { contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) + alertRuleService := provisioning.NewAlertRuleService(store, store, store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), ng.Log) api := api.API{ Cfg: ng.Cfg, @@ -179,6 +180,7 @@ func (ng *AlertNG) init() error { ContactPointService: contactPointService, Templates: templateService, MuteTimings: muteTimingService, + AlertRules: alertRuleService, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go new file mode 100644 index 00000000000..483373e5d50 --- /dev/null +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -0,0 +1,151 @@ +package provisioning + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/util" +) + +type AlertRuleService struct { + defaultInterval int64 + ruleStore store.RuleStore + provenanceStore ProvisioningStore + xact TransactionManager + log log.Logger +} + +func NewAlertRuleService(ruleStore store.RuleStore, + provenanceStore ProvisioningStore, + xact TransactionManager, + defaultInterval int64, + log log.Logger) *AlertRuleService { + return &AlertRuleService{ + defaultInterval: defaultInterval, + ruleStore: ruleStore, + provenanceStore: provenanceStore, + xact: xact, + log: log, + } +} + +func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (models.AlertRule, models.Provenance, error) { + query := &models.GetAlertRuleByUIDQuery{ + OrgID: orgID, + UID: ruleUID, + } + err := service.ruleStore.GetAlertRuleByUID(ctx, query) + if err != nil { + return models.AlertRule{}, models.ProvenanceNone, err + } + provenance, err := service.provenanceStore.GetProvenance(ctx, query.Result, orgID) + if err != nil { + return models.AlertRule{}, models.ProvenanceNone, err + } + return *query.Result, provenance, nil +} + +func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) { + if rule.UID == "" { + rule.UID = util.GenerateShortUID() + } + interval, err := service.ruleStore.GetRuleGroupInterval(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup) + // if the alert group does not exists we just use the default interval + if err != nil && errors.Is(err, store.ErrAlertRuleGroupNotFound) { + interval = service.defaultInterval + } else if err != nil { + return models.AlertRule{}, err + } + rule.IntervalSeconds = interval + rule.Updated = time.Now() + err = service.xact.InTransaction(ctx, func(ctx context.Context) error { + ids, err := service.ruleStore.InsertAlertRules(ctx, []models.AlertRule{ + rule, + }) + if err != nil { + return err + } + if id, ok := ids[rule.UID]; ok { + rule.ID = id + } else { + return errors.New("couldn't find newly created id") + } + err = service.ruleStore.UpdateRuleGroup(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup, rule.IntervalSeconds) + if err != nil { + return err + } + return service.provenanceStore.SetProvenance(ctx, &rule, rule.OrgID, provenance) + }) + if err != nil { + return models.AlertRule{}, err + } + return rule, nil +} + +func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) { + storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID) + if err != nil { + return models.AlertRule{}, err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return models.AlertRule{}, fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance) + } + rule.Updated = time.Now() + rule.ID = storedRule.ID + rule.IntervalSeconds, err = service.ruleStore.GetRuleGroupInterval(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup) + if err != nil { + return models.AlertRule{}, err + } + service.log.Info("update rule", "ID", storedRule.ID, "labels", fmt.Sprintf("%+v", rule.Labels)) + err = service.xact.InTransaction(ctx, func(ctx context.Context) error { + err := service.ruleStore.UpdateAlertRules(ctx, []store.UpdateRule{ + { + Existing: &storedRule, + New: rule, + }, + }) + if err != nil { + return err + } + err = service.ruleStore.UpdateRuleGroup(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup, rule.IntervalSeconds) + if err != nil { + return err + } + return service.provenanceStore.SetProvenance(ctx, &rule, rule.OrgID, provenance) + }) + if err != nil { + return models.AlertRule{}, err + } + return rule, err +} + +func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance models.Provenance) error { + rule := &models.AlertRule{ + OrgID: orgID, + UID: ruleUID, + } + // check that provenance is not changed in a invalid way + storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID) + if err != nil { + return err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + } + return service.xact.InTransaction(ctx, func(ctx context.Context) error { + err := service.ruleStore.DeleteAlertRulesByUID(ctx, orgID, ruleUID) + if err != nil { + return err + } + return service.provenanceStore.DeleteProvenance(ctx, rule, rule.OrgID) + }) +} + +func (service *AlertRuleService) UpdateAlertGroup(ctx context.Context, orgID int64, folderUID, roulegroup string, interval int64) error { + return service.ruleStore.UpdateRuleGroup(ctx, orgID, folderUID, roulegroup, interval) +} diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go new file mode 100644 index 00000000000..37bb8f9d6eb --- /dev/null +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -0,0 +1,166 @@ +package provisioning + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestAlertRuleService(t *testing.T) { + ruleService := createAlertRuleService(t) + t.Run("alert rule creation should return the created id", func(t *testing.T) { + var orgID int64 = 1 + rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", orgID), models.ProvenanceNone) + require.NoError(t, err) + require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value") + }) + t.Run("alert rule creation should set the right provenance", func(t *testing.T) { + var orgID int64 = 1 + rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#2", orgID), models.ProvenanceAPI) + require.NoError(t, err) + + _, provenance, err := ruleService.GetAlertRule(context.Background(), orgID, rule.UID) + require.NoError(t, err) + require.Equal(t, models.ProvenanceAPI, provenance) + }) + t.Run("alert rule group should be updated correctly", func(t *testing.T) { + var orgID int64 = 1 + rule := dummyRule("test#3", orgID) + rule.RuleGroup = "a" + rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone) + require.NoError(t, err) + require.Equal(t, int64(60), rule.IntervalSeconds) + + var interval int64 = 120 + err = ruleService.UpdateAlertGroup(context.Background(), orgID, rule.NamespaceUID, rule.RuleGroup, 120) + require.NoError(t, err) + + rule, _, err = ruleService.GetAlertRule(context.Background(), orgID, rule.UID) + require.NoError(t, err) + require.Equal(t, interval, rule.IntervalSeconds) + }) + t.Run("alert rule should get interval from existing rule group", func(t *testing.T) { + var orgID int64 = 1 + rule := dummyRule("test#4", orgID) + rule.RuleGroup = "b" + rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone) + require.NoError(t, err) + + var interval int64 = 120 + err = ruleService.UpdateAlertGroup(context.Background(), orgID, rule.NamespaceUID, rule.RuleGroup, 120) + require.NoError(t, err) + + rule = dummyRule("test#4-1", orgID) + rule.RuleGroup = "b" + rule, err = ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone) + require.NoError(t, err) + require.Equal(t, interval, rule.IntervalSeconds) + }) + t.Run("alert rule provenace should be correctly checked", func(t *testing.T) { + tests := []struct { + name string + from models.Provenance + to models.Provenance + errNil bool + }{ + { + name: "should be able to update from provenance none to api", + from: models.ProvenanceNone, + to: models.ProvenanceAPI, + errNil: true, + }, + { + name: "should be able to update from provenance none to file", + from: models.ProvenanceNone, + to: models.ProvenanceFile, + errNil: true, + }, + { + name: "should not be able to update from provenance api to file", + from: models.ProvenanceAPI, + to: models.ProvenanceFile, + errNil: false, + }, + { + name: "should not be able to update from provenance api to none", + from: models.ProvenanceAPI, + to: models.ProvenanceNone, + errNil: false, + }, + { + name: "should not be able to update from provenance file to api", + from: models.ProvenanceFile, + to: models.ProvenanceAPI, + errNil: false, + }, + { + name: "should not be able to update from provenance file to none", + from: models.ProvenanceFile, + to: models.ProvenanceNone, + errNil: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var orgID int64 = 1 + rule := dummyRule(t.Name(), orgID) + rule, err := ruleService.CreateAlertRule(context.Background(), rule, test.from) + require.NoError(t, err) + + _, err = ruleService.UpdateAlertRule(context.Background(), rule, test.to) + if test.errNil { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + }) +} + +func createAlertRuleService(t *testing.T) AlertRuleService { + t.Helper() + sqlStore := sqlstore.InitTestDB(t) + store := store.DBstore{ + SQLStore: sqlStore, + BaseInterval: time.Second * 10, + } + return AlertRuleService{ + ruleStore: store, + provenanceStore: store, + xact: sqlStore, + log: log.New("testing"), + defaultInterval: 60, + } +} + +func dummyRule(title string, orgID int64) models.AlertRule { + return models.AlertRule{ + OrgID: orgID, + Title: title, + Condition: "A", + Version: 1, + IntervalSeconds: 60, + Data: []models.AlertQuery{ + { + RefID: "A", + Model: json.RawMessage("{}"), + RelativeTimeRange: models.RelativeTimeRange{ + From: models.Duration(60), + To: models.Duration(0), + }, + }, + }, + RuleGroup: "my-cool-group", + For: time.Second * 60, + NoDataState: models.OK, + ExecErrState: models.OkErrState, + } +} diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 0402942bc8e..13264bc7b3a 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -2,6 +2,7 @@ package store import ( "context" + "errors" "fmt" "strings" "time" @@ -32,6 +33,10 @@ type UpdateRule struct { New ngmodels.AlertRule } +var ( + ErrAlertRuleGroupNotFound = errors.New("rulegroup not found") +) + // RuleStore is the interface for persisting alert rules and instances type RuleStore interface { DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error @@ -41,9 +46,14 @@ type RuleStore interface { ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error // GetRuleGroups returns the unique rule groups across all organizations. GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGroupsQuery) error + GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) + // UpdateRuleGroup will update the interval for all rules in the group. + UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error) GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error) - InsertAlertRules(ctx context.Context, rule []ngmodels.AlertRule) error + // InsertAlertRules will insert all alert rules passed into the function + // and return the map of uuid to id. + InsertAlertRules(ctx context.Context, rule []ngmodels.AlertRule) (map[string]int64, error) UpdateAlertRules(ctx context.Context, rule []UpdateRule) error } @@ -110,17 +120,20 @@ func (st DBstore) GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAler } // InsertAlertRules is a handler for creating/updating alert rules. -func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) error { - return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { +func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) (map[string]int64, error) { + ids := make(map[string]int64, len(rules)) + return ids, st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { newRules := make([]ngmodels.AlertRule, 0, len(rules)) ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) for i := range rules { r := rules[i] - uid, err := GenerateNewAlertRuleUID(sess, r.OrgID, r.Title) - if err != nil { - return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.Title, err) + if r.UID == "" { + uid, err := GenerateNewAlertRuleUID(sess, r.OrgID, r.Title) + if err != nil { + return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.Title, err) + } + r.UID = uid } - r.UID = uid r.Version = 1 if err := st.validateAlertRule(r); err != nil { return err @@ -130,8 +143,8 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu } newRules = append(newRules, r) ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{ - RuleOrgID: r.OrgID, RuleUID: r.UID, + RuleOrgID: r.OrgID, RuleNamespaceUID: r.NamespaceUID, RuleGroup: r.RuleGroup, ParentVersion: 0, @@ -149,11 +162,16 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu }) } if len(newRules) > 0 { - if _, err := sess.Insert(&newRules); err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleUniqueConstraintViolation + // we have to insert the rules one by one as otherwise we are + // not able to fetch the inserted id as it's not supported by xorm + for i := range newRules { + if _, err := sess.Insert(&newRules[i]); err != nil { + if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return ngmodels.ErrAlertRuleUniqueConstraintViolation + } + return fmt.Errorf("failed to create new rules: %w", err) } - return fmt.Errorf("failed to create new rules: %w", err) + ids[newRules[i].UID] = newRules[i].ID } } @@ -162,15 +180,13 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu return fmt.Errorf("failed to create new rule versions: %w", err) } } - return nil }) } -// UpdateAlertRules is a handler for creating/updating alert rules. +// UpdateAlertRules is a handler for updating alert rules. func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - newRules := make([]ngmodels.AlertRule, 0, len(rules)) ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) for _, r := range rules { var parentVersion int64 @@ -209,14 +225,6 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro Labels: r.New.Labels, }) } - if len(newRules) > 0 { - if _, err := sess.Insert(&newRules); err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleUniqueConstraintViolation - } - return fmt.Errorf("failed to create new rules: %w", err) - } - } if len(ruleVersions) > 0 { if _, err := sess.Insert(&ruleVersions); err != nil { return fmt.Errorf("failed to create new rule versions: %w", err) @@ -279,6 +287,32 @@ func (st DBstore) GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGro }) } +func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) { + var interval int64 = 0 + return interval, st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + ruleGroups := make([]ngmodels.AlertRule, 0) + err := sess.Find( + &ruleGroups, + ngmodels.AlertRule{OrgID: orgID, RuleGroup: ruleGroup, NamespaceUID: namespaceUID}, + ) + if len(ruleGroups) == 0 { + return ErrAlertRuleGroupNotFound + } + interval = ruleGroups[0].IntervalSeconds + return err + }) +} + +func (st DBstore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error { + return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.Update( + ngmodels.AlertRule{IntervalSeconds: interval}, + ngmodels.AlertRule{OrgID: orgID, RuleGroup: ruleGroup, NamespaceUID: namespaceUID}, + ) + return err + }) +} + // GetNamespaces returns the folders that are visible to the user and have at least one alert in it func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) { namespaceMap := make(map[string]*models.Folder) @@ -416,5 +450,13 @@ func (st DBstore) validateAlertRule(alertRule ngmodels.AlertRule) error { return fmt.Errorf("%w: cannot have Panel ID without a Dashboard UID", ngmodels.ErrAlertRuleFailedValidation) } + if _, err := ngmodels.ErrStateFromString(string(alertRule.ExecErrState)); err != nil { + return err + } + + if _, err := ngmodels.NoDataStateFromString(string(alertRule.NoDataState)); err != nil { + return err + } + return nil } diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index a6cad95bcfb..afb2c1c1eed 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -284,20 +284,43 @@ func (f *FakeRuleStore) UpdateAlertRules(_ context.Context, q []UpdateRule) erro return nil } -func (f *FakeRuleStore) InsertAlertRules(_ context.Context, q []models.AlertRule) error { +func (f *FakeRuleStore) InsertAlertRules(_ context.Context, q []models.AlertRule) (map[string]int64, error) { f.mtx.Lock() defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, q) + ids := make(map[string]int64, len(q)) if err := f.Hook(q); err != nil { - return err + return ids, err } - return nil + return ids, nil } func (f *FakeRuleStore) InTransaction(ctx context.Context, fn func(c context.Context) error) error { return fn(ctx) } +func (f *FakeRuleStore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + for _, rule := range f.Rules[orgID] { + if rule.RuleGroup == ruleGroup && rule.NamespaceUID == namespaceUID { + return rule.IntervalSeconds, nil + } + } + return 0, ErrAlertRuleGroupNotFound +} + +func (f *FakeRuleStore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error { + f.mtx.Lock() + defer f.mtx.Unlock() + for _, rule := range f.Rules[orgID] { + if rule.RuleGroup == ruleGroup && rule.NamespaceUID == namespaceUID { + rule.IntervalSeconds = interval + } + } + return nil +} + type FakeInstanceStore struct { mtx sync.Mutex RecordedOps []interface{} diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 20954caf973..9d1694a4d22 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -83,7 +83,7 @@ func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBsto func CreateTestAlertRuleWithLabels(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64, labels map[string]string) *models.AlertRule { ruleGroup := fmt.Sprintf("ruleGroup-%s", util.GenerateShortUID()) - err := dbstore.InsertAlertRules(ctx, []models.AlertRule{ + _, err := dbstore.InsertAlertRules(ctx, []models.AlertRule{ { ID: 0, From 3e7a2111e630a69557b111f3581d35a569dae4e4 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 09:15:40 -0400 Subject: [PATCH 49/95] RBAC: Make RBAC action names more consistent (#49730) (#50083) * update action names * correctly retrieve teams for signed in user * remove test * undo swagger changes * undo swagger changes pt2 * add migration from old action names to the new ones * rename from list to read * linting * also update alertign actions * fix migration (cherry picked from commit 5dbea9996b7979de89814097a6c7aa72305f5a14) Co-authored-by: Ieva --- .../developers/http_api/access_control.md | 12 +- docs/sources/developers/http_api/admin.md | 20 ++-- docs/sources/developers/http_api/licensing.md | 6 +- docs/sources/developers/http_api/org.md | 12 +- docs/sources/developers/http_api/user.md | 25 +++-- .../enterprise/access-control/about-rbac.md | 4 +- .../custom-role-actions-scopes.md | 34 +++--- .../access-control/manage-rbac-roles.md | 6 +- .../plan-rbac-rollout-strategy.md | 13 +-- .../rbac-fixed-basic-role-definitions.md | 106 +++++++++--------- pkg/api/api.go | 6 +- pkg/api/org_users_test.go | 8 +- pkg/api/user.go | 10 +- pkg/models/team.go | 7 +- pkg/services/accesscontrol/filter.go | 2 +- pkg/services/accesscontrol/models.go | 29 +++-- pkg/services/accesscontrol/roles.go | 12 +- pkg/services/guardian/guardian.go | 2 +- pkg/services/licensing/accesscontrol.go | 2 +- pkg/services/ngalert/accesscontrol.go | 4 +- .../serviceaccounts/database/database.go | 20 +--- .../serviceaccounts/database/database_test.go | 18 --- .../accesscontrol/action_migrator.go | 68 +++++++++++ .../sqlstore/migrations/migrations.go | 1 + pkg/services/sqlstore/team.go | 13 ++- pkg/services/sqlstore/team_test.go | 9 +- pkg/services/sqlstore/user.go | 15 ++- .../api/alerting/api_alertmanager_test.go | 2 +- public/app/features/admin/UserOrgs.tsx | 4 +- .../ServiceAccountsListItem.tsx | 2 +- public/app/features/users/UsersTable.tsx | 4 +- public/app/types/accessControl.ts | 25 ++--- 32 files changed, 279 insertions(+), 222 deletions(-) create mode 100644 pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go diff --git a/docs/sources/developers/http_api/access_control.md b/docs/sources/developers/http_api/access_control.md index d6c1a4cbc16..fa92323ad2c 100644 --- a/docs/sources/developers/http_api/access_control.md +++ b/docs/sources/developers/http_api/access_control.md @@ -79,7 +79,7 @@ Query Parameters: | Action | Scope | | ---------- | -------- | -| roles:list | roles:\* | +| roles:read | roles:\* | #### Example request @@ -180,13 +180,13 @@ Content-Type: application/json; charset=UTF-8 "created": "2021-11-19T10:48:00+01:00" }, { - "action": "reports.admin:create", + "action": "reports:create", "scope": "", "updated": "2021-11-19T10:48:00+01:00", "created": "2021-11-19T10:48:00+01:00" }, { - "action": "reports.admin:write", + "action": "reports:write", "scope": "reports:*", "updated": "2021-11-19T10:48:00+01:00", "created": "2021-11-19T10:48:00+01:00" @@ -489,7 +489,7 @@ Query Parameters: | Action | Scope | | ---------------- | -------------------- | -| users.roles:list | users:id:`` | +| users.roles:read | users:id:`` | #### Example request @@ -537,7 +537,7 @@ Lists the permissions that a given user has. | Action | Scope | | ---------------------- | -------------------- | -| users.permissions:list | users:id:`` | +| users.permissions:read | users:id:`` | #### Example request @@ -763,7 +763,7 @@ Query Parameters: | Action | Scope | | ---------------- | -------------------- | -| teams.roles:list | teams:id:`` | +| teams.roles:read | teams:id:`` | #### Example request diff --git a/docs/sources/developers/http_api/admin.md b/docs/sources/developers/http_api/admin.md index d33b9f94ee8..65d977afdeb 100644 --- a/docs/sources/developers/http_api/admin.md +++ b/docs/sources/developers/http_api/admin.md @@ -380,9 +380,9 @@ Change password for a specific user. See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation. -| Action | Scope | -| --------------------- | --------------- | -| users.password:update | global.users:\* | +| Action | Scope | +| -------------------- | --------------- | +| users.password:write | global.users:\* | **Example Request**: @@ -413,9 +413,9 @@ Only works with Basic Authentication (username and password). See [introduction] See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation. -| Action | Scope | -| ------------------------ | --------------- | -| users.permissions:update | global.users:\* | +| Action | Scope | +| ----------------------- | --------------- | +| users.permissions:write | global.users:\* | **Example Request**: @@ -516,7 +516,7 @@ See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation. | Action | Scope | | -------------------- | --------------- | -| users.authtoken:list | global.users:\* | +| users.authtoken:read | global.users:\* | **Example Request**: @@ -573,9 +573,9 @@ Only works with Basic Authentication (username and password). See [introduction] See note in the [introduction]({{< ref "#admin-api" >}}) for an explanation. -| Action | Scope | -| ---------------------- | --------------- | -| users.authtoken:update | global.users:\* | +| Action | Scope | +| --------------------- | --------------- | +| users.authtoken:write | global.users:\* | **Example Request**: diff --git a/docs/sources/developers/http_api/licensing.md b/docs/sources/developers/http_api/licensing.md index 3328b214d01..9a61e645564 100644 --- a/docs/sources/developers/http_api/licensing.md +++ b/docs/sources/developers/http_api/licensing.md @@ -71,9 +71,9 @@ Manually ask license issuer for a new token. See note in the [introduction]({{< ref "#enterprise-license-api" >}}) for an explanation. -| Action | Scope | -| ---------------- | ----- | -| licensing:update | n/a | +| Action | Scope | +| --------------- | ----- | +| licensing:write | n/a | ### Examples diff --git a/docs/sources/developers/http_api/org.md b/docs/sources/developers/http_api/org.md index 5b3f152c822..26f0fdff4d4 100644 --- a/docs/sources/developers/http_api/org.md +++ b/docs/sources/developers/http_api/org.md @@ -149,9 +149,9 @@ Content-Type: application/json See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation. -| Action | Scope | -| --------------------- | -------- | -| org.users.role:update | users:\* | +| Action | Scope | +| --------------- | -------- | +| org.users:write | users:\* | **Example Request**: @@ -605,9 +605,9 @@ Only works with Basic Authentication (username and password), see [introduction] See note in the [introduction]({{< ref "#organization-api" >}}) for an explanation. -| Action | Scope | -| --------------------- | -------- | -| org.users.role:update | users:\* | +| Action | Scope | +| --------------- | -------- | +| org.users:write | users:\* | **Example Request**: diff --git a/docs/sources/developers/http_api/user.md b/docs/sources/developers/http_api/user.md index ab45168a5ee..5d8e64488ae 100644 --- a/docs/sources/developers/http_api/user.md +++ b/docs/sources/developers/http_api/user.md @@ -140,9 +140,9 @@ Content-Type: application/json See note in the [introduction]({{< ref "#user-api" >}}) for an explanation. -| Action | Scope | -| ---------- | -------- | -| users:read | users:\* | +| Action | Scope | +| ---------- | --------------- | +| users:read | global.users:\* | **Example Request**: @@ -241,9 +241,9 @@ Content-Type: application/json See note in the [introduction]({{< ref "#user-api" >}}) for an explanation. -| Action | Scope | -| ----------- | -------- | -| users:write | users:\* | +| Action | Scope | +| ----------- | --------------- | +| users:write | global.users:\* | **Example Request**: @@ -280,9 +280,9 @@ Content-Type: application/json See note in the [introduction]({{< ref "#user-api" >}}) for an explanation. -| Action | Scope | -| ---------- | -------- | -| users:read | users:\* | +| Action | Scope | +| ---------- | --------------- | +| users:read | global.users:\* | **Example Request**: @@ -318,9 +318,10 @@ Content-Type: application/json See note in the [introduction]({{< ref "#user-api" >}}) for an explanation. -| Action | Scope | -| ---------------- | -------- | -| users.teams:read | users:\* | +| Action | Scope | +| ---------- | --------------- | +| users:read | global.users:\* | +| teams:read | teams:\* | **Example Request**: diff --git a/docs/sources/enterprise/access-control/about-rbac.md b/docs/sources/enterprise/access-control/about-rbac.md index 22175cd181e..3d52df80c1f 100644 --- a/docs/sources/enterprise/access-control/about-rbac.md +++ b/docs/sources/enterprise/access-control/about-rbac.md @@ -91,9 +91,9 @@ To learn more about the permissions you can grant for each resource, refer to [R If you are a Grafana Enterprise customer, you can create custom roles to manage user permissions in a way that meets your security requirements. -Custom roles contain unique combinations of permissions _actions_ and _scopes_. An action defines the action a use can perform on a Grafana resource. For example, the `teams.roles:list` action allows a user to see a list of roles associated with each team. +Custom roles contain unique combinations of permissions _actions_ and _scopes_. An action defines the action a use can perform on a Grafana resource. For example, the `teams.roles:read` action allows a user to see a list of roles associated with each team. -A scope describes where an action can be performed. For example, the `teams:id:1` scope restricts the user's action to the team with ID `1`. When paired with the `teams.roles:list` action, this permission prohibits the user from viewing the roles for teams other than team `1`. +A scope describes where an action can be performed. For example, the `teams:id:1` scope restricts the user's action to the team with ID `1`. When paired with the `teams.roles:read` action, this permission prohibits the user from viewing the roles for teams other than team `1`. Consider creating a custom role when fixed roles do not meet your permissions requirements. diff --git a/docs/sources/enterprise/access-control/custom-role-actions-scopes.md b/docs/sources/enterprise/access-control/custom-role-actions-scopes.md index 71844c782b0..010079e5534 100644 --- a/docs/sources/enterprise/access-control/custom-role-actions-scopes.md +++ b/docs/sources/enterprise/access-control/custom-role-actions-scopes.md @@ -27,7 +27,7 @@ The following list contains role-based access control actions. | `alert.instances.external:write` | `datasources:*`
`datasources:uid:*` | Manage alerts and silences in data sources that support alerting. | | `alert.instances:create` | n/a | Create silences in the current organization. | | `alert.instances:read` | n/a | Read alerts and silences in the current organization. | -| `alert.instances:update` | n/a | Update and expire silences in the current organization. | +| `alert.instances:write` | n/a | Update and expire silences in the current organization. | | `alert.notifications.external:read` | `datasources:*`
`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. | | `alert.notifications.external:write` | `datasources:*`
`datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. | | `alert.notifications:write` | n/a | Manage templates, contact points, notification policies, and mute timings in the current organization. | @@ -37,7 +37,7 @@ The following list contains role-based access control actions. | `alert.rules:create` | `folders:*`
`folders:uid:*` | Create Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | | `alert.rules:delete` | `folders:*`
`folders:uid:*` | Delete Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | | `alert.rules:read` | `folders:*`
`folders:uid:*` | Read Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | -| `alert.rules:update` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | +| `alert.rules:write` | `folders:*`
`folders:uid:*` | Update Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. | | `annotations:create` | `annotations:*`
`annotations:type:*` | Create annotations. | | `annotations:delete` | `annotations:*`
`annotations:type:*` | Delete annotations. | | `annotations:read` | `annotations:*`
`annotations:type:*` | Read annotations and annotation tags. | @@ -73,8 +73,8 @@ The following list contains role-based access control actions. | `licensing.reports:read` | n/a | Get custom permission reports. | | `licensing:delete` | n/a | Delete the license token. | | `licensing:read` | n/a | Read licensing information. | -| `licensing:update` | n/a | Update the license token. | -| `org.users.role:update` | `users:*`
`users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of an organization. | +| `licensing:write` | n/a | Update the license token. | +| `org.users:write` | `users:*`
`users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of a user. | | `org.users:add` | `users:*` | Add a user to an organization. | | `org.users:read` | `users:*`
`users:id:*` | Get user profiles within an organization. | | `org.users:remove` | `users:*`
`users:id:*` | Remove a user from an organization. | @@ -87,16 +87,15 @@ The following list contains role-based access control actions. | `orgs:read` | `orgs:*`
`orgs:id:*` | Read one or more organizations. | | `orgs:write` | `orgs:*`
`orgs:id:*` | Update one or more organizations. | | `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "#scope-definitions" >}}). | -| `reports.admin:create` | n/a | Create reports. | -| `reports.admin:write` | `reports:*`
`reports:id:*` | Update reports. | +| `reports:create` | n/a | Create reports. | +| `reports:write` | `reports:*`
`reports:id:*` | Update reports. | | `reports.settings:read` | n/a | Read report settings. | | `reports.settings:write` | n/a | Update report settings. | | `reports:delete` | `reports:*`
`reports:id:*` | Delete reports. | | `reports:read` | `reports:*` | List all available reports or get a specific report. | | `reports:send` | `reports:*` | Send a report email. | | `roles:delete` | `permissions:type:delegate` | Delete a custom role. | -| `roles:list` | `roles:*` | List available roles without permissions. | -| `roles:read` | `roles:*`
`roles:uid:*` | Read a specific role with its permissions. | +| `roles:read` | `roles:*`
`roles:uid:*` | List roles and read a specific with its permissions. | | `roles:write` | `permissions:type:delegate` | Create or update a custom role. | | `roles:write` | `permissions:type:escalate` | Reset basic roles to their default permissions. | | `server.stats:read` | n/a | Read Grafana instance statistics. | @@ -106,23 +105,22 @@ The following list contains role-based access control actions. | `teams.permissions:read` | `teams:*`
`teams:id:*` | Read members and External Group Synchronization setup for teams. | | `teams.permissions:write` | `teams:*`
`teams:id:*` | Add, remove and update members and manage External Group Synchronization setup for teams. | | `teams.roles:add` | `permissions:type:delegate` | Assign a role to a team. | -| `teams.roles:list` | `teams:*` | List roles assigned directly to a team. | +| `teams.roles:read` | `teams:*` | List roles assigned directly to a team. | | `teams.roles:remove` | `permissions:type:delegate` | Unassign a role from a team. | | `teams:create` | n/a | Create teams. | | `teams:delete` | `teams:*`
`teams:id:*` | Delete one or more teams. | | `teams:read` | `teams:*`
`teams:id:*` | Read one or more teams and team preferences. | | `teams:write` | `teams:*`
`teams:id:*` | Update one or more teams and team preferences. | -| `users.authtoken:list` | `global.users:*`
`global.users:id:*` | List authentication tokens that are assigned to a user. | -| `users.authtoken:update` | `global.users:*`
`global.users:id:*` | Update authentication tokens that are assigned to a user. | -| `users.password:update` | `global.users:*`
`global.users:id:*` | Update a user’s password. | -| `users.permissions:list` | `users:*` | List permissions of a user. | -| `users.permissions:update` | `global.users:*`
`global.users:id:*` | Update a user’s organization-level permissions. | -| `users.quotas:list` | `global.users:*`
`global.users:id:*` | List a user’s quotas. | -| `users.quotas:update` | `global.users:*`
`global.users:id:*` | Update a user’s quotas. | +| `users.authtoken:read` | `global.users:*`
`global.users:id:*` | List authentication tokens that are assigned to a user. | +| `users.authtoken:write` | `global.users:*`
`global.users:id:*` | Update authentication tokens that are assigned to a user. | +| `users.password:write` | `global.users:*`
`global.users:id:*` | Update a user’s password. | +| `users.permissions:read` | `users:*` | List permissions of a user. | +| `users.permissions:write` | `global.users:*`
`global.users:id:*` | Update a user’s organization-level permissions. | +| `users.quotas:read` | `global.users:*`
`global.users:id:*` | List a user’s quotas. | +| `users.quotas:write` | `global.users:*`
`global.users:id:*` | Update a user’s quotas. | | `users.roles:add` | `permissions:type:delegate` | Assign a role to a user. | -| `users.roles:list` | `users:*` | List roles assigned directly to a user. | +| `users.roles:read` | `users:*` | List roles assigned directly to a user. | | `users.roles:remove` | `permissions:type:delegate` | Unassign a role from a user. | -| `users.teams:read` | `global.users:*`
`global.users:id:*` | Read a user’s teams. | | `users:create` | n/a | Create a user. | | `users:delete` | `global.users:*`
`global.users:id:*` | Delete a user. | | `users:disable` | `global.users:*`
`global.users:id:*` | Disable a user. | diff --git a/docs/sources/enterprise/access-control/manage-rbac-roles.md b/docs/sources/enterprise/access-control/manage-rbac-roles.md index f7c0c048e75..e57f709238b 100644 --- a/docs/sources/enterprise/access-control/manage-rbac-roles.md +++ b/docs/sources/enterprise/access-control/manage-rbac-roles.md @@ -65,7 +65,7 @@ curl --location --request GET '/api/access-control/roles/qQui_LCMk' "created": "2021-05-17T20:49:18+02:00" }, { - "action": "org.users.role:update", + "action": "org.users:write", "scope": "users:*", "updated": "2021-05-17T20:49:18+02:00", "created": "2021-05-17T20:49:18+02:00" @@ -178,7 +178,7 @@ roles: - name: 'fixed:org.users:writer' global: true permissions: - - action: 'org.users.role:update' + - action: 'org.users:write' scope: 'users:*' state: 'absent' - action: 'org.users:add' @@ -283,7 +283,7 @@ roles: global: true permissions: # Permissions to remove - - action: 'teams.roles:list' + - action: 'teams.roles:read' scope: 'teams:*' state: 'absent' - action: 'teams.roles:remove' diff --git a/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md b/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md index c02e7963f48..8498c2a1253 100644 --- a/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md +++ b/docs/sources/enterprise/access-control/plan-rbac-rollout-strategy.md @@ -173,7 +173,6 @@ roles: | action | scope | | -------------- | --------------------------- | -| `roles:list` | `roles:*` | | `roles:read` | `roles:*` | | `roles:write` | `permissions:type:delegate` | | `roles:delete` | `permissions:type:delegate` | @@ -204,12 +203,12 @@ roles: - Add the following permissions to the `basic:viewer` role, using provisioning or the [RBAC HTTP API]({{< relref "../../developers/http_api/access_control.md#update-a-role" >}}): -| Action | Scope | -| ---------------------- | ------------------------------- | -| `reports.admin:create` | n/a | -| `reports.admin:write` | `reports:*`
`reports:id:*` | -| `reports:read` | `reports:*` | -| `reports:send` | `reports:*` | +| Action | Scope | +| ---------------- | ------------------------------- | +| `reports:create` | n/a | +| `reports:write` | `reports:*`
`reports:id:*` | +| `reports:read` | `reports:*` | +| `reports:send` | `reports:*` | ### Prevent a Grafana Admin from creating and inviting users diff --git a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md index 4bcb53bed71..3a7b6fda2a4 100644 --- a/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md +++ b/docs/sources/enterprise/access-control/rbac-fixed-basic-role-definitions.md @@ -24,59 +24,59 @@ The following tables list permissions associated with basic and fixed roles. ## Fixed role definitions -| Fixed role | Permissions | Description | -| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `fixed:alerting.instances:editor` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:update` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | -| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | -| `fixed:alerting.notifications:editor` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | -| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting.rules:editor` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:update`
`alert.rule:delete` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | -| `fixed:alerting.rules:reader` | `alert.rule:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | -| `fixed:alerting:editor` | All permissions from `fixed:alerting.rules:editor`
`fixed:alerting.instances:editor`
`fixed:alerting.notifications:editor` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | -| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | -| `fixed:annotations.dashboard:writer` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | -| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | -| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | -| `fixed:apikeys:reader` | `apikeys:read` for scope `apikeys:*` | Read all api keys. | -| `fixed:apikeys:writer` | All permissions from `fixed:apikeys:reader` and
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | -| `fixed:dashboards.permissions:reader` | `dashboards.permissions:read` | Read all dashboard permissions. | -| `fixed:dashboards.permissions:writer` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | -| `fixed:dashboards:creator` | `dashboards:create`
`folders:read` | Create dashboards. | -| `fixed:dashboards:reader` | `dashboards:read` | Read all dashboards. | -| `fixed:dashboards:writer` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | -| `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. | -| `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | -| `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | -| `fixed:datasources:id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | -| `fixed:datasources:reader` | `datasources:read`
`datasources:query` | Read and query data sources. | -| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | -| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. | -| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | -| `fixed:folders:creator` | `folders:create` | Create folders. | -| `fixed:folders:reader` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | -| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. | -| `fixed:ldap:reader` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | -| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | -| `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | -| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:update`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | -| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. | -| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users.role:update` | Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user. | -| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | -| `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | -| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | -| `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | -| `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | -| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports.admin:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | -| `fixed:roles:reader` | `roles:read`
`roles:list`
`teams.roles:list`
`users.roles:list`
`users.permissions:list` | Read all access control roles, roles and permissions assigned to users, teams. | -| `fixed:roles:writer` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | -| `fixed:roles:resetter` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | -| `fixed:settings:reader` | `settings:read` | Read Grafana instance settings. | -| `fixed:settings:writer` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | -| `fixed:stats:reader` | `server.stats:read` | Read Grafana instance statistics. | -| `fixed:teams:creator` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | -| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | -| `fixed:users:reader` | `users:read`
`users.quotas:list`
`users.authtoken:list`
`users.teams:read` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | -| `fixed:users:writer` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:update`
`users.permissions:update`
`users:logout`
`users.authtoken:update`
`users.quotas:update` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | +| Fixed role | Permissions | Description | +| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fixed:alerting.instances:editor` | All permissions from `fixed:alerting.instances:reader` and
`alert.instances:create`
`alert.instances:write` for organization scope
`alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki.[\*](#alerting-roles) | +| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope
`alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences.[\*](#alerting-roles) | +| `fixed:alerting.notifications:editor` | All permissions from `fixed:alerting.notifications:reader` and
`alert.notifications:write`for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager.[\*](#alerting-roles) | +| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope
`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting.rules:editor` | All permissions from `fixed:alerting.rules:reader` and
`alert.rule:create`
`alert.rule:update`
`alert.rule:delete` for scope `folders:*`
`alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | +| `fixed:alerting.rules:reader` | `alert.rule:read` for scope `folders:*`
`alert.rules.external:read` for scope `datasources:*` | Read all\* Grafana, Mimir, and Loki alert rules.[\*](#alerting-roles) | +| `fixed:alerting:editor` | All permissions from `fixed:alerting.rules:editor`
`fixed:alerting.instances:editor`
`fixed:alerting.notifications:editor` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies.[\*](#alerting-roles) | +| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader`
`fixed:alerting.instances:reader`
`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies.[\*](#alerting-roles) | +| `fixed:annotations.dashboard:writer` | `annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. | +| `fixed:annotations:reader` | `annotations:read` for scopes `annotations:type:*` | Read all annotations and annotation tags. | +| `fixed:annotations:writer` | All permissions from `fixed:annotations:reader`
`annotations:write`
`annotations.create`
`annotations:delete` for scope `annotations:type:*` | Read, create, update and delete all annotations and annotation tags. | +| `fixed:apikeys:reader` | `apikeys:read` for scope `apikeys:*` | Read all api keys. | +| `fixed:apikeys:writer` | All permissions from `fixed:apikeys:reader` and
`apikeys:create`
`apikeys:delete` for scope `apikeys:*` | Read, create, delete all api keys. | +| `fixed:dashboards.permissions:reader` | `dashboards.permissions:read` | Read all dashboard permissions. | +| `fixed:dashboards.permissions:writer` | All permissions from `fixed:dashboards.permissions:reader` and
`dashboards.permissions:write` | Read and update all dashboard permissions. | +| `fixed:dashboards:creator` | `dashboards:create`
`folders:read` | Create dashboards. | +| `fixed:dashboards:reader` | `dashboards:read` | Read all dashboards. | +| `fixed:dashboards:writer` | All permissions from `fixed:dashboards:reader` and
`dashboards:write`
`dashboards:edit`
`dashboards:delete`
`dashboards:create`
`dashboards.permissions:read`
`dashboards.permissions:write` | Read, create, update, and delete all dashboards. | +| `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. | +| `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | +| `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | +| `fixed:datasources:id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | +| `fixed:datasources:reader` | `datasources:read`
`datasources:query` | Read and query data sources. | +| `fixed:datasources:writer` | All permissions from `fixed:datasources:reader` and
`datasources:create`
`datasources:write`
`datasources:delete` | Read, query, create, delete, or update a data source. | +| `fixed:folders.permissions:reader` | `folders.permissions:read` | Read all folder permissions. | +| `fixed:folders.permissions:writer` | All permissions from `fixed:folders.permissions:reader` and
`folders.permissions:write` | Read and update all folder permissions. | +| `fixed:folders:creator` | `folders:create` | Create folders. | +| `fixed:folders:reader` | `folders:read`
`dashboards:read` | Read all folders and dashboards. | +| `fixed:folders:writer` | All permissions from `fixed:dashboards:writer` and
`folders:read`
`folders:write`
`folders:create`
`folders:delete`
`folders.permissions:read`
`folders.permissions:write` | Read, create, update, and delete all folders and dashboards. | +| `fixed:ldap:reader` | `ldap.user:read`
`ldap.status:read` | Read the LDAP configuration and LDAP status information. | +| `fixed:ldap:writer` | All permissions from `fixed:ldap:reader` and
`ldap.user:sync`
`ldap.config:reload` | Read and update the LDAP configuration, and read LDAP status information. | +| `fixed:licensing:reader` | `licensing:read`
`licensing.reports:read` | Read licensing information and licensing reports. | +| `fixed:licensing:writer` | All permissions from `fixed:licensing:viewer` and
`licensing:write`
`licensing:delete` | Read licensing information and licensing reports, update and delete the license token. | +| `fixed:org.users:reader` | `org.users:read` | Read users within a single organization. | +| `fixed:org.users:writer` | All permissions from `fixed:org.users:reader` and
`org.users:add`
`org.users:remove`
`org.users:write` | Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user. | +| `fixed:organization:maintainer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs:create`
`orgs:delete`
`orgs.quotas:write` | Create, read, write, or delete an organization. Read or write its quotas. This role needs to be assigned globally. | +| `fixed:organization:reader` | `orgs:read`
`orgs.quotas:read` | Read an organization and its quotas. | +| `fixed:organization:writer` | All permissions from `fixed:organization:reader` and
`orgs:write`
`orgs.preferences:read`
`orgs.preferences:write` | Read an organization, its quotas, or its preferences. Update organization properties, or its preferences. | +| `fixed:provisioning:writer` | `provisioning:reload` | Reload provisioning. | +| `fixed:reports:reader` | `reports:read`
`reports:send`
`reports.settings:read` | Read all reports and shared report settings. | +| `fixed:reports:writer` | All permissions from `fixed:reports:reader` and
`reports:create`
`reports:write`
`reports:delete`
`reports.settings:write` | Create, read, update, or delete all reports and shared report settings. | +| `fixed:roles:reader` | `roles:read`
`teams.roles:read`
`users.roles:read`
`users.permissions:read` | Read all access control roles, roles and permissions assigned to users, teams. | +| `fixed:roles:writer` | All permissions from `fixed:roles:reader` and
`roles:write`
`roles:delete`
`teams.roles:add`
`teams.roles:remove`
`users.roles:add`
`users.roles:remove` | Create, read, update, or delete all roles, assign or unassign roles to users, teams. | +| `fixed:roles:resetter` | `roles:write` with scope `permissions:type:escalate` | Reset basic roles to their default. | +| `fixed:settings:reader` | `settings:read` | Read Grafana instance settings. | +| `fixed:settings:writer` | All permissions from `fixed:settings:reader` and
`settings:write` | Read and update Grafana instance settings. | +| `fixed:stats:reader` | `server.stats:read` | Read Grafana instance statistics. | +| `fixed:teams:creator` | `teams:create`
`org.users:read` | Create a team and list organization users (required to manage the created team). | +| `fixed:teams:writer` | `teams:create`
`teams:delete`
`teams:read`
`teams:write`
`teams.permissions:read`
`teams.permissions:write` | Create, read, update and delete teams and manage team memberships. | +| `fixed:users:reader` | `users:read`
`users.quotas:read`
`users.authtoken:read`
` | Read all users and their information, such as team memberships, authentication tokens, and quotas. | +| `fixed:users:writer` | All permissions from `fixed:users:reader` and
`users:write`
`users:create`
`users:delete`
`users:enable`
`users:disable`
`users.password:write`
`users.permissions:write`
`users:logout`
`users.authtoken:write`
`users.quotas:write` | Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users. | ### Alerting roles diff --git a/pkg/api/api.go b/pkg/api/api.go index 9e755fcffe1..bc9df6a1f93 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -179,7 +179,7 @@ func (hs *HTTPServer) registerRoutes() { usersRoute.Get("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsers)) usersRoute.Get("/search", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead)), routing.Wrap(hs.searchUsersService.SearchUsersWithPaging)) usersRoute.Get("/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserByID)) - usersRoute.Get("/:id/teams", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersTeamRead, userIDScope)), routing.Wrap(hs.GetUserTeams)) + usersRoute.Get("/:id/teams", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserTeams)) usersRoute.Get("/:id/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, userIDScope)), routing.Wrap(hs.GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com usersRoute.Get("/lookup", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll)), routing.Wrap(hs.GetUserByLoginOrEmail)) @@ -233,7 +233,7 @@ func (hs *HTTPServer) registerRoutes() { orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrg)) orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsersWithPaging)) orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), routing.Wrap(hs.AddOrgUserToCurrentOrg)) - orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), routing.Wrap(hs.UpdateOrgUserForCurrentOrg)) + orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUserForCurrentOrg)) orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg)) // invites @@ -279,7 +279,7 @@ func (hs *HTTPServer) registerRoutes() { orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsDelete)), routing.Wrap(hs.DeleteOrgByID)) orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsers)) orgsRoute.Post("/users", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), routing.Wrap(hs.AddOrgUser)) - orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), routing.Wrap(hs.UpdateOrgUser)) + orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUser)) orgsRoute.Delete("/users/:userId", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser)) orgsRoute.Get("/quotas", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsQuotasRead)), routing.Wrap(hs.GetOrgQuotas)) orgsRoute.Put("/quotas/:target", authorizeInOrg(reqGrafanaAdmin, ac.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsQuotasWrite)), routing.Wrap(hs.UpdateOrgQuota)) diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 38858f640bf..f2bc6297baa 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -325,10 +325,10 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) { enableAccessControl: true, expectedCode: http.StatusOK, expectedMetadata: map[string]bool{ - "org.users.role:update": true, - "org.users:add": true, - "org.users:read": true, - "org.users:remove": true}, + "org.users:write": true, + "org.users:add": true, + "org.users:read": true, + "org.users:remove": true}, user: testServerAdminViewer, targetOrg: testServerAdminViewer.OrgId, }, diff --git a/pkg/api/user.go b/pkg/api/user.go index 8b0c59debf7..4ea249b29ca 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -154,7 +154,7 @@ func (hs *HTTPServer) GetSignedInUserOrgList(c *models.ReqContext) response.Resp // GET /api/user/teams func (hs *HTTPServer) GetSignedInUserTeamList(c *models.ReqContext) response.Response { - return hs.getUserTeamList(c.Req.Context(), c.OrgId, c.UserId) + return hs.getUserTeamList(c, c.OrgId, c.UserId) } // GET /api/users/:id/teams @@ -163,13 +163,13 @@ func (hs *HTTPServer) GetUserTeams(c *models.ReqContext) response.Response { if err != nil { return response.Error(http.StatusBadRequest, "id is invalid", err) } - return hs.getUserTeamList(c.Req.Context(), c.OrgId, id) + return hs.getUserTeamList(c, c.OrgId, id) } -func (hs *HTTPServer) getUserTeamList(ctx context.Context, orgID int64, userID int64) response.Response { - query := models.GetTeamsByUserQuery{OrgId: orgID, UserId: userID} +func (hs *HTTPServer) getUserTeamList(c *models.ReqContext, orgID int64, userID int64) response.Response { + query := models.GetTeamsByUserQuery{OrgId: orgID, UserId: userID, SignedInUser: c.SignedInUser} - if err := hs.SQLStore.GetTeamsByUser(ctx, &query); err != nil { + if err := hs.SQLStore.GetTeamsByUser(c.Req.Context(), &query); err != nil { return response.Error(500, "Failed to get user teams", err) } diff --git a/pkg/models/team.go b/pkg/models/team.go index 567798869da..2f3852765d0 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -62,9 +62,10 @@ type GetTeamByIdQuery struct { const FilterIgnoreUser int64 = 0 type GetTeamsByUserQuery struct { - OrgId int64 - UserId int64 `json:"userId"` - Result []*TeamDTO `json:"teams"` + OrgId int64 + UserId int64 `json:"userId"` + Result []*TeamDTO `json:"teams"` + SignedInUser *SignedInUser } type SearchTeamsQuery struct { diff --git a/pkg/services/accesscontrol/filter.go b/pkg/services/accesscontrol/filter.go index 4980522d15f..8aec1d186f5 100644 --- a/pkg/services/accesscontrol/filter.go +++ b/pkg/services/accesscontrol/filter.go @@ -11,7 +11,7 @@ import ( var sqlIDAcceptList = map[string]struct{}{ "id": {}, "org_user.user_id": {}, - "role.id": {}, + "role.uid": {}, "t.id": {}, "team.id": {}, "u.id": {}, diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 83ca55c1a1c..b47a9cebb69 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -285,32 +285,31 @@ const ( ActionAPIKeyDelete = "apikeys:delete" // Users actions - ActionUsersRead = "users:read" - ActionUsersWrite = "users:write" - ActionUsersTeamRead = "users.teams:read" + ActionUsersRead = "users:read" + ActionUsersWrite = "users:write" // We can ignore gosec G101 since this does not contain any credentials. // nolint:gosec - ActionUsersAuthTokenList = "users.authtoken:list" + ActionUsersAuthTokenList = "users.authtoken:read" // We can ignore gosec G101 since this does not contain any credentials. // nolint:gosec - ActionUsersAuthTokenUpdate = "users.authtoken:update" + ActionUsersAuthTokenUpdate = "users.authtoken:write" // We can ignore gosec G101 since this does not contain any credentials. // nolint:gosec - ActionUsersPasswordUpdate = "users.password:update" + ActionUsersPasswordUpdate = "users.password:write" ActionUsersDelete = "users:delete" ActionUsersCreate = "users:create" ActionUsersEnable = "users:enable" ActionUsersDisable = "users:disable" - ActionUsersPermissionsUpdate = "users.permissions:update" + ActionUsersPermissionsUpdate = "users.permissions:write" ActionUsersLogout = "users:logout" - ActionUsersQuotasList = "users.quotas:list" - ActionUsersQuotasUpdate = "users.quotas:update" + ActionUsersQuotasList = "users.quotas:read" + ActionUsersQuotasUpdate = "users.quotas:write" // Org actions - ActionOrgUsersRead = "org.users:read" - ActionOrgUsersAdd = "org.users:add" - ActionOrgUsersRemove = "org.users:remove" - ActionOrgUsersRoleUpdate = "org.users.role:update" + ActionOrgUsersRead = "org.users:read" + ActionOrgUsersAdd = "org.users:add" + ActionOrgUsersRemove = "org.users:remove" + ActionOrgUsersWrite = "org.users:write" // LDAP actions ActionLDAPUsersRead = "ldap.user:read" @@ -363,12 +362,12 @@ const ( // Alerting rules actions ActionAlertingRuleCreate = "alert.rules:create" ActionAlertingRuleRead = "alert.rules:read" - ActionAlertingRuleUpdate = "alert.rules:update" + ActionAlertingRuleUpdate = "alert.rules:write" ActionAlertingRuleDelete = "alert.rules:delete" // Alerting instances (+silences) actions ActionAlertingInstanceCreate = "alert.instances:create" - ActionAlertingInstanceUpdate = "alert.instances:update" + ActionAlertingInstanceUpdate = "alert.instances:write" ActionAlertingInstanceRead = "alert.instances:read" // Alerting Notification policies actions diff --git a/pkg/services/accesscontrol/roles.go b/pkg/services/accesscontrol/roles.go index 8296f1d28de..0de846f12ff 100644 --- a/pkg/services/accesscontrol/roles.go +++ b/pkg/services/accesscontrol/roles.go @@ -53,14 +53,14 @@ var ( DisplayName: "Organization user writer", Description: "Within a single organization, add a user, invite a user, read information about a user and their role, remove a user from that organization, or change the role of a user.", Group: "User administration (organizational)", - Version: 3, + Version: 4, Permissions: ConcatPermissions(orgUsersReaderRole.Permissions, []Permission{ { Action: ActionOrgUsersAdd, Scope: ScopeUsersAll, }, { - Action: ActionOrgUsersRoleUpdate, + Action: ActionOrgUsersWrite, Scope: ScopeUsersAll, }, { @@ -116,16 +116,12 @@ var ( DisplayName: "User reader", Description: "Read all users and their information, such as team memberships, authentication tokens, and quotas.", Group: "User administration (global)", - Version: 4, + Version: 6, Permissions: []Permission{ { Action: ActionUsersRead, Scope: ScopeGlobalUsersAll, }, - { - Action: ActionUsersTeamRead, - Scope: ScopeGlobalUsersAll, - }, { Action: ActionUsersAuthTokenList, Scope: ScopeGlobalUsersAll, @@ -142,7 +138,7 @@ var ( DisplayName: "User writer", Description: "Read and update all attributes and settings for all users in Grafana: update user information, read user information, create or enable or disable a user, make a user a Grafana administrator, sign out a user, update a user’s authentication token, or update quotas for all users.", Group: "User administration (global)", - Version: 4, + Version: 5, Permissions: ConcatPermissions(usersReaderRole.Permissions, []Permission{ { Action: ActionUsersPasswordUpdate, diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 5b624bf73e9..b642036d1e1 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -270,7 +270,7 @@ func (g *dashboardGuardianImpl) getTeams() ([]*models.TeamDTO, error) { return g.teams, nil } - query := models.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId} + query := models.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId, SignedInUser: g.user} err := g.store.GetTeamsByUser(g.ctx, &query) g.teams = query.Result diff --git a/pkg/services/licensing/accesscontrol.go b/pkg/services/licensing/accesscontrol.go index f5a0f1e6331..9534f888f72 100644 --- a/pkg/services/licensing/accesscontrol.go +++ b/pkg/services/licensing/accesscontrol.go @@ -4,7 +4,7 @@ import "github.com/grafana/grafana/pkg/services/accesscontrol" const ( ActionRead = "licensing:read" - ActionUpdate = "licensing:update" + ActionUpdate = "licensing:write" ActionDelete = "licensing:delete" ActionReportsRead = "licensing.reports:read" ) diff --git a/pkg/services/ngalert/accesscontrol.go b/pkg/services/ngalert/accesscontrol.go index 97340750f13..fa442b9e80b 100644 --- a/pkg/services/ngalert/accesscontrol.go +++ b/pkg/services/ngalert/accesscontrol.go @@ -36,7 +36,7 @@ var ( DisplayName: "Rules Editor", Description: "Can add, update, and delete rules in any Grafana folder and external providers", Group: AlertRolesGroup, - Version: 2, + Version: 3, Permissions: accesscontrol.ConcatPermissions(rulesReaderRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingRuleCreate, @@ -84,7 +84,7 @@ var ( DisplayName: "Silences Editor", Description: "Can add and update silences in Grafana and external providers", Group: AlertRolesGroup, - Version: 1, + Version: 2, Permissions: accesscontrol.ConcatPermissions(instancesReaderRole.Role.Permissions, []accesscontrol.Permission{ { Action: accesscontrol.ActionAlertingInstanceCreate, diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 4dd44d14193..f0627bd74d9 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -209,25 +209,7 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o return nil }) - if err != nil { - return nil, err - } - - // Get Teams of service account. Can be optimized by combining with the query above - // in refactor - getTeamQuery := models.GetTeamsByUserQuery{UserId: serviceAccountID, OrgId: orgID} - if err := s.sqlStore.GetTeamsByUser(ctx, &getTeamQuery); err != nil { - return nil, err - } - teams := make([]string, len(getTeamQuery.Result)) - - for i := range getTeamQuery.Result { - teams[i] = getTeamQuery.Result[i].Name - } - - serviceAccount.Teams = teams - - return serviceAccount, nil + return serviceAccount, err } func (s *ServiceAccountsStoreImpl) RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error) { diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go index 44c388034dd..97a40680d01 100644 --- a/pkg/services/serviceaccounts/database/database_test.go +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -4,7 +4,6 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -107,20 +106,3 @@ func TestStore_RetrieveServiceAccount(t *testing.T) { }) } } -func TestStore_RetrieveServiceAccountWithTeams(t *testing.T) { - userToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} - db, store := setupTestDatabase(t) - user := tests.SetupUserServiceAccount(t, db, userToCreate) - - team, err := store.sqlStore.CreateTeam("serviceTeam", "serviceTeam", user.OrgId) - require.NoError(t, err) - - err = store.sqlStore.AddTeamMember(user.Id, user.OrgId, team.Id, false, models.PERMISSION_VIEW) - require.NoError(t, err) - - dto, err := store.RetrieveServiceAccount(context.Background(), user.OrgId, user.Id) - require.NoError(t, err) - require.Equal(t, userToCreate.Login, dto.Login) - require.Len(t, dto.Teams, 1) - require.Equal(t, "serviceTeam", dto.Teams[0]) -} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go new file mode 100644 index 00000000000..704f268812f --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/action_migrator.go @@ -0,0 +1,68 @@ +package accesscontrol + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + + "xorm.io/xorm" +) + +func AddActionNameMigrator(mg *migrator.Migrator) { + mg.AddMigration("RBAC action name migrator", &actionNameMigrator{}) +} + +type actionNameMigrator struct { + sess *xorm.Session + migrator *migrator.Migrator + migrator.MigrationBase +} + +var _ migrator.CodeMigration = new(actionNameMigrator) + +func (m *actionNameMigrator) SQL(migrator.Dialect) string { + return CodeMigrationSQL +} + +func (m *actionNameMigrator) Exec(sess *xorm.Session, migrator *migrator.Migrator) error { + m.sess = sess + m.migrator = migrator + return m.migrateActionNames() +} + +func (m *actionNameMigrator) migrateActionNames() error { + actionNameMapping := map[string]string{ + "licensing:update": "licensing:write", + "reports.admin:create": "reports:create", + "reports.admin:write": "reports:write", + "org.users.role:update": accesscontrol.ActionOrgUsersWrite, + "users.authtoken:update": accesscontrol.ActionUsersAuthTokenUpdate, + "users.password:update": accesscontrol.ActionUsersPasswordUpdate, + "users.permissions:update": accesscontrol.ActionUsersPermissionsUpdate, + "users.quotas:update": accesscontrol.ActionUsersQuotasUpdate, + "teams.roles:list": "teams.roles:read", + "users.roles:list": "users.roles:read", + "users.authtoken:list": accesscontrol.ActionUsersAuthTokenList, + "users.quotas:list": accesscontrol.ActionUsersQuotasList, + "users.permissions:list": "users.permissions:read", + "alert.instances:update": accesscontrol.ActionAlertingInstanceUpdate, + "alert.rules:update": accesscontrol.ActionAlertingRuleUpdate, + } + for oldName, newName := range actionNameMapping { + _, err := m.sess.Table(&accesscontrol.Permission{}).Where("action = ?", oldName).Update(&accesscontrol.Permission{Action: newName}) + if err != nil { + return fmt.Errorf("failed to update permission table for action %s: %w", oldName, err) + } + } + + actionsToDelete := []string{"users.teams:read", "roles:list"} + for _, action := range actionsToDelete { + _, err := m.sess.Table(&accesscontrol.Permission{}).Where("action = ?", action).Delete(accesscontrol.Permission{}) + if err != nil { + return fmt.Errorf("failed to update permission table for action %s: %w", action, err) + } + } + + return nil +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 5804bce53a1..e3eb05aad52 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -90,6 +90,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { accesscontrol.AddManagedPermissionsMigration(mg) accesscontrol.AddManagedFolderAlertActionsMigration(mg) + accesscontrol.AddActionNameMigrator(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 065cc901e29..b303b031a51 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -310,12 +310,23 @@ func (ss *SQLStore) GetTeamsByUser(ctx context.Context, query *models.GetTeamsBy query.Result = make([]*models.TeamDTO, 0) var sql bytes.Buffer + var params []interface{} + params = append(params, query.OrgId, query.UserId) sql.WriteString(getTeamSelectSQLBase([]string{})) sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`) sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`) - err := sess.SQL(sql.String(), query.OrgId, query.UserId).Find(&query.Result) + if !ac.IsDisabled(ss.Cfg) { + acFilter, err := ac.Filter(query.SignedInUser, "team.id", "teams:id:", ac.ActionTeamsRead) + if err != nil { + return err + } + sql.WriteString(` and` + acFilter.Where) + params = append(params, acFilter.Args...) + } + + err := sess.SQL(sql.String(), params...).Find(&query.Result) return err }) } diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index 0284b45c84e..aff00337940 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -222,7 +222,14 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { err := sqlStore.AddTeamMember(userIds[0], testOrgID, groupId, false, 0) require.NoError(t, err) - query := &models.GetTeamsByUserQuery{OrgId: testOrgID, UserId: userIds[0]} + query := &models.GetTeamsByUserQuery{ + OrgId: testOrgID, + UserId: userIds[0], + SignedInUser: &models.SignedInUser{ + OrgId: testOrgID, + Permissions: map[int64]map[string][]string{testOrgID: {ac.ActionOrgUsersRead: {ac.ScopeUsersAll}, ac.ActionTeamsRead: {ac.ScopeTeamsAll}}}, + }, + } err = sqlStore.GetTeamsByUser(context.Background(), query) require.NoError(t, err) require.Equal(t, len(query.Result), 1) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 51b21360002..51f68366bfb 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -577,7 +577,20 @@ func (ss *SQLStore) GetSignedInUser(ctx context.Context, query *models.GetSigned user.ExternalAuthId = "" } - getTeamsByUserQuery := &models.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId} + // tempUser is used to retrieve the teams for the signed in user for internal use. + tempUser := &models.SignedInUser{ + OrgId: user.OrgId, + Permissions: map[int64]map[string][]string{ + user.OrgId: { + ac.ActionTeamsRead: {ac.ScopeTeamsAll}, + }, + }, + } + getTeamsByUserQuery := &models.GetTeamsByUserQuery{ + OrgId: user.OrgId, + UserId: user.UserId, + SignedInUser: tempUser, + } err = ss.GetTeamsByUser(ctx, getTeamsByUserQuery) if err != nil { return err diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index d7ff048ab13..705f882d9a9 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -617,7 +617,7 @@ func TestRulerAccess(t *testing.T) { desc: "viewer request should fail", url: "http://viewer:viewer@%s/api/ruler/grafana/api/v1/rules/default", expStatus: http.StatusForbidden, - expectedMessage: `You'll need additional permissions to perform this action. Permissions needed: any of alert.rules:update, alert.rules:create, alert.rules:delete`, + expectedMessage: `You'll need additional permissions to perform this action. Permissions needed: any of alert.rules:write, alert.rules:create, alert.rules:delete`, }, { desc: "editor request should succeed", diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index bb90b80a665..b85247e1248 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -193,7 +193,7 @@ class UnThemedOrgRow extends PureComponent { const { currentRole, isChangingRole } = this.state; const styles = getOrgRowStyles(theme); const labelClass = cx('width-16', styles.label); - const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersRoleUpdate); + const canChangeRole = contextSrv.hasPermission(AccessControlAction.OrgUsersWrite); const canRemoveFromOrg = contextSrv.hasPermission(AccessControlAction.OrgUsersRemove); const rolePickerDisabled = isExternalUser || !canChangeRole; @@ -328,7 +328,7 @@ export class AddToOrgModal extends PureComponent diff --git a/public/app/features/users/UsersTable.tsx b/public/app/features/users/UsersTable.tsx index a31a0229f7f..71d26e057e4 100644 --- a/public/app/features/users/UsersTable.tsx +++ b/public/app/features/users/UsersTable.tsx @@ -94,13 +94,13 @@ const UsersTable: FC = (props) => { onBuiltinRoleChange={(newRole) => onRoleChange(newRole, user)} roleOptions={roleOptions} builtInRoles={builtinRoles} - disabled={!contextSrv.hasPermissionInMetadata(AccessControlAction.OrgUsersRoleUpdate, user)} + disabled={!contextSrv.hasPermissionInMetadata(AccessControlAction.OrgUsersWrite, user)} /> ) : ( onRoleChange(newRole, user)} /> )} diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index d98c1127f4c..c86c922880d 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -10,18 +10,17 @@ export type UserPermission = Record; export enum AccessControlAction { UsersRead = 'users:read', UsersWrite = 'users:write', - UsersTeamRead = 'users.teams:read', - UsersAuthTokenList = 'users.authtoken:list', - UsersAuthTokenUpdate = 'users.authtoken:update', - UsersPasswordUpdate = 'users.password:update', + UsersAuthTokenList = 'users.authtoken:read', + UsersAuthTokenUpdate = 'users.authtoken:write', + UsersPasswordUpdate = 'users.password:write', UsersDelete = 'users:delete', UsersCreate = 'users:create', UsersEnable = 'users:enable', UsersDisable = 'users:disable', - UsersPermissionsUpdate = 'users.permissions:update', + UsersPermissionsUpdate = 'users.permissions:write', UsersLogout = 'users:logout', - UsersQuotasList = 'users.quotas:list', - UsersQuotasUpdate = 'users.quotas:update', + UsersQuotasList = 'users.quotas:read', + UsersQuotasUpdate = 'users.quotas:write', ServiceAccountsRead = 'serviceaccounts:read', ServiceAccountsCreate = 'serviceaccounts:create', @@ -37,7 +36,7 @@ export enum AccessControlAction { OrgUsersRead = 'org.users:read', OrgUsersAdd = 'org.users:add', OrgUsersRemove = 'org.users:remove', - OrgUsersRoleUpdate = 'org.users.role:update', + OrgUsersWrite = 'org.users:write', LDAPUsersRead = 'ldap.user:read', LDAPUsersSync = 'ldap.user:sync', @@ -59,12 +58,12 @@ export enum AccessControlAction { ActionTeamsPermissionsRead = 'teams.permissions:read', ActionTeamsPermissionsWrite = 'teams.permissions:write', - ActionRolesList = 'roles:list', + ActionRolesList = 'roles:read', ActionBuiltinRolesList = 'roles.builtin:list', - ActionTeamsRolesList = 'teams.roles:list', + ActionTeamsRolesList = 'teams.roles:read', ActionTeamsRolesAdd = 'teams.roles:add', ActionTeamsRolesRemove = 'teams.roles:remove', - ActionUserRolesList = 'users.roles:list', + ActionUserRolesList = 'users.roles:read', DashboardsRead = 'dashboards:read', DashboardsWrite = 'dashboards:write', @@ -83,12 +82,12 @@ export enum AccessControlAction { // Alerting rules AlertingRuleCreate = 'alert.rules:create', AlertingRuleRead = 'alert.rules:read', - AlertingRuleUpdate = 'alert.rules:update', + AlertingRuleUpdate = 'alert.rules:write', AlertingRuleDelete = 'alert.rules:delete', // Alerting instances (+silences) AlertingInstanceCreate = 'alert.instances:create', - AlertingInstanceUpdate = 'alert.instances:update', + AlertingInstanceUpdate = 'alert.instances:write', AlertingInstanceRead = 'alert.instances:read', // Alerting Notification policies From 131f42d59a2b151dbe25e1cedd022342e5021438 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 11:03:05 -0400 Subject: [PATCH 50/95] [v9.0.x] Alerting: Fix alert creation form layout when errors occur (#50106) Co-authored-by: Gilles De Mey --- .../alerting/unified/components/rule-editor/DetailsStep.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 0462b64845f..5cefd9b21f5 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import classNames from 'classnames'; import React, { FC } from 'react'; import { useFormContext } from 'react-hook-form'; @@ -75,7 +76,7 @@ export const DetailsStep: FC = () => { dataSourceName && } {ruleFormType === RuleFormType.grafana && ( -
+
@@ -137,6 +138,9 @@ export const DetailsStep: FC = () => { }; const getStyles = (theme: GrafanaTheme2) => ({ + alignBaseline: css` + align-items: baseline; + `, formInput: css` width: 330px; & + & { From ce6fb9f083f59913b443e4f7e1a9e867bd66e430 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 11:03:33 -0400 Subject: [PATCH 51/95] [v9.0.x] Alerting: Fix alert list panel showing firing alerts with no instances (#50095) Co-authored-by: Gilles De Mey --- .../panel/alertlist/AlertInstances.tsx | 21 +++++++++++++++---- .../panel/alertlist/UnifiedAlertList.tsx | 20 +++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/panel/alertlist/AlertInstances.tsx b/public/app/plugins/panel/alertlist/AlertInstances.tsx index 8fd3cd5dfe6..7a4e7abbbbb 100644 --- a/public/app/plugins/panel/alertlist/AlertInstances.tsx +++ b/public/app/plugins/panel/alertlist/AlertInstances.tsx @@ -1,6 +1,7 @@ import { css } from '@emotion/css'; +import { noop } from 'lodash'; import pluralize from 'pluralize'; -import React, { FC, useCallback, useMemo, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { GrafanaTheme2, PanelProps } from '@grafana/data'; import { Icon, useStyles2 } from '@grafana/ui'; @@ -31,12 +32,24 @@ export const AlertInstances: FC = ({ alerts, options }) => { [alerts, options] ); + const hiddenInstances = alerts.length - filteredAlerts.length; + + const uncollapsible = filteredAlerts.length > 0; + const toggleShowInstances = uncollapsible ? toggleDisplayInstances : noop; + + useEffect(() => { + if (filteredAlerts.length === 0) { + setDisplayInstances(false); + } + }, [filteredAlerts]); + return (
{options.groupMode === GroupMode.Default && ( -
toggleDisplayInstances()}> - +
toggleShowInstances()}> + {uncollapsible && } {`${filteredAlerts.length} ${pluralize('instance', filteredAlerts.length)}`} + {hiddenInstances > 0 && , {`${hiddenInstances} hidden by filters`}}
)} {displayInstances && } @@ -45,7 +58,7 @@ export const AlertInstances: FC = ({ alerts, options }) => { }; const getStyles = (_: GrafanaTheme2) => ({ - instance: css` + clickable: css` cursor: pointer; `, }); diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx index 194b7ceea09..3d423a8a843 100644 --- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx +++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx @@ -60,7 +60,7 @@ export function UnifiedAlertList(props: PanelProps) { [props, promRulesRequests] ); - const noAlertsMessage = rules.length ? '' : 'No alerts'; + const noAlertsMessage = rules.length === 0 ? 'No alerts matching filters' : undefined; if ( !contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) && @@ -122,15 +122,15 @@ function filterRules(props: PanelProps, rules: PromRule name.toLocaleLowerCase().includes(replacedName.toLocaleLowerCase()) ); } - if (Object.values(options.stateFilter).some((value) => value)) { - filteredRules = filteredRules.filter((rule) => { - return ( - (options.stateFilter.firing && rule.rule.state === PromAlertingRuleState.Firing) || - (options.stateFilter.pending && rule.rule.state === PromAlertingRuleState.Pending) || - (options.stateFilter.inactive && rule.rule.state === PromAlertingRuleState.Inactive) - ); - }); - } + + filteredRules = filteredRules.filter((rule) => { + return ( + (options.stateFilter.firing && rule.rule.state === PromAlertingRuleState.Firing) || + (options.stateFilter.pending && rule.rule.state === PromAlertingRuleState.Pending) || + (options.stateFilter.inactive && rule.rule.state === PromAlertingRuleState.Inactive) + ); + }); + if (options.alertInstanceLabelFilter) { const replacedLabelFilter = replaceVariables(options.alertInstanceLabelFilter); const matchers = parseMatchers(replacedLabelFilter); From 3422f70f666528364101de8d339853144386c37d Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 12:08:53 -0400 Subject: [PATCH 52/95] SearchV2: include appSubUrl in the response URL (#50059) (#50118) (cherry picked from commit 15b3bbad6b0bf6c091dde046220dcc4b199fc6a9) Co-authored-by: Ryan McKinley --- pkg/services/searchV2/bluge.go | 12 ++++++++++-- pkg/services/searchV2/index_test.go | 2 +- pkg/services/searchV2/service.go | 2 +- pkg/services/searchV2/testdata/basic-search.txt | 4 ++-- pkg/services/searchV2/testdata/dashboard-create.txt | 4 ++-- pkg/services/searchV2/testdata/dashboard-update.txt | 4 ++-- .../testdata/multiple-tokens-beginning-lower.txt | 4 ++-- .../searchV2/testdata/multiple-tokens-beginning.txt | 4 ++-- .../testdata/multiple-tokens-middle-lower.txt | 4 ++-- .../searchV2/testdata/multiple-tokens-middle.txt | 4 ++-- .../testdata/prefix-search-beginning-lower.txt | 4 ++-- .../searchV2/testdata/prefix-search-beginning.txt | 4 ++-- .../searchV2/testdata/prefix-search-middle-lower.txt | 4 ++-- .../searchV2/testdata/prefix-search-middle.txt | 4 ++-- .../testdata/prefix-search-ngram-exceeded.txt | 4 ++-- .../testdata/scattered-tokens-match-reversed.txt | 6 +++--- .../searchV2/testdata/scattered-tokens-match.txt | 6 +++--- pkg/services/searchV2/testdata/sort-asc.txt | 6 +++--- pkg/services/searchV2/testdata/sort-desc.txt | 6 +++--- 19 files changed, 48 insertions(+), 40 deletions(-) diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index e3e542efc83..ebaf3358dc0 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -291,7 +291,15 @@ func getDashboardPanelIDs(reader *bluge.Reader, dashboardUID string) ([]string, } //nolint: gocyclo -func doSearchQuery(ctx context.Context, logger log.Logger, reader *bluge.Reader, filter ResourceFilter, q DashboardQuery, extender QueryExtender) *backend.DataResponse { +func doSearchQuery( + ctx context.Context, + logger log.Logger, + reader *bluge.Reader, + filter ResourceFilter, + q DashboardQuery, + extender QueryExtender, + appSubUrl string, +) *backend.DataResponse { response := &backend.DataResponse{} header := &customMeta{} @@ -473,7 +481,7 @@ func doSearchQuery(ctx context.Context, logger log.Logger, reader *bluge.Reader, case documentFieldName: name = string(value) case documentFieldURL: - url = string(value) + url = appSubUrl + string(value) case documentFieldLocation: loc = string(value) case documentFieldDSUID: diff --git a/pkg/services/searchV2/index_test.go b/pkg/services/searchV2/index_test.go index 662ae09e26b..c958bcbba65 100644 --- a/pkg/services/searchV2/index_test.go +++ b/pkg/services/searchV2/index_test.go @@ -68,7 +68,7 @@ func checkSearchResponse(t *testing.T, fileName string, reader *bluge.Reader, fi func checkSearchResponseExtended(t *testing.T, fileName string, reader *bluge.Reader, filter ResourceFilter, query DashboardQuery, extender QueryExtender) { t.Helper() - resp := doSearchQuery(context.Background(), testLogger, reader, filter, query, extender) + resp := doSearchQuery(context.Background(), testLogger, reader, filter, query, extender, "/pfix") goldenFile := filepath.Join("testdata", fileName) err := experimental.CheckGoldenDataResponse(goldenFile, resp, true) require.NoError(t, err) diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index 6d73bb19c15..1ac2a467353 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -137,5 +137,5 @@ func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *back return rsp } - return doSearchQuery(ctx, s.logger, reader, filter, q, s.extender.GetQueryExtender(q)) + return doSearchQuery(ctx, s.logger, reader, filter, q, s.extender.GetQueryExtender(q), s.cfg.AppSubURL) } diff --git a/pkg/services/searchV2/testdata/basic-search.txt b/pkg/services/searchV2/testdata/basic-search.txt index 89375780cb3..7292cd008e1 100644 --- a/pkg/services/searchV2/testdata/basic-search.txt +++ b/pkg/services/searchV2/testdata/basic-search.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 2 | boom | | /d/2/ | null | null | | +| dashboard | 2 | boom | | /pfix/d/2/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/dashboard-create.txt b/pkg/services/searchV2/testdata/dashboard-create.txt index 79b98ad2fb2..de675b27e4a 100644 --- a/pkg/services/searchV2/testdata/dashboard-create.txt +++ b/pkg/services/searchV2/testdata/dashboard-create.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 3 | created | | /d/3/ | null | null | general | +| dashboard | 3 | created | | /pfix/d/3/ | null | null | general | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/dashboard-update.txt b/pkg/services/searchV2/testdata/dashboard-update.txt index 5ff092ec67a..83928c0d0c7 100644 --- a/pkg/services/searchV2/testdata/dashboard-update.txt +++ b/pkg/services/searchV2/testdata/dashboard-update.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 2 | nginx | | /d/2/ | null | null | general | +| dashboard | 2 | nginx | | /pfix/d/2/ | null | null | general | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.txt b/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.txt index 05dd4ec625b..cd8b7d21e0d 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.txt +++ b/pkg/services/searchV2/testdata/multiple-tokens-beginning-lower.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Archer Data | | /d/1/ | null | null | | +| dashboard | 1 | Archer Data | | /pfix/d/1/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/multiple-tokens-beginning.txt b/pkg/services/searchV2/testdata/multiple-tokens-beginning.txt index 05dd4ec625b..cd8b7d21e0d 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-beginning.txt +++ b/pkg/services/searchV2/testdata/multiple-tokens-beginning.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Archer Data | | /d/1/ | null | null | | +| dashboard | 1 | Archer Data | | /pfix/d/1/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.txt b/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.txt index 782f301df7d..359c6ed7fd3 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.txt +++ b/pkg/services/searchV2/testdata/multiple-tokens-middle-lower.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 2 | Document Sync | | /d/2/ | null | null | | +| dashboard | 2 | Document Sync | | /pfix/d/2/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/multiple-tokens-middle.txt b/pkg/services/searchV2/testdata/multiple-tokens-middle.txt index 05dd4ec625b..cd8b7d21e0d 100644 --- a/pkg/services/searchV2/testdata/multiple-tokens-middle.txt +++ b/pkg/services/searchV2/testdata/multiple-tokens-middle.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Archer Data | | /d/1/ | null | null | | +| dashboard | 1 | Archer Data | | /pfix/d/1/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/prefix-search-beginning-lower.txt b/pkg/services/searchV2/testdata/prefix-search-beginning-lower.txt index 05dd4ec625b..cd8b7d21e0d 100644 --- a/pkg/services/searchV2/testdata/prefix-search-beginning-lower.txt +++ b/pkg/services/searchV2/testdata/prefix-search-beginning-lower.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Archer Data | | /d/1/ | null | null | | +| dashboard | 1 | Archer Data | | /pfix/d/1/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/prefix-search-beginning.txt b/pkg/services/searchV2/testdata/prefix-search-beginning.txt index 05dd4ec625b..cd8b7d21e0d 100644 --- a/pkg/services/searchV2/testdata/prefix-search-beginning.txt +++ b/pkg/services/searchV2/testdata/prefix-search-beginning.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Archer Data | | /d/1/ | null | null | | +| dashboard | 1 | Archer Data | | /pfix/d/1/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/prefix-search-middle-lower.txt b/pkg/services/searchV2/testdata/prefix-search-middle-lower.txt index 782f301df7d..359c6ed7fd3 100644 --- a/pkg/services/searchV2/testdata/prefix-search-middle-lower.txt +++ b/pkg/services/searchV2/testdata/prefix-search-middle-lower.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 2 | Document Sync | | /d/2/ | null | null | | +| dashboard | 2 | Document Sync | | /pfix/d/2/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/prefix-search-middle.txt b/pkg/services/searchV2/testdata/prefix-search-middle.txt index 782f301df7d..359c6ed7fd3 100644 --- a/pkg/services/searchV2/testdata/prefix-search-middle.txt +++ b/pkg/services/searchV2/testdata/prefix-search-middle.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 2 | Document Sync | | /d/2/ | null | null | | +| dashboard | 2 | Document Sync | | /pfix/d/2/ | null | null | | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.txt b/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.txt index c6c870d0b3d..12f94187e6a 100644 --- a/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.txt +++ b/pkg/services/searchV2/testdata/prefix-search-ngram-exceeded.txt @@ -13,9 +13,9 @@ Dimensions: 8 Fields by 1 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Eyjafjallajökull Eruption data | | /d/1/ | null | null | | +| dashboard | 1 | Eyjafjallajökull Eruption data | | /pfix/d/1/ | null | null | | +----------------+----------------+--------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/scattered-tokens-match-reversed.txt b/pkg/services/searchV2/testdata/scattered-tokens-match-reversed.txt index 11f1298bcc1..ad06c3381ee 100644 --- a/pkg/services/searchV2/testdata/scattered-tokens-match-reversed.txt +++ b/pkg/services/searchV2/testdata/scattered-tokens-match-reversed.txt @@ -13,10 +13,10 @@ Dimensions: 8 Fields by 2 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /d/2/ | null | null | | -| dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /d/1/ | null | null | | +| dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /pfix/d/2/ | null | null | | +| dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | null | | +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/scattered-tokens-match.txt b/pkg/services/searchV2/testdata/scattered-tokens-match.txt index 97337d7d8e6..5b8d56fa303 100644 --- a/pkg/services/searchV2/testdata/scattered-tokens-match.txt +++ b/pkg/services/searchV2/testdata/scattered-tokens-match.txt @@ -13,10 +13,10 @@ Dimensions: 8 Fields by 2 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+ -| dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /d/1/ | null | null | | -| dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /d/2/ | null | null | | +| dashboard | 1 | Three can keep a secret, if two of them are dead (Benjamin Franklin) | | /pfix/d/1/ | null | null | | +| dashboard | 2 | A secret is powerful when it is empty (Umberto Eco) | | /pfix/d/2/ | null | null | | +----------------+----------------+----------------------------------------------------------------------+------------------+----------------+--------------------------+--------------------------+----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=QVJST1cxAAD/////UAQAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKwAAAADAAAAWAAAACgAAAAEAAAAPPz//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAABc/P//CAAAABgAAAANAAAAUXVlcnkgcmVzdWx0cwAAAAQAAABuYW1lAAAAAIj8//8IAAAAOAAAAC4AAAB7InR5cGUiOiJzZWFyY2gtcmVzdWx0cyIsImN1c3RvbSI6eyJjb3VudCI6Mn19AAAEAAAAbWV0YQAAAAAIAAAACAMAAKACAABEAgAA4AEAADQBAADYAAAAaAAAAAQAAAAq/f//FAAAAEAAAABAAAAAAAAABTwAAAABAAAABAAAABj9//8IAAAAFAAAAAgAAABsb2NhdGlvbgAAAAAEAAAAbmFtZQAAAAAAAAAAFP3//wgAAABsb2NhdGlvbgAAAACm////FAAAADwAAAA8AAAAAAAEATgAAAABAAAABAAAAHj9//8IAAAAEAAAAAYAAABkc191aWQAAAQAAABuYW1lAAAAAAAAAABw/f//BgAAAGRzX3VpZAAAAAASABgAFAATABIADAAAAAgABAASAAAAFAAAADwAAAA8AAAAAAAEATgAAAABAAAABAAAAOT9//8IAAAAEAAAAAQAAAB0YWdzAAAAAAQAAABuYW1lAAAAAAAAAADc/f//BAAAAHRhZ3MAAAAATv7//xQAAACQAAAAkAAAAAAAAAWMAAAAAgAAACgAAAAEAAAAQP7//wgAAAAMAAAAAwAAAHVybAAEAAAAbmFtZQAAAABg/v//CAAAAEAAAAA0AAAAeyJsaW5rcyI6W3sidGl0bGUiOiJsaW5rIiwidXJsIjoiJHtfX3ZhbHVlLnRleHR9In1dfQAAAAAGAAAAY29uZmlnAAAAAAAAiP7//wMAAAB1cmwA9v7//xQAAABAAAAAQAAAAAAAAAU8AAAAAQAAAAQAAADk/v//CAAAABQAAAAKAAAAcGFuZWxfdHlwZQAABAAAAG5hbWUAAAAAAAAAAOD+//8KAAAAcGFuZWxfdHlwZQAAVv///xQAAAA8AAAAPAAAAAAAAAU4AAAAAQAAAAQAAABE////CAAAABAAAAAEAAAAbmFtZQAAAAAEAAAAbmFtZQAAAAAAAAAAPP///wQAAABuYW1lAAAAAK7///8UAAAAOAAAADgAAAAAAAAFNAAAAAEAAAAEAAAAnP///wgAAAAMAAAAAwAAAHVpZAAEAAAAbmFtZQAAAAAAAAAAkP///wMAAAB1aWQAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABIAAAAAAAABUQAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABAAAAGtpbmQAAAAABAAAAG5hbWUAAAAAAAAAAAQABAAEAAAABAAAAGtpbmQAAAAAAAAAAP////9YAgAAFAAAAAAAAAAMABYAFAATAAwABAAMAAAAOAEAAAAAAAAUAAAAAAAAAwQACgAYAAwACAAEAAoAAAAUAAAAmAEAAAIAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAABAAAAAAAAAAEgAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAKAAAAAAAAAAMAAAAAAAAADgAAAAAAAAAAgAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAMAAAAAAAAAFAAAAAAAAAAdwAAAAAAAADIAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAAMAAAAAAAAANgAAAAAAAAAAAAAAAAAAADYAAAAAAAAAAAAAAAAAAAA2AAAAAAAAAAMAAAAAAAAAOgAAAAAAAAACgAAAAAAAAD4AAAAAAAAAAEAAAAAAAAAAAEAAAAAAAAMAAAAAAAAABABAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAEAAAAAAAAAGAEAAAAAAAAMAAAAAAAAACgBAAAAAAAAAAAAAAAAAAAoAQAAAAAAAAAAAAAAAAAAKAEAAAAAAAAMAAAAAAAAADgBAAAAAAAAAAAAAAAAAAAAAAAACAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAkAAAASAAAAAAAAAGRhc2hib2FyZGRhc2hib2FyZAAAAAAAAAAAAAABAAAAAgAAAAAAAAAxMgAAAAAAAAAAAABEAAAAdwAAAAAAAABUaHJlZSBjYW4ga2VlcCBhIHNlY3JldCwgaWYgdHdvIG9mIHRoZW0gYXJlIGRlYWQgKEJlbmphbWluIEZyYW5rbGluKUEgc2VjcmV0IGlzIHBvd2VyZnVsIHdoZW4gaXQgaXMgZW1wdHkgKFVtYmVydG8gRWNvKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAKAAAAAAAAAC9kLzEvL2QvMi8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAAGAEAAAAAAAAYAIAAAAAAAA4AQAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACsAAAAAwAAAFgAAAAoAAAABAAAADz8//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAAXPz//wgAAAAYAAAADQAAAFF1ZXJ5IHJlc3VsdHMAAAAEAAAAbmFtZQAAAACI/P//CAAAADgAAAAuAAAAeyJ0eXBlIjoic2VhcmNoLXJlc3VsdHMiLCJjdXN0b20iOnsiY291bnQiOjJ9fQAABAAAAG1ldGEAAAAACAAAAAgDAACgAgAARAIAAOABAAA0AQAA2AAAAGgAAAAEAAAAKv3//xQAAABAAAAAQAAAAAAAAAU8AAAAAQAAAAQAAAAY/f//CAAAABQAAAAIAAAAbG9jYXRpb24AAAAABAAAAG5hbWUAAAAAAAAAABT9//8IAAAAbG9jYXRpb24AAAAApv///xQAAAA8AAAAPAAAAAAABAE4AAAAAQAAAAQAAAB4/f//CAAAABAAAAAGAAAAZHNfdWlkAAAEAAAAbmFtZQAAAAAAAAAAcP3//wYAAABkc191aWQAAAAAEgAYABQAEwASAAwAAAAIAAQAEgAAABQAAAA8AAAAPAAAAAAABAE4AAAAAQAAAAQAAADk/f//CAAAABAAAAAEAAAAdGFncwAAAAAEAAAAbmFtZQAAAAAAAAAA3P3//wQAAAB0YWdzAAAAAE7+//8UAAAAkAAAAJAAAAAAAAAFjAAAAAIAAAAoAAAABAAAAED+//8IAAAADAAAAAMAAAB1cmwABAAAAG5hbWUAAAAAYP7//wgAAABAAAAANAAAAHsibGlua3MiOlt7InRpdGxlIjoibGluayIsInVybCI6IiR7X192YWx1ZS50ZXh0fSJ9XX0AAAAABgAAAGNvbmZpZwAAAAAAAIj+//8DAAAAdXJsAPb+//8UAAAAQAAAAEAAAAAAAAAFPAAAAAEAAAAEAAAA5P7//wgAAAAUAAAACgAAAHBhbmVsX3R5cGUAAAQAAABuYW1lAAAAAAAAAADg/v//CgAAAHBhbmVsX3R5cGUAAFb///8UAAAAPAAAADwAAAAAAAAFOAAAAAEAAAAEAAAARP///wgAAAAQAAAABAAAAG5hbWUAAAAABAAAAG5hbWUAAAAAAAAAADz///8EAAAAbmFtZQAAAACu////FAAAADgAAAA4AAAAAAAABTQAAAABAAAABAAAAJz///8IAAAADAAAAAMAAAB1aWQABAAAAG5hbWUAAAAAAAAAAJD///8DAAAAdWlkAAAAEgAYABQAAAATAAwAAAAIAAQAEgAAABQAAABEAAAASAAAAAAAAAVEAAAAAQAAAAwAAAAIAAwACAAEAAgAAAAIAAAAEAAAAAQAAABraW5kAAAAAAQAAABuYW1lAAAAAAAAAAAEAAQABAAAAAQAAABraW5kAAAAAHgEAABBUlJPVzE= +FRAME=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 diff --git a/pkg/services/searchV2/testdata/sort-asc.txt b/pkg/services/searchV2/testdata/sort-asc.txt index 643b9d163b0..9efca857109 100644 --- a/pkg/services/searchV2/testdata/sort-asc.txt +++ b/pkg/services/searchV2/testdata/sort-asc.txt @@ -14,10 +14,10 @@ Dimensions: 9 Fields by 2 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []float64 | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+ -| dashboard | 1 | a-test | | /d/1/ | null | null | | 0 | -| dashboard | 2 | z-test | | /d/2/ | null | null | | 1 | +| dashboard | 1 | a-test | | /pfix/d/1/ | null | null | | 0 | +| dashboard | 2 | z-test | | /pfix/d/2/ | null | null | | 1 | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 diff --git a/pkg/services/searchV2/testdata/sort-desc.txt b/pkg/services/searchV2/testdata/sort-desc.txt index f4de969ecb3..520618d821d 100644 --- a/pkg/services/searchV2/testdata/sort-desc.txt +++ b/pkg/services/searchV2/testdata/sort-desc.txt @@ -14,10 +14,10 @@ Dimensions: 9 Fields by 2 Rows | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | Labels: | | Type: []string | Type: []string | Type: []string | Type: []string | Type: []string | Type: []*json.RawMessage | Type: []*json.RawMessage | Type: []string | Type: []float64 | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+ -| dashboard | 2 | z-test | | /d/2/ | null | null | | 3 | -| dashboard | 1 | a-test | | /d/1/ | null | null | | 2 | +| dashboard | 2 | z-test | | /pfix/d/2/ | null | null | | 3 | +| dashboard | 1 | a-test | | /pfix/d/1/ | null | null | | 2 | +----------------+----------------+----------------+------------------+----------------+--------------------------+--------------------------+----------------+-----------------+ ====== TEST DATA RESPONSE (arrow base64) ====== -FRAME=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 +FRAME=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 From afec2786fce8bfecd1e7e1c4af8efda362d7a8c6 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 14:20:29 -0400 Subject: [PATCH 53/95] Loki: Fix uncaught errors if `labelKey` contains special characters (#49887) (#50067) * added regex check of labelKeys - labelKeys should not contain any special characters - added encoding of labelKeys in the URL - don't offer autocomplete if label with special characters is detected * removed additional regex check for labels (cherry picked from commit d7139e75fb303e4753338cba2b2fb8bc9619bd48) Co-authored-by: svennergr --- .../plugins/datasource/loki/language_provider.test.ts | 9 +++++++++ public/app/plugins/datasource/loki/language_provider.ts | 3 ++- public/app/plugins/datasource/loki/mocks.ts | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/loki/language_provider.test.ts b/public/app/plugins/datasource/loki/language_provider.test.ts index de2bcdd94ae..441341dfdd1 100644 --- a/public/app/plugins/datasource/loki/language_provider.test.ts +++ b/public/app/plugins/datasource/loki/language_provider.test.ts @@ -248,6 +248,15 @@ describe('Language completion provider', () => { expect(requestSpy).toHaveBeenCalledTimes(1); expect(nextLabelValues).toEqual(['label1_val1', 'label1_val2']); }); + + it('should encode special characters', async () => { + const datasource = makeMockLokiDatasource({ '`\\"testkey': ['label1_val1', 'label1_val2'], label2: [] }); + const provider = await getLanguageProvider(datasource); + const requestSpy = jest.spyOn(provider, 'request'); + await provider.fetchLabelValues('`\\"testkey'); + + expect(requestSpy).toHaveBeenCalledWith('label/%60%5C%22testkey/values', expect.any(Object)); + }); }); }); diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index 0d7cb538ade..ca50bfcd770 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -439,7 +439,8 @@ export default class LokiLanguageProvider extends LanguageProvider { } async fetchLabelValues(key: string): Promise { - const interpolatedKey = this.datasource.interpolateString(key); + const interpolatedKey = encodeURIComponent(this.datasource.interpolateString(key)); + const url = `label/${interpolatedKey}/values`; const rangeParams = this.datasource.getTimeRangeParams(); const { start, end } = rangeParams; diff --git a/public/app/plugins/datasource/loki/mocks.ts b/public/app/plugins/datasource/loki/mocks.ts index 47fce43c4dd..bab7b758a9b 100644 --- a/public/app/plugins/datasource/loki/mocks.ts +++ b/public/app/plugins/datasource/loki/mocks.ts @@ -18,7 +18,8 @@ interface SeriesForSelector { } export function makeMockLokiDatasource(labelsAndValues: Labels, series?: SeriesForSelector): LokiDatasource { - const lokiLabelsAndValuesEndpointRegex = /^label\/(\w*)\/values/; + // added % to allow urlencoded labelKeys. Note, that this is not confirm with Loki, as loki does not allow specialcharacters in labelKeys, but needed for tests. + const lokiLabelsAndValuesEndpointRegex = /^label\/([%\w]*)\/values/; const lokiSeriesEndpointRegex = /^series/; const lokiLabelsEndpoint = 'labels'; From d8eb9fcb9c3d4cd4437ed8039fc392666a452d5b Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 20:39:56 -0400 Subject: [PATCH 54/95] Search: exclude rows from search index (#50124) (#50132) (cherry picked from commit efca93a3f32a6ee719f8c97835dc1d2210364cc2) Co-authored-by: Ryan McKinley --- pkg/services/searchV2/bluge.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index ebaf3358dc0..6982db40e79 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -186,12 +186,13 @@ func getDashboardPanelDocs(dash dashboard, location string) []*bluge.Document { var docs []*bluge.Document url := fmt.Sprintf("/d/%s/%s", dash.uid, dash.slug) for _, panel := range dash.info.Panels { - uid := dash.uid + "#" + strconv.FormatInt(panel.ID, 10) - purl := url - if panel.Type != "row" { - purl = fmt.Sprintf("%s?viewPanel=%d", url, panel.ID) + if panel.Type == "row" { + continue // for now, we are excluding rows from the search index } + uid := dash.uid + "#" + strconv.FormatInt(panel.ID, 10) + purl := fmt.Sprintf("%s?viewPanel=%d", url, panel.ID) + doc := newSearchDocument(uid, panel.Title, panel.Description, purl). AddField(bluge.NewKeywordField(documentFieldDSUID, dash.uid).StoreValue()). AddField(bluge.NewKeywordField(documentFieldPanelType, panel.Type).Aggregatable().StoreValue()). From d5e996780964932c98bbf395bad075fdc8c599d9 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 21:06:13 -0400 Subject: [PATCH 55/95] Add a section to the alerting documents for performance considerations (#49663) (#50087) This change adds documentation about how the configuration of Grafana Alerting affects the performance of the Grafana backend, and how to control the load that Grafana Alerting generates. Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> (cherry picked from commit 8ad1f4f9be0b2edb8758a8f3e824d47822353818) --- docs/sources/alerting/_index.md | 1 + docs/sources/alerting/performance.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 docs/sources/alerting/performance.md diff --git a/docs/sources/alerting/_index.md b/docs/sources/alerting/_index.md index ff0d66edb68..97e11964f61 100644 --- a/docs/sources/alerting/_index.md +++ b/docs/sources/alerting/_index.md @@ -35,3 +35,4 @@ Before you begin, we recommend that you familiarize yourself with some of the [f - [Add or edit an alert contact point]({{< relref "contact-points/_index.md" >}}) - [Add or edit notification policies]({{< relref "notifications/_index.md" >}}) - [Add or edit silences]({{< relref "silences/_index.md" >}}) +- [Performance considerations for alerting]({{< relref "performance.md" >}}) diff --git a/docs/sources/alerting/performance.md b/docs/sources/alerting/performance.md new file mode 100644 index 00000000000..a24fa0bb661 --- /dev/null +++ b/docs/sources/alerting/performance.md @@ -0,0 +1,24 @@ ++++ +title = "Performance considerations" +description = "Understanding alerting performance" +keywords = ["grafana", "alerting", "performance"] +weight = 100 ++++ + +# Alerting performance considerations + +Grafana alerting supports multi-dimensional alerting, where one alert rule can generate many alerts. For example, you can configure an alert rule to fire an alert every time the CPU of individual VMs max out. This topic discusses performance considerations resulting from multi-dimensional alerting. + +Evaluating alerting rules consumes RAM and CPU to compute the output of an alerting query, and network resources to send alert notifications and write the results to the Grafana SQL database. The configuration of individual alert rules affects the resource consumption and, therefore, the maximum number of rules a given configuration can support. + +The following section provides a list of alerting performance considerations. + +- Frequency of rule evaluation consideration. The "Evaluate Every" property of an alert rule controls the frequency of rule evaluation. We recommend using the lowest acceptable evaluation frequency to support more concurrent rules. +- Cardinality of the rule's result set. For example, suppose you are monitoring API response errors for every API path, on every VM in your fleet. This set has a cardinality of _n_ number of paths multiplied by _v_ number of VMs. You can reduce the cardinality of a result set - perhaps by monitoring errors-per-VM instead of for each path per VM. +- Complexity of the alerting query consideration. Queries that data sources can process and respond to quickly consume fewer resources. Although this consideration is less important than the other considerations listed above, if you have reduced those as much as possible, looking at individual query performance could make a difference. + +Each evaluation of an alert rule generates a set of alert instances; one for each member of the result set. The state of all the instances is written to the `alert_instance` table in Grafana's SQL database. + +Grafana alerting exposes a metric, `grafana_alerting_rule_evaluations_total` that counts the number of alert rule evaluations. To get a feel for the influence of rule evaluations on your Grafana instance, you can observe the rate of evaluations and compare it with resource consumption. In a Prometheus-compatible database, you can use the query `rate(grafana_alerting_rule_evaluations_total[5m])` to compute the rate over 5 minute windows of time. It's important to remember that this isn't the full picture of rule evaluation. For example, the load will be unevenly distributed if you have some rules that evaluate every 10 seconds, and others every 30 minutes. + +These factors all affect the load on the Grafana instance, but you should also be aware of the performance impact that evaluating these rules has on your data sources. Alerting queries are often the vast majority of queries handled by monitoring databases, so the same load factors that affect the Grafana instance affect them as well. From e674c9a2d1115433db7d432218ba0513b955c43a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 21:10:29 -0400 Subject: [PATCH 56/95] [v9.0.x] Search (SQL): support dashboardUID query parameter (#50126) * Search (SQL): support dashboardUID query parameter (#50121) (cherry picked from commit d452322aa8ee94c1f1fd764568fb5681dc0f3b68) * manual merge Co-authored-by: Ryan McKinley --- pkg/api/search.go | 31 ++++++++----- pkg/models/search.go | 25 ++++++----- pkg/services/dashboards/database/database.go | 6 ++- pkg/services/search/service.go | 46 ++++++++++---------- pkg/services/sqlstore/dashboard.go | 6 ++- pkg/services/sqlstore/searchstore/filters.go | 27 +++++++++++- public/app/features/search/service/sql.ts | 7 +-- 7 files changed, 91 insertions(+), 57 deletions(-) diff --git a/pkg/api/search.go b/pkg/api/search.go index a28d0275a3b..042a008ef82 100644 --- a/pkg/api/search.go +++ b/pkg/api/search.go @@ -39,6 +39,8 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { } } + dbUIDs := c.QueryStrings("dashboardUID") + folderIDs := make([]int64, 0) for _, id := range c.QueryStrings("folderIds") { folderID, err := strconv.ParseInt(id, 10, 64) @@ -47,19 +49,24 @@ func (hs *HTTPServer) Search(c *models.ReqContext) response.Response { } } + if len(dbIDs) > 0 && len(dbUIDs) > 0 { + return response.Error(400, "search supports UIDs or IDs, not both", nil) + } + searchQuery := search.Query{ - Title: query, - Tags: tags, - SignedInUser: c.SignedInUser, - Limit: limit, - Page: page, - IsStarred: starred == "true", - OrgId: c.OrgId, - DashboardIds: dbIDs, - Type: dashboardType, - FolderIds: folderIDs, - Permission: permission, - Sort: sort, + Title: query, + Tags: tags, + SignedInUser: c.SignedInUser, + Limit: limit, + Page: page, + IsStarred: starred == "true", + OrgId: c.OrgId, + DashboardIds: dbIDs, + DashboardUIDs: dbUIDs, + Type: dashboardType, + FolderIds: folderIDs, + Permission: permission, + Sort: sort, } err := hs.SearchService.SearchHandler(c.Req.Context(), &searchQuery) diff --git a/pkg/models/search.go b/pkg/models/search.go index 2bc351678e7..db6f9090c2a 100644 --- a/pkg/models/search.go +++ b/pkg/models/search.go @@ -20,18 +20,19 @@ type SortOptionFilter interface { } type FindPersistedDashboardsQuery struct { - Title string - OrgId int64 - SignedInUser *SignedInUser - IsStarred bool - DashboardIds []int64 - Type string - FolderIds []int64 - Tags []string - Limit int64 - Page int64 - Permission PermissionType - Sort SortOption + Title string + OrgId int64 + SignedInUser *SignedInUser + IsStarred bool + DashboardIds []int64 + DashboardUIDs []string + Type string + FolderIds []int64 + Tags []string + Limit int64 + Page int64 + Permission PermissionType + Sort SortOption Filters []interface{} diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 684477e7291..cdb1f60681a 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -1024,8 +1024,10 @@ func (d *DashboardStore) FindDashboards(ctx context.Context, query *models.FindP filters = append(filters, searchstore.TagsFilter{Tags: query.Tags}) } - if len(query.DashboardIds) > 0 { - filters = append(filters, searchstore.DashboardFilter{IDs: query.DashboardIds}) + if len(query.DashboardUIDs) > 0 { + filters = append(filters, searchstore.DashboardFilter{UIDs: query.DashboardUIDs}) + } else if len(query.DashboardIds) > 0 { + filters = append(filters, searchstore.DashboardIDFilter{IDs: query.DashboardIds}) } if query.IsStarred { diff --git a/pkg/services/search/service.go b/pkg/services/search/service.go index fc5c1a0bf06..812c4e8a5ab 100644 --- a/pkg/services/search/service.go +++ b/pkg/services/search/service.go @@ -25,18 +25,19 @@ func ProvideService(cfg *setting.Cfg, sqlstore *sqlstore.SQLStore, starService s } type Query struct { - Title string - Tags []string - OrgId int64 - SignedInUser *models.SignedInUser - Limit int64 - Page int64 - IsStarred bool - Type string - DashboardIds []int64 - FolderIds []int64 - Permission models.PermissionType - Sort string + Title string + Tags []string + OrgId int64 + SignedInUser *models.SignedInUser + Limit int64 + Page int64 + IsStarred bool + Type string + DashboardUIDs []string + DashboardIds []int64 + FolderIds []int64 + Permission models.PermissionType + Sort string Result models.HitList } @@ -55,16 +56,17 @@ type SearchService struct { func (s *SearchService) SearchHandler(ctx context.Context, query *Query) error { dashboardQuery := models.FindPersistedDashboardsQuery{ - Title: query.Title, - SignedInUser: query.SignedInUser, - IsStarred: query.IsStarred, - DashboardIds: query.DashboardIds, - Type: query.Type, - FolderIds: query.FolderIds, - Tags: query.Tags, - Limit: query.Limit, - Page: query.Page, - Permission: query.Permission, + Title: query.Title, + SignedInUser: query.SignedInUser, + IsStarred: query.IsStarred, + DashboardUIDs: query.DashboardUIDs, + DashboardIds: query.DashboardIds, + Type: query.Type, + FolderIds: query.FolderIds, + Tags: query.Tags, + Limit: query.Limit, + Page: query.Page, + Permission: query.Permission, } if sortOpt, exists := s.sortOptions[query.Sort]; exists { diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 7f77158e748..f5c76933165 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -75,8 +75,10 @@ func (ss *SQLStore) FindDashboards(ctx context.Context, query *models.FindPersis filters = append(filters, searchstore.TagsFilter{Tags: query.Tags}) } - if len(query.DashboardIds) > 0 { - filters = append(filters, searchstore.DashboardFilter{IDs: query.DashboardIds}) + if len(query.DashboardUIDs) > 0 { + filters = append(filters, searchstore.DashboardFilter{UIDs: query.DashboardUIDs}) + } else if len(query.DashboardIds) > 0 { + filters = append(filters, searchstore.DashboardIDFilter{IDs: query.DashboardIds}) } if query.IsStarred { diff --git a/pkg/services/sqlstore/searchstore/filters.go b/pkg/services/sqlstore/searchstore/filters.go index 99810190c38..5d82e1720f9 100644 --- a/pkg/services/sqlstore/searchstore/filters.go +++ b/pkg/services/sqlstore/searchstore/filters.go @@ -95,14 +95,22 @@ func (f FolderFilter) Where() (string, []interface{}) { return sqlIDin("dashboard.folder_id", f.IDs) } -type DashboardFilter struct { +type DashboardIDFilter struct { IDs []int64 } -func (f DashboardFilter) Where() (string, []interface{}) { +func (f DashboardIDFilter) Where() (string, []interface{}) { return sqlIDin("dashboard.id", f.IDs) } +type DashboardFilter struct { + UIDs []string +} + +func (f DashboardFilter) Where() (string, []interface{}) { + return sqlUIDin("dashboard.uid", f.UIDs) +} + type TagsFilter struct { Tags []string } @@ -150,6 +158,21 @@ func sqlIDin(column string, ids []int64) (string, []interface{}) { return fmt.Sprintf("%s IN %s", column, sqlArray), params } +func sqlUIDin(column string, uids []string) (string, []interface{}) { + length := len(uids) + if length < 1 { + return "", nil + } + + sqlArray := "(?" + strings.Repeat(",?", length-1) + ")" + + params := []interface{}{} + for _, id := range uids { + params = append(params, id) + } + return fmt.Sprintf("%s IN %s", column, sqlArray), params +} + // FolderWithAlertsFilter applies a filter that makes the result contain only folders that contain alert rules type FolderWithAlertsFilter struct { } diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts index 96e68ac6946..d20b38b7001 100644 --- a/public/app/features/search/service/sql.ts +++ b/public/app/features/search/service/sql.ts @@ -16,11 +16,9 @@ interface APIQuery { page?: number; type?: string; // DashboardIds []int64 + dashboardUID?: string[]; folderIds?: number[]; sort?: string; - - // NEW!!!! TODO TODO: needs backend support? - dashboardUIDs?: string[]; } // Internal object to hold folderId @@ -57,8 +55,7 @@ export class SQLSearcher implements GrafanaSearcher { } if (query.uid) { - q.query = query.uid.join(', '); // TODO! this will return nothing - q.dashboardUIDs = query.uid; + q.dashboardUID = query.uid; } else if (query.location?.length) { let info = this.locationInfo[query.location]; if (!info) { From ce10741d769f31a4c7cde563ad78786da88f290a Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Thu, 2 Jun 2022 23:38:04 -0400 Subject: [PATCH 57/95] AzureMonitor: add NewDimension component using experimental UI (#48946) (#50133) * AzureMonitor: add NewDimension component using experimental UI This new component is exercised by the same unit test file as the current Dimension component. Also cleans up a few unneeded `await` keywords in the Dimensions test file. * AzureMonitor: make tweaks based on PR comments. - I was importing the wrong Field component - We can use a typeguard to avoid the strange `if`. (cherry picked from commit 53cb94a2ad0888a52ba859712519ad6f0c153d26) Co-authored-by: Adam Simpson --- .../DimensionFields.test.tsx | 674 +++++++++--------- .../MetricsQueryEditor/NewDimensionFields.tsx | 206 ++++++ .../MetricsQueryEditor.tsx | 7 +- 3 files changed, 557 insertions(+), 330 deletions(-) create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NewDimensionFields.tsx diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx index b4c6e16cf31..13635a0d36c 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/DimensionFields.test.tsx @@ -10,6 +10,7 @@ import createMockPanelData from '../../__mocks__/panelData'; import createMockQuery from '../../__mocks__/query'; import DimensionFields from './DimensionFields'; +import NewDimensionFields from './NewDimensionFields'; import { appendDimensionFilter, setDimensionFilterValue } from './setQueryValue'; const variableOptionGroup = { @@ -18,342 +19,365 @@ const variableOptionGroup = { }; const user = userEvent.setup(); -describe('Azure Monitor QueryEditor', () => { - const mockDatasource = createMockDatasource(); +const tests = [ + { + component: DimensionFields, + label: 'Dimension Fields', + addDimension: async () => { + const addDimension = await screen.findByText('Add new dimension'); + await user.click(addDimension); + }, + }, + { + component: NewDimensionFields, + label: 'Dimension Fields experimental UI', + addDimension: async () => { + const addDimension = await screen.findByLabelText('Add'); + await user.click(addDimension); + }, + }, +]; - it('should render a dimension filter', async () => { - let mockQuery = createMockQuery(); - const mockPanelData = createMockPanelData(); - const onQueryChange = jest.fn(); - const dimensionOptions = [ - { label: 'Test Dimension 1', value: 'TestDimension1' }, - { label: 'Test Dimension 2', value: 'TestDimension2' }, - ]; - const { rerender } = render( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const addDimension = await screen.findByText('Add new dimension'); - await user.click(addDimension); - mockQuery = appendDimensionFilter(mockQuery); - expect(onQueryChange).toHaveBeenCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }], - }, +for (const t of tests) { + describe(`Azure Monitor QueryEditor: ${t.label}`, () => { + const mockDatasource = createMockDatasource(); + + it('should render a dimension filter', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + const onQueryChange = jest.fn(); + const dimensionOptions = [ + { label: 'Test Dimension 1', value: 'TestDimension1' }, + { label: 'Test Dimension 2', value: 'TestDimension2' }, + ]; + const { rerender } = render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + + await t.addDimension(); + + mockQuery = appendDimensionFilter(mockQuery); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: '', operator: 'eq', filters: [] }], + }, + }); + rerender( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const dimensionSelect = await screen.findByText('Field'); + await selectOptionInTest(dimensionSelect, 'Test Dimension 1'); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], + }, + }); + expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument(); + expect(screen.queryByText('==')).toBeInTheDocument(); }); - rerender( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const dimensionSelect = await screen.findByText('Field'); - await selectOptionInTest(dimensionSelect, 'Test Dimension 1'); - expect(onQueryChange).toHaveBeenCalledWith({ - ...mockQuery, - azureMonitor: { + + it('correctly filters out dimensions when selected', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + mockQuery.azureMonitor = { ...mockQuery.azureMonitor, dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], - }, + }; + const onQueryChange = jest.fn(); + const dimensionOptions = [ + { label: 'Test Dimension 1', value: 'TestDimension1' }, + { label: 'Test Dimension 2', value: 'TestDimension2' }, + ]; + const { rerender } = render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + + await t.addDimension(); + + mockQuery = appendDimensionFilter(mockQuery); + rerender( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const dimensionSelect = await screen.findByText('Field'); + await user.click(dimensionSelect); + const options = await screen.findAllByLabelText('Select option'); + expect(options).toHaveLength(1); + expect(options[0]).toHaveTextContent('Test Dimension 2'); }); - expect(screen.queryByText('Test Dimension 1')).toBeInTheDocument(); - expect(screen.queryByText('==')).toBeInTheDocument(); - }); - it('correctly filters out dimensions when selected', async () => { - let mockQuery = createMockQuery(); - const mockPanelData = createMockPanelData(); - mockQuery.azureMonitor = { - ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], - }; - const onQueryChange = jest.fn(); - const dimensionOptions = [ - { label: 'Test Dimension 1', value: 'TestDimension1' }, - { label: 'Test Dimension 2', value: 'TestDimension2' }, - ]; - const { rerender } = render( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const addDimension = await screen.findByText('Add new dimension'); - await user.click(addDimension); - mockQuery = appendDimensionFilter(mockQuery); - rerender( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const dimensionSelect = await screen.findByText('Field'); - await user.click(dimensionSelect); - const options = await screen.findAllByLabelText('Select option'); - expect(options).toHaveLength(1); - expect(options[0]).toHaveTextContent('Test Dimension 2'); - }); - - it('correctly displays dimension labels', async () => { - let mockQuery = createMockQuery(); - const mockPanelData = createMockPanelData(); - mockQuery.azureMonitor = { - ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], - }; - - mockPanelData.series = [ - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel' }, - }, - ], - }, - ]; - const onQueryChange = jest.fn(); - const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; - render( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const labelSelect = await screen.findByText('Select value(s)'); - await user.click(labelSelect); - const options = await screen.findAllByLabelText('Select option'); - expect(options).toHaveLength(1); - expect(options[0]).toHaveTextContent('testlabel'); - }); - - it('correctly updates dimension labels', async () => { - let mockQuery = createMockQuery(); - const mockPanelData = createMockPanelData(); - mockQuery.azureMonitor = { - ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], - }; - - mockPanelData.series = [ - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel' }, - }, - ], - }, - ]; - const onQueryChange = jest.fn(); - const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; - const { rerender } = render( - {}} - dimensionOptions={dimensionOptions} - /> - ); - await screen.findByText('testlabel'); - const labelClear = await screen.findByLabelText('Remove testlabel'); - await user.click(labelClear); - mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []); - expect(onQueryChange).toHaveBeenCalledWith({ - ...mockQuery, - azureMonitor: { + it('correctly displays dimension labels', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + mockQuery.azureMonitor = { ...mockQuery.azureMonitor, dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], - }, - }); - mockPanelData.series = [ - ...mockPanelData.series, - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel2' }, - }, - ], - }, - ]; - rerender( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const labelSelect = await screen.getByLabelText('dimension-labels-select'); - await openMenu(labelSelect); - const options = await screen.findAllByLabelText('Select option'); - expect(options).toHaveLength(2); - expect(options[0]).toHaveTextContent('testlabel'); - expect(options[1]).toHaveTextContent('testlabel2'); - }); + }; - it('correctly selects multiple dimension labels', async () => { - let mockQuery = createMockQuery(); - const mockPanelData = createMockPanelData(); - mockPanelData.series = [ - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel' }, - }, - ], - }, - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel2' }, - }, - ], - }, - ]; - const onQueryChange = jest.fn(); - const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; - mockQuery = appendDimensionFilter(mockQuery, 'TestDimension1'); - const { rerender } = render( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const labelSelect = await screen.getByLabelText('dimension-labels-select'); - await user.click(labelSelect); - await openMenu(labelSelect); - await screen.getByText('testlabel'); - await screen.getByText('testlabel2'); - await selectOptionInTest(labelSelect, 'testlabel'); - mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel']); - expect(onQueryChange).toHaveBeenCalledWith({ - ...mockQuery, - azureMonitor: { + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + ]; + const onQueryChange = jest.fn(); + const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; + render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect = await screen.findByText('Select value(s)'); + await user.click(labelSelect); + const options = await screen.findAllByLabelText('Select option'); + expect(options).toHaveLength(1); + expect(options[0]).toHaveTextContent('testlabel'); + }); + + it('correctly updates dimension labels', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + mockQuery.azureMonitor = { ...mockQuery.azureMonitor, dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], - }, + }; + + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + ]; + const onQueryChange = jest.fn(); + const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; + const { rerender } = render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + await screen.findByText('testlabel'); + const labelClear = await screen.findByLabelText('Remove testlabel'); + await user.click(labelClear); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', []); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: [] }], + }, + }); + mockPanelData.series = [ + ...mockPanelData.series, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; + rerender( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect = screen.getByLabelText('dimension-labels-select'); + await openMenu(labelSelect); + const options = await screen.findAllByLabelText('Select option'); + expect(options).toHaveLength(2); + expect(options[0]).toHaveTextContent('testlabel'); + expect(options[1]).toHaveTextContent('testlabel2'); }); - mockPanelData.series = [ - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel' }, - }, - ], - }, - ]; - rerender( - {}} - dimensionOptions={dimensionOptions} - /> - ); - const labelSelect2 = await screen.getByLabelText('dimension-labels-select'); - await openMenu(labelSelect2); - const refreshedOptions = await screen.findAllByLabelText('Select options menu'); - expect(refreshedOptions).toHaveLength(1); - expect(refreshedOptions[0]).toHaveTextContent('testlabel2'); - await selectOptionInTest(labelSelect2, 'testlabel2'); - mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel', 'testlabel2']); - expect(onQueryChange).toHaveBeenCalledWith({ - ...mockQuery, - azureMonitor: { - ...mockQuery.azureMonitor, - dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel', 'testlabel2'] }], - }, + + it('correctly selects multiple dimension labels', async () => { + let mockQuery = createMockQuery(); + const mockPanelData = createMockPanelData(); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; + const onQueryChange = jest.fn(); + const dimensionOptions = [{ label: 'Test Dimension 1', value: 'TestDimension1' }]; + mockQuery = appendDimensionFilter(mockQuery, 'TestDimension1'); + const { rerender } = render( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect = screen.getByLabelText('dimension-labels-select'); + await user.click(labelSelect); + await openMenu(labelSelect); + screen.getByText('testlabel'); + screen.getByText('testlabel2'); + await selectOptionInTest(labelSelect, 'testlabel'); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel']); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel'] }], + }, + }); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + ]; + rerender( + {}} + dimensionOptions={dimensionOptions} + /> + ); + const labelSelect2 = screen.getByLabelText('dimension-labels-select'); + await openMenu(labelSelect2); + const refreshedOptions = await screen.findAllByLabelText('Select options menu'); + expect(refreshedOptions).toHaveLength(1); + expect(refreshedOptions[0]).toHaveTextContent('testlabel2'); + await selectOptionInTest(labelSelect2, 'testlabel2'); + mockQuery = setDimensionFilterValue(mockQuery, 0, 'filters', ['testlabel', 'testlabel2']); + expect(onQueryChange).toHaveBeenCalledWith({ + ...mockQuery, + azureMonitor: { + ...mockQuery.azureMonitor, + dimensionFilters: [{ dimension: 'TestDimension1', operator: 'eq', filters: ['testlabel', 'testlabel2'] }], + }, + }); + mockPanelData.series = [ + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel' }, + }, + ], + }, + { + ...mockPanelData.series[0], + fields: [ + { + ...mockPanelData.series[0].fields[0], + name: 'Test Dimension 1', + labels: { testdimension1: 'testlabel2' }, + }, + ], + }, + ]; }); - mockPanelData.series = [ - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel' }, - }, - ], - }, - { - ...mockPanelData.series[0], - fields: [ - { - ...mockPanelData.series[0].fields[0], - name: 'Test Dimension 1', - labels: { testdimension1: 'testlabel2' }, - }, - ], - }, - ]; }); -}); +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NewDimensionFields.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NewDimensionFields.tsx new file mode 100644 index 00000000000..8531fcd3457 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/NewDimensionFields.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useMemo, useState } from 'react'; + +import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data'; +import { AccessoryButton, EditorList } from '@grafana/experimental'; +import { Select, HorizontalGroup, MultiSelect } from '@grafana/ui'; + +import { AzureMetricDimension, AzureMonitorOption, AzureMonitorQuery, AzureQueryEditorFieldProps } from '../../types'; +import { Field } from '../Field'; + +import { setDimensionFilters } from './setQueryValue'; + +interface DimensionFieldsProps extends AzureQueryEditorFieldProps { + dimensionOptions: AzureMonitorOption[]; +} + +interface DimensionLabels { + [key: string]: Set; +} + +const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuery) => { + const [dimensionLabels, setDimensionLabels] = useState({}); + useEffect(() => { + let labelsObj: DimensionLabels = {}; + if (data?.series?.length) { + // Identify which series' in the dataframe are relevant to the current query + const series: DataFrame[] = data.series.flat().filter((series) => series.refId === query.refId); + const fields = series.flatMap((series) => series.fields); + // Retrieve labels for series fields + const labels = fields + .map((fields) => fields.labels) + .flat() + .filter((item): item is Labels => item !== null && item !== undefined); + for (const label of labels) { + // Labels only exist for series that have a dimension selected + for (const [dimension, value] of Object.entries(label)) { + if (labelsObj[dimension]) { + labelsObj[dimension].add(value); + } else { + labelsObj[dimension] = new Set([value]); + } + } + } + } + setDimensionLabels((prevLabels) => { + const newLabels: DimensionLabels = {}; + const currentLabels = Object.keys(labelsObj); + if (currentLabels.length === 0) { + return prevLabels; + } + for (const label of currentLabels) { + if (prevLabels[label] && labelsObj[label].size < prevLabels[label].size) { + newLabels[label] = prevLabels[label]; + } else { + newLabels[label] = labelsObj[label]; + } + } + return newLabels; + }); + }, [data?.series, query.refId]); + return dimensionLabels; +}; + +const NewDimensionFields: React.FC = ({ data, query, dimensionOptions, onQueryChange }) => { + const dimensionFilters = useMemo( + () => query.azureMonitor?.dimensionFilters ?? [], + [query.azureMonitor?.dimensionFilters] + ); + + const dimensionLabels = useDimensionLabels(data, query); + + const dimensionOperators: Array> = [ + { label: '==', value: 'eq' }, + { label: '!=', value: 'ne' }, + { label: 'starts with', value: 'sw' }, + ]; + + const validDimensionOptions = useMemo(() => { + // We filter out any dimensions that have already been used in a filter as the API doesn't support having multiple filters with the same dimension name. + // The Azure portal also doesn't support this feature so it makes sense for consistency. + let t = dimensionOptions; + if (dimensionFilters.length) { + t = dimensionOptions.filter( + (val) => !dimensionFilters.some((dimensionFilter) => dimensionFilter.dimension === val.value) + ); + } + return t; + }, [dimensionFilters, dimensionOptions]); + + const onFieldChange = ( + fieldName: Key, + item: Partial, + value: AzureMetricDimension[Key], + onChange: (item: Partial) => void + ) => { + item[fieldName] = value; + onChange(item); + }; + + const getValidDimensionOptions = (selectedDimension: string) => { + return validDimensionOptions.concat(dimensionOptions.filter((item) => item.value === selectedDimension)); + }; + + const getValidFilterOptions = (selectedFilter: string | undefined, dimension: string) => { + const dimensionFilters = Array.from(dimensionLabels[dimension.toLowerCase()] ?? []); + if (dimensionFilters.find((filter) => filter === selectedFilter)) { + return dimensionFilters.map((filter) => ({ value: filter, label: filter })); + } + return [...dimensionFilters, ...(selectedFilter && selectedFilter !== '*' ? [selectedFilter] : [])].map((item) => ({ + value: item, + label: item, + })); + }; + + const getValidMultiSelectOptions = (selectedFilters: string[] | undefined, dimension: string) => { + const labelOptions = getValidFilterOptions(undefined, dimension); + if (selectedFilters) { + for (const filter of selectedFilters) { + if (!labelOptions.find((label) => label.value === filter)) { + labelOptions.push({ value: filter, label: filter }); + } + } + } + return labelOptions; + }; + const getValidOperators = (selectedOperator: string) => { + if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) { + return dimensionOperators; + } + return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])]; + }; + + const changedFunc = (changed: Array>) => { + const properData: AzureMetricDimension[] = changed.map((x) => { + return { + dimension: x.dimension ?? '', + operator: x.operator ?? 'eq', + filters: x.filters ?? [], + }; + }); + onQueryChange(setDimensionFilters(query, properData)); + }; + + const renderFilters = ( + item: Partial, + onChange: (item: Partial) => void, + onDelete: () => void + ) => { + return ( + + onFieldChange('operator', item, e.value ?? '', onChange)} + allowCustomValue + /> + {item.operator === 'eq' || item.operator === 'ne' ? ( + + onFieldChange( + 'filters', + item, + e.map((x) => x.value ?? ''), + onChange + ) + } + aria-label={'dimension-labels-select'} + allowCustomValue + /> + ) : ( + // The API does not currently allow for multiple "starts with" clauses to be used. + > = (props) => { useEffect(() => { - if (!props.options.xAxis?.mode) { + if (!props.options.xBuckets?.mode) { const opts = getDefaultOptions(supplier); props.onChange({ ...opts, ...props.options }); console.log('geometry useEffect', opts); diff --git a/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx b/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx index c459dba078a..ca55a149b9e 100644 --- a/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx +++ b/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { SelectableValue, StandardEditorProps } from '@grafana/data'; -import { HorizontalGroup, Input, RadioButtonGroup } from '@grafana/ui'; +import { HorizontalGroup, Input, RadioButtonGroup, ScaleDistribution } from '@grafana/ui'; -import { HeatmapCalculationAxisConfig, HeatmapCalculationMode } from '../models.gen'; +import { HeatmapCalculationBucketConfig, HeatmapCalculationMode } from '../models.gen'; const modeOptions: Array> = [ { @@ -18,7 +18,20 @@ const modeOptions: Array> = [ }, ]; -export const AxisEditor: React.FC> = ({ +const logModeOptions: Array> = [ + { + label: 'Split', + value: HeatmapCalculationMode.Size, + description: 'Split the buckets based on size', + }, + { + label: 'Count', + value: HeatmapCalculationMode.Count, + description: 'Split the buckets based on count', + }, +]; + +export const AxisEditor: React.FC> = ({ value, onChange, item, @@ -27,7 +40,7 @@ export const AxisEditor: React.FC { onChange({ ...value, diff --git a/public/app/features/transformers/calculateHeatmap/editor/helper.ts b/public/app/features/transformers/calculateHeatmap/editor/helper.ts index af78c5113c6..4498baf9228 100644 --- a/public/app/features/transformers/calculateHeatmap/editor/helper.ts +++ b/public/app/features/transformers/calculateHeatmap/editor/helper.ts @@ -1,4 +1,6 @@ import { PanelOptionsEditorBuilder } from '@grafana/data'; +import { ScaleDistribution } from '@grafana/schema'; +import { ScaleDistributionEditor } from '@grafana/ui/src/options/builder'; import { HeatmapCalculationMode, HeatmapCalculationOptions } from '../models.gen'; @@ -11,9 +13,9 @@ export function addHeatmapCalculationOptions( category?: string[] ) { builder.addCustomEditor({ - id: 'xAxis', - path: `${prefix}xAxis`, - name: 'X Buckets', + id: 'xBuckets', + path: `${prefix}xBuckets`, + name: 'X Bucket', editor: AxisEditor, category, defaultValue: { @@ -22,13 +24,22 @@ export function addHeatmapCalculationOptions( }); builder.addCustomEditor({ - id: 'yAxis', - path: `${prefix}yAxis`, - name: 'Y Buckets', + id: 'yBuckets', + path: `${prefix}yBuckets`, + name: 'Y Bucket', editor: AxisEditor, category, defaultValue: { mode: HeatmapCalculationMode.Size, }, }); + + builder.addCustomEditor({ + id: 'yBuckets-scale', + path: `${prefix}yBuckets.scale`, + name: 'Y Bucket scale', + category, + editor: ScaleDistributionEditor, + defaultValue: { type: ScaleDistribution.Linear }, + }); } diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.test.ts b/public/app/features/transformers/calculateHeatmap/heatmap.test.ts index d2302dcf2d3..8eebf558ffa 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.test.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.test.ts @@ -1,7 +1,7 @@ import { FieldType } from '@grafana/data'; import { toDataFrame } from '@grafana/data/src/dataframe/processDataFrame'; -import { calculateHeatmapFromData } from './heatmap'; +import { bucketsToScanlines, calculateHeatmapFromData } from './heatmap'; import { HeatmapCalculationOptions } from './models.gen'; describe('Heatmap transformer', () => { @@ -13,12 +13,100 @@ describe('Heatmap transformer', () => { const data = toDataFrame({ fields: [ { name: 'time', type: FieldType.time, values: [1, 2, 3, 4] }, - { name: 'temp', type: FieldType.number, values: [1.1, 2.2, 3.3, 4.4] }, + { name: 'temp', type: FieldType.number, config: { unit: 'm2' }, values: [1.1, 2.2, 3.3, 4.4] }, ], }); const heatmap = calculateHeatmapFromData([data], options); + expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object {}, + "name": "xMin", + "type": "time", + }, + Object { + "config": Object { + "custom": Object { + "scaleDistribution": Object { + "type": "linear", + }, + }, + "unit": "m2", + }, + "name": "yMin", + "type": "number", + }, + Object { + "config": Object { + "unit": "short", + }, + "name": "Count", + "type": "number", + }, + ] + `); + }); - expect(heatmap).toBeDefined(); + it('convert heatmap buckets to scanlines', async () => { + const frame = toDataFrame({ + fields: [ + { name: 'time', type: FieldType.time, values: [1, 2, 3] }, + { name: 'A', type: FieldType.number, config: { unit: 'm2' }, values: [1.1, 1.2, 1.3] }, + { name: 'B', type: FieldType.number, config: { unit: 'm2' }, values: [2.1, 2.2, 2.3] }, + { name: 'C', type: FieldType.number, config: { unit: 'm2' }, values: [3.1, 3.2, 3.3] }, + ], + }); + + const heatmap = bucketsToScanlines({ frame, name: 'Speed' }); + expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object {}, + "name": "xMax", + "type": "time", + }, + Object { + "config": Object { + "unit": "short", + }, + "name": "y", + "type": "number", + }, + Object { + "config": Object { + "unit": "m2", + }, + "name": "Speed", + "type": "number", + }, + ] + `); + expect(heatmap.meta).toMatchInlineSnapshot(` + Object { + "custom": Object { + "yMatchWithLabel": undefined, + "yOrdinalDisplay": Array [ + "A", + "B", + "C", + ], + }, + "type": "heatmap-scanlines", + } + `); + expect(heatmap.fields[1].values.toArray()).toMatchInlineSnapshot(` + Array [ + 0, + 1, + 2, + 0, + 1, + 2, + 0, + 1, + 2, + ] + `); }); }); diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index ff79b393e7d..6ef7275c076 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -12,8 +12,9 @@ import { getFieldDisplayName, Field, } from '@grafana/data'; +import { ScaleDistribution } from '@grafana/schema'; -import { HeatmapCalculationMode, HeatmapCalculationOptions } from './models.gen'; +import { HeatmapBucketLayout, HeatmapCalculationMode, HeatmapCalculationOptions } from './models.gen'; import { niceLinearIncrs, niceTimeIncrs } from './utils'; export interface HeatmapTransformerOptions extends HeatmapCalculationOptions { @@ -48,21 +49,39 @@ export function sortAscStrInf(aName?: string | null, bName?: string | null) { return parseNumeric(aName) - parseNumeric(bName); } +export interface HeatmapScanlinesCustomMeta { + /** This provides the lookup values */ + yOrdinalDisplay: string[]; + yOrdinalLabel?: string[]; + yMatchWithLabel?: string; +} + +/** simple utility to get heatmap metadata from a frame */ +export function readHeatmapScanlinesCustomMeta(frame?: DataFrame): HeatmapScanlinesCustomMeta { + return (frame?.meta?.custom ?? {}) as HeatmapScanlinesCustomMeta; +} + +export interface BucketsOptions { + frame: DataFrame; + name?: string; + layout?: HeatmapBucketLayout; +} + /** Given existing buckets, create a values style frame */ // Assumes frames have already been sorted ASC and de-accumulated. -export function bucketsToScanlines(frame: DataFrame): DataFrame { +export function bucketsToScanlines(opts: BucketsOptions): DataFrame { // TODO: handle null-filling w/ fields[0].config.interval? - const xField = frame.fields[0]; + const xField = opts.frame.fields[0]; const xValues = xField.values.toArray(); - const yField = frame.fields[1]; + const yFields = opts.frame.fields.filter((f, idx) => f.type === FieldType.number && idx > 0); // similar to initBins() below - const len = xValues.length * (frame.fields.length - 1); + const len = xValues.length * yFields.length; const xs = new Array(len); const ys = new Array(len); const counts2 = new Array(len); - const counts = frame.fields.slice(1).map((field) => field.values.toArray().slice()); + const counts = yFields.map((field) => field.values.toArray().slice()); // transpose counts.forEach((bucketCounts, bi) => { @@ -71,7 +90,7 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame { } }); - const bucketBounds = Array.from({ length: frame.fields.length - 1 }, (v, i) => i); + const bucketBounds = Array.from({ length: yFields.length }, (v, i) => i); // fill flat/repeating array for (let i = 0, yi = 0, xi = 0; i < len; yi = ++i % bucketBounds.length) { @@ -84,10 +103,33 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame { xs[i] = xValues[xi]; } + // this name determines whether cells are drawn above, below, or centered on the values + let ordinalFieldName = yFields[0].labels?.le != null ? 'yMax' : 'y'; + switch (opts.layout) { + case HeatmapBucketLayout.le: + ordinalFieldName = 'yMax'; + break; + case HeatmapBucketLayout.ge: + ordinalFieldName = 'yMin'; + break; + case HeatmapBucketLayout.unknown: + ordinalFieldName = 'y'; + break; + } + + const custom: HeatmapScanlinesCustomMeta = { + yOrdinalDisplay: yFields.map((f) => getFieldDisplayName(f, opts.frame)), + yMatchWithLabel: Object.keys(yFields[0].labels ?? {})[0], + }; + if (custom.yMatchWithLabel) { + custom.yOrdinalLabel = yFields.map((f) => f.labels?.[custom.yMatchWithLabel!] ?? ''); + } return { length: xs.length, + refId: opts.frame.refId, meta: { type: DataFrameType.HeatmapScanlines, + custom, }, fields: [ { @@ -97,19 +139,19 @@ export function bucketsToScanlines(frame: DataFrame): DataFrame { config: xField.config, }, { - // this name determines whether cells are drawn above, below, or centered on the values - name: yField.labels?.le != null ? 'yMax' : 'y', + name: ordinalFieldName, type: FieldType.number, values: new ArrayVector(ys), - config: yField.config, + config: { + unit: 'short', // ordinal lookup + }, }, { - name: 'count', + name: opts.name?.length ? opts.name : 'Value', type: FieldType.number, values: new ArrayVector(counts2), - config: { - unit: 'short', - }, + config: yFields[0].config, + display: yFields[0].display, }, ], }; @@ -195,13 +237,24 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa throw 'no values found'; } + const xBucketsCfg = options.xBuckets ?? {}; + const yBucketsCfg = options.yBuckets ?? {}; + + if (xBucketsCfg.scale?.type === ScaleDistribution.Log) { + throw 'X axis only supports linear buckets'; + } + + const scaleDistribution = options.yBuckets?.scale ?? { + type: ScaleDistribution.Linear, + }; const heat2d = heatmap(xs, ys, { xSorted: true, xTime: xField.type === FieldType.time, - xMode: options.xAxis?.mode, - xSize: +(options.xAxis?.value ?? 0), - yMode: options.yAxis?.mode, - ySize: +(options.yAxis?.value ?? 0), + xMode: xBucketsCfg.mode, + xSize: xBucketsCfg.value ? +xBucketsCfg.value : undefined, + yMode: yBucketsCfg.mode, + ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined, + yLog: scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as any) : undefined, }); const frame = { @@ -221,10 +274,15 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa name: 'yMin', type: FieldType.number, values: new ArrayVector(heat2d.y), - config: yField.config, // keep units from the original source + config: { + ...yField.config, // keep units from the original source + custom: { + scaleDistribution, + }, + }, }, { - name: 'count', + name: 'Count', type: FieldType.number, values: new ArrayVector(heat2d.count), config: { @@ -294,6 +352,12 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { } } + let yExp = opts?.yLog; + + if (yExp && (minY <= 0 || maxY <= 0)) { + throw 'Log Y axes cannot have values <= 0'; + } + //let scaleX = opts?.xLog === 10 ? Math.log10 : opts?.xLog === 2 ? Math.log2 : (v: number) => v; //let scaleY = opts?.yLog === 10 ? Math.log10 : opts?.yLog === 2 ? Math.log2 : (v: number) => v; @@ -338,6 +402,12 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { let binX = opts?.xCeil ? (v: number) => incrRoundUp(v, xBinIncr) : (v: number) => incrRoundDn(v, xBinIncr); let binY = opts?.yCeil ? (v: number) => incrRoundUp(v, yBinIncr) : (v: number) => incrRoundDn(v, yBinIncr); + if (yExp) { + yBinIncr = 1 / (opts?.ySize ?? 1); // sub-divides log exponents + let yLog = yExp === 2 ? Math.log2 : Math.log10; + binY = opts?.yCeil ? (v: number) => incrRoundUp(yLog(v), yBinIncr) : (v: number) => incrRoundDn(yLog(v), yBinIncr); + } + let minXBin = binX(minX); let maxXBin = binX(maxX); let minYBin = binY(minY); @@ -346,7 +416,7 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { let xBinQty = Math.round((maxXBin - minXBin) / xBinIncr) + 1; let yBinQty = Math.round((maxYBin - minYBin) / yBinIncr) + 1; - let [xs2, ys2, counts] = initBins(xBinQty, yBinQty, minXBin, xBinIncr, minYBin, yBinIncr); + let [xs2, ys2, counts] = initBins(xBinQty, yBinQty, minXBin, xBinIncr, minYBin, yBinIncr, yExp); for (let i = 0; i < len; i++) { const xi = (binX(xs[i]) - minXBin) / xBinIncr; @@ -363,7 +433,7 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) { }; } -function initBins(xQty: number, yQty: number, xMin: number, xIncr: number, yMin: number, yIncr: number) { +function initBins(xQty: number, yQty: number, xMin: number, xIncr: number, yMin: number, yIncr: number, yExp?: number) { const len = xQty * yQty; const xs = new Array(len); const ys = new Array(len); @@ -371,7 +441,12 @@ function initBins(xQty: number, yQty: number, xMin: number, xIncr: number, yMin: for (let i = 0, yi = 0, x = xMin; i < len; yi = ++i % yQty) { counts[i] = 0; - ys[i] = yMin + yi * yIncr; + + if (yExp) { + ys[i] = yExp ** (yMin + yi * yIncr); + } else { + ys[i] = yMin + yi * yIncr; + } if (yi === 0 && i >= yQty) { x += xIncr; diff --git a/public/app/features/transformers/calculateHeatmap/models.gen.ts b/public/app/features/transformers/calculateHeatmap/models.gen.ts index eda086f883b..02566383d6a 100644 --- a/public/app/features/transformers/calculateHeatmap/models.gen.ts +++ b/public/app/features/transformers/calculateHeatmap/models.gen.ts @@ -1,18 +1,24 @@ -import { DataFrameType } from '@grafana/data'; +import { ScaleDistributionConfig } from '@grafana/schema'; export enum HeatmapCalculationMode { - Size = 'size', + Size = 'size', // When exponential, this is "splitFactor" Count = 'count', } -export interface HeatmapCalculationAxisConfig { +export const enum HeatmapBucketLayout { + le = 'le', + ge = 'ge', + unknown = 'unknown', // unknown + auto = 'auto', // becomes unknown +} + +export interface HeatmapCalculationBucketConfig { mode?: HeatmapCalculationMode; - value?: string; // number or interval string ie 10s + value?: string; // number or interval string ie 10s, or log "split" divisor + scale?: ScaleDistributionConfig; } export interface HeatmapCalculationOptions { - xAxis?: HeatmapCalculationAxisConfig; - yAxis?: HeatmapCalculationAxisConfig; - xAxisField?: string; // name of the x field - encoding?: DataFrameType.HeatmapBuckets | DataFrameType.HeatmapScanlines; + xBuckets?: HeatmapCalculationBucketConfig; + yBuckets?: HeatmapCalculationBucketConfig; } diff --git a/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx b/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx index 3d9e1a1e624..3befa041e46 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapHoverView.tsx @@ -3,10 +3,12 @@ import React, { useEffect, useRef } from 'react'; import { DataFrameType, Field, FieldType, formattedValueToString, getFieldDisplayName, LinkModel } from '@grafana/data'; import { LinkButton, VerticalGroup } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; +import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen'; import { DataHoverView } from '../geomap/components/DataHoverView'; -import { BucketLayout, HeatmapData } from './fields'; +import { HeatmapData } from './fields'; import { HeatmapHoverEvent } from './utils'; type Props = { @@ -44,26 +46,15 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { const yVals = yField?.values.toArray(); const countVals = countField?.values.toArray(); - let yDispSrc, yDisp; - // labeled buckets - if (data.yAxisValues) { - yDispSrc = data.yAxisValues; - yDisp = (v: any) => v; - } else { - yDispSrc = yVals; - yDisp = (v: any) => { - if (yField?.display) { - return formattedValueToString(yField.display(v)); - } - return `${v}`; - }; - } + const meta = readHeatmapScanlinesCustomMeta(data.heatmap); + const yDispSrc = meta.yOrdinalDisplay ?? yVals; + const yDisp = yField?.display ? (v: any) => formattedValueToString(yField.display!(v)) : (v: any) => `${v}`; const yValueIdx = index % data.yBucketCount! ?? 0; - const yMinIdx = data.yLayout === BucketLayout.le ? yValueIdx - 1 : yValueIdx; - const yMaxIdx = data.yLayout === BucketLayout.le ? yValueIdx : yValueIdx + 1; + const yMinIdx = data.yLayout === HeatmapBucketLayout.le ? yValueIdx - 1 : yValueIdx; + const yMaxIdx = data.yLayout === HeatmapBucketLayout.le ? yValueIdx : yValueIdx + 1; const yBucketMin = yDispSrc?.[yMinIdx]; const yBucketMax = yDispSrc?.[yMaxIdx]; @@ -171,6 +162,18 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { ); } + const renderYBuckets = () => { + switch (data.yLayout) { + case HeatmapBucketLayout.unknown: + return
{yDisp(yBucketMin)}
; + } + return ( +
+ Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)} +
+ ); + }; + return ( <>
@@ -186,15 +189,9 @@ const HeatmapHoverCell = ({ data, hover, showHistogram }: Props) => { /> )}
- {data.yLayout === BucketLayout.unknown ? ( -
{yDisp(yBucketMin)}
- ) : ( -
- Bucket: {yDisp(yBucketMin)} - {yDisp(yBucketMax)} -
- )} + {renderYBuckets()}
- {getFieldDisplayName(countField!, data.heatmap)}: {count} + {getFieldDisplayName(countField!, data.heatmap)}: {data.display!(count)}
{links.length > 0 && ( diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 01ce263be9b..7516c203662 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -3,9 +3,19 @@ import React, { useCallback, useMemo, useRef, useState } from 'react'; import { DataFrameType, GrafanaTheme2, PanelProps, reduceField, ReducerID, TimeRange } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; -import { Portal, UPlotChart, useStyles2, useTheme2, VizLayout, VizTooltipContainer } from '@grafana/ui'; +import { ScaleDistributionConfig } from '@grafana/schema'; +import { + Portal, + ScaleDistribution, + UPlotChart, + useStyles2, + useTheme2, + VizLayout, + VizTooltipContainer, +} from '@grafana/ui'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; +import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; import { HeatmapHoverView } from './HeatmapHoverView'; import { prepareHeatmapData } from './fields'; @@ -46,24 +56,25 @@ export const HeatmapPanel: React.FC = ({ let exemplarsXFacet: number[] = []; // "Time" field let exemplarsyFacet: number[] = []; - if (info.exemplars && info.matchByLabel) { + const meta = readHeatmapScanlinesCustomMeta(info.heatmap); + if (info.exemplars && meta.yMatchWithLabel) { exemplarsXFacet = info.exemplars?.fields[0].values.toArray(); // ordinal/labeled heatmap-buckets? - const hasLabeledY = info.yLabelValues != null; + const hasLabeledY = meta.yOrdinalDisplay != null; if (hasLabeledY) { let matchExemplarsBy = info.exemplars?.fields - .find((field) => field.name === info.matchByLabel)! + .find((field) => field.name === meta.yMatchWithLabel)! .values.toArray(); - exemplarsyFacet = matchExemplarsBy.map((label) => info.yLabelValues?.indexOf(label)) as number[]; + exemplarsyFacet = matchExemplarsBy.map((label) => meta.yOrdinalLabel?.indexOf(label)) as number[]; } else { exemplarsyFacet = info.exemplars?.fields[1].values.toArray() as number[]; // "Value" field } } return [null, info.heatmap?.fields.map((f) => f.values.toArray()), [exemplarsXFacet, exemplarsyFacet]]; - }, [info.heatmap, info.exemplars, info.yLabelValues, info.matchByLabel]); + }, [info.heatmap, info.exemplars]); const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]); @@ -97,6 +108,8 @@ export const HeatmapPanel: React.FC = ({ dataRef.current = info; const builder = useMemo(() => { + const scaleConfig = dataRef.current?.heatmap?.fields[1].config?.custom + ?.scaleDistribution as ScaleDistributionConfig; return prepConfig({ dataRef, theme, @@ -113,9 +126,10 @@ export const HeatmapPanel: React.FC = ({ getTimeRange: () => timeRangeRef.current, palette, cellGap: options.cellGap, - hideThreshold: options.hideThreshold, + hideThreshold: options.filterValues?.min, // eventually a better range exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)', - yAxisReverse: options.yAxisReverse, + yAxisConfig: options.yAxis, + ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [options, data.structureRev]); diff --git a/public/app/plugins/panel/heatmap-new/fields.ts b/public/app/plugins/panel/heatmap-new/fields.ts index 9e5095fd216..bbbad5f1567 100644 --- a/public/app/plugins/panel/heatmap-new/fields.ts +++ b/public/app/plugins/panel/heatmap-new/fields.ts @@ -1,42 +1,31 @@ import { DataFrame, DataFrameType, - FieldType, formattedValueToString, getDisplayProcessor, - getFieldDisplayName, getValueFormat, GrafanaTheme2, outerJoinDataFrames, PanelData, } from '@grafana/data'; import { calculateHeatmapFromData, bucketsToScanlines } from 'app/features/transformers/calculateHeatmap/heatmap'; +import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen'; -import { HeatmapMode, PanelOptions } from './models.gen'; - -export const enum BucketLayout { - le = 'le', - ge = 'ge', - unknown = 'unknown', // unknown -} +import { PanelOptions } from './models.gen'; export interface HeatmapData { heatmap?: DataFrame; // data we will render exemplars?: DataFrame; // optionally linked exemplars exemplarColor?: string; - yAxisValues?: Array; - yLabelValues?: string[]; // matched ordinally to yAxisValues - matchByLabel?: string; // e.g. le, pod, etc. - xBucketSize?: number; yBucketSize?: number; xBucketCount?: number; yBucketCount?: number; - xLayout?: BucketLayout; - yLayout?: BucketLayout; + xLayout?: HeatmapBucketLayout; + yLayout?: HeatmapBucketLayout; // Print a heatmap cell value display?: (v: number) => string; @@ -51,13 +40,11 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme return {}; } - const { mode } = options; - const exemplars = data.annotations?.find((f) => f.name === 'exemplar'); - if (mode === HeatmapMode.Calculate) { + if (options.calculate) { // TODO, check for error etc - return getHeatmapData(calculateHeatmapFromData(frames, options.calculate ?? {}), exemplars, theme); + return getHeatmapData(calculateHeatmapFromData(frames, options.calculation ?? {}), exemplars, theme); } // Check for known heatmap types @@ -88,21 +75,7 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme } } - // Some datasources return values in ascending order and require math to know the deltas - if (mode === HeatmapMode.Accumulated) { - console.log('TODO, deaccumulate the values'); - } - - const yFields = bucketHeatmap.fields.filter((f) => f.type === FieldType.number); - const matchByLabel = Object.keys(yFields[0].labels ?? {})[0]; - - const scanlinesFrame = bucketsToScanlines(bucketHeatmap); - return { - matchByLabel, - yLabelValues: matchByLabel ? yFields.map((f) => f.labels?.[matchByLabel] ?? '') : undefined, - yAxisValues: yFields.map((f) => getFieldDisplayName(f, bucketHeatmap, frames)), - ...getHeatmapData(scanlinesFrame, exemplars, theme), - }; + return getHeatmapData(bucketsToScanlines({ ...options.bucket, frame: bucketHeatmap }), exemplars, theme); } const getSparseHeatmapData = ( @@ -173,8 +146,18 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them yBucketCount: yBinQty, // TODO: improve heuristic - xLayout: xName === 'xMax' ? BucketLayout.le : xName === 'xMin' ? BucketLayout.ge : BucketLayout.unknown, - yLayout: yName === 'yMax' ? BucketLayout.le : yName === 'yMin' ? BucketLayout.ge : BucketLayout.unknown, + xLayout: + xName === 'xMax' + ? HeatmapBucketLayout.le + : xName === 'xMin' + ? HeatmapBucketLayout.ge + : HeatmapBucketLayout.unknown, + yLayout: + yName === 'yMax' + ? HeatmapBucketLayout.le + : yName === 'yMin' + ? HeatmapBucketLayout.ge + : HeatmapBucketLayout.unknown, display: (v) => formattedValueToString(disp(v)), }; diff --git a/public/app/plugins/panel/heatmap-new/migrations.test.ts b/public/app/plugins/panel/heatmap-new/migrations.test.ts index 8d8c5bf8018..86ff840d106 100644 --- a/public/app/plugins/panel/heatmap-new/migrations.test.ts +++ b/public/app/plugins/panel/heatmap-new/migrations.test.ts @@ -25,14 +25,22 @@ describe('Heatmap Migrations', () => { "overrides": Array [], }, "options": Object { - "calculate": Object { - "xAxis": Object { + "bucket": Object { + "layout": "auto", + }, + "calculate": true, + "calculation": Object { + "xBuckets": Object { "mode": "count", "value": "100", }, - "yAxis": Object { + "yBuckets": Object { "mode": "count", - "value": "20", + "scale": Object { + "log": 2, + "type": "log", + }, + "value": "3", }, }, "cellGap": 2, @@ -40,6 +48,8 @@ describe('Heatmap Migrations', () => { "color": Object { "exponent": 0.5, "fill": "dark-orange", + "max": 100, + "min": 5, "mode": "scheme", "scale": "exponential", "scheme": "BuGn", @@ -48,17 +58,22 @@ describe('Heatmap Migrations', () => { "exemplars": Object { "color": "rgba(255,0,255,0.7)", }, + "filterValues": Object { + "min": 1e-9, + }, "legend": Object { "show": true, }, - "mode": "calculate", "showValue": "never", "tooltip": Object { "show": true, "yHistogram": true, }, - "yAxisLabels": "auto", - "yAxisReverse": false, + "yAxis": Object { + "axisPlacement": "left", + "axisWidth": 400, + "reverse": false, + }, }, } `); @@ -103,8 +118,8 @@ const oldHeatmap = { colorScale: 'sqrt', exponent: 0.5, colorScheme: 'interpolateBuGn', - min: null, - max: null, + min: 5, + max: 100, }, legend: { show: true, @@ -119,10 +134,11 @@ const oldHeatmap = { show: true, format: 'short', decimals: null, - logBase: 1, - splitFactor: null, + logBase: 2, + splitFactor: 3, min: null, max: null, + width: '400', }, xBucketSize: null, xBucketNumber: 100, diff --git a/public/app/plugins/panel/heatmap-new/migrations.ts b/public/app/plugins/panel/heatmap-new/migrations.ts index 7864f8ff4ae..d3d96a7945c 100644 --- a/public/app/plugins/panel/heatmap-new/migrations.ts +++ b/public/app/plugins/panel/heatmap-new/migrations.ts @@ -1,11 +1,12 @@ import { FieldConfigSource, PanelModel, PanelTypeChangedHandler } from '@grafana/data'; -import { VisibilityMode } from '@grafana/schema'; +import { AxisPlacement, ScaleDistribution, VisibilityMode } from '@grafana/schema'; import { + HeatmapBucketLayout, HeatmapCalculationMode, HeatmapCalculationOptions, } from 'app/features/transformers/calculateHeatmap/models.gen'; -import { HeatmapMode, PanelOptions, defaultPanelOptions, HeatmapColorMode } from './models.gen'; +import { PanelOptions, defaultPanelOptions, HeatmapColorMode } from './models.gen'; import { colorSchemes } from './palettes'; /** @@ -29,36 +30,55 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS overrides: [], }; - const mode = angular.dataFormat === 'tsbuckets' ? HeatmapMode.Aggregated : HeatmapMode.Calculate; - const calculate: HeatmapCalculationOptions = { - ...defaultPanelOptions.calculate, + const calculate = angular.dataFormat === 'tsbuckets' ? false : true; + const calculation: HeatmapCalculationOptions = { + ...defaultPanelOptions.calculation, }; - if (mode === HeatmapMode.Calculate) { + const oldYAxis = { logBase: 1, ...angular.yAxis }; + + if (calculate) { if (angular.xBucketSize) { - calculate.xAxis = { mode: HeatmapCalculationMode.Size, value: `${angular.xBucketSize}` }; + calculation.xBuckets = { mode: HeatmapCalculationMode.Size, value: `${angular.xBucketSize}` }; } else if (angular.xBucketNumber) { - calculate.xAxis = { mode: HeatmapCalculationMode.Count, value: `${angular.xBucketNumber}` }; + calculation.xBuckets = { mode: HeatmapCalculationMode.Count, value: `${angular.xBucketNumber}` }; } if (angular.yBucketSize) { - calculate.yAxis = { mode: HeatmapCalculationMode.Size, value: `${angular.yBucketSize}` }; + calculation.yBuckets = { mode: HeatmapCalculationMode.Size, value: `${angular.yBucketSize}` }; } else if (angular.xBucketNumber) { - calculate.yAxis = { mode: HeatmapCalculationMode.Count, value: `${angular.yBucketNumber}` }; + calculation.yBuckets = { mode: HeatmapCalculationMode.Count, value: `${angular.yBucketNumber}` }; + } + + if (oldYAxis.logBase > 1) { + calculation.yBuckets = { + mode: HeatmapCalculationMode.Count, + value: +oldYAxis.splitFactor > 0 ? `${oldYAxis.splitFactor}` : undefined, + scale: { + type: ScaleDistribution.Log, + log: oldYAxis.logBase, + }, + }; } } const options: PanelOptions = { - mode, calculate, + calculation, color: { ...defaultPanelOptions.color, steps: 128, // best match with existing colors }, cellGap: asNumber(angular.cards?.cardPadding), cellSize: asNumber(angular.cards?.cardRound), - yAxisLabels: angular.yBucketBound, - yAxisReverse: angular.reverseYBuckets, + yAxis: { + axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left, + reverse: Boolean(angular.reverseYBuckets), + axisWidth: oldYAxis.width ? +oldYAxis.width : undefined, + }, + bucket: { + layout: getHeatmapBucketLayout(angular.yBucketBound), + }, legend: { show: Boolean(angular.legend.show), }, @@ -72,6 +92,10 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS }, }; + if (angular.hideZeroBuckets) { + options.filterValues = { ...defaultPanelOptions.filterValues }; // min: 1e-9 + } + // Migrate color options const color = angular.color; switch (color?.mode) { @@ -92,10 +116,24 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS break; } } + options.color.min = color.min; + options.color.max = color.max; return { fieldConfig, options }; } +function getHeatmapBucketLayout(v?: string): HeatmapBucketLayout { + switch (v) { + case 'upper': + return HeatmapBucketLayout.ge; + case 'lower': + return HeatmapBucketLayout.le; + case 'middle': + return HeatmapBucketLayout.unknown; + } + return HeatmapBucketLayout.auto; +} + function asNumber(v: any): number | undefined { const num = +v; return isNaN(num) ? undefined : num; diff --git a/public/app/plugins/panel/heatmap-new/models.gen.ts b/public/app/plugins/panel/heatmap-new/models.gen.ts index 14f886a1196..c7450496fae 100644 --- a/public/app/plugins/panel/heatmap-new/models.gen.ts +++ b/public/app/plugins/panel/heatmap-new/models.gen.ts @@ -3,17 +3,11 @@ // It is currenty hand written but will serve as the target for cuetsy //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import { HideableFieldConfig, VisibilityMode } from '@grafana/schema'; -import { HeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/models.gen'; +import { AxisConfig, AxisPlacement, HideableFieldConfig, ScaleDistributionConfig, VisibilityMode } from '@grafana/schema'; +import { HeatmapBucketLayout, HeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/models.gen'; export const modelVersion = Object.freeze([1, 0]); -export enum HeatmapMode { - Aggregated = 'agg', - Calculate = 'calculate', - Accumulated = 'acc', // accumulated -} - export enum HeatmapColorMode { Opacity = 'opacity', Scheme = 'scheme', @@ -36,6 +30,16 @@ export interface HeatmapColorOptions { min?: number; max?: number; } +export interface YAxisConfig extends AxisConfig { + unit?: string; + reverse?: boolean; + decimals?: number; +} + +export interface FilterValueRange { + min?: number; + max?: number; +} export interface HeatmapTooltip { show: boolean; @@ -49,19 +53,24 @@ export interface ExemplarConfig { color: string; } +export interface BucketOptions { + name?: string; + layout?: HeatmapBucketLayout; +} + export interface PanelOptions { - mode: HeatmapMode; + calculate?: boolean; + calculation?: HeatmapCalculationOptions; color: HeatmapColorOptions; - calculate?: HeatmapCalculationOptions; + filterValues?: FilterValueRange; // was hideZeroBuckets + bucket?: BucketOptions; showValue: VisibilityMode; cellGap?: number; // was cardPadding cellSize?: number; // was cardRadius - hideThreshold?: number; // was hideZeroBuckets - yAxisLabels?: string; - yAxisReverse?: boolean; + yAxis: YAxisConfig; legend: HeatmapLegend; tooltip: HeatmapTooltip; @@ -69,7 +78,7 @@ export interface PanelOptions { } export const defaultPanelOptions: PanelOptions = { - mode: HeatmapMode.Aggregated, + calculate: false, color: { mode: HeatmapColorMode.Scheme, scheme: 'Oranges', @@ -78,6 +87,12 @@ export const defaultPanelOptions: PanelOptions = { exponent: 0.5, steps: 64, }, + bucket: { + layout: HeatmapBucketLayout.auto, + }, + yAxis: { + axisPlacement: AxisPlacement.Left, + }, showValue: VisibilityMode.Auto, tooltip: { show: true, @@ -89,13 +104,14 @@ export const defaultPanelOptions: PanelOptions = { exemplars: { color: 'rgba(255,0,255,0.7)', }, + filterValues: { + min: 1e-9, + }, cellGap: 1, }; export interface PanelFieldConfig extends HideableFieldConfig { - // TODO points vs lines etc + scaleDistribution?: ScaleDistributionConfig; } -export const defaultPanelFieldConfig: PanelFieldConfig = { - // default to points? -}; +export const defaultPanelFieldConfig: PanelFieldConfig = {}; diff --git a/public/app/plugins/panel/heatmap-new/module.tsx b/public/app/plugins/panel/heatmap-new/module.tsx index d037f070176..0b583cf14a1 100644 --- a/public/app/plugins/panel/heatmap-new/module.tsx +++ b/public/app/plugins/panel/heatmap-new/module.tsx @@ -1,20 +1,45 @@ import React from 'react'; -import { FieldConfigProperty, PanelPlugin } from '@grafana/data'; +import { FieldConfigProperty, FieldType, identityOverrideProcessor, PanelPlugin } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { GraphFieldConfig } from '@grafana/schema'; +import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema'; +import { addHideFrom, ScaleDistributionEditor } from '@grafana/ui/src/options/builder'; import { ColorScale } from 'app/core/components/ColorScale/ColorScale'; import { addHeatmapCalculationOptions } from 'app/features/transformers/calculateHeatmap/editor/helper'; +import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen'; import { HeatmapPanel } from './HeatmapPanel'; import { heatmapChangedHandler, heatmapMigrationHandler } from './migrations'; -import { PanelOptions, defaultPanelOptions, HeatmapMode, HeatmapColorMode, HeatmapColorScale } from './models.gen'; +import { PanelOptions, defaultPanelOptions, HeatmapColorMode, HeatmapColorScale } from './models.gen'; import { colorSchemes, quantizeScheme } from './palettes'; import { HeatmapSuggestionsSupplier } from './suggestions'; export const plugin = new PanelPlugin(HeatmapPanel) .useFieldConfig({ - disableStandardOptions: [FieldConfigProperty.Color, FieldConfigProperty.Thresholds], + // This keeps: unit, decimals, displayName + disableStandardOptions: [ + FieldConfigProperty.Color, + FieldConfigProperty.Thresholds, + FieldConfigProperty.Min, + FieldConfigProperty.Max, + FieldConfigProperty.Mappings, + FieldConfigProperty.NoValue, + ], + useCustomConfig: (builder) => { + builder.addCustomEditor({ + id: 'scaleDistribution', + path: 'scaleDistribution', + name: 'Y axis scale', + category: ['Heatmap'], + editor: ScaleDistributionEditor as any, + override: ScaleDistributionEditor as any, + defaultValue: { type: ScaleDistribution.Linear }, + shouldApply: (f) => f.type === FieldType.number, + process: identityOverrideProcessor, + hideFromDefaults: true, + }); + addHideFrom(builder); // for tooltip etc + }, }) .setPanelChangeHandler(heatmapChangedHandler) .setMigrationHandler(heatmapMigrationHandler) @@ -24,23 +49,88 @@ export const plugin = new PanelPlugin(HeatmapPan let category = ['Heatmap']; builder.addRadio({ - path: 'mode', - name: 'Data', - defaultValue: defaultPanelOptions.mode, + path: 'calculate', + name: 'Calculate from data', + defaultValue: defaultPanelOptions.calculate, category, settings: { options: [ - { label: 'Aggregated', value: HeatmapMode.Aggregated }, - { label: 'Calculate', value: HeatmapMode.Calculate }, - // { label: 'Accumulated', value: HeatmapMode.Accumulated, description: 'The query response values are accumulated' }, + { label: 'Yes', value: true }, + { label: 'No', value: false }, ], }, }); - if (opts.mode === HeatmapMode.Calculate) { - addHeatmapCalculationOptions('calculate.', builder, opts.calculate, category); + if (opts.calculate) { + addHeatmapCalculationOptions('calculation.', builder, opts.calculation, category); + } else { + builder.addTextInput({ + path: 'bucket.name', + name: 'Cell value name', + defaultValue: defaultPanelOptions.bucket?.name, + settings: { + placeholder: 'Value', + }, + category, + }); + builder.addRadio({ + path: 'bucket.layout', + name: 'Layout', + defaultValue: defaultPanelOptions.bucket?.layout ?? HeatmapBucketLayout.auto, + category, + settings: { + options: [ + { label: 'Auto', value: HeatmapBucketLayout.auto }, + { label: 'Middle', value: HeatmapBucketLayout.unknown }, + { label: 'Lower (LE)', value: HeatmapBucketLayout.le }, + { label: 'Upper (GE)', value: HeatmapBucketLayout.ge }, + ], + }, + }); } + category = ['Y Axis']; + builder.addRadio({ + path: 'yAxis.axisPlacement', + name: 'Placement', + defaultValue: defaultPanelOptions.yAxis.axisPlacement ?? AxisPlacement.Left, + category, + settings: { + options: [ + { label: 'Left', value: AxisPlacement.Left }, + { label: 'Right', value: AxisPlacement.Right }, + { label: 'Hidden', value: AxisPlacement.Hidden }, + ], + }, + }); + + builder + .addNumberInput({ + path: 'yAxis.axisWidth', + name: 'Axis width', + defaultValue: defaultPanelOptions.yAxis.axisWidth, + settings: { + placeholder: 'Auto', + min: 5, // smaller should just be hidden + }, + category, + }) + .addTextInput({ + path: 'yAxis.axisLabel', + name: 'Axis label', + defaultValue: defaultPanelOptions.yAxis.axisLabel, + settings: { + placeholder: 'Auto', + }, + category, + }) + .addBooleanSwitch({ + path: 'yAxis.reverse', + name: 'Reverse', + defaultValue: defaultPanelOptions.yAxis.reverse === true, + category, + }); + category = ['Colors']; builder.addRadio({ @@ -152,9 +242,9 @@ export const plugin = new PanelPlugin(HeatmapPan // }, // }) .addNumberInput({ - path: 'hideThreshold', + path: 'filterValues.min', name: 'Hide cell counts <=', - defaultValue: 1e-9, + defaultValue: defaultPanelOptions.filterValues?.min, category, }) .addSliderInput({ @@ -166,37 +256,17 @@ export const plugin = new PanelPlugin(HeatmapPan min: 0, max: 25, }, - }) - // .addSliderInput({ - // name: 'Cell radius', - // path: 'cellRadius', - // defaultValue: defaultPanelOptions.cellRadius, - // category, - // settings: { - // min: 0, - // max: 100, - // }, - // }) - // .addRadio({ - // path: 'yAxisLabels', - // name: 'Axis labels', - // defaultValue: 'auto', - // category, - // settings: { - // options: [ - // { value: 'auto', label: 'Auto' }, - // { value: 'middle', label: 'Middle' }, - // { value: 'bottom', label: 'Bottom' }, - // { value: 'top', label: 'Top' }, - // ], - // }, - // }) - .addBooleanSwitch({ - path: 'yAxisReverse', - name: 'Reverse buckets', - defaultValue: defaultPanelOptions.yAxisReverse === true, - category, }); + // .addSliderInput({ + // name: 'Cell radius', + // path: 'cellRadius', + // defaultValue: defaultPanelOptions.cellRadius, + // category, + // settings: { + // min: 0, + // max: 100, + // }, + // }) category = ['Tooltip']; diff --git a/public/app/plugins/panel/heatmap-new/utils.ts b/public/app/plugins/panel/heatmap-new/utils.ts index 15b4cb6e5a1..d75c6329720 100644 --- a/public/app/plugins/panel/heatmap-new/utils.ts +++ b/public/app/plugins/panel/heatmap-new/utils.ts @@ -1,13 +1,16 @@ import { MutableRefObject, RefObject } from 'react'; import uPlot from 'uplot'; -import { DataFrameType, GrafanaTheme2, TimeRange } from '@grafana/data'; +import { DataFrameType, GrafanaTheme2, incrRoundDn, incrRoundUp, TimeRange } from '@grafana/data'; import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema'; import { UPlotConfigBuilder } from '@grafana/ui'; +import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; +import { HeatmapBucketLayout } from 'app/features/transformers/calculateHeatmap/models.gen'; import { pointWithin, Quadtree, Rect } from '../barchart/quadtree'; -import { BucketLayout, HeatmapData } from './fields'; +import { HeatmapData } from './fields'; +import { PanelFieldConfig, YAxisConfig } from './models.gen'; interface PathbuilderOpts { each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void; @@ -15,6 +18,7 @@ interface PathbuilderOpts { hideThreshold?: number; xAlign?: -1 | 0 | 1; yAlign?: -1 | 0 | 1; + ySizeDivisor?: number; disp: { fill: { values: (u: uPlot, seriesIndex: number) => number[]; @@ -52,7 +56,8 @@ interface PrepConfigOpts { exemplarColor: string; cellGap?: number | null; // in css pixels hideThreshold?: number; - yAxisReverse?: boolean; + yAxisConfig: YAxisConfig; + ySizeDivisor?: number; } export function prepConfig(opts: PrepConfigOpts) { @@ -68,7 +73,8 @@ export function prepConfig(opts: PrepConfigOpts) { palette, cellGap, hideThreshold, - yAxisReverse, + yAxisConfig, + ySizeDivisor, } = opts; const pxRatio = devicePixelRatio; @@ -205,7 +211,10 @@ export function prepConfig(opts: PrepConfigOpts) { theme: theme, }); - const shouldUseLogScale = heatmapType === DataFrameType.HeatmapSparse; + const yFieldConfig = dataRef.current?.heatmap?.fields[1]?.config?.custom as PanelFieldConfig | undefined; + const yScale = yFieldConfig?.scaleDistribution ?? { type: ScaleDistribution.Linear }; + const yAxisReverse = Boolean(yAxisConfig.reverse); + const shouldUseLogScale = yScale.type !== ScaleDistribution.Linear || heatmapType === DataFrameType.HeatmapSparse; builder.addScale({ scaleKey: 'y', @@ -215,38 +224,84 @@ export function prepConfig(opts: PrepConfigOpts) { direction: yAxisReverse ? ScaleDirection.Down : ScaleDirection.Up, // should be tweakable manually distribution: shouldUseLogScale ? ScaleDistribution.Log : ScaleDistribution.Linear, - log: 2, - range: shouldUseLogScale - ? undefined - : (u, dataMin, dataMax) => { - let bucketSize = dataRef.current?.yBucketSize; + log: yScale.log ?? 2, + range: + // sparse already accounts for le/ge by explicit yMin & yMax cell bounds, so use default log ranging + heatmapType === DataFrameType.HeatmapSparse + ? undefined + : // dense and ordinal only have one of yMin|yMax|y, so expand range by one cell in the direction of le/ge/unknown + (u, dataMin, dataMax) => { + // logarithmic expansion + if (shouldUseLogScale) { + let yExp = u.scales['y'].log!; - if (bucketSize === 0) { - bucketSize = 1; - } + let minExpanded = false; + let maxExpanded = false; - if (bucketSize) { - if (dataRef.current?.yLayout === BucketLayout.le) { - dataMin -= bucketSize!; - } else if (dataRef.current?.yLayout === BucketLayout.ge) { - dataMax += bucketSize!; - } else { - dataMin -= bucketSize! / 2; - dataMax += bucketSize! / 2; + if (ySizeDivisor !== 1) { + let log = yExp === 2 ? Math.log2 : Math.log10; + + let minLog = log(dataMin); + let maxLog = log(dataMax); + + if (!Number.isInteger(minLog)) { + dataMin = yExp ** incrRoundDn(minLog, 1); + minExpanded = true; + } + + if (!Number.isInteger(maxLog)) { + dataMax = yExp ** incrRoundUp(maxLog, 1); + maxExpanded = true; + } + } + + if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { + if (!minExpanded) { + dataMin /= yExp; + } + } else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) { + if (!maxExpanded) { + dataMax *= yExp; + } + } else { + dataMin /= yExp / 2; + dataMax *= yExp / 2; + } } - } else { - // how to expand scale range if inferred non-regular or log buckets? - } + // linear expansion + else { + let bucketSize = dataRef.current?.yBucketSize; - return [dataMin, dataMax]; - }, + if (bucketSize === 0) { + bucketSize = 1; + } + + if (bucketSize) { + if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { + dataMin -= bucketSize!; + } else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) { + dataMax += bucketSize!; + } else { + dataMin -= bucketSize! / 2; + dataMax += bucketSize! / 2; + } + } else { + // how to expand scale range if inferred non-regular or log buckets? + } + } + + return [dataMin, dataMax]; + }, }); - const hasLabeledY = dataRef.current?.yAxisValues != null; + const hasLabeledY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null; builder.addAxis({ scaleKey: 'y', - placement: AxisPlacement.Left, + show: yAxisConfig.axisPlacement !== AxisPlacement.Hidden, + placement: yAxisConfig.axisPlacement || AxisPlacement.Left, + size: yAxisConfig.axisWidth || null, + label: yAxisConfig.axisLabel, theme: theme, splits: hasLabeledY ? () => { @@ -255,7 +310,7 @@ export function prepConfig(opts: PrepConfigOpts) { const bucketSize = dataRef.current?.yBucketSize!; - if (dataRef.current?.yLayout === BucketLayout.le) { + if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { splits.unshift(ys[0] - bucketSize); } else { splits.push(ys[ys.length - 1] + bucketSize); @@ -266,12 +321,14 @@ export function prepConfig(opts: PrepConfigOpts) { : undefined, values: hasLabeledY ? () => { - const yAxisValues = dataRef.current?.yAxisValues?.slice()!; + const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap); + const yAxisValues = meta.yOrdinalDisplay?.slice()!; + const isFromBuckets = meta.yOrdinalDisplay?.length && !('le' === meta.yMatchWithLabel); - if (dataRef.current?.yLayout === BucketLayout.le) { - yAxisValues.unshift('0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish - } else if (dataRef.current?.yLayout === BucketLayout.ge) { - yAxisValues.push('+Inf'); + if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { + yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish + } else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) { + yAxisValues.push(isFromBuckets ? '' : '+Inf'); } return yAxisValues; @@ -307,12 +364,18 @@ export function prepConfig(opts: PrepConfigOpts) { }, gap: cellGap, hideThreshold, - xAlign: dataRef.current?.xLayout === BucketLayout.le ? -1 : dataRef.current?.xLayout === BucketLayout.ge ? 1 : 0, - yAlign: ((dataRef.current?.yLayout === BucketLayout.le + xAlign: + dataRef.current?.xLayout === HeatmapBucketLayout.le + ? -1 + : dataRef.current?.xLayout === HeatmapBucketLayout.ge + ? 1 + : 0, + yAlign: ((dataRef.current?.yLayout === HeatmapBucketLayout.le ? -1 - : dataRef.current?.yLayout === BucketLayout.ge + : dataRef.current?.yLayout === HeatmapBucketLayout.ge ? 1 : 0) * (yAxisReverse ? -1 : 1)) as -1 | 0 | 1, + ySizeDivisor, disp: { fill: { values: (u, seriesIdx) => { @@ -402,7 +465,7 @@ export function prepConfig(opts: PrepConfigOpts) { const CRISP_EDGES_GAP_MIN = 4; export function heatmapPathsDense(opts: PathbuilderOpts) { - const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1 } = opts; + const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts; const pxRatio = devicePixelRatio; @@ -451,8 +514,22 @@ export function heatmapPathsDense(opts: PathbuilderOpts) { let xBinIncr = xs[yBinQty] - xs[0]; // uniform tile sizes based on zoom level - let xSize = Math.abs(valToPosX(xBinIncr, scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff)); - let ySize = Math.abs(valToPosY(yBinIncr, scaleY, yDim, yOff) - valToPosY(0, scaleY, yDim, yOff)); + let xSize: number; + let ySize: number; + + if (scaleX.distr === 3) { + xSize = Math.abs(valToPosX(xs[0] * scaleX.log!, scaleX, xDim, xOff) - valToPosX(xs[0], scaleX, xDim, xOff)); + } else { + xSize = Math.abs(valToPosX(xBinIncr, scaleX, xDim, xOff) - valToPosX(0, scaleX, xDim, xOff)); + } + + if (scaleY.distr === 3) { + ySize = + Math.abs(valToPosY(ys[0] * scaleY.log!, scaleY, yDim, yOff) - valToPosY(ys[0], scaleY, yDim, yOff)) / + ySizeDivisor; + } else { + ySize = Math.abs(valToPosY(yBinIncr, scaleY, yDim, yOff) - valToPosY(0, scaleY, yDim, yOff)) / ySizeDivisor; + } // clamp min tile size to 1px xSize = Math.max(1, round(xSize - cellGap)); From 635a6b69b3457a41675e70cbd7da87b11d676e4b Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sat, 4 Jun 2022 00:11:49 -0400 Subject: [PATCH 81/95] State Timeline: Fix Null Value Filling and Value Transformation (#50054) (#50196) (cherry picked from commit 12ba2d6b8b3be0f21fc8057887ead7bb8b272d87) Co-authored-by: Kyle Cunningham --- packages/grafana-data/src/types/dataFrame.ts | 7 ++ .../GraphNG/nullInsertThreshold.test.ts | 96 ++++++++++++++++--- .../components/GraphNG/nullInsertThreshold.ts | 57 ++++++++--- .../components/GraphNG/nullToValue.test.ts | 94 ++++++++++++++++++ .../src/components/GraphNG/nullToValue.ts | 17 ++++ .../src/components/GraphNG/utils.ts | 13 ++- .../src/components/Sparkline/utils.ts | 20 ++-- .../app/plugins/panel/graph/data_processor.ts | 2 +- .../state-timeline/StateTimelinePanel.tsx | 4 +- .../panel/state-timeline/utils.test.ts | 13 ++- .../app/plugins/panel/state-timeline/utils.ts | 20 +++- .../status-history/StatusHistoryPanel.tsx | 5 +- 12 files changed, 303 insertions(+), 45 deletions(-) create mode 100644 packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts create mode 100644 packages/grafana-ui/src/components/GraphNG/nullToValue.ts diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index e385ca4fb15..75d7482a455 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -181,6 +181,13 @@ export interface FieldState { * This is only related to the cached displayName property above. */ multipleFrames?: boolean; + + /** + * Boolean value is true if a null filling threshold has been applied + * against the frame of the field. This is used to avoid cases in which + * this would applied more than one time. + */ + nullThresholdApplied?: boolean; } /** @public */ diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts index 210233a89b9..6373e8884b8 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts @@ -57,7 +57,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, null, null, null, null, null, 8]); @@ -74,7 +74,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]); expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, null, 8]); @@ -91,13 +91,63 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, null, null, null, null, null, 8]); expect(result.fields[2].values.toArray()).toStrictEqual(['a', null, 'b', null, null, null, null, null, null, 'c']); }); + test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [4, 6, 13] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ + frame: df, + refFieldName: null, + refFieldPseudoMin: 1, + refFieldPseudoMax: 13, + }); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(result.fields[1].values.toArray()).toStrictEqual([ + null, + null, + null, + 4, + null, + 6, + null, + null, + null, + null, + null, + null, + 8, + ]); + expect(result.fields[2].values.toArray()).toStrictEqual([ + null, + null, + null, + 'a', + null, + 'b', + null, + null, + null, + null, + null, + null, + 'c', + ]); + }); + test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => { const df = new MutableDataFrame({ refId: 'A', @@ -108,10 +158,24 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df, null, 13); + const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 13 }); - expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, null, null, null, null, null, 8, null]); + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(result.fields[1].values.toArray()).toStrictEqual([ + 4, + null, + 6, + null, + null, + null, + null, + null, + null, + 8, + null, + null, + null, + ]); expect(result.fields[2].values.toArray()).toStrictEqual([ 'a', null, @@ -124,6 +188,8 @@ describe('nullInsertThreshold Transformer', () => { null, 'c', null, + null, + null, ]); // should work for frames with 1 datapoint @@ -136,7 +202,9 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result2 = applyNullInsertThreshold(df2, null, 13); + // Max is 2 as opposed to the above 13 otherwise + // we get 12 nulls instead of the additional 1 + const result2 = applyNullInsertThreshold({ frame: df2, refFieldName: null, refFieldPseudoMax: 2 }); expect(result2.fields[0].values.toArray()).toStrictEqual([1, 2]); expect(result2.fields[1].values.toArray()).toStrictEqual([1, null]); @@ -154,7 +222,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result.fields[0].values.toArray()).toStrictEqual([5, 6, 7, 8, 11]); expect(result.fields[1].values.toArray()).toStrictEqual([4, null, 6, null, 8]); @@ -170,7 +238,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result).toBe(df); }); @@ -184,7 +252,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result).toBe(df); }); @@ -198,7 +266,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result).toBe(df); }); @@ -212,7 +280,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df); + const result = applyNullInsertThreshold({ frame: df }); expect(result).toBe(df); }); @@ -226,7 +294,7 @@ describe('nullInsertThreshold Transformer', () => { ], }); - const result = applyNullInsertThreshold(df, 'Time2'); + const result = applyNullInsertThreshold({ frame: df, refFieldName: 'Time2' }); expect(result).toBe(df); }); @@ -238,7 +306,7 @@ describe('nullInsertThreshold Transformer', () => { // eslint-disable-next-line no-console console.time('insertValues-10x3k'); - applyNullInsertThreshold(bigFrameA); + applyNullInsertThreshold({ frame: bigFrameA }); // eslint-disable-next-line no-console console.timeEnd('insertValues-10x3k'); }); diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts index 6c61426f930..cd996e1179c 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts +++ b/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts @@ -9,15 +9,24 @@ const INSERT_MODES = { plusone: (prev: number, next: number, threshold: number) => prev + 1, }; -export function applyNullInsertThreshold( - frame: DataFrame, - refFieldName?: string | null, - refFieldPseudoMax: number | null = null, - insertMode: InsertMode = INSERT_MODES.threshold, - thorough = true -): DataFrame { - if (frame.length === 0) { - return frame; +interface NullInsertOptions { + frame: DataFrame; + refFieldName?: string | null; + refFieldPseudoMax?: number; + refFieldPseudoMin?: number; + insertMode?: InsertMode; +} + +export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame { + if (opts.frame.length === 0) { + return opts.frame; + } + + let thorough = true; + let { frame, refFieldName, refFieldPseudoMax, refFieldPseudoMin, insertMode } = opts; + + if (!insertMode) { + insertMode = INSERT_MODES.threshold; } const refField = frame.fields.find((field) => { @@ -54,6 +63,7 @@ export function applyNullInsertThreshold( refValues, frameValues, threshold, + refFieldPseudoMin, refFieldPseudoMax, insertMode, thorough @@ -83,6 +93,7 @@ function nullInsertThreshold( refValues: number[], frameValues: any[][], threshold: number, + refFieldPseudoMin: number | null = null, // will insert a trailing null when refFieldPseudoMax > last datapoint + threshold refFieldPseudoMax: number | null = null, getInsertValue: InsertMode, @@ -91,8 +102,26 @@ function nullInsertThreshold( ) { const len = refValues.length; let prevValue: number = refValues[0]; - const refValuesNew: number[] = [prevValue]; + const refValuesNew: number[] = []; + // Continiuously add the threshold to the minimum value + // While this is less than "prevValue" which is the lowest + // time value in the sequence add in time frames + if (refFieldPseudoMin != null) { + let minValue = refFieldPseudoMin - threshold; + + while (minValue < prevValue - threshold) { + let nextValue = minValue + threshold; + refValuesNew.push(getInsertValue(minValue, nextValue, threshold)); + minValue = nextValue; + } + } + + // Insert initial value + refValuesNew.push(prevValue); + + // Fill nulls when a value is greater than + // the threshold value for (let i = 1; i < len; i++) { const curValue = refValues[i]; @@ -111,8 +140,12 @@ function nullInsertThreshold( prevValue = curValue; } - if (refFieldPseudoMax != null && prevValue + threshold <= refFieldPseudoMax) { - refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold)); + // At the end of the sequence + if (refFieldPseudoMax != null) { + while (prevValue + threshold <= refFieldPseudoMax) { + refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold)); + prevValue += threshold; + } } const filledLen = refValuesNew.length; diff --git a/packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts b/packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts new file mode 100644 index 00000000000..f5c3bfb1a7e --- /dev/null +++ b/packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts @@ -0,0 +1,94 @@ +import { FieldType, MutableDataFrame } from '@grafana/data'; + +import { applyNullInsertThreshold } from './nullInsertThreshold'; +import { nullToValue } from './nullToValue'; + +describe('nullToValue Transformer', () => { + test('should change all nulls to configured zero value', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, 0, 6, 0, 0, 0, 0, 0, 0, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', 0, 'b', 0, 0, 0, 0, 0, 0, 'c']); + }); + + test('should change all nulls to configured positive value', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [5, 7, 11] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 2 }, noValue: '1' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 2 }, noValue: '1' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values.toArray()).toStrictEqual([5, 7, 9, 11]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, 1, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', 1, 'c']); + }); + + test('should change all nulls to configured negative value', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, config: { noValue: '-1' }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { noValue: '-1' }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, -1, 6, -1, -1, -1, -1, -1, -1, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', -1, 'b', -1, -1, -1, -1, -1, -1, 'c']); + }); + + test('should have no effect without nulls', () => { + const df = new MutableDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df, refFieldName: null })); + + expect(result.fields[0].values.toArray()).toStrictEqual([1, 2, 3]); + expect(result.fields[1].values.toArray()).toStrictEqual([4, 6, 8]); + expect(result.fields[2].values.toArray()).toStrictEqual(['a', 'b', 'c']); + }); +}); diff --git a/packages/grafana-ui/src/components/GraphNG/nullToValue.ts b/packages/grafana-ui/src/components/GraphNG/nullToValue.ts new file mode 100644 index 00000000000..b79ac200214 --- /dev/null +++ b/packages/grafana-ui/src/components/GraphNG/nullToValue.ts @@ -0,0 +1,17 @@ +import { DataFrame } from '@grafana/data'; + +export function nullToValue(frame: DataFrame) { + frame.fields.forEach((f) => { + const noValue = +f.config?.noValue!; + if (!Number.isNaN(noValue)) { + const values = f.values.toArray(); + for (let i = 0; i < values.length; i++) { + if (values[i] === null) { + values[i] = noValue; + } + } + } + }); + + return frame; +} diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 5d35a8e3926..e5de3bd9f66 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -44,7 +44,18 @@ function applySpanNullsThresholds(frame: DataFrame) { export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { // apply null insertions at interval - frames = frames.map((frame) => applyNullInsertThreshold(frame, null, timeRange?.to.valueOf())); + frames = frames.map((frame) => { + if (!frame.fields[0].state?.nullThresholdApplied) { + return applyNullInsertThreshold({ + frame, + refFieldName: null, + refFieldPseudoMin: timeRange?.from.valueOf(), + refFieldPseudoMax: timeRange?.to.valueOf(), + }); + } else { + return frame; + } + }); let numBarSeries = 0; diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index 0d8952f9daa..38a925e1d1b 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -14,14 +14,16 @@ export function preparePlotFrame(sparkline: FieldSparkline, config?: FieldConfig }; return applyNullInsertThreshold({ - refId: 'sparkline', - fields: [ - sparkline.x ?? IndexVector.newField(length), - { - ...sparkline.y, - config: yFieldConfig, - }, - ], - length, + frame: { + refId: 'sparkline', + fields: [ + sparkline.x ?? IndexVector.newField(length), + { + ...sparkline.y, + config: yFieldConfig, + }, + ], + length, + }, }); } diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index b0db3635b80..e95cb7e74d8 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -30,7 +30,7 @@ export class DataProcessor { continue; } - series = applyNullInsertThreshold(series, timeField.name); + series = applyNullInsertThreshold({ frame: series, refFieldName: timeField.name }); timeField = getTimeField(series).timeField!; // use updated length for (let j = 0; j < series.fields.length; j++) { diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 3a0f899c06a..09746ba56c8 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -29,8 +29,8 @@ export const StateTimelinePanel: React.FC = ({ const { sync } = usePanelContext(); const { frames, warn } = useMemo( - () => prepareTimelineFields(data?.series, options.mergeValues ?? true, theme), - [data, options.mergeValues, theme] + () => prepareTimelineFields(data?.series, options.mergeValues ?? true, timeRange, theme), + [data, options.mergeValues, timeRange, theme] ); const legendItems = useMemo( diff --git a/public/app/plugins/panel/state-timeline/utils.test.ts b/public/app/plugins/panel/state-timeline/utils.test.ts index 19dc9f188bf..92bdc05f3f7 100644 --- a/public/app/plugins/panel/state-timeline/utils.test.ts +++ b/public/app/plugins/panel/state-timeline/utils.test.ts @@ -1,4 +1,4 @@ -import { ArrayVector, createTheme, FieldType, ThresholdsMode, toDataFrame } from '@grafana/data'; +import { ArrayVector, createTheme, FieldType, ThresholdsMode, TimeRange, toDataFrame, dateTime } from '@grafana/data'; import { LegendDisplayMode } from '@grafana/schema'; import { @@ -12,6 +12,11 @@ import { const theme = createTheme(); describe('prepare timeline graph', () => { + const timeRange: TimeRange = { + from: dateTime(1), + to: dateTime(3), + raw: {} as any, + }; it('errors with no time fields', () => { const frames = [ toDataFrame({ @@ -21,7 +26,7 @@ describe('prepare timeline graph', () => { ], }), ]; - const info = prepareTimelineFields(frames, true, theme); + const info = prepareTimelineFields(frames, true, timeRange, theme); expect(info.warn).toEqual('Data does not have a time field'); }); @@ -34,7 +39,7 @@ describe('prepare timeline graph', () => { ], }), ]; - const info = prepareTimelineFields(frames, true, theme); + const info = prepareTimelineFields(frames, true, timeRange, theme); expect(info.warn).toEqual('No graphable fields'); }); @@ -47,7 +52,7 @@ describe('prepare timeline graph', () => { ], }), ]; - const info = prepareTimelineFields(frames, true, theme); + const info = prepareTimelineFields(frames, true, timeRange, theme); expect(info.warn).toBeUndefined(); const out = info.frames![0]; diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index 9d0e7e44625..d85ca7dd67e 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -21,6 +21,7 @@ import { Threshold, getFieldConfigWithMinMax, ThresholdsMode, + TimeRange, } from '@grafana/data'; import { VizLegendOptions, AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/schema'; import { @@ -30,6 +31,8 @@ import { UPlotConfigPrepFn, VizLegendItem, } from '@grafana/ui'; +import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; +import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue'; import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types'; import { preparePlotData2, getStackingGroups } from '../../../../../packages/grafana-ui/src/components/uPlot/utils'; @@ -379,6 +382,7 @@ export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field export function prepareTimelineFields( series: DataFrame[] | undefined, mergeValues: boolean, + timeRange: TimeRange, theme: GrafanaTheme2 ): { frames?: DataFrame[]; warn?: string } { if (!series?.length) { @@ -386,11 +390,25 @@ export function prepareTimelineFields( } let hasTimeseries = false; const frames: DataFrame[] = []; + for (let frame of series) { let isTimeseries = false; let changed = false; + + let nulledFrame = applyNullInsertThreshold({ + frame, + refFieldPseudoMin: timeRange.from.valueOf(), + refFieldPseudoMax: timeRange.to.valueOf(), + }); + + // Mark the field state as having a null threhold applied + frame.fields[0].state = { + ...frame.fields[0].state, + nullThresholdApplied: true, + }; + const fields: Field[] = []; - for (let field of frame.fields) { + for (let field of nullToValue(nulledFrame).fields) { switch (field.type) { case FieldType.time: isTimeseries = true; diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index 8a1fcbc2a78..5ae1d277983 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -26,7 +26,10 @@ export const StatusHistoryPanel: React.FC = ({ }) => { const theme = useTheme2(); - const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, false, theme), [data, theme]); + const { frames, warn } = useMemo( + () => prepareTimelineFields(data?.series, false, timeRange, theme), + [data, timeRange, theme] + ); const legendItems = useMemo( () => prepareTimelineLegendItems(frames, options.legend, theme), From f404191ccbe84cf2a00bac71fb0aafc1ecc89884 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sat, 4 Jun 2022 02:06:17 -0400 Subject: [PATCH 82/95] Alerting: only delete mute time if not used by route (#50193) (#50205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Alerting: only delete mute time if not used by route * add a testcase * import package only once * replace apimodels with definitions (cherry picked from commit 8de4ffe61f28efb93e62856169d31acf372a0791) Co-authored-by: Jean-Philippe Quéméner --- .../ngalert/provisioning/mute_timings.go | 20 ++++++++ .../ngalert/provisioning/mute_timings_test.go | 50 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index 0df16beada4..3df436a7c8b 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -159,6 +159,9 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string, if revision.cfg.AlertmanagerConfig.MuteTimeIntervals == nil { return nil } + if isMuteTimeInUse(name, []*definitions.Route{revision.cfg.AlertmanagerConfig.Route}) { + return fmt.Errorf("mute time '%s' is currently used by a notification policy", name) + } for i, existing := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals { if name == existing.Name { intervals := revision.cfg.AlertmanagerConfig.MuteTimeIntervals @@ -190,3 +193,20 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, name string, return nil }) } + +func isMuteTimeInUse(name string, routes []*definitions.Route) bool { + if len(routes) == 0 { + return false + } + for _, route := range routes { + for _, mtName := range route.MuteTimeIntervals { + if mtName == name { + return true + } + } + if isMuteTimeInUse(name, route.Routes) { + return true + } + } + return false +} diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go index 9a6b6e90853..4085f713d7a 100644 --- a/pkg/services/ngalert/provisioning/mute_timings_test.go +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -356,6 +356,18 @@ func TestMuteTimingService(t *testing.T) { require.ErrorContains(t, err, "failed to save config") }) + + t.Run("when mute timing is used in route", func(t *testing.T) { + sut := createMuteTimingSvcSut() + sut.config.(*MockAMConfigStore).EXPECT(). + getsConfig(models.AlertConfiguration{ + AlertmanagerConfiguration: configWithMuteTimingsInRoute, + }) + + err := sut.DeleteMuteTiming(context.Background(), "asdf", 1) + + require.Error(t, err) + }) }) }) } @@ -408,3 +420,41 @@ var configWithMuteTimings = ` } } ` + +var configWithMuteTimingsInRoute = ` +{ + "template_files": { + "a": "template" + }, + "alertmanager_config": { + "route": { + "receiver": "grafana-default-email", + "routes": [ + { + "receiver": "grafana-default-email", + "mute_time_intervals": ["asdf"] + } + ] + }, + "mute_time_intervals": [{ + "name": "asdf", + "time_intervals": [{ + "times": [], + "weekdays": ["monday"] + }] + }], + "receivers": [{ + "name": "grafana-default-email", + "grafana_managed_receiver_configs": [{ + "uid": "", + "name": "email receiver", + "type": "email", + "isDefault": true, + "settings": { + "addresses": "" + } + }] + }] + } +} +` From 67cbd5015d08a256a1c657eec7cb3a62117f46fd Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sat, 4 Jun 2022 07:13:34 -0400 Subject: [PATCH 83/95] Encryption: Fix multiple data keys migration (#49848) (#50207) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add migration * Migrator: Extend support to rename columns * Fix getting current key * Fix column name in migration * Fix deks reencryption * Fix caching * Add back separate caches for byName and byPrefix * Do not concatenate prefix with uid * Rename DataKey struc fields * SQLStore: Add deprecation comments for breaking migrations * Add comment * Minor corrections Co-authored-by: Joan López de la Franca Beltran (cherry picked from commit 4f8111e24eafc0184b2490f015709e4e099dd722) Co-authored-by: Tania --- pkg/services/datasources/datasources.go | 6 +-- pkg/services/secrets/database/database.go | 20 ++++---- pkg/services/secrets/fakes/fake_store.go | 4 +- pkg/services/secrets/manager/cache.go | 21 ++++---- pkg/services/secrets/manager/manager.go | 49 +++++++++++++------ pkg/services/secrets/manager/manager_test.go | 20 ++++---- pkg/services/secrets/secrets.go | 4 +- pkg/services/secrets/types.go | 4 +- .../sqlstore/migrations/secrets_mig.go | 12 +++++ 9 files changed, 85 insertions(+), 55 deletions(-) diff --git a/pkg/services/datasources/datasources.go b/pkg/services/datasources/datasources.go index b6212794328..67296b72231 100644 --- a/pkg/services/datasources/datasources.go +++ b/pkg/services/datasources/datasources.go @@ -40,15 +40,15 @@ type DataSourceService interface { DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) // DecryptedValue decrypts the encrypted datasource secureJSONData identified by key - // and returns the decryped value. + // and returns the decrypted value. DecryptedValue(ctx context.Context, ds *models.DataSource, key string) (string, bool, error) // DecryptedBasicAuthPassword decrypts the encrypted datasource basic authentication - // password and returns the decryped value. + // password and returns the decrypted value. DecryptedBasicAuthPassword(ctx context.Context, ds *models.DataSource) (string, error) // DecryptedPassword decrypts the encrypted datasource password and returns the - // decryped value. + // decrypted value. DecryptedPassword(ctx context.Context, ds *models.DataSource) (string, error) } diff --git a/pkg/services/secrets/database/database.go b/pkg/services/secrets/database/database.go index 137a797d73a..57867d81d55 100644 --- a/pkg/services/secrets/database/database.go +++ b/pkg/services/secrets/database/database.go @@ -33,7 +33,7 @@ func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, id string) (*secrets err := ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { var err error exists, err = sess.Table(dataKeysTable). - Where("id = ?", id). + Where("name = ?", id). Get(dataKey) return err }) @@ -49,14 +49,14 @@ func (ss *SecretsStoreImpl) GetDataKey(ctx context.Context, id string) (*secrets return dataKey, nil } -func (ss *SecretsStoreImpl) GetCurrentDataKey(ctx context.Context, name string) (*secrets.DataKey, error) { +func (ss *SecretsStoreImpl) GetCurrentDataKey(ctx context.Context, label string) (*secrets.DataKey, error) { dataKey := &secrets.DataKey{} var exists bool err := ss.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { var err error exists, err = sess.Table(dataKeysTable). - Where("name = ? AND active = ?", name, ss.sqlStore.Dialect.BooleanStr(true)). + Where("label = ? AND active = ?", label, ss.sqlStore.Dialect.BooleanStr(true)). Get(dataKey) return err }) @@ -66,7 +66,7 @@ func (ss *SecretsStoreImpl) GetCurrentDataKey(ctx context.Context, name string) } if err != nil { - return nil, fmt.Errorf("failed getting data key: %w", err) + return nil, fmt.Errorf("failed getting current data key: %w", err) } return dataKey, nil @@ -137,7 +137,7 @@ func (ss *SecretsStoreImpl) ReEncryptDataKeys( ss.log.Warn( "Could not find provider to re-encrypt data encryption key", "id", k.Id, - "name", k.Name, + "label", k.Label, "provider", k.Provider, ) return nil @@ -148,7 +148,7 @@ func (ss *SecretsStoreImpl) ReEncryptDataKeys( ss.log.Warn( "Error while decrypting data encryption key to re-encrypt it", "id", k.Id, - "name", k.Name, + "label", k.Label, "provider", k.Provider, "err", err, ) @@ -158,25 +158,25 @@ func (ss *SecretsStoreImpl) ReEncryptDataKeys( // Updating current data key by re-encrypting it with current provider. // Accessing the current provider within providers map should be safe. k.Provider = currProvider - k.Name = secrets.KeyName(k.Scope, currProvider) + k.Label = secrets.KeyLabel(k.Scope, currProvider) k.Updated = time.Now() k.EncryptedData, err = providers[currProvider].Encrypt(ctx, decrypted) if err != nil { ss.log.Warn( "Error while re-encrypting data encryption key", "id", k.Id, - "name", k.Name, + "label", k.Label, "provider", k.Provider, "err", err, ) return nil } - if _, err := sess.Table(dataKeysTable).Where("id = ?", k.Id).Update(k); err != nil { + if _, err := sess.Table(dataKeysTable).Where("name = ?", k.Id).Update(k); err != nil { ss.log.Warn( "Error while re-encrypting data encryption key", "id", k.Id, - "name", k.Name, + "label", k.Label, "provider", k.Provider, "err", err, ) diff --git a/pkg/services/secrets/fakes/fake_store.go b/pkg/services/secrets/fakes/fake_store.go index fca7d02744d..12650a87615 100644 --- a/pkg/services/secrets/fakes/fake_store.go +++ b/pkg/services/secrets/fakes/fake_store.go @@ -24,9 +24,9 @@ func (f FakeSecretsStore) GetDataKey(_ context.Context, id string) (*secrets.Dat return key, nil } -func (f FakeSecretsStore) GetCurrentDataKey(_ context.Context, name string) (*secrets.DataKey, error) { +func (f FakeSecretsStore) GetCurrentDataKey(_ context.Context, label string) (*secrets.DataKey, error) { for _, key := range f.store { - if key.Name == name && key.Active { + if key.Label == label && key.Active { return key, nil } } diff --git a/pkg/services/secrets/manager/cache.go b/pkg/services/secrets/manager/cache.go index 84438ae7eb2..7ea65d11ec6 100644 --- a/pkg/services/secrets/manager/cache.go +++ b/pkg/services/secrets/manager/cache.go @@ -14,8 +14,9 @@ var ( type dataKeyCacheEntry struct { id string - name string + label string dataKey []byte + active bool expiration time.Time } @@ -26,14 +27,14 @@ func (e dataKeyCacheEntry) expired() bool { type dataKeyCache struct { mtx sync.RWMutex byId map[string]*dataKeyCacheEntry - byName map[string]*dataKeyCacheEntry + byLabel map[string]*dataKeyCacheEntry cacheTTL time.Duration } func newDataKeyCache(ttl time.Duration) *dataKeyCache { return &dataKeyCache{ byId: make(map[string]*dataKeyCacheEntry), - byName: make(map[string]*dataKeyCacheEntry), + byLabel: make(map[string]*dataKeyCacheEntry), cacheTTL: ttl, } } @@ -56,15 +57,15 @@ func (c *dataKeyCache) getById(id string) (*dataKeyCacheEntry, bool) { return entry, true } -func (c *dataKeyCache) getByName(name string) (*dataKeyCacheEntry, bool) { +func (c *dataKeyCache) getByLabel(label string) (*dataKeyCacheEntry, bool) { c.mtx.RLock() defer c.mtx.RUnlock() - entry, exists := c.byName[name] + entry, exists := c.byLabel[label] cacheReadsCounter.With(prometheus.Labels{ "hit": strconv.FormatBool(exists), - "method": "byName", + "method": "byLabel", }).Inc() if !exists || entry.expired() { @@ -81,7 +82,7 @@ func (c *dataKeyCache) add(entry *dataKeyCacheEntry) { entry.expiration = now().Add(c.cacheTTL) c.byId[entry.id] = entry - c.byName[entry.name] = entry + c.byLabel[entry.label] = entry } func (c *dataKeyCache) removeExpired() { @@ -94,9 +95,9 @@ func (c *dataKeyCache) removeExpired() { } } - for name, entry := range c.byName { + for label, entry := range c.byLabel { if entry.expired() { - delete(c.byName, name) + delete(c.byLabel, label) } } } @@ -104,6 +105,6 @@ func (c *dataKeyCache) removeExpired() { func (c *dataKeyCache) flush() { c.mtx.Lock() c.byId = make(map[string]*dataKeyCacheEntry) - c.byName = make(map[string]*dataKeyCacheEntry) + c.byLabel = make(map[string]*dataKeyCacheEntry) c.mtx.Unlock() } diff --git a/pkg/services/secrets/manager/manager.go b/pkg/services/secrets/manager/manager.go index aa5c539ecf8..544f1d4e8c4 100644 --- a/pkg/services/secrets/manager/manager.go +++ b/pkg/services/secrets/manager/manager.go @@ -146,11 +146,13 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt // If encryption featuremgmt.FlagEnvelopeEncryption toggle is on, use envelope encryption scope := opt() - name := secrets.KeyName(scope, s.currentProviderID) + label := secrets.KeyLabel(scope, s.currentProviderID) - id, dataKey, err := s.currentDataKey(ctx, name, scope, sess) + var id string + var dataKey []byte + id, dataKey, err = s.currentDataKey(ctx, label, scope, sess) if err != nil { - s.log.Error("Failed to get current data key", "error", err, "name", name) + s.log.Error("Failed to get current data key", "error", err, "label", label) return nil, err } @@ -176,21 +178,21 @@ func (s *SecretsService) EncryptWithDBSession(ctx context.Context, payload []byt // currentDataKey looks up for current data key in cache or database by name, and decrypts it. // If there's no current data key in cache nor in database it generates a new random data key, // and stores it into both the in-memory cache and database (encrypted by the encryption provider). -func (s *SecretsService) currentDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) (string, []byte, error) { +func (s *SecretsService) currentDataKey(ctx context.Context, label string, scope string, sess *xorm.Session) (string, []byte, error) { // We want only one request fetching current data key at time to // avoid the creation of multiple ones in case there's no one existing. s.mtx.Lock() defer s.mtx.Unlock() // We try to fetch the data key, either from cache or database - id, dataKey, err := s.dataKeyByName(ctx, name) + id, dataKey, err := s.dataKeyByLabel(ctx, label) if err != nil { return "", nil, err } // If no existing data key was found, create a new one if dataKey == nil { - id, dataKey, err = s.newDataKey(ctx, name, scope, sess) + id, dataKey, err = s.newDataKey(ctx, label, scope, sess) if err != nil { return "", nil, err } @@ -199,16 +201,16 @@ func (s *SecretsService) currentDataKey(ctx context.Context, name string, scope return id, dataKey, nil } -// dataKeyByName looks up for data key in cache. +// dataKeyByLabel looks up for data key in cache. // Otherwise, it fetches it from database, decrypts it and caches it decrypted. -func (s *SecretsService) dataKeyByName(ctx context.Context, name string) (string, []byte, error) { +func (s *SecretsService) dataKeyByLabel(ctx context.Context, label string) (string, []byte, error) { // 0. Get data key from in-memory cache. - if entry, exists := s.dataKeyCache.getByName(name); exists { + if entry, exists := s.dataKeyCache.getByLabel(label); exists && entry.active { return entry.id, entry.dataKey, nil } // 1. Get data key from database. - dataKey, err := s.store.GetCurrentDataKey(ctx, name) + dataKey, err := s.store.GetCurrentDataKey(ctx, label) if err != nil { if errors.Is(err, secrets.ErrDataKeyNotFound) { return "", nil, nil @@ -229,13 +231,18 @@ func (s *SecretsService) dataKeyByName(ctx context.Context, name string) (string } // 3. Store the decrypted data key into the in-memory cache. - s.dataKeyCache.add(&dataKeyCacheEntry{id: dataKey.Id, name: dataKey.Name, dataKey: decrypted}) + s.dataKeyCache.add(&dataKeyCacheEntry{ + id: dataKey.Id, + label: dataKey.Label, + dataKey: decrypted, + active: dataKey.Active, + }) return dataKey.Id, decrypted, nil } // newDataKey creates a new random data key, encrypts it and stores it into the database and cache. -func (s *SecretsService) newDataKey(ctx context.Context, name string, scope string, sess *xorm.Session) (string, []byte, error) { +func (s *SecretsService) newDataKey(ctx context.Context, label string, scope string, sess *xorm.Session) (string, []byte, error) { // 1. Create new data key. dataKey, err := newRandomDataKey() if err != nil { @@ -257,11 +264,11 @@ func (s *SecretsService) newDataKey(ctx context.Context, name string, scope stri // 3. Store its encrypted value into the DB. id := util.GenerateShortUID() dbDataKey := secrets.DataKey{ - Id: id, Active: true, - Name: name, + Id: id, Provider: s.currentProviderID, EncryptedData: encrypted, + Label: label, Scope: scope, } @@ -276,7 +283,12 @@ func (s *SecretsService) newDataKey(ctx context.Context, name string, scope stri } // 4. Store the decrypted data key into the in-memory cache. - s.dataKeyCache.add(&dataKeyCacheEntry{id: id, name: name, dataKey: dataKey}) + s.dataKeyCache.add(&dataKeyCacheEntry{ + id: id, + label: label, + dataKey: dataKey, + active: true, + }) return id, dataKey, nil } @@ -417,7 +429,12 @@ func (s *SecretsService) dataKeyById(ctx context.Context, id string) ([]byte, er } // 3. Store the decrypted data key into the in-memory cache. - s.dataKeyCache.add(&dataKeyCacheEntry{id: id, name: dataKey.Name, dataKey: decrypted}) + s.dataKeyCache.add(&dataKeyCacheEntry{ + id: dataKey.Id, + label: dataKey.Label, + dataKey: decrypted, + active: dataKey.Active, + }) return decrypted, nil } diff --git a/pkg/services/secrets/manager/manager_test.go b/pkg/services/secrets/manager/manager_test.go index 4144c1940cf..85ba4d259ec 100644 --- a/pkg/services/secrets/manager/manager_test.go +++ b/pkg/services/secrets/manager/manager_test.go @@ -100,8 +100,8 @@ func TestSecretsService_DataKeys(t *testing.T) { dataKey := &secrets.DataKey{ Id: util.GenerateShortUID(), + Label: "test1", Active: true, - Name: "test1", Provider: "test", EncryptedData: []byte{0x62, 0xAF, 0xA1, 0x1A}, } @@ -120,15 +120,15 @@ func TestSecretsService_DataKeys(t *testing.T) { require.NoError(t, err) assert.Equal(t, dataKey.EncryptedData, res.EncryptedData) assert.Equal(t, dataKey.Provider, res.Provider) - assert.Equal(t, dataKey.Name, res.Name) + assert.Equal(t, dataKey.Label, res.Label) assert.Equal(t, dataKey.Id, res.Id) assert.True(t, dataKey.Active) - current, err := store.GetCurrentDataKey(ctx, dataKey.Name) + current, err := store.GetCurrentDataKey(ctx, dataKey.Label) require.NoError(t, err) assert.Equal(t, dataKey.EncryptedData, current.EncryptedData) assert.Equal(t, dataKey.Provider, current.Provider) - assert.Equal(t, dataKey.Name, current.Name) + assert.Equal(t, dataKey.Label, current.Label) assert.Equal(t, dataKey.Id, current.Id) assert.True(t, current.Active) }) @@ -137,7 +137,7 @@ func TestSecretsService_DataKeys(t *testing.T) { k := &secrets.DataKey{ Id: util.GenerateShortUID(), Active: false, - Name: "test2", + Label: "test2", Provider: "test", EncryptedData: []byte{0x62, 0xAF, 0xA1, 0x1A}, } @@ -145,7 +145,7 @@ func TestSecretsService_DataKeys(t *testing.T) { err := store.CreateDataKey(ctx, k) require.Error(t, err) - res, err := store.GetDataKey(ctx, k.Name) + res, err := store.GetDataKey(ctx, k.Id) assert.Equal(t, secrets.ErrDataKeyNotFound, err) assert.Nil(t, res) }) @@ -287,7 +287,7 @@ func TestSecretsService_Run(t *testing.T) { // Data encryption key cache should contain one element require.Len(t, svc.dataKeyCache.byId, 1) - require.Len(t, svc.dataKeyCache.byName, 1) + require.Len(t, svc.dataKeyCache.byLabel, 1) t.Cleanup(func() { now = time.Now }) now = func() time.Time { return time.Now().Add(10 * time.Minute) } @@ -302,7 +302,7 @@ func TestSecretsService_Run(t *testing.T) { // the cleanup process should have happened, // therefore the cache should be empty. require.Len(t, svc.dataKeyCache.byId, 0) - require.Len(t, svc.dataKeyCache.byName, 0) + require.Len(t, svc.dataKeyCache.byLabel, 0) }) } @@ -337,12 +337,12 @@ func TestSecretsService_ReEncryptDataKeys(t *testing.T) { _, err := svc.Decrypt(ctx, ciphertext) require.NoError(t, err) require.NotEmpty(t, svc.dataKeyCache.byId) - require.NotEmpty(t, svc.dataKeyCache.byName) + require.NotEmpty(t, svc.dataKeyCache.byLabel) err = svc.ReEncryptDataKeys(ctx) require.NoError(t, err) assert.Empty(t, svc.dataKeyCache.byId) - assert.Empty(t, svc.dataKeyCache.byName) + assert.Empty(t, svc.dataKeyCache.byLabel) }) } diff --git a/pkg/services/secrets/secrets.go b/pkg/services/secrets/secrets.go index 9c71ed4ff4b..443d3687588 100644 --- a/pkg/services/secrets/secrets.go +++ b/pkg/services/secrets/secrets.go @@ -33,7 +33,7 @@ type Service interface { // Store defines methods to interact with secrets storage type Store interface { GetDataKey(ctx context.Context, id string) (*DataKey, error) - GetCurrentDataKey(ctx context.Context, name string) (*DataKey, error) + GetCurrentDataKey(ctx context.Context, label string) (*DataKey, error) GetAllDataKeys(ctx context.Context) ([]*DataKey, error) CreateDataKey(ctx context.Context, dataKey *DataKey) error CreateDataKeyWithDBSession(ctx context.Context, dataKey *DataKey, sess *xorm.Session) error @@ -61,7 +61,7 @@ func (id ProviderID) Kind() (string, error) { return parts[0], nil } -func KeyName(scope string, providerID ProviderID) string { +func KeyLabel(scope string, providerID ProviderID) string { return fmt.Sprintf("%s/%s@%s", time.Now().Format("2006-01-02"), scope, providerID) } diff --git a/pkg/services/secrets/types.go b/pkg/services/secrets/types.go index b67a7f59b82..ddb96e350a2 100644 --- a/pkg/services/secrets/types.go +++ b/pkg/services/secrets/types.go @@ -9,8 +9,8 @@ var ErrDataKeyNotFound = errors.New("data key not found") type DataKey struct { Active bool - Id string - Name string + Id string `xorm:"name"` // renaming the col in the db itself would break backward compatibility with 8.5.x + Label string Scope string Provider ProviderID EncryptedData []byte diff --git a/pkg/services/sqlstore/migrations/secrets_mig.go b/pkg/services/sqlstore/migrations/secrets_mig.go index 5baadca3eb8..abd4e5aa348 100644 --- a/pkg/services/sqlstore/migrations/secrets_mig.go +++ b/pkg/services/sqlstore/migrations/secrets_mig.go @@ -61,4 +61,16 @@ func addSecretsMigration(mg *migrator.Migrator) { mg.AddMigration("copy data_keys id column values into name", migrator.NewRawSQLMigration( fmt.Sprintf("UPDATE %s SET %s = %s", dataKeysV1.Name, "name", "id"), )) + // ------- This is done for backward compatibility with versions > v8.3.x + mg.AddMigration("rename data_keys name column to label", migrator.NewRenameColumnMigration( + dataKeysV1, dataKeysV1.Columns[0], "label", + )) + + mg.AddMigration("rename data_keys id column back to name", migrator.NewRenameColumnMigration( + dataKeysV1, + &migrator.Column{Name: "id", Type: migrator.DB_NVarchar, Length: 100, IsPrimaryKey: true}, + "name", + )) + + // -------------------- } From 92d995d6582b2312e564b44664dfb95a5b756f2c Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sat, 4 Jun 2022 09:13:02 -0400 Subject: [PATCH 84/95] Alerting: Add provenance guard to config api (#50147) (#50209) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Alerting: add provenance guard to config api * add tests * only guard if config valid * adapt error message * simplify logic * rename arguments * make logic more straight forward * rename opt to options * remove useless maps (cherry picked from commit 4cc8c6f7459ed892e0c11e0aa2c561df2d79f34e) Co-authored-by: Jean-Philippe Quéméner --- pkg/services/ngalert/api/api_alertmanager.go | 10 +- .../ngalert/api/api_alertmanager_guards.go | 146 +++++ .../api/api_alertmanager_guards_test.go | 589 ++++++++++++++++++ 3 files changed, 744 insertions(+), 1 deletion(-) create mode 100644 pkg/services/ngalert/api/api_alertmanager_guards.go create mode 100644 pkg/services/ngalert/api/api_alertmanager_guards_test.go diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 26317fbbd34..1f9cdce877e 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -217,7 +217,15 @@ func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Respo } func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { - err := srv.mam.ApplyAlertmanagerConfiguration(c.Req.Context(), c.OrgId, body) + currentConfig, err := srv.mam.GetAlertmanagerConfiguration(c.Req.Context(), c.OrgId) + // If a config is present and valid we proceed with the guard, otherwise we + // just bypass the guard which is okay as we are anyway in an invalid state. + if err == nil { + if err := srv.provenanceGuard(currentConfig, body); err != nil { + return ErrResp(http.StatusBadRequest, err, "") + } + } + err = srv.mam.ApplyAlertmanagerConfiguration(c.Req.Context(), c.OrgId, body) if err == nil { return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) } diff --git a/pkg/services/ngalert/api/api_alertmanager_guards.go b/pkg/services/ngalert/api/api_alertmanager_guards.go new file mode 100644 index 00000000000..d619b22b8d5 --- /dev/null +++ b/pkg/services/ngalert/api/api_alertmanager_guards.go @@ -0,0 +1,146 @@ +package api + +import ( + "fmt" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/util/cmputil" + amConfig "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" +) + +func (srv AlertmanagerSrv) provenanceGuard(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { + if err := checkRoutes(currentConfig, newConfig); err != nil { + return err + } + if err := checkTemplates(currentConfig, newConfig); err != nil { + return err + } + if err := checkContactPoints(currentConfig.AlertmanagerConfig.Receivers, newConfig.AlertmanagerConfig.Receivers); err != nil { + return err + } + if err := checkMuteTimes(currentConfig, newConfig); err != nil { + return err + } + return nil +} + +func checkRoutes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { + reporter := cmputil.DiffReporter{} + options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(labels.Matcher{})} + routesEqual := cmp.Equal(currentConfig.AlertmanagerConfig.Route, newConfig.AlertmanagerConfig.Route, options...) + if !routesEqual && currentConfig.AlertmanagerConfig.Route.Provenance != ngmodels.ProvenanceNone { + return fmt.Errorf("policies were provisioned and cannot be changed through the UI") + } + return nil +} + +func checkTemplates(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { + for name, template := range currentConfig.TemplateFiles { + provenance := ngmodels.ProvenanceNone + if prov, present := currentConfig.TemplateFileProvenances[name]; present { + provenance = prov + } + if provenance == ngmodels.ProvenanceNone { + continue // we are only interested in non none + } + found := false + for newName, newTemplate := range newConfig.TemplateFiles { + if name != newName { + continue + } + found = true + if template != newTemplate { + return fmt.Errorf("cannot save provisioned template '%s'", name) + } + break // we found the template and we can proceed + } + if !found { + return fmt.Errorf("cannot delete provisioned template '%s'", name) + } + } + return nil +} + +func checkContactPoints(currReceivers []*apimodels.GettableApiReceiver, newReceivers []*apimodels.PostableApiReceiver) error { + newCPs := make(map[string]*apimodels.PostableGrafanaReceiver) + for _, postedReceiver := range newReceivers { + for _, postedContactPoint := range postedReceiver.GrafanaManagedReceivers { + newCPs[postedContactPoint.UID] = postedContactPoint + } + } + for _, existingReceiver := range currReceivers { + for _, contactPoint := range existingReceiver.GrafanaManagedReceivers { + if contactPoint.Provenance == ngmodels.ProvenanceNone { + continue // we are only interested in non none + } + postedContactPoint, present := newCPs[contactPoint.UID] + if !present { + return fmt.Errorf("cannot delete provisioned contact point '%s'", contactPoint.Name) + } + editErr := fmt.Errorf("cannot save provisioned contact point '%s'", contactPoint.Name) + if contactPoint.DisableResolveMessage != postedContactPoint.DisableResolveMessage { + return editErr + } + if contactPoint.Name != postedContactPoint.Name { + return editErr + } + if contactPoint.Type != postedContactPoint.Type { + return editErr + } + for key := range contactPoint.SecureFields { + if value, present := postedContactPoint.SecureSettings[key]; present && value != "" { + return editErr + } + } + existingSettings, err := contactPoint.Settings.Map() + if err != nil { + return err + } + newSettings, err := postedContactPoint.Settings.Map() + if err != nil { + return err + } + for key, val := range existingSettings { + if newVal, present := newSettings[key]; present { + if val != newVal { + return editErr + } + } else { + return editErr + } + } + } + } + return nil +} + +func checkMuteTimes(currentConfig apimodels.GettableUserConfig, newConfig apimodels.PostableUserConfig) error { + newMTs := make(map[string]amConfig.MuteTimeInterval) + for _, newMuteTime := range newConfig.AlertmanagerConfig.MuteTimeIntervals { + newMTs[newMuteTime.Name] = newMuteTime + } + for _, muteTime := range currentConfig.AlertmanagerConfig.MuteTimeIntervals { + provenance := ngmodels.ProvenanceNone + if prov, present := currentConfig.AlertmanagerConfig.MuteTimeProvenances[muteTime.Name]; present { + provenance = prov + } + if provenance == ngmodels.ProvenanceNone { + continue // we are only interested in non none + } + postedMT, present := newMTs[muteTime.Name] + if !present { + return fmt.Errorf("cannot delete provisioned mute time '%s'", muteTime.Name) + } + reporter := cmputil.DiffReporter{} + options := []cmp.Option{cmp.Reporter(&reporter), cmpopts.EquateEmpty()} + timesEqual := cmp.Equal(muteTime.TimeIntervals, postedMT.TimeIntervals, options...) + if !timesEqual { + return fmt.Errorf("cannot save provisioned mute time '%s'", muteTime.Name) + } + } + return nil +} diff --git a/pkg/services/ngalert/api/api_alertmanager_guards_test.go b/pkg/services/ngalert/api/api_alertmanager_guards_test.go new file mode 100644 index 00000000000..a87733d1c3a --- /dev/null +++ b/pkg/services/ngalert/api/api_alertmanager_guards_test.go @@ -0,0 +1,589 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" + amConfig "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/pkg/labels" + "github.com/prometheus/alertmanager/timeinterval" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" +) + +func TestCheckRoute(t *testing.T) { + tests := []struct { + name string + shouldErr bool + currentConfig definitions.GettableUserConfig + newConfig definitions.PostableUserConfig + }{ + { + name: "equal configs should not error", + shouldErr: false, + currentConfig: gettableRoute(t, models.ProvenanceAPI), + newConfig: postableRoute(t, models.ProvenanceAPI), + }, + { + name: "editing a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableRoute(t, models.ProvenanceNone), + newConfig: func() definitions.PostableUserConfig { + cfg := postableRoute(t, models.ProvenanceNone) + cfg.AlertmanagerConfig.Route.Matchers[0].Value = "123" + return cfg + }(), + }, + { + name: "editing a provisioned object should fail", + shouldErr: true, + currentConfig: gettableRoute(t, models.ProvenanceAPI), + newConfig: func() definitions.PostableUserConfig { + cfg := postableRoute(t, models.ProvenanceAPI) + cfg.AlertmanagerConfig.Route.Matchers[0].Value = "123" + return cfg + }(), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := checkRoutes(test.currentConfig, test.newConfig) + if test.shouldErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func gettableRoute(t *testing.T, provenance models.Provenance) definitions.GettableUserConfig { + t.Helper() + return definitions.GettableUserConfig{ + AlertmanagerConfig: definitions.GettableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Provenance: provenance, + Continue: true, + GroupBy: []model.LabelName{ + "...", + }, + Matchers: amConfig.Matchers{ + { + Name: "a", + Type: labels.MatchEqual, + Value: "b", + }, + }, + Routes: []*definitions.Route{ + { + Matchers: amConfig.Matchers{ + { + Name: "x", + Type: labels.MatchNotEqual, + Value: "y", + }, + }, + }, + }, + }, + }, + }, + } +} + +func postableRoute(t *testing.T, provenace models.Provenance) definitions.PostableUserConfig { + t.Helper() + return definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + Route: &definitions.Route{ + Provenance: provenace, + Continue: true, + GroupBy: []model.LabelName{ + "...", + }, + Matchers: amConfig.Matchers{ + { + Name: "a", + Type: labels.MatchEqual, + Value: "b", + }, + }, + Routes: []*definitions.Route{ + { + Matchers: amConfig.Matchers{ + { + Name: "x", + Type: labels.MatchNotEqual, + Value: "y", + }, + }, + }, + }, + }, + }, + }, + } +} + +func TestCheckTemplates(t *testing.T) { + tests := []struct { + name string + shouldErr bool + currentConfig definitions.GettableUserConfig + newConfig definitions.PostableUserConfig + }{ + { + name: "equal configs should not error", + shouldErr: false, + currentConfig: gettableTemplates(t, "test-1", models.ProvenanceAPI), + newConfig: postableTemplate(t, "test-1"), + }, + { + name: "removing a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableTemplates(t, "test-1", models.ProvenanceNone), + newConfig: definitions.PostableUserConfig{}, + }, + { + name: "removing a provisioned object should fail", + shouldErr: true, + currentConfig: gettableTemplates(t, "test-1", models.ProvenanceAPI), + newConfig: definitions.PostableUserConfig{}, + }, + { + name: "adding a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableTemplates(t, "test-1", models.ProvenanceAPI), + newConfig: postableTemplate(t, "test-1", "test-2"), + }, + { + name: "editing a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableTemplates(t, "test-1", models.ProvenanceNone), + newConfig: func() definitions.PostableUserConfig { + cfg := postableTemplate(t, "test-1") + cfg.TemplateFiles["test-1"] = "some updated value" + return cfg + }(), + }, + { + name: "editing a provisioned object should fail", + shouldErr: true, + currentConfig: gettableTemplates(t, "test-1", models.ProvenanceAPI), + newConfig: func() definitions.PostableUserConfig { + cfg := postableTemplate(t, "test-1") + cfg.TemplateFiles["test-1"] = "some updated value" + return cfg + }(), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := checkTemplates(test.currentConfig, test.newConfig) + if test.shouldErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func gettableTemplates(t *testing.T, name string, provenance models.Provenance) definitions.GettableUserConfig { + t.Helper() + return definitions.GettableUserConfig{ + TemplateFiles: map[string]string{ + name: "some-template", + }, + TemplateFileProvenances: map[string]models.Provenance{ + name: provenance, + }, + } +} + +func postableTemplate(t *testing.T, names ...string) definitions.PostableUserConfig { + t.Helper() + files := map[string]string{} + for _, name := range names { + files[name] = "some-template" + } + return definitions.PostableUserConfig{ + TemplateFiles: files, + } +} + +func TestCheckContactPoints(t *testing.T) { + tests := []struct { + name string + shouldErr bool + currentConfig []*definitions.GettableApiReceiver + newConfig []*definitions.PostableApiReceiver + }{ + { + name: "equal configs should not error", + shouldErr: false, + currentConfig: []*definitions.GettableApiReceiver{ + defaultGettableReceiver(t, "test-1", models.ProvenanceAPI), + }, + newConfig: []*definitions.PostableApiReceiver{ + defaultPostableReceiver(t, "test-1"), + }, + }, + { + name: "removing a non provisioned object should not fail", + shouldErr: false, + currentConfig: []*definitions.GettableApiReceiver{ + defaultGettableReceiver(t, "test-1", models.ProvenanceNone), + }, + newConfig: []*definitions.PostableApiReceiver{}, + }, + { + name: "removing a provisioned object should fail", + shouldErr: true, + currentConfig: []*definitions.GettableApiReceiver{ + defaultGettableReceiver(t, "test-1", models.ProvenanceAPI), + }, + newConfig: []*definitions.PostableApiReceiver{}, + }, + { + name: "adding a non provisioned object should not fail", + shouldErr: false, + currentConfig: []*definitions.GettableApiReceiver{ + defaultGettableReceiver(t, "test-1", models.ProvenanceAPI), + }, + newConfig: []*definitions.PostableApiReceiver{ + defaultPostableReceiver(t, "test-1"), + defaultPostableReceiver(t, "test-2"), + }, + }, + { + name: "editing a non provisioned object should not fail", + shouldErr: false, + currentConfig: []*definitions.GettableApiReceiver{ + defaultGettableReceiver(t, "test-1", models.ProvenanceNone), + }, + newConfig: []*definitions.PostableApiReceiver{ + func() *definitions.PostableApiReceiver { + receiver := defaultPostableReceiver(t, "test-1") + receiver.GrafanaManagedReceivers[0].SecureSettings = map[string]string{ + "url": "newUrl", + } + return receiver + }(), + }, + }, + { + name: "editing a provisioned object should fail", + shouldErr: true, + currentConfig: []*definitions.GettableApiReceiver{ + defaultGettableReceiver(t, "test-1", models.ProvenanceAPI), + }, + newConfig: []*definitions.PostableApiReceiver{ + func() *definitions.PostableApiReceiver { + receiver := defaultPostableReceiver(t, "test-1") + receiver.GrafanaManagedReceivers[0].SecureSettings = map[string]string{ + "url": "newUrl", + } + return receiver + }(), + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := checkContactPoints(test.currentConfig, test.newConfig) + if test.shouldErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func defaultGettableReceiver(t *testing.T, uid string, provenance models.Provenance) *definitions.GettableApiReceiver { + t.Helper() + return &definitions.GettableApiReceiver{ + GettableGrafanaReceivers: definitions.GettableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.GettableGrafanaReceiver{ + { + UID: "123", + Name: "yeah", + Type: "slack", + DisableResolveMessage: true, + Provenance: provenance, + SecureFields: map[string]bool{ + "url": true, + }, + Settings: simplejson.NewFromAny(map[string]interface{}{ + "hello": "world", + }), + }, + }, + }, + } +} + +func defaultPostableReceiver(t *testing.T, uid string) *definitions.PostableApiReceiver { + t.Helper() + return &definitions.PostableApiReceiver{ + PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{ + GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{ + { + UID: "123", + Name: "yeah", + Type: "slack", + DisableResolveMessage: true, + Settings: simplejson.NewFromAny(map[string]interface{}{ + "hello": "world", + }), + }, + }, + }, + } +} + +func TestCheckMuteTimes(t *testing.T) { + tests := []struct { + name string + shouldErr bool + currentConfig definitions.GettableUserConfig + newConfig definitions.PostableUserConfig + }{ + { + name: "equal configs should not error", + shouldErr: false, + currentConfig: gettableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + { + Name: "test-2", + TimeIntervals: defaultInterval(t), + }, + }, + map[string]models.Provenance{ + "test-1": models.ProvenanceNone, + }), + newConfig: postableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + { + Name: "test-2", + TimeIntervals: defaultInterval(t), + }, + }), + }, + { + name: "removing a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + }, + map[string]models.Provenance{ + "test-1": models.ProvenanceNone, + }), + newConfig: postableMuteIntervals(t, []amConfig.MuteTimeInterval{}), + }, + { + name: "removing a provisioned object should fail", + shouldErr: true, + currentConfig: gettableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + { + Name: "test-2", + TimeIntervals: defaultInterval(t), + }, + }, + map[string]models.Provenance{ + "test-1": models.ProvenanceAPI, + }), + newConfig: postableMuteIntervals(t, []amConfig.MuteTimeInterval{ + { + Name: "test-2", + TimeIntervals: defaultInterval(t), + }, + }), + }, + { + name: "adding a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + }, + map[string]models.Provenance{ + "test-1": models.ProvenanceNone, + }), + newConfig: postableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + { + Name: "test-2", + TimeIntervals: defaultInterval(t), + }, + }), + }, + { + name: "editing a non provisioned object should not fail", + shouldErr: false, + currentConfig: gettableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + }, + map[string]models.Provenance{ + "test-1": models.ProvenanceNone, + }), + newConfig: postableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: func() []timeinterval.TimeInterval { + intervals := defaultInterval(t) + intervals[0].Times = []timeinterval.TimeRange{ + { + StartMinute: 10, + EndMinute: 50, + }, + } + return intervals + }(), + }, + }), + }, + { + name: "editing a provisioned object should fail", + shouldErr: true, + currentConfig: gettableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: defaultInterval(t), + }, + }, + map[string]models.Provenance{ + "test-1": models.ProvenanceAPI, + }), + newConfig: postableMuteIntervals(t, + []amConfig.MuteTimeInterval{ + { + Name: "test-1", + TimeIntervals: func() []timeinterval.TimeInterval { + intervals := defaultInterval(t) + intervals[0].Times = []timeinterval.TimeRange{ + { + StartMinute: 10, + EndMinute: 50, + }, + } + return intervals + }(), + }, + }), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := checkMuteTimes(test.currentConfig, test.newConfig) + if test.shouldErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func gettableMuteIntervals(t *testing.T, muteTimeIntervals []amConfig.MuteTimeInterval, provenances map[string]models.Provenance) definitions.GettableUserConfig { + return definitions.GettableUserConfig{ + AlertmanagerConfig: definitions.GettableApiAlertingConfig{ + MuteTimeProvenances: provenances, + Config: definitions.Config{ + MuteTimeIntervals: muteTimeIntervals, + }, + }, + } +} + +func postableMuteIntervals(t *testing.T, muteTimeIntervals []amConfig.MuteTimeInterval) definitions.PostableUserConfig { + t.Helper() + return definitions.PostableUserConfig{ + AlertmanagerConfig: definitions.PostableApiAlertingConfig{ + Config: definitions.Config{ + MuteTimeIntervals: muteTimeIntervals, + }, + }, + } +} + +func defaultInterval(t *testing.T) []timeinterval.TimeInterval { + t.Helper() + return []timeinterval.TimeInterval{ + { + Years: []timeinterval.YearRange{ + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 2002, + End: 2008, + }, + }, + }, + Times: []timeinterval.TimeRange{ + { + StartMinute: 10, + EndMinute: 40, + }, + }, + Weekdays: []timeinterval.WeekdayRange{ + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 5, + }, + }, + }, + DaysOfMonth: []timeinterval.DayOfMonthRange{ + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 20, + }, + }, + }, + Months: []timeinterval.MonthRange{ + { + InclusiveRange: timeinterval.InclusiveRange{ + Begin: 1, + End: 6, + }, + }, + }, + }, + } +} From 22673382b5e5605086887d3bc43f6bbbb4206731 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sun, 5 Jun 2022 02:03:47 -0400 Subject: [PATCH 85/95] Alerting: remove feature toggle for provisioning API (#50167) (#50213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Alerting: remove feature toggle for provisioning API * remove missed code parts * remove unused import * remove empty line * mark routes as stable (cherry picked from commit 4b8a4449ed0ec5632ba9bf207600ba458c85247f) Co-authored-by: Jean-Philippe Quéméner --- .../src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 5 -- pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/ngalert/api/api.go | 19 ++-- .../ngalert/api/api_alertmanager_test.go | 4 - .../definitions/provisioning_alert_rules.go | 10 +-- .../definitions/provisioning_contactpoints.go | 8 +- .../definitions/provisioning_mute_timings.go | 12 +-- .../definitions/provisioning_policies.go | 4 +- .../definitions/provisioning_templates.go | 8 +- pkg/services/ngalert/api/tooling/post.json | 42 +++++---- pkg/services/ngalert/api/tooling/spec.json | 88 +++++++++++++------ .../ngalert/notifier/alertmanager_config.go | 9 +- pkg/services/ngalert/tests/util.go | 5 -- .../api/alerting/api_provisioning_test.go | 2 - 15 files changed, 117 insertions(+), 104 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 1d6dbe5f7a6..50fd56fa9ff 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -44,7 +44,6 @@ export interface FeatureToggles { annotationComments?: boolean; migrationLocking?: boolean; storage?: boolean; - alertProvisioning?: boolean; export?: boolean; storageLocalUpload?: boolean; azureMonitorResourcePickerForMetrics?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3182c53c179..73e59f13bea 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -158,11 +158,6 @@ var ( Description: "Configurable storage for dashboards, datasources, and resources", State: FeatureStateAlpha, }, - { - Name: "alertProvisioning", - Description: "Provisioning-friendly routes for alerting", - State: FeatureStateAlpha, - }, { Name: "export", Description: "Export grafana instance (to git, etc)", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 13ad09f4067..95d0cc121fe 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -119,10 +119,6 @@ const ( // Configurable storage for dashboards, datasources, and resources FlagStorage = "storage" - // FlagAlertProvisioning - // Provisioning-friendly routes for alerting - FlagAlertProvisioning = "alertProvisioning" - // FlagExport // Export grafana instance (to git, etc) FlagExport = "export" diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index feff31c52ab..57cb59e470b 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -136,14 +135,12 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { }, ), m) - if api.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAlertProvisioning) { - api.RegisterProvisioningApiEndpoints(NewForkedProvisioningApi(&ProvisioningSrv{ - log: logger, - policies: api.Policies, - contactPointService: api.ContactPointService, - templates: api.Templates, - muteTimings: api.MuteTimings, - alertRules: api.AlertRules, - }), m) - } + api.RegisterProvisioningApiEndpoints(NewForkedProvisioningApi(&ProvisioningSrv{ + log: logger, + policies: api.Policies, + contactPointService: api.ContactPointService, + templates: api.Templates, + muteTimings: api.MuteTimings, + alertRules: api.AlertRules, + }), m) } diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 304ce1e0ffd..2585130d9ca 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -18,7 +18,6 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/featuremgmt" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/metrics" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -531,9 +530,6 @@ func createMultiOrgAlertmanager(t *testing.T) *notifier.MultiOrgAlertmanager { DefaultConfiguration: setting.GetAlertmanagerDefaultConfiguration(), DisabledOrgs: map[int64]struct{}{5: {}}, }, // do not poll in tests. - IsFeatureToggleEnabled: func(key string) bool { - return key == featuremgmt.FlagAlertProvisioning - }, } mam, err := notifier.NewMultiOrgAlertmanager(cfg, &configStore, &orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService) diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go index 9308923fa18..7e6de39c079 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) -// swagger:route GET /api/v1/provisioning/alert-rules/{UID} provisioning RouteGetAlertRule +// swagger:route GET /api/v1/provisioning/alert-rules/{UID} provisioning stable RouteGetAlertRule // // Get a specific alert rule by UID. // @@ -14,7 +14,7 @@ import ( // 200: AlertRule // 400: ValidationError -// swagger:route POST /api/v1/provisioning/alert-rules provisioning RoutePostAlertRule +// swagger:route POST /api/v1/provisioning/alert-rules provisioning stable RoutePostAlertRule // // Create a new alert rule. // @@ -22,7 +22,7 @@ import ( // 201: AlertRule // 400: ValidationError -// swagger:route PUT /api/v1/provisioning/alert-rules/{UID} provisioning RoutePutAlertRule +// swagger:route PUT /api/v1/provisioning/alert-rules/{UID} provisioning stable RoutePutAlertRule // // Update an existing alert rule. // @@ -33,7 +33,7 @@ import ( // 200: AlertRule // 400: ValidationError -// swagger:route DELETE /api/v1/provisioning/alert-rules/{UID} provisioning RouteDeleteAlertRule +// swagger:route DELETE /api/v1/provisioning/alert-rules/{UID} provisioning stable RouteDeleteAlertRule // // Delete a specific alert rule by UID. // @@ -110,7 +110,7 @@ func NewAlertRule(rule models.AlertRule, provenance models.Provenance) AlertRule } } -// swagger:route PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning RoutePutAlertRuleGroup +// swagger:route PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning stable RoutePutAlertRuleGroup // // Update the interval of a rule group. // diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go index e29ea1c0621..dbb1f502eb0 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_contactpoints.go @@ -7,7 +7,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" ) -// swagger:route GET /api/v1/provisioning/contact-points provisioning RouteGetContactpoints +// swagger:route GET /api/v1/provisioning/contact-points provisioning stable RouteGetContactpoints // // Get all the contact points. // @@ -15,7 +15,7 @@ import ( // 200: Route // 400: ValidationError -// swagger:route POST /api/v1/provisioning/contact-points provisioning RoutePostContactpoints +// swagger:route POST /api/v1/provisioning/contact-points provisioning stable RoutePostContactpoints // // Create a contact point. // @@ -26,7 +26,7 @@ import ( // 202: Ack // 400: ValidationError -// swagger:route PUT /api/v1/provisioning/contact-points/{UID} provisioning RoutePutContactpoint +// swagger:route PUT /api/v1/provisioning/contact-points/{UID} provisioning stable RoutePutContactpoint // // Update an existing contact point. // @@ -37,7 +37,7 @@ import ( // 202: Ack // 400: ValidationError -// swagger:route DELETE /api/v1/provisioning/contact-points/{UID} provisioning RouteDeleteContactpoints +// swagger:route DELETE /api/v1/provisioning/contact-points/{UID} provisioning stable RouteDeleteContactpoints // // Delete a contact point. // diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go index 84794e0805f..72ad6efca88 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_mute_timings.go @@ -5,7 +5,7 @@ import ( "github.com/prometheus/alertmanager/config" ) -// swagger:route GET /api/v1/provisioning/mute-timings provisioning RouteGetMuteTimings +// swagger:route GET /api/v1/provisioning/mute-timings provisioning stable RouteGetMuteTimings // // Get all the mute timings. // @@ -13,7 +13,7 @@ import ( // 200: MuteTimings // 400: ValidationError -// swagger:route GET /api/v1/provisioning/mute-timings/{name} provisioning RouteGetMuteTiming +// swagger:route GET /api/v1/provisioning/mute-timings/{name} provisioning stable RouteGetMuteTiming // // Get a mute timing. // @@ -21,7 +21,7 @@ import ( // 200: MuteTimeInterval // 400: ValidationError -// swagger:route POST /api/v1/provisioning/mute-timings provisioning RoutePostMuteTiming +// swagger:route POST /api/v1/provisioning/mute-timings provisioning stable RoutePostMuteTiming // // Create a new mute timing. // @@ -32,7 +32,7 @@ import ( // 201: MuteTimeInterval // 400: ValidationError -// swagger:route PUT /api/v1/provisioning/mute-timings/{name} provisioning RoutePutMuteTiming +// swagger:route PUT /api/v1/provisioning/mute-timings/{name} provisioning stable RoutePutMuteTiming // // Replace an existing mute timing. // @@ -43,7 +43,7 @@ import ( // 200: MuteTimeInterval // 400: ValidationError -// swagger:route DELETE /api/v1/provisioning/mute-timings/{name} provisioning RouteDeleteMuteTiming +// swagger:route DELETE /api/v1/provisioning/mute-timings/{name} provisioning stable RouteDeleteMuteTiming // // Delete a mute timing. // @@ -55,7 +55,7 @@ import ( // swagger:model type MuteTimings []MuteTimeInterval -// swagger:parameters RouteGetTemplate RouteGetMuteTiming RoutePutMuteTiming RouteDeleteMuteTiming +// swagger:parameters RouteGetTemplate RouteGetMuteTiming RoutePutMuteTiming stable RouteDeleteMuteTiming type RouteGetMuteTimingParam struct { // Template Name // in:path diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go index 3c5bbbf4532..c6d21784b09 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go @@ -1,6 +1,6 @@ package definitions -// swagger:route GET /api/v1/provisioning/policies provisioning RouteGetPolicyTree +// swagger:route GET /api/v1/provisioning/policies provisioning stable RouteGetPolicyTree // // Get the notification policy tree. // @@ -8,7 +8,7 @@ package definitions // 200: Route // 400: ValidationError -// swagger:route PUT /api/v1/provisioning/policies provisioning RoutePutPolicyTree +// swagger:route PUT /api/v1/provisioning/policies provisioning stable RoutePutPolicyTree // // Sets the notification policy tree. // diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go index c9e594e23bd..fcbc2d80203 100644 --- a/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_templates.go @@ -4,7 +4,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" ) -// swagger:route GET /api/v1/provisioning/templates provisioning RouteGetTemplates +// swagger:route GET /api/v1/provisioning/templates provisioning stable RouteGetTemplates // // Get all message templates. // @@ -12,7 +12,7 @@ import ( // 200: MessageTemplate // 400: ValidationError -// swagger:route GET /api/v1/provisioning/templates/{name} provisioning RouteGetTemplate +// swagger:route GET /api/v1/provisioning/templates/{name} provisioning stable RouteGetTemplate // // Get a message template. // @@ -20,7 +20,7 @@ import ( // 200: MessageTemplate // 404: NotFound -// swagger:route PUT /api/v1/provisioning/templates/{name} provisioning RoutePutTemplate +// swagger:route PUT /api/v1/provisioning/templates/{name} provisioning stable RoutePutTemplate // // Updates an existing template. // @@ -31,7 +31,7 @@ import ( // 202: Ack // 400: ValidationError -// swagger:route DELETE /api/v1/provisioning/templates/{name} provisioning RouteDeleteTemplate +// swagger:route DELETE /api/v1/provisioning/templates/{name} provisioning stable RouteDeleteTemplate // // Delete a template. // diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index 150845a1854..7fecceb91ce 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -813,6 +813,13 @@ "type": "array", "x-go-name": "InhibitRules" }, + "muteTimeProvenances": { + "additionalProperties": { + "$ref": "#/definitions/Provenance" + }, + "type": "object", + "x-go-name": "MuteTimeProvenances" + }, "mute_time_intervals": { "items": { "$ref": "#/definitions/MuteTimeInterval" @@ -3248,6 +3255,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { + "description": "AlertGroup alert group", "properties": { "alerts": { "description": "alerts", @@ -3269,9 +3277,7 @@ "labels", "receiver" ], - "type": "object", - "x-go-name": "AlertGroup", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "alertGroups": { "description": "AlertGroups alert groups", @@ -3398,7 +3404,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3457,18 +3462,18 @@ "status", "updatedAt" ], - "type": "object" + "type": "object", + "x-go-name": "GettableAlert", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "items": { "$ref": "#/definitions/gettableAlert" }, - "type": "array", - "x-go-name": "GettableAlerts", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "array" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3520,14 +3525,17 @@ "status", "updatedAt" ], - "type": "object" + "type": "object", + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "items": { "$ref": "#/definitions/gettableSilence" }, - "type": "array" + "type": "array", + "x-go-name": "GettableSilences", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "labelSet": { "additionalProperties": { @@ -3656,7 +3664,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3696,10 +3703,11 @@ "matchers", "startsAt" ], - "type": "object" + "type": "object", + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "receiver": { - "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -3710,7 +3718,9 @@ "required": [ "name" ], - "type": "object" + "type": "object", + "x-go-name": "Receiver", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "silence": { "description": "Silence silence", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index 0b90d7fbeb9..ee3e57b7a13 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1670,7 +1670,8 @@ "/api/v1/provisioning/alert-rules": { "post": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Create a new alert rule.", "operationId": "RoutePostAlertRule", @@ -1702,7 +1703,8 @@ "/api/v1/provisioning/alert-rules/{UID}": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get a specific alert rule by UID.", "operationId": "RouteGetAlertRule", @@ -1734,7 +1736,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Update an existing alert rule.", "operationId": "RoutePutAlertRule", @@ -1770,7 +1773,8 @@ }, "delete": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Delete a specific alert rule by UID.", "operationId": "RouteDeleteAlertRule", @@ -1798,7 +1802,8 @@ "/api/v1/provisioning/contact-points": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get all the contact points.", "operationId": "RouteGetContactpoints", @@ -1822,7 +1827,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Create a contact point.", "operationId": "RoutePostContactpoints", @@ -1857,7 +1863,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Update an existing contact point.", "operationId": "RoutePutContactpoint", @@ -1897,7 +1904,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Delete a contact point.", "operationId": "RouteDeleteContactpoints", @@ -1932,7 +1940,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Update the interval of a rule group.", "operationId": "RoutePutAlertRuleGroup", @@ -1976,7 +1985,8 @@ "/api/v1/provisioning/mute-timings": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get all the mute timings.", "operationId": "RouteGetMuteTimings", @@ -2000,7 +2010,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Create a new mute timing.", "operationId": "RoutePostMuteTiming", @@ -2032,7 +2043,8 @@ "/api/v1/provisioning/mute-timings/{name}": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get a mute timing.", "operationId": "RouteGetMuteTiming", @@ -2066,7 +2078,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Replace an existing mute timing.", "operationId": "RoutePutMuteTiming", @@ -2104,7 +2117,8 @@ }, "delete": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Delete a mute timing.", "operationId": "RouteDeleteMuteTiming", @@ -2131,7 +2145,8 @@ "/api/v1/provisioning/policies": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get the notification policy tree.", "operationId": "RouteGetPolicyTree", @@ -2155,7 +2170,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Sets the notification policy tree.", "operationId": "RoutePutPolicyTree", @@ -2187,7 +2203,8 @@ "/api/v1/provisioning/templates": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get all message templates.", "operationId": "RouteGetTemplates", @@ -2210,7 +2227,8 @@ "/api/v1/provisioning/templates/{name}": { "get": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Get a message template.", "operationId": "RouteGetTemplate", @@ -2244,7 +2262,8 @@ "application/json" ], "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Updates an existing template.", "operationId": "RoutePutTemplate", @@ -2282,7 +2301,8 @@ }, "delete": { "tags": [ - "provisioning" + "provisioning", + "stable" ], "summary": "Delete a template.", "operationId": "RouteDeleteTemplate", @@ -3192,6 +3212,13 @@ }, "x-go-name": "InhibitRules" }, + "muteTimeProvenances": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Provenance" + }, + "x-go-name": "MuteTimeProvenances" + }, "mute_time_intervals": { "type": "array", "items": { @@ -5627,6 +5654,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "alertGroup": { + "description": "AlertGroup alert group", "type": "object", "required": [ "alerts", @@ -5649,8 +5677,6 @@ "$ref": "#/definitions/receiver" } }, - "x-go-name": "AlertGroup", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroup" }, "alertGroups": { @@ -5779,7 +5805,6 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { - "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -5839,19 +5864,19 @@ "x-go-name": "UpdatedAt" } }, + "x-go-name": "GettableAlert", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { + "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" }, - "x-go-name": "GettableAlerts", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5904,14 +5929,17 @@ "x-go-name": "UpdatedAt" } }, + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" }, + "x-go-name": "GettableSilences", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilences" }, "labelSet": { @@ -6041,7 +6069,6 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { - "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -6082,10 +6109,11 @@ "x-go-name": "StartsAt" } }, + "x-go-name": "PostableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/postableSilence" }, "receiver": { - "description": "Receiver receiver", "type": "object", "required": [ "name" @@ -6097,6 +6125,8 @@ "x-go-name": "Name" } }, + "x-go-name": "Receiver", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/receiver" }, "silence": { diff --git a/pkg/services/ngalert/notifier/alertmanager_config.go b/pkg/services/ngalert/notifier/alertmanager_config.go index 9491cf5a0ef..e564813f151 100644 --- a/pkg/services/ngalert/notifier/alertmanager_config.go +++ b/pkg/services/ngalert/notifier/alertmanager_config.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/store" @@ -82,11 +81,9 @@ func (moa *MultiOrgAlertmanager) GetAlertmanagerConfiguration(ctx context.Contex result.AlertmanagerConfig.Receivers = append(result.AlertmanagerConfig.Receivers, &gettableApiReceiver) } - if moa.settings.IsFeatureToggleEnabled(featuremgmt.FlagAlertProvisioning) { - result, err = moa.mergeProvenance(ctx, result, org) - if err != nil { - return definitions.GettableUserConfig{}, err - } + result, err = moa.mergeProvenance(ctx, result, org) + if err != nil { + return definitions.GettableUserConfig{}, err } return result, nil diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 9d1694a4d22..4c5a9433349 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -40,11 +40,6 @@ func SetupTestEnv(t *testing.T, baseInterval time.Duration) (*ngalert.AlertNG, * cfg.UnifiedAlerting.Enabled = new(bool) *cfg.UnifiedAlerting.Enabled = true - cfg.IsFeatureToggleEnabled = func(key string) bool { - // Enable alert provisioning FF when running tests. - return key == featuremgmt.FlagAlertProvisioning - } - m := metrics.NewNGAlert(prometheus.NewRegistry()) sqlStore := sqlstore.InitTestDB(t) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) diff --git a/pkg/tests/api/alerting/api_provisioning_test.go b/pkg/tests/api/alerting/api_provisioning_test.go index a391c9a3154..197dfa4d57f 100644 --- a/pkg/tests/api/alerting/api_provisioning_test.go +++ b/pkg/tests/api/alerting/api_provisioning_test.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/tests/testinfra" "github.com/stretchr/testify/require" ) @@ -23,7 +22,6 @@ func TestProvisioning(t *testing.T) { EnableUnifiedAlerting: true, DisableAnonymous: true, AppModeProduction: true, - EnableFeatureToggles: []string{featuremgmt.FlagAlertProvisioning}, }) grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) From 7bcbf45c661c804774c6aef6557668e5da0358cb Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sun, 5 Jun 2022 18:19:41 -0400 Subject: [PATCH 86/95] SearchV2: explicit dashboard loading order and cleanups (#50210) (#50217) (cherry picked from commit da49f907bb95cc876166dae919321ec1f993b3c1) Co-authored-by: Alexander Emelin --- pkg/services/searchV2/bluge.go | 17 +---------------- pkg/services/searchV2/index.go | 5 +---- .../features/search/page/components/columns.tsx | 3 ++- .../grafana/components/QueryEditor.tsx | 3 ++- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/pkg/services/searchV2/bluge.go b/pkg/services/searchV2/bluge.go index 2cb83549a44..3736ac333bf 100644 --- a/pkg/services/searchV2/bluge.go +++ b/pkg/services/searchV2/bluge.go @@ -120,14 +120,7 @@ func initIndex(dashboards []dashboard, logger log.Logger, extendDoc ExtendDashbo if err := flushIfRequired(true); err != nil { return nil, nil, err } - logger.Info("Finish inserting docs into batch", "elapsed", time.Since(label)) - label = time.Now() - - err = writer.Batch(batch) - if err != nil { - return nil, nil, err - } - logger.Info("Finish writing batch", "elapsed", time.Since(label)) + logger.Info("Finish inserting docs into index", "elapsed", time.Since(label)) reader, err := writer.Reader() if err != nil { @@ -470,9 +463,6 @@ func doSearchQuery( return response } - dvfieldNames := []string{"type"} - sctx := search.NewSearchContext(0, 0) - fScore := data.NewFieldFromFieldType(data.FieldTypeFloat64, 0) fUID := data.NewFieldFromFieldType(data.FieldTypeString, 0) fKind := data.NewFieldFromFieldType(data.FieldTypeString, 0) @@ -517,11 +507,6 @@ func doSearchQuery( // iterate through the document matches match, err := documentMatchIterator.Next() for err == nil && match != nil { - err = match.LoadDocumentValues(sctx, dvfieldNames) - if err != nil { - continue - } - uid := "" kind := "" ptype := "" diff --git a/pkg/services/searchV2/index.go b/pkg/services/searchV2/index.go index 9e5520e654b..0fc19d3bd44 100644 --- a/pkg/services/searchV2/index.go +++ b/pkg/services/searchV2/index.go @@ -567,6 +567,7 @@ func (l sqlDashboardLoader) LoadDashboards(ctx context.Context, orgID int64, das sess.Cols("id", "uid", "is_folder", "folder_id", "data", "slug", "created", "updated") + sess.OrderBy("id ASC") sess.Limit(limit) return sess.Find(&rows) @@ -607,10 +608,6 @@ func newFolderIDLookup(sql *sqlstore.SQLStore) folderUIDLookup { return func(ctx context.Context, folderID int64) (string, error) { uid := "" err := sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { - sess.Table("dashboard"). - Where("id = ?", folderID). - Cols("uid") - res, err := sess.Query("SELECT uid FROM dashboard WHERE id=?", folderID) if err != nil { return err diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index 5c35e6b7432..2e80a72d4ac 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -6,6 +6,7 @@ import SVG from 'react-inlinesvg'; import { Field, getFieldDisplayName } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { Checkbox, Icon, IconButton, IconName, TagList } from '@grafana/ui'; +import { PluginIconName } from 'app/features/plugins/admin/types'; import { QueryResponse, SearchResultMeta } from '../../service'; import { SelectionChecker, SelectionToggle } from '../selection'; @@ -287,7 +288,7 @@ function makeTypeColumn( break; case 'panel': - icon = 'public/img/icons/mono/library-panel.svg'; + icon = `public/img/icons/unicons/${PluginIconName.panel}.svg`; const type = typeField.values.get(i); if (type) { txt = type; diff --git a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx index a561c21eb63..668cb94b65c 100644 --- a/public/app/plugins/datasource/grafana/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/grafana/components/QueryEditor.tsx @@ -10,6 +10,7 @@ import { } from '@grafana/data'; import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; import { InlineField, Select, Alert, Input, InlineFieldRow, CodeEditor } from '@grafana/ui'; +import { hasAlphaPanels } from 'app/core/config'; import { SearchQuery } from 'app/features/search/service'; import { GrafanaDatasource } from '../datasource'; @@ -49,7 +50,7 @@ export class QueryEditor extends PureComponent { constructor(props: Props) { super(props); - if (config.featureToggles.panelTitleSearch) { + if (config.featureToggles.panelTitleSearch && hasAlphaPanels) { this.queryTypes.push({ label: 'Search', value: GrafanaQueryType.Search, From 4f284f167e5c93c61c3e29d99cd643038a4945d2 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sun, 5 Jun 2022 21:11:45 -0400 Subject: [PATCH 87/95] Prometheus: Migrate metadata queries to use resource calls (#49921) (#50219) --- pkg/tsdb/prometheus/client/client.go | 31 ++++--- pkg/tsdb/prometheus/prometheus.go | 29 +++++++ pkg/tsdb/prometheus/resource/resource.go | 85 +++++++++++++++++++ .../datasource/prometheus/datasource.test.ts | 11 +-- .../datasource/prometheus/datasource.tsx | 33 +++++-- .../prometheus/metric_find_query.test.ts | 20 +++-- 6 files changed, 181 insertions(+), 28 deletions(-) create mode 100644 pkg/tsdb/prometheus/resource/resource.go diff --git a/pkg/tsdb/prometheus/client/client.go b/pkg/tsdb/prometheus/client/client.go index 23292ef41f2..500125eb1ed 100644 --- a/pkg/tsdb/prometheus/client/client.go +++ b/pkg/tsdb/prometheus/client/client.go @@ -42,7 +42,7 @@ func (c *Client) QueryRange(ctx context.Context, q *models.Query) (*http.Respons qs.Set("end", formatTime(tr.End)) qs.Set("step", strconv.FormatFloat(tr.Step.Seconds(), 'f', -1, 64)) - return c.fetch(ctx, u, qs) + return c.fetch(ctx, c.method, u, qs) } func (c *Client) QueryInstant(ctx context.Context, q *models.Query) (*http.Response, error) { @@ -60,7 +60,7 @@ func (c *Client) QueryInstant(ctx context.Context, q *models.Query) (*http.Respo qs.Set("time", formatTime(tr.End)) } - return c.fetch(ctx, u, qs) + return c.fetch(ctx, c.method, u, qs) } func (c *Client) QueryExemplars(ctx context.Context, q *models.Query) (*http.Response, error) { @@ -77,20 +77,31 @@ func (c *Client) QueryExemplars(ctx context.Context, q *models.Query) (*http.Res qs.Set("start", formatTime(tr.Start)) qs.Set("end", formatTime(tr.End)) - return c.fetch(ctx, u, qs) + return c.fetch(ctx, c.method, u, qs) } -func (c *Client) fetch(ctx context.Context, u *url.URL, qs url.Values) (*http.Response, error) { - if strings.ToUpper(c.method) == http.MethodGet { - u.RawQuery = qs.Encode() - } - - r, err := http.NewRequestWithContext(ctx, c.method, u.String(), nil) +func (c *Client) QueryResource(ctx context.Context, method string, p string, qs url.Values) (*http.Response, error) { + u, err := url.ParseRequestURI(c.baseUrl) if err != nil { return nil, err } - if strings.ToUpper(c.method) == http.MethodPost { + u.Path = path.Join(u.Path, p) + + return c.fetch(ctx, method, u, qs) +} + +func (c *Client) fetch(ctx context.Context, method string, u *url.URL, qs url.Values) (*http.Response, error) { + if strings.ToUpper(method) == http.MethodGet { + u.RawQuery = qs.Encode() + } + + r, err := http.NewRequestWithContext(ctx, method, u.String(), nil) + if err != nil { + return nil, err + } + + if strings.ToUpper(method) == http.MethodPost { r.Body = ioutil.NopCloser(strings.NewReader(qs.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index d4e0362c220..310cb5a69a5 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/prometheus/buffered" "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata" + "github.com/grafana/grafana/pkg/tsdb/prometheus/resource" apiv1 "github.com/prometheus/client_golang/api/prometheus/v1" ) @@ -29,6 +30,7 @@ type Service struct { type instance struct { buffered *buffered.Buffered queryData *querydata.QueryData + resource *resource.Resource } func ProvideService(httpClientProvider httpclient.Provider, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) *Service { @@ -57,9 +59,15 @@ func newInstanceSettings(httpClientProvider httpclient.Provider, cfg *setting.Cf return nil, err } + r, err := resource.New(httpClientProvider, cfg, features, settings, plog) + if err != nil { + return nil, err + } + return instance{ buffered: b, queryData: qd, + resource: r, }, nil } } @@ -81,6 +89,27 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return i.buffered.ExecuteTimeSeriesQuery(ctx, req) } +func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + i, err := s.getInstance(req.PluginContext) + if err != nil { + return err + } + + statusCode, bytes, err := i.resource.Execute(ctx, req) + body := bytes + if err != nil { + body = []byte(err.Error()) + } + + return sender.Send(&backend.CallResourceResponse{ + Status: statusCode, + Headers: map[string][]string{ + "content-type": {"application/json"}, + }, + Body: body, + }) +} + func (s *Service) getInstance(pluginCtx backend.PluginContext) (*instance, error) { i, err := s.im.Get(pluginCtx) if err != nil { diff --git a/pkg/tsdb/prometheus/resource/resource.go b/pkg/tsdb/prometheus/resource/resource.go new file mode 100644 index 00000000000..9d9ca4a2154 --- /dev/null +++ b/pkg/tsdb/prometheus/resource/resource.go @@ -0,0 +1,85 @@ +package resource + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/infra/httpclient" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tsdb/prometheus/client" +) + +type Resource struct { + provider *client.Provider + log log.Logger +} + +func New( + httpClientProvider httpclient.Provider, + cfg *setting.Cfg, + features featuremgmt.FeatureToggles, + settings backend.DataSourceInstanceSettings, + plog log.Logger, +) (*Resource, error) { + var jsonData map[string]interface{} + if err := json.Unmarshal(settings.JSONData, &jsonData); err != nil { + return nil, fmt.Errorf("error reading settings: %w", err) + } + + p := client.NewProvider(settings, jsonData, httpClientProvider, cfg, features, plog) + + return &Resource{ + log: plog, + provider: p, + }, nil +} + +func (r *Resource) Execute(ctx context.Context, req *backend.CallResourceRequest) (int, []byte, error) { + client, err := r.provider.GetClient(reqHeaders(req.Headers)) + if err != nil { + return 500, nil, err + } + + return r.fetch(ctx, client, req) +} + +func (r *Resource) fetch(ctx context.Context, client *client.Client, req *backend.CallResourceRequest) (int, []byte, error) { + r.log.Debug("Sending resource query", "URL", req.URL) + u, err := url.Parse(req.URL) + if err != nil { + return 500, nil, err + } + + resp, err := client.QueryResource(ctx, req.Method, u.Path, u.Query()) + if err != nil { + return resp.StatusCode, nil, err + } + + defer resp.Body.Close() //nolint (we don't care about the error being returned by resp.Body.Close()) + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return 500, nil, err + } + + return resp.StatusCode, data, err +} + +func reqHeaders(headers map[string][]string) map[string]string { + // Keep only the authorization header, incase downstream the authorization header is required. + // Strip all the others out as appropriate headers will be applied to speak with prometheus. + h := make(map[string]string) + accessValues := headers["Authorization"] + + if len(accessValues) > 0 { + h["Authorization"] = accessValues[0] + } + + return h +} diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 6da512d0faa..d5705107cee 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -59,6 +59,7 @@ describe('PrometheusDatasource', () => { let ds: PrometheusDatasource; const instanceSettings = { url: 'proxied', + id: 1, directUrl: 'direct', user: 'test', password: 'mupp', @@ -149,7 +150,7 @@ describe('PrometheusDatasource', () => { it('added to metadata request', () => { promDs.metadataRequest('/foo'); expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); + expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/1/resources/foo?customQuery=123'); }); it('adds params to timeseries query', () => { @@ -184,13 +185,13 @@ describe('PrometheusDatasource', () => { it('added to metadata request with non-POST endpoint', () => { promDs.metadataRequest('/foo'); expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe('proxied/foo?customQuery=123'); + expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/1/resources/foo?customQuery=123'); }); it('added to metadata request with POST endpoint', () => { promDs.metadataRequest('/api/v1/labels'); expect(fetchMock.mock.calls.length).toBe(1); - expect(fetchMock.mock.calls[0][0].url).toBe('proxied/api/v1/labels'); + expect(fetchMock.mock.calls[0][0].url).toBe('/api/datasources/1/resources/api/v1/labels'); expect(fetchMock.mock.calls[0][0].data.customQuery).toBe('123'); }); @@ -431,7 +432,7 @@ describe('PrometheusDatasource', () => { }); }); - describe('Prometheus regular escaping', () => { + describe('Prometheus regular escaping', () => { it('should not escape non-string', () => { expect(prometheusRegularEscape(12)).toEqual(12); }); @@ -457,7 +458,7 @@ describe('PrometheusDatasource', () => { }); }); - describe('Prometheus regexes escaping', () => { + describe('Prometheus regexes escaping', () => { it('should not escape simple string', () => { expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression'); }); diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 6877bc011ae..4d93ca7a861 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -109,7 +109,7 @@ export class PrometheusDatasource this.withCredentials = instanceSettings.withCredentials; this.interval = instanceSettings.jsonData.timeInterval || '15s'; this.queryTimeout = instanceSettings.jsonData.queryTimeout; - this.httpMethod = instanceSettings.jsonData.httpMethod || 'POST'; + this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; // `directUrl` is never undefined, we set it at https://github.com/grafana/grafana/blob/main/pkg/api/frontendsettings.go#L108 // here we "fall back" to this.url to make typescript happy, but it should never happen this.directUrl = instanceSettings.jsonData.directUrl ?? this.url; @@ -165,8 +165,14 @@ export class PrometheusDatasource } } + let queryUrl = this.url + url; + if (url.startsWith(`/api/datasources/${this.id}`)) { + // This url is meant to be a replacement for the whole URL. Replace the entire URL + queryUrl = url; + } + const options: BackendSrvRequest = defaults(overrides, { - url: this.url + url, + url: queryUrl, method: this.httpMethod, headers: {}, }); @@ -209,10 +215,16 @@ export class PrometheusDatasource // If URL includes endpoint that supports POST and GET method, try to use configured method. This might fail as POST is supported only in v2.10+. if (GET_AND_POST_METADATA_ENDPOINTS.some((endpoint) => url.includes(endpoint))) { try { - return await lastValueFrom(this._request(url, params, { method: this.httpMethod, hideFromInspector: true })); + return await lastValueFrom( + this._request(`/api/datasources/${this.id}/resources${url}`, params, { + method: this.httpMethod, + hideFromInspector: true, + showErrorAlert: false, + }) + ); } catch (err) { // If status code of error is Method Not Allowed (405) and HTTP method is POST, retry with GET - if (this.httpMethod === 'POST' && err.status === 405) { + if (this.httpMethod === 'POST' && (err.status === 405 || err.status === 400)) { console.warn(`Couldn't use configured POST HTTP method for this request. Trying to use GET method instead.`); } else { throw err; @@ -220,7 +232,12 @@ export class PrometheusDatasource } } - return await lastValueFrom(this._request(url, params, { method: 'GET', hideFromInspector: true })); // toPromise until we change getTagValues, getTagKeys to Observable + return await lastValueFrom( + this._request(`/api/datasources/${this.id}/resources${url}`, params, { + method: 'GET', + hideFromInspector: true, + }) + ); // toPromise until we change getTagValues, getTagKeys to Observable } interpolateQueryExpr(value: string | string[] = [], variable: any) { @@ -995,7 +1012,11 @@ export class PrometheusDatasource async areExemplarsAvailable() { try { - const res = await this.metadataRequest('/api/v1/query_exemplars', { query: 'test' }); + const res = await this.getResource('/api/v1/query_exemplars', { + query: 'test', + start: dateTime().subtract(30, 'minutes').valueOf(), + end: dateTime().valueOf(), + }); if (res.data.status === 'success') { return true; } diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts index c13a538f9c6..8bb1e2ecf75 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.test.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.test.ts @@ -18,6 +18,7 @@ const fetchMock = jest.spyOn(backendSrv, 'fetch'); const instanceSettings = { url: 'proxied', + id: 1, directUrl: 'direct', user: 'test', password: 'mupp', @@ -75,8 +76,9 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `proxied/api/v1/labels?start=${raw.from.unix()}&end=${raw.to.unix()}`, + url: `/api/datasources/1/resources/api/v1/labels?start=${raw.from.unix()}&end=${raw.to.unix()}`, hideFromInspector: true, + showErrorAlert: false, headers: {}, }); }); @@ -94,7 +96,7 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `proxied/api/v1/label/resource/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, + url: `/api/datasources/1/resources/api/v1/label/resource/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, hideFromInspector: true, headers: {}, }); @@ -117,10 +119,11 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `proxied/api/v1/series?match${encodeURIComponent( + url: `/api/datasources/1/resources/api/v1/series?match${encodeURIComponent( '[]' )}=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, hideFromInspector: true, + showErrorAlert: false, headers: {}, }); }); @@ -142,8 +145,9 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: 'proxied/api/v1/series?match%5B%5D=metric%7Blabel1%3D%22foo%22%2C%20label2%3D%22bar%22%2C%20label3%3D%22baz%22%7D&start=1524650400&end=1524654000', + url: '/api/datasources/1/resources/api/v1/series?match%5B%5D=metric%7Blabel1%3D%22foo%22%2C%20label2%3D%22bar%22%2C%20label3%3D%22baz%22%7D&start=1524650400&end=1524654000', hideFromInspector: true, + showErrorAlert: false, headers: {}, }); }); @@ -167,10 +171,11 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `proxied/api/v1/series?match${encodeURIComponent( + url: `/api/datasources/1/resources/api/v1/series?match${encodeURIComponent( '[]' )}=metric&start=${raw.from.unix()}&end=${raw.to.unix()}`, hideFromInspector: true, + showErrorAlert: false, headers: {}, }); }); @@ -188,7 +193,7 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `proxied/api/v1/label/__name__/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, + url: `/api/datasources/1/resources/api/v1/label/__name__/values?start=${raw.from.unix()}&end=${raw.to.unix()}`, hideFromInspector: true, headers: {}, }); @@ -242,10 +247,11 @@ describe('PrometheusMetricFindQuery', () => { expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith({ method: 'GET', - url: `proxied/api/v1/series?match${encodeURIComponent('[]')}=${encodeURIComponent( + url: `/api/datasources/1/resources/api/v1/series?match${encodeURIComponent('[]')}=${encodeURIComponent( 'up{job="job1"}' )}&start=${raw.from.unix()}&end=${raw.to.unix()}`, hideFromInspector: true, + showErrorAlert: false, headers: {}, }); }); From bb801de9ed4b6e96ff2bb9ca07acfb38a65efcc6 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Sun, 5 Jun 2022 21:52:58 -0400 Subject: [PATCH 88/95] Prometheus: Fix resource call panic (#50216) (#50221) (cherry picked from commit 4aa5e7e69e767439c63e73c1d5a099700272351f) Co-authored-by: Todd Treece <360020+toddtreece@users.noreply.github.com> --- pkg/tsdb/prometheus/resource/resource.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/prometheus/resource/resource.go b/pkg/tsdb/prometheus/resource/resource.go index 9d9ca4a2154..5ff90575264 100644 --- a/pkg/tsdb/prometheus/resource/resource.go +++ b/pkg/tsdb/prometheus/resource/resource.go @@ -58,7 +58,11 @@ func (r *Resource) fetch(ctx context.Context, client *client.Client, req *backen resp, err := client.QueryResource(ctx, req.Method, u.Path, u.Query()) if err != nil { - return resp.StatusCode, nil, err + statusCode := 500 + if resp != nil { + statusCode = resp.StatusCode + } + return statusCode, nil, err } defer resp.Body.Close() //nolint (we don't care about the error being returned by resp.Body.Close()) From d929d745f1f2e394f00132f28c221244d23cc40d Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 6 Jun 2022 03:11:06 -0400 Subject: [PATCH 89/95] Chore: uPlot 1.6.21 (#50223) (#50224) (cherry picked from commit e3815111eaa3e86f4e19e480bd8ff4b482f876d1) Co-authored-by: Leon Sorokin --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-ui/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 0678edb2fd3..6cf62e008bf 100644 --- a/package.json +++ b/package.json @@ -387,7 +387,7 @@ "tether-drop": "https://github.com/torkelo/drop", "tinycolor2": "1.4.2", "tslib": "2.4.0", - "uplot": "1.6.20", + "uplot": "1.6.21", "uuid": "8.3.2", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 12f6319ac00..62ae5e48c34 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -38,7 +38,7 @@ "regenerator-runtime": "0.13.9", "rxjs": "7.5.5", "tslib": "2.4.0", - "uplot": "1.6.20", + "uplot": "1.6.21", "xss": "1.0.11" }, "devDependencies": { diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7d3ddc2317d..1ad6ca7be3c 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -90,7 +90,7 @@ "slate-plain-serializer": "0.7.10", "tinycolor2": "1.4.2", "tslib": "2.4.0", - "uplot": "1.6.20", + "uplot": "1.6.21", "uuid": "8.3.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index d8cce474e8e..c20af542def 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3908,7 +3908,7 @@ __metadata: tinycolor2: 1.4.2 tslib: 2.4.0 typescript: 4.6.4 - uplot: 1.6.20 + uplot: 1.6.21 xss: 1.0.11 languageName: unknown linkType: soft @@ -4353,7 +4353,7 @@ __metadata: ts-loader: 8.0.11 tslib: 2.4.0 typescript: 4.6.4 - uplot: 1.6.20 + uplot: 1.6.21 uuid: 8.3.2 webpack: 5.72.1 webpack-filter-warnings-plugin: 1.2.1 @@ -19990,7 +19990,7 @@ __metadata: ts-node: 10.7.0 tslib: 2.4.0 typescript: 4.6.4 - uplot: 1.6.20 + uplot: 1.6.21 uuid: 8.3.2 vendor: "link:./public/vendor" visjs-network: 4.25.0 @@ -34361,10 +34361,10 @@ __metadata: languageName: node linkType: hard -"uplot@npm:1.6.20": - version: 1.6.20 - resolution: "uplot@npm:1.6.20" - checksum: 17ddacefbba2b0db0e919ad5ed85f43ebe40afbfced442bc821adbf6d84dc4f969ebf5754f913b316acf17f24ca18c3ca971181a530ea1fc91f45763f25cddaf +"uplot@npm:1.6.21": + version: 1.6.21 + resolution: "uplot@npm:1.6.21" + checksum: 38aa8c899f5010ce6a340db2717ef14e50e49a227a64be225ecba5a5d64bbda4d84a29a6a93ce77aa19a68f0631beaab8ba9ed6a456f11ce65160a6757b5b384 languageName: node linkType: hard From 0ab03dbf3aa2c2885111a3f0bd42b482249b83f1 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 6 Jun 2022 04:33:32 -0400 Subject: [PATCH 90/95] HeatmapNG: cell value filtering and color clamping (#50204) (#50225) Co-authored-by: Ryan McKinley (cherry picked from commit 8cdfef4796dab61ba20ef97d63bb5e10a8542c2a) Co-authored-by: Leon Sorokin --- .../panel-heatmap/heatmap-calculate-log.json | 14 +- .../core/components/ColorScale/ColorScale.tsx | 24 +-- .../calculateHeatmap/editor/AxisEditor.tsx | 5 - .../calculateHeatmap/heatmap.test.ts | 2 +- .../transformers/calculateHeatmap/heatmap.ts | 4 +- .../panel/heatmap-new/HeatmapPanel.tsx | 22 ++- .../app/plugins/panel/heatmap-new/fields.ts | 4 +- .../{heatmap.svg => icn-heatmap-panel.svg} | 0 .../panel/heatmap-new/migrations.test.ts | 19 ++- .../plugins/panel/heatmap-new/migrations.ts | 18 ++- .../plugins/panel/heatmap-new/models.gen.ts | 19 ++- .../app/plugins/panel/heatmap-new/module.tsx | 131 +++++++++++----- .../app/plugins/panel/heatmap-new/plugin.json | 6 +- public/app/plugins/panel/heatmap-new/utils.ts | 148 ++++++++++-------- 14 files changed, 265 insertions(+), 151 deletions(-) rename public/app/plugins/panel/heatmap-new/img/{heatmap.svg => icn-heatmap-panel.svg} (100%) diff --git a/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json b/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json index b330ce03596..5481b136d9d 100644 --- a/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json +++ b/devenv/dev-dashboards/panel-heatmap/heatmap-calculate-log.json @@ -23,7 +23,7 @@ }, "editable": true, "fiscalYearStartMonth": 0, - "graphTooltip": 0, + "graphTooltip": 1, "links": [], "liveNow": false, "panels": [ @@ -172,7 +172,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -257,7 +257,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -333,7 +333,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -417,7 +417,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -502,7 +502,7 @@ "color": "rgba(255,0,255,0.7)" }, "filterValues": { - "min": 1e-9 + "le": 1e-9 }, "legend": { "show": true @@ -548,6 +548,6 @@ "timezone": "", "title": "Heatmap calculate (log)", "uid": "ZXYQTA97ZZ", - "version": 4, + "version": 1, "weekStart": "" } diff --git a/public/app/core/components/ColorScale/ColorScale.tsx b/public/app/core/components/ColorScale/ColorScale.tsx index ba9ece2581d..6da245922ac 100644 --- a/public/app/core/components/ColorScale/ColorScale.tsx +++ b/public/app/core/components/ColorScale/ColorScale.tsx @@ -26,7 +26,7 @@ const GRADIENT_STOPS = 10; export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useStopsPercentage }: Props) => { const [colors, setColors] = useState([]); const [scaleHover, setScaleHover] = useState({ isShown: false, value: 0 }); - const [percent, setPercent] = useState(null); + const [percent, setPercent] = useState(null); // 0-100 for CSS percentage const theme = useTheme2(); const styles = getStyles(theme, colors); @@ -50,15 +50,12 @@ export const ColorScale = ({ colorPalette, min, max, display, hoverValue, useSto }; useEffect(() => { - if (hoverValue != null) { - const percent = hoverValue / (max - min); - setPercent(percent * 100); - } + setPercent(hoverValue == null ? null : clampPercent100((hoverValue - min) / (max - min))); }, [hoverValue, min, max]); return ( -
-
+
+
{display && (scaleHover.isShown || hoverValue !== undefined) && (
@@ -121,10 +118,19 @@ const getGradientStops = ({ return [...gradientStops]; }; +function clampPercent100(v: number) { + if (v > 1) { + return 100; + } + if (v < 0) { + return 0; + } + return v * 100; +} + const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({ scaleWrapper: css` width: 100%; - max-width: 300px; font-size: 11px; opacity: 1; `, @@ -138,7 +144,7 @@ const getStyles = (theme: GrafanaTheme2, colors: string[]) => ({ `, hoverValue: css` position: absolute; - padding-top: 5px; + padding-top: 4px; `, followerContainer: css` position: relative; diff --git a/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx b/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx index ca55a149b9e..64b45a5d894 100644 --- a/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx +++ b/public/app/features/transformers/calculateHeatmap/editor/AxisEditor.tsx @@ -24,11 +24,6 @@ const logModeOptions: Array> = [ value: HeatmapCalculationMode.Size, description: 'Split the buckets based on size', }, - { - label: 'Count', - value: HeatmapCalculationMode.Count, - description: 'Split the buckets based on count', - }, ]; export const AxisEditor: React.FC> = ({ diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.test.ts b/public/app/features/transformers/calculateHeatmap/heatmap.test.ts index 8eebf558ffa..160bc920ed8 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.test.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.test.ts @@ -58,7 +58,7 @@ describe('Heatmap transformer', () => { ], }); - const heatmap = bucketsToScanlines({ frame, name: 'Speed' }); + const heatmap = bucketsToScanlines({ frame, value: 'Speed' }); expect(heatmap.fields.map((f) => ({ name: f.name, type: f.type, config: f.config }))).toMatchInlineSnapshot(` Array [ Object { diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 6ef7275c076..62416f358bb 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -63,7 +63,7 @@ export function readHeatmapScanlinesCustomMeta(frame?: DataFrame): HeatmapScanli export interface BucketsOptions { frame: DataFrame; - name?: string; + value?: string; // the field value name layout?: HeatmapBucketLayout; } @@ -147,7 +147,7 @@ export function bucketsToScanlines(opts: BucketsOptions): DataFrame { }, }, { - name: opts.name?.length ? opts.name : 'Value', + name: opts.value?.length ? opts.value : 'Value', type: FieldType.number, values: new ArrayVector(counts2), config: yFields[0].config, diff --git a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx index 7516c203662..6d4e3ed1739 100644 --- a/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx +++ b/public/app/plugins/panel/heatmap-new/HeatmapPanel.tsx @@ -57,7 +57,7 @@ export const HeatmapPanel: React.FC = ({ let exemplarsyFacet: number[] = []; const meta = readHeatmapScanlinesCustomMeta(info.heatmap); - if (info.exemplars && meta.yMatchWithLabel) { + if (info.exemplars?.length && meta.yMatchWithLabel) { exemplarsXFacet = info.exemplars?.fields[0].values.toArray(); // ordinal/labeled heatmap-buckets? @@ -126,7 +126,10 @@ export const HeatmapPanel: React.FC = ({ getTimeRange: () => timeRangeRef.current, palette, cellGap: options.cellGap, - hideThreshold: options.filterValues?.min, // eventually a better range + hideLE: options.filterValues?.le, + hideGE: options.filterValues?.ge, + valueMin: options.color.min, + valueMax: options.color.max, exemplarColor: options.exemplars?.color ?? 'rgba(255,0,255,0.7)', yAxisConfig: options.yAxis, ySizeDivisor: scaleConfig?.type === ScaleDistribution.Log ? +(options.calculation?.yBuckets?.value || 1) : 1, @@ -143,7 +146,17 @@ export const HeatmapPanel: React.FC = ({ let countFieldIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3; const countField = info.heatmap.fields[countFieldIdx]; - const { min, max } = reduceField({ field: countField, reducers: [ReducerID.min, ReducerID.max] }); + // TODO -- better would be to get the range from the real color scale! + let { min, max } = options.color; + if (min == null || max == null) { + const calc = reduceField({ field: countField, reducers: [ReducerID.min, ReducerID.max] }); + if (min == null) { + min = calc[ReducerID.min]; + } + if (max == null) { + max = calc[ReducerID.max]; + } + } let hoverValue: number | undefined = undefined; // seriesIdx: 1 is heatmap layer; 2 is exemplar layer @@ -154,7 +167,7 @@ export const HeatmapPanel: React.FC = ({ return (
- +
); @@ -209,5 +222,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ colorScaleWrapper: css` margin-left: 25px; padding: 10px 0; + max-width: 300px; `, }); diff --git a/public/app/plugins/panel/heatmap-new/fields.ts b/public/app/plugins/panel/heatmap-new/fields.ts index bbbad5f1567..121df353783 100644 --- a/public/app/plugins/panel/heatmap-new/fields.ts +++ b/public/app/plugins/panel/heatmap-new/fields.ts @@ -75,7 +75,7 @@ export function prepareHeatmapData(data: PanelData, options: PanelOptions, theme } } - return getHeatmapData(bucketsToScanlines({ ...options.bucket, frame: bucketHeatmap }), exemplars, theme); + return getHeatmapData(bucketsToScanlines({ ...options.bucketFrame, frame: bucketHeatmap }), exemplars, theme); } const getSparseHeatmapData = ( @@ -139,7 +139,7 @@ const getHeatmapData = (frame: DataFrame, exemplars: DataFrame | undefined, them const data: HeatmapData = { heatmap: frame, - exemplars, + exemplars: exemplars?.length ? exemplars : undefined, xBucketSize: xBinIncr, yBucketSize: yBinIncr, xBucketCount: xBinQty, diff --git a/public/app/plugins/panel/heatmap-new/img/heatmap.svg b/public/app/plugins/panel/heatmap-new/img/icn-heatmap-panel.svg similarity index 100% rename from public/app/plugins/panel/heatmap-new/img/heatmap.svg rename to public/app/plugins/panel/heatmap-new/img/icn-heatmap-panel.svg diff --git a/public/app/plugins/panel/heatmap-new/migrations.test.ts b/public/app/plugins/panel/heatmap-new/migrations.test.ts index 86ff840d106..9ac89efd9ec 100644 --- a/public/app/plugins/panel/heatmap-new/migrations.test.ts +++ b/public/app/plugins/panel/heatmap-new/migrations.test.ts @@ -21,11 +21,14 @@ describe('Heatmap Migrations', () => { expect(panel).toMatchInlineSnapshot(` Object { "fieldConfig": Object { - "defaults": Object {}, + "defaults": Object { + "decimals": 6, + "unit": "short", + }, "overrides": Array [], }, "options": Object { - "bucket": Object { + "bucketFrame": Object { "layout": "auto", }, "calculate": true, @@ -44,7 +47,7 @@ describe('Heatmap Migrations', () => { }, }, "cellGap": 2, - "cellSize": 10, + "cellRadius": 10, "color": Object { "exponent": 0.5, "fill": "dark-orange", @@ -59,7 +62,7 @@ describe('Heatmap Migrations', () => { "color": "rgba(255,0,255,0.7)", }, "filterValues": Object { - "min": 1e-9, + "le": 1e-9, }, "legend": Object { "show": true, @@ -72,6 +75,8 @@ describe('Heatmap Migrations', () => { "yAxis": Object { "axisPlacement": "left", "axisWidth": 400, + "max": 22, + "min": 7, "reverse": false, }, }, @@ -133,11 +138,11 @@ const oldHeatmap = { yAxis: { show: true, format: 'short', - decimals: null, + decimals: 6, logBase: 2, splitFactor: 3, - min: null, - max: null, + min: 7, + max: 22, width: '400', }, xBucketSize: null, diff --git a/public/app/plugins/panel/heatmap-new/migrations.ts b/public/app/plugins/panel/heatmap-new/migrations.ts index d3d96a7945c..602f09bece7 100644 --- a/public/app/plugins/panel/heatmap-new/migrations.ts +++ b/public/app/plugins/panel/heatmap-new/migrations.ts @@ -60,6 +60,9 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS }, }; } + + fieldConfig.defaults.unit = oldYAxis.format; + fieldConfig.defaults.decimals = oldYAxis.decimals; } const options: PanelOptions = { @@ -69,14 +72,16 @@ export function angularToReactHeatmap(angular: any): { fieldConfig: FieldConfigS ...defaultPanelOptions.color, steps: 128, // best match with existing colors }, - cellGap: asNumber(angular.cards?.cardPadding), - cellSize: asNumber(angular.cards?.cardRound), + cellGap: asNumber(angular.cards?.cardPadding, 2), + cellRadius: asNumber(angular.cards?.cardRound), // just to keep it yAxis: { axisPlacement: oldYAxis.show === false ? AxisPlacement.Hidden : AxisPlacement.Left, reverse: Boolean(angular.reverseYBuckets), axisWidth: oldYAxis.width ? +oldYAxis.width : undefined, + min: oldYAxis.min, + max: oldYAxis.max, }, - bucket: { + bucketFrame: { layout: getHeatmapBucketLayout(angular.yBucketBound), }, legend: { @@ -134,9 +139,12 @@ function getHeatmapBucketLayout(v?: string): HeatmapBucketLayout { return HeatmapBucketLayout.auto; } -function asNumber(v: any): number | undefined { +function asNumber(v: any, defaultValue?: number): number | undefined { + if (v == null || v === '') { + return defaultValue; + } const num = +v; - return isNaN(num) ? undefined : num; + return isNaN(num) ? defaultValue : num; } export const heatmapMigrationHandler = (panel: PanelModel): Partial => { diff --git a/public/app/plugins/panel/heatmap-new/models.gen.ts b/public/app/plugins/panel/heatmap-new/models.gen.ts index c7450496fae..d5ff17e1fe3 100644 --- a/public/app/plugins/panel/heatmap-new/models.gen.ts +++ b/public/app/plugins/panel/heatmap-new/models.gen.ts @@ -34,11 +34,14 @@ export interface YAxisConfig extends AxisConfig { unit?: string; reverse?: boolean; decimals?: number; + // Only used when the axis is not ordinal + min?: number; + max?: number; } export interface FilterValueRange { - min?: number; - max?: number; + le?: number; + ge?: number; } export interface HeatmapTooltip { @@ -53,8 +56,8 @@ export interface ExemplarConfig { color: string; } -export interface BucketOptions { - name?: string; +export interface BucketFrameOptions { + value?: string; // value field name layout?: HeatmapBucketLayout; } @@ -64,11 +67,11 @@ export interface PanelOptions { color: HeatmapColorOptions; filterValues?: FilterValueRange; // was hideZeroBuckets - bucket?: BucketOptions; + bucketFrame?: BucketFrameOptions; showValue: VisibilityMode; cellGap?: number; // was cardPadding - cellSize?: number; // was cardRadius + cellRadius?: number; // was cardRadius (not used, but migrated from angular) yAxis: YAxisConfig; legend: HeatmapLegend; @@ -87,7 +90,7 @@ export const defaultPanelOptions: PanelOptions = { exponent: 0.5, steps: 64, }, - bucket: { + bucketFrame: { layout: HeatmapBucketLayout.auto, }, yAxis: { @@ -105,7 +108,7 @@ export const defaultPanelOptions: PanelOptions = { color: 'rgba(255,0,255,0.7)', }, filterValues: { - min: 1e-9, + le: 1e-9, }, cellGap: 1, }; diff --git a/public/app/plugins/panel/heatmap-new/module.tsx b/public/app/plugins/panel/heatmap-new/module.tsx index 0b583cf14a1..19cd36c2d64 100644 --- a/public/app/plugins/panel/heatmap-new/module.tsx +++ b/public/app/plugins/panel/heatmap-new/module.tsx @@ -63,33 +63,10 @@ export const plugin = new PanelPlugin(HeatmapPan if (opts.calculate) { addHeatmapCalculationOptions('calculation.', builder, opts.calculation, category); - } else { - builder.addTextInput({ - path: 'bucket.name', - name: 'Cell value name', - defaultValue: defaultPanelOptions.bucket?.name, - settings: { - placeholder: 'Value', - }, - category, - }); - builder.addRadio({ - path: 'bucket.layout', - name: 'Layout', - defaultValue: defaultPanelOptions.bucket?.layout ?? HeatmapBucketLayout.auto, - category, - settings: { - options: [ - { label: 'Auto', value: HeatmapBucketLayout.auto }, - { label: 'Middle', value: HeatmapBucketLayout.unknown }, - { label: 'Lower (LE)', value: HeatmapBucketLayout.le }, - { label: 'Upper (GE)', value: HeatmapBucketLayout.ge }, - ], - }, - }); } category = ['Y Axis']; + builder.addRadio({ path: 'yAxis.axisPlacement', name: 'Placement', @@ -104,6 +81,27 @@ export const plugin = new PanelPlugin(HeatmapPan }, }); + // TODO: support clamping the min/max range when there is a real axis + if (false && opts.calculate) { + builder + .addNumberInput({ + path: 'yAxis.min', + name: 'Min value', + settings: { + placeholder: 'Auto', + }, + category, + }) + .addTextInput({ + path: 'yAxis.max', + name: 'Max value', + settings: { + placeholder: 'Auto', + }, + category, + }); + } + builder .addNumberInput({ path: 'yAxis.axisWidth', @@ -123,14 +121,31 @@ export const plugin = new PanelPlugin(HeatmapPan placeholder: 'Auto', }, category, - }) - .addBooleanSwitch({ - path: 'yAxis.reverse', - name: 'Reverse', - defaultValue: defaultPanelOptions.yAxis.reverse === true, - category, }); + if (!opts.calculate) { + builder.addRadio({ + path: 'bucketFrame.layout', + name: 'Tick alignment', + defaultValue: defaultPanelOptions.bucketFrame?.layout ?? HeatmapBucketLayout.auto, + category, + settings: { + options: [ + { label: 'Auto', value: HeatmapBucketLayout.auto }, + { label: 'Top (LE)', value: HeatmapBucketLayout.le }, + { label: 'Middle', value: HeatmapBucketLayout.unknown }, + { label: 'Bottom (GE)', value: HeatmapBucketLayout.ge }, + ], + }, + }); + } + builder.addBooleanSwitch({ + path: 'yAxis.reverse', + name: 'Reverse', + defaultValue: defaultPanelOptions.yAxis.reverse === true, + category, + }); + category = ['Colors']; builder.addRadio({ @@ -225,6 +240,26 @@ export const plugin = new PanelPlugin(HeatmapPan }, }); + builder + .addNumberInput({ + path: 'color.min', + name: 'Start color scale from value', + defaultValue: defaultPanelOptions.color.min, + settings: { + placeholder: 'Auto (min)', + }, + category, + }) + .addNumberInput({ + path: 'color.max', + name: 'End color scale at value', + defaultValue: defaultPanelOptions.color.max, + settings: { + placeholder: 'Auto (max)', + }, + category, + }); + category = ['Display']; builder @@ -241,12 +276,6 @@ export const plugin = new PanelPlugin(HeatmapPan // ], // }, // }) - .addNumberInput({ - path: 'filterValues.min', - name: 'Hide cell counts <=', - defaultValue: defaultPanelOptions.filterValues?.min, - category, - }) .addSliderInput({ name: 'Cell gap', path: 'cellGap', @@ -256,6 +285,24 @@ export const plugin = new PanelPlugin(HeatmapPan min: 0, max: 25, }, + }) + .addNumberInput({ + path: 'filterValues.le', + name: 'Hide cells with values <=', + defaultValue: defaultPanelOptions.filterValues?.le, + settings: { + placeholder: 'None', + }, + category, + }) + .addNumberInput({ + path: 'filterValues.ge', + name: 'Hide cells with values >=', + defaultValue: defaultPanelOptions.filterValues?.ge, + settings: { + placeholder: 'None', + }, + category, }); // .addSliderInput({ // name: 'Cell radius', @@ -277,6 +324,18 @@ export const plugin = new PanelPlugin(HeatmapPan category, }); + if (!opts.calculate) { + builder.addTextInput({ + path: 'bucketFrame.value', + name: 'Cell value name', + defaultValue: defaultPanelOptions.bucketFrame?.value, + settings: { + placeholder: 'Value', + }, + category, + }); + } + builder.addBooleanSwitch({ path: 'tooltip.yHistogram', name: 'Show histogram (Y axis)', diff --git a/public/app/plugins/panel/heatmap-new/plugin.json b/public/app/plugins/panel/heatmap-new/plugin.json index 4a320c089fb..66a6241b9f9 100644 --- a/public/app/plugins/panel/heatmap-new/plugin.json +++ b/public/app/plugins/panel/heatmap-new/plugin.json @@ -5,14 +5,14 @@ "state": "alpha", "info": { - "description": "Next generation heatmap visualization", + "description": "Like a histogram over time", "author": { "name": "Grafana Labs", "url": "https://grafana.com" }, "logos": { - "small": "img/heatmap.svg", - "large": "img/heatmap.svg" + "small": "img/icn-heatmap-panel.svg", + "large": "img/icn-heatmap-panel.svg" } } } diff --git a/public/app/plugins/panel/heatmap-new/utils.ts b/public/app/plugins/panel/heatmap-new/utils.ts index d75c6329720..e225d5451e9 100644 --- a/public/app/plugins/panel/heatmap-new/utils.ts +++ b/public/app/plugins/panel/heatmap-new/utils.ts @@ -1,7 +1,15 @@ import { MutableRefObject, RefObject } from 'react'; import uPlot from 'uplot'; -import { DataFrameType, GrafanaTheme2, incrRoundDn, incrRoundUp, TimeRange } from '@grafana/data'; +import { + DataFrameType, + formattedValueToString, + getValueFormat, + GrafanaTheme2, + incrRoundDn, + incrRoundUp, + TimeRange, +} from '@grafana/data'; import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '@grafana/schema'; import { UPlotConfigBuilder } from '@grafana/ui'; import { readHeatmapScanlinesCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap'; @@ -15,7 +23,8 @@ import { PanelFieldConfig, YAxisConfig } from './models.gen'; interface PathbuilderOpts { each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void; gap?: number | null; - hideThreshold?: number; + hideLE?: number; + hideGE?: number; xAlign?: -1 | 0 | 1; yAlign?: -1 | 0 | 1; ySizeDivisor?: number; @@ -55,7 +64,10 @@ interface PrepConfigOpts { palette: string[]; exemplarColor: string; cellGap?: number | null; // in css pixels - hideThreshold?: number; + hideLE?: number; + hideGE?: number; + valueMin?: number; + valueMax?: number; yAxisConfig: YAxisConfig; ySizeDivisor?: number; } @@ -72,7 +84,10 @@ export function prepConfig(opts: PrepConfigOpts) { getTimeRange, palette, cellGap, - hideThreshold, + hideLE, + hideGE, + valueMin, + valueMax, yAxisConfig, ySizeDivisor, } = opts; @@ -289,12 +304,12 @@ export function prepConfig(opts: PrepConfigOpts) { // how to expand scale range if inferred non-regular or log buckets? } } - return [dataMin, dataMax]; }, }); - const hasLabeledY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null; + const isOrdianalY = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap).yOrdinalDisplay != null; + const disp = dataRef.current?.heatmap?.fields[1].display ?? getValueFormat('short'); builder.addAxis({ scaleKey: 'y', @@ -303,35 +318,51 @@ export function prepConfig(opts: PrepConfigOpts) { size: yAxisConfig.axisWidth || null, label: yAxisConfig.axisLabel, theme: theme, - splits: hasLabeledY - ? () => { - const ys = dataRef.current?.heatmap?.fields[1].values.toArray()!; - const splits = ys.slice(0, ys.length - ys.lastIndexOf(ys[0])); + formatValue: (v: any) => formattedValueToString(disp(v)), + splits: isOrdianalY + ? (self: uPlot) => { + const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap); + if (!meta.yOrdinalDisplay) { + return [0, 1]; //? + } + let splits = meta.yOrdinalDisplay.map((v, idx) => idx); - const bucketSize = dataRef.current?.yBucketSize!; - - if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { - splits.unshift(ys[0] - bucketSize); - } else { - splits.push(ys[ys.length - 1] + bucketSize); + switch (dataRef.current?.yLayout) { + case HeatmapBucketLayout.le: + splits.unshift(-1); + break; + case HeatmapBucketLayout.ge: + splits.push(splits.length); + break; } + // Skip labels when the height is too small + if (self.height < 60) { + splits = [splits[0], splits[splits.length - 1]]; + } else { + while (splits.length > 3 && (self.height - 15) / splits.length < 10) { + splits = splits.filter((v, idx) => idx % 2 === 0); // remove half the items + } + } return splits; } : undefined, - values: hasLabeledY - ? () => { + values: isOrdianalY + ? (self: uPlot, splits) => { const meta = readHeatmapScanlinesCustomMeta(dataRef.current?.heatmap); - const yAxisValues = meta.yOrdinalDisplay?.slice()!; - const isFromBuckets = meta.yOrdinalDisplay?.length && !('le' === meta.yMatchWithLabel); - - if (dataRef.current?.yLayout === HeatmapBucketLayout.le) { - yAxisValues.unshift(isFromBuckets ? '' : '0.0'); // assumes dense layout where lowest bucket's low bound is 0-ish - } else if (dataRef.current?.yLayout === HeatmapBucketLayout.ge) { - yAxisValues.push(isFromBuckets ? '' : '+Inf'); + if (meta.yOrdinalDisplay) { + return splits.map((v) => { + const txt = meta.yOrdinalDisplay[v]; + if (!txt && v < 0) { + // Check prometheus style labels + if ('le' === meta.yMatchWithLabel) { + return '0.0'; + } + } + return txt; + }); } - - return yAxisValues; + return splits.map((v) => `${v}`); } : undefined, }); @@ -363,7 +394,8 @@ export function prepConfig(opts: PrepConfigOpts) { }); }, gap: cellGap, - hideThreshold, + hideLE, + hideGE, xAlign: dataRef.current?.xLayout === HeatmapBucketLayout.le ? -1 @@ -380,7 +412,7 @@ export function prepConfig(opts: PrepConfigOpts) { fill: { values: (u, seriesIdx) => { let countFacetIdx = heatmapType === DataFrameType.HeatmapScanlines ? 2 : 3; - return countsToFills(u.data[seriesIdx][countFacetIdx] as unknown as number[], palette); + return valuesToFills(u.data[seriesIdx][countFacetIdx] as unknown as number[], palette, valueMin, valueMax); }, index: palette, }, @@ -465,7 +497,7 @@ export function prepConfig(opts: PrepConfigOpts) { const CRISP_EDGES_GAP_MIN = 4; export function heatmapPathsDense(opts: PathbuilderOpts) { - const { disp, each, gap = 1, hideThreshold = 0, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts; + const { disp, each, gap = 1, hideLE = -Infinity, hideGE = Infinity, xAlign = 1, yAlign = 1, ySizeDivisor = 1 } = opts; const pxRatio = devicePixelRatio; @@ -549,14 +581,7 @@ export function heatmapPathsDense(opts: PathbuilderOpts) { ); for (let i = 0; i < dlen; i++) { - // filter out 0 counts and out of view - if ( - counts[i] > hideThreshold && - xs[i] + xBinIncr >= scaleX.min! && - xs[i] - xBinIncr <= scaleX.max! && - ys[i] + yBinIncr >= scaleY.min! && - ys[i] - yBinIncr <= scaleY.max! - ) { + if (counts[i] > hideLE && counts[i] < hideGE) { let cx = cxs[~~(i / yBinQty)]; let cy = cys[i % yBinQty]; @@ -646,7 +671,7 @@ export function heatmapPathsPoints(opts: PointsBuilderOpts, exemplarColor: strin // accepts xMax, yMin, yMax, count // xbinsize? x tile sizes are uniform? export function heatmapPathsSparse(opts: PathbuilderOpts) { - const { disp, each, gap = 1, hideThreshold = 0 } = opts; + const { disp, each, gap = 1, hideLE = -Infinity, hideGE = Infinity } = opts; const pxRatio = devicePixelRatio; @@ -717,7 +742,7 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { let xSizeUniform = xOffs.get(xMaxs.find((v) => v !== xMaxs[0])) - xOffs.get(xMaxs[0]); for (let i = 0; i < dlen; i++) { - if (counts[i] <= hideThreshold) { + if (counts[i] <= hideLE || counts[i] >= hideGE) { continue; } @@ -739,19 +764,11 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { let x = xMaxPx; let y = yMinPx; - // filter out 0 counts and out of view - // if ( - // xs[i] + xBinIncr >= scaleX.min! && - // xs[i] - xBinIncr <= scaleX.max! && - // ys[i] + yBinIncr >= scaleY.min! && - // ys[i] - yBinIncr <= scaleY.max! - // ) { let fillPath = fillPaths[fills[i]]; rect(fillPath, x, y, xSize, ySize); each(u, 1, i, x, y, xSize, ySize); - // } } u.ctx.save(); @@ -772,29 +789,36 @@ export function heatmapPathsSparse(opts: PathbuilderOpts) { }; } -export const countsToFills = (counts: number[], palette: string[]) => { - // TODO: integrate 1e-9 hideThreshold? - const hideThreshold = 0; +export const valuesToFills = (values: number[], palette: string[], minValue?: number, maxValue?: number) => { + if (minValue == null) { + minValue = Infinity; - let minCount = Infinity; - let maxCount = -Infinity; - - for (let i = 0; i < counts.length; i++) { - if (counts[i] > hideThreshold) { - minCount = Math.min(minCount, counts[i]); - maxCount = Math.max(maxCount, counts[i]); + for (let i = 0; i < values.length; i++) { + minValue = Math.min(minValue, values[i]); } } - let range = maxCount - minCount; + if (maxValue == null) { + maxValue = -Infinity; + + for (let i = 0; i < values.length; i++) { + maxValue = Math.max(maxValue, values[i]); + } + } + + let range = maxValue - minValue; let paletteSize = palette.length; - let indexedFills = Array(counts.length); + let indexedFills = Array(values.length); - for (let i = 0; i < counts.length; i++) { + for (let i = 0; i < values.length; i++) { indexedFills[i] = - counts[i] === 0 ? -1 : Math.min(paletteSize - 1, Math.floor((paletteSize * (counts[i] - minCount)) / range)); + values[i] < minValue + ? 0 + : values[i] > maxValue + ? paletteSize - 1 + : Math.min(paletteSize - 1, Math.floor((paletteSize * (values[i] - minValue)) / range)); } return indexedFills; From 4a5d5e6ead8f7254227d9581bb1ca92670a6ce2d Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 6 Jun 2022 05:07:32 -0400 Subject: [PATCH 91/95] Loki: Add user analytics for query editor mode (#49619) (#50232) * Add reportInteraction call * Update public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> (cherry picked from commit 37aedd6906bff7850ad81cd743ea8ed7e75b00b6) Co-authored-by: Andrej Ocenas --- .../components/LokiQueryEditorSelector.test.tsx | 7 +++++++ .../components/LokiQueryEditorSelector.tsx | 12 ++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx index d105157ef0e..23398469510 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx @@ -10,6 +10,13 @@ import { LokiQuery, LokiQueryType } from '../../types'; import { LokiQueryEditorSelector } from './LokiQueryEditorSelector'; +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + }; +}); + jest.mock('app/core/store', () => { return { get() { diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index e374302a2b6..1523fa90721 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -2,6 +2,7 @@ import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react'; import { LoadingState } from '@grafana/data'; import { EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/experimental'; +import { reportInteraction } from '@grafana/runtime'; import { Button, ConfirmModal } from '@grafana/ui'; import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle'; import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch'; @@ -19,7 +20,7 @@ import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; import { LokiQueryCodeEditor } from './LokiQueryCodeEditor'; export const LokiQueryEditorSelector = React.memo((props) => { - const { onChange, onRunQuery, data } = props; + const { onChange, onRunQuery, data, app } = props; const [parseModalOpen, setParseModalOpen] = useState(false); const [dataIsStale, setDataIsStale] = useState(false); @@ -30,6 +31,13 @@ export const LokiQueryEditorSelector = React.memo((props) const onEditorModeChange = useCallback( (newEditorMode: QueryEditorMode) => { + reportInteraction('grafana_loki_editor_mode_clicked', { + newEditor: newEditorMode, + previousEditor: query.editorMode ?? '', + newQuery: !query.expr, + app: app ?? '', + }); + if (newEditorMode === QueryEditorMode.Builder) { const result = buildVisualQueryFromString(query.expr || ''); // If there are errors, give user a chance to decide if they want to go to builder as that can loose some data. @@ -40,7 +48,7 @@ export const LokiQueryEditorSelector = React.memo((props) } changeEditorMode(query, newEditorMode, onChange); }, - [onChange, query] + [onChange, query, app] ); useEffect(() => { From 760b9ac9d06abeeb54c563fc2cc30964301baae2 Mon Sep 17 00:00:00 2001 From: "Grot (@grafanabot)" <43478413+grafanabot@users.noreply.github.com> Date: Mon, 6 Jun 2022 07:12:36 -0400 Subject: [PATCH 92/95] Expression: Filter query, mixed mode fixes and panel error message (#50218) (#50242) * implement filterQuery to support query.hide * Fixed - expression ds name in mixed mode * Execute expression query on blur * show actual error message when ds return Query data error (cherry picked from commit a3071b7797a0261db604a352553c86f5f96c953c) Co-authored-by: Sriram --- .../src/components/DataSourcePicker.tsx | 7 +++++++ .../src/utils/toDataQueryError.ts | 4 +++- .../expressions/ExpressionDatasource.ts | 7 +++++++ .../expressions/ExpressionQueryEditor.tsx | 4 ++-- .../features/expressions/components/Math.tsx | 19 ++++++++++++++++--- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/grafana-runtime/src/components/DataSourcePicker.tsx b/packages/grafana-runtime/src/components/DataSourcePicker.tsx index 16d3ad9b3d4..71b5db4e4a9 100644 --- a/packages/grafana-runtime/src/components/DataSourcePicker.tsx +++ b/packages/grafana-runtime/src/components/DataSourcePicker.tsx @@ -14,6 +14,8 @@ import { ActionMeta, HorizontalGroup, PluginSignatureBadge, Select } from '@graf import { getDataSourceSrv } from '../services/dataSourceSrv'; +import { ExpressionDatasourceRef } from './../utils/DataSourceWithBackend'; + /** * Component props description for the {@link DataSourcePicker} * @@ -117,6 +119,11 @@ export class DataSourcePicker extends PureComponent): Observable { let targets = request.targets.map(async (query: ExpressionQuery): Promise => { const ds = await getDataSourceSrv().get(query.datasource); diff --git a/public/app/features/expressions/ExpressionQueryEditor.tsx b/public/app/features/expressions/ExpressionQueryEditor.tsx index d81bc7825a7..618bdf93787 100644 --- a/public/app/features/expressions/ExpressionQueryEditor.tsx +++ b/public/app/features/expressions/ExpressionQueryEditor.tsx @@ -21,12 +21,12 @@ export class ExpressionQueryEditor extends PureComponent { }; renderExpressionType() { - const { onChange, query, queries } = this.props; + const { onChange, onRunQuery, query, queries } = this.props; const refIds = queries!.filter((q) => query.refId !== q.refId).map((q) => ({ value: q.refId, label: q.refId })); switch (query.type) { case ExpressionQueryType.math: - return ; + return ; case ExpressionQueryType.reduce: return ; diff --git a/public/app/features/expressions/components/Math.tsx b/public/app/features/expressions/components/Math.tsx index 15cc9d86a56..577ed90356b 100644 --- a/public/app/features/expressions/components/Math.tsx +++ b/public/app/features/expressions/components/Math.tsx @@ -12,14 +12,15 @@ interface Props { labelWidth: number; query: ExpressionQuery; onChange: (query: ExpressionQuery) => void; + onRunQuery: () => void; } const mathPlaceholder = 'Math operations on one or more queries. You reference the query by ${refId} ie. $A, $B, $C etc\n' + 'The sum of two scalar values: $A + $B > 10'; -export const Math: FC = ({ labelWidth, onChange, query }) => { - const [showHelp, toggleShowHelp] = useToggle(true); +export const Math: FC = ({ labelWidth, onChange, query, onRunQuery }) => { + const [showHelp, toggleShowHelp] = useToggle(false); const onExpressionChange = (event: ChangeEvent) => { onChange({ ...query, expression: event.target.value }); @@ -27,6 +28,12 @@ export const Math: FC = ({ labelWidth, onChange, query }) => { const styles = useStyles2((theme) => getStyles(theme, showHelp)); + const executeQuery = () => { + if (query.expression) { + onRunQuery(); + } + }; + return ( = ({ labelWidth, onChange, query }) => { `} > <> -