Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync

This commit is contained in:
Ryan McKinley committed 2025-02-20 14:21:45 +03:00
commit 562f4f2189
46 files changed
+746 -311

No files matched your search

+1 -4
View File
@@ -2868,10 +2868,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/features/alerting/unified/plugins/PluginOriginBadge.tsx:5381": [
[0, 0, 0, "\'@grafana/ui/src/components/Icon/utils\' import is restricted from being used by a pattern. Import from the public export instead.", "0"]
@@ -18,20 +18,26 @@ weight: 250
LBAC for data sources is available in private preview on Grafana Cloud for Prometheus created with basic authentication. Prometheus data sources for LBAC for data sources can only be created, provisioning is currently not available.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. Alternatively, you can replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for New Prometheus Data Source](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/configure-teamlbac-for-Prometheus/#task-1-lbac-configuration-for-new-Prometheus-data-source) and then add the LBAC configuration to the new data source.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. Alternatively, you can replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for New Prometheus Data Source](#task-1-lbac-configuration-for-new-prometheus-data-source) and then add the LBAC configuration to the new data source.
## Before you begin
To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeadersMimir` on your Grafana instance. Contact support to enable the feature toggle for you.
To be able to use LBAC for Prometheus data sources, you need to enable the feature toggle `teamHttpHeadersMimir` on your Grafana instance. Contact support to enable the feature toggle for you.
- Be sure that you have the permission setup to create a Prometheus tenant in Grafana Cloud
- Be sure that you have admin data source permissions for Grafana.
## Grafana Cloud
LBAC for data sources is available in private preview on Grafana Cloud for Prometheus created with basic authentication. Prometheus data sources for LBAC for data sources can only be created, provisioning is currently not available.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. We recommend that you replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for New Prometheus Data Source](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/configure-teamlbac-for-Prometheus/#task-1-lbac-configuration-for-new-Prometheus-data-source) and then add the LBAC configuration to the new data source.
### Permissions
We recommend that you remove all permissions for roles and teams that are not required to access the data source. This will help to ensure that only the required teams have access to the data source. The recommended permissions are `Admin` permission and only add the teams `Query` permissions that you want to add LBAC for data sources rules for.
## Task 1: LBAC Configuration for New Prometheus Data Source
## Task 1: LBAC Configuration for new Prometheus data source
1. Access Prometheus data sources details for your stack through grafana.com
1. Copy Prometheus details and create a CAP
@@ -53,11 +59,11 @@ For more information on how to setup LBAC for data sources rules for a Prometheu
LBAC for data sources is available in Grafana Enterprise for Prometheus created with basic authentication. Prometheus data sources for LBAC for data sources can only be created.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. Alternatively, you can replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for New Prometheus Data Source](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/configure-teamlbac-for-Prometheus/#task-1-lbac-configuration-for-new-Prometheus-data-source) and then add the LBAC configuration to the new data source.
You cannot configure LBAC rules for Grafana-provisioned data sources from the UI. Alternatively, you can replicate the setting of the provisioned data source in a new data source as described in [LBAC Configuration for new Prometheus data source](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/teamlbac/configure-teamlbac-for-prometheus/#task-1-lbac-configuration-for-new-prometheus-data-source) and then add the LBAC configuration to the new data source.
## Before you begin
To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you.
To be able to use LBAC for Prometheus data sources, you need to enable the feature toggle `teamHttpHeadersMimir` on your Grafana instance. Contact support to enable the feature toggle for you.
- Be sure that you have the permission setup to create a cluster in your Grafana
- Be sure that you have admin plugins permissions for Grafana.
@@ -79,7 +85,7 @@ We recommend that you remove all permissions for roles and teams that are not re
- Ensure the CAP does not include `labels` rules.
- Store the `token` to be used as password for authentication.
## Task 1: LBAC Configuration for New Prometheus Data Source
## Task 1: LBAC Configuration for new Prometheus data source
1. Create a new Prometheus data source
- In Grafana, proceed to add a new data source and select Prometheus as the type.
@@ -17,8 +17,6 @@ LBAC for data sources is available for LBAC-supported data sources created with
## Before you begin
To be able to use LBAC for data sources rules, you need to enable the feature toggle `teamHttpHeaders` on your Grafana instance. Contact support to enable the feature toggle for you.
- Be sure that you have the permission setup to create a Loki tenant in Grafana Cloud.
- Be sure that you have admin data source permissions for Grafana.
- Be sure that you have a team setup in Grafana.
+18 -18
View File
@@ -13,7 +13,7 @@ weight: 200
Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source version.
Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive datasource plugins]({{< relref "#enterprise-data-sources" >}}) and [additional features]({{< relref "#enterprise-features" >}}). You also get 24x7x365 support and training from the core Grafana team.
Building on everything you already know and love about Grafana open source, Grafana Enterprise includes [exclusive data source plugins](#enterprise-data-sources) and [additional features](#enterprise-features). You also get 24x7x365 support and training from the core Grafana team.
To learn more about Grafana Enterprise, refer to [our product page](/enterprise).
@@ -21,7 +21,7 @@ To learn more about Grafana Enterprise, refer to [our product page](/enterprise)
Many Grafana Enterprise features are also available in [Grafana Cloud](/docs/grafana-cloud) Free, Pro, and Advanced accounts. For details, refer to [Grafana Cloud pricing](/pricing/#featuresTable).
To migrate to Grafana Cloud, refer to [Migrate from Grafana Enterprise to Grafana Cloud](/docs/grafana-cloud/account-management/e2c-guide/).
To migrate to Grafana Cloud, refer to [Migrate from Grafana Enterprise to Grafana Cloud](/docs/grafana/<GRAFANA_VERSION>/administration/migration-guide/)
## Authentication
@@ -29,7 +29,7 @@ Grafana Enterprise includes integrations with more ways to authenticate your use
### Team sync
[Team sync]({{< relref "../setup-grafana/configure-security/configure-team-sync" >}}) allows you to set up synchronization between teams in Grafana and teams in your auth provider so that your users automatically end up in the right team.
[Team sync](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-team-sync/) allows you to set up synchronization between teams in Grafana and teams in your auth provider so that your users automatically end up in the right team.
Supported auth providers:
@@ -45,32 +45,32 @@ Supported auth providers:
### Enhanced LDAP integration
With [enhanced LDAP integration]({{< relref "../setup-grafana/configure-security/configure-authentication/enhanced-ldap" >}}), you can set up active LDAP synchronization.
With [enhanced LDAP integration](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/enhanced-ldap/), you can set up active LDAP synchronization.
### SAML authentication
[SAML authentication]({{< relref "../setup-grafana/configure-security/configure-authentication/saml" >}}) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML).
[SAML authentication](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/saml/) enables users to authenticate with single sign-on services that use Security Assertion Markup Language (SAML).
### Protected roles
With [protected roles]({{< relref "../setup-grafana/configure-security/configure-authentication#protected-roles" >}}), you can define user roles that are exempt from being converted from one authentication type to another when changing auth providers.
With [protected roles](/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-authentication/#protected-roles), you can define user roles that are exempt from being converted from one authentication type to another when changing auth providers.
## Enterprise features
Grafana Enterprise adds the following features:
- [Role-based access control]({{< relref "../administration/roles-and-permissions/access-control" >}}) to control access with role-based permissions.
- [Data source permissions]({{< relref "../administration/data-source-management#data-source-permissions" >}}) to restrict query access to specific teams and users.
- [Data source query and resource caching]({{< relref "../administration/data-source-management#query-and-resource-caching" >}}) to temporarily store query results in Grafana to reduce data source load and rate limiting.
- [Reporting]({{< relref "../dashboards/create-reports" >}}) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whomever you choose.
- [Export dashboard as PDF]({{< relref "../dashboards/share-dashboards-panels#export-a-dashboard-as-pdf" >}})
- [Custom branding]({{< relref "../setup-grafana/configure-grafana/configure-custom-branding" >}}) to customize Grafana from the brand and logo to the footer links.
- [Usage insights]({{< relref "../dashboards/assess-dashboard-usage" >}}) to understand how your Grafana instance is used.
- [Recorded queries]({{< relref "../administration/recorded-queries" >}}) to see trends over time for your data sources.
- [Vault integration]({{< relref "../setup-grafana/configure-security/configure-database-encryption#encrypting-your-database-with-a-key-from-a-key-management-service-kms" >}}) to manage your configuration or provisioning secrets with Vault.
- [Auditing]({{< relref "../setup-grafana/configure-security/audit-grafana" >}}) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements.
- [Request security]({{< relref "../setup-grafana/configure-security/configure-request-security" >}}) makes it possible to restrict outgoing requests from the Grafana server.
- [Settings updates at runtime]({{< relref "../setup-grafana/configure-grafana/settings-updates-at-runtime" >}}) allows you to update Grafana settings at runtime without requiring a restart.
- [Role-based access control](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/) to control access with role-based permissions.
- [Data source permissions](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#data-source-permissions) to restrict query access to specific teams and users.
- [Data source query and resource caching](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/data-source-management/#query-and-resource-caching) to temporarily store query results in Grafana to reduce data source load and rate limiting.
- [Reporting](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/create-reports/) to generate a PDF report from any dashboard and set up a schedule to have it emailed to whomever you choose.
- [Export dashboard as PDF](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/share-dashboards-panels/#export-a-dashboard-as-pdf)
- [Custom branding](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/configure-custom-branding/) to customize Grafana from the brand and logo to the footer links.
- [Usage insights](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/dashboards/assess-dashboard-usage/) to understand how your Grafana instance is used.
- [Recorded queries](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/administration/recorded-queries/) to see trends over time for your data sources.
- [Vault integration](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-database-encryption/#encrypting-your-database-with-a-key-from-a-key-management-service-kms) to manage your configuration or provisioning secrets with Vault.
- [Auditing](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/audit-grafana/) tracks important changes to your Grafana instance to help you manage and mitigate suspicious activity and meet compliance requirements.
- [Request security](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-security/configure-request-security/) makes it possible to restrict outgoing requests from the Grafana server.
- [Settings updates at runtime](https://grafana.com/docs/grafana/<GRAFANA_VERSION>/setup-grafana/configure-grafana/settings-updates-at-runtime/) allows you to update Grafana settings at runtime without requiring a restart.
## Enterprise data sources
@@ -107,7 +107,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `reportingRetries` | Enables rendering retries for the reporting feature |
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
| `teamHttpHeaders` | Enables LBAC for datasources to apply LogQL filtering of logs to the client requests for users in teams |
| `pdfTables` | Enables generating table data as PDF in reporting |
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
| `regressionTransformation` | Enables regression analysis transformation |
@@ -117,7 +117,6 @@ export interface FeatureToggles {
cloudWatchBatchQueries?: boolean;
recoveryThreshold?: boolean;
lokiStructuredMetadata?: boolean;
teamHttpHeaders?: boolean;
cachingOptimizeSerializationMemoryUsage?: boolean;
prometheusPromQAIL?: boolean;
prometheusCodeModeMetricNamesSearch?: boolean;
+3 -6
View File
@@ -20,7 +20,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@@ -394,11 +393,9 @@ func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Respons
// It's forbidden to update the rules from the datasource api.
// team HTTP headers update have to be done through `updateDatasourceLBACRules`
if hs.Features != nil && hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagTeamHttpHeaders) {
if cmd.JsonData != nil {
if _, ok := cmd.JsonData.CheckGet("teamHttpHeaders"); ok {
return response.Error(http.StatusForbidden, "Cannot create datasource with team HTTP headers, need to use updateDatasourceLBACRules API", nil)
}
if cmd.JsonData != nil {
if _, ok := cmd.JsonData.CheckGet("teamHttpHeaders"); ok {
return response.Error(http.StatusForbidden, "Cannot create datasource with team HTTP headers, need to use updateDatasourceLBACRules API", nil)
}
}
+1 -1
View File
@@ -229,7 +229,7 @@ func TestAddDataSourceTeamHTTPHeaders(t *testing.T) {
expectedDatasource: &datasources.DataSource{},
},
Cfg: setting.NewCfg(),
Features: featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders),
Features: featuremgmt.WithFeatures(),
accesscontrolService: actest.FakeService{},
AccessControl: actest.FakeAccessControl{
ExpectedEvaluate: true,
+1 -1
View File
@@ -249,7 +249,7 @@ func TestDataSourceQueryError(t *testing.T) {
expectedStatus: errutil.StatusInternal.HTTPStatus(),
expectedBody: body{
Message: "An error occurred within the plugin",
MessageId: "plugin.downstreamError",
MessageId: "plugin.requestFailureError",
StatusCode: 500,
},
},
+1 -1
View File
@@ -114,7 +114,7 @@ func ValidationFailed(msgID string, opts ...BaseOpt) Base {
// msgID should be structured as component.errorBrief, for example
//
// sqleng.connectionError
// plugin.downstreamError
// plugin.requestFailureError
func Internal(msgID string, opts ...BaseOpt) Base {
return NewBase(StatusInternal, msgID, opts...)
}
+67
View File
@@ -0,0 +1,67 @@
package utils
import "time"
// ManagerProperties is used to identify the manager of the resource.
type ManagerProperties struct {
// The kind of manager, which is responsible for managing the resource.
// Examples include "git", "terraform", "kubectl", etc.
Kind ManagerKind
// The identity of the manager, which refers to a specific instance of the manager.
// The format & the value depends on the manager kind.
Identity string
// AllowsEdits indicates whether the manager allows edits to the resource.
// If set to true, it means that other requesters can edit the resource.
AllowsEdits bool
// Suspended indicates whether the manager is suspended.
// If set to true, then the manager skip updates to the resource.
Suspended bool
}
// ManagerKind is the type of manager, which is responsible for managing the resource.
// It can be a user or a tool or a generic API client.
type ManagerKind string
// Known values for ManagerKind.
const (
ManagerKindUnknown ManagerKind = ""
ManagerKindRepo ManagerKind = "repo"
ManagerKindTerraform ManagerKind = "terraform"
ManagerKindKubectl ManagerKind = "kubectl"
)
// ParseManagerKindString parses a string into a ManagerKind.
// It returns the ManagerKind and a boolean indicating whether the string was a valid ManagerKind.
// For unknown values, it returns ManagerKindUnknown and false.
func ParseManagerKindString(v string) ManagerKind {
switch v {
case string(ManagerKindRepo):
return ManagerKindRepo
case string(ManagerKindTerraform):
return ManagerKindTerraform
case string(ManagerKindKubectl):
return ManagerKindKubectl
default:
return ManagerKindUnknown
}
}
// SourceProperties is used to identify the source of a provisioned resource.
// It is used by managers for reconciling data from a source to Grafana.
// Not all managers use these properties, some (like Terraform) don't have a concept of a source.
type SourceProperties struct {
// The path to the source of the resource.
// Can be a file path, a URL, etc.
Path string
// The checksum of the source of the resource.
// An example could be a git commit hash.
Checksum string
// The timestamp of the source of the resource.
// An example could be the file modification time.
Timestamp time.Time
}
+130
View File
@@ -46,6 +46,19 @@ const AnnoKeyRepoPath = "grafana.app/repoPath"
const AnnoKeyRepoHash = "grafana.app/repoHash"
const AnnoKeyRepoTimestamp = "grafana.app/repoTimestamp"
// Annotations used to store manager properties
const AnnoKeyManagerKind = "grafana.app/managedBy"
const AnnoKeyManagerIdentity = "grafana.app/managerId"
const AnnoKeyManagerAllowsEdits = "grafana.app/managerAllowsEdits"
const AnnoKeyManagerSuspended = "grafana.app/managerSuspended"
// Annotations used to store source properties
const AnnoKeySourcePath = "grafana.app/sourcePath"
const AnnoKeySourceHash = "grafana.app/sourceHash"
const AnnoKeySourceTimestamp = "grafana.app/sourceTimestamp"
// LabelKeyDeprecatedInternalID gives the deprecated internal ID of a resource
// Deprecated: will be removed in grafana 13
const LabelKeyDeprecatedInternalID = "grafana.app/deprecatedInternalID"
@@ -138,6 +151,22 @@ type GrafanaMetaAccessor interface {
// * title
// and return an empty string if nothing was found
FindTitle(defaultTitle string) string
// GetManagerProperties returns the identity of the tool,
// which is responsible for managing the resource.
//
// If the identity is not known, the second return value will be false.
GetManagerProperties() (ManagerProperties, bool)
// SetManagerProperties sets the identity of the tool,
// which is responsible for managing the resource.
SetManagerProperties(ManagerProperties)
// GetSourceProperties returns the source properties of the resource.
GetSourceProperties() (SourceProperties, bool)
// SetSourceProperties sets the source properties of the resource.
SetSourceProperties(SourceProperties)
}
var _ GrafanaMetaAccessor = (*grafanaMetaAccessor)(nil)
@@ -718,6 +747,107 @@ func (m *grafanaMetaAccessor) FindTitle(defaultTitle string) string {
return defaultTitle
}
func (m *grafanaMetaAccessor) GetManagerProperties() (ManagerProperties, bool) {
res := ManagerProperties{
Identity: "",
Kind: ManagerKindUnknown,
AllowsEdits: true,
Suspended: false,
}
annot := m.obj.GetAnnotations()
id, ok := annot[AnnoKeyManagerIdentity]
if !ok || id == "" {
// If the identity is not set, we should ignore the other annotations and return the default values.
//
// This is to prevent inadvertently marking resources as managed,
// since that can potentially block updates from other sources.
return res, false
}
res.Identity = id
if v, ok := annot[AnnoKeyManagerKind]; ok {
res.Kind = ParseManagerKindString(v)
}
if v, ok := annot[AnnoKeyManagerAllowsEdits]; ok {
res.AllowsEdits = v == "true"
}
if v, ok := annot[AnnoKeyManagerSuspended]; ok {
res.Suspended = v == "true"
}
return res, true
}
func (m *grafanaMetaAccessor) SetManagerProperties(v ManagerProperties) {
annot := m.obj.GetAnnotations()
if annot == nil {
annot = make(map[string]string, 4)
}
annot[AnnoKeyManagerIdentity] = v.Identity
annot[AnnoKeyManagerKind] = string(v.Kind)
annot[AnnoKeyManagerAllowsEdits] = strconv.FormatBool(v.AllowsEdits)
annot[AnnoKeyManagerSuspended] = strconv.FormatBool(v.Suspended)
m.obj.SetAnnotations(annot)
}
func (m *grafanaMetaAccessor) GetSourceProperties() (SourceProperties, bool) {
var (
res SourceProperties
found bool
)
annot := m.obj.GetAnnotations()
if annot == nil {
return res, false
}
if path, ok := annot[AnnoKeySourcePath]; ok && path != "" {
res.Path = path
found = true
}
if hash, ok := annot[AnnoKeySourceHash]; ok && hash != "" {
res.Checksum = hash
found = true
}
if timestamp, ok := annot[AnnoKeySourceTimestamp]; ok && timestamp != "" {
if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
res.Timestamp = t
found = true
}
}
return res, found
}
func (m *grafanaMetaAccessor) SetSourceProperties(v SourceProperties) {
annot := m.obj.GetAnnotations()
if annot == nil {
annot = make(map[string]string, 3)
}
if v.Path != "" {
annot[AnnoKeySourcePath] = v.Path
}
if v.Checksum != "" {
annot[AnnoKeySourceHash] = v.Checksum
}
if !v.Timestamp.IsZero() {
annot[AnnoKeySourceTimestamp] = v.Timestamp.Format(time.RFC3339)
}
m.obj.SetAnnotations(annot)
}
type BlobInfo struct {
UID string `json:"uid"`
Size int64 `json:"size,omitempty"`
+130
View File
@@ -3,6 +3,7 @@ package utils_test
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -433,6 +434,135 @@ func TestMetaAccessor(t *testing.T) {
require.Equal(t, obj2.Spec, spec)
require.NoError(t, err)
})
t.Run("ManagerProperties", func(t *testing.T) {
tests := []struct {
name string
setProperties *utils.ManagerProperties
wantProperties utils.ManagerProperties
wantOK bool
}{
{
name: "get default values",
wantProperties: utils.ManagerProperties{
Identity: "",
Kind: utils.ManagerKindUnknown,
AllowsEdits: true,
Suspended: false,
},
wantOK: false,
},
{
name: "set and get valid values",
setProperties: &utils.ManagerProperties{
Identity: "identity",
Kind: utils.ManagerKindTerraform,
AllowsEdits: false,
Suspended: false,
},
wantProperties: utils.ManagerProperties{
Identity: "identity",
Kind: utils.ManagerKindTerraform,
AllowsEdits: false,
Suspended: false,
},
wantOK: true,
},
{
name: "set empty identity returns default values",
setProperties: &utils.ManagerProperties{
Identity: "",
Kind: utils.ManagerKindRepo,
AllowsEdits: false,
Suspended: false,
},
wantProperties: utils.ManagerProperties{
Identity: "",
Kind: utils.ManagerKindUnknown,
AllowsEdits: true,
Suspended: false,
},
wantOK: false,
},
{
name: "invalid kind falls back to generic kind",
setProperties: &utils.ManagerProperties{
Identity: "identity",
Kind: utils.ManagerKind("invalid"),
AllowsEdits: false,
Suspended: true,
},
wantProperties: utils.ManagerProperties{
Identity: "identity",
Kind: utils.ManagerKindUnknown,
AllowsEdits: false,
Suspended: true,
},
wantOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := &TestResource2{}
meta, err := utils.MetaAccessor(res)
require.NoError(t, err)
if tt.setProperties != nil {
meta.SetManagerProperties(*tt.setProperties)
}
mp, ok := meta.GetManagerProperties()
require.Equal(t, tt.wantOK, ok)
require.Equal(t, tt.wantProperties, mp)
})
}
})
t.Run("SourceProperties", func(t *testing.T) {
tests := []struct {
name string
setProperties *utils.SourceProperties
wantProperties utils.SourceProperties
wantOK bool
}{
{
name: "get default values",
wantProperties: utils.SourceProperties{},
wantOK: false,
},
{
name: "set and get valid values",
setProperties: &utils.SourceProperties{
Path: "path",
Checksum: "hash",
Timestamp: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
},
wantProperties: utils.SourceProperties{
Path: "path",
Checksum: "hash",
Timestamp: time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC),
},
wantOK: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res := &TestResource2{}
meta, err := utils.MetaAccessor(res)
require.NoError(t, err)
if tt.setProperties != nil {
meta.SetSourceProperties(*tt.setProperties)
}
sp, ok := meta.GetSourceProperties()
require.Equal(t, tt.wantProperties, sp)
require.Equal(t, tt.wantOK, ok)
})
}
})
}
func asJSON(v any, pretty bool) string {
@@ -356,15 +356,14 @@ func handleGrpcStatusError(ctx context.Context, errorSource errstatus.Source, er
if innerErr != nil {
logger.Error("Could not set downstream error source", "error", innerErr)
}
return plugins.ErrPluginDownstreamErrorBase.Errorf("%v", err)
return plugins.ErrPluginRequestFailureErrorBase.Errorf("%v", err)
case backend.ErrorSourcePlugin:
errorSourceErr := backend.WithErrorSource(ctx, backend.ErrorSourcePlugin)
if errorSourceErr != nil {
logger.Error("Could not set plugin error source", "error", errorSourceErr)
}
// a downstream error is returned here as plugin errors are considered as downstream errors in the
// context of the Grafana server.
return plugins.ErrPluginDownstreamErrorBase.Errorf("%v", err)
// plugin request has failed after being sent from the Grafana server
return plugins.ErrPluginRequestFailureErrorBase.Errorf("%v", err)
}
return fmt.Errorf("%v: %w", "Failed to query data", err)
}
+3 -3
View File
@@ -24,9 +24,9 @@ var (
errutil.WithPublicMessage("Plugin health check failed"),
errutil.WithDownstream())
// ErrPluginDownstreamErrorBase error returned when a plugin request fails.
// Exposed as a base error to wrap it with plugin downstream errors.
ErrPluginDownstreamErrorBase = errutil.Internal("plugin.downstreamError",
// ErrPluginRequestFailureErrorBase error returned when a plugin request fails.
// Exposed as a base error to wrap it with plugin request errors.
ErrPluginRequestFailureErrorBase = errutil.Internal("plugin.requestFailureError",
errutil.WithPublicMessage("An error occurred within the plugin"),
errutil.WithDownstream())
+3 -3
View File
@@ -68,7 +68,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
return nil, plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: query data request canceled: %w", err)
}
return nil, plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to query data: %w", err)
return nil, plugins.ErrPluginRequestFailureErrorBase.Errorf("client: failed to query data: %w", err)
}
for refID, res := range resp.Responses {
@@ -127,7 +127,7 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq
return plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: call resource request canceled: %w", err)
}
return plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to call resources: %w", err)
return plugins.ErrPluginRequestFailureErrorBase.Errorf("client: failed to call resources: %w", err)
}
return nil
@@ -149,7 +149,7 @@ func (s *Service) CollectMetrics(ctx context.Context, req *backend.CollectMetric
return nil, plugins.ErrPluginRequestCanceledErrorBase.Errorf("client: collect metrics request canceled: %w", err)
}
return nil, plugins.ErrPluginDownstreamErrorBase.Errorf("client: failed to collect metrics: %w", err)
return nil, plugins.ErrPluginRequestFailureErrorBase.Errorf("client: failed to collect metrics: %w", err)
}
return resp, nil
+1 -1
View File
@@ -38,7 +38,7 @@ func TestQueryData(t *testing.T) {
},
{
err: errors.New("surprise surprise"),
expectedError: plugins.ErrPluginDownstreamErrorBase,
expectedError: plugins.ErrPluginRequestFailureErrorBase,
},
{
err: context.Canceled,
@@ -536,7 +536,7 @@ func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateD
// preserve existing lbac rules when updating datasource if we're not updating lbac rules
// TODO: Refactor to store lbac rules separate from a datasource
if s.features != nil && s.features.IsEnabled(ctx, featuremgmt.FlagTeamHttpHeaders) && !cmd.AllowLBACRuleUpdates {
if !cmd.AllowLBACRuleUpdates {
s.logger.Debug("Overriding LBAC rules with stored ones using updateLBACRules API",
"reason", "overriding_lbac_rules_from_datasource_api",
"datasource_id", dataSource.ID,
@@ -572,7 +572,7 @@ func TestService_UpdateDataSource(t *testing.T) {
t.Run("Should update LBAC rules when updating from API", func(t *testing.T) {
dsService := initDSService(t)
dsService.features = featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders)
dsService.features = featuremgmt.WithFeatures()
// Create a datasource with existing LBAC rules
existingRules := []interface{}{
@@ -629,7 +629,7 @@ func TestService_UpdateDataSource(t *testing.T) {
})
t.Run("Should preserve LBAC rules when not updating from API", func(t *testing.T) {
dsService := initDSService(t)
dsService.features = featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders)
dsService.features = featuremgmt.WithFeatures()
// Create a datasource with existing LBAC rules
existingRules := []interface{}{
map[string]interface{}{
@@ -680,7 +680,7 @@ func TestService_UpdateDataSource(t *testing.T) {
t.Run("Should not remove stored rules without AllowLBACRuleUpdates", func(t *testing.T) {
dsService := initDSService(t)
dsService.features = featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders)
dsService.features = featuremgmt.WithFeatures()
// Create a datasource with existing LBAC rules
existingRules := []interface{}{
@@ -720,7 +720,7 @@ func TestService_UpdateDataSource(t *testing.T) {
t.Run("Should not populate empty stored rules without AllowLBACRuleUpdates", func(t *testing.T) {
dsService := initDSService(t)
dsService.features = featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders)
dsService.features = featuremgmt.WithFeatures()
// Create a datasource with empty LBAC rules
jsonData := simplejson.New()
-9
View File
@@ -743,15 +743,6 @@ var (
Owner: grafanaObservabilityLogsSquad,
Expression: "true",
},
{
Name: "teamHttpHeaders",
Description: "Enables LBAC for datasources to apply LogQL filtering of logs to the client requests for users in teams",
Stage: FeatureStagePublicPreview,
FrontendOnly: false,
AllowSelfServe: true,
Owner: identityAccessTeam,
Expression: "true",
},
{
Name: "cachingOptimizeSerializationMemoryUsage",
Description: "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.",
-1
View File
@@ -98,7 +98,6 @@ queryServiceFromUI,experimental,@grafana/grafana-app-platform-squad,false,false,
cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false
recoveryThreshold,GA,@grafana/alerting-squad,false,true,false
lokiStructuredMetadata,GA,@grafana/observability-logs,false,false,false
teamHttpHeaders,preview,@grafana/identity-access-team,false,false,false
cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false
prometheusPromQAIL,experimental,@grafana/oss-big-tent,false,false,true
prometheusCodeModeMetricNamesSearch,experimental,@grafana/oss-big-tent,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
98 cloudWatchBatchQueries preview @grafana/aws-datasources false false false
99 recoveryThreshold GA @grafana/alerting-squad false true false
100 lokiStructuredMetadata GA @grafana/observability-logs false false false
teamHttpHeaders preview @grafana/identity-access-team false false false
101 cachingOptimizeSerializationMemoryUsage experimental @grafana/grafana-operator-experience-squad false false false
102 prometheusPromQAIL experimental @grafana/oss-big-tent false false true
103 prometheusCodeModeMetricNamesSearch experimental @grafana/oss-big-tent false false true
-4
View File
@@ -403,10 +403,6 @@ const (
// Enables the loki data source to request structured metadata from the Loki server
FlagLokiStructuredMetadata = "lokiStructuredMetadata"
// FlagTeamHttpHeaders
// Enables LBAC for datasources to apply LogQL filtering of logs to the client requests for users in teams
FlagTeamHttpHeaders = "teamHttpHeaders"
// FlagCachingOptimizeSerializationMemoryUsage
// If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.
FlagCachingOptimizeSerializationMemoryUsage = "cachingOptimizeSerializationMemoryUsage"
@@ -3846,6 +3846,7 @@
"name": "teamHttpHeaders",
"resourceVersion": "1738590709387",
"creationTimestamp": "2023-10-17T10:23:54Z",
"deletionTimestamp": "2025-02-19T10:48:55Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-02-03 13:51:49.3871 +0000 UTC"
}
+1 -1
View File
@@ -135,7 +135,7 @@ func (c *baseClientImpl) executeRequest(method, uriPath, uriQuery string, body [
c.logger.Debug("Sending request to Elasticsearch", "url", c.ds.URL)
u, err := url.Parse(c.ds.URL)
if err != nil {
return nil, err
return nil, backend.DownstreamError(fmt.Errorf("URL could not be parsed: %w", err))
}
u.Path = path.Join(u.Path, uriPath)
u.RawQuery = uriQuery
+10
View File
@@ -3,10 +3,13 @@ package elasticsearch
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -82,6 +85,13 @@ func (e *elasticsearchDataQuery) execute() (*backend.QueryDataResponse, error) {
if backend.IsDownstreamHTTPError(err) {
err = backend.DownstreamError(err)
}
var urlErr *url.Error
if errors.As(err, &urlErr) {
// Unsupported protocol scheme is a common error when the URL is not valid and should be treated as a downstream error
if urlErr.Err != nil && strings.HasPrefix(urlErr.Err.Error(), "unsupported protocol scheme") {
err = backend.DownstreamError(err)
}
}
response.Responses[e.dataQueries[0].RefID] = backend.ErrorResponseWithErrorSource(err)
return response, nil
}
+1 -86
View File
@@ -1,28 +1,19 @@
import { produce } from 'immer';
import { isEmpty, pick } from 'lodash';
import { Observable } from 'rxjs';
import {
DataQuery,
DataQueryRequest,
DataQueryResponse,
DataSourceApi,
DataSourceInstanceSettings,
DataSourceJsonData,
DataSourcePluginMeta,
DataSourceRef,
PluginExtensionLink,
PluginExtensionTypes,
ReducerID,
ScopedVars,
TestDataSourceResponse,
} from '@grafana/data';
import { DataSourceSrv, GetDataSourceListFilters, RuntimeDataSourceRegistration, config } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import { defaultDashboard } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/grafanaRuler';
import { ExpressionQuery, ExpressionQueryType, ReducerMode } from 'app/features/expressions/types';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
AlertManagerCortexConfig,
AlertState,
@@ -400,82 +391,6 @@ export const mockReceiversState = (partial: Partial<ReceiversState> = {}): Recei
};
};
class MockDataSourceApi extends DataSourceApi {
constructor(instanceSettings: DataSourceInstanceSettings<DataSourceJsonData>) {
super(instanceSettings);
}
query(request: DataQueryRequest<DataQuery>): Promise<DataQueryResponse> | Observable<DataQueryResponse> {
throw new Error('Method not implemented.');
}
testDatasource(): Promise<TestDataSourceResponse> {
throw new Error('Method not implemented.');
}
}
/** @deprecated use `setupDatasources` instead */
export class MockDataSourceSrv implements DataSourceSrv {
datasources: Record<string, DataSourceApi> = {};
// @ts-ignore
private settingsMapByName: Record<string, DataSourceInstanceSettings> = {};
private settingsMapByUid: Record<string, DataSourceInstanceSettings> = {};
private settingsMapById: Record<string, DataSourceInstanceSettings> = {};
// @ts-ignore
private templateSrv = {
getVariables: () => [],
replace: (name: any) => name,
};
defaultName = '';
constructor(datasources: Record<string, DataSourceInstanceSettings>) {
this.datasources = {};
this.settingsMapByName = Object.values(datasources).reduce<Record<string, DataSourceInstanceSettings>>(
(acc, ds) => {
acc[ds.name] = ds;
return acc;
},
{}
);
for (const dsSettings of Object.values(this.settingsMapByName)) {
this.settingsMapByUid[dsSettings.uid] = dsSettings;
this.settingsMapById[dsSettings.id] = dsSettings;
if (dsSettings.isDefault) {
this.defaultName = dsSettings.name;
}
this.datasources[dsSettings.uid] = new MockDataSourceApi(dsSettings);
}
}
registerRuntimeDataSource(entry: RuntimeDataSourceRegistration): void {}
get(name?: string | null | DataSourceRef, scopedVars?: ScopedVars): Promise<DataSourceApi> {
return DatasourceSrv.prototype.get.call(this, name, scopedVars);
//return Promise.reject(new Error('not implemented'));
}
/**
* Get a list of data sources
*/
getList(filters?: GetDataSourceListFilters): DataSourceInstanceSettings[] {
return DatasourceSrv.prototype.getList.call(this, filters);
}
/**
* Get settings and plugin metadata by name or uid
*/
getInstanceSettings(nameOrUid: string | null | undefined): DataSourceInstanceSettings | undefined {
return DatasourceSrv.prototype.getInstanceSettings.call(this, nameOrUid);
}
async loadDatasource(name: string): Promise<DataSourceApi<any, any>> {
return DatasourceSrv.prototype.loadDatasource.call(this, name);
}
reload() {}
}
export const mockGrafanaReceiver = (
type: string,
overrides: Partial<GrafanaManagedReceiverConfig> = {}
@@ -2,7 +2,7 @@ import { Factory } from 'fishery';
import { uniqueId } from 'lodash';
import { DataSourceInstanceSettings, PluginType } from '@grafana/data';
import { config, setDataSourceSrv } from '@grafana/runtime';
import { config } from '@grafana/runtime';
import { FolderDTO } from 'app/types';
import {
PromAlertingRuleDTO,
@@ -14,7 +14,7 @@ import {
RulerRuleGroupDTO,
} from 'app/types/unified-alerting-dto';
import { MockDataSourceSrv } from '../../mocks';
import { setupDataSources } from '../../testSetup/datasources';
import { DataSourceType } from '../../utils/datasource';
const prometheusRuleFactory = Factory.define<PromAlertingRuleDTO>(({ sequence }) => ({
@@ -62,7 +62,7 @@ const prometheusRuleGroupFactory = Factory.define<PromRuleGroupDTO>(({ sequence
const dataSourceFactory = Factory.define<DataSourceInstanceSettings>(({ sequence, params, afterBuild }) => {
afterBuild((dataSource) => {
config.datasources[dataSource.name] = dataSource;
setDataSourceSrv(new MockDataSourceSrv(config.datasources));
setupDataSources(...Object.values(config.datasources));
});
const uid = params.uid ?? `mock-ds-${sequence}`;
@@ -4,16 +4,17 @@ import { merge, uniqueId } from 'lodash';
import { openMenu } from 'react-select-event';
import { Observable } from 'rxjs';
import { TestProvider } from 'test/helpers/TestProvider';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { DataSourcePluginMeta, SupportedTransformationType } from '@grafana/data';
import { DataSourceInstanceSettings, SupportedTransformationType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { BackendSrv, setDataSourceSrv, BackendSrvRequest, reportInteraction } from '@grafana/runtime';
import { BackendSrv, BackendSrvRequest, reportInteraction, setBackendSrv, setAppEvents } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import { contextSrv } from 'app/core/services/context_srv';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { configureStore } from 'app/store/configureStore';
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
import { mockDataSource } from '../alerting/unified/mocks';
import CorrelationsPage from './CorrelationsPage';
import {
@@ -25,8 +26,11 @@ import {
} from './__mocks__/useCorrelations.mocks';
import { Correlation, CreateCorrelationParams, OmitUnion } from './types';
// Set app events up, otherwise plugin modules will fail to load
setAppEvents(appEvents);
const renderWithContext = async (
datasources: ConstructorParameters<typeof MockDataSourceSrv>[0] = {},
datasources: Record<string, DataSourceInstanceSettings>,
correlations: Correlation[] = []
) => {
const backend = {
@@ -86,17 +90,8 @@ const renderWithContext = async (
},
} as unknown as BackendSrv;
const grafanaContext = getGrafanaContextMock({ backend });
const dsServer = new MockDataSourceSrv(datasources);
dsServer.get = (name: string) => {
const dsApi = new MockDataSourceApi(name);
dsApi.components = {
QueryEditor: () => <>{name} query editor</>,
};
return Promise.resolve(dsApi);
};
setDataSourceSrv(dsServer);
setBackendSrv(backend);
setupDataSources(...Object.values(datasources));
const renderResult = render(
<TestProvider store={configureStore({})} grafanaContext={grafanaContext}>
@@ -210,10 +205,9 @@ describe('CorrelationsPage', () => {
name: 'loki',
readOnly: false,
jsonData: {},
access: 'direct',
type: 'datasource',
},
{ logs: true }
{ logs: true, module: 'core:plugin/loki' }
),
prometheus: mockDataSource(
{
@@ -221,10 +215,9 @@ describe('CorrelationsPage', () => {
name: 'prometheus',
readOnly: false,
jsonData: {},
access: 'direct',
type: 'datasource',
},
{ metrics: true }
{ metrics: true, module: 'core:plugin/prometheus' }
),
});
});
@@ -319,11 +312,11 @@ describe('CorrelationsPage', () => {
name: 'loki',
readOnly: false,
jsonData: {},
access: 'direct',
type: 'datasource',
},
{
logs: true,
module: 'core:plugin/loki',
}
),
prometheus: mockDataSource(
@@ -332,11 +325,11 @@ describe('CorrelationsPage', () => {
name: 'prometheus',
readOnly: false,
jsonData: {},
access: 'direct',
type: 'datasource',
},
{
metrics: true,
module: 'core:plugin/prometheus',
}
),
elastic: mockDataSource(
@@ -345,12 +338,12 @@ describe('CorrelationsPage', () => {
name: 'elastic',
readOnly: false,
jsonData: {},
access: 'direct',
type: 'datasource',
},
{
metrics: true,
logs: true,
module: 'core:plugin/elasticsearch',
}
),
},
@@ -589,6 +582,7 @@ describe('CorrelationsPage', () => {
},
{
logs: true,
module: 'core:plugin/loki',
}
),
},
@@ -675,15 +669,17 @@ describe('CorrelationsPage', () => {
beforeEach(async () => {
await renderWithContext(
{
loki: mockDataSource({
uid: 'loki',
name: 'loki',
readOnly: true,
jsonData: {},
access: 'direct',
meta: { info: { logos: {} } } as DataSourcePluginMeta,
type: 'datasource',
}),
loki: mockDataSource(
{
uid: 'loki',
name: 'loki',
readOnly: true,
jsonData: {},
access: 'direct',
type: 'datasource',
},
{ logs: true, module: 'core:plugin/loki' }
),
},
correlations
);
@@ -4,7 +4,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { MockDataSourceApi } from 'test/mocks/datasource_srv';
import { setDataSourceSrv } from '@grafana/runtime';
import { MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { QueryEditorField } from './QueryEditorField';
@@ -25,7 +25,7 @@ const renderWithContext = (
children: ReactNode,
getHandler: (name: string) => Promise<MockDataSourceApi> = defaultGetHandler
) => {
const dsServer = new MockDataSourceSrv({});
const dsServer = setupDataSources();
dsServer.get = getHandler;
setDataSourceSrv(dsServer);
@@ -2,16 +2,14 @@ import userEvent from '@testing-library/user-event';
import { render } from 'test/test-utils';
import { byTestId } from 'testing-library-selector';
import { DataSourceApi } from '@grafana/data';
import { PromOptions, PrometheusDatasource } from '@grafana/prometheus';
import { config, locationService, setDataSourceSrv, setPluginLinksHook } from '@grafana/runtime';
import { PromOptions } from '@grafana/prometheus';
import { config, locationService, setPluginLinksHook } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import * as ruler from 'app/features/alerting/unified/api/ruler';
import * as ruleActionButtons from 'app/features/alerting/unified/components/rules/RuleActionsButtons';
import * as alertingAbilities from 'app/features/alerting/unified/hooks/useAbilities';
import { mockAlertRuleApi, setupMswServer } from 'app/features/alerting/unified/mockApi';
import {
MockDataSourceSrv,
grantUserPermissions,
mockDataSource,
mockFolder,
@@ -20,13 +18,12 @@ import {
mockRulerAlertingRule,
mockRulerRuleGroup,
} from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import * as configDS from 'app/features/alerting/unified/utils/config';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { configureStore } from 'app/store/configureStore';
import { AccessControlAction, DashboardDataDTO } from 'app/types';
import { AlertQuery, PromRulesResponse } from 'app/types/unified-alerting-dto';
@@ -44,7 +41,6 @@ import { PanelDataAlertingTab, PanelDataAlertingTabRendered } from './PanelDataA
jest.mock('app/features/alerting/unified/api/prometheus');
jest.mock('app/features/alerting/unified/api/ruler');
jest.spyOn(configDS, 'getAllDataSources');
jest.spyOn(ruleActionButtons, 'matchesWidth').mockReturnValue(false);
jest.spyOn(ruler, 'rulerUrlBuilder');
jest.spyOn(alertingAbilities, 'useAlertRuleAbility');
@@ -55,22 +51,34 @@ setPluginLinksHook(() => ({
}));
const dataSources = {
prometheus: mockDataSource<PromOptions>({
name: 'Prometheus',
type: DataSourceType.Prometheus,
isDefault: false,
}),
default: mockDataSource<PromOptions>({
name: 'Default',
type: DataSourceType.Prometheus,
isDefault: true,
}),
prometheus: mockDataSource<PromOptions>(
{
name: 'Prometheus',
type: DataSourceType.Prometheus,
isDefault: false,
},
{ alerting: true, module: 'core:plugin/prometheus' }
),
default: mockDataSource<PromOptions>(
{
name: 'Default',
type: DataSourceType.Prometheus,
isDefault: true,
},
{ alerting: true, module: 'core:plugin/prometheus' }
),
prometheusMinInterval: mockDataSource<PromOptions>(
{
name: 'Prometheus Min Interval',
type: DataSourceType.Prometheus,
isDefault: false,
jsonData: { manageAlerts: true, timeInterval: '7m' },
},
{ alerting: true, module: 'core:plugin/prometheus' }
),
};
dataSources.prometheus.meta.alerting = true;
dataSources.default.meta.alerting = true;
const mocks = {
getAllDataSources: jest.mocked(configDS.getAllDataSources),
useAlertRuleAbilityMock: jest.mocked(alertingAbilities.useAlertRuleAbility),
rulerBuilderMock: jest.mocked(ruler.rulerUrlBuilder),
};
@@ -182,14 +190,8 @@ describe('PanelAlertTabContent', () => {
]);
jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue(mockFolder());
setupDataSources(...Object.values(dataSources));
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
const dsService = new MockDataSourceSrv(dataSources);
dsService.datasources[dataSources.prometheus.uid] = new PrometheusDatasource(
dataSources.prometheus
) as DataSourceApi;
dsService.datasources[dataSources.default.uid] = new PrometheusDatasource(dataSources.default) as DataSourceApi;
setDataSourceSrv(dsService);
mocks.rulerBuilderMock.mockReturnValue({
rules: () => ({ path: `api/ruler/${GRAFANA_RULES_SOURCE_NAME}/api/v1/rules` }),
namespace: () => ({ path: 'ruler' }),
@@ -281,11 +283,13 @@ describe('PanelAlertTabContent', () => {
});
it('Will take into account datasource minInterval', async () => {
(getDatasourceSrv() as unknown as MockDataSourceSrv).datasources[dataSources.prometheus.uid].interval = '7m';
dashboard.panels = [
new PanelModel({
...panel,
datasource: {
type: 'prometheus',
uid: dataSources.prometheusMinInterval.uid,
},
maxDataPoints: 100,
}),
];
@@ -298,7 +302,7 @@ describe('PanelAlertTabContent', () => {
refId: 'A',
datasource: {
type: 'prometheus',
uid: 'mock-ds-2',
uid: 'mock-ds-4',
},
interval: '',
intervalMs: 420000,
@@ -2,7 +2,6 @@ import { of } from 'rxjs';
import { DataQueryRequest, DataSourceApi, LoadingState, PanelPlugin } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setDataSourceSrv } from '@grafana/runtime';
import {
CancelActivationHandler,
CustomVariable,
@@ -14,7 +13,8 @@ import {
SceneVariableSet,
VizPanel,
} from '@grafana/scenes';
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import * as libAPI from 'app/features/library-panels/state/api';
@@ -61,13 +61,16 @@ jest.mock('@grafana/runtime', () => ({
}));
const dataSources = {
ds1: mockDataSource({
uid: 'ds1',
type: DataSourceType.Prometheus,
}),
ds1: mockDataSource(
{
uid: 'ds1',
type: DataSourceType.Prometheus,
},
{ module: 'core:plugin/prometheus' }
),
};
setDataSourceSrv(new MockDataSourceSrv(dataSources));
setupDataSources(...Object.values(dataSources));
let deactivate: CancelActivationHandler | undefined;
@@ -210,6 +210,22 @@ describe('DefaultGridLayoutManager', () => {
expect(gridRow.state.children.length).toBe(3);
});
it('Should clone the layout correctly', () => {
const { manager } = setup();
const clone = manager.cloneLayout('foo', true) as DefaultGridLayoutManager;
const panelA = findVizPanelByKey(clone, 'foo/grid-item-0/panel-0');
expect(panelA?.state.title).toBe('Panel A');
const panelB = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1');
expect(panelB?.state.title).toBe('Panel B');
const panelC = findVizPanelByKey(clone, 'foo/panel-2/grid-item-3/panel-3');
expect(panelC?.state.title).toBe('Panel C');
const panelD = findVizPanelByKey(clone, 'foo/panel-2/grid-item-4/panel-4');
expect(panelD?.state.title).toBe('Panel D');
});
});
});
@@ -0,0 +1,53 @@
import { SceneCSSGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { findVizPanelByKey } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { ResponsiveGridItem } from './ResponsiveGridItem';
import { ResponsiveGridLayoutManager } from './ResponsiveGridLayoutManager';
describe('ResponsiveGridLayoutManager', () => {
it('Should clone the layout', () => {
const { manager } = setup();
const clone = manager.cloneLayout('foo', true) as ResponsiveGridLayoutManager;
expect(clone).not.toBe(manager);
expect(clone.state.layout).not.toBe(manager.state.layout);
expect(clone.state.layout.state.children).not.toBe(manager.state.layout.state.children);
expect(clone.state.layout.state.children.length).toBe(manager.state.layout.state.children.length);
const panelA = findVizPanelByKey(clone, 'foo/grid-item-1/panel-1');
expect(panelA?.state.title).toBe('Panel A');
const panelB = findVizPanelByKey(clone, 'foo/grid-item-2/panel-2');
expect(panelB?.state.title).toBe('Panel B');
});
});
function setup() {
const gridItems = [
new ResponsiveGridItem({
key: 'grid-item-1',
body: new VizPanel({
title: 'Panel A',
key: 'panel-1',
pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
}),
}),
new ResponsiveGridItem({
key: 'grid-item-2',
body: new VizPanel({
title: 'Panel B',
key: 'panel-2',
pluginId: 'table',
}),
}),
];
const manager = new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ children: gridItems }) });
new DashboardScene({ body: manager });
return { manager };
}
@@ -2,8 +2,14 @@ import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectSt
import { t } from 'app/core/internationalization';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { joinCloneKeys } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { getDashboardSceneFor, getGridItemKeyForPanelId, getVizPanelKeyForPanelId } from '../../utils/utils';
import {
getDashboardSceneFor,
getGridItemKeyForPanelId,
getPanelIdForVizPanel,
getVizPanelKeyForPanelId,
} from '../../utils/utils';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
@@ -115,6 +121,28 @@ export class ResponsiveGridLayoutManager
return false;
}
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
return this.clone({
layout: this.state.layout.clone({
children: this.state.layout.state.children.map((gridItem) => {
if (gridItem instanceof ResponsiveGridItem) {
// Get the original panel ID from the gridItem's key
const panelId = getPanelIdForVizPanel(gridItem.state.body);
const gridItemKey = joinCloneKeys(ancestorKey, getGridItemKeyForPanelId(panelId));
return gridItem.clone({
key: gridItemKey,
body: gridItem.state.body.clone({
key: joinCloneKeys(gridItemKey, getVizPanelKeyForPanelId(panelId)),
}),
});
}
throw new Error('Unexpected child type');
}),
}),
});
}
public addNewRow() {
const shouldAddRow = this.hasVizPanels();
const rowsLayout = RowsLayoutManager.createFromLayout(this);
@@ -77,6 +77,10 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
return false;
}
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
throw new Error('Method not implemented.');
}
public addNewRow() {
this.setState({ rows: [...this.state.rows, new RowItem()] });
}
@@ -47,6 +47,10 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
return panels;
}
public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager {
throw new Error('Method not implemented.');
}
public hasVizPanels(): boolean {
for (const tab of this.state.tabs) {
if (tab.getLayout().hasVizPanels()) {
@@ -80,7 +80,7 @@ export interface DashboardLayoutManager<S = {}> extends SceneObject {
* @param ancestorKey
* @param isSource
*/
cloneLayout?(ancestorKey: string, isSource: boolean): DashboardLayoutManager;
cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager;
}
export interface LayoutManagerSerializer {
@@ -0,0 +1,100 @@
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
import { ResponsiveGridLayoutManager } from '../../scene/layout-responsive-grid/ResponsiveGridLayoutManager';
import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager';
import { RowsLayoutSerializer } from './RowsLayoutSerializer';
describe('deserialization', () => {
it('should deserialize rows layout with default grid child', () => {
const layout: DashboardV2Spec['layout'] = {
kind: 'RowsLayout',
spec: {
rows: [
{
kind: 'RowsLayoutRow',
spec: {
title: 'Row 1',
collapsed: false,
layout: { kind: 'GridLayout', spec: { items: [] } },
},
},
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(DefaultGridLayoutManager);
});
it('should deserialize rows layout with responsive grid child', () => {
const layout: DashboardV2Spec['layout'] = {
kind: 'RowsLayout',
spec: {
rows: [
{
kind: 'RowsLayoutRow',
spec: {
title: 'Row 1',
collapsed: false,
layout: { kind: 'ResponsiveGridLayout', spec: { row: '', col: '', items: [] } },
},
},
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(ResponsiveGridLayoutManager);
});
it('should handle multiple rows with different layouts', () => {
const layout: DashboardV2Spec['layout'] = {
kind: 'RowsLayout',
spec: {
rows: [
{
kind: 'RowsLayoutRow',
spec: {
title: 'Row 1',
collapsed: false,
layout: { kind: 'ResponsiveGridLayout', spec: { row: '', col: '', items: [] } },
},
},
{
kind: 'RowsLayoutRow',
spec: {
title: 'Row 2',
collapsed: true,
layout: { kind: 'GridLayout', spec: { items: [] } },
},
},
],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows).toHaveLength(2);
expect(deserialized.state.rows[0].state.layout).toBeInstanceOf(ResponsiveGridLayoutManager);
expect(deserialized.state.rows[1].state.layout).toBeInstanceOf(DefaultGridLayoutManager);
expect(deserialized.state.rows[0].state.isCollapsed).toBe(false);
expect(deserialized.state.rows[1].state.isCollapsed).toBe(true);
});
it('should handle 0 rows', () => {
const layout: DashboardV2Spec['layout'] = {
kind: 'RowsLayout',
spec: {
rows: [],
},
};
const serializer = new RowsLayoutSerializer();
const deserialized = serializer.deserialize(layout, {}, false);
expect(deserialized).toBeInstanceOf(RowsLayoutManager);
expect(deserialized.state.rows).toHaveLength(0);
});
});
@@ -1,9 +1,8 @@
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
import { render, screen, within } from 'test/test-utils';
import { locationService, setAngularLoader, setDataSourceSrv } from '@grafana/runtime';
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { locationService, setAngularLoader } from '@grafana/runtime';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { DashboardModel } from '../../state/DashboardModel';
import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures';
@@ -18,11 +17,7 @@ function setup(dashboard: DashboardModel, editIndex?: number) {
},
};
return render(
<TestProvider>
<AnnotationsSettings sectionNav={sectionNav} dashboard={dashboard} editIndex={editIndex} />
</TestProvider>
);
return render(<AnnotationsSettings sectionNav={sectionNav} dashboard={dashboard} editIndex={editIndex} />);
}
describe('AnnotationsSettings', () => {
@@ -56,7 +51,7 @@ describe('AnnotationsSettings', () => {
),
};
setDataSourceSrv(new MockDataSourceSrv(dataSources));
setupDataSources(...Object.values(dataSources));
const getTableBody = () => screen.getAllByRole('rowgroup')[1];
const getTableBodyRows = () => within(getTableBody()).getAllByRole('row');
@@ -149,7 +144,7 @@ describe('AnnotationsSettings', () => {
},
];
setup(dashboard);
const { user } = setup(dashboard);
// Check that we have sorting buttons
expect(within(getTableBodyRows()[0]).queryByRole('button', { name: 'Move up' })).not.toBeInTheDocument();
@@ -166,9 +161,9 @@ describe('AnnotationsSettings', () => {
expect(within(getTableBodyRows()[1]).queryByText(/annotation 2/i)).toBeInTheDocument();
expect(within(getTableBodyRows()[2]).queryByText(/annotation 3/i)).toBeInTheDocument();
await userEvent.click(within(getTableBody()).getAllByRole('button', { name: 'Move down' })[0]);
await userEvent.click(within(getTableBody()).getAllByRole('button', { name: 'Move down' })[1]);
await userEvent.click(within(getTableBody()).getAllByRole('button', { name: 'Move up' })[0]);
await user.click(within(getTableBody()).getAllByRole('button', { name: 'Move down' })[0]);
await user.click(within(getTableBody()).getAllByRole('button', { name: 'Move down' })[1]);
await user.click(within(getTableBody()).getAllByRole('button', { name: 'Move up' })[0]);
// Checking if it has changed the sorting accordingly
expect(within(getTableBodyRows()[0]).queryByText(/annotation 3/i)).toBeInTheDocument();
@@ -177,9 +172,9 @@ describe('AnnotationsSettings', () => {
});
test('Adding a new annotation', async () => {
setup(dashboard);
const { user } = setup(dashboard);
await userEvent.click(screen.getByRole('button', { name: 'Add annotation query' }));
await user.click(screen.getByRole('button', { name: 'Add annotation query' }));
expect(locationService.getSearchObject().editIndex).toBe('1');
expect(dashboard.annotations.list.length).toBe(2);
@@ -193,18 +188,18 @@ describe('AnnotationsSettings', () => {
enable: true,
});
setup(dashboard, 1);
const { user } = setup(dashboard, 1);
const nameInput = screen.getByRole('textbox', { name: /name/i });
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'My Prometheus Annotation');
await user.clear(nameInput);
await user.type(nameInput, 'My Prometheus Annotation');
await userEvent.click(screen.getByPlaceholderText(/testdata/i));
await user.click(screen.getByPlaceholderText(/testdata/i));
expect(await screen.findByText(/Prometheus/i)).toBeVisible();
expect(screen.queryAllByText(/testdata/i)).toHaveLength(1);
await userEvent.click(screen.getByText(/prometheus/i));
await user.click(screen.getByText(/prometheus/i));
expect(screen.getByRole('checkbox', { name: /hidden/i })).not.toBeChecked();
});
@@ -222,9 +217,9 @@ describe('AnnotationsSettings', () => {
type: 'dashboard',
},
];
setup(dashboard, 1); // Edit the not built-in annotations
const { user } = setup(dashboard, 1); // Edit the not built-in annotations
await userEvent.click(screen.getByRole('button', { name: 'Delete' }));
await user.click(screen.getByRole('button', { name: 'Delete' }));
expect(locationService.getSearchObject().editIndex).toBe(undefined);
expect(dashboard.annotations.list.length).toBe(1); // started with two
@@ -2,11 +2,11 @@ import { each, map } from 'lodash';
import { DataLinkBuiltInVars, MappingType, VariableHide } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setDataSourceSrv } from '@grafana/runtime';
import { FieldConfigSource } from '@grafana/schema';
import { config } from 'app/core/config';
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { DashboardModel } from '../state/DashboardModel';
@@ -41,7 +41,7 @@ const dataSources = {
}),
};
setDataSourceSrv(new MockDataSourceSrv(dataSources));
setupDataSources(...Object.values(dataSources));
describe('DashboardModel', () => {
describe('when creating dashboard with old schema', () => {
@@ -155,6 +155,7 @@ export const usePluginDetailsTabs = (
const navModel: NavModelItem = {
text: plugin?.name ?? '',
img: plugin?.info.logos.small,
url: pathname,
children: [
{
text: PluginTabLabels.OVERVIEW,
@@ -1,5 +1,5 @@
import { dateTime, QueryVariableModel, TimeRange, TypedVariableModel } from '@grafana/data';
import { setDataSourceSrv, VariableInterpolation } from '@grafana/runtime';
import { VariableInterpolation } from '@grafana/runtime';
import {
ConstantVariable,
CustomVariable,
@@ -13,10 +13,11 @@ import {
TestVariable,
} from '@grafana/scenes';
import { VariableFormatID } from '@grafana/schema';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput';
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
import { mockDataSource } from '../alerting/unified/mocks';
import { VariableAdapter, variableAdapters } from '../variables/adapters';
import { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
import { createQueryVariableAdapter } from '../variables/query/adapter';
@@ -218,16 +219,14 @@ describe('templateSrv', () => {
{ type: 'adhoc', name: 'test', datasource: { uid: 'oogle' }, filters: [1] },
{ type: 'adhoc', name: 'test2', datasource: { uid: '$ds' }, filters: [2] },
]);
setDataSourceSrv(
new MockDataSourceSrv({
oogle: mockDataSource({
name: 'oogle',
uid: 'oogle',
}),
logstash: mockDataSource({
name: 'logstash',
uid: 'logstash-id',
}),
setupDataSources(
mockDataSource({
name: 'oogle',
uid: 'oogle',
}),
mockDataSource({
name: 'logstash',
uid: 'logstash-id',
})
);
});
@@ -1,9 +1,10 @@
import { VariableHide } from '@grafana/data';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import { AdHocFiltersVariable, ConstantVariable, sceneGraph } from '@grafana/scenes';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import { MockDataSourceSrv, mockDataSource } from '../alerting/unified/mocks';
import { mockDataSource } from '../alerting/unified/mocks';
import { activateFullSceneTree } from '../dashboard-scene/utils/test-utils';
import { DataTrail } from './DataTrail';
@@ -26,12 +27,11 @@ jest.mock('./otel/api', () => ({
describe('DataTrail', () => {
beforeAll(() => {
jest.spyOn(DataTrail.prototype, 'checkDataSourceForOTelResources').mockImplementation(() => Promise.resolve());
setDataSourceSrv(
new MockDataSourceSrv({
prom: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
}),
setupDataSources(
mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
})
);
});
@@ -1,8 +1,9 @@
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import { AdHocFiltersVariable, sceneGraph, sceneUtils } from '@grafana/scenes';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import { MockDataSourceSrv, mockDataSource } from '../../alerting/unified/mocks';
import { mockDataSource } from '../../alerting/unified/mocks';
import { DataTrail } from '../DataTrail';
import { TRAIL_BOOKMARKS_KEY, RECENT_TRAILS_KEY, VAR_FILTERS } from '../shared';
@@ -31,13 +32,10 @@ describe('TrailStore', () => {
jest.useFakeTimers();
// Having the mock service set up is required for activating the loaded trails
setDataSourceSrv(
new MockDataSourceSrv({
prom: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
uid: 'ds',
}),
setupDataSources(
mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
})
);
});
+18 -14
View File
@@ -1,7 +1,8 @@
import { AdHocVariableFilter, MetricFindValue } from '@grafana/data';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import { AdHocFiltersVariable, ConstantVariable, sceneGraph } from '@grafana/scenes';
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
import { activateFullSceneTree } from 'app/features/dashboard-scene/utils/test-utils';
@@ -205,12 +206,10 @@ describe('updateOtelJoinWithGroupLeft', () => {
beforeEach(() => {
jest.spyOn(DataTrail.prototype, 'checkDataSourceForOTelResources').mockImplementation(() => Promise.resolve());
setDataSourceSrv(
new MockDataSourceSrv({
prom: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
}),
setupDataSources(
mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
})
);
trail = new DataTrail({
@@ -274,12 +273,11 @@ describe('getProdOrDefaultEnv', () => {
describe('util functions that rely on trail and variable setup', () => {
beforeAll(() => {
jest.spyOn(DataTrail.prototype, 'checkDataSourceForOTelResources').mockImplementation(() => Promise.resolve());
setDataSourceSrv(
new MockDataSourceSrv({
prom: mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
}),
setupDataSources(
mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
})
);
});
@@ -332,6 +330,12 @@ describe('util functions that rely on trail and variable setup', () => {
useOtelExperience: true,
nonPromotedOtelResources,
});
setupDataSources(
mockDataSource({
name: 'Prometheus',
type: DataSourceType.Prometheus,
})
);
locationService.push(preTrailUrl);
activateFullSceneTree(trail);
getOtelGroupLeftVar(trail).setState({ value: 'attribute1,attribute2' });
@@ -2,8 +2,8 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
import { setDataSourceSrv } from '@grafana/runtime';
import { mockDataSource, MockDataSourceSrv } from 'app/features/alerting/unified/mocks';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
@@ -30,11 +30,7 @@ jest.mock('app/core/config', () => ({
},
}));
setDataSourceSrv(
new MockDataSourceSrv({
test: mockDataSource({ isDefault: true }),
})
);
setupDataSources(mockDataSource({ isDefault: true }));
describe('DashboardQueryEditor', () => {
const mockOnChange = jest.fn();