pull/100013/head
Ryan McKinley 4 months ago
commit b5a02afdc9
  1. 2
      Makefile
  2. 14
      apps/advisor/pkg/app/app.go
  3. 6
      docs/sources/administration/migration-guide/cloud-migration-assistant.md
  4. 2
      docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md
  5. 4
      docs/sources/alerting/alerting-rules/templates/_index.md
  6. 4
      docs/sources/alerting/alerting-rules/templates/examples.md
  7. 32
      docs/sources/alerting/configure-notifications/template-notifications/reference.md
  8. 2
      docs/sources/alerting/fundamentals/alert-rule-evaluation/state-and-health.md
  9. 28
      docs/sources/dashboards/share-dashboards-panels/_index.md
  10. 6
      docs/sources/datasources/loki/query-editor/index.md
  11. 120
      docs/sources/setup-grafana/configure-security/configure-authentication/saml/index.md
  12. 13
      docs/sources/shared/alerts/alerting-provisioning-export-produces.md
  13. 633
      docs/sources/shared/alerts/alerting_provisioning.md
  14. 14
      docs/sources/shared/alerts/note-dynamic-labels.md
  15. 14
      docs/sources/shared/alerts/table-configure-no-data-and-error.md
  16. 4
      go.mod
  17. 8
      go.sum
  18. 2
      go.work.sum
  19. 2
      package.json
  20. 49
      packages/grafana-runtime/src/components/FolderPicker.tsx
  21. 1
      packages/grafana-runtime/src/index.ts
  22. 5
      packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx
  23. 5
      packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx
  24. 46
      packages/grafana-ui/src/components/ThemeDemos/BorderRadius.internal.story.tsx
  25. 87
      packages/grafana-ui/src/components/ThemeDemos/BorderRadius.tsx
  26. 52
      packages/grafana-ui/src/themes/mixins.ts
  27. 17
      packages/grafana-ui/src/utils/i18n.tsx
  28. 2
      pkg/apimachinery/go.mod
  29. 4
      pkg/apimachinery/go.sum
  30. 2
      pkg/infra/db/sqlbuilder.go
  31. 8
      pkg/registry/apis/dashboard/legacy/client.go
  32. 4
      pkg/registry/apis/dashboard/legacy/storage.go
  33. 4
      pkg/registry/apis/dashboard/search_test.go
  34. 5
      pkg/services/accesscontrol/database/database.go
  35. 7
      pkg/services/accesscontrol/filter.go
  36. 2
      pkg/services/annotations/accesscontrol/accesscontrol.go
  37. 2
      pkg/services/dashboards/database/database.go
  38. 8
      pkg/services/libraryelements/database.go
  39. 121
      pkg/services/sqlstore/migrations/dialect_migration_test.go
  40. 10
      pkg/services/sqlstore/migrator/dialect.go
  41. 6
      pkg/services/sqlstore/migrator/migrator.go
  42. 85
      pkg/services/sqlstore/migrator/snapshot/spanner-ddl.json
  43. 117
      pkg/services/sqlstore/migrator/snapshot/spanner-log.json
  44. 4
      pkg/services/sqlstore/migrator/spanner_dialect.go
  45. 11
      pkg/services/sqlstore/permissions/dashboard.go
  46. 5
      pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go
  47. 10
      pkg/services/sqlstore/permissions/dashboard_test.go
  48. 2
      pkg/services/sqlstore/permissions/dashboards_bench_test.go
  49. 1
      pkg/services/sqlstore/searchstore/search_test.go
  50. 9
      pkg/services/sqlstore/session.go
  51. 34
      pkg/storage/legacysql/dualwrite/dualwriter.go
  52. 4
      pkg/storage/unified/apistore/go.mod
  53. 8
      pkg/storage/unified/apistore/go.sum
  54. 2
      pkg/storage/unified/apistore/store_test.go
  55. 42
      pkg/storage/unified/resource/client.go
  56. 34
      pkg/storage/unified/resource/document.go
  57. 7
      pkg/storage/unified/resource/document_test.go
  58. 4
      pkg/storage/unified/resource/go.mod
  59. 8
      pkg/storage/unified/resource/go.sum
  60. 855
      pkg/storage/unified/resource/resource.pb.go
  61. 40
      pkg/storage/unified/resource/resource.proto
  62. 104
      pkg/storage/unified/resource/resource_grpc.pb.go
  63. 36
      pkg/storage/unified/resource/search.go
  64. 10
      pkg/storage/unified/resource/server.go
  65. 2
      pkg/storage/unified/resource/testdata/playlist-resource.json
  66. 57
      pkg/storage/unified/search/bleve.go
  67. 14
      pkg/storage/unified/search/bleve_mappings.go
  68. 3
      pkg/storage/unified/search/bleve_mappings_test.go
  69. 52
      pkg/storage/unified/search/bleve_search_test.go
  70. 36
      pkg/storage/unified/search/bleve_test.go
  71. 3
      pkg/storage/unified/search/testdata/doc/folder-aaa-out.json
  72. 3
      pkg/storage/unified/search/testdata/doc/folder-bbb-out.json
  73. 4
      pkg/storage/unified/sql/search.go
  74. 2
      pkg/storage/unified/sql/service.go
  75. 3
      pkg/tests/apis/openapi_test.go
  76. 3
      pkg/tsdb/tempo/traceql_query.go
  77. 6
      pkg/tsdb/tempo/traceql_query_test.go
  78. 6
      pkg/util/xorm/engine.go
  79. 4
      pkg/util/xorm/go.mod
  80. 4
      pkg/util/xorm/go.sum
  81. 69
      pkg/util/xorm/sequence.go
  82. 31
      pkg/util/xorm/sequence_test.go
  83. 104
      pkg/util/xorm/session_insert.go
  84. 12
      pkg/util/xorm/statement.go
  85. 4
      pkg/util/xorm/xorm.go
  86. 29
      pkg/util/xorm/xorm_spanner_test.go
  87. 44
      pkg/util/xorm/xorm_test.go
  88. 3
      public/app/app.ts
  89. 4
      public/app/core/internationalization/constants.ts
  90. 4
      public/app/core/reducers/root.ts
  91. 41
      public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx
  92. 29
      public/app/features/alerting/unified/components/rule-viewer/tabs/Details.tsx
  93. 2
      public/app/features/alerting/unified/types/rule-form.ts
  94. 1
      public/app/features/alerting/unified/utils/__snapshots__/rule-form.test.ts.snap
  95. 3
      public/app/features/alerting/unified/utils/rule-form.ts
  96. 25
      public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
  97. 12
      public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
  98. 2
      public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx
  99. 111
      public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx
  100. 40
      public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -131,14 +131,12 @@ else
i18n-extract-enterprise:
@echo "Extracting i18n strings for Enterprise"
yarn run i18next --config public/locales/i18next-parser-enterprise.config.cjs
node ./public/locales/pseudo.mjs --mode enterprise
endif
.PHONY: i18n-extract
i18n-extract: i18n-extract-enterprise
@echo "Extracting i18n strings for OSS"
yarn run i18next --config public/locales/i18next-parser.config.cjs
node ./public/locales/pseudo.mjs --mode oss
##@ Building
.PHONY: gen-cue

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/apps/advisor/pkg/app/checks"
"github.com/grafana/grafana/apps/advisor/pkg/app/checkscheduler"
"github.com/grafana/grafana/apps/advisor/pkg/app/checktyperegisterer"
"github.com/grafana/grafana/pkg/infra/log"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
)
@ -24,6 +25,12 @@ func New(cfg app.Config) (app.App, error) {
return nil, fmt.Errorf("invalid config type")
}
checkRegistry := specificConfig.CheckRegistry
stackID := specificConfig.StackID
namespace, err := checks.GetNamespace(stackID)
if err != nil {
return nil, err
}
log := log.New("advisor.app")
// Prepare storage client
clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{})
@ -60,6 +67,13 @@ func New(cfg app.Config) (app.App, error) {
},
Watcher: &simple.Watcher{
AddFunc: func(ctx context.Context, obj resource.Object) error {
log.Debug("Adding check", "namespace", obj.GetNamespace())
if obj.GetNamespace() != namespace {
log.Debug("Skipping check in namespace", "namespace", obj.GetNamespace())
return nil
} else {
log.Debug("Processing check in namespace", "namespace", obj.GetNamespace())
}
check, err := getCheck(obj, checkMap)
if err != nil {
return err

@ -147,6 +147,12 @@ Your data sources, including credentials, are migrated securely and seamlessly t
The migration assistant supports any plugins found in the plugins catalog. As long as the plugin is signed or is a core plugin built into Grafana, it is eligible for migration. Due to security reasons, unsigned plugins are not supported in Grafana Cloud. If you are using any unsigned private plugins, Grafana recommends you seek an alternative plugin for the catalog or work on a strategy to deprecate certain functionality from your self-managed instance.
Upgrade any plugins you intend to migrate before using the migration assistant as any migrated plugins will be configured on the Grafana Cloud instance as the latest version of that plugin.
{{< admonition type="caution">}}
If you want to migrate Enterprise plugins, check what type of plan your Grafana Cloud instance is on and whether or not this plan requires an Enterprise plugin add-on.
{{< /admonition >}}
### Grafana Alerting resources
The migration assistant can migrate the majority of Grafana Alerting resources to your Grafana Cloud instance. These include:

@ -94,7 +94,7 @@ To learn how to use the roles API to determine the role UUIDs, refer to [Manage
| `fixed:authentication.config:writer` | `fixed_0rYhZ2Qnzs8AdB1nX7gexk3fHDw` | `settings:read` for scope `settings:auth.saml:*` <br> `settings:write` for scope `settings:auth.saml:*` | Read and update authentication and SAML settings. |
| `fixed:dashboards:creator` | `fixed_ZorKUcEPCM01A1fPakEzGBUyU64` | `dashboards:create`<br>`folders:read` | Create dashboards. |
| `fixed:dashboards:reader` | `fixed_Sgr67JTOhjQGFlzYRahOe45TdWM` | `dashboards:read` | Read all dashboards. |
| `fixed:dashboards:writer` | `fixed_OK2YOQGIoI1G031hVzJB6rAJQAs` | All permissions from `fixed:dashboards:reader` and <br>`dashboards:write`<br>`dashboards:edit`<br>`dashboards:delete`<br>`dashboards:create`<br>`dashboards.permissions:read`<br>`dashboards.permissions:write` | Read, create, update, and delete all dashboards. |
| `fixed:dashboards:writer` | `fixed_OK2YOQGIoI1G031hVzJB6rAJQAs` | All permissions from `fixed:dashboards:reader` and <br>`dashboards:write`<br>`dashboards:delete`<br>`dashboards:create`<br>`dashboards.permissions:read`<br>`dashboards.permissions:write` | Read, create, update, and delete all dashboards. |
| `fixed:dashboards.insights:reader` | `fixed_JlBJ2_gizP8zhgaeGE2rjyZe2Rs` | `dashboards.insights:read` | Read dashboard insights data and see presence indicators. |
| `fixed:dashboards.permissions:reader` | `fixed_f17oxuXW_58LL8mYJsm4T_mCeIw` | `dashboards.permissions:read` | Read all dashboard permissions. |
| `fixed:dashboards.permissions:writer` | `fixed_CcznxhWX_Yqn8uWMXMQ-b5iFW9k` | All permissions from `fixed:dashboards.permissions:reader` and <br>`dashboards.permissions:write` | Read and update all dashboard permissions. |

@ -190,9 +190,7 @@ low
In this example, the value of the `severity` label is determined by the query value, and the possible options are `critical`, `high`, `medium`, or `low`. You can then use the `severity` label to change their notifications—for instance, sending `critical` alerts immediately or routing `low` alerts to a specific team for further review.
{{% admonition type="note" %}}
You should avoid displaying query values in labels, as this may create numerous unique alert instances—one for each distinct label value. Instead, use annotations for query values.
{{% /admonition %}}
{{< docs/shared lookup="alerts/note-dynamic-labels.md" source="grafana" version="<GRAFANA_VERSION>" >}}
### How to template a label

@ -209,9 +209,7 @@ In this example, the `severity` label is determined by the query value:
You can then use the `severity` label to control how alerts are handled. For instance, you could send `critical` alerts immediately, while routing `low` severity alerts to a team for further investigation.
{{% admonition type="note" %}}
You should avoid displaying query values in labels, as this may create many alert instances—one for each distinct label value. Instead, use annotations to convey query values.
{{% /admonition %}}
{{< docs/shared lookup="alerts/note-dynamic-labels.md" source="grafana" version="<GRAFANA_VERSION>" >}}
### Based on query label

@ -96,25 +96,25 @@ You can execute this template by passing the dot (`.`):
`Alert` contains data for an individual alert:
| Name | Type | Description |
| -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `Status` | string | Firing or resolved. |
| `Labels` | [KV](#kv) | The labels for this alert. It includes all [types of labels](ref:label-types). |
| `Annotations` | [KV](#kv) | The annotations for this alert. |
| `StartsAt` | [Time](#time) | The time the alert fired |
| `EndsAt` | [Time](#time) | Only set if the end time of an alert is known. Otherwise set to a configurable timeout period from the time since the last alert was received. |
| `GeneratorURL` | string | A link to Grafana, or the source of the alert if using an external alert generator. |
| `Fingerprint` | string | A unique string that identifies the alert. |
| Name | Type | Description |
| -------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Status` | string | Firing or resolved. |
| `Labels` | [KV](#kv) | The labels associated with this alert. <br/> It includes all [types of labels](ref:label-types), but only query labels used in the alert condition. |
| `Annotations` | [KV](#kv) | The annotations for this alert. |
| `StartsAt` | [Time](#time) | The time the alert fired |
| `EndsAt` | [Time](#time) | Only set if the end time of an alert is known. Otherwise set to a configurable timeout period from the time since the last alert was received. |
| `GeneratorURL` | string | A link to Grafana, or the source of the alert if using an external alert generator. |
| `Fingerprint` | string | A unique string that identifies the alert. |
Grafana-managed alerts include these additional properties:
| Name | Type | Description |
| -------------- | --------- | ------------------------------------------------------------------------------------ |
| `DashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. |
| `PanelURL` | string | A link to the panel if the alert has a Panel ID annotation. |
| `SilenceURL` | string | A link to silence the alert. |
| `Values` | [KV](#kv) | The values of all expressions, including Classic Conditions. |
| `ValueString` | string | A string that contains the labels and value of each reduced expression in the alert. |
| Name | Type | Description |
| -------------- | --------- | -------------------------------------------------------------------------------------------------- |
| `DashboardURL` | string | A link to the Grafana Dashboard if the alert has a Dashboard UID annotation. |
| `PanelURL` | string | A link to the panel if the alert has a Panel ID annotation. |
| `SilenceURL` | string | A link to silence the alert. |
| `Values` | [KV](#kv) | The values of expressions used to evaluate the alert condition. Only relevant values are included. |
| `ValueString` | string | A string that contains the labels and value of each reduced expression in the alert. |
This example iterates over the list of firing and resolved alerts (`.Alerts`) in the notification and prints the data for each alert:

@ -68,7 +68,7 @@ Alert instances will be routed for [notifications](ref:notifications) when they
### `No Data` and `Error` alerts
When evaluation of an alert rule produces state `No Data` or `Error`, Grafana Alerting generates a new alert instance that have the following additional labels:
When an alert rule evaluation results in a `No Data` or `Error` state, Grafana Alerting immediately creates a new alert instance —skipping the pending period—with the following additional labels:
- `alertname`: Either `DatasourceNoData` or `DatasourceError` depending on the state.
- `datasource_uid`: The UID of the data source that caused the state.

@ -81,8 +81,6 @@ Grafana enables you to share dashboards and panels with other users within your
- Reports
- Library panels
You can also invite new members to your organization from the **Share** menu. For more information, refer to [Invite new members](#invite-new-members).
You must have an authorized viewer permission to see an image rendered by a direct link. The same permission is also required to view embedded links unless you have anonymous access permission enabled for your Grafana instance.
{{< admonition type="note" >}}
@ -140,7 +138,7 @@ Learn how to configure and manage externally shared dashboards in [Externally sh
### Schedule a report
{{< admonition type="note" >}}
This feature is only available in Grafana Enterprise.
This feature is only available on Grafana Enterprise.
{{< /admonition >}}
To share your dashboard as a report, follow these steps:
@ -204,6 +202,10 @@ In addition to sharing dashboards as links, reports, and snapshots, you can expo
### Export a dashboard as PDF
{{< admonition type="note" >}}
This feature is only available on Grafana Enterprise.
{{< /admonition >}}
To export a dashboard in its current state as a PDF, follow these steps:
1. Click **Dashboards** in the main menu.
@ -365,23 +367,3 @@ To delete existing snapshots, follow these steps:
1. Click the red **x** next to the snapshot URL that you want to delete.
The snapshot is immediately deleted. You may need to clear your browser cache or use a private or incognito browser to confirm this.
## Invite new members
{{< admonition type="note" >}}
This feature is only available on Grafana Cloud.
{{< /admonition >}}
You can invite new members to your organization using the **Share** drop-down menu. You must have the `OrgUsersAdd` permission to use this feature.
To invite a new member to your organization, follow these steps:
1. Click **Dashboards** in the main menu and open any dashboard.
1. Click the **Share** drop-down list in the top-right corner and select **Invite new member**.
The **Members** page of your Grafana Cloud Portal opens.
1. Enter the email address of the new member in the provided field.
1. Make a selection in the **Role** drop-down list.
1. (Optional) Select the **Receive billing emails** checkbox, if applicable.
1. Click **Invite**.

@ -173,9 +173,7 @@ The following options are the same for both **Builder** and **Code** mode:
- **Direction** - Determines the search order. **Backward** is a backward search starting at the end of the time range. **Forward** is a forward search starting at the beginning of the time range. The default is **Backward**
- **Step** Sets the step parameter of Loki metrics queries. The default value equals to the value of `$__interval` variable, which is calculated using the time range and the width of the graph (the number of pixels).
- **Resolution** Deprecated. Sets the step parameter of Loki metrics range queries. With a resolution of `1/1`, each pixel corresponds to one data point. `1/2` retrieves one data point for every other pixel, `1/10` retrieves one data point per 10 pixels, and so on. Lower resolutions perform better.
- **Step** Sets the step parameter of Loki metrics queries. The default value equals to the value of `$__auto` variable, which is calculated using the time range and the width of the graph (the number of pixels).
## Create a log query
@ -263,6 +261,6 @@ For more information about metric queries, refer to the [Loki metric queries doc
[Annotations](ref:annotate-visualizations) overlay rich event information on top of graphs.
You can add annotation queries in the Dashboard menu's Annotations view.
You can use any non-metric Loki query as a source for annotations.
You can only use log queries as a source for annotations.
Grafana automatically uses log content as annotation text and your log stream labels as tags.
You don't need to create any additional mapping.

@ -22,23 +22,23 @@ weight: 500
# Configure SAML authentication using the configuration file
{{% admonition type="note" %}}
Available in [Grafana Enterprise](../../../../introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud).
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud).
{{% /admonition %}}
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.
You can configure SAML authentication in Grafana through one of the following methods:
- the Grafana configuration file
- the API (refer to [SSO Settings API](../../../../developers/http_api/sso-settings/))
- the user interface (refer to [Configure SAML authentication using the Grafana user interface](../saml-ui/))
- the Terraform provider (refer to [Terraform docs](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/sso_settings))
- The Grafana configuration file
- The API (refer to [SSO Settings API](/docs/grafana/<GRAFANA_VERSION>/developers/http_api/sso-settings/)
- The user interface (refer to [Configure SAML authentication using the Grafana user interface](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml-ui/)
- The Terraform provider (refer to [Terraform docs](https://registry.terraform.io/providers/grafana/grafana/<GRAFANA_VERSION>/docs/resources/sso_settings))
{{% admonition type="note" %}}
The API and Terraform support are available in Public Preview in Grafana v11.1 behind the `ssoSettingsSAML` feature toggle. You must also enable the `ssoSettingsApi` flag.
{{% /admonition %}}
All methods offer the same configuration options, but you might prefer using the Grafana configuration file or the Terraform provider if you want to keep all of Grafana's authentication settings in one place. Grafana Cloud users do not have access to Grafana configuration file, so they should configure SAML through the other methods.
All methods offer the same configuration options. However, if you want to keep all of Grafana authentication settings in one place, use the Grafana configuration file or the Terraform provider. If you are a Grafana Cloud user, you do not have access to Grafana configuration file. Instead, configure SAML through the other methods.
{{% admonition type="note" %}}
Configuration in the API takes precedence over the configuration in the Grafana configuration file. SAML settings from the API will override any SAML configuration set in the Grafana configuration file.
@ -46,6 +46,10 @@ Configuration in the API takes precedence over the configuration in the Grafana
## Supported SAML
The following indicate what Grafana supports.
### Bindings
Grafana supports the following SAML 2.0 bindings:
- From the Service Provider (SP) to the Identity Provider (IdP):
@ -56,12 +60,13 @@ Grafana supports the following SAML 2.0 bindings:
- From the Identity Provider (IdP) to the Service Provider (SP):
- `HTTP-POST` binding
In terms of security:
### Security
- Grafana supports signed and encrypted assertions.
- Grafana does not support signed or encrypted requests.
Grafana supports signed and encrypted assertions, and does _not_ support encrypted requests.
In terms of initiation, Grafana supports:
### Initiation
Grafana supports:
- SP-initiated requests
- IdP-initiated requests
@ -71,7 +76,7 @@ By default, SP-initiated requests are enabled. For instructions on how to enable
{{% admonition type="note" %}}
It is possible to set up Grafana with SAML authentication using Azure AD. However, if an Azure AD user belongs to more than 150 groups, a Graph API endpoint is shared instead.
Grafana versions 11.1 and below, do not support fetching the groups from the Graph API endpoint. As a result, users with more than 150 groups will not be able to retrieve their groups. Instead, it is recommended that you use OIDC/OAuth workflows,.
Grafana versions 11.1 and below, do not support fetching the groups from the Graph API endpoint. As a result, users with more than 150 groups will not be able to retrieve their groups. Instead, it is recommended that you use OIDC/OAuth workflows.
As of Grafana 11.2, the SAML integration offers a mechanism to retrieve user groups from the Graph API.
@ -84,16 +89,16 @@ Related links:
### Edit SAML options in the Grafana config file
1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`](../../../configure-grafana/enterprise-configuration/#enabled) to `true`.
1. Configure the [certificate and private key](#certificate-and-private-key).
1. In the `[auth.saml]` section in the Grafana configuration file, set [`enabled`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#enabled-3) to `true`.
1. Optionally, configure the [certificate and private key](#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`](../../../configure-grafana/enterprise-configuration/#idp_metadata_url) to the URL obtained from the previous step. The URL should look like `https://<your-org-id>.okta.com/app/<application-id>/sso/saml/metadata`.
1. Set the [`idp_metadata_url`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#idp_metadata_url) to the URL obtained from the previous step. The URL should look like `https://<your-org-id>.okta.com/app/<application-id>/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`](../../../configure-grafana/enterprise-configuration/#assertion_attribute_login)
- [`assertion_attribute_email`](../../../configure-grafana/enterprise-configuration/#assertion_attribute_email)
- [`assertion_attribute_name`](../../../configure-grafana/enterprise-configuration/#assertion_attribute_name)
- [`assertion_attribute_groups`](../../../configure-grafana/enterprise-configuration/#assertion_attribute_groups)
1. (Optional) Set the `name` parameter in the `[auth.saml]` section in the Grafana configuration file. This parameter replaces SAML in the Grafana user interface in locations such as the sign-in button.
- [`assertion_attribute_login`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_login)
- [`assertion_attribute_email`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_email)
- [`assertion_attribute_name`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_name)
- [`assertion_attribute_groups`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_groups)
1. Optionally, set the `name` parameter in the `[auth.saml]` section in the Grafana configuration file. This parameter replaces SAML in the Grafana user interface in locations such as the sign-in button.
1. Save the configuration file and then restart the Grafana server.
When you are finished, the Grafana configuration might look like this example:
@ -119,7 +124,7 @@ assertion_attribute_groups = Group
To use the SAML integration, in the `auth.saml` section of in the Grafana custom configuration file, set `enabled` to `true`.
Refer to [Configuration](../../../configure-grafana/) for more information about configuring Grafana.
Refer to [Configuration](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/) for more information about configuring Grafana.
## Additional configuration for HTTP-Post binding
@ -133,9 +138,11 @@ For Grafana Cloud instances, please contact Grafana Support to update the `conte
## Certificate and private key
Commonly, the certificate and key are embedded in the [IDP metadata](#configure-the-saml-toolkit-application-endpoints) and refreshed as needed by Grafana automatically. However, if your IdP expects signed requests, you must supply a 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`.
If you are directly supplying the certificate and key, 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.
@ -144,9 +151,9 @@ Grafana supports two ways of specifying both the `certificate` and `private_key`
You can only use one form of each configuration option. Using multiple forms, such as both `certificate` and `certificate_path`, results in an error.
{{% /admonition %}}
---
Always work with your company's security team on setting up certificates and private keys. If you need to generate them yourself (such as in the short term, for testing purposes, and so on), use the following example to generate your certificate and private key, including the step of ensuring that the key is generated with the [PKCS#8](https://en.wikipedia.org/wiki/PKCS_8) format.
### Generate private key for SAML authentication:
### Example of private key generation for SAML authentication
An example of how to generate a self-signed certificate and private key that's valid for one year:
@ -154,7 +161,15 @@ An example of how to generate a self-signed certificate and private key that's v
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
```
The generated `key.pem` and `cert.pem` files are then used for certificate and private_key.
Base64-encode the cert.pem and key.pem files:
(-w0 switch is not needed on Mac, only for Linux)
```sh
$ base64 -i key.pem -o key.pem.base64
$ base64 -i cert.pem -o cert.pem.base64
```
The base64-encoded values (`key.pem.base64, cert.pem.base64` files) are then used for certificate and private key.
The key you provide should look like:
@ -169,16 +184,15 @@ The key you provide should look like:
Grafana supports user authentication through Azure AD, which is useful when you want users to access Grafana using single sign-on. This topic shows you how to configure SAML authentication in Grafana with [Azure AD](https://azure.microsoft.com/en-us/services/active-directory/).
**Before you begin:**
**Before you begin**
Ensure you have permission to administer SAML authentication. For more information about roles and permissions in Grafana, refer to [Roles and permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/).
Learn the [limitations of Azure AD SAML] (https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference#groups-overage-claim) integration.
Configure SAML integration with Azure AD, [creating an Enterprise Application](#add-microsoft-entra-saml-toolkit-from-the-gallery) inside the Azure AD organization first and then [enable single sign-on](#configure-the-saml-toolkit-application-endpoints).
- Ensure you have permission to administer SAML authentication. For more information about roles and permissions in Grafana.
- [Roles and permissions](../../../../administration/roles-and-permissions/).
- Learn the limitations of Azure AD SAML integration.
- [Azure AD SAML limitations](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference#groups-overage-claim)
- Configure SAML integration with Azure AD, create an app integration inside the Azure AD organization first.
- [Add app integration in Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/add-application-portal-configure)
- If you have users that belong to more than 150 groups, you need to configure a registered application to provide an Azure Graph API to retrieve the groups.
- [Setup Azure AD Graph API applications](#set-up-saml-with-azure-ad)
If you have users that belong to more than 150 groups, configure a registered application to provide an Azure Graph API to retrieve the groups. Refer to [Setup Azure AD Graph API applications](#configure-a-graph-api-application-in-azure-ad).
### Generate self-signed certificates
@ -277,7 +291,7 @@ Grafana supports user authentication through Okta, which is useful when you want
**Before you begin:**
- To configure SAML integration with Okta, create an app integration inside the Okta organization first. [Add app 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 roles and permissions in Grafana, refer to [Roles and permissions](../../../../administration/roles-and-permissions/).
- Ensure you have permission to administer SAML authentication. For more information about roles and permissions in Grafana, refer to [Roles and permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/).
**To set up SAML with Okta:**
@ -391,7 +405,7 @@ Additionally, Grafana did not support IdP sessions and could not include the `Se
Starting from Grafana version 11.5, Grafana uses the `NameID` from the SAML assertion to create the logout request. If the `NameID` is not present in the assertion, Grafana defaults to using the user's `Login` attribute. Additionally, Grafana supports including the `SessionIndex` in the logout request if it is provided in the SAML assertion by the IdP.
{{% admonition type="note" %}}
These improvements are available in public preview behind the `improvedExternalSessionHandling` feature toggle, starting from Grafana v11.5. To enable it, refer to the [Configure feature toggles](../../../configure-grafana/feature-toggles/)
These improvements are available in public preview behind the `improvedExternalSessionHandlingSAML` feature toggle, starting from Grafana v11.5. To enable it, refer to the [Configure feature toggles](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/feature-toggles/)
{{% /admonition %}}
### Assertion mapping
@ -435,11 +449,11 @@ auto_login = true
Group synchronization allows you to map user groups from an identity provider to Grafana teams and roles.
To use SAML group synchronization, set [`assertion_attribute_groups`](../../../configure-grafana/enterprise-configuration/#assertion_attribute_groups) to the attribute name where you store user groups.
To use SAML group synchronization, set [`assertion_attribute_groups`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#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 to Grafana teams and grant them roles.
{{% admonition type="note" %}}
Team sync allows you sync users from SAML to Grafana teams. It does not automatically create teams in Grafana. You need to create teams in Grafana before you can use this feature.
Team sync allows you sync users from SAML to Grafana teams, but you must create teams in Grafana before you can use this feature. It does not automatically create teams in Grafana.
{{% /admonition %}}
Given the following partial SAML assertion:
@ -474,22 +488,22 @@ The following `External Group ID`s would be valid for configuring team sync or r
- `admins_group`
- `division_1`
To learn more about how to configure group synchronization, refer to [Configure team sync](../../configure-team-sync/) and [Configure group attribute sync](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-group-attribute-sync) documentation.
To learn more about how to configure group synchronization, refer to [Configure team sync](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-group-attribute-sync/) and [Configure group attribute sync](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-group-attribute-sync) documentation.
### Configure role sync
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 [Roles and permissions](../../../../administration/roles-and-permissions/).
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 [Roles and permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/).
1. In the configuration file, set [`assertion_attribute_role`](../../../configure-grafana/enterprise-configuration/#assertion_attribute_role) option to the attribute name where the role information will be extracted from.
1. Set the [`role_values_none`](../../../configure-grafana/enterprise-configuration/#role_values_none) option to the values mapped to the `None` role.
1. Set the [`role_values_viewer`](../../../configure-grafana/enterprise-configuration/#role_values_viewer) option to the values mapped to the `Viewer` role.
1. Set the [`role_values_editor`](../../../configure-grafana/enterprise-configuration/#role_values_editor) option to the values mapped to the `Editor` role.
1. Set the [`role_values_admin`](../../../configure-grafana/enterprise-configuration/#role_values_admin) option to the values mapped to the organization `Admin` role.
1. Set the [`role_values_grafana_admin`](../../../configure-grafana/enterprise-configuration/#role_values_grafana_admin) option to the values mapped to the `Grafana Admin` role.
1. In the configuration file, set [`assertion_attribute_role`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#assertion_attribute_role) option to the attribute name where the role information will be extracted from.
1. Set the [`role_values_none`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#role_values_none) option to the values mapped to the `None` role.
1. Set the [`role_values_viewer`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#role_values_viewer) option to the values mapped to the `Viewer` role.
1. Set the [`role_values_editor`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#role_values_editor) option to the values mapped to the `Editor` role.
1. Set the [`role_values_admin`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#role_values_admin) option to the values mapped to the organization `Admin` role.
1. Set the [`role_values_grafana_admin`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#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 role specified by the `auto_assign_org_role` config option will be assigned. If the `auto_assign_org_role` field is not set then the user role will default to `Viewer`.
For more information about roles and permissions in Grafana, refer to [Roles and permissions](../../../../administration/roles-and-permissions/).
For more information about roles and permissions in Grafana, refer to [Roles and permissions](/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/).
Example configuration:
@ -518,8 +532,8 @@ skip_org_role_sync = true
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`](../../../configure-grafana/enterprise-configuration/#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`](../../../configure-grafana/enterprise-configuration/#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`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#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`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#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:
@ -566,7 +580,7 @@ You can use `*` as the Grafana organization in the mapping if you want all users
### Configure allowed organizations
With the [`allowed_organizations`](../../../configure-grafana/enterprise-configuration/#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`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/enterprise-configuration/#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.
To put values containing spaces in the list, use the following JSON syntax:
@ -632,11 +646,11 @@ resource "grafana_sso_settings" "saml_sso_settings" {
}
```
Go to [Terraform Registry](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/sso_settings) for a complete reference on using the `grafana_sso_settings` resource.
Go to [Terraform Registry](https://registry.terraform.io/providers/grafana/grafana/<GRAFANA_VERSION>/docs/resources/sso_settings) for a complete reference on using the `grafana_sso_settings` resource.
## Troubleshoot SAML authentication in Grafana
To troubleshoot and get more log information, enable SAML debug logging in the configuration file. Refer to [Configuration](../../../configure-grafana/#filters) for more information.
To troubleshoot and get more log information, enable SAML debug logging in the configuration file. Refer to [Configuration](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#filters) for more information.
```ini
[log]
@ -692,7 +706,7 @@ The keys you provide should look like:
When the user logs in using SAML and gets presented with "origin not allowed", the user might be issuing the login from an IdP (identity provider) service or the user is behind a reverse proxy. This potentially happens as Grafana's CSRF checks deem the requests to be invalid. For more information [CSRF](https://owasp.org/www-community/attacks/csrf).
To solve this issue, you can configure either the [`csrf_trusted_origins`](../../../configure-grafana/#csrf_trusted_origins) or [`csrf_additional_headers`](../../../configure-grafana/#csrf_additional_headers) option in the SAML configuration.
To solve this issue, you can configure either the [`csrf_trusted_origins`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#csrf_trusted_origins) or [`csrf_additional_headers`](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#csrf_additional_headers) option in the SAML configuration.
Example of a configuration file:
@ -735,7 +749,7 @@ Ensure cookie_secure is set to true to ensure that cookies are only sent over HT
## 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](../../../configure-grafana/#override-configuration-with-environment-variables).
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](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/#override-configuration-with-environment-variables).
| Setting | Required | Description | Default |
| ---------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |

@ -0,0 +1,13 @@
---
title: 'Alerting Produces '
---
#### Produces
- `application/json`
- `application/yaml`
- `application/terraform+hcl`
- `text/yaml`
- `text/hcl`
These outputs are for [file provisioning](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning) or [Terraform provisioning](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning), and they-including the JSON output—cannot be used to update resources via the HTTP API.

@ -14,7 +14,12 @@ For more information on the differences between Grafana-managed and data source-
## Grafana-managed endpoints
Note that the JSON format from most of the following endpoints is not fully compatible with [provisioning via configuration JSON files](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning/).
{{< admonition type="note" >}}
In the Alerting provisioning HTTP API, the endpoints use a JSON format that differs from the format returned by the `export` endpoints.
The `export` endpoints allow you to export alerting resources in a JSON format suitable for [provisioning via files](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/alerting/set-up/provision-alerting-resources/file-provisioning/). However, this format cannot be used to update resources via the HTTP API.
{{< /admonition >}}
### Alert rules
@ -373,16 +378,21 @@ Content-Type: application/json
### Edit resources in the Grafana UI
By default, you cannot edit API-provisioned alerting resources in Grafana. To enable editing these resources in the Grafana UI, add the `X-Disable-Provenance` header to the following requests in the API:
By default, you cannot edit API-provisioned alerting resources in Grafana.
To enable editing these resources in the Grafana UI, add the **`X-Disable-Provenance: true`** header to the following API requests:
- `POST /api/v1/provisioning/alert-rules`
- `PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}` (calling this endpoint will change provenance for all alert rules within the alert group)
- `PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}` _(This endpoint changes provenance for all alert rules in the alert group)_
- `POST /api/v1/provisioning/contact-points`
- `POST /api/v1/provisioning/mute-timings`
- `PUT /api/v1/provisioning/policies`
- `PUT /api/v1/provisioning/templates/{name}`
- `PUT /api/v1/provisioning/policies`
To reset the notification policy tree to the default and unlock it for editing in the Grafana UI, use:
To reset the notification policy tree to the default and unlock it for editing in the Grafana UI, use the `DELETE /api/v1/provisioning/policies` endpoint.
- `DELETE /api/v1/provisioning/policies`
## Data source-managed resources
@ -408,10 +418,10 @@ DELETE /api/v1/provisioning/alert-rules/:uid
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ------ | -------- | --------- | :------: | ------- | --------------------------------------------------------- |
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ------ | ------- | :------: | ------- | --------------------------------------------------------- |
| `UID` | path | string | string | ✓ | | Alert rule UID |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
{{% /responsive-table %}}
@ -437,9 +447,9 @@ DELETE /api/v1/provisioning/contact-points/:uid
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------ |
| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier |
| Name | Source | Type | Go type | Required | Default | Description |
| ----- | ------ | ------ | ------- | :------: | ------- | ------------------------------------------ |
| `UID` | path | string | string | ✓ | | UID is the contact point unique identifier |
#### All responses
@ -463,10 +473,10 @@ DELETE /api/v1/provisioning/mute-timings/:name
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ------- | ------- | ------ | -------- | --------- | :------: | ------- | ------------------------------------------------------------------------------------------------------------- |
| name | `path` | string | `string` | | ✓ | | Mute timing name |
| version | `query` | string | `string` | | | | Current version of the resource. Used for optimistic concurrency validation. Keep empty to bypass validation. |
| Name | Source | Type | Go type | Required | Default | Description |
| --------- | ------ | ------ | ------- | :------: | ------- | ------------------------------------------------------------------------------------------------------------- |
| `name` | path | string | string | ✓ | | Mute timing name |
| `version` | query | string | string | | | Current version of the resource. Used for optimistic concurrency validation. Keep empty to bypass validation. |
#### All responses
@ -499,10 +509,10 @@ DELETE /api/v1/provisioning/templates/:name
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ------- | ------- | ------ | -------- | --------- | :------: | ------- | ------------------------------------------------------------------------------------------------------------- |
| name | `path` | string | `string` | | ✓ | | Name of the template group |
| version | `query` | string | `string` | | | | Current version of the resource. Used for optimistic concurrency validation. Keep empty to bypass validation. |
| Name | Source | Type | Go type | Required | Default | Description |
| --------- | ------ | ------ | ------- | :------: | ------- | ------------------------------------------------------------------------------------------------------------- |
| `name` | path | string | string | ✓ | | Name of the template group |
| `version` | query | string | string | | | Current version of the resource. Used for optimistic concurrency validation. Keep empty to bypass validation. |
#### All responses
@ -535,9 +545,9 @@ GET /api/v1/provisioning/alert-rules/:uid
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- |
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
| Name | Source | Type | Go type | Required | Default | Description |
| ----- | ------ | ------ | ------- | :------: | ------- | -------------- |
| `UID` | path | string | string | ✓ | | Alert rule UID |
#### All responses
@ -568,21 +578,15 @@ Status: Not Found
GET /api/v1/provisioning/alert-rules/:uid/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------- | ------- | ------- | -------- | --------- | :------: | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------- | ------ | ------- | ------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `UID` | path | string | string | ✓ | | Alert rule UID |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
#### All responses
@ -615,10 +619,10 @@ GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| --------- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- |
| FolderUID | `path` | string | `string` | | ✓ | | |
| Group | `path` | string | `string` | | ✓ | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ----------- | ------ | ------ | ------- | :------: | ------- | ----------- |
| `FolderUID` | path | string | string | ✓ | | |
| `Group` | path | string | string | ✓ | | |
#### All responses
@ -649,22 +653,16 @@ Status: Not Found
GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| --------- | ------- | ------- | -------- | --------- | :------: | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| FolderUID | `path` | string | `string` | | ✓ | | |
| Group | `path` | string | `string` | | ✓ | | |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| Name | Source | Type | Go type | Required | Default | Description |
| ----------- | ------ | ------- | ------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `FolderUID` | path | string | string | ✓ | | |
| `Group` | path | string | string | ✓ | | |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
#### All responses
@ -717,20 +715,14 @@ Status: OK
GET /api/v1/provisioning/alert-rules/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------- | ------- | ------- | -------- | --------- | :------: | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------- | ------ | ------- | ------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
#### All responses
@ -763,9 +755,9 @@ GET /api/v1/provisioning/contact-points
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ---- | ------- | ------ | -------- | --------- | :------: | ------- | -------------- |
| name | `query` | string | `string` | | | | Filter by name |
| Name | Source | Type | Go type | Required | Default | Description |
| ------ | ------ | ------ | ------- | :------: | ------- | -------------- |
| `name` | query | string | string | | | Filter by name |
#### All responses
@ -789,22 +781,16 @@ Status: OK
GET /api/v1/provisioning/contact-points/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------- | ------- | ------- | -------- | --------- | :------: | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| decrypt | `query` | boolean | `bool` | | | | Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings. |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| name | `query` | string | `string` | | | | Filter by name |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------- | ------ | ------- | ------- | :------: | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decrypt` | query | boolean | `bool` | | | Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings. |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
| `name` | query | string | string | | | Filter by name |
#### All responses
@ -839,9 +825,9 @@ GET /api/v1/provisioning/mute-timings/:name
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- |
| name | `path` | string | `string` | | ✓ | | Mute timing name |
| Name | Source | Type | Go type | Required | Default | Description |
| ------ | ------ | ------ | ------- | :------: | ------- | ---------------- |
| `name` | path | string | string | ✓ | | Mute timing name |
#### All responses
@ -894,20 +880,14 @@ Status: OK
GET /api/v1/provisioning/mute-timings/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------- | ------- | ------- | -------- | --------- | :------: | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------- | ------ | ------- | ------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
#### All responses
@ -940,21 +920,15 @@ Status: Forbidden
GET /api/v1/provisioning/mute-timings/:name/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------- | ------- | ------- | -------- | --------- | :------: | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| name | `path` | string | `string` | | ✓ | | Mute timing name. |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------- | ------ | ------- | ------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `name` | path | string | string | ✓ | | Mute timing name. |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
#### All responses
@ -1009,20 +983,14 @@ Status: OK
GET /api/v1/provisioning/policies/export
```
#### Produces
- application/json
- application/yaml
- application/terraform+hcl
- text/yaml
- text/hcl
{{< docs/shared lookup="alerts/alerting-provisioning-export-produces.md" source="grafana" version="<GRAFANA_VERSION>" >}}
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------- | ------- | ------- | -------- | --------- | :------: | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml, json or hcl. Accept header can also be used, but the query parameter will take precedence. |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------- | ------ | ------- | ------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `download` | query | boolean | `bool` | | | Whether to initiate a download of the file or not. |
| `format` | query | string | string | | `yaml` | Format of the downloaded file, either `yaml`, `json` or `hcl`. Accept header can also be used, but the query parameter will take precedence. |
#### All responses
@ -1057,9 +1025,9 @@ GET /api/v1/provisioning/templates/:name
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------------------- |
| name | `path` | string | `string` | | ✓ | | Name of the template group |
| Name | Source | Type | Go type | Required | Default | Description |
| ------ | ------ | ------ | ------- | :------: | ------- | -------------------------- |
| `name` | path | string | string | ✓ | | Name of the template group |
#### All responses
@ -1116,10 +1084,10 @@ POST /api/v1/provisioning/alert-rules
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ----------------------------------------------- | ----------------------------- | :------: | ------- | --------------------------------------------------------- |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | |
{{% /responsive-table %}}
@ -1160,10 +1128,10 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ----------------------------------------------- | ----------------------------- | :------: | ------- | --------------------------------------------------------- |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | |
{{% /responsive-table %}}
@ -1202,10 +1170,10 @@ POST /api/v1/provisioning/mute-timings
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | --------------------------------------- | ------------------------- | :------: | ------- | --------------------------------------------------------- |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | |
{{% /responsive-table %}}
@ -1244,11 +1212,11 @@ PUT /api/v1/provisioning/alert-rules/:uid
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ----------------------------------------------- | ----------------------------- | :------: | ------- | --------------------------------------------------------- | --- |
| `UID` | path | string | string | ✓ | | Alert rule UID |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | |
{{% /responsive-table %}}
@ -1287,12 +1255,12 @@ PUT /api/v1/provisioning/folder/:folderUid/rule-groups/:group
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ----------------------------------- | ----------------------- | --------- | :------: | ------- | ------------------------------------------------------------------------------------------------------- |
| FolderUID | `path` | string | `string` | | ✓ | | |
| Group | `path` | string | `string` | | ✓ | | |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | | This action is idempotent and rules included in this body will overwrite configured rules for the group |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ----------------------------------- | ----------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------- |
| `FolderUID` | path | string | string | ✓ | | |
| `Group` | path | string | string | ✓ | | |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | This action is idempotent and rules included in this body will overwrite configured rules for the group |
{{% /responsive-table %}}
@ -1331,11 +1299,11 @@ PUT /api/v1/provisioning/contact-points/:uid
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ----------------------------------------------- | ----------------------------- | :------: | ------- | --------------------------------------------------------- |
| `UID` | path | string | string | ✓ | | UID is the contact point unique identifier |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | |
{{% /responsive-table %}}
@ -1374,11 +1342,11 @@ PUT /api/v1/provisioning/mute-timings/:name
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| name | `path` | string | `string` | | | | Mute timing name |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | --------------------------------------- | ------------------------- | -------- | :-----: | --------------------------------------------------------- |
| `name` | path | string | string | | | Mute timing name |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | |
{{% /responsive-table %}}
@ -1426,10 +1394,10 @@ PUT /api/v1/provisioning/policies
{{% responsive-table %}}
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | --------------- | -------------- | --------- | :------: | ------- | --------------------------------------------------------- |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | --------------- | -------------- | :------: | ------- | --------------------------------------------------------- |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [Route](#route) | `models.Route` | | | The new notification routing tree to use |
{{% /responsive-table %}}
@ -1468,11 +1436,11 @@ PUT /api/v1/provisioning/templates/:name
#### Parameters
| Name | Source | Type | Go type | Separator | Required | Default | Description |
| -------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | --------------------------------------------------------- |
| name | `path` | string | `string` | | ✓ | | Name of the template group |
| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI |
| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | |
| Name | Source | Type | Go type | Required | Default | Description |
| ---------------------------- | ------ | ------------------------------------------------------------- | ------------------------------------ | -------- | :-----: | --------------------------------------------------------- | --- |
| `name` | path | string | string | ✓ | | Name of the template group |
| `X-Disable-Provenance: true` | header | string | string | | | Allows editing of provisioned resources in the Grafana UI |
| `Body` | body | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | |
{{% /responsive-table %}}
@ -1544,14 +1512,13 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------ | ------- |
| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '**expr**' for a Server Side Expression operation. | |
| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | |
| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. |
| It can be used to distinguish different types of queries. | |
| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | |
| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------------- | ----------------------------------------- | ------------------- | :------: | ------- | -------------------------------------------------------------------------------------------------------------------- | ------- |
| `datasourceUid` | string | string | | | Grafana data source unique identifier; it should be '**expr**' for a Server Side Expression operation. | |
| `model` | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | |
| `queryType` | string | string | | | QueryType is an optional identifier for the type of query. It can be used to distinguish different types of queries. | |
| `refId` | string | string | | | RefID is the unique identifier of the query, set by the frontend call. | |
| `relativeTimeRange` | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
{{% /responsive-table %}}
@ -1561,13 +1528,13 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ----------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- |
| datasourceUid | string | `string` | | | | |
| model | [interface{}](#interface) | `interface{}` | | | | |
| queryType | string | `string` | | | | |
| refId | string | `string` | | | | |
| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- |
| `datasourceUid` | string | string | | | | |
| `model` | [interface{}](#interface) | `interface{}` | | | | |
| `queryType` | string | string | | | | |
| `refId` | string | string | | | | |
| `relativeTimeRange` | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
{{% /responsive-table %}}
@ -1577,20 +1544,20 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ------------ | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- |
| annotations | map of string | `map[string]string` | | | | |
| condition | string | `string` | | | | |
| dashboardUid | string | `string` | | | | |
| data | [][AlertQueryExport](#alert-query-export) | `[]*AlertQueryExport` | | | | |
| execErrState | string | `string` | | | | |
| for | [Duration](#duration) | `Duration` | | | | |
| isPaused | boolean | `bool` | | | | |
| labels | map of string | `map[string]string` | | | | |
| noDataState | string | `string` | | | | |
| panelId | int64 (formatted integer) | `int64` | | | | |
| title | string | `string` | | | | |
| uid | string | `string` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| -------------- | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- |
| `annotations` | map of string | `map[string]string` | | | | |
| `condition` | string | string | | | | |
| `dashboardUid` | string | string | | | | |
| `data` | [][AlertQueryExport](#alert-query-export) | `[]*AlertQueryExport` | | | | |
| `execErrState` | string | string | | | | |
| `for` | [Duration](#duration) | Duration | | | | |
| `isPaused` | boolean | `bool` | | | | |
| `labels` | map of string | `map[string]string` | | | | |
| `noDataState` | string | string | | | | |
| `panelId` | int64 (formatted integer) | int64 | | | | |
| `title` | string | string | | | | |
| `uid` | string | string | | | | |
{{% /responsive-table %}}
@ -1600,12 +1567,12 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- |
| folderUid | string | `string` | | | | |
| interval | int64 (formatted integer) | `int64` | | | | |
| rules | [][ProvisionedAlertRule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | |
| title | string | `string` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ----------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- |
| `folderUid` | string | string | | | | |
| `interval` | int64 (formatted integer) | int64 | | | | |
| `rules` | [][ProvisionedAlertRule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | |
| `title` | string | string | | | | |
{{% /responsive-table %}}
@ -1615,13 +1582,13 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| -------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- |
| folder | string | `string` | | | | |
| interval | [Duration](#duration) | `Duration` | | | | |
| name | string | `string` | | | | |
| orgId | int64 (formatted integer) | `int64` | | | | |
| rules | [][AlertRuleExport](#alert-rule-export) | `[]*AlertRuleExport` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ---------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- |
| `folder` | string | string | | | | |
| `interval` | [Duration](#duration) | Duration | | | | |
| `name` | string | string | | | | |
| `orgId` | int64 (formatted integer) | int64 | | | | |
| `rules` | [][AlertRuleExport](#alert-rule-export) | `[]*AlertRuleExport` | | | | |
{{% /responsive-table %}}
@ -1631,12 +1598,12 @@ Status: Accepted
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ------------- | --------------------------------------------------------- | ----------------------------- | :------: | ------- | ----------- | ------- |
| apiVersion | int64 (formatted integer) | `int64` | | | | |
| contactPoints | [][ContactPointExport](#contact-point-export) | `[]*ContactPointExport` | | | | |
| groups | [][AlertRuleGroupExport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | |
| policies | [][NotificationPolicyExport](#notification-policy-export) | `[]*NotificationPolicyExport` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| --------------- | --------------------------------------------------------- | ----------------------------- | :------: | ------- | ----------- | ------- |
| `apiVersion` | int64 (formatted integer) | int64 | | | | |
| `contactPoints` | [][ContactPointExport](#contact-point-export) | `[]*ContactPointExport` | | | | |
| `groups` | [][AlertRuleGroupExport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | |
| `policies` | [][NotificationPolicyExport](#notification-policy-export) | `[]*NotificationPolicyExport` | | | | |
{{% /responsive-table %}}
@ -1644,11 +1611,11 @@ Status: Accepted
**Properties**
| Name | Type | Go type | Required | Default | Description | Example |
| --------- | ------------------------------------ | ------------------- | :------: | ------- | ----------- | ------- |
| name | string | `string` | | | | |
| orgId | int64 (formatted integer) | `int64` | | | | |
| receivers | [][ReceiverExport](#receiver-export) | `[]*ReceiverExport` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ----------- | ------------------------------------ | ------------------- | :------: | ------- | ----------- | ------- |
| `name` | string | string | | | | |
| `orgId` | int64 (formatted integer) | int64 | | | | |
| `receivers` | [][ReceiverExport](#receiver-export) | `[]*ReceiverExport` | | | | |
### <span id="contact-points"></span> ContactPoints
@ -1656,9 +1623,9 @@ Status: Accepted
### <span id="duration"></span> Duration
| Name | Type | Go type | Default | Description | Example |
| -------- | ------ | ------- | ------- | ----------- | ------- |
| Duration | string | int64 | | | |
| Name | Type | Go type | Default | Description | Example |
| ---------- | ------ | ------- | ------- | ----------- | ------- |
| `Duration` | string | int64 | | | |
### <span id="embedded-contact-point"></span> EmbeddedContactPoint
@ -1670,14 +1637,14 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------- | ------------- | -------- | :------: | ------- | ---------------------------------------------------------------------------------- | ----------------------- |
| disableResolveMessage | boolean | `bool` | | | | `false` |
| name | string | `string` | | | `name` groups multiple contact points with the same name in the UI. | `webhook_1` |
| provenance | string | `string` | | | | |
| settings | [JSON](#json) | `JSON` | ✓ | | | |
| type | string | `string` | ✓ | | | `webhook` |
| uid | string | `string` | | | UID is the unique identifier of the contact point. The UID can be set by the user. | `my_external_reference` |
| Name | Type | Go type | Required | Default | Description | Example |
| ----------------------- | ------------- | ------- | :------: | ------- | ---------------------------------------------------------------------------------- | ----------------------- |
| `disableResolveMessage` | boolean | `bool` | | | | false |
| `name` | string | string | | | `name` groups multiple contact points with the same name in the UI. | `webhook_1` |
| `provenance` | string | string | | | | |
| `settings` | [JSON](#json) | JSON | ✓ | | | |
| `type` | string | string | ✓ | | | `webhook` |
| `uid` | string | string | | | UID is the unique identifier of the contact point. The UID can be set by the user. | `my_external_reference` |
{{% /responsive-table %}}
@ -1691,9 +1658,9 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
### <span id="match-type"></span> MatchType
| Name | Type | Go type | Default | Description | Example |
| --------- | ------------------------- | ------- | ------- | ----------- | ------- |
| MatchType | int64 (formatted integer) | int64 | | | |
| Name | Type | Go type | Default | Description | Example |
| ----------- | ------------------------- | ------- | ------- | ----------- | ------- |
| `MatchType` | int64 (formatted integer) | int64 | | | |
### <span id="matcher"></span> Matcher
@ -1701,11 +1668,11 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ----- | ------------------------ | ----------- | :------: | ------- | ----------- | ------- |
| Name | string | `string` | | | | |
| Type | [MatchType](#match-type) | `MatchType` | | | | |
| Value | string | `string` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------- | ------------------------ | --------- | :------: | ------- | ----------- | ------- |
| `Name` | string | string | | | | |
| `Type` | [MatchType](#match-type) | MatchType | | | | |
| `Value` | string | string | | | | |
{{% /responsive-table %}}
@ -1723,11 +1690,11 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| -------------- | -------------------------------- | ----------------- | :------: | ------- | ------------------- | ------- |
| name | string | `string` | | | | |
| time_intervals | [][TimeInterval](#time-interval) | `[]*TimeInterval` | | | | |
| version | string | `string` | | | Version of resource | |
| Name | Type | Go type | Required | Default | Description | Example |
| ---------------- | -------------------------------- | ----------------- | :------: | ------- | ------------------- | ------- |
| `name` | string | string | | | | |
| `time_intervals` | [][TimeInterval](#time-interval) | `[]*TimeInterval` | | | | |
| `version` | string | string | | | Version of resource | |
{{% /responsive-table %}}
@ -1751,10 +1718,10 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
**Properties**
| Name | Type | Go type | Required | Default | Description | Example |
| ------ | ---------------------------- | ------------- | :------: | ------- | ----------- | ------- |
| Policy | [RouteExport](#route-export) | `RouteExport` | | | inline | |
| orgId | int64 (formatted integer) | `int64` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| -------- | ---------------------------- | ---------------------------- | :------: | ------- | ----------- | ------- |
| `Policy` | [RouteExport](#route-export) | [RouteExport](#route-export) | | | inline | |
| `orgId` | int64 (formatted integer) | int64 | | | | |
### <span id="notification-template"></span> NotificationTemplate
@ -1762,12 +1729,12 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ---------- | ------------------------- | ------------ | :------: | ------- | ------------------- | ------- |
| name | string | `string` | | | | |
| provenance | [Provenance](#provenance) | `Provenance` | | | | |
| template | string | `string` | | | | |
| version | string | `string` | | | Version of resource | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------ | ------------------------- | ------------------------- | :------: | ------- | ------------------- | ------- |
| `name` | string | string | | | | |
| `provenance` | [Provenance](#provenance) | [Provenance](#provenance) | | | | |
| `template` | string | string | | | | |
| `version` | string | string | | | Version of resource | |
{{% /responsive-table %}}
@ -1777,10 +1744,10 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| -------- | ------ | -------- | :------: | ------- | ------------------------------------------------------- | ------- |
| template | string | `string` | | | | |
| version | string | `string` | | | Version of resource. Should be empty for new templates. | |
| Name | Type | Go type | Required | Default | Description | Example |
| ---------- | ------ | ------- | :------: | ------- | ------------------------------------------------------- | ------- |
| `template` | string | string | | | | |
| `version` | string | string | | | Version of resource. Should be empty for new templates. | |
{{% /responsive-table %}}
@ -1800,9 +1767,9 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
### <span id="provenance"></span> Provenance
| Name | Type | Go type | Default | Description | Example |
| ---------- | ------ | ------- | ------- | ----------- | ------- |
| Provenance | string | string | | | |
| Name | Type | Go type | Default | Description | Example |
| ------------ | ------ | ------- | ------- | ----------- | ------- |
| `Provenance` | string | string | | | |
### <span id="provisioned-alert-rule"></span> ProvisionedAlertRule
@ -1810,24 +1777,24 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| annotations | map of string | `map[string]string` | | | Optional key-value pairs. `dashboardUId` and `panelId` must be set together; one cannot be set without the other. | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` |
| condition | string | `string` | ✓ | | | `A` |
| data | [][AlertQuery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` |
| execErrState | string | `string` | ✓ | | | |
| folderUID | string | `string` | ✓ | | | `project_x` |
| for | [Duration](#duration) | `Duration` | ✓ | | | |
| id | int64 (formatted integer) | `int64` | | | | |
| isPaused | boolean | `bool` | | | | `false` |
| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` |
| noDataState | string | `string` | ✓ | | | |
| orgID | int64 (formatted integer) | `int64` | ✓ | | | |
| provenance | [Provenance](#provenance) | `Provenance` | | | | |
| ruleGroup | string | `string` | ✓ | | | `eval_group_1` |
| title | string | `string` | ✓ | | | `Always firing` |
| uid | string | `string` | | | | |
| updated | date-time (formatted string) | `strfmt.DateTime` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------- | ---------------------------- | ------------------------- | :------: | ------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `annotations` | map of string | `map[string]string` | | | Optional key-value pairs. `dashboardUId` and `panelId` must be set together; one cannot be set without the other. | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` |
| `condition` | string | string | ✓ | | | `A` |
| `data` | [][AlertQuery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` |
| execErrState | string | string | ✓ | | | |
| `folderUID` | string | string | ✓ | | | `project_x` |
| `for` | [Duration](#duration) | [Duration](#duration) | ✓ | | | |
| `id` | int64 (formatted integer) | int64 | | | | |
| `isPaused` | boolean | `bool` | | | | `false` |
| `labels` | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` |
| `noDataState` | string | string | ✓ | | | |
| `orgID` | int64 (formatted integer) | `int64 | ✓ | | | |
| `provenance` | [Provenance](#provenance) | [Provenance](#provenance) | | | | |
| `ruleGroup` | string | string | ✓ | | | `eval_group_1` |
| `title` | string | string | ✓ | | | `Always firing` |
| `uid` | string | string | | | | |
| `updated` | date-time (formatted string) | `strfmt.DateTime` | | | | |
{{% /responsive-table %}}
@ -1843,12 +1810,12 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
**Properties**
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------- | -------------------------- | ------------ | :------: | ------- | ----------- | ------- |
| disableResolveMessage | boolean | `bool` | | | | |
| settings | [RawMessage](#raw-message) | `RawMessage` | | | | |
| type | string | `string` | | | | |
| uid | string | `string` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ----------------------- | -------------------------- | ---------- | :------: | ------- | ----------- | ------- |
| `disableResolveMessage` | boolean | `bool` | | | | |
| `settings` | [RawMessage](#raw-message) | RawMessage | | | | |
| `type` | string | string | | | | |
| `uid` | string | string | | | | |
### <span id="regexp"></span> Regexp
@ -1866,10 +1833,10 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ---- | --------------------- | ---------- | :------: | ------- | ----------- | ------- |
| from | [Duration](#duration) | `Duration` | | | | |
| to | [Duration](#duration) | `Duration` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------ | --------------------- | -------- | :------: | ------- | ----------- | ------- |
| `from` | [Duration](#duration) | Duration | | | | |
| `to` | [Duration](#duration) | Duration | | | | |
{{% /responsive-table %}}
@ -1882,21 +1849,21 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- |
| continue | boolean | `bool` | | | | |
| group_by | []string | `[]string` | | | | |
| group_interval | string | `string` | | | | |
| group_wait | string | `string` | | | | |
| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | |
| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | |
| matchers | [Matchers](#matchers) | `Matchers` | | | | |
| mute_time_intervals | []string | `[]string` | | | | |
| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | |
| provenance | [Provenance](#provenance) | `Provenance` | | | | |
| receiver | string | `string` | | | | |
| repeat_interval | string | `string` | | | | |
| routes | [][Route](#route) | `[]*Route` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- |
| `continue` | boolean | `bool` | | | | |
| `group_by` | []string | `[]string` | | | | |
| `group_interval` | string | string | | | | |
| `group_wait` | string | string | | | | |
| `match` | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | |
| `match_re` | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | |
| `matchers` | [Matchers](#matchers) | `Matchers` | | | | |
| `mute_time_intervals` | []string | `[]string` | | | | |
| `object_matchers` | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | |
| `provenance` | [Provenance](#provenance) | Provenance | | | | |
| `receiver` | string | string | | | | |
| `repeat_interval` | string | string | | | | |
| `routes` | [][Route](#route) | `[]*Route` | | | | |
{{% /responsive-table %}}
@ -1907,20 +1874,20 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
**Properties**
| Name | Type | Go type | Required | Default | Description | Example |
| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- |
| continue | boolean | `bool` | | | | |
| group_by | []string | `[]string` | | | | |
| group_interval | string | `string` | | | | |
| group_wait | string | `string` | | | | |
| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | |
| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | |
| matchers | [Matchers](#matchers) | `Matchers` | | | | |
| mute_time_intervals | []string | `[]string` | | | | |
| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | |
| receiver | string | `string` | | | | |
| repeat_interval | string | `string` | | | | |
| routes | [][RouteExport](#route-export) | `[]*RouteExport` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| --------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- |
| `continue` | boolean | `bool` | | | | |
| `group_by` | []string | `[]string` | | | | |
| `group_interval` | string | string | | | | |
| `group_wait` | string | string | | | | |
| `match` | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | |
| `match_re` | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | |
| `matchers` | [Matchers](#matchers) | `Matchers` | | | | |
| `mute_time_intervals` | []string | `[]string` | | | | |
| `object_matchers` | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | |
| `receiver` | string | string | | | | |
| `repeat_interval` | string | string | | | | |
| `routes` | [][RouteExport](#route-export) | `[]*RouteExport` | | | | |
### <span id="time-interval"></span> TimeInterval
@ -1931,14 +1898,14 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- |
| days_of_month | []string | `[]string` | | | | |
| location | string | `string` | | | | |
| months | []string | `[]string` | | | | |
| times | [][TimeRange](#time-range) | `[]*TimeRange` | | | | |
| weekdays | []string | `[]string` | | | | |
| years | []string | `[]string` | | | | |
| Name | Type | Go type | Required | Default | Description | Example |
| --------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- |
| `days_of_month` | []string | []string | | | | |
| `location` | string | string | | | | |
| `months` | []string | []string | | | | |
| `times` | [][TimeRange](#time-range) | `[]*TimeRange` | | | | |
| `weekdays` | []string | []string | | | | |
| `years` | []string | []string | | | | |
{{% /responsive-table %}}
@ -1950,10 +1917,10 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ---------- | ------ | -------- | :------: | ------- | ----------- | ----------------------- |
| end_time | string | `string` | | | | `"end_time": "24:00"` |
| start_time | string | `string` | | | | `"start_time": "18:00"` |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------ | ------ | ------- | :------: | ------- | ----------- | ----------------------- |
| `end_time` | string | string | | | | `"end_time": "24:00"` |
| `start_time` | string | string | | | | `"start_time": "18:00"` |
{{% /responsive-table %}}
@ -1963,9 +1930,9 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ---- | ------ | -------- | :------: | ------- | ----------- | --------------- |
| msg | string | `string` | | | | `error message` |
| Name | Type | Go type | Required | Default | Description | Example |
| ----- | ------ | ------- | :------: | ------- | ----------- | --------------- |
| `msg` | string | string | | | | `error message` |
{{% /responsive-table %}}
@ -1975,11 +1942,11 @@ When creating a contact point, the `EmbeddedContactPoint.name` property determin
{{% responsive-table %}}
| Name | Type | Go type | Required | Default | Description | Example |
| ---------- | ---------- | ---------------- | :------: | ------- | ------------------------------------------------------------------------ | ------- |
| statusCode | string | `string` | ✓ | | HTTP Status Code | |
| messageId | string | `string` | ✓ | | Unique code of the error | |
| message | string | `string` | | | Error message | |
| extra | map of any | `map[string]any` | | | Extra information about the error. Format is specific to the error code. | |
| Name | Type | Go type | Required | Default | Description | Example |
| ------------ | ---------- | ---------------- | :------: | ------- | ------------------------------------------------------------------------ | ------- |
| `statusCode` | string | string | ✓ | | HTTP Status Code | |
| `messageId` | string | string | ✓ | | Unique code of the error | |
| `message` | string | string | | | Error message | |
| `extra` | map of any | `map[string]any` | | | Extra information about the error. Format is specific to the error code. | |
{{% /responsive-table %}}

@ -0,0 +1,14 @@
---
labels:
products:
- oss
title: 'Note Dynamic labels'
---
{{% admonition type="note" %}}
An alert instance is uniquely identified by its set of labels.
- Avoid displaying query values in labels, as this can create numerous alert instances—one for each distinct label set. Instead, use annotations for query values.
- If a templated label's value changes, it maps to a different alert instance, and the previous instance transitions to the `No data` state when its label value is no longer present.
{{% /admonition %}}

@ -5,10 +5,10 @@ labels:
title: 'Table configure no data and error'
---
| Configure | Set alert state | Description |
| ---------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No Data | No Data | The default option for **No Data** events.<br/>Sets alert instance state to `No Data`. <br/> The alert rule also creates a new alert instance `DatasourceNoData` with the name and UID of the alert rule, and UID of the datasource that returned no data as labels. |
| Error | Error | The default option for **Error** events.<br/>Sets alert instance state to `Error`. <br/> The alert rule also creates a new alert instance `DatasourceError` with the name and UID of the alert rule, and UID of the datasource that returned no data as labels. |
| No Data or Error | Alerting | Sets the alert instance state to `Pending` and then transitions to `Alerting` once the pending period ends. If you sent the pending period to 0, the alert instance state is immediately set to `Alerting`. |
| No Data or Error | Normal | Sets alert instance state to `Normal`. |
| No Data or Error | Keep Last State | Maintains the alert instance in its last state. Useful for mitigating temporary issues. |
| Configure | Set alert state | Description |
| ---------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No Data | No Data | The default option for **No Data** events.<br/>Sets alert instance state to `No Data`. <br/> The alert rule immediately creates a new `DatasourceNoData` alert instance after evaluation, with the alert rule's name, UID, and the data source UID as labels. |
| Error | Error | The default option for **Error** events.<br/>Sets alert instance state to `Error`. <br/> The alert rule immediately creates a new `DatasourceError` alert instance after evaluation, with the alert rule's name, UID, and the data source UID as labels. |
| No Data or Error | Alerting | Sets the alert instance state to `Pending` and then transitions to `Alerting` once the pending period ends. If you sent the pending period to 0, the alert instance state is immediately set to `Alerting`. |
| No Data or Error | Normal | Sets alert instance state to `Normal`. |
| No Data or Error | Keep Last State | Maintains the alert instance in its last state. Useful for mitigating temporary issues. |

@ -73,8 +73,8 @@ require (
github.com/googleapis/go-sql-spanner v1.11.1 // @grafana/grafana-search-and-storage
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // @grafana/identity-access-team
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 // @grafana/identity-access-team
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics
github.com/grafana/dataplane/sdata v0.0.9 // @grafana/observability-metrics

@ -1540,10 +1540,10 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356 h1:71o8Bxw/wg+aBRUASiGux67gDEUC2bqq3I+x2hw8eIc=
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e h1:noJzp/qZGIto4XdZkvj2EKQ1bQeqCRs0bedxdJN17sQ=
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3UqCIpoy9Z2wgKGrZ4A5vz+yQlP9ZXvCfEkiA=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa h1:08Wh/svkv8WpDuOBBKAzSPa14gKjYLZvQJsHWXLjPuc=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=

@ -813,6 +813,7 @@ github.com/go-fonts/liberation v0.3.2/go.mod h1:N0QsDLVUQPy3UYg9XAc3Uh3UDMp2Z7M1
github.com/go-fonts/stix v0.1.0 h1:UlZlgrvvmT/58o573ot7NFw0vZasZ5I6bcIft/oMdgg=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU=
github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg=
github.com/go-latex/latex v0.0.0-20231108140139-5c1ce85aa4ea h1:DfZQkvEbdmOe+JK2TMtBM+0I9GSdzE2y/L1/AmD8xKc=
@ -1146,7 +1147,6 @@ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=
github.com/phpdave11/gofpdi v1.0.14 h1:jlcDIJ6ObCh3X9nANGEK6RY5wbUKHJ5unBjrzG4i89A=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=

@ -223,7 +223,6 @@
"postcss-reporter": "7.1.0",
"postcss-scss": "4.0.9",
"prettier": "3.4.2",
"pseudoizer": "^0.1.0",
"react-refresh": "0.14.0",
"react-select-event": "5.5.1",
"redux-mock-store": "1.5.5",
@ -336,6 +335,7 @@
"history": "4.10.1",
"i18next": "^24.0.0",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-pseudo": "^2.2.1",
"immer": "10.1.1",
"immutable": "5.0.3",
"ix": "^7.0.0",

@ -0,0 +1,49 @@
import * as React from 'react';
interface FolderPickerProps {
/* Folder UID to show as selected */
value?: string;
/** Show an invalid state around the folder picker */
invalid?: boolean;
/* Whether to show the root 'Dashboards' (formally General) folder as selectable */
showRootFolder?: boolean;
/* Folder UIDs to exclude from the picker, to prevent invalid operations */
excludeUIDs?: string[];
/* Show folders matching this permission, mainly used to also show folders user can view. Defaults to showing only folders user has Edit */
permission?: 'view' | 'edit';
/* Callback for when the user selects a folder */
onChange?: (folderUID: string | undefined, folderName: string | undefined) => void;
/* Whether the picker should be clearable */
clearable?: boolean;
}
type FolderPickerComponentType = React.ComponentType<FolderPickerProps>;
let FolderPickerComponent: FolderPickerComponentType | undefined;
/**
* Used to bootstrap the FolderPicker during application start
*
* @internal
*/
export function setFolderPicker(component: FolderPickerComponentType) {
FolderPickerComponent = component;
}
export function FolderPicker(props: FolderPickerProps) {
if (FolderPickerComponent) {
return <FolderPickerComponent {...props} />;
}
if (process.env.NODE_ENV !== 'production') {
return <div>@grafana/runtime FolderPicker is not set</div>;
}
return null;
}

@ -57,6 +57,7 @@ export { hasPermission, hasPermissionInMetadata, hasAllPermissions, hasAnyPermis
export { QueryEditorWithMigration } from './components/QueryEditorWithMigration';
export { type MigrationHandler, isMigrationHandler, migrateQuery, migrateRequest } from './utils/migrationHandler';
export { usePluginUserStorage } from './utils/userStorage';
export { FolderPicker, setFolderPicker } from './components/FolderPicker';
export {
type CorrelationsService,
type CorrelationData,

@ -5,10 +5,11 @@ import { GrafanaTheme2 } from '@grafana/data';
import { StringSelector, selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../../themes';
import { getFocusStyles, getMouseFocusStyles } from '../../../themes/mixins';
import { getFocusStyles, getInternalRadius, getMouseFocusStyles } from '../../../themes/mixins';
import { Tooltip } from '../../Tooltip/Tooltip';
import { getPropertiesForButtonSize } from '../commonStyles';
export const RADIO_GROUP_PADDING = 2;
export type RadioButtonSize = 'sm' | 'md';
export interface RadioButtonProps {
@ -130,7 +131,7 @@ const getRadioButtonStyles = (theme: GrafanaTheme2, size: RadioButtonSize, fullW
lineHeight: `${labelHeight}px`,
color: textColor,
padding: theme.spacing(0, padding),
borderRadius: theme.shape.radius.default,
borderRadius: getInternalRadius(theme, RADIO_GROUP_PADDING),
background: theme.colors.background.primary,
cursor: 'pointer',
userSelect: 'none',

@ -7,8 +7,7 @@ import { GrafanaTheme2, SelectableValue, toIconName } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { Icon } from '../../Icon/Icon';
import { RadioButtonSize, RadioButton } from './RadioButton';
import { RadioButtonSize, RadioButton, RADIO_GROUP_PADDING } from './RadioButton';
export interface RadioButtonGroupProps<T> {
value?: T;
id?: string;
@ -119,7 +118,7 @@ const getStyles = (theme: GrafanaTheme2) => {
flexWrap: 'nowrap',
border: `1px solid ${theme.components.input.borderColor}`,
borderRadius: theme.shape.radius.default,
padding: '2px',
padding: RADIO_GROUP_PADDING,
'&:hover': {
borderColor: theme.components.input.borderHover,
},

@ -0,0 +1,46 @@
import { Meta, StoryFn } from '@storybook/react';
import { BorderRadiusContainer } from './BorderRadius';
const meta: Meta = {
title: 'Docs Overview/Theme',
component: BorderRadiusContainer,
decorators: [],
parameters: {
layout: 'centered',
},
args: {
referenceBorderRadius: 20,
referenceBorderWidth: 10,
offset: 0,
borderWidth: 2,
},
argTypes: {
offset: {
control: {
min: 0,
},
},
referenceBorderRadius: {
control: {
min: 0,
},
},
referenceBorderWidth: {
control: {
min: 0,
},
},
borderWidth: {
control: {
min: 0,
},
},
},
};
export const OffsetBorderRadius: StoryFn<typeof BorderRadiusContainer> = (args) => {
return <BorderRadiusContainer {...args} />;
};
export default meta;

@ -0,0 +1,87 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { getInternalRadius, getExternalRadius } from '../../themes/mixins';
import { Stack } from '../Layout/Stack/Stack';
import { Text } from '../Text/Text';
interface DemoBoxProps {
referenceBorderRadius: number;
referenceBorderWidth: number;
offset: number;
borderWidth: number;
}
export const BorderRadiusContainer = ({
referenceBorderRadius,
referenceBorderWidth,
offset,
borderWidth,
}: DemoBoxProps) => {
const styles = useStyles2(getStyles, referenceBorderRadius, referenceBorderWidth, offset, borderWidth);
return (
<Stack direction="column" alignItems="center" gap={4}>
<Stack alignItems="center">
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
<Text variant="code">getInternalRadius</Text>
<div className={styles.baseForInternal}>
<div className={styles.internalContainer} />
</div>
</Stack>
<Stack alignItems="center">
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
<Text variant="code">getExternalRadius</Text>
<div className={styles.externalContainer}>
<div className={styles.baseForExternal} />
</div>
</Stack>
</Stack>
);
};
const getStyles = (
theme: GrafanaTheme2,
referenceBorderRadius: number,
referenceBorderWidth: number,
offset: number,
borderWidth: number
) => ({
baseForInternal: css({
backgroundColor: theme.colors.action.disabledBackground,
border: `${referenceBorderWidth}px dashed ${theme.colors.action.disabledText}`,
borderRadius: referenceBorderRadius,
display: 'flex',
height: '80px',
padding: offset,
width: '300px',
}),
baseForExternal: css({
backgroundColor: theme.colors.action.disabledBackground,
border: `${referenceBorderWidth}px dashed ${theme.colors.action.disabledText}`,
borderRadius: referenceBorderRadius,
height: '80px',
flex: 1,
width: '300px',
}),
internalContainer: css({
backgroundColor: theme.colors.background.primary,
border: `${borderWidth}px solid ${theme.colors.primary.main}`,
borderRadius: getInternalRadius(theme, offset, {
parentBorderRadius: referenceBorderRadius,
parentBorderWidth: referenceBorderWidth,
}),
flex: 1,
}),
externalContainer: css({
border: `${borderWidth}px solid ${theme.colors.primary.main}`,
borderRadius: getExternalRadius(theme, offset, {
childBorderRadius: referenceBorderRadius,
selfBorderWidth: borderWidth,
}),
display: 'flex',
flex: 1,
padding: offset,
}),
});

@ -82,3 +82,55 @@ export const getTooltipContainerStyles = (theme: GrafanaTheme2) => ({
borderRadius: theme.shape.radius.default,
zIndex: theme.zIndex.tooltip,
});
interface ExternalRadiusAdditionalOptions {
selfBorderWidth?: number;
childBorderRadius?: number;
}
/**
* Calculates a border radius for an element, based on border radius of its child.
*
* @param theme
* @param offset - The distance to offset from the child element, should be >= 0.
* @param additionalOptions
* @param additionalOptions.selfBorderWidth - The border width of the element itself (default: 1)
* @param additionalOptions.childBorderRadius - The border radius of the child element (default: theme default radius)
* @returns A CSS calc() expression that returns the relative external radius value
*/
export const getExternalRadius = (
theme: GrafanaTheme2,
offset: number,
additionalOptions: ExternalRadiusAdditionalOptions = {}
) => {
const { selfBorderWidth = 1, childBorderRadius } = additionalOptions;
const childBorderRadiusPx = childBorderRadius !== undefined ? `${childBorderRadius}px` : theme.shape.radius.default;
return `calc(max(0px, ${childBorderRadiusPx} + ${offset}px + ${selfBorderWidth}px))`;
};
interface InternalRadiusAdditionalOptions {
parentBorderWidth?: number;
parentBorderRadius?: number;
}
/**
* Calculates a border radius for an element, based on border radius of its parent.
*
* @param theme
* @param offset - The distance to offset from the parent element, should be >= 0.
* @param additionalOptions
* @param additionalOptions.parentBorderWidth - The border width of the parent element (default: 1)
* @param additionalOptions.parentBorderRadius - The border radius of the parent element (default: theme default radius)
* @returns A CSS calc() expression that returns the relative internal radius value
*/
export const getInternalRadius = (
theme: GrafanaTheme2,
offset: number,
additionalOptions: InternalRadiusAdditionalOptions = {}
) => {
const { parentBorderWidth = 1, parentBorderRadius } = additionalOptions;
const parentBorderRadiusPx =
parentBorderRadius !== undefined ? `${parentBorderRadius}px` : theme.shape.radius.default;
return `calc(max(0px, ${parentBorderRadiusPx} - ${offset}px - ${parentBorderWidth}px))`;
};

@ -20,6 +20,23 @@ function initI18n() {
resources: {},
returnEmptyString: false,
lng: 'en-US', // this should be the locale of the phrases in our source JSX
postProcess: [
// Add pseudo processing even if we aren't necessarily going to use it
'pseudo',
],
});
}
if (process.env.NODE_ENV === 'development') {
import('i18next-pseudo').then((module) => {
const Pseudo = module.default;
i18next.use(
new Pseudo({
languageToPseudo: 'pseudo',
enabled: true,
wrapped: true,
})
);
});
}
}

@ -3,7 +3,7 @@ module github.com/grafana/grafana/pkg/apimachinery
go 1.23.7
require (
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // @grafana/identity-access-team
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 // @grafana/identity-access-team
github.com/stretchr/testify v1.10.0
k8s.io/apimachinery v0.32.1

@ -32,8 +32,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa h1:08Wh/svkv8WpDuOBBKAzSPa14gKjYLZvQJsHWXLjPuc=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=

@ -72,7 +72,7 @@ func (sb *SQLBuilder) WriteDashboardPermissionFilter(user identity.Requester, pe
leftJoin string
)
filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, queryType, sb.features, sb.recursiveQueriesAreSupported)
filterRBAC := permissions.NewAccessControlDashboardPermissionFilter(user, permission, queryType, sb.features, sb.recursiveQueriesAreSupported, sb.dialect)
leftJoin = filterRBAC.LeftJoin()
sql, params = filterRBAC.Where()
recQry, recQryParams = filterRBAC.With()

@ -52,12 +52,12 @@ func (d *directResourceClient) List(ctx context.Context, in *resource.ListReques
return d.server.List(ctx, in)
}
func (d *directResourceClient) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) {
return d.server.ListRepositoryObjects(ctx, in)
func (d *directResourceClient) ListManagedObjects(ctx context.Context, in *resource.ListManagedObjectsRequest, opts ...grpc.CallOption) (*resource.ListManagedObjectsResponse, error) {
return d.server.ListManagedObjects(ctx, in)
}
func (d *directResourceClient) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) {
return d.server.CountRepositoryObjects(ctx, in)
func (d *directResourceClient) CountManagedObjects(ctx context.Context, in *resource.CountManagedObjectsRequest, opts ...grpc.CallOption) (*resource.CountManagedObjectsResponse, error) {
return d.server.CountManagedObjects(ctx, in)
}
// PutBlob implements ResourceClient.

@ -262,11 +262,11 @@ func (a *dashboardSqlAccess) Search(ctx context.Context, req *resource.ResourceS
return a.dashboardSearchClient.Search(ctx, req)
}
func (a *dashboardSqlAccess) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
func (a *dashboardSqlAccess) ListManagedObjects(ctx context.Context, req *resource.ListManagedObjectsRequest) (*resource.ListManagedObjectsResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (a *dashboardSqlAccess) CountRepositoryObjects(context.Context, *resource.CountRepositoryObjectsRequest) (*resource.CountRepositoryObjectsResponse, error) {
func (a *dashboardSqlAccess) CountManagedObjects(context.Context, *resource.CountManagedObjectsRequest) (*resource.CountManagedObjectsResponse, error) {
return nil, fmt.Errorf("not implemented")
}

@ -674,7 +674,7 @@ func (m *MockClient) Search(ctx context.Context, in *resource.ResourceSearchRequ
func (m *MockClient) GetStats(ctx context.Context, in *resource.ResourceStatsRequest, opts ...grpc.CallOption) (*resource.ResourceStatsResponse, error) {
return nil, nil
}
func (m *MockClient) CountRepositoryObjects(ctx context.Context, in *resource.CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.CountRepositoryObjectsResponse, error) {
func (m *MockClient) CountManagedObjects(ctx context.Context, in *resource.CountManagedObjectsRequest, opts ...grpc.CallOption) (*resource.CountManagedObjectsResponse, error) {
return nil, nil
}
func (m *MockClient) Watch(ctx context.Context, in *resource.WatchRequest, opts ...grpc.CallOption) (resource.ResourceStore_WatchClient, error) {
@ -704,7 +704,7 @@ func (m *MockClient) PutBlob(ctx context.Context, in *resource.PutBlobRequest, o
func (m *MockClient) List(ctx context.Context, in *resource.ListRequest, opts ...grpc.CallOption) (*resource.ListResponse, error) {
return nil, nil
}
func (m *MockClient) ListRepositoryObjects(ctx context.Context, in *resource.ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*resource.ListRepositoryObjectsResponse, error) {
func (m *MockClient) ListManagedObjects(ctx context.Context, in *resource.ListManagedObjectsRequest, opts ...grpc.CallOption) (*resource.ListManagedObjectsResponse, error) {
return nil, nil
}
func (m *MockClient) IsHealthy(ctx context.Context, in *resource.HealthCheckRequest, opts ...grpc.CallOption) (*resource.HealthCheckResponse, error) {

@ -6,9 +6,10 @@ import (
"strconv"
"strings"
"go.opentelemetry.io/otel"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"go.opentelemetry.io/otel"
)
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/accesscontrol/database")
@ -58,7 +59,7 @@ func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query acces
return nil
}
filter, params := accesscontrol.UserRolesFilter(query.OrgID, query.UserID, query.TeamIDs, query.Roles)
filter, params := accesscontrol.UserRolesFilter(query.OrgID, query.UserID, query.TeamIDs, query.Roles, s.sql.GetDialect())
q := `
SELECT

@ -6,6 +6,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
var sqlIDAcceptList = map[string]struct{}{
@ -127,7 +128,7 @@ func SetAcceptListForTest(list map[string]struct{}) func() {
}
}
func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (string, []any) {
func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string, dialect migrator.Dialect) (string, []any) {
var params []any
builder := strings.Builder{}
@ -145,7 +146,7 @@ func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (stri
if len(teamIDs) > 0 {
if builder.Len() > 0 {
builder.WriteString("UNION")
builder.WriteString(dialect.UnionDistinct())
}
builder.WriteString(`
SELECT tr.role_id FROM team_role as tr
@ -160,7 +161,7 @@ func UserRolesFilter(orgID, userID int64, teamIDs []int64, roles []string) (stri
if len(roles) != 0 {
if builder.Len() > 0 {
builder.WriteString("UNION")
builder.WriteString(dialect.UnionDistinct())
}
builder.WriteString(`

@ -121,7 +121,7 @@ func (authz *AuthService) dashboardsWithVisibleAnnotations(ctx context.Context,
}
filters := []any{
permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, dashboardaccess.PERMISSION_VIEW, filterType, authz.features, recursiveQueriesSupported),
permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, dashboardaccess.PERMISSION_VIEW, filterType, authz.features, recursiveQueriesSupported, authz.db.GetDialect()),
searchstore.OrgFilter{OrgId: query.OrgID},
}

@ -1002,7 +1002,7 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F
}
if !query.SkipAccessControlFilter {
filters = append(filters, permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported))
filters = append(filters, permissions.NewAccessControlDashboardPermissionFilter(query.SignedInUser, query.Permission, query.Type, d.features, recursiveQueriesAreSupported, d.store.GetDialect()))
}
filters = append(filters, searchstore.DeletedFilter{Deleted: query.IsDeleted})

@ -455,7 +455,9 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
writeSearchStringSQL(query, l.SQLStore, &builder)
writeExcludeSQL(query, &builder)
writeTypeFilterSQL(typeFilter, &builder)
builder.Write(" UNION ")
builder.Write(" ")
builder.Write(l.SQLStore.GetDialect().UnionDistinct())
builder.Write(" ")
}
builder.Write(selectLibraryElementDTOWithMeta)
builder.Write(", le.folder_uid as folder_uid ")
@ -544,7 +546,9 @@ func (l *LibraryElementService) getAllLibraryElements(c context.Context, signedI
writeSearchStringSQL(query, l.SQLStore, &countBuilder)
writeExcludeSQL(query, &countBuilder)
writeTypeFilterSQL(typeFilter, &countBuilder)
countBuilder.Write(" UNION ")
countBuilder.Write(" ")
countBuilder.Write(l.SQLStore.GetDialect().UnionDistinct())
countBuilder.Write(" ")
}
countBuilder.Write(selectLibraryElementDTOWithMeta)
countBuilder.Write(getFromLibraryElementDTOWithMeta(l.SQLStore.GetDialect()))

@ -1,121 +0,0 @@
//go:build enterprise || pro
package migrations
import (
"encoding/json"
"fmt"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
"xorm.io/core"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/setting"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
)
func setupTestDB(t *testing.T) (*migrator.Migrator, *xorm.Engine) {
t.Helper()
dbType := sqlutil.GetTestDBType()
testDB, err := sqlutil.GetTestDB(dbType)
require.NoError(t, err)
t.Cleanup(testDB.Cleanup)
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
t.Cleanup(func() {
if err := x.Close(); err != nil {
fmt.Printf("failed to close xorm engine: %v", err)
}
})
err = migrator.NewDialect(x.DriverName()).CleanDB(x)
require.NoError(t, err)
mg := migrator.NewMigrator(x, &setting.Cfg{
Logger: log.New("users.test"),
Raw: ini.Empty(),
})
migrations := &OSSMigrations{}
migrations.AddMigration(mg)
err = mg.Start(false, 0)
require.NoError(t, err)
return mg, x
}
// This "test" migrates database from scratch, and then generates Spanner DDL statements for re-creating the same database.
func TestMigrateToSpannerDialect(t *testing.T) {
t.Skip("Skipping because test returns panic: unknown column type: INTEGER")
mg, eng := setupTestDB(t)
tables, err := eng.DBMetas()
require.NoError(t, err)
var statements []string
spannerDialect := migrator.NewSpannerDialect()
for _, table := range tables {
t := &migrator.Table{
Name: table.Name,
Columns: nil,
PrimaryKeys: table.PrimaryKeys,
Indices: nil,
}
for _, c := range table.Columns() {
col := &migrator.Column{
Name: c.Name,
Type: c.SQLType.Name,
Length: c.Length,
Length2: c.Length2,
Nullable: c.Nullable,
IsPrimaryKey: c.IsPrimaryKey,
IsAutoIncrement: c.IsAutoIncrement,
IsLatin: false,
Default: c.Default,
}
if (col.Type == core.Bool || col.Type == core.TinyInt) && c.Default != "" {
b, err := strconv.ParseBool(c.Default)
if err == nil {
// Format bool values as true/false.
col.Default = strconv.FormatBool(b)
}
}
t.Columns = append(t.Columns, col)
}
for _, ix := range table.Indexes {
nix := &migrator.Index{
Name: ix.Name,
Type: ix.Type,
Cols: ix.Cols,
}
t.Indices = append(t.Indices, nix)
}
statements = append(statements, spannerDialect.CreateTableSQL(t))
for _, nix := range t.Indices {
if nix.Name != "PRIMARY_KEY" {
statements = append(statements, spannerDialect.CreateIndexSQL(table.Name, nix))
}
}
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
require.NoError(t, enc.Encode(statements))
fmt.Println()
require.NoError(t, enc.Encode(mg.GetMigrationIDs(true)))
}

@ -32,6 +32,8 @@ type Dialect interface {
BooleanStr(bool) string
DateTimeFunc(string) string
BatchSize() int
UnionDistinct() string // this is the default UNION type
UnionAll() string
OrderBy(order string) string
@ -467,3 +469,11 @@ func (b *BaseDialect) Update(ctx context.Context, tx *session.SessionTx, tableNa
func (b *BaseDialect) Concat(strs ...string) string {
return fmt.Sprintf("CONCAT(%s)", strings.Join(strs, ", "))
}
func (b *BaseDialect) UnionDistinct() string {
return "UNION"
}
func (b *BaseDialect) UnionAll() string {
return "UNION ALL"
}

@ -240,19 +240,19 @@ func (mg *Migrator) run(ctx context.Context) (err error) {
migrationLogExists, err := mg.DBEngine.IsTableExist(mg.tableName)
if err != nil {
return fmt.Errorf("%v: %w", "failed to check table existence", err)
return fmt.Errorf("failed to check table existence: %w", err)
}
if !migrationLogExists {
// Check if dialect can initialize database from a snapshot.
err := mg.Dialect.CreateDatabaseFromSnapshot(ctx, mg.DBEngine, mg.tableName)
if err != nil {
return fmt.Errorf("%v: %w", "failed to create database from snapshot", err)
return fmt.Errorf("failed to create database from snapshot: %w", err)
}
migrationLogExists, err = mg.DBEngine.IsTableExist(mg.tableName)
if err != nil {
return fmt.Errorf("%v: %w", "failed to check table existence after applying snapshot", err)
return fmt.Errorf("failed to check table existence after applying snapshot: %w", err)
}
}

@ -9,8 +9,8 @@
"CREATE TABLE `alert_image` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `token` STRING(190) NOT NULL, `path` STRING(190) NOT NULL, `url` STRING(2048) NOT NULL, `created_at` TIMESTAMP NOT NULL, `expires_at` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_image_token` ON `alert_image` (token)",
"CREATE TABLE `alert_instance` (`rule_org_id` INT64 NOT NULL, `rule_uid` STRING(40) NOT NULL, `labels` STRING(MAX) NOT NULL, `labels_hash` STRING(190) NOT NULL, `current_state` STRING(190) NOT NULL, `current_state_since` INT64 NOT NULL, `last_eval_time` INT64 NOT NULL, `current_state_end` INT64 NOT NULL DEFAULT (0), `current_reason` STRING(190), `result_fingerprint` STRING(16), `resolved_at` INT64, `last_sent_at` INT64) PRIMARY KEY (rule_org_id,rule_uid,labels_hash)",
"CREATE INDEX `IDX_alert_instance_rule_org_id_rule_uid_current_state` ON `alert_instance` (rule_org_id, rule_uid, current_state)",
"CREATE INDEX `IDX_alert_instance_rule_org_id_current_state` ON `alert_instance` (rule_org_id, current_state)",
"CREATE INDEX `IDX_alert_instance_rule_org_id_rule_uid_current_state` ON `alert_instance` (rule_org_id, rule_uid, current_state)",
"CREATE TABLE `alert_notification` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `name` STRING(190) NOT NULL, `type` STRING(255) NOT NULL, `settings` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `is_default` BOOL NOT NULL DEFAULT (false), `frequency` INT64, `send_reminder` BOOL DEFAULT (false), `disable_resolve_message` BOOL NOT NULL DEFAULT (false), `uid` STRING(40), `secure_settings` STRING(MAX)) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_alert_notification_org_id_uid` ON `alert_notification` (org_id, uid)",
"CREATE TABLE `alert_notification_state` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `alert_id` INT64 NOT NULL, `notifier_id` INT64 NOT NULL, `state` STRING(50) NOT NULL, `version` INT64 NOT NULL, `updated_at` INT64 NOT NULL, `alert_rule_state_updated_version` INT64 NOT NULL) PRIMARY KEY (id)",
@ -48,11 +48,12 @@
"CREATE INDEX `IDX_api_key_org_id` ON `api_key` (org_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_api_key_key` ON `api_key` (key)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_api_key_org_id_name` ON `api_key` (org_id, name)",
"CREATE TABLE `autoincrement_sequences` (`name` STRING(128) NOT NULL, `next_value` INT64 NOT NULL) PRIMARY KEY (name)",
"CREATE TABLE `builtin_role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `role` STRING(190) NOT NULL, `role_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `org_id` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_builtin_role_org_id_role_id_role` ON `builtin_role` (org_id, role_id, role)",
"CREATE INDEX `IDX_builtin_role_org_id` ON `builtin_role` (org_id)",
"CREATE INDEX `IDX_builtin_role_role` ON `builtin_role` (role)",
"CREATE INDEX `IDX_builtin_role_role_id` ON `builtin_role` (role_id)",
"CREATE INDEX `IDX_builtin_role_role` ON `builtin_role` (role)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_builtin_role_org_id_role_id_role` ON `builtin_role` (org_id, role_id, role)",
"CREATE TABLE `cache_data` (`cache_key` STRING(168) NOT NULL, `data` BYTES(MAX) NOT NULL, `expires` INT64 NOT NULL, `created_at` INT64 NOT NULL) PRIMARY KEY (cache_key)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_cache_data_cache_key` ON `cache_data` (cache_key)",
"CREATE TABLE `cloud_migration_resource` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40) NOT NULL, `resource_type` STRING(40) NOT NULL, `resource_uid` STRING(255), `status` STRING(20) NOT NULL, `error_string` STRING(MAX), `snapshot_uid` STRING(40) NOT NULL, `name` STRING(MAX), `parent_name` STRING(MAX), `error_code` STRING(MAX)) PRIMARY KEY (id)",
@ -61,48 +62,67 @@
"CREATE TABLE `cloud_migration_snapshot` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `uid` STRING(40), `session_uid` STRING(40), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `finished` TIMESTAMP, `upload_url` STRING(MAX), `status` STRING(MAX) NOT NULL, `local_directory` STRING(MAX), `gms_snapshot_uid` STRING(MAX), `encryption_key` STRING(MAX), `error_string` STRING(MAX)) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_cloud_migration_snapshot_uid` ON `cloud_migration_snapshot` (uid)",
"CREATE TABLE `correlation` (`uid` STRING(40) NOT NULL, `org_id` INT64 NOT NULL DEFAULT (0), `source_uid` STRING(40) NOT NULL, `target_uid` STRING(40), `label` STRING(MAX) NOT NULL, `description` STRING(MAX) NOT NULL, `config` STRING(MAX), `provisioned` BOOL NOT NULL DEFAULT (false), `type` STRING(40) NOT NULL DEFAULT ('query')) PRIMARY KEY (uid,org_id,source_uid)",
"CREATE INDEX `IDX_correlation_org_id` ON `correlation` (org_id)",
"CREATE INDEX `IDX_correlation_source_uid` ON `correlation` (source_uid)",
"CREATE INDEX `IDX_correlation_uid` ON `correlation` (uid)",
"CREATE INDEX `IDX_correlation_org_id` ON `correlation` (org_id)",
"CREATE TABLE `dashboard` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `slug` STRING(189) NOT NULL, `title` STRING(189) NOT NULL, `data` STRING(MAX) NOT NULL, `org_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `updated_by` INT64, `created_by` INT64, `gnet_id` INT64, `plugin_id` STRING(189), `folder_id` INT64 NOT NULL DEFAULT (0), `is_folder` BOOL NOT NULL DEFAULT (false), `has_acl` BOOL NOT NULL DEFAULT (false), `uid` STRING(40), `is_public` BOOL NOT NULL DEFAULT (false), `deleted` TIMESTAMP, `api_version` STRING(16), `folder_uid` STRING(40)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_dashboard_deleted` ON `dashboard` (deleted)",
"CREATE INDEX `IDX_dashboard_gnet_id` ON `dashboard` (gnet_id)",
"CREATE INDEX `IDX_dashboard_is_folder` ON `dashboard` (is_folder)",
"CREATE INDEX `IDX_dashboard_org_id` ON `dashboard` (org_id)",
"CREATE INDEX `IDX_dashboard_org_id_folder_id_title` ON `dashboard` (org_id, folder_id, title)",
"CREATE INDEX `IDX_dashboard_org_id_plugin_id` ON `dashboard` (org_id, plugin_id)",
"CREATE INDEX `IDX_dashboard_gnet_id` ON `dashboard` (gnet_id)",
"CREATE INDEX `IDX_dashboard_org_id` ON `dashboard` (org_id)",
"CREATE INDEX `IDX_dashboard_title` ON `dashboard` (title)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_org_id_uid` ON `dashboard` (org_id, uid)",
"CREATE TABLE `dashboard_acl` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `user_id` INT64, `team_id` INT64, `permission` INT64 NOT NULL DEFAULT (4), `role` STRING(20), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE INDEX `IDX_dashboard_acl_dashboard_id` ON `dashboard_acl` (dashboard_id)",
"CREATE INDEX `IDX_dashboard_acl_org_id_role` ON `dashboard_acl` (org_id, role)",
"CREATE INDEX `IDX_dashboard_acl_permission` ON `dashboard_acl` (permission)",
"CREATE INDEX `IDX_dashboard_acl_team_id` ON `dashboard_acl` (team_id)",
"CREATE INDEX `IDX_dashboard_acl_user_id` ON `dashboard_acl` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_acl_dashboard_id_team_id` ON `dashboard_acl` (dashboard_id, team_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_acl_dashboard_id_user_id` ON `dashboard_acl` (dashboard_id, user_id)",
"CREATE INDEX `IDX_dashboard_acl_dashboard_id` ON `dashboard_acl` (dashboard_id)",
"CREATE INDEX `IDX_dashboard_acl_org_id_role` ON `dashboard_acl` (org_id, role)",
"CREATE INDEX `IDX_dashboard_acl_permission` ON `dashboard_acl` (permission)",
"CREATE TABLE `dashboard_provisioning` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64, `name` STRING(150) NOT NULL, `external_id` STRING(MAX) NOT NULL, `updated` INT64 NOT NULL DEFAULT (0), `check_sum` STRING(32)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_dashboard_provisioning_dashboard_id` ON `dashboard_provisioning` (dashboard_id)",
"CREATE INDEX `IDX_dashboard_provisioning_dashboard_id_name` ON `dashboard_provisioning` (dashboard_id, name)",
"CREATE INDEX `IDX_dashboard_provisioning_dashboard_id` ON `dashboard_provisioning` (dashboard_id)",
"CREATE TABLE `dashboard_public` (`uid` STRING(40) NOT NULL, `dashboard_uid` STRING(40) NOT NULL, `org_id` INT64 NOT NULL, `time_settings` STRING(MAX), `template_variables` STRING(MAX), `access_token` STRING(32) NOT NULL, `created_by` INT64 NOT NULL, `updated_by` INT64, `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP, `is_enabled` BOOL NOT NULL DEFAULT (false), `annotations_enabled` BOOL NOT NULL DEFAULT (false), `time_selection_enabled` BOOL NOT NULL DEFAULT (false), `share` STRING(64) NOT NULL DEFAULT ('public')) PRIMARY KEY (uid)",
"CREATE INDEX `IDX_dashboard_public_config_org_id_dashboard_uid` ON `dashboard_public` (org_id, dashboard_uid)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_public_config_access_token` ON `dashboard_public` (access_token)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_public_config_uid` ON `dashboard_public` (uid)",
"CREATE TABLE `dashboard_public_email_share` (`uid` STRING(40) NOT NULL, `public_dashboard_uid` STRING(64) NOT NULL, `recipient` STRING(255) NOT NULL, `type` STRING(64) NOT NULL DEFAULT ('email'), `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP NOT NULL) PRIMARY KEY (uid)",
"CREATE TABLE `dashboard_public_magic_link` (`uid` STRING(40) NOT NULL, `token_uuid` STRING(64) NOT NULL, `public_dashboard_uid` STRING(64) NOT NULL, `email` STRING(255) NOT NULL, `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP NOT NULL) PRIMARY KEY (uid)",
"CREATE TABLE `dashboard_public_session` (`uid` STRING(40) NOT NULL, `cookie_uuid` STRING(64) NOT NULL, `public_dashboard_uid` STRING(64) NOT NULL, `email` STRING(255) NOT NULL, `created_at` TIMESTAMP NOT NULL, `updated_at` TIMESTAMP NOT NULL, `last_seen_at` TIMESTAMP) PRIMARY KEY (uid)",
"CREATE TABLE `dashboard_public_usage_by_day` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `public_dashboard_uid` STRING(255) NOT NULL, `day` STRING(40) NOT NULL, `views` INT64 NOT NULL, `queries` INT64 NOT NULL, `errors` INT64 NOT NULL, `load_duration` FLOAT64 NOT NULL, `cached_queries` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
"CREATE TABLE `dashboard_snapshot` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(255) NOT NULL, `key` STRING(190) NOT NULL, `delete_key` STRING(190) NOT NULL, `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `external` BOOL NOT NULL, `external_url` STRING(255) NOT NULL, `dashboard` STRING(MAX) NOT NULL, `expires` TIMESTAMP NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `external_delete_url` STRING(255), `dashboard_encrypted` BYTES(MAX)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_dashboard_snapshot_user_id` ON `dashboard_snapshot` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_snapshot_delete_key` ON `dashboard_snapshot` (delete_key)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_snapshot_key` ON `dashboard_snapshot` (key)",
"CREATE TABLE `dashboard_tag` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64 NOT NULL, `term` STRING(50) NOT NULL, `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_dashboard_tag_dashboard_id` ON `dashboard_tag` (dashboard_id)",
"CREATE TABLE `dashboard_usage_by_day` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64 NOT NULL, `day` STRING(40) NOT NULL, `views` INT64 NOT NULL, `queries` INT64 NOT NULL, `errors` INT64 NOT NULL, `load_duration` FLOAT64 NOT NULL, `cached_queries` INT64 NOT NULL DEFAULT (0), `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_dashboard_usage_by_day_dashboard_id` ON `dashboard_usage_by_day` (dashboard_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_usage_by_day_dashboard_id_day` ON `dashboard_usage_by_day` (dashboard_id, day)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_usage_by_day_dashboard_uid_org_id_day` ON `dashboard_usage_by_day` (dashboard_uid, org_id, day)",
"CREATE TABLE `dashboard_usage_sums` (`dashboard_id` INT64 NOT NULL, `updated` TIMESTAMP NOT NULL, `views_last_1_days` INT64 NOT NULL, `views_last_7_days` INT64 NOT NULL, `views_last_30_days` INT64 NOT NULL, `views_total` INT64 NOT NULL, `queries_last_1_days` INT64 NOT NULL, `queries_last_7_days` INT64 NOT NULL, `queries_last_30_days` INT64 NOT NULL, `queries_total` INT64 NOT NULL, `errors_last_1_days` INT64 NOT NULL DEFAULT (0), `errors_last_7_days` INT64 NOT NULL DEFAULT (0), `errors_last_30_days` INT64 NOT NULL DEFAULT (0), `errors_total` INT64 NOT NULL DEFAULT (0), `dashboard_uid` STRING(40), `org_id` INT64 DEFAULT (1)) PRIMARY KEY (dashboard_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_usage_sums_org_id_dashboard_uid` ON `dashboard_usage_sums` (org_id, dashboard_uid)",
"CREATE TABLE `dashboard_version` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `dashboard_id` INT64 NOT NULL, `parent_version` INT64 NOT NULL, `restored_from` INT64 NOT NULL, `version` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `created_by` INT64 NOT NULL, `message` STRING(MAX) NOT NULL, `data` STRING(MAX), `api_version` STRING(16)) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_version_dashboard_id_version` ON `dashboard_version` (dashboard_id, version)",
"CREATE INDEX `IDX_dashboard_version_dashboard_id` ON `dashboard_version` (dashboard_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_dashboard_version_dashboard_id_version` ON `dashboard_version` (dashboard_id, version)",
"CREATE TABLE `data_keys` (`name` STRING(100) NOT NULL, `active` BOOL NOT NULL, `scope` STRING(30) NOT NULL, `provider` STRING(50) NOT NULL, `encrypted_data` BYTES(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `label` STRING(100)) PRIMARY KEY (name)",
"CREATE TABLE `data_source` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `version` INT64 NOT NULL, `type` STRING(255) NOT NULL, `name` STRING(190) NOT NULL, `access` STRING(255) NOT NULL, `url` STRING(255) NOT NULL, `password` STRING(255), `user` STRING(255), `database` STRING(255), `basic_auth` BOOL NOT NULL, `basic_auth_user` STRING(255), `basic_auth_password` STRING(255), `is_default` BOOL NOT NULL, `json_data` STRING(MAX), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `with_credentials` BOOL NOT NULL DEFAULT (false), `secure_json_data` STRING(MAX), `read_only` BOOL, `uid` STRING(40) NOT NULL DEFAULT ('0'), `is_prunable` BOOL DEFAULT (false), `api_version` STRING(20)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_data_source_org_id` ON `data_source` (org_id)",
"CREATE INDEX `IDX_data_source_org_id_is_default` ON `data_source` (org_id, is_default)",
"CREATE INDEX `IDX_data_source_org_id` ON `data_source` (org_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_org_id_name` ON `data_source` (org_id, name)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_org_id_uid` ON `data_source` (org_id, uid)",
"CREATE TABLE `data_source_acl` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `data_source_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `permission` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE INDEX `IDX_data_source_acl_data_source_id` ON `data_source_acl` (data_source_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_acl_data_source_id_team_id_user_id` ON `data_source_acl` (data_source_id, team_id, user_id)",
"CREATE TABLE `data_source_cache` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `data_source_id` INT64 NOT NULL, `enabled` BOOL NOT NULL, `ttl_ms` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `use_default_ttl` BOOL NOT NULL DEFAULT (true), `data_source_uid` STRING(40) NOT NULL DEFAULT ('0'), `ttl_resources_ms` INT64 NOT NULL DEFAULT (300000)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_data_source_cache_data_source_id` ON `data_source_cache` (data_source_id)",
"CREATE INDEX `IDX_data_source_cache_data_source_uid` ON `data_source_cache` (data_source_uid)",
"CREATE TABLE `data_source_usage_by_day` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `data_source_id` INT64 NOT NULL, `day` STRING(40) NOT NULL, `queries` INT64 NOT NULL, `errors` INT64 NOT NULL, `load_duration_ms` INT64 NOT NULL) PRIMARY KEY (id)",
"CREATE INDEX `IDX_data_source_usage_by_day_data_source_id` ON `data_source_usage_by_day` (data_source_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_data_source_usage_by_day_data_source_id_day` ON `data_source_usage_by_day` (data_source_id, day)",
"CREATE TABLE `entity_event` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `entity_id` STRING(1024) NOT NULL, `event_type` STRING(8) NOT NULL, `created` INT64 NOT NULL) PRIMARY KEY (id)",
"CREATE TABLE `file` (`path` STRING(1024) NOT NULL, `path_hash` STRING(64) NOT NULL, `parent_folder_path_hash` STRING(64) NOT NULL, `contents` BYTES(MAX), `etag` STRING(32) NOT NULL, `cache_control` STRING(128) NOT NULL, `content_disposition` STRING(128) NOT NULL, `updated` TIMESTAMP NOT NULL, `created` TIMESTAMP NOT NULL, `size` INT64 NOT NULL, `mime_type` STRING(255) NOT NULL) PRIMARY KEY (path_hash)",
"CREATE INDEX `IDX_file_parent_folder_path_hash` ON `file` (parent_folder_path_hash)",
@ -119,6 +139,7 @@
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_library_element_org_id_uid` ON `library_element` (org_id, uid)",
"CREATE TABLE `library_element_connection` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `element_id` INT64 NOT NULL, `kind` INT64 NOT NULL, `connection_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `created_by` INT64 NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_library_element_connection_element_id_kind_connection_id` ON `library_element_connection` (element_id, kind, connection_id)",
"CREATE TABLE `license_token` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `token` STRING(MAX) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE TABLE `login_attempt` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `username` STRING(190) NOT NULL, `ip_address` STRING(30) NOT NULL, `created` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_login_attempt_username` ON `login_attempt` (username)",
"CREATE TABLE `migration_log` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `migration_id` STRING(255) NOT NULL, `sql` STRING(MAX) NOT NULL, `success` BOOL NOT NULL, `error` STRING(MAX) NOT NULL, `timestamp` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
@ -127,13 +148,13 @@
"CREATE TABLE `org` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `name` STRING(190) NOT NULL, `address1` STRING(255), `address2` STRING(255), `city` STRING(255), `state` STRING(255), `zip_code` STRING(50), `country` STRING(255), `billing_email` STRING(255), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_org_name` ON `org` (name)",
"CREATE TABLE `org_user` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `role` STRING(20) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_org_user_org_id_user_id` ON `org_user` (org_id, user_id)",
"CREATE INDEX `IDX_org_user_org_id` ON `org_user` (org_id)",
"CREATE INDEX `IDX_org_user_user_id` ON `org_user` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_org_user_org_id_user_id` ON `org_user` (org_id, user_id)",
"CREATE TABLE `permission` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `role_id` INT64 NOT NULL, `action` STRING(190) NOT NULL, `scope` STRING(190) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `kind` STRING(40) NOT NULL DEFAULT (''), `attribute` STRING(40) NOT NULL DEFAULT (''), `identifier` STRING(40) NOT NULL DEFAULT ('')) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_permission_action_scope_role_id` ON `permission` (action, scope, role_id)",
"CREATE INDEX `IDX_permission_identifier` ON `permission` (identifier)",
"CREATE INDEX `IDX_permission_role_id` ON `permission` (role_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_permission_action_scope_role_id` ON `permission` (action, scope, role_id)",
"CREATE TABLE `playlist` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(255) NOT NULL, `interval` STRING(255) NOT NULL, `org_id` INT64 NOT NULL, `created_at` INT64 NOT NULL DEFAULT (0), `updated_at` INT64 NOT NULL DEFAULT (0), `uid` STRING(80) NOT NULL DEFAULT ('0')) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_playlist_org_id_uid` ON `playlist` (org_id, uid)",
"CREATE TABLE `playlist_item` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `playlist_id` INT64 NOT NULL, `type` STRING(255) NOT NULL, `value` STRING(MAX) NOT NULL, `title` STRING(MAX) NOT NULL, `order` INT64 NOT NULL) PRIMARY KEY (id)",
@ -151,6 +172,16 @@
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_query_history_star_user_id_query_uid` ON `query_history_star` (user_id, query_uid)",
"CREATE TABLE `quota` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64, `user_id` INT64, `target` STRING(190) NOT NULL, `limit` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_quota_org_id_user_id_target` ON `quota` (org_id, user_id, target)",
"CREATE TABLE `recording_rules` (`id` STRING(128) NOT NULL, `target_ref_id` STRING(128) NOT NULL, `name` STRING(128) NOT NULL, `description` STRING(MAX) NOT NULL, `org_id` INT64 NOT NULL, `interval` INT64 NOT NULL, `range` INT64 NOT NULL, `active` BOOL NOT NULL DEFAULT (false), `count` BOOL NOT NULL DEFAULT (false), `queries` BYTES(MAX) NOT NULL, `created_at` TIMESTAMP NOT NULL, `prom_name` STRING(128)) PRIMARY KEY (id,target_ref_id)",
"CREATE TABLE `remote_write_targets` (`id` STRING(128) NOT NULL, `data_source_uid` STRING(128) NOT NULL, `write_path` STRING(128) NOT NULL, `org_id` INT64 NOT NULL) PRIMARY KEY (id,data_source_uid,write_path)",
"CREATE TABLE `report` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `org_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `name` STRING(MAX) NOT NULL, `recipients` STRING(MAX) NOT NULL, `reply_to` STRING(MAX), `message` STRING(MAX), `schedule_frequency` STRING(32) NOT NULL, `schedule_day` STRING(32) NOT NULL, `schedule_hour` INT64 NOT NULL, `schedule_minute` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `schedule_timezone` STRING(50) NOT NULL DEFAULT ('Europe/Stockholm'), `time_from` STRING(255), `time_to` STRING(255), `pdf_landscape` BOOL, `schedule_day_of_month` STRING(32), `pdf_layout` STRING(255), `pdf_orientation` STRING(32), `dashboard_uid` STRING(40), `template_vars` STRING(MAX), `enable_dashboard_url` BOOL, `state` STRING(32), `enable_csv` BOOL, `schedule_start` INT64, `schedule_end` INT64, `schedule_interval_frequency` STRING(32), `schedule_interval_amount` INT64, `schedule_workdays_only` BOOL, `formats` STRING(190) NOT NULL DEFAULT ('[\"pdf\"]'), `scale_factor` INT64 NOT NULL DEFAULT (2), `uid` STRING(40), `pdf_show_template_variables` BOOL NOT NULL DEFAULT (false), `pdf_combine_one_file` BOOL NOT NULL DEFAULT (true), `subject` STRING(MAX)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_report_dashboard_id` ON `report` (dashboard_id)",
"CREATE INDEX `IDX_report_org_id` ON `report` (org_id)",
"CREATE INDEX `IDX_report_user_id` ON `report` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_report_org_id_uid` ON `report` (org_id, uid)",
"CREATE TABLE `report_dashboards` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `report_id` INT64 NOT NULL, `dashboard_uid` STRING(40) NOT NULL DEFAULT (''), `report_variables` STRING(MAX), `time_to` STRING(255), `time_from` STRING(255), `created` TIMESTAMP) PRIMARY KEY (id)",
"CREATE INDEX `IDX_report_dashboards_report_id` ON `report_dashboards` (report_id)",
"CREATE TABLE `report_settings` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `org_id` INT64 NOT NULL, `branding_report_logo_url` STRING(MAX), `branding_email_logo_url` STRING(MAX), `branding_email_footer_link` STRING(MAX), `branding_email_footer_text` STRING(MAX), `branding_email_footer_mode` STRING(50), `pdf_theme` STRING(40) NOT NULL DEFAULT ('light'), `embedded_image_theme` STRING(40) NOT NULL DEFAULT ('dark')) PRIMARY KEY (id)",
"CREATE TABLE `role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `name` STRING(190) NOT NULL, `description` STRING(MAX), `version` INT64 NOT NULL, `org_id` INT64 NOT NULL, `uid` STRING(40) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `display_name` STRING(190), `group_name` STRING(190), `hidden` BOOL NOT NULL DEFAULT (false)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_role_org_id` ON `role` (org_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_role_org_id_name` ON `role` (org_id, name)",
@ -162,6 +193,8 @@
"CREATE TABLE `server_lock` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `operation_uid` STRING(100) NOT NULL, `version` INT64 NOT NULL, `last_execution` INT64 NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_server_lock_operation_uid` ON `server_lock` (operation_uid)",
"CREATE TABLE `session` (`key` STRING(16) NOT NULL, `data` BYTES(MAX) NOT NULL, `expiry` INT64 NOT NULL) PRIMARY KEY (key)",
"CREATE TABLE `setting` (`section` STRING(100) NOT NULL, `key` STRING(100) NOT NULL, `value` STRING(MAX) NOT NULL, `encrypted_value` STRING(MAX)) PRIMARY KEY (section,key)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_setting_section_key` ON `setting` (section, key)",
"CREATE TABLE `short_url` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `uid` STRING(40) NOT NULL, `path` STRING(MAX) NOT NULL, `created_by` INT64, `created_at` INT64 NOT NULL, `last_seen_at` INT64) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_short_url_org_id_uid` ON `short_url` (org_id, uid)",
"CREATE TABLE `signing_key` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `key_id` STRING(255) NOT NULL, `private_key` STRING(MAX) NOT NULL, `added_at` TIMESTAMP NOT NULL, `expires_at` TIMESTAMP, `alg` STRING(255) NOT NULL) PRIMARY KEY (id)",
@ -176,20 +209,24 @@
"CREATE INDEX `IDX_team_org_id` ON `team` (org_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_org_id_name` ON `team` (org_id, name)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_org_id_uid` ON `team` (org_id, uid)",
"CREATE TABLE `team_group` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `group_id` STRING(190) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE INDEX `IDX_team_group_group_id` ON `team_group` (group_id)",
"CREATE INDEX `IDX_team_group_org_id` ON `team_group` (org_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_group_org_id_team_id_group_id` ON `team_group` (org_id, team_id, group_id)",
"CREATE TABLE `team_member` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `external` BOOL, `permission` INT64) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_member_org_id_team_id_user_id` ON `team_member` (org_id, team_id, user_id)",
"CREATE INDEX `IDX_team_member_org_id` ON `team_member` (org_id)",
"CREATE INDEX `IDX_team_member_team_id` ON `team_member` (team_id)",
"CREATE INDEX `IDX_team_member_user_id_org_id` ON `team_member` (user_id, org_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_member_org_id_team_id_user_id` ON `team_member` (org_id, team_id, user_id)",
"CREATE TABLE `team_role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `team_id` INT64 NOT NULL, `role_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE INDEX `IDX_team_role_org_id` ON `team_role` (org_id)",
"CREATE INDEX `IDX_team_role_team_id` ON `team_role` (team_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_team_role_org_id_team_id_role_id` ON `team_role` (org_id, team_id, role_id)",
"CREATE TABLE `temp_user` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `version` INT64 NOT NULL, `email` STRING(190) NOT NULL, `name` STRING(255), `role` STRING(20), `code` STRING(190) NOT NULL, `status` STRING(20) NOT NULL, `invited_by_user_id` INT64, `email_sent` BOOL NOT NULL, `email_sent_on` TIMESTAMP, `remote_addr` STRING(255), `created` INT64 NOT NULL DEFAULT (0), `updated` INT64 NOT NULL DEFAULT (0)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_temp_user_org_id` ON `temp_user` (org_id)",
"CREATE INDEX `IDX_temp_user_status` ON `temp_user` (status)",
"CREATE INDEX `IDX_temp_user_code` ON `temp_user` (code)",
"CREATE INDEX `IDX_temp_user_email` ON `temp_user` (email)",
"CREATE INDEX `IDX_temp_user_org_id` ON `temp_user` (org_id)",
"CREATE INDEX `IDX_temp_user_status` ON `temp_user` (status)",
"CREATE TABLE `test_data` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `metric1` STRING(20), `metric2` STRING(150), `value_big_int` INT64, `value_double` FLOAT64, `value_float` FLOAT64, `value_int` INT64, `time_epoch` INT64 NOT NULL, `time_date_time` TIMESTAMP NOT NULL, `time_time_stamp` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE TABLE `user` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `version` INT64 NOT NULL, `login` STRING(190) NOT NULL, `email` STRING(190) NOT NULL, `name` STRING(255), `password` STRING(255), `salt` STRING(50), `rands` STRING(50), `company` STRING(255), `org_id` INT64 NOT NULL, `is_admin` BOOL NOT NULL, `email_verified` BOOL, `theme` STRING(255), `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL, `help_flags1` INT64 NOT NULL DEFAULT (0), `last_seen_at` TIMESTAMP, `is_disabled` BOOL NOT NULL DEFAULT (false), `is_service_account` BOOL DEFAULT (false), `uid` STRING(40), `is_provisioned` BOOL NOT NULL DEFAULT (false)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_user_login_email` ON `user` (login, email)",
@ -200,13 +237,21 @@
"CREATE INDEX `IDX_user_auth_auth_module_auth_id` ON `user_auth` (auth_module, auth_id)",
"CREATE INDEX `IDX_user_auth_user_id` ON `user_auth` (user_id)",
"CREATE TABLE `user_auth_token` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `auth_token` STRING(100) NOT NULL, `prev_auth_token` STRING(100) NOT NULL, `user_agent` STRING(255) NOT NULL, `client_ip` STRING(255) NOT NULL, `auth_token_seen` BOOL NOT NULL, `seen_at` INT64, `rotated_at` INT64 NOT NULL, `created_at` INT64 NOT NULL, `updated_at` INT64 NOT NULL, `revoked_at` INT64, `external_session_id` INT64) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_auth_token_auth_token` ON `user_auth_token` (auth_token)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_auth_token_prev_auth_token` ON `user_auth_token` (prev_auth_token)",
"CREATE INDEX `IDX_user_auth_token_revoked_at` ON `user_auth_token` (revoked_at)",
"CREATE INDEX `IDX_user_auth_token_user_id` ON `user_auth_token` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_auth_token_auth_token` ON `user_auth_token` (auth_token)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_auth_token_prev_auth_token` ON `user_auth_token` (prev_auth_token)",
"CREATE TABLE `user_dashboard_views` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `dashboard_id` INT64 NOT NULL, `viewed` TIMESTAMP NOT NULL, `org_id` INT64, `dashboard_uid` STRING(40)) PRIMARY KEY (id)",
"CREATE INDEX `IDX_user_dashboard_views_dashboard_id` ON `user_dashboard_views` (dashboard_id)",
"CREATE INDEX `IDX_user_dashboard_views_org_id_dashboard_uid` ON `user_dashboard_views` (org_id, dashboard_uid)",
"CREATE INDEX `IDX_user_dashboard_views_user_id` ON `user_dashboard_views` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_dashboard_views_user_id_dashboard_id` ON `user_dashboard_views` (user_id, dashboard_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_dashboard_views_user_id_org_id_dashboard_uid` ON `user_dashboard_views` (user_id, org_id, dashboard_uid)",
"CREATE TABLE `user_external_session` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_auth_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `auth_module` STRING(190) NOT NULL, `access_token` STRING(MAX), `id_token` STRING(MAX), `refresh_token` STRING(MAX), `session_id` STRING(1024), `session_id_hash` STRING(44), `name_id` STRING(1024), `name_id_hash` STRING(44), `expires_at` TIMESTAMP, `created_at` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE TABLE `user_role` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `org_id` INT64 NOT NULL, `user_id` INT64 NOT NULL, `role_id` INT64 NOT NULL, `created` TIMESTAMP NOT NULL, `group_mapping_uid` STRING(40) DEFAULT ('')) PRIMARY KEY (id)",
"CREATE INDEX `IDX_user_role_org_id` ON `user_role` (org_id)",
"CREATE INDEX `IDX_user_role_user_id` ON `user_role` (user_id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_role_org_id_user_id_role_id_group_mapping_uid` ON `user_role` (org_id, user_id, role_id, group_mapping_uid)"
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_role_org_id_user_id_role_id_group_mapping_uid` ON `user_role` (org_id, user_id, role_id, group_mapping_uid)",
"CREATE TABLE `user_stats` (`id` INT64 NOT NULL GENERATED BY DEFAULT AS IDENTITY (BIT_REVERSED_POSITIVE), `user_id` INT64 NOT NULL, `billing_role` STRING(40) NOT NULL, `created` TIMESTAMP NOT NULL, `updated` TIMESTAMP NOT NULL) PRIMARY KEY (id)",
"CREATE UNIQUE NULL_FILTERED INDEX `UQE_user_stats_user_id` ON `user_stats` (user_id)"
]

@ -577,6 +577,7 @@
"create sso_setting table",
"copy kvstore migration status to each org",
"add back entry for orgid=0 migrated status",
"managed dashboard permissions annotation actions migration",
"create cloud_migration table v1",
"create cloud_migration_run table v1",
"add stack_id column",
@ -640,5 +641,119 @@
"populate rule guid in alert rule table",
"add index in alert_rule_version table on rule_org_id, rule_uid, rule_guid and version columns",
"add index in alert_rule_version table on rule_guid and version columns",
"add index in alert_rule table on guid columns"
"add index in alert_rule table on guid columns",
"create data_source_usage_by_day table",
"create data_source_usage_by_day(data_source_id) index",
"create data_source_usage_by_day(data_source_id, day) unique index",
"create dashboard_usage_by_day table",
"create dashboard_usage_sums table",
"create dashboard_usage_by_day(dashboard_id) index",
"create dashboard_usage_by_day(dashboard_id, day) index",
"add column errors_last_1_days to dashboard_usage_sums",
"add column errors_last_7_days to dashboard_usage_sums",
"add column errors_last_30_days to dashboard_usage_sums",
"add column errors_total to dashboard_usage_sums",
"create dashboard_public_usage_by_day table",
"add column cached_queries to dashboard_usage_by_day table",
"add column cached_queries to dashboard_public_usage_by_day table",
"add column dashboard_uid to dashboard_usage_sums",
"add column org_id to dashboard_usage_sums",
"add column dashboard_uid to dashboard_usage_by_day",
"add column org_id to dashboard_usage_by_day",
"create dashboard_usage_by_day(dashboard_uid, org_id, day) unique index",
"Add missing dashboard_uid and org_id to dashboard_usage_by_day and dashboard_usage_sums",
"Add dashboard_usage_sums(org_id, dashboard_uid) index",
"create user_dashboard_views table",
"add index user_dashboard_views.user_id",
"add index user_dashboard_views.dashboard_id",
"add unique index user_dashboard_views_user_id_dashboard_id",
"add org_id column to user_dashboard_views",
"add dashboard_uid column to user_dashboard_views",
"add unique index user_dashboard_views_org_id_dashboard_uid",
"add unique index user_dashboard_views_org_id_user_id_dashboard_uid",
"populate user_dashboard_views.dashboard_uid and user_dashboard_views.org_id from dashboard table",
"create user_stats table",
"add unique index user_stats(user_id)",
"create data_source_cache table",
"add index data_source_cache.data_source_id",
"add use_default_ttl column",
"add data_source_cache.data_source_uid column",
"remove abandoned data_source_cache records",
"update data_source_cache.data_source_uid value",
"add index data_source_cache.data_source_uid",
"add data_source_cache.ttl_resources_ms column",
"update data_source_cache.ttl_resources_ms to have the same value as ttl_ms",
"create data_source_acl table",
"add index data_source_acl.data_source_id",
"add unique index datasource_acl.unique",
"create license_token table",
"drop recorded_queries table v14",
"drop recording_rules table v14",
"create recording_rules table v14",
"create remote_write_targets table v1",
"Add prom_name to recording_rules table",
"ensure remote_write_targets table",
"create report config table v1",
"Add index report.user_id",
"add index to dashboard_id",
"add index to org_id",
"Add timezone to the report",
"Add time_from to the report",
"Add time_to to the report",
"Add PDF landscape option to the report",
"Add monthly day scheduling option to the report",
"Add PDF layout option to the report",
"Add PDF orientation option to the report",
"Update report pdf_orientation from pdf_landscape",
"create report settings table",
"Add dashboard_uid field to the report",
"Add template_vars field to the report",
"Add option to include dashboard url in the report",
"Add state field to the report",
"Add option to add CSV files to the report",
"Add scheduling start date",
"Add missing schedule_start date for old reports",
"Add scheduling end date",
"Add schedulinng custom interval frequency",
"Add scheduling custom interval amount",
"Add workdays only flag to report",
"create report dashboards table",
"Add index report_dashboards.report_id",
"Migrate report fields into report_dashboards",
"Add formats option to the report",
"Migrate reports with csv enabled",
"Migrate ancient reports",
"Add created column in report_dashboards",
"Add scale_factor to the report",
"Alter scale_factor from TINYINT to SMALLINT",
"Add uid column to report",
"Add unique index reports_org_id_uid",
"Add pdf show template variable values to the report",
"Add pdf combine in one file",
"Add pdf theme to report settings table",
"Add email subject to the report",
"Populate email subject with report name",
"Add embedded image theme to report settings table",
"create team group table",
"add index team_group.org_id",
"add unique index team_group.org_id_team_id_group_id",
"add index team_group.group_id",
"create settings table",
"add unique index settings.section_key",
"add setting.encrypted_value",
"migrate role names",
"rename orgs roles",
"remove duplicated org role",
"migrate alerting role names",
"data source permissions",
"data source uid permissions",
"rename permissions:delegate scope",
"remove invalid managed permissions",
"builtin role migration",
"seed permissions migration",
"managed permissions migration enterprise",
"create table dashboard_public_email_share",
"create table dashboard_public_magic_link",
"create table dashboard_public_session",
"add last_seen_at column"
]

@ -325,3 +325,7 @@ func confToClientOptions(connectorConfig spannerdriver.ConnectorConfig) []option
}
return opts
}
func (s *SpannerDialect) UnionDistinct() string {
return "UNION DISTINCT"
}

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
)
@ -44,14 +45,14 @@ type PermissionsFilter interface {
With() (string, []any)
Where() (string, []any)
buildClauses()
buildClauses(dialect migrator.Dialect)
nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTable string, Col string, rightTableCol string, orgID int64) (string, []any)
}
// NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboardaccess.PermissionType and query type
// The filter is configured to use the new permissions filter (without subqueries) if the feature flag is enabled
// The filter is configured to use the old permissions filter (with subqueries) if the feature flag is disabled
func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissionLevel dashboardaccess.PermissionType, queryType string, features featuremgmt.FeatureToggles, recursiveQueriesAreSupported bool) PermissionsFilter {
func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissionLevel dashboardaccess.PermissionType, queryType string, features featuremgmt.FeatureToggles, recursiveQueriesAreSupported bool, dialect migrator.Dialect) PermissionsFilter {
needEdit := permissionLevel > dashboardaccess.PERMISSION_VIEW
var folderAction string
@ -129,7 +130,7 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi
features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported,
}
}
f.buildClauses()
f.buildClauses(dialect)
return f
}
@ -157,7 +158,7 @@ func (f *accessControlDashboardPermissionFilter) hasRequiredActions() bool {
return false
}
func (f *accessControlDashboardPermissionFilter) buildClauses() {
func (f *accessControlDashboardPermissionFilter) buildClauses(dialect migrator.Dialect) {
if f.user == nil || f.user.IsNil() || !f.hasRequiredActions() {
f.where = clause{string: "(1 = 0)"}
return
@ -171,7 +172,7 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() {
}
orgID := f.user.GetOrgID()
filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user))
filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user), dialect)
rolesFilter := " AND role_id IN(SELECT id FROM role " + filter + ") "
var args []any
builder := strings.Builder{}

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
type accessControlDashboardPermissionFilterNoFolderSubquery struct {
@ -25,7 +26,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) LeftJoin() stri
return " dashboard AS folder ON dashboard.org_id = folder.org_id AND dashboard.folder_id = folder.id"
}
func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() {
func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses(dialect migrator.Dialect) {
if f.user == nil || f.user.IsNil() || len(f.user.GetPermissions()) == 0 {
f.where = clause{string: "(1 = 0)"}
return
@ -39,7 +40,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses()
}
orgID := f.user.GetOrgID()
filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user))
filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user), dialect)
rolesFilter := " AND role_id IN(SELECT id FROM role " + filter + ") "
var args []any
builder := strings.Builder{}

@ -183,7 +183,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) {
keys = append(keys, k)
}
t.Run(tt.desc+" with features "+strings.Join(keys, ","), func(t *testing.T) {
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, features, recursiveQueriesAreSupported)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, features, recursiveQueriesAreSupported, store.GetDialect())
var result int
err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
@ -355,7 +355,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t
keys = append(keys, k)
}
t.Run(tt.desc+" with features "+strings.Join(keys, ","), func(t *testing.T) {
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, features, recursiveQueriesAreSupported)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tt.permission, tt.queryType, features, recursiveQueriesAreSupported, store.GetDialect())
var result int
err = store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
@ -470,7 +470,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) {
db := setupNestedTest(t, usr, tc.permissions, orgID, features)
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
require.NoError(t, err)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported, db.GetDialect())
var result []string
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
@ -588,7 +588,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission
db := setupNestedTest(t, helperUser, []accesscontrol.Permission{}, orgID, features)
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
require.NoError(t, err)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported, db.GetDialect())
var result []string
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()
@ -707,7 +707,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T
db := setupNestedTest(t, usr, tc.signedInUserPermissions, orgID, features)
recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported()
require.NoError(t, err)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported)
filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported, db.GetDialect())
var result []string
err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()

@ -56,7 +56,7 @@ func benchmarkDashboardPermissionFilter(b *testing.B, numUsers, numDashboards, n
b.ResetTimer()
for i := 0; i < b.N; i++ {
filter := permissions.NewAccessControlDashboardPermissionFilter(&usr, dashboardaccess.PERMISSION_VIEW, "", features, recursiveQueriesAreSupported)
filter := permissions.NewAccessControlDashboardPermissionFilter(&usr, dashboardaccess.PERMISSION_VIEW, "", features, recursiveQueriesAreSupported, store.GetDialect())
var result int
err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
q, params := filter.Where()

@ -334,6 +334,7 @@ func TestBuilder_RBAC(t *testing.T) {
"",
tc.features,
recursiveQueriesAreSupported,
store.GetDialect(),
),
},
Dialect: store.GetDialect(),

@ -11,6 +11,7 @@ import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
"xorm.io/xorm"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
@ -140,14 +141,18 @@ func (sess *DBSession) InsertId(bean any, dialect migrator.Dialect) error {
}
func (sess *DBSession) WithReturningID(driverName string, query string, args []any) (int64, error) {
supported := driverName != migrator.Postgres
var id int64
if !supported {
if driverName == migrator.Postgres {
query = fmt.Sprintf("%s RETURNING id", query)
if _, err := sess.SQL(query, args...).Get(&id); err != nil {
return id, err
}
} else {
if driverName == migrator.Spanner {
// Only works with INSERT statements.
query = fmt.Sprintf("%s THEN RETURN id", query)
}
sqlOrArgs := append([]any{query}, args...)
res, err := sess.Exec(sqlOrArgs...)
if err != nil {

@ -82,13 +82,13 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida
createdFromLegacy, err := d.legacy.Create(ctx, in, createValidation, options)
if err != nil {
log.Error("unable to create object in legacy storage", "err", err)
return createdFromLegacy, err
return nil, err
}
createdCopy := createdFromLegacy.DeepCopyObject()
accCreated, err := meta.Accessor(createdCopy)
if err != nil {
return createdFromLegacy, err
return nil, err
}
accCreated.SetResourceVersion("")
accCreated.SetUID("")
@ -105,13 +105,13 @@ func (d *dualWriter) Create(ctx context.Context, in runtime.Object, createValida
if err != nil {
log.Error("unable to cleanup object in legacy storage", "err", err)
}
return storageObj, errObjectSt
return nil, errObjectSt
}
if d.readUnified {
return storageObj, errObjectSt
return storageObj, nil
}
return createdFromLegacy, err
return createdFromLegacy, nil
}
func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
@ -123,19 +123,19 @@ func (d *dualWriter) Delete(ctx context.Context, name string, deleteValidation r
// as they would not be able to see the object in unistore anymore.
objFromLegacy, asyncLegacy, err := d.legacy.Delete(ctx, name, deleteValidation, options)
if err != nil && (!d.readUnified || !d.errorIsOK && !apierrors.IsNotFound(err)) {
return objFromLegacy, asyncLegacy, err
return nil, false, err
}
objFromStorage, asyncStorage, err := d.unified.Delete(ctx, name, deleteValidation, options)
if err != nil && apierrors.IsNotFound(err) || d.errorIsOK {
err = nil // clear the error
if err != nil && !apierrors.IsNotFound(err) && !d.errorIsOK {
return nil, false, err
}
if d.readUnified {
return objFromStorage, asyncStorage, err
return objFromStorage, asyncStorage, nil
}
return objFromLegacy, asyncLegacy, err
return objFromLegacy, asyncLegacy, nil
}
// Update overrides the behavior of the generic DualWriter and writes first to Storage and then to LegacyStorage.
@ -155,7 +155,7 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat
objFromLegacy, createdLegacy, err := d.legacy.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
if err != nil {
log.With("object", objFromLegacy).Error("could not update in legacy storage", "err", err)
return objFromLegacy, createdLegacy, err
return nil, false, err
}
objFromStorage, created, err := d.unified.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
@ -164,13 +164,14 @@ func (d *dualWriter) Update(ctx context.Context, name string, objInfo rest.Updat
if d.errorIsOK {
return objFromLegacy, createdLegacy, nil
}
return nil, false, err
}
if d.readUnified {
return objFromStorage, created, err
return objFromStorage, created, nil
}
return objFromLegacy, createdLegacy, err
return objFromLegacy, createdLegacy, nil
}
// DeleteCollection overrides the behavior of the generic DualWriter and deletes from both LegacyStorage and Storage.
@ -186,7 +187,7 @@ func (d *dualWriter) DeleteCollection(ctx context.Context, deleteValidation rest
deletedLegacy, err := d.legacy.DeleteCollection(ctx, deleteValidation, options, listOptions)
if err != nil {
log.With("deleted", deletedLegacy).Error("failed to delete collection successfully from legacy storage", "err", err)
return deletedLegacy, err
return nil, err
}
deletedStorage, err := d.unified.DeleteCollection(ctx, deleteValidation, options, listOptions)
@ -195,13 +196,14 @@ func (d *dualWriter) DeleteCollection(ctx context.Context, deleteValidation rest
if d.errorIsOK {
return deletedLegacy, nil
}
return nil, err
}
if d.readUnified {
return deletedStorage, err
return deletedStorage, nil
}
return deletedLegacy, err
return deletedLegacy, nil
}
func (d *dualWriter) Destroy() {

@ -203,8 +203,8 @@ require (
github.com/googleapis/go-sql-spanner v1.11.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356 // indirect
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 // indirect
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e // indirect
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040 // indirect
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect

@ -1253,10 +1253,10 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356 h1:71o8Bxw/wg+aBRUASiGux67gDEUC2bqq3I+x2hw8eIc=
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e h1:noJzp/qZGIto4XdZkvj2EKQ1bQeqCRs0bedxdJN17sQ=
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3UqCIpoy9Z2wgKGrZ4A5vz+yQlP9ZXvCfEkiA=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa h1:08Wh/svkv8WpDuOBBKAzSPa14gKjYLZvQJsHWXLjPuc=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
github.com/grafana/dataplane/examples v0.0.1 h1:K9M5glueWyLoL4//H+EtTQq16lXuHLmOhb6DjSCahzA=

@ -155,7 +155,7 @@ func TestDeleteWithSuggestionAndConflict(t *testing.T) {
type resourceClientMock struct {
resource.ResourceStoreClient
resource.ResourceIndexClient
resource.RepositoryIndexClient
resource.ManagedObjectIndexClient
resource.BulkStoreClient
resource.BlobStoreClient
resource.DiagnosticsClient

@ -24,7 +24,7 @@ import (
type ResourceClient interface {
ResourceStoreClient
ResourceIndexClient
RepositoryIndexClient
ManagedObjectIndexClient
BulkStoreClient
BlobStoreClient
DiagnosticsClient
@ -34,7 +34,7 @@ type ResourceClient interface {
type resourceClient struct {
ResourceStoreClient
ResourceIndexClient
RepositoryIndexClient
ManagedObjectIndexClient
BulkStoreClient
BlobStoreClient
DiagnosticsClient
@ -43,12 +43,12 @@ type resourceClient struct {
func NewLegacyResourceClient(channel *grpc.ClientConn) ResourceClient {
cc := grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor)
return &resourceClient{
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
RepositoryIndexClient: NewRepositoryIndexClient(cc),
BulkStoreClient: NewBulkStoreClient(cc),
BlobStoreClient: NewBlobStoreClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
ManagedObjectIndexClient: NewManagedObjectIndexClient(cc),
BulkStoreClient: NewBulkStoreClient(cc),
BlobStoreClient: NewBlobStoreClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
}
}
@ -60,7 +60,7 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
for _, desc := range []*grpc.ServiceDesc{
&ResourceStore_ServiceDesc,
&ResourceIndex_ServiceDesc,
&RepositoryIndex_ServiceDesc,
&ManagedObjectIndex_ServiceDesc,
&BlobStore_ServiceDesc,
&BulkStore_ServiceDesc,
&Diagnostics_ServiceDesc,
@ -82,12 +82,12 @@ func NewLocalResourceClient(server ResourceServer) ResourceClient {
cc := grpchan.InterceptClientConn(channel, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor)
return &resourceClient{
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
RepositoryIndexClient: NewRepositoryIndexClient(cc),
BulkStoreClient: NewBulkStoreClient(cc),
BlobStoreClient: NewBlobStoreClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
ManagedObjectIndexClient: NewManagedObjectIndexClient(cc),
BulkStoreClient: NewBulkStoreClient(cc),
BlobStoreClient: NewBlobStoreClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
}
}
@ -124,12 +124,12 @@ func NewRemoteResourceClient(tracer tracing.Tracer, conn *grpc.ClientConn, cfg R
cc := grpchan.InterceptClientConn(conn, clientInt.UnaryClientInterceptor, clientInt.StreamClientInterceptor)
return &resourceClient{
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
BlobStoreClient: NewBlobStoreClient(cc),
BulkStoreClient: NewBulkStoreClient(cc),
RepositoryIndexClient: NewRepositoryIndexClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
ResourceStoreClient: NewResourceStoreClient(cc),
ResourceIndexClient: NewResourceIndexClient(cc),
BlobStoreClient: NewBlobStoreClient(cc),
BulkStoreClient: NewBulkStoreClient(cc),
ManagedObjectIndexClient: NewManagedObjectIndexClient(cc),
DiagnosticsClient: NewDiagnosticsClient(cc),
}, nil
}

@ -107,10 +107,22 @@ type IndexableDocument struct {
// When the resource is managed by an upstream repository
Manager *utils.ManagerProperties `json:"manager,omitempty"`
// indexed only field for faceting manager info
ManagedBy string `json:"managedBy,omitempty"`
// When the manager knows about file paths
Source *utils.SourceProperties `json:"source,omitempty"`
}
func (m *IndexableDocument) UpdateCopyFields() *IndexableDocument {
m.TitleNgram = m.Title
m.TitlePhrase = strings.ToLower(m.Title) // Lowercase for case-insensitive sorting ?? in the analyzer?
if m.Manager != nil {
m.ManagedBy = fmt.Sprintf("%s:%s", m.Manager.Kind, m.Manager.Identity)
}
return m
}
func (m *IndexableDocument) Type() string {
return m.Key.Resource
}
@ -169,20 +181,19 @@ func NewIndexableDocument(key *ResourceKey, rv int64, obj utils.GrafanaMetaAcces
}
}
doc := &IndexableDocument{
Key: key,
RV: rv,
Name: key.Name,
Title: title, // We always want *something* to display
TitleNgram: title,
TitlePhrase: strings.ToLower(title), // Lowercase for case-insensitive sorting
Labels: obj.GetLabels(),
Folder: obj.GetFolder(),
CreatedBy: obj.GetCreatedBy(),
UpdatedBy: obj.GetUpdatedBy(),
Key: key,
RV: rv,
Name: key.Name,
Title: title, // We always want *something* to display
Labels: obj.GetLabels(),
Folder: obj.GetFolder(),
CreatedBy: obj.GetCreatedBy(),
UpdatedBy: obj.GetUpdatedBy(),
}
m, ok := obj.GetManagerProperties()
if ok {
doc.Manager = &m
doc.ManagedBy = fmt.Sprintf("%s:%s", m.Kind, m.Identity)
}
s, ok := obj.GetSourceProperties()
if ok {
@ -196,7 +207,7 @@ func NewIndexableDocument(key *ResourceKey, rv int64, obj utils.GrafanaMetaAcces
if err != nil && tt != nil {
doc.Updated = tt.UnixMilli()
}
return doc
return doc.UpdateCopyFields()
}
func StandardDocumentBuilder() DocumentBuilder {
@ -280,6 +291,7 @@ const SEARCH_FIELD_CREATED_BY = "createdBy"
const SEARCH_FIELD_UPDATED = "updated"
const SEARCH_FIELD_UPDATED_BY = "updatedBy"
const SEARCH_FIELD_MANAGED_BY = "managedBy" // {kind}:{id}
const SEARCH_FIELD_MANAGER_KIND = "manager.kind"
const SEARCH_FIELD_MANAGER_ID = "manager.id"
const SEARCH_FIELD_SOURCE_PATH = "source.path"

@ -35,9 +35,9 @@ func TestStandardDocumentBuilder(t *testing.T) {
},
"name": "test1",
"rv": 10,
"title": "test playlist unified storage",
"title_phrase": "test playlist unified storage",
"title_ngram": "test playlist unified storage",
"title": "Test Playlist from Unified Storage",
"title_ngram": "Test Playlist from Unified Storage",
"title_phrase": "test playlist from unified storage",
"created": 1717236672000,
"createdBy": "user:ABC",
"updatedBy": "user:XYZ",
@ -45,6 +45,7 @@ func TestStandardDocumentBuilder(t *testing.T) {
"kind": "repo",
"id": "something"
},
"managedBy": "repo:something",
"source": {
"path": "path/in/system.json",
"checksum": "xyz"

@ -11,7 +11,7 @@ replace (
require (
github.com/fullstorydev/grpchan v1.1.1
github.com/google/uuid v1.6.0
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82
github.com/grafana/dskit v0.0.0-20241105154643-a6b453a88040
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
@ -131,7 +131,7 @@ require (
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/googleapis/go-sql-spanner v1.11.1 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356 // indirect
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e // indirect
github.com/grafana/dataplane/sdata v0.0.9 // indirect
github.com/grafana/grafana-app-sdk/logging v0.30.0 // indirect
github.com/grafana/grafana-aws-sdk v0.31.5 // indirect

@ -1150,10 +1150,10 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356 h1:71o8Bxw/wg+aBRUASiGux67gDEUC2bqq3I+x2hw8eIc=
github.com/grafana/alerting v0.0.0-20250307175047-1d263576d356/go.mod h1:hdGB3dSl8Ma9Rjo2YiAEAjMkZ5HiNJbNDqRKDefRZrM=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501 h1:FTuDRy/Shw8yOdG+v1DnkeuaCAl8fvwgcfaG9Wccuhg=
github.com/grafana/authlib v0.0.0-20250225105729-99e678595501/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e h1:noJzp/qZGIto4XdZkvj2EKQ1bQeqCRs0bedxdJN17sQ=
github.com/grafana/alerting v0.0.0-20250310104713-16b885f1c79e/go.mod h1:HfvjmU3UqCIpoy9Z2wgKGrZ4A5vz+yQlP9ZXvCfEkiA=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa h1:08Wh/svkv8WpDuOBBKAzSPa14gKjYLZvQJsHWXLjPuc=
github.com/grafana/authlib v0.0.0-20250305132846-37f49eb947fa/go.mod h1:XVpdLhaeYqz414FmGnW00/0vTe1x8c0GRH3KaeRtyg0=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82 h1:DnRUYiAotHXnrfYJCvhH1NkiyWVcPm5Pd+P7Ugqt/d8=
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM=
github.com/grafana/dataplane/sdata v0.0.9 h1:AGL1LZnCUG4MnQtnWpBPbQ8ZpptaZs14w6kE/MWfg7s=

File diff suppressed because it is too large Load Diff

@ -506,18 +506,21 @@ message ResourceSearchResponse {
// List items within a resource type & repository name
// Access control is managed above this request
message ListRepositoryObjectsRequest {
message ListManagedObjectsRequest {
// Starting from the requested page (other query parameters must match!)
string next_page_token = 1;
// Namespace (tenant)
string namespace = 2;
// The name of the repository
string name = 3;
// The manager type (eg, terraform vs repo)
string kind = 3;
// The name of the manager
string id = 4;
}
message ListRepositoryObjectsResponse {
message ListManagedObjectsResponse {
message Item {
// The resource object key
ResourceKey object = 1;
@ -549,22 +552,25 @@ message ListRepositoryObjectsResponse {
}
// Count the items that exist with
message CountRepositoryObjectsRequest {
message CountManagedObjectsRequest {
// Namespace (tenant)
string namespace = 1;
// The name of the repository
// empty to count across all repositories
string name = 2;
// Manager kind: terraform, plugin, kubectl, repo
string kind = 2;
// Name of the manager (meaningful inside kind)
string id = 3;
}
// Count the items that exist with
message CountRepositoryObjectsResponse {
message CountManagedObjectsResponse {
message ResourceCount {
string repository = 1;
string group = 2;
string resource = 3;
int64 count = 4;
string kind = 1;
string id = 2;
string group = 3;
string resource = 4;
int64 count = 5;
}
// Resource counts
@ -837,14 +843,14 @@ service ResourceIndex {
rpc GetStats(ResourceStatsRequest) returns (ResourceStatsResponse);
}
// Query repository info from the search index.
// Query managed objects
// Results access control is based on access to the repository *not* the items
service RepositoryIndex {
service ManagedObjectIndex {
// Describe how many resources of each type exist within a repository
rpc CountRepositoryObjects(CountRepositoryObjectsRequest) returns (CountRepositoryObjectsResponse);
rpc CountManagedObjects(CountManagedObjectsRequest) returns (CountManagedObjectsResponse);
// List the resources of a specific kind within a repository
rpc ListRepositoryObjects(ListRepositoryObjectsRequest) returns (ListRepositoryObjectsResponse);
rpc ListManagedObjects(ListManagedObjectsRequest) returns (ListManagedObjectsResponse);
}
service BlobStore {

@ -650,136 +650,136 @@ var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
}
const (
RepositoryIndex_CountRepositoryObjects_FullMethodName = "/resource.RepositoryIndex/CountRepositoryObjects"
RepositoryIndex_ListRepositoryObjects_FullMethodName = "/resource.RepositoryIndex/ListRepositoryObjects"
ManagedObjectIndex_CountManagedObjects_FullMethodName = "/resource.ManagedObjectIndex/CountManagedObjects"
ManagedObjectIndex_ListManagedObjects_FullMethodName = "/resource.ManagedObjectIndex/ListManagedObjects"
)
// RepositoryIndexClient is the client API for RepositoryIndex service.
// ManagedObjectIndexClient is the client API for ManagedObjectIndex service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// Query repository info from the search index.
// Query managed objects
// Results access control is based on access to the repository *not* the items
type RepositoryIndexClient interface {
type ManagedObjectIndexClient interface {
// Describe how many resources of each type exist within a repository
CountRepositoryObjects(ctx context.Context, in *CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*CountRepositoryObjectsResponse, error)
CountManagedObjects(ctx context.Context, in *CountManagedObjectsRequest, opts ...grpc.CallOption) (*CountManagedObjectsResponse, error)
// List the resources of a specific kind within a repository
ListRepositoryObjects(ctx context.Context, in *ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*ListRepositoryObjectsResponse, error)
ListManagedObjects(ctx context.Context, in *ListManagedObjectsRequest, opts ...grpc.CallOption) (*ListManagedObjectsResponse, error)
}
type repositoryIndexClient struct {
type managedObjectIndexClient struct {
cc grpc.ClientConnInterface
}
func NewRepositoryIndexClient(cc grpc.ClientConnInterface) RepositoryIndexClient {
return &repositoryIndexClient{cc}
func NewManagedObjectIndexClient(cc grpc.ClientConnInterface) ManagedObjectIndexClient {
return &managedObjectIndexClient{cc}
}
func (c *repositoryIndexClient) CountRepositoryObjects(ctx context.Context, in *CountRepositoryObjectsRequest, opts ...grpc.CallOption) (*CountRepositoryObjectsResponse, error) {
func (c *managedObjectIndexClient) CountManagedObjects(ctx context.Context, in *CountManagedObjectsRequest, opts ...grpc.CallOption) (*CountManagedObjectsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CountRepositoryObjectsResponse)
err := c.cc.Invoke(ctx, RepositoryIndex_CountRepositoryObjects_FullMethodName, in, out, cOpts...)
out := new(CountManagedObjectsResponse)
err := c.cc.Invoke(ctx, ManagedObjectIndex_CountManagedObjects_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *repositoryIndexClient) ListRepositoryObjects(ctx context.Context, in *ListRepositoryObjectsRequest, opts ...grpc.CallOption) (*ListRepositoryObjectsResponse, error) {
func (c *managedObjectIndexClient) ListManagedObjects(ctx context.Context, in *ListManagedObjectsRequest, opts ...grpc.CallOption) (*ListManagedObjectsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRepositoryObjectsResponse)
err := c.cc.Invoke(ctx, RepositoryIndex_ListRepositoryObjects_FullMethodName, in, out, cOpts...)
out := new(ListManagedObjectsResponse)
err := c.cc.Invoke(ctx, ManagedObjectIndex_ListManagedObjects_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// RepositoryIndexServer is the server API for RepositoryIndex service.
// All implementations should embed UnimplementedRepositoryIndexServer
// ManagedObjectIndexServer is the server API for ManagedObjectIndex service.
// All implementations should embed UnimplementedManagedObjectIndexServer
// for forward compatibility
//
// Query repository info from the search index.
// Query managed objects
// Results access control is based on access to the repository *not* the items
type RepositoryIndexServer interface {
type ManagedObjectIndexServer interface {
// Describe how many resources of each type exist within a repository
CountRepositoryObjects(context.Context, *CountRepositoryObjectsRequest) (*CountRepositoryObjectsResponse, error)
CountManagedObjects(context.Context, *CountManagedObjectsRequest) (*CountManagedObjectsResponse, error)
// List the resources of a specific kind within a repository
ListRepositoryObjects(context.Context, *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error)
ListManagedObjects(context.Context, *ListManagedObjectsRequest) (*ListManagedObjectsResponse, error)
}
// UnimplementedRepositoryIndexServer should be embedded to have forward compatible implementations.
type UnimplementedRepositoryIndexServer struct {
// UnimplementedManagedObjectIndexServer should be embedded to have forward compatible implementations.
type UnimplementedManagedObjectIndexServer struct {
}
func (UnimplementedRepositoryIndexServer) CountRepositoryObjects(context.Context, *CountRepositoryObjectsRequest) (*CountRepositoryObjectsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CountRepositoryObjects not implemented")
func (UnimplementedManagedObjectIndexServer) CountManagedObjects(context.Context, *CountManagedObjectsRequest) (*CountManagedObjectsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CountManagedObjects not implemented")
}
func (UnimplementedRepositoryIndexServer) ListRepositoryObjects(context.Context, *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListRepositoryObjects not implemented")
func (UnimplementedManagedObjectIndexServer) ListManagedObjects(context.Context, *ListManagedObjectsRequest) (*ListManagedObjectsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListManagedObjects not implemented")
}
// UnsafeRepositoryIndexServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to RepositoryIndexServer will
// UnsafeManagedObjectIndexServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ManagedObjectIndexServer will
// result in compilation errors.
type UnsafeRepositoryIndexServer interface {
mustEmbedUnimplementedRepositoryIndexServer()
type UnsafeManagedObjectIndexServer interface {
mustEmbedUnimplementedManagedObjectIndexServer()
}
func RegisterRepositoryIndexServer(s grpc.ServiceRegistrar, srv RepositoryIndexServer) {
s.RegisterService(&RepositoryIndex_ServiceDesc, srv)
func RegisterManagedObjectIndexServer(s grpc.ServiceRegistrar, srv ManagedObjectIndexServer) {
s.RegisterService(&ManagedObjectIndex_ServiceDesc, srv)
}
func _RepositoryIndex_CountRepositoryObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CountRepositoryObjectsRequest)
func _ManagedObjectIndex_CountManagedObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CountManagedObjectsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RepositoryIndexServer).CountRepositoryObjects(ctx, in)
return srv.(ManagedObjectIndexServer).CountManagedObjects(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RepositoryIndex_CountRepositoryObjects_FullMethodName,
FullMethod: ManagedObjectIndex_CountManagedObjects_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RepositoryIndexServer).CountRepositoryObjects(ctx, req.(*CountRepositoryObjectsRequest))
return srv.(ManagedObjectIndexServer).CountManagedObjects(ctx, req.(*CountManagedObjectsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RepositoryIndex_ListRepositoryObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRepositoryObjectsRequest)
func _ManagedObjectIndex_ListManagedObjects_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListManagedObjectsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RepositoryIndexServer).ListRepositoryObjects(ctx, in)
return srv.(ManagedObjectIndexServer).ListManagedObjects(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RepositoryIndex_ListRepositoryObjects_FullMethodName,
FullMethod: ManagedObjectIndex_ListManagedObjects_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RepositoryIndexServer).ListRepositoryObjects(ctx, req.(*ListRepositoryObjectsRequest))
return srv.(ManagedObjectIndexServer).ListManagedObjects(ctx, req.(*ListManagedObjectsRequest))
}
return interceptor(ctx, in, info, handler)
}
// RepositoryIndex_ServiceDesc is the grpc.ServiceDesc for RepositoryIndex service.
// ManagedObjectIndex_ServiceDesc is the grpc.ServiceDesc for ManagedObjectIndex service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var RepositoryIndex_ServiceDesc = grpc.ServiceDesc{
ServiceName: "resource.RepositoryIndex",
HandlerType: (*RepositoryIndexServer)(nil),
var ManagedObjectIndex_ServiceDesc = grpc.ServiceDesc{
ServiceName: "resource.ManagedObjectIndex",
HandlerType: (*ManagedObjectIndexServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "CountRepositoryObjects",
Handler: _RepositoryIndex_CountRepositoryObjects_Handler,
MethodName: "CountManagedObjects",
Handler: _ManagedObjectIndex_CountManagedObjects_Handler,
},
{
MethodName: "ListRepositoryObjects",
Handler: _RepositoryIndex_ListRepositoryObjects_Handler,
MethodName: "ListManagedObjects",
Handler: _ManagedObjectIndex_ListManagedObjects_Handler,
},
},
Streams: []grpc.StreamDesc{},

@ -48,10 +48,10 @@ type ResourceIndex interface {
Search(ctx context.Context, access types.AccessClient, req *ResourceSearchRequest, federate []ResourceIndex) (*ResourceSearchResponse, error)
// List within an response
ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error)
ListManagedObjects(ctx context.Context, req *ListManagedObjectsRequest) (*ListManagedObjectsResponse, error)
// Counts the values in a repo
CountRepositoryObjects(ctx context.Context) ([]*CountRepositoryObjectsResponse_ResourceCount, error)
CountManagedObjects(ctx context.Context) ([]*CountManagedObjectsResponse_ResourceCount, error)
// Get the number of documents in the index
DocCount(ctx context.Context, folder string) (int64, error)
@ -99,8 +99,8 @@ type searchSupport struct {
}
var (
_ ResourceIndexServer = (*searchSupport)(nil)
_ RepositoryIndexServer = (*searchSupport)(nil)
_ ResourceIndexServer = (*searchSupport)(nil)
_ ManagedObjectIndexServer = (*searchSupport)(nil)
)
func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.AccessClient, blob BlobSupport, tracer trace.Tracer) (support *searchSupport, err error) {
@ -139,14 +139,14 @@ func newSearchSupport(opts SearchOptions, storage StorageBackend, access types.A
return support, err
}
func (s *searchSupport) ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
func (s *searchSupport) ListManagedObjects(ctx context.Context, req *ListManagedObjectsRequest) (*ListManagedObjectsResponse, error) {
if req.NextPageToken != "" {
return &ListRepositoryObjectsResponse{
return &ListManagedObjectsResponse{
Error: NewBadRequestError("multiple pages not yet supported"),
}, nil
}
rsp := &ListRepositoryObjectsResponse{}
rsp := &ListManagedObjectsResponse{}
stats, err := s.storage.GetResourceStats(ctx, req.Namespace, 0)
if err != nil {
rsp.Error = AsErrorResult(err)
@ -164,7 +164,7 @@ func (s *searchSupport) ListRepositoryObjects(ctx context.Context, req *ListRepo
return rsp, nil
}
kind, err := idx.ListRepositoryObjects(ctx, req)
kind, err := idx.ListManagedObjects(ctx, req)
if err != nil {
rsp.Error = AsErrorResult(err)
return rsp, nil
@ -179,15 +179,15 @@ func (s *searchSupport) ListRepositoryObjects(ctx context.Context, req *ListRepo
}
// Sort based on path
slices.SortFunc(rsp.Items, func(a, b *ListRepositoryObjectsResponse_Item) int {
slices.SortFunc(rsp.Items, func(a, b *ListManagedObjectsResponse_Item) int {
return cmp.Compare(a.Path, b.Path)
})
return rsp, nil
}
func (s *searchSupport) CountRepositoryObjects(ctx context.Context, req *CountRepositoryObjectsRequest) (*CountRepositoryObjectsResponse, error) {
rsp := &CountRepositoryObjectsResponse{}
func (s *searchSupport) CountManagedObjects(ctx context.Context, req *CountManagedObjectsRequest) (*CountManagedObjectsResponse, error) {
rsp := &CountManagedObjectsResponse{}
stats, err := s.storage.GetResourceStats(ctx, req.Namespace, 0)
if err != nil {
rsp.Error = AsErrorResult(err)
@ -205,27 +205,27 @@ func (s *searchSupport) CountRepositoryObjects(ctx context.Context, req *CountRe
return rsp, nil
}
counts, err := idx.CountRepositoryObjects(ctx)
counts, err := idx.CountManagedObjects(ctx)
if err != nil {
rsp.Error = AsErrorResult(err)
return rsp, nil
}
if req.Name == "" {
if req.Id == "" {
rsp.Items = append(rsp.Items, counts...)
} else {
for _, k := range counts {
if k.Repository == req.Name {
k.Repository = "" // avoid duplicate response metadata
if k.Id == req.Id {
rsp.Items = append(rsp.Items, k)
}
}
}
}
// Sort based on repo/group/resource
slices.SortFunc(rsp.Items, func(a, b *CountRepositoryObjectsResponse_ResourceCount) int {
// Sort based on manager/group/resource
slices.SortFunc(rsp.Items, func(a, b *CountManagedObjectsResponse_ResourceCount) int {
return cmp.Or(
cmp.Compare(a.Repository, b.Repository),
cmp.Compare(a.Kind, b.Kind),
cmp.Compare(a.Id, b.Id),
cmp.Compare(a.Group, b.Group),
cmp.Compare(a.Resource, b.Resource),
)

@ -28,7 +28,7 @@ type ResourceServer interface {
ResourceStoreServer
BulkStoreServer
ResourceIndexServer
RepositoryIndexServer
ManagedObjectIndexServer
BlobStoreServer
DiagnosticsServer
}
@ -1105,12 +1105,12 @@ func (s *server) GetStats(ctx context.Context, req *ResourceStatsRequest) (*Reso
return s.search.GetStats(ctx, req)
}
func (s *server) ListRepositoryObjects(ctx context.Context, req *ListRepositoryObjectsRequest) (*ListRepositoryObjectsResponse, error) {
return s.search.ListRepositoryObjects(ctx, req)
func (s *server) ListManagedObjects(ctx context.Context, req *ListManagedObjectsRequest) (*ListManagedObjectsResponse, error) {
return s.search.ListManagedObjects(ctx, req)
}
func (s *server) CountRepositoryObjects(ctx context.Context, req *CountRepositoryObjectsRequest) (*CountRepositoryObjectsResponse, error) {
return s.search.CountRepositoryObjects(ctx, req)
func (s *server) CountManagedObjects(ctx context.Context, req *CountManagedObjectsRequest) (*CountManagedObjectsResponse, error) {
return s.search.CountManagedObjects(ctx, req)
}
// IsHealthy implements ResourceServer.

@ -16,7 +16,7 @@
}
},
"spec": {
"title": "test playlist unified storage",
"title": "Test Playlist from Unified Storage",
"description": "description for the test playlist"
}
}

@ -283,6 +283,8 @@ type bleveIndex struct {
// Write implements resource.DocumentIndex.
func (b *bleveIndex) Write(v *resource.IndexableDocument) error {
v = v.UpdateCopyFields()
// remove references (for now!)
v.References = nil
if b.batch != nil {
@ -321,21 +323,33 @@ func (b *bleveIndex) Flush() (err error) {
return err
}
func (b *bleveIndex) ListRepositoryObjects(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
func (b *bleveIndex) ListManagedObjects(ctx context.Context, req *resource.ListManagedObjectsRequest) (*resource.ListManagedObjectsResponse, error) {
if req.NextPageToken != "" {
return nil, fmt.Errorf("next page not implemented yet")
}
if req.Name == "" {
return &resource.ListRepositoryObjectsResponse{
Error: resource.NewBadRequestError("empty repository name"),
if req.Kind == "" {
return &resource.ListManagedObjectsResponse{
Error: resource.NewBadRequestError("empty manager kind"),
}, nil
}
if req.Id == "" {
return &resource.ListManagedObjectsResponse{
Error: resource.NewBadRequestError("empty manager id"),
}, nil
}
q := bleve.NewBooleanQuery()
q.AddMust(&query.TermQuery{
Term: req.Kind,
FieldVal: resource.SEARCH_FIELD_MANAGER_KIND,
})
q.AddMust(&query.TermQuery{
Term: req.Id,
FieldVal: resource.SEARCH_FIELD_MANAGER_ID,
})
found, err := b.index.SearchInContext(ctx, &bleve.SearchRequest{
Query: &query.TermQuery{
Term: req.Name,
FieldVal: resource.SEARCH_FIELD_MANAGER_ID,
},
Query: q,
Fields: []string{
resource.SEARCH_FIELD_TITLE,
resource.SEARCH_FIELD_FOLDER,
@ -390,9 +404,9 @@ func (b *bleveIndex) ListRepositoryObjects(ctx context.Context, req *resource.Li
return 0
}
rsp := &resource.ListRepositoryObjectsResponse{}
rsp := &resource.ListManagedObjectsResponse{}
for _, hit := range found.Hits {
item := &resource.ListRepositoryObjectsResponse_Item{
item := &resource.ListManagedObjectsResponse_Item{
Object: &resource.ResourceKey{},
Hash: asString(hit.Fields[resource.SEARCH_FIELD_SOURCE_CHECKSUM]),
Path: asString(hit.Fields[resource.SEARCH_FIELD_SOURCE_PATH]),
@ -409,27 +423,32 @@ func (b *bleveIndex) ListRepositoryObjects(ctx context.Context, req *resource.Li
return rsp, nil
}
func (b *bleveIndex) CountRepositoryObjects(ctx context.Context) ([]*resource.CountRepositoryObjectsResponse_ResourceCount, error) {
func (b *bleveIndex) CountManagedObjects(ctx context.Context) ([]*resource.CountManagedObjectsResponse_ResourceCount, error) {
found, err := b.index.SearchInContext(ctx, &bleve.SearchRequest{
Query: bleve.NewMatchAllQuery(),
Size: 0,
Facets: bleve.FacetsRequest{
"count": bleve.NewFacetRequest(resource.SEARCH_FIELD_MANAGER_ID, 1000), // typically less then 5
"count": bleve.NewFacetRequest(resource.SEARCH_FIELD_MANAGED_BY, 1000), // typically less then 5
},
})
if err != nil {
return nil, err
}
vals := make([]*resource.CountRepositoryObjectsResponse_ResourceCount, 0)
vals := make([]*resource.CountManagedObjectsResponse_ResourceCount, 0)
f, ok := found.Facets["count"]
if ok && f.Terms != nil {
for _, v := range f.Terms.Terms() {
vals = append(vals, &resource.CountRepositoryObjectsResponse_ResourceCount{
Repository: v.Term,
Group: b.key.Group,
Resource: b.key.Resource,
Count: int64(v.Count),
})
val := v.Term
idx := strings.Index(val, ":")
if idx > 0 {
vals = append(vals, &resource.CountManagedObjectsResponse_ResourceCount{
Kind: val[0:idx],
Id: val[idx+1:],
Group: b.key.Group,
Resource: b.key.Resource,
Count: int64(v.Count),
})
}
}
}
return vals, nil

@ -34,7 +34,7 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
// for searching by title - uses an edge ngram token filter
titleSearchMapping := bleve.NewTextFieldMapping()
titleSearchMapping.Analyzer = TITLE_ANALYZER
titleSearchMapping.Store = true
titleSearchMapping.Store = false // already stored in title
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_NGRAM, titleSearchMapping)
// mapping for title to search on words/tokens larger than the ngram size
@ -45,6 +45,7 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
// for filtering/sorting by title full phrase
titlePhraseMapping := bleve.NewKeywordFieldMapping()
titleSearchMapping.Store = false // already stored in title
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_TITLE_PHRASE, titlePhraseMapping)
descriptionMapping := &mapping.FieldMapping{
@ -124,8 +125,17 @@ func getBleveDocMappings(_ resource.SearchableDocumentFields) *mapping.DocumentM
})
source.AddFieldMappingsAt("timestampMillis", mapping.NewNumericFieldMapping())
mapper.AddSubDocumentMapping("manager", manager)
mapper.AddSubDocumentMapping("source", source)
mapper.AddSubDocumentMapping("manager", manager)
mapper.AddFieldMappingsAt(resource.SEARCH_FIELD_MANAGED_BY, &mapping.FieldMapping{
Name: "managedBy",
Type: "text",
Analyzer: keyword.Name,
Index: true, // only used for faceting
Store: false,
IncludeTermVectors: false,
IncludeInAll: false,
})
labelMapper := bleve.NewDocumentMapping()
mapper.AddSubDocumentMapping(resource.SEARCH_FIELD_LABELS, labelMapper)

@ -36,6 +36,7 @@ func TestDocumentMapping(t *testing.T) {
TimestampMillis: 1234,
},
}
data.UpdateCopyFields()
doc := document.NewDocument("id")
err = mappings.MapDocument(doc, data)
@ -47,5 +48,5 @@ func TestDocumentMapping(t *testing.T) {
fmt.Printf("DOC: fields %d\n", len(doc.Fields))
fmt.Printf("DOC: size %d\n", doc.Size())
require.Equal(t, 15, len(doc.Fields))
require.Equal(t, 16, len(doc.Fields))
}

@ -8,13 +8,14 @@ import (
"testing"
"github.com/blevesearch/bleve/v2"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/store/kind/dashboard"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/stretchr/testify/require"
)
func TestCanSearchByTitle(t *testing.T) {
@ -35,9 +36,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "bbb",
TitleNgram: "bbb",
TitlePhrase: "bbb",
Title: "bbb",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
@ -49,9 +48,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "aaa",
TitleNgram: "aaa",
TitlePhrase: "aaa",
Title: "aaa",
})
require.NoError(t, err)
@ -74,9 +71,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "I want to say a hello",
TitleNgram: "I want to say a hello",
TitlePhrase: "I want to say a hello",
Title: "I want to say a hello",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
@ -88,9 +83,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "we want hello",
TitleNgram: "we want hello",
TitlePhrase: "we want hello",
Title: "we want hello",
})
require.NoError(t, err)
@ -113,9 +106,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "Asserts Dashboards",
TitleNgram: "Asserts Dashboards",
TitlePhrase: "Asserts Dashboards",
Title: "Asserts Dashboards",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
@ -127,9 +118,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "New dashboard 10",
TitleNgram: "New dashboard 10",
TitlePhrase: "New dashboard 10",
Title: "New dashboard 10",
})
require.NoError(t, err)
@ -151,9 +140,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "we want hello pls",
TitleNgram: "we want hello pls",
TitlePhrase: "we want hello pls",
Title: "we want hello pls",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
@ -165,9 +152,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "we want hello",
TitleNgram: "we want hello",
TitlePhrase: "we want hello",
Title: "we want hello",
})
require.NoError(t, err)
@ -190,8 +175,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "A123456",
TitleNgram: "A123456",
Title: "A123456",
})
require.NoError(t, err)
@ -219,9 +203,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "I want to say a wonderfully Hello to the WORLD! Hello-world",
TitleNgram: "I want to say a wonderfully Hello to the WORLD! Hello-world",
TitlePhrase: "I want to say a wonderfully Hello to the WORLD! Hello-world",
Title: "I want to say a wonderfully Hello to the WORLD! Hello-world",
})
require.NoError(t, err)
@ -279,8 +261,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "I want to say a wonderful Hello to the WORLD! Hello-world",
TitleNgram: "I want to say a wonderful Hello to the WORLD! Hello-world",
Title: "I want to say a wonderful Hello to the WORLD! Hello-world",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
@ -292,8 +273,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "A0456",
TitleNgram: "A0456",
Title: "A0456",
})
require.NoError(t, err)
err = index.Write(&resource.IndexableDocument{
@ -305,9 +285,7 @@ func TestCanSearchByTitle(t *testing.T) {
Group: key.Group,
Resource: key.Resource,
},
Title: "mash-A02382-10",
TitleNgram: "mash-A02382-10",
TitlePhrase: "mash-A02382-10",
Title: "mash-A02382-10",
})
require.NoError(t, err)

@ -77,9 +77,8 @@ func TestBleveBackend(t *testing.T) {
Group: "dashboard.grafana.app",
Resource: "dashboards",
},
Title: "aaa (dash)",
TitlePhrase: "aaa (dash)",
Folder: "xxx",
Title: "aaa (dash)",
Folder: "xxx",
Fields: map[string]any{
DASHBOARD_PANEL_TYPES: []string{"timeseries", "table"},
DASHBOARD_ERRORS_TODAY: 25,
@ -109,9 +108,8 @@ func TestBleveBackend(t *testing.T) {
Group: "dashboard.grafana.app",
Resource: "dashboards",
},
Title: "bbb (dash)",
TitlePhrase: "bbb (dash)",
Folder: "xxx",
Title: "bbb (dash)",
Folder: "xxx",
Fields: map[string]any{
DASHBOARD_PANEL_TYPES: []string{"timeseries"},
DASHBOARD_ERRORS_TODAY: 40,
@ -141,10 +139,9 @@ func TestBleveBackend(t *testing.T) {
Group: "dashboard.grafana.app",
Resource: "dashboards",
},
Name: "ccc",
Title: "ccc (dash)",
TitlePhrase: "ccc (dash)",
Folder: "zzz",
Name: "ccc",
Title: "ccc (dash)",
Folder: "zzz",
Manager: &utils.ManagerProperties{
Kind: utils.ManagerKindRepo,
Identity: "repo2",
@ -267,8 +264,9 @@ func TestBleveBackend(t *testing.T) {
require.Equal(t, 0, len(rsp.Results.Rows))
// Now look for repositories
found, err := index.ListRepositoryObjects(ctx, &resource.ListRepositoryObjectsRequest{
Name: "repo-1",
found, err := index.ListManagedObjects(ctx, &resource.ListManagedObjectsRequest{
Kind: "repo",
Id: "repo-1",
})
require.NoError(t, err)
jj, err := json.MarshalIndent(found, "", " ")
@ -306,20 +304,22 @@ func TestBleveBackend(t *testing.T) {
]
}`, string(jj))
counts, err := index.CountRepositoryObjects(ctx)
counts, err := index.CountManagedObjects(ctx)
require.NoError(t, err)
jj, err = json.MarshalIndent(counts, "", " ")
require.NoError(t, err)
fmt.Printf("%s\n", string(jj))
require.JSONEq(t, `[
{
"repository": "repo-1",
"kind": "repo",
"id": "repo-1",
"group": "dashboard.grafana.app",
"resource": "dashboards",
"count": 2
},
{
"repository": "repo2",
"kind": "repo",
"id": "repo2",
"group": "dashboard.grafana.app",
"resource": "dashboards",
"count": 1
@ -344,8 +344,7 @@ func TestBleveBackend(t *testing.T) {
Group: "folder.grafana.app",
Resource: "folders",
},
Title: "zzz (folder)",
TitlePhrase: "zzz (folder)",
Title: "zzz (folder)",
Manager: &utils.ManagerProperties{
Kind: utils.ManagerKindRepo,
Identity: "repo-1",
@ -364,8 +363,7 @@ func TestBleveBackend(t *testing.T) {
Group: "folder.grafana.app",
Resource: "folders",
},
Title: "yyy (folder)",
TitlePhrase: "yyy (folder)",
Title: "yyy (folder)",
Labels: map[string]string{
"region": "west",
},

@ -15,5 +15,6 @@
"manager": {
"kind": "repo",
"id": "MyGIT"
}
},
"managedBy": "repo:MyGIT"
}

@ -15,5 +15,6 @@
"manager": {
"kind": "repo",
"id": "MyGIT"
}
},
"managedBy": "repo:MyGIT"
}

@ -53,11 +53,11 @@ func (b *backend) GetStats(ctx context.Context, req *resource.ResourceStatsReque
return rsp, nil
}
func (b *backend) RepositoryList(ctx context.Context, req *resource.ListRepositoryObjectsRequest) (*resource.ListRepositoryObjectsResponse, error) {
func (b *backend) RepositoryList(ctx context.Context, req *resource.ListManagedObjectsRequest) (*resource.ListManagedObjectsResponse, error) {
return nil, fmt.Errorf("SQL backend does not implement RepositoryList")
}
func (b *backend) RepositoryStats(context.Context, *resource.CountRepositoryObjectsRequest) (*resource.CountRepositoryObjectsResponse, error) {
func (b *backend) RepositoryStats(context.Context, *resource.CountManagedObjectsRequest) (*resource.CountManagedObjectsResponse, error) {
return nil, fmt.Errorf("SQL backend does not implement RepositoryStats")
}

@ -133,7 +133,7 @@ func (s *service) start(ctx context.Context) error {
resource.RegisterResourceStoreServer(srv, server)
resource.RegisterBulkStoreServer(srv, server)
resource.RegisterResourceIndexServer(srv, server)
resource.RegisterRepositoryIndexServer(srv, server)
resource.RegisterManagedObjectIndexServer(srv, server)
resource.RegisterBlobStoreServer(srv, server)
resource.RegisterDiagnosticsServer(srv, server)
grpc_health_v1.RegisterHealthServer(srv, healthService)

@ -74,6 +74,9 @@ func TestIntegrationOpenAPIs(t *testing.T) {
}, {
Group: "investigations.grafana.app",
Version: "v0alpha1",
}, {
Group: "folder.grafana.app",
Version: "v0alpha1",
}}
for _, gv := range groups {
VerifyOpenAPISnapshots(t, dir, gv, h)

@ -128,7 +128,6 @@ func handleConversionError(ctxLogger log.Logger, span trace.Span, err error) (*b
func (s *Service) performMetricsQuery(ctx context.Context, dsInfo *Datasource, model *dataquery.TempoQuery, query backend.DataQuery, span trace.Span) (*http.Response, []byte, error) {
ctxLogger := s.logger.FromContext(ctx)
request, err := s.createMetricsQuery(ctx, dsInfo, model, query.TimeRange.From.Unix(), query.TimeRange.To.Unix())
if err != nil {
ctxLogger.Error("Failed to create request", "error", err, "function", logEntrypoint())
span.RecordError(err)
@ -202,6 +201,6 @@ func isInstantQuery(metricQueryType *dataquery.MetricsQueryType) bool {
}
func isMetricsQuery(query string) bool {
match, _ := regexp.MatchString("\\|\\s*(rate|count_over_time|avg_over_time|max_over_time|min_over_time|quantile_over_time|histogram_over_time|compare)\\s*\\(", query)
match, _ := regexp.MatchString("\\|\\s*(rate|count_over_time|avg_over_time|sum_over_time|max_over_time|min_over_time|quantile_over_time|histogram_over_time|compare)\\s*\\(", query)
return match
}

@ -34,6 +34,7 @@ func TestCreateMetricsQuery_Success(t *testing.T) {
assert.Equal(t, "http://tempo:3100/api/metrics/query_range?end=1625184000&exemplars=123&q=%7Battribute%3D%22value%22%7D&start=1625097600&step=14", req.URL.String())
assert.Equal(t, "application/json", req.Header.Get("Accept"))
}
func TestCreateMetricsQuery_OnlyQuery(t *testing.T) {
logger := backend.NewLoggerWith("logger", "tsdb.tempo.test")
service := &Service{
@ -94,6 +95,11 @@ func TestQueryWithAvgOverTimeFunction_ReturnsTrue(t *testing.T) {
assert.True(t, result)
}
func TestQueryWithSumOverTimeFunction_ReturnsTrue(t *testing.T) {
result := isMetricsQuery("{} | sum_over_time(foo)")
assert.True(t, result)
}
func TestQueryWithCountOverTimeFunction_ReturnsTrue(t *testing.T) {
result := isMetricsQuery("{} | count_over_time(foo)")
assert.True(t, result)

@ -42,7 +42,8 @@ type Engine struct {
tagHandlers map[string]tagHandler
defaultContext context.Context
defaultContext context.Context
sequenceGenerator *sequenceGenerator // If not nil, this generator is used to generate auto-increment values for inserts.
}
// CondDeleted returns the conditions whether a record is soft deleted.
@ -237,6 +238,9 @@ func (engine *Engine) NewSession() *Session {
// Close the engine
func (engine *Engine) Close() error {
if engine.sequenceGenerator != nil {
engine.sequenceGenerator.close()
}
return engine.db.Close()
}

@ -3,7 +3,9 @@ module github.com/grafana/grafana/pkg/util/xorm
go 1.23.7
require (
cloud.google.com/go/spanner v1.75.0
github.com/googleapis/go-sql-spanner v1.11.1
github.com/grafana/grafana v5.4.5+incompatible
github.com/mattn/go-sqlite3 v1.14.22
github.com/stretchr/testify v1.10.0
xorm.io/builder v0.3.6
@ -19,9 +21,9 @@ require (
cloud.google.com/go/iam v1.3.1 // indirect
cloud.google.com/go/longrunning v0.6.4 // indirect
cloud.google.com/go/monitoring v1.23.0 // indirect
cloud.google.com/go/spanner v1.75.0 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect

@ -632,6 +632,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -832,6 +834,8 @@ github.com/googleapis/go-sql-spanner v1.11.1 h1:z3ThtKV5HFvaNv9UGc26+ggS+lS0dsCA
github.com/googleapis/go-sql-spanner v1.11.1/go.mod h1:fuA5q4yMS3SZiVfRr5bvksPNk7zUn/irbQW62H/ffZw=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/grafana/grafana v5.4.5+incompatible h1:xNuhSBxLgwDwesuQIAhQu1QCk6tD0TAghKHE36/hxrs=
github.com/grafana/grafana v5.4.5+incompatible/go.mod h1:U8QyUclJHj254BFcuw45p6sg7eeGYX44qn1ShYo5rGE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=

@ -0,0 +1,69 @@
package xorm
import (
"context"
"database/sql"
"errors"
"fmt"
)
type sequenceGenerator struct {
db *sql.DB
sequencesTable string
}
func newSequenceGenerator(db *sql.DB) *sequenceGenerator {
return &sequenceGenerator{
db: db,
sequencesTable: "autoincrement_sequences",
}
}
func (sg *sequenceGenerator) Next(ctx context.Context, table, column string) (int64, error) {
// Current implementation fetches new value for each Next call.
key := fmt.Sprintf("%s:%s", table, column)
tx, err := sg.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return 0, err
}
// TODO: "FOR UPDATE". (Somehow this doesn't seem to be supported in Spanner emulator?)
r, err := tx.QueryContext(ctx, "SELECT next_value FROM "+sg.sequencesTable+" WHERE name = ?", key)
if err != nil {
err2 := tx.Rollback()
return 0, errors.Join(err, err2)
}
defer r.Close()
// Sequence doesn't exist yet. Return 1, and put 2 into the table.
if !r.Next() {
if err := r.Err(); err != nil {
return 0, errors.Join(err, tx.Rollback())
}
val := int64(1)
_, err := tx.ExecContext(ctx, "INSERT INTO "+sg.sequencesTable+" (name, next_value) VALUES(?, ?)", key, val+1)
if err != nil {
return 0, errors.Join(err, tx.Rollback())
}
return val, tx.Commit()
}
var val int64
if err := r.Scan(&val); err != nil {
return 0, errors.Join(err, tx.Rollback())
}
_, err = tx.ExecContext(ctx, "UPDATE "+sg.sequencesTable+" SET next_value = ? WHERE name = ?", val+1, key)
if err != nil {
return 0, errors.Join(err, tx.Rollback())
}
return val, tx.Commit()
}
func (sg *sequenceGenerator) close() {
// Nothing to do just yet.
}

@ -0,0 +1,31 @@
package xorm
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestSequenceGenerator(t *testing.T) {
eng, err := NewEngine("sqlite3", ":memory:")
require.NoError(t, err)
require.NotNil(t, eng)
require.Equal(t, "sqlite3", eng.DriverName())
_, err = eng.Exec("CREATE TABLE `autoincrement_sequences` (`name` STRING(128) NOT NULL PRIMARY KEY, `next_value` INT64 NOT NULL)")
require.NoError(t, err)
sg := newSequenceGenerator(eng.db.DB)
val, err := sg.Next(context.Background(), "test", "test")
require.NoError(t, err)
require.Equal(t, int64(1), val)
val, err = sg.Next(context.Background(), "test", "different")
require.NoError(t, err)
require.Equal(t, int64(1), val)
val, err = sg.Next(context.Background(), "test", "different")
require.NoError(t, err)
require.Equal(t, int64(2), val)
}

@ -345,20 +345,25 @@ func (session *Session) innerInsert(bean any) (int64, error) {
return 0, err
}
//// XXX: hack to handle autoincrement in spanner
//if len(table.AutoIncrement) > 0 && session.engine.dialect.DBType() == "spanner" {
// var found bool
// for _, col := range colNames {
// if col == table.AutoIncrement {
// found = true
// break
// }
// }
// if !found {
// colNames = append(colNames, table.AutoIncrement)
// args = append(args, rand.Int63n(9e15))
// }
//}
// If engine has a sequence number generator, use it to produce values for auto-increment columns.
if len(table.AutoIncrement) > 0 && session.engine.sequenceGenerator != nil {
var found bool
for _, col := range colNames {
if col == table.AutoIncrement {
found = true
break
}
}
if !found {
seq, err := session.engine.sequenceGenerator.Next(session.ctx, table.Name, table.AutoIncrement)
if err != nil {
return 0, fmt.Errorf("failed to generate next value for auto_increment columns: %v", err)
}
colNames = append(colNames, table.AutoIncrement)
args = append(args, seq)
}
}
exprs := session.statement.exprColumns
colPlaces := strings.Repeat("?, ", len(colNames))
@ -438,9 +443,11 @@ func (session *Session) innerInsert(bean any) (int64, error) {
}
if len(table.AutoIncrement) > 0 && session.engine.dialect.DBType() == core.POSTGRES {
if _, err := buf.WriteString(" RETURNING " + session.engine.Quote(table.AutoIncrement)); err != nil {
return 0, err
}
buf.WriteString(" RETURNING " + session.engine.Quote(table.AutoIncrement))
}
if len(table.AutoIncrement) > 0 && session.engine.dialect.DBType() == "spanner" {
buf.WriteString(" THEN RETURN " + session.engine.Quote(table.AutoIncrement))
}
sqlStr := buf.String()
@ -476,6 +483,7 @@ func (session *Session) innerInsert(bean any) (int64, error) {
// for postgres, many of them didn't implement lastInsertId, so we should
// implemented it ourself.
var insertID, rowsAffected int64
if session.engine.dialect.DBType() == core.ORACLE && len(table.AutoIncrement) > 0 {
res, err := session.queryBytes("select seq_atable.currval from dual", args...)
if err != nil {
@ -498,23 +506,11 @@ func (session *Session) innerInsert(bean any) (int64, error) {
}
idByte := res[0][table.AutoIncrement]
id, err := strconv.ParseInt(string(idByte), 10, 64)
if err != nil || id <= 0 {
insertID, err = strconv.ParseInt(string(idByte), 10, 64)
if err != nil || insertID <= 0 {
return 1, err
}
aiValue, err := table.AutoIncrColumn().ValueOf(bean)
if err != nil {
session.engine.logger.Error(err)
}
if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() {
return 1, nil
}
aiValue.Set(int64ToIntValue(id, aiValue.Type()))
return 1, nil
rowsAffected = 1
} else if len(table.AutoIncrement) > 0 && (session.engine.dialect.DBType() == core.POSTGRES) {
res, err := session.queryBytes(sqlStr, args...)
@ -537,23 +533,11 @@ func (session *Session) innerInsert(bean any) (int64, error) {
}
idByte := res[0][table.AutoIncrement]
id, err := strconv.ParseInt(string(idByte), 10, 64)
if err != nil || id <= 0 {
insertID, err = strconv.ParseInt(string(idByte), 10, 64)
if err != nil || insertID <= 0 {
return 1, err
}
aiValue, err := table.AutoIncrColumn().ValueOf(bean)
if err != nil {
session.engine.logger.Error(err)
}
if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() {
return 1, nil
}
aiValue.Set(int64ToIntValue(id, aiValue.Type()))
return 1, nil
rowsAffected = 1
} else {
res, err := session.exec(sqlStr, args...)
if err != nil {
@ -575,25 +559,29 @@ func (session *Session) innerInsert(bean any) (int64, error) {
return res.RowsAffected()
}
var id int64
id, err = res.LastInsertId()
if err != nil || id <= 0 {
insertID, err = res.LastInsertId()
if err != nil || insertID <= 0 {
return res.RowsAffected()
}
aiValue, err := table.AutoIncrColumn().ValueOf(bean)
rowsAffected, err = res.RowsAffected()
if err != nil {
session.engine.logger.Error(err)
}
if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() {
return res.RowsAffected()
return 0, err
}
}
aiValue.Set(int64ToIntValue(id, aiValue.Type()))
// Set insertID back to the bean.
aiValue, err := table.AutoIncrColumn().ValueOf(bean)
if err != nil {
session.engine.logger.Error(err)
}
return res.RowsAffected()
if aiValue == nil || !aiValue.IsValid() || !aiValue.CanSet() {
return rowsAffected, nil
}
aiValue.Set(int64ToIntValue(insertID, aiValue.Type()))
return rowsAffected, nil
}
// InsertOne insert only one struct into database as a record.

@ -320,7 +320,11 @@ func (statement *Statement) buildUpdates(bean any,
if err != nil {
engine.logger.Error(err)
} else {
val = data
if col.SQLType.IsText() {
val = string(data)
} else {
val = data
}
}
goto APPEND
}
@ -331,7 +335,11 @@ func (statement *Statement) buildUpdates(bean any,
if err != nil {
engine.logger.Error(err)
} else {
val = data
if col.SQLType.IsText() {
val = string(data)
} else {
val = data
}
}
goto APPEND
}

@ -106,5 +106,9 @@ func NewEngine(driverName string, dataSourceName string) (*Engine, error) {
runtime.SetFinalizer(engine, close)
if dialect.DBType() == "spanner" {
engine.sequenceGenerator = newSequenceGenerator(db.DB)
}
return engine, nil
}

@ -0,0 +1,29 @@
//go:build enterprise || pro
package xorm
import (
"fmt"
"testing"
"cloud.google.com/go/spanner/spannertest"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
)
func TestBasicOperationsWithSpanner(t *testing.T) {
span, err := spannertest.NewServer("localhost:0")
require.NoError(t, err)
defer span.Close()
eng, err := NewEngine("spanner", fmt.Sprintf("%s/projects/test/instances/test/databases/test;usePlainText=true", span.Addr))
require.NoError(t, err)
require.NotNil(t, eng)
require.Equal(t, "spanner", eng.DriverName())
_, err = eng.Exec("CREATE TABLE test_struct (id int64, comment string(max), json string(max)) primary key (id)")
require.NoError(t, err)
// Currently broken because simple INSERT into spannertest doesn't work: https://github.com/googleapis/go-sql-spanner/issues/392
// testBasicOperations(t, eng)
}

@ -5,13 +5,47 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/components/simplejson"
)
func TestNewEngine(t *testing.T) {
t.Run("successfully create a new engine", func(t *testing.T) {
eng, err := NewEngine("sqlite3", "./test.db")
func TestBasicOperationsWithSqlite(t *testing.T) {
eng, err := NewEngine("sqlite3", ":memory:")
require.NoError(t, err)
require.NotNil(t, eng)
require.Equal(t, "sqlite3", eng.DriverName())
_, err = eng.Exec("CREATE TABLE test_struct (id int primary key, comment text, json text)")
require.NoError(t, err)
testBasicOperations(t, eng)
}
func testBasicOperations(t *testing.T, eng *Engine) {
t.Run("insert object", func(t *testing.T) {
obj := &TestStruct{Comment: "test comment"}
_, err := eng.Insert(obj)
require.NoError(t, err)
require.NotNil(t, eng)
require.Equal(t, "sqlite3", eng.DriverName())
require.Equal(t, int64(1), obj.Id)
})
t.Run("update object with json field", func(t *testing.T) {
sess := eng.NewSession()
defer sess.Close()
obj := &TestStruct{Comment: "new comment"}
_, err := sess.Insert(obj)
require.NoError(t, err)
require.NotZero(t, obj.Id)
obj.Json = simplejson.MustJson([]byte(`{"test": "test", "key": null}`))
_, err = sess.Update(obj)
require.NoError(t, err)
})
}
type TestStruct struct {
Id int64
Comment string
Json *simplejson.Json
}

@ -38,6 +38,7 @@ import {
setCurrentUser,
setChromeHeaderHeightHook,
setPluginLinksHook,
setFolderPicker,
setCorrelationsService,
setPluginFunctionsHook,
} from '@grafana/runtime';
@ -52,6 +53,7 @@ import getDefaultMonacoLanguages from '../lib/monaco-languages';
import { AppWrapper } from './AppWrapper';
import appEvents from './core/app_events';
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
import { LazyFolderPicker } from './core/components/NestedFolderPicker/LazyFolderPicker';
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
import { PluginPage } from './core/components/Page/PluginPage';
import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext';
@ -134,6 +136,7 @@ export class GrafanaApp {
setWeekStart(config.bootData.user.weekStart);
setPanelRenderer(PanelRenderer);
setPluginPage(PluginPage);
setFolderPicker(LazyFolderPicker);
setPanelDataErrorView(PanelDataErrorView);
setLocationSrv(locationService);
setCorrelationsService(new CorrelationsService());

@ -79,7 +79,9 @@ if (process.env.NODE_ENV === 'development') {
code: PSEUDO_LOCALE,
name: 'Pseudo-locale',
loader: {
grafana: () => import('../../../locales/pseudo-LOCALE/grafana.json'),
// Load the English locale as the pseudo-locale,
// as it will be post-processed by i18next-pseudo library
grafana: () => import('../../../locales/en-US/grafana.json'),
},
});
}

@ -28,8 +28,10 @@ import usersReducers from 'app/features/users/state/reducers';
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
import { alertingApi } from '../../features/alerting/unified/api/alertingApi';
import { folderAPI } from '../../features/folders/api';
import { iamApi } from '../../features/iam/api/api';
import { userPreferencesAPI } from '../../features/preferences/api';
import { provisioningAPI } from '../../features/provisioning/api';
import { cleanUpAction } from '../actions/cleanUp';
const rootReducers = {
@ -61,6 +63,8 @@ const rootReducers = {
[cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer,
[iamApi.reducerPath]: iamApi.reducer,
[userPreferencesAPI.reducerPath]: userPreferencesAPI.reducer,
[provisioningAPI.reducerPath]: provisioningAPI.reducer,
[folderAPI.reducerPath]: folderAPI.reducer,
};
const addedReducers = {};

@ -1,7 +1,11 @@
import { useFormContext } from 'react-hook-form';
import { Controller, useFormContext } from 'react-hook-form';
import { DataSourceInstanceSettings } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { Field, Input, Stack, Text } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { isCloudRecordingRuleByType, isGrafanaRecordingRuleByType, isRecordingRuleByType } from '../../utils/rules';
@ -21,9 +25,11 @@ const recordingRuleNameValidationPattern = (type: RuleFormType) => ({
*/
export const AlertRuleNameAndMetric = () => {
const {
control,
register,
watch,
formState: { errors },
setValue,
} = useFormContext<RuleFormValues>();
const ruleFormType = watch('type');
@ -76,6 +82,39 @@ export const AlertRuleNameAndMetric = () => {
/>
</Field>
)}
{isGrafanaRecordingRule && config.featureToggles.grafanaManagedRecordingRulesDatasources && (
<Field
id="target-data-source"
label={t('alerting.recording-rules.label-target-data-source', 'Target data source')}
description={t(
'alerting.recording-rules.description-target-data-source',
'The Prometheus data source to store the recording rule in'
)}
error={errors.targetDatasourceUid?.message}
invalid={!!errors.targetDatasourceUid?.message}
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<DataSourcePicker
{...field}
current={field.value}
noDefault
// Filter with `filter` prop instead of `type` prop to avoid showing the `-- Grafana --` data source
filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'}
onChange={(ds: DataSourceInstanceSettings) => {
setValue('targetDatasourceUid', ds.uid);
}}
/>
)}
name="targetDatasourceUid"
control={control}
rules={{
required: { value: true, message: 'Please select a data source' },
}}
/>
</Field>
)}
</Stack>
</RuleEditorSection>
);

@ -2,8 +2,10 @@ import { css } from '@emotion/css';
import { formatDistanceToNowStrict } from 'date-fns';
import { GrafanaTheme2, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
import { Icon, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
import { config } from '@grafana/runtime';
import { Icon, Link, Stack, Text, TextLink, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { useDatasource } from 'app/features/datasources/hooks';
import { CombinedRule } from 'app/types/unified-alerting';
import { usePendingPeriod } from '../../../hooks/rules/usePendingPeriod';
@ -53,6 +55,17 @@ export const Details = ({ rule }: DetailsProps) => {
determinedRuleType = RuleType.CloudRecordingRule;
}
const targetDatasourceUid = rulerRuleType.grafana.recordingRule(rule.rulerRule)
? rule.rulerRule.grafana_alert.record?.target_datasource_uid
: null;
const datasource = useDatasource(targetDatasourceUid);
const showTargetDatasource =
config.featureToggles.grafanaManagedRecordingRulesDatasources &&
targetDatasourceUid &&
targetDatasourceUid !== 'grafana';
const evaluationDuration = rule.promRule?.evaluationTime;
const evaluationTimestamp = rule.promRule?.lastEvaluation;
@ -101,6 +114,20 @@ export const Details = ({ rule }: DetailsProps) => {
)}
</>
)}
{showTargetDatasource && (
<DetailText
id="target-datasource-uid"
label={t('alerting.alert.target-datasource-uid', 'Target data source')}
value={
<Link href={`/connections/datasources/edit/${datasource?.uid}`}>
<Stack direction="row" gap={1}>
<img style={{ width: '16px' }} src={datasource?.meta.info.logos.small} alt="datasource logo" />
{datasource?.name}
</Stack>
</Link>
}
/>
)}
</DetailGroup>
<DetailGroup title={t('alerting.alert.evaluation', 'Evaluation')}>

@ -54,7 +54,7 @@ export interface RuleFormValues {
contactPoints?: AlertManagerManualRouting;
editorSettings?: SimplifiedEditor;
metric?: string;
targetDatasourceUid?: string;
// cortex / loki rules
namespace: string;
forTime: number;

@ -28,6 +28,7 @@ exports[`formValuesToRulerGrafanaRuleDTO should correctly convert rule form valu
"record": {
"from": "A",
"metric": "",
"target_datasource_uid": undefined,
},
"title": "",
},

@ -148,6 +148,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
manualRouting,
type,
metric,
targetDatasourceUid,
} = values;
if (!condition) {
throw new Error('You cannot create an alert rule without specifying the alert condition');
@ -196,6 +197,7 @@ export function formValuesToRulerGrafanaRuleDTO(values: RuleFormValues): Postabl
record: {
metric: metric ?? name,
from: condition,
target_datasource_uid: targetDatasourceUid,
},
},
annotations,
@ -284,6 +286,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
folder: { title: namespace, uid: ga.namespace_uid },
isPaused: ga.is_paused,
metric: ga.record?.metric,
targetDatasourceUid: ga.record?.target_datasource_uid,
};
} else if (rulerRuleType.grafana.rule(rule)) {
// grafana alerting rule

@ -15,11 +15,13 @@ import {
import { t } from 'app/core/internationalization';
import { isInCloneChain } from '../utils/clone';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardAddPane } from './DashboardAddPane';
import { DashboardOutline } from './DashboardOutline';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { NewObjectAddedToCanvasEvent } from './shared';
import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
@ -39,6 +41,18 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
onSelect: (item, multi) => this.selectElement(item, multi),
},
});
this.addActivationHandler(this.onActivate.bind(this));
}
private onActivate() {
const dashboard = getDashboardSceneFor(this);
this._subs.add(
dashboard.subscribeToEvent(NewObjectAddedToCanvasEvent, ({ payload }) => {
this.newObjectAddedToCanvas(payload);
})
);
}
public enableSelection() {
@ -134,7 +148,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
this.setState({ tab });
};
public newObjectAddedToCanvas(obj: SceneObject) {
private newObjectAddedToCanvas(obj: SceneObject) {
this.selectObject(obj, obj.state.key!, false);
if (this.state.tab !== 'configure') {
@ -173,13 +187,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
const editableElement = useEditableElement(selection, editPane);
const selectedObject = selection?.getFirstObject();
if (!editableElement) {
return null;
}
const { typeId } = editableElement.getEditableElementInfo();
if (isCollapsed) {
return (
<>
@ -196,7 +209,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{openOverlay && (
<Resizable className={cx(styles.fixed, styles.container)} defaultSize={{ height: '100%', width: '20vw' }}>
<ElementEditPane element={editableElement} key={typeId} />
<ElementEditPane element={editableElement} key={selectedObject?.state.key} />
</Resizable>
)}
</>
@ -224,7 +237,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
</TabsBar>
<div className={styles.tabContent}>
{tab === 'add' && <DashboardAddPane editPane={editPane} />}
{tab === 'configure' && <ElementEditPane element={editableElement} key={typeId} />}
{tab === 'configure' && <ElementEditPane element={editableElement} key={selectedObject?.state.key} />}
{tab === 'outline' && <DashboardOutline editPane={editPane} />}
</div>
</div>
@ -250,7 +263,7 @@ function getStyles(theme: GrafanaTheme2) {
}),
tabsbar: css({
padding: theme.spacing(0, 1),
margin: theme.spacing(0.5, 1),
margin: theme.spacing(0.5, 0),
}),
expandOptionsWrapper: css({
display: 'flex',

@ -15,7 +15,11 @@ export class DashboardEditableElement implements EditableDashboardElement {
public constructor(private dashboard: DashboardScene) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { typeId: 'dashboard', icon: 'apps', name: t('dashboard.edit-pane.elements.dashboard', 'Dashboard') };
return {
typeName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
icon: 'apps',
instanceName: this.dashboard.state.title,
};
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
@ -25,11 +29,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
const { body } = dashboard.useState();
const dashboardOptions = useMemo(() => {
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
title: t('dashboard.options.title', 'Dashboard options'),
id: 'dashboard-options',
isOpenable: false,
})
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'dashboard-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.options.title-option', 'Title'),

@ -65,7 +65,7 @@ function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneO
onPointerDown={(evt) => onSelect?.(evt)}
>
<Icon name={elementInfo.icon} />
<span>{elementInfo.name}</span>
<span>{elementInfo.instanceName}</span>
</button>
</Stack>
{expandable && isExpanded && (

@ -1,57 +1,86 @@
import { Dropdown, Button, IconButton, Menu, Stack, Icon } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
interface EditPaneHeaderProps {
title: string;
onDelete?: () => void;
onCopy?: () => void;
onDuplicate?: () => void;
element: EditableDashboardElement;
}
export const EditPaneHeader = ({ title, onDelete, onCopy, onDuplicate }: EditPaneHeaderProps) => {
const addCopyOrDuplicate = onCopy || onDuplicate;
export function EditPaneHeader({ element }: EditPaneHeaderProps) {
const elementInfo = element.getEditableElementInfo();
const styles = useStyles2(getStyles);
const onCopy = element.onCopy?.bind(element);
const onDuplicate = element.onDuplicate?.bind(element);
const onDelete = element.onDelete?.bind(element);
return (
<Stack justifyContent="space-between" alignItems="center" width="100%">
<span>{title}</span>
<Stack alignItems="center">
{addCopyOrDuplicate ? (
<Dropdown overlay={<MenuItems onCopy={onCopy} onDuplicate={onDuplicate} />}>
<div className={styles.wrapper}>
<Text variant="h5">{elementInfo.typeName}</Text>
<Stack direction="row" gap={1}>
{(onCopy || onDelete) && (
<Dropdown
overlay={
<Menu>
{onCopy ? (
<Menu.Item icon="copy" label={t('dashboard.layout.common.copy', 'Copy')} onClick={onCopy} />
) : null}
{onDuplicate ? (
<Menu.Item
icon="file-copy-alt"
label={t('dashboard.layout.common.duplicate', 'Duplicate')}
onClick={onDuplicate}
/>
) : null}
</Menu>
}
>
<Button
tooltip={t('dashboard.layout.common.copy-or-duplicate', 'Copy or Duplicate')}
tooltipPlacement="bottom"
variant="secondary"
fill="text"
size="md"
size="sm"
icon="copy"
>
<Icon name="copy" /> <Icon name="angle-down" />
<Icon name="angle-down" />
</Button>
</Dropdown>
) : null}
<IconButton
size="md"
variant="secondary"
onClick={onDelete}
name="trash-alt"
tooltip={t('dashboard.layout.common.delete', 'Delete')}
/>
)}
{onDelete && (
<ConfirmButton
onConfirm={onDelete}
confirmText="Confirm"
confirmVariant="destructive"
size="sm"
closeOnConfirm={true}
>
<Button
size="sm"
variant="destructive"
fill="outline"
icon="trash-alt"
tooltip={t('dashboard.layout.common.delete', 'Delete')}
/>
</ConfirmButton>
)}
</Stack>
</Stack>
</div>
);
};
type MenuItemsProps = {
onCopy?: () => void;
onDuplicate?: () => void;
};
}
const MenuItems = ({ onCopy, onDuplicate }: MenuItemsProps) => {
return (
<Menu>
{onCopy ? <Menu.Item label={t('dashboard.layout.common.copy', 'Copy')} onClick={onCopy} /> : null}
{onDuplicate ? (
<Menu.Item label={t('dashboard.layout.common.duplicate', 'Duplicate')} onClick={onDuplicate} />
) : null}
</Menu>
);
};
function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
};
}

@ -1,50 +1,20 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
import { Stack } from '@grafana/ui';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { EditPaneHeader } from './EditPaneHeader';
export interface Props {
element: EditableDashboardElement | MultiSelectedEditableDashboardElement;
element: EditableDashboardElement;
}
export function ElementEditPane({ element }: Props) {
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
const styles = useStyles2(getStyles);
const elementInfo = element.getEditableElementInfo();
return (
<Stack direction="column" gap={0}>
{element.renderActions && (
<OptionsPaneCategory
id="selected-item"
title={elementInfo.name}
isOpenDefault={true}
className={styles.noBorderTop}
renderTitle={element.renderTitle}
isOpenable={element.isOpenable}
>
<div className={styles.actionsBox}>{element.renderActions()}</div>
</OptionsPaneCategory>
)}
<EditPaneHeader element={element} />
{categories.map((cat) => cat.render())}
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
noBorderTop: css({
borderTop: 'none',
}),
actionsBox: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
paddingBottom: theme.spacing(1),
}),
};
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save