diff --git a/package.json b/package.json
index e97932640cf..3f6ac927b08 100644
--- a/package.json
+++ b/package.json
@@ -253,7 +253,7 @@
"@emotion/css": "11.13.5",
"@emotion/react": "11.14.0",
"@fingerprintjs/fingerprintjs": "^3.4.2",
- "@floating-ui/react": "0.27.2",
+ "@floating-ui/react": "0.27.3",
"@formatjs/intl-durationformat": "^0.7.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/aws-sdk": "0.6.0",
diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts
index b84d081e84a..9f816dcd695 100644
--- a/packages/grafana-e2e-selectors/src/selectors/components.ts
+++ b/packages/grafana-e2e-selectors/src/selectors/components.ts
@@ -881,6 +881,7 @@ export const versionedComponents = {
Select: {
menu: {
'11.4.0': 'data-testid Select menu',
+ [MIN_GRAFANA_VERSION]: 'Select options menu',
},
option: {
'11.1.0': 'data-testid Select option',
@@ -1234,6 +1235,11 @@ export const versionedComponents = {
'11.2.0': 'data-testid entity-not-found',
},
},
+ Portal: {
+ container: {
+ '11.4.1': 'data-testid portal-container',
+ },
+ },
} satisfies VersionedSelectorGroup;
export type VersionedComponents = typeof versionedComponents;
diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json
index 64d7fd36360..14e060c5863 100644
--- a/packages/grafana-prometheus/package.json
+++ b/packages/grafana-prometheus/package.json
@@ -37,7 +37,7 @@
},
"dependencies": {
"@emotion/css": "11.13.5",
- "@floating-ui/react": "0.27.2",
+ "@floating-ui/react": "0.27.3",
"@grafana/data": "11.5.0-pre",
"@grafana/experimental": "2.1.6",
"@grafana/faro-web-sdk": "1.12.2",
diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json
index fd4d9de3172..d8fac079ad9 100644
--- a/packages/grafana-ui/package.json
+++ b/packages/grafana-ui/package.json
@@ -49,7 +49,7 @@
"@emotion/css": "11.13.5",
"@emotion/react": "11.14.0",
"@emotion/serialize": "1.3.3",
- "@floating-ui/react": "0.27.2",
+ "@floating-ui/react": "0.27.3",
"@grafana/data": "11.5.0-pre",
"@grafana/e2e-selectors": "11.5.0-pre",
"@grafana/faro-web-sdk": "^1.3.6",
diff --git a/packages/grafana-ui/src/components/Portal/Portal.tsx b/packages/grafana-ui/src/components/Portal/Portal.tsx
index 67cf3caf5d4..4cc232e331a 100644
--- a/packages/grafana-ui/src/components/Portal/Portal.tsx
+++ b/packages/grafana-ui/src/components/Portal/Portal.tsx
@@ -4,6 +4,7 @@ import * as React from 'react';
import ReactDOM from 'react-dom';
import { GrafanaTheme2 } from '@grafana/data';
+import { selectors } from '@grafana/e2e-selectors';
import { useStyles2, useTheme2 } from '../../themes';
@@ -51,7 +52,13 @@ export function getPortalContainer() {
/** @internal */
export function PortalContainer() {
const styles = useStyles2(getStyles);
- return
;
+ return (
+
+ );
}
const getStyles = (theme: GrafanaTheme2) => {
diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go
index decb1642657..4d89fb51a4a 100644
--- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go
+++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go
@@ -12,6 +12,9 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
+ "github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/folder/foldertest"
+ "github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/stats/statsimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
@@ -25,7 +28,7 @@ func TestMain(m *testing.M) {
func TestConcurrentUsersMetrics(t *testing.T) {
sqlStore, cfg := db.InitTestDBWithCfg(t)
- statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore)
+ statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore, &dashboards.FakeDashboardService{}, &foldertest.FakeService{}, &orgtest.FakeOrgService{})
s := createService(t, cfg, sqlStore, statsService)
createConcurrentTokens(t, sqlStore)
@@ -43,7 +46,7 @@ func TestConcurrentUsersMetrics(t *testing.T) {
func TestConcurrentUsersStats(t *testing.T) {
sqlStore, cfg := db.InitTestDBWithCfg(t)
- statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore)
+ statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore, &dashboards.FakeDashboardService{}, &foldertest.FakeService{}, &orgtest.FakeOrgService{})
s := createService(t, cfg, sqlStore, statsService)
createConcurrentTokens(t, sqlStore)
diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go
index d40ba27d45b..a87f2a41ca4 100644
--- a/pkg/registry/apis/dashboard/register.go
+++ b/pkg/registry/apis/dashboard/register.go
@@ -1,18 +1,26 @@
package dashboard
import (
+ "context"
+ "errors"
+ "fmt"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
+ "github.com/grafana/authlib/claims"
dashboardinternal "github.com/grafana/grafana/pkg/apis/dashboard"
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
dashboardv1alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v1alpha1"
dashboardv2alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v2alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
+ "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
@@ -22,13 +30,18 @@ var (
)
// This is used just so wire has something unique to return
-type DashboardsAPIBuilder struct{}
+type DashboardsAPIBuilder struct {
+ ProvisioningDashboardService dashboards.DashboardProvisioningService
+}
func RegisterAPIService(
features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
+ provisioningDashboardService dashboards.DashboardProvisioningService,
) *DashboardsAPIBuilder {
- builder := &DashboardsAPIBuilder{}
+ builder := &DashboardsAPIBuilder{
+ ProvisioningDashboardService: provisioningDashboardService,
+ }
apiregistration.RegisterAPI(builder)
return builder
}
@@ -63,3 +76,37 @@ func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefiniti
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
return oas, nil
}
+
+// Validate will prevent deletion of provisioned dashboards, unless the grace period is set to 0, indicating a force deletion
+func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
+ op := a.GetOperation()
+ if op == admission.Delete {
+ obj := a.GetOperationOptions()
+ deleteOptions, ok := obj.(*metav1.DeleteOptions)
+ if !ok {
+ return fmt.Errorf("expected v1.DeleteOptions")
+ }
+
+ if deleteOptions.GracePeriodSeconds == nil || *deleteOptions.GracePeriodSeconds != 0 {
+ nsInfo, err := claims.ParseNamespace(a.GetNamespace())
+ if err != nil {
+ return fmt.Errorf("%v: %w", "failed to parse namespace", err)
+ }
+
+ provisioningData, err := b.ProvisioningDashboardService.GetProvisionedDashboardDataByDashboardUID(ctx, nsInfo.OrgID, a.GetName())
+ if err != nil {
+ if errors.Is(err, dashboards.ErrProvisionedDashboardNotFound) {
+ return nil
+ }
+
+ return fmt.Errorf("%v: %w", "failed to check if dashboard is provisioned", err)
+ }
+
+ if provisioningData != nil {
+ return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/registry/apis/dashboard/register_test.go b/pkg/registry/apis/dashboard/register_test.go
new file mode 100644
index 00000000000..31f3c2d801a
--- /dev/null
+++ b/pkg/registry/apis/dashboard/register_test.go
@@ -0,0 +1,160 @@
+package dashboard
+
+import (
+ "context"
+ "fmt"
+ "testing"
+
+ common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
+ "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/user"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apiserver/pkg/admission"
+)
+
+func TestDashboardAPIBuilder_Validate(t *testing.T) {
+ oneInt64 := int64(1)
+ zeroInt64 := int64(0)
+ tests := []struct {
+ name string
+ inputObj *v0alpha1.Dashboard
+ deletionOptions metav1.DeleteOptions
+ dashboardResponse *dashboards.DashboardProvisioning
+ dashboardErrorResponse error
+ checkRan bool
+ expectedError bool
+ }{
+ {
+ name: "should return an error if data is found",
+ inputObj: &v0alpha1.Dashboard{
+ Spec: common.Unstructured{},
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Dashboard",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ },
+ deletionOptions: metav1.DeleteOptions{
+ GracePeriodSeconds: nil,
+ },
+ dashboardResponse: &dashboards.DashboardProvisioning{ID: 1},
+ dashboardErrorResponse: nil,
+ checkRan: true,
+ expectedError: true,
+ },
+ {
+ name: "should return an error if unable to check",
+ inputObj: &v0alpha1.Dashboard{
+ Spec: common.Unstructured{},
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Dashboard",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ },
+ deletionOptions: metav1.DeleteOptions{
+ GracePeriodSeconds: nil,
+ },
+ dashboardResponse: nil,
+ dashboardErrorResponse: fmt.Errorf("generic error"),
+ checkRan: true,
+ expectedError: true,
+ },
+ {
+ name: "should be okay if error is provisioned dashboard not found",
+ inputObj: &v0alpha1.Dashboard{
+ Spec: common.Unstructured{},
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Dashboard",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ },
+ deletionOptions: metav1.DeleteOptions{
+ GracePeriodSeconds: nil,
+ },
+ dashboardResponse: nil,
+ dashboardErrorResponse: dashboards.ErrProvisionedDashboardNotFound,
+ checkRan: true,
+ expectedError: false,
+ },
+ {
+ name: "Should still run the check for delete if grace period is not 0",
+ inputObj: &v0alpha1.Dashboard{
+ Spec: common.Unstructured{},
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Dashboard",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ },
+ deletionOptions: metav1.DeleteOptions{
+ GracePeriodSeconds: &oneInt64,
+ },
+ dashboardResponse: nil,
+ dashboardErrorResponse: nil,
+ checkRan: true,
+ expectedError: false,
+ },
+ {
+ name: "should not run the check for delete if grace period is set to 0",
+ inputObj: &v0alpha1.Dashboard{
+ Spec: common.Unstructured{},
+ TypeMeta: metav1.TypeMeta{
+ Kind: "Dashboard",
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test",
+ },
+ },
+ deletionOptions: metav1.DeleteOptions{
+ GracePeriodSeconds: &zeroInt64,
+ },
+ dashboardResponse: nil,
+ dashboardErrorResponse: nil,
+ checkRan: false,
+ expectedError: false,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fakeService := &dashboards.FakeDashboardProvisioning{}
+ fakeService.On("GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything).Return(tt.dashboardResponse, tt.dashboardErrorResponse).Once()
+ b := &DashboardsAPIBuilder{
+ ProvisioningDashboardService: fakeService,
+ }
+ err := b.Validate(context.Background(), admission.NewAttributesRecord(
+ tt.inputObj,
+ nil,
+ v0alpha1.DashboardResourceInfo.GroupVersionKind(),
+ "stacks-123",
+ tt.inputObj.Name,
+ v0alpha1.DashboardResourceInfo.GroupVersionResource(),
+ "",
+ admission.Operation("DELETE"),
+ &tt.deletionOptions,
+ true,
+ &user.SignedInUser{},
+ ), nil)
+
+ if tt.expectedError {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+
+ if tt.checkRan {
+ fakeService.AssertCalled(t, "GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything)
+ } else {
+ fakeService.AssertNotCalled(t, "GetProvisionedDashboardDataByDashboardUID", mock.Anything, mock.Anything, mock.Anything)
+ }
+ })
+ }
+}
diff --git a/pkg/registry/apis/dashboard/v0alpha1/register.go b/pkg/registry/apis/dashboard/v0alpha1/register.go
index 57bedd9b759..392d1420d93 100644
--- a/pkg/registry/apis/dashboard/v0alpha1/register.go
+++ b/pkg/registry/apis/dashboard/v0alpha1/register.go
@@ -44,6 +44,7 @@ var (
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
+ dashboard.DashboardsAPIBuilder
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
@@ -59,6 +60,7 @@ type DashboardsAPIBuilder struct {
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
+ provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
@@ -72,7 +74,9 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v0alpha1"),
-
+ DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
+ ProvisioningDashboardService: provisioningDashboardService,
+ },
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
diff --git a/pkg/registry/apis/dashboard/v1alpha1/register.go b/pkg/registry/apis/dashboard/v1alpha1/register.go
index 1c80817b032..d43ae3961bb 100644
--- a/pkg/registry/apis/dashboard/v1alpha1/register.go
+++ b/pkg/registry/apis/dashboard/v1alpha1/register.go
@@ -42,6 +42,7 @@ var (
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
+ dashboard.DashboardsAPIBuilder
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
@@ -56,6 +57,7 @@ type DashboardsAPIBuilder struct {
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
+ provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
@@ -69,7 +71,9 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v1alpha1"),
-
+ DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
+ ProvisioningDashboardService: provisioningDashboardService,
+ },
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
diff --git a/pkg/registry/apis/dashboard/v2alpha1/register.go b/pkg/registry/apis/dashboard/v2alpha1/register.go
index 90ce1ef1684..8ee4e8d1ca9 100644
--- a/pkg/registry/apis/dashboard/v2alpha1/register.go
+++ b/pkg/registry/apis/dashboard/v2alpha1/register.go
@@ -42,6 +42,7 @@ var (
// This is used just so wire has something unique to return
type DashboardsAPIBuilder struct {
+ dashboard.DashboardsAPIBuilder
dashboardService dashboards.DashboardService
features featuremgmt.FeatureToggles
@@ -56,6 +57,7 @@ type DashboardsAPIBuilder struct {
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
+ provisioningDashboardService dashboards.DashboardProvisioningService,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
@@ -70,6 +72,9 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
builder := &DashboardsAPIBuilder{
log: log.New("grafana-apiserver.dashboards.v2alpha1"),
+ DashboardsAPIBuilder: dashboard.DashboardsAPIBuilder{
+ ProvisioningDashboardService: provisioningDashboardService,
+ },
dashboardService: dashboardService,
features: features,
accessControl: accessControl,
diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go
index 7589eadfd2d..f6b8b5c2fe5 100644
--- a/pkg/services/authz/rbac/service.go
+++ b/pkg/services/authz/rbac/service.go
@@ -255,32 +255,42 @@ func (s *Service) getUserPermissions(ctx context.Context, ns claims.NamespaceInf
return cached.(map[string]bool), nil
}
- basicRoles, err := s.getUserBasicRole(ctx, ns, userIdentifiers)
+ res, err, _ := s.sf.Do(userPermKey+"_getUserPermissions", func() (interface{}, error) {
+ basicRoles, err := s.getUserBasicRole(ctx, ns, userIdentifiers)
+ if err != nil {
+ return nil, err
+ }
+
+ teamIDs, err := s.getUserTeams(ctx, ns, userIdentifiers)
+ if err != nil {
+ return nil, err
+ }
+
+ userPermQuery := store.PermissionsQuery{
+ UserID: userIdentifiers.ID,
+ Action: action,
+ TeamIDs: teamIDs,
+ Role: basicRoles.Role,
+ IsServerAdmin: basicRoles.IsAdmin,
+ }
+
+ permissions, err := s.store.GetUserPermissions(ctx, ns, userPermQuery)
+ if err != nil {
+ return nil, err
+ }
+ scopeMap := getScopeMap(permissions)
+
+ s.permCache.Set(userPermKey, scopeMap, 0)
+ span.SetAttributes(attribute.Int("num_permissions_fetched", len(permissions)))
+
+ return scopeMap, nil
+ })
+
if err != nil {
return nil, err
}
- teamIDs, err := s.getUserTeams(ctx, ns, userIdentifiers)
- if err != nil {
- return nil, err
- }
-
- userPermQuery := store.PermissionsQuery{
- UserID: userIdentifiers.ID,
- Action: action,
- TeamIDs: teamIDs,
- Role: basicRoles.Role,
- IsServerAdmin: basicRoles.IsAdmin,
- }
-
- permissions, err := s.store.GetUserPermissions(ctx, ns, userPermQuery)
- if err != nil {
- return nil, err
- }
- scopeMap := getScopeMap(permissions)
- s.permCache.Set(userPermKey, scopeMap, 0)
- span.SetAttributes(attribute.Int("num_permissions_fetched", len(permissions)))
- return scopeMap, nil
+ return res.(map[string]bool), nil
}
func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.NamespaceInfo, action string) (map[string]bool, error) {
@@ -292,13 +302,21 @@ func (s *Service) getAnonymousPermissions(ctx context.Context, ns claims.Namespa
return cached.(map[string]bool), nil
}
- permissions, err := s.store.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, Role: "Viewer"})
+ res, err, _ := s.sf.Do(anonPermKey+"_getAnonymousPermissions", func() (interface{}, error) {
+ permissions, err := s.store.GetUserPermissions(ctx, ns, store.PermissionsQuery{Action: action, Role: "Viewer"})
+ if err != nil {
+ return nil, err
+ }
+ scopeMap := getScopeMap(permissions)
+ s.permCache.Set(anonPermKey, scopeMap, 0)
+ return scopeMap, nil
+ })
+
if err != nil {
return nil, err
}
- scopeMap := getScopeMap(permissions)
- s.permCache.Set(anonPermKey, scopeMap, 0)
- return scopeMap, nil
+
+ return res.(map[string]bool), nil
}
func (s *Service) GetUserIdentifiers(ctx context.Context, ns claims.NamespaceInfo, userUID string) (*store.UserIdentifiers, error) {
diff --git a/pkg/services/authz/rbac/service_test.go b/pkg/services/authz/rbac/service_test.go
index 6aaa0c80eec..8d10c1693f2 100644
--- a/pkg/services/authz/rbac/service_test.go
+++ b/pkg/services/authz/rbac/service_test.go
@@ -368,6 +368,7 @@ func TestService_getUserPermissions(t *testing.T) {
tracer: tracing.NewNoopTracerService(),
idCache: localcache.New(longCacheTTL, longCleanupInterval),
permCache: cacheService,
+ sf: new(singleflight.Group),
basicRoleCache: localcache.New(longCacheTTL, longCleanupInterval),
teamCache: localcache.New(shortCacheTTL, shortCleanupInterval),
}
diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go
index 53a81e558dd..328481908de 100644
--- a/pkg/services/dashboards/service/dashboard_service.go
+++ b/pkg/services/dashboards/service/dashboard_service.go
@@ -492,16 +492,16 @@ func (dr *DashboardServiceImpl) SoftDeleteDashboard(ctx context.Context, orgID i
return fmt.Errorf("feature flag %s is not enabled", featuremgmt.FlagDashboardRestore)
}
+ if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
+ // deletes in unistore are soft deletes, so we can just delete in the same way
+ return dr.deleteDashboardThroughK8s(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgID, UID: dashboardUID}, true)
+ }
+
provisionedData, _ := dr.GetProvisionedDashboardDataByDashboardUID(ctx, orgID, dashboardUID)
if provisionedData != nil && provisionedData.ID != 0 {
return dashboards.ErrDashboardCannotDeleteProvisionedDashboard
}
- if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
- // deletes in unistore are soft deletes, so we can just delete in the same way
- return dr.deleteDashboardThroughK8s(ctx, &dashboards.DeleteDashboardCommand{OrgID: orgID, UID: dashboardUID})
- }
-
return dr.dashboardStore.SoftDeleteDashboard(ctx, orgID, dashboardUID)
}
@@ -533,6 +533,12 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
ctx, span := tracer.Start(ctx, "dashboards.service.deleteDashboard")
defer span.End()
+ cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID}
+
+ if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
+ return dr.deleteDashboardThroughK8s(ctx, cmd, validateProvisionedDashboard)
+ }
+
if validateProvisionedDashboard {
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dashboardId)
if err != nil {
@@ -544,12 +550,6 @@ func (dr *DashboardServiceImpl) deleteDashboard(ctx context.Context, dashboardId
}
}
- cmd := &dashboards.DeleteDashboardCommand{OrgID: orgId, ID: dashboardId, UID: dashboardUID}
-
- if dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesCliDashboards) {
- return dr.deleteDashboardThroughK8s(ctx, cmd)
- }
-
return dr.dashboardStore.DeleteDashboard(ctx, cmd)
}
@@ -1218,7 +1218,7 @@ func (dr *DashboardServiceImpl) deleteAllDashboardThroughK8s(ctx context.Context
return nil
}
-func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, cmd *dashboards.DeleteDashboardCommand) error {
+func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, cmd *dashboards.DeleteDashboardCommand, validateProvisionedDashboard bool) error {
// create a new context - prevents issues when the request stems from the k8s api itself
// otherwise the context goes through the handlers twice and causes issues
newCtx, cancel, err := dr.getK8sContext(ctx)
@@ -1245,7 +1245,16 @@ func (dr *DashboardServiceImpl) deleteDashboardThroughK8s(ctx context.Context, c
cmd.UID = result.UID
}
- err = client.Delete(newCtx, cmd.UID, v1.DeleteOptions{})
+ // use a grace period of 0 to indicate to skip the check of deleting provisioned dashboards
+ var gracePeriod *int64
+ if !validateProvisionedDashboard {
+ noGracePeriod := int64(0)
+ gracePeriod = &noGracePeriod
+ }
+
+ err = client.Delete(newCtx, cmd.UID, v1.DeleteOptions{
+ GracePeriodSeconds: gracePeriod,
+ })
if err != nil {
return err
}
diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go
index 5d8c2a67613..613ecf60fcb 100644
--- a/pkg/services/dashboards/service/dashboard_service_test.go
+++ b/pkg/services/dashboards/service/dashboard_service_test.go
@@ -656,19 +656,22 @@ func TestDeleteDashboard(t *testing.T) {
t.Run("Should use Kubernetes client if feature flags are enabled", func(t *testing.T) {
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once()
- fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil).Once()
- k8sResourceMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
+ k8sResourceMock.On("Delete", mock.Anything, mock.Anything, metav1.DeleteOptions{
+ GracePeriodSeconds: nil,
+ }, mock.Anything).Return(nil).Once()
err := service.DeleteDashboard(ctx, 1, "uid", 1)
require.NoError(t, err)
k8sClientMock.AssertExpectations(t)
})
- t.Run("If UID is not passed in, it should retrieve that first", func(t *testing.T) {
+ t.Run("When UID is not passed in for provisioned dashboards, should retrieve that first and set grace period to zero", func(t *testing.T) {
ctx, k8sClientMock, k8sResourceMock := setupK8sDashboardTests(service)
+ zeroInt64 := int64(0)
k8sClientMock.On("getClient", mock.Anything, int64(1)).Return(k8sResourceMock, true).Once()
- fakeStore.On("GetProvisionedDataByDashboardID", mock.Anything, mock.Anything).Return(nil, nil).Once()
- k8sResourceMock.On("Delete", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
+ k8sResourceMock.On("Delete", mock.Anything, mock.Anything, metav1.DeleteOptions{
+ GracePeriodSeconds: &zeroInt64,
+ }, mock.Anything).Return(nil).Once()
k8sClientMock.searcher.On("Search", mock.Anything).Return(&resource.ResourceSearchResponse{
Results: &resource.ResourceTable{
Columns: []*resource.ResourceTableColumnDefinition{
@@ -694,7 +697,7 @@ func TestDeleteDashboard(t *testing.T) {
},
TotalHits: 1,
}, nil)
- err := service.DeleteDashboard(ctx, 1, "", 1)
+ err := service.DeleteProvisionedDashboard(ctx, 1, 1)
require.NoError(t, err)
k8sClientMock.AssertExpectations(t)
k8sClientMock.searcher.AssertExpectations(t)
diff --git a/pkg/services/stats/statsimpl/stats.go b/pkg/services/stats/statsimpl/stats.go
index e2139c80b57..6b27a239813 100644
--- a/pkg/services/stats/statsimpl/stats.go
+++ b/pkg/services/stats/statsimpl/stats.go
@@ -6,7 +6,11 @@ import (
"strconv"
"time"
+ "github.com/grafana/authlib/claims"
+ "github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
+ "github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/libraryelements/model"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -17,13 +21,96 @@ import (
const activeUserTimeLimit = time.Hour * 24 * 30
const dailyActiveUserTimeLimit = time.Hour * 24
-func ProvideService(cfg *setting.Cfg, db db.DB) stats.Service {
- return &sqlStatsService{cfg: cfg, db: db}
+func ProvideService(cfg *setting.Cfg, db db.DB, dashSvc dashboards.DashboardService, folderSvc folder.Service, orgSvc org.Service) stats.Service {
+ return &sqlStatsService{
+ cfg: cfg,
+ db: db,
+ folderSvc: folderSvc,
+ dashSvc: dashSvc,
+ orgSvc: orgSvc,
+ }
}
type sqlStatsService struct {
- db db.DB
- cfg *setting.Cfg
+ db db.DB
+ cfg *setting.Cfg
+ dashSvc dashboards.DashboardService
+ folderSvc folder.Service
+ orgSvc org.Service
+}
+
+type dashboardStats struct {
+ count int
+ bytesTotal int
+ bytesMax int
+}
+
+func (ss *sqlStatsService) collectDashboardStats(ctx context.Context, orgs []*org.OrgDTO, calculateByteSize bool) (dashboardStats, error) {
+ stats := dashboardStats{
+ count: 0,
+ bytesTotal: 0,
+ bytesMax: 0,
+ }
+
+ for _, org := range orgs {
+ ctx = identity.WithRequester(ctx, getStatsRequester(org.ID))
+ dashs, err := ss.dashSvc.GetAllDashboardsByOrgId(ctx, org.ID)
+ if err != nil {
+ return stats, err
+ }
+ stats.count += len(dashs)
+
+ // only calculate bytes if needed
+ if calculateByteSize {
+ for _, dash := range dashs {
+ b, err := dash.Data.ToDB()
+ if err != nil {
+ return stats, err
+ }
+ stats.bytesTotal += len(b)
+
+ if len(b) > stats.bytesMax {
+ stats.bytesMax = len(b)
+ }
+ }
+ }
+ }
+
+ return stats, nil
+}
+
+func (ss *sqlStatsService) getTagCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
+ total := 0
+ for _, org := range orgs {
+ ctx = identity.WithRequester(ctx, getStatsRequester(org.ID))
+ tags, err := ss.dashSvc.GetDashboardTags(ctx, &dashboards.GetDashboardTagsQuery{
+ OrgID: org.ID,
+ })
+ if err != nil {
+ return 0, err
+ }
+ total += len(tags)
+ }
+
+ return int64(total), nil
+}
+
+func (ss *sqlStatsService) getFolderCount(ctx context.Context, orgs []*org.OrgDTO) (int64, error) {
+ total := 0
+ for _, org := range orgs {
+ backgroundUser := getStatsRequester(org.ID)
+ ctx = identity.WithRequester(ctx, backgroundUser)
+ folders, err := ss.folderSvc.GetFolders(ctx, folder.GetFoldersQuery{
+ OrgID: org.ID,
+ SignedInUser: backgroundUser,
+ })
+ if err != nil {
+ return 0, err
+ }
+
+ total += len(folders)
+ }
+ return int64(total), nil
}
func (ss *sqlStatsService) GetAlertNotifiersUsageStats(ctx context.Context, query *stats.GetAlertNotifierUsageStatsQuery) (result []*stats.NotifierUsageStats, err error) {
@@ -67,7 +154,6 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS
sb := &db.SQLBuilder{}
sb.Write("SELECT ")
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + `) AS users,`)
- sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("org") + `) AS orgs,`)
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_source") + `) AS datasources,`)
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("star") + `) AS stars,`)
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("playlist") + `) AS playlists,`)
@@ -87,11 +173,6 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS
sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE `+
notServiceAccount(dialect)+` AND last_seen_at > ?) AS monthly_active_users,`, monthlyActiveUserDeadlineDate)
- sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboards,`, dialect.BooleanStr(false))
- sb.Write(`(SELECT SUM(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_total,`, dialect.BooleanStr(false))
- sb.Write(`(SELECT MAX(LENGTH(data)) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboard_bytes_max,`, dialect.BooleanStr(false))
- sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS folders,`, dialect.BooleanStr(true))
-
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`)
sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_version") + `) AS dashboard_versions,`)
@@ -124,6 +205,30 @@ func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *stats.GetS
return nil
})
+ if err != nil {
+ return result, err
+ }
+
+ orgs, err := ss.orgSvc.Search(ctx, &org.SearchOrgsQuery{})
+ if err != nil {
+ return result, err
+ }
+ result.Orgs = int64(len(orgs))
+
+ // for services in unified storage, get the stats through the service rather than the db directly
+ dashStats, err := ss.collectDashboardStats(ctx, orgs, true)
+ if err != nil {
+ return result, err
+ }
+ result.DashboardBytesMax = int64(dashStats.bytesMax)
+ result.DashboardBytesTotal = int64(dashStats.bytesTotal)
+ result.Dashboards = int64(dashStats.count)
+
+ folderCount, err := ss.getFolderCount(ctx, orgs)
+ if err != nil {
+ return result, err
+ }
+ result.Folders = folderCount
return result, err
}
@@ -161,22 +266,10 @@ func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *stats.GetAd
}
var rawSQL = `SELECT
- (
- SELECT COUNT(*)
- FROM ` + dialect.Quote("org") + `
- ) AS orgs,
- (
- SELECT COUNT(*)
- FROM ` + dialect.Quote("dashboard") + `WHERE is_folder=` + dialect.BooleanStr(false) + `
- ) AS dashboards,
(
SELECT COUNT(*)
FROM ` + dialect.Quote("dashboard_snapshot") + `
) AS snapshots,
- (
- SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
- FROM ` + dialect.Quote("dashboard_tag") + `
- ) AS tags,
(
SELECT COUNT(*)
FROM ` + dialect.Quote("data_source") + `
@@ -225,6 +318,29 @@ func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *stats.GetAd
result = &stats
return nil
})
+ if err != nil {
+ return result, err
+ }
+
+ orgs, err := ss.orgSvc.Search(ctx, &org.SearchOrgsQuery{})
+ if err != nil {
+ return result, err
+ }
+ result.Orgs = int64(len(orgs))
+
+ // for services in unified storage, get the stats through the service rather than the db directly
+ dashStats, err := ss.collectDashboardStats(ctx, orgs, false)
+ if err != nil {
+ return result, err
+ }
+ result.Dashboards = int64(dashStats.count)
+
+ tagCount, err := ss.getTagCount(ctx, orgs)
+ if err != nil {
+ return result, err
+ }
+ result.Tags = tagCount
+
return result, err
}
@@ -343,3 +459,20 @@ func addToStats(base stats.UserStats, role org.RoleType, count int64) stats.User
return base
}
+
+func getStatsRequester(orgId int64) *identity.StaticRequester {
+ return &identity.StaticRequester{
+ Type: claims.TypeServiceAccount,
+ UserID: 1,
+ OrgID: orgId,
+ Name: "admin",
+ Login: "admin",
+ OrgRole: identity.RoleAdmin,
+ IsGrafanaAdmin: true,
+ Permissions: map[int64]map[string][]string{
+ orgId: {
+ "*": {"*"},
+ },
+ },
+ }
+}
diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go
index e07c059bf9d..3b3951ed27b 100644
--- a/pkg/services/stats/statsimpl/stats_test.go
+++ b/pkg/services/stats/statsimpl/stats_test.go
@@ -6,13 +6,18 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/correlations/correlationstest"
+ "github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/folder"
+ "github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
@@ -33,8 +38,30 @@ func TestIntegrationStatsDataAccess(t *testing.T) {
t.Skip("skipping integration test")
}
db, cfg := db.InitTestDBWithCfg(t)
- statsService := &sqlStatsService{db: db}
- populateDB(t, db, cfg)
+ orgSvc := populateDB(t, db, cfg)
+ dashSvc := &dashboards.FakeDashboardService{}
+ emptyJson := simplejson.New()
+ emptyJsonBytes, err := emptyJson.ToDB()
+ require.NoError(t, err)
+ largerJson := simplejson.NewFromAny(map[string]string{"key": "value"})
+ largerJsonBytes, err := largerJson.ToDB()
+ require.NoError(t, err)
+ dashSvc.On("GetAllDashboardsByOrgId", mock.Anything, int64(1)).Return([]*dashboards.Dashboard{{Data: largerJson}, {Data: emptyJson}}, nil)
+ dashSvc.On("GetAllDashboardsByOrgId", mock.Anything, int64(2)).Return([]*dashboards.Dashboard{}, nil)
+ dashSvc.On("GetAllDashboardsByOrgId", mock.Anything, int64(3)).Return([]*dashboards.Dashboard{}, nil)
+ dashSvc.On("GetDashboardTags", mock.Anything, &dashboards.GetDashboardTagsQuery{OrgID: 1}).Return([]*dashboards.DashboardTagCloudItem{{Term: "test"}}, nil)
+ dashSvc.On("GetDashboardTags", mock.Anything, &dashboards.GetDashboardTagsQuery{OrgID: 2}).Return([]*dashboards.DashboardTagCloudItem{}, nil)
+ dashSvc.On("GetDashboardTags", mock.Anything, &dashboards.GetDashboardTagsQuery{OrgID: 3}).Return([]*dashboards.DashboardTagCloudItem{}, nil)
+
+ folderService := &foldertest.FakeService{}
+ folderService.ExpectedFolders = []*folder.Folder{{ID: 1}, {ID: 2}, {ID: 3}}
+
+ statsService := &sqlStatsService{
+ db: db,
+ dashSvc: dashSvc,
+ orgSvc: orgSvc,
+ folderSvc: folderService,
+ }
t.Run("Get system stats should not results in error", func(t *testing.T) {
query := stats.GetSystemStatsQuery{}
@@ -48,6 +75,11 @@ func TestIntegrationStatsDataAccess(t *testing.T) {
assert.Equal(t, int64(0), result.LibraryVariables)
assert.Equal(t, int64(0), result.APIKeys)
assert.Equal(t, int64(2), result.Correlations)
+ assert.Equal(t, int64(3), result.Orgs)
+ assert.Equal(t, int64(2), result.Dashboards)
+ assert.Equal(t, int64(9), result.Folders) // will return 3 folders for each org
+ assert.Equal(t, int64(len(largerJsonBytes)+len(emptyJsonBytes)), result.DashboardBytesTotal)
+ assert.Equal(t, int64(len(largerJsonBytes)), result.DashboardBytesMax)
assert.NotNil(t, result.DatabaseCreatedTime)
assert.Equal(t, db.GetDialect().DriverName(), result.DatabaseDriver)
})
@@ -78,12 +110,15 @@ func TestIntegrationStatsDataAccess(t *testing.T) {
t.Run("Get admin stats should not result in error", func(t *testing.T) {
query := stats.GetAdminStatsQuery{}
- _, err := statsService.GetAdminStats(context.Background(), &query)
+ stats, err := statsService.GetAdminStats(context.Background(), &query)
assert.NoError(t, err)
+ assert.Equal(t, int64(1), stats.Tags)
+ assert.Equal(t, int64(2), stats.Dashboards)
+ assert.Equal(t, int64(3), stats.Orgs)
})
}
-func populateDB(t *testing.T, db db.DB, cfg *setting.Cfg) {
+func populateDB(t *testing.T, db db.DB, cfg *setting.Cfg) org.Service {
t.Helper()
orgService, _ := orgimpl.ProvideService(db, cfg, quotatest.New(false, nil))
@@ -151,16 +186,6 @@ func populateDB(t *testing.T, db db.DB, cfg *setting.Cfg) {
}
err = orgService.AddOrgUser(context.Background(), cmd)
require.NoError(t, err)
-}
-func TestIntegration_GetAdminStats(t *testing.T) {
- if testing.Short() {
- t.Skip("skipping integration test")
- }
- db, cfg := db.InitTestDBWithCfg(t)
- statsService := ProvideService(cfg, db)
-
- query := stats.GetAdminStatsQuery{}
- _, err := statsService.GetAdminStats(context.Background(), &query)
- require.NoError(t, err)
+ return orgService
}
diff --git a/pkg/tests/apis/dashboard/dashboards_test.go b/pkg/tests/apis/dashboard/dashboards_test.go
index e9ca8082f9e..cedc1142213 100644
--- a/pkg/tests/apis/dashboard/dashboards_test.go
+++ b/pkg/tests/apis/dashboard/dashboards_test.go
@@ -83,8 +83,11 @@ func runDashboardTest(t *testing.T, helper *apis.K8sTestHelper) {
require.Equal(t, obj.GetUID(), updated.GetUID())
require.Less(t, obj.GetResourceVersion(), updated.GetResourceVersion())
- // Delete the object
- err = client.Resource.Delete(ctx, created, metav1.DeleteOptions{})
+ // Delete the object, skipping the provisioned dashboard check
+ zeroInt64 := int64(0)
+ err = client.Resource.Delete(ctx, created, metav1.DeleteOptions{
+ GracePeriodSeconds: &zeroInt64,
+ })
require.NoError(t, err)
// Now it is not in the list
diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx
new file mode 100644
index 00000000000..655b33b2e7e
--- /dev/null
+++ b/public/app/features/alerting/unified/RuleEditorGrafanaRecordingRules.test.tsx
@@ -0,0 +1,119 @@
+import { UserEvent } from '@testing-library/user-event';
+import * as React from 'react';
+import { renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
+import { clickSelectOption } from 'test/helpers/selectOptionInTest';
+import { screen } from 'test/test-utils';
+import { byRole } from 'testing-library-selector';
+
+import { FeatureToggles } from '@grafana/data';
+import { contextSrv } from 'app/core/services/context_srv';
+import { setupMswServer } from 'app/features/alerting/unified/mockApi';
+import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
+import { AccessControlAction } from 'app/types';
+
+import { grantUserPermissions, mockDataSource } from './mocks';
+import { grafanaRulerGroup } from './mocks/grafanaRulerApi';
+import { captureRequests, serializeRequests } from './mocks/server/events';
+import { FOLDER_TITLE_HAPPY_PATH } from './mocks/server/handlers/search';
+import { testWithFeatureToggles } from './test/test-utils';
+import { setupDataSources } from './testSetup/datasources';
+
+jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
+ AppChromeUpdate: ({ actions }: { actions: React.ReactNode }) => {actions}
,
+}));
+
+jest.setTimeout(60 * 1000);
+
+setupMswServer();
+
+const selectFolderAndGroup = async (user: UserEvent) => {
+ await user.click(await screen.findByRole('button', { name: /select folder/i }));
+ await user.click(await screen.findByLabelText(FOLDER_TITLE_HAPPY_PATH));
+ const groupInput = await ui.inputs.group.find();
+ await user.click(await byRole('combobox').find(groupInput));
+ await clickSelectOption(groupInput, grafanaRulerGroup.name);
+};
+
+const dataSources = {
+ default: mockDataSource(
+ {
+ type: 'prometheus',
+ name: 'Prom',
+ uid: PROMETHEUS_DATASOURCE_UID,
+ isDefault: true,
+ },
+ { alerting: true, module: 'core:plugin/prometheus' }
+ ),
+};
+describe('RuleEditor grafana recording rules', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setupDataSources(dataSources.default);
+ contextSrv.isEditor = true;
+ contextSrv.hasEditPermissionInFolders = true;
+ grantUserPermissions([
+ AccessControlAction.AlertingRuleRead,
+ AccessControlAction.AlertingRuleUpdate,
+ AccessControlAction.AlertingRuleDelete,
+ AccessControlAction.AlertingRuleCreate,
+ AccessControlAction.DataSourcesRead,
+ AccessControlAction.DataSourcesWrite,
+ AccessControlAction.DataSourcesCreate,
+ AccessControlAction.FoldersWrite,
+ AccessControlAction.FoldersRead,
+ AccessControlAction.AlertingRuleExternalRead,
+ AccessControlAction.AlertingRuleExternalWrite,
+ ]);
+ });
+
+ const testCreateGrafanaRR = (featureToggles: Array, testName: string) => {
+ testWithFeatureToggles(featureToggles);
+
+ it(testName, async () => {
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+
+ const { user } = renderRuleEditor(undefined, 'grafana-recording');
+
+ await user.type(await ui.inputs.name.find(), 'my great new rule');
+ await user.type(await ui.inputs.metric.find(), 'metricName');
+ await selectFolderAndGroup(user);
+
+ await user.click(ui.buttons.saveAndExit.get());
+
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
+ });
+ };
+
+ const testCreateGrafanaRRWithInvalidMetricName = (featureToggles: Array, testName: string) => {
+ testWithFeatureToggles(featureToggles);
+
+ it(testName, async () => {
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+ const { user } = renderRuleEditor(undefined, 'grafana-recording');
+
+ await user.type(await ui.inputs.name.find(), 'my great new rule');
+ await selectFolderAndGroup(user);
+
+ await user.click(ui.buttons.saveAndExit.get());
+ const requests = await capture;
+ expect(requests).toHaveLength(0);
+ });
+ };
+
+ testCreateGrafanaRR([], 'can create new grafana recording rule with simplified steps feature toggles disabled');
+ testCreateGrafanaRR(
+ ['alertingQueryAndExpressionsStepMode', 'alertingNotificationsStepMode'],
+ 'can create new grafana recording rule with simplified steps enabled'
+ );
+
+ testCreateGrafanaRRWithInvalidMetricName(
+ [],
+ 'cannot create new grafana recording rule with invalid metric name with simplified steps feature toggles disabled'
+ );
+ testCreateGrafanaRRWithInvalidMetricName(
+ ['alertingQueryAndExpressionsStepMode', 'alertingNotificationsStepMode'],
+ 'cannot create new grafana recording rule with invalid metric name with simplified steps enabled'
+ );
+});
diff --git a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx
index 41a35d2ff60..9b863517b14 100644
--- a/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx
+++ b/public/app/features/alerting/unified/RuleEditorGrafanaRules.test.tsx
@@ -11,6 +11,7 @@ import { AccessControlAction } from 'app/types';
import { grantUserPermissions, mockDataSource } from './mocks';
import { grafanaRulerGroup } from './mocks/grafanaRulerApi';
+import { captureRequests, serializeRequests } from './mocks/server/events';
import { setupDataSources } from './testSetup/datasources';
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
@@ -42,6 +43,7 @@ describe('RuleEditor grafana managed rules', () => {
});
it('can create new grafana managed alert', async () => {
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
const dataSources = {
default: mockDataSource(
{
@@ -50,7 +52,7 @@ describe('RuleEditor grafana managed rules', () => {
uid: PROMETHEUS_DATASOURCE_UID,
isDefault: true,
},
- { alerting: false }
+ { alerting: true, module: 'core:plugin/prometheus' }
),
};
@@ -69,5 +71,8 @@ describe('RuleEditor grafana managed rules', () => {
await user.click(ui.buttons.saveAndExit.get());
expect(await screen.findByRole('status')).toHaveTextContent('Rule added successfully');
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
});
});
diff --git a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx
index f61352d06b8..766253febf1 100644
--- a/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx
+++ b/public/app/features/alerting/unified/RuleEditorRecordingRule.test.tsx
@@ -67,7 +67,7 @@ describe('RuleEditor recording rules', () => {
});
it('can create a new cloud recording rule', async () => {
- renderRuleEditor(undefined, true);
+ renderRuleEditor(undefined, 'recording');
await waitForElementToBeRemoved(screen.queryAllByTestId('Spinner'));
await userEvent.type(await ui.inputs.name.find(), 'my great new recording rule');
diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap
new file mode 100644
index 00000000000..e6a833408b1
--- /dev/null
+++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRecordingRules.test.tsx.snap
@@ -0,0 +1,259 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RuleEditor grafana recording rules can create new grafana recording rule with simplified steps enabled 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {},
+ "grafana_alert": {
+ "condition": "B",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ ],
+ "is_paused": false,
+ "record": {
+ "from": "B",
+ "metric": "metricName",
+ },
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
+
+exports[`RuleEditor grafana recording rules can create new grafana recording rule with simplified steps feature toggles disabled 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {},
+ "grafana_alert": {
+ "condition": "B",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ ],
+ "is_paused": false,
+ "record": {
+ "from": "B",
+ "metric": "metricName",
+ },
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
diff --git a/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap
new file mode 100644
index 00000000000..821c074014f
--- /dev/null
+++ b/public/app/features/alerting/unified/__snapshots__/RuleEditorGrafanaRules.test.tsx.snap
@@ -0,0 +1,168 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RuleEditor grafana managed rules can create new grafana managed alert 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {
+ "description": "some description",
+ },
+ "for": "1m",
+ "grafana_alert": {
+ "condition": "C",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ ],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "C",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "B",
+ "refId": "C",
+ "type": "threshold",
+ },
+ "queryType": "",
+ "refId": "C",
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "no_data_state": "NoData",
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
index bdd5fced16b..11b3a0f5dfc 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRuleEditor.test.tsx
@@ -1,27 +1,25 @@
+import { UserEvent } from '@testing-library/user-event';
import { ReactNode } from 'react';
-import { Route, Routes } from 'react-router-dom-v5-compat';
-import { ui } from 'test/helpers/alertingRuleEditor';
+import { GrafanaRuleFormStep, renderRuleEditor, ui } from 'test/helpers/alertingRuleEditor';
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
-import { render, screen, userEvent } from 'test/test-utils';
+import { screen, waitFor } from 'test/test-utils';
import { byRole } from 'testing-library-selector';
-import { config } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
-import RuleEditor from 'app/features/alerting/unified/RuleEditor';
import { setupMswServer } from 'app/features/alerting/unified/mockApi';
import { grantUserPermissions, mockDataSource } from 'app/features/alerting/unified/mocks';
import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure';
import { PROMETHEUS_DATASOURCE_UID } from 'app/features/alerting/unified/mocks/server/constants';
import { captureRequests, serializeRequests } from 'app/features/alerting/unified/mocks/server/events';
import { FOLDER_TITLE_HAPPY_PATH } from 'app/features/alerting/unified/mocks/server/handlers/search';
-import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
import { testWithFeatureToggles } from 'app/features/alerting/unified/test/test-utils';
-import { DataSourceType, GRAFANA_DATASOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
+import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
+import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
+import { MANUAL_ROUTING_KEY, SIMPLIFIED_QUERY_EDITOR_KEY } from 'app/features/alerting/unified/utils/rule-form';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { grafanaRulerGroup } from '../../../../mocks/grafanaRulerApi';
-import { setupDataSources } from '../../../../testSetup/datasources';
jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
AppChromeUpdate: ({ actions }: { actions: ReactNode }) => {actions}
,
@@ -29,8 +27,6 @@ jest.mock('app/core/components/AppChrome/AppChromeUpdate', () => ({
jest.setTimeout(60 * 1000);
-setupMswServer();
-
const dataSources = {
default: mockDataSource(
{
@@ -39,7 +35,7 @@ const dataSources = {
uid: PROMETHEUS_DATASOURCE_UID,
isDefault: true,
},
- { alerting: false }
+ { alerting: true, module: 'core:plugin/prometheus' }
),
am: mockDataSource({
name: 'Alertmanager',
@@ -47,10 +43,7 @@ const dataSources = {
}),
};
-setupDataSources(dataSources.default, dataSources.am);
-
-const selectFolderAndGroup = async () => {
- const user = userEvent.setup();
+const selectFolderAndGroup = async (user: UserEvent) => {
await user.click(await screen.findByRole('button', { name: /select folder/i }));
await user.click(await screen.findByLabelText(FOLDER_TITLE_HAPPY_PATH));
const groupInput = await ui.inputs.group.find();
@@ -58,12 +51,21 @@ const selectFolderAndGroup = async () => {
await clickSelectOption(groupInput, grafanaRulerGroup.name);
};
+const selectContactPoint = async (user: UserEvent, contactPointName: string) => {
+ const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find();
+ await user.click(byRole('combobox').get(contactPointInput));
+ await clickSelectOption(contactPointInput, contactPointName);
+};
+
+setupMswServer();
describe('Can create a new grafana managed alert using simplified routing', () => {
+ testWithFeatureToggles(['alertingSimplifiedRouting']);
+
beforeEach(() => {
- jest.clearAllMocks();
+ window.localStorage.clear();
+ setupDataSources(dataSources.default, dataSources.am);
contextSrv.isEditor = true;
contextSrv.hasEditPermissionInFolders = true;
- config.featureToggles.alertingSimplifiedRouting = true;
grantUserPermissions([
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
@@ -84,11 +86,11 @@ describe('Can create a new grafana managed alert using simplified routing', () =
it('cannot create new grafana managed alert when using simplified routing and not selecting a contact point', async () => {
const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
- const { user } = renderSimplifiedRuleEditor();
+ const { user } = renderRuleEditor();
await user.type(await ui.inputs.name.find(), 'my great new rule');
- await selectFolderAndGroup();
+ await selectFolderAndGroup(user);
//select contact point routing
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
@@ -104,26 +106,25 @@ describe('Can create a new grafana managed alert using simplified routing', () =
it('simplified routing is not available when Grafana AM is not enabled', async () => {
setAlertmanagerChoices(AlertmanagerChoice.External, 1);
- renderSimplifiedRuleEditor();
+ renderRuleEditor();
- expect(ui.inputs.simplifiedRouting.contactPointRouting.query()).not.toBeInTheDocument();
+ await waitFor(() => expect(ui.inputs.simplifiedRouting.contactPointRouting.query()).not.toBeInTheDocument());
});
it('can create new grafana managed alert when using simplified routing and selecting a contact point', async () => {
const contactPointName = 'lotsa-emails';
const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
- const { user } = renderSimplifiedRuleEditor();
+ const { user } = renderRuleEditor();
await user.type(await ui.inputs.name.find(), 'my great new rule');
- await selectFolderAndGroup();
+ await selectFolderAndGroup(user);
//select contact point routing
await user.click(ui.inputs.simplifiedRouting.contactPointRouting.get());
- const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find();
- await user.click(byRole('combobox').get(contactPointInput));
- await clickSelectOption(contactPointInput, contactPointName);
+
+ await selectContactPoint(user, contactPointName);
// save and check what was sent to backend
await user.click(ui.buttons.saveAndExit.get());
@@ -137,27 +138,108 @@ describe('Can create a new grafana managed alert using simplified routing', () =
testWithFeatureToggles(['alertingApiServer']);
it('allows selecting a contact point when using alerting API server', async () => {
- const { user } = renderSimplifiedRuleEditor();
+ const { user } = renderRuleEditor();
await user.click(await ui.inputs.simplifiedRouting.contactPointRouting.find());
- const contactPointInput = await ui.inputs.simplifiedRouting.contactPoint.find();
- await user.click(byRole('combobox').get(contactPointInput));
- await clickSelectOption(contactPointInput, 'lotsa-emails');
+ await selectContactPoint(user, 'Email');
expect(await screen.findByText('Email')).toBeInTheDocument();
});
});
-});
+ describe('switch modes enabled', () => {
+ testWithFeatureToggles(['alertingQueryAndExpressionsStepMode', 'alertingNotificationsStepMode']);
-function renderSimplifiedRuleEditor() {
- return render(
-
-
- } />
- } />
-
- ,
- { historyOptions: { initialEntries: ['/alerting/new/alerting'] } }
- );
-}
+ it('can create the new grafana-managed rule with default modes', async () => {
+ const contactPointName = 'lotsa-emails';
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+
+ const { user } = renderRuleEditor();
+
+ await user.type(await ui.inputs.name.find(), 'my great new rule');
+
+ await selectFolderAndGroup(user);
+
+ await selectContactPoint(user, contactPointName);
+
+ // save and check what was sent to backend
+ await user.click(ui.buttons.saveAndExit.get());
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
+ });
+ it('can create the new grafana-managed rule with advanced modes', async () => {
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+
+ const { user } = renderRuleEditor();
+
+ await user.click(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Query).get()); // switch to query step advanced mode
+ await user.click(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Notification).get()); // switch to notifications step advanced mode
+ await user.type(await ui.inputs.name.find(), 'my great new rule');
+
+ await selectFolderAndGroup(user);
+
+ // save and check what was sent to backend
+ await user.click(ui.buttons.saveAndExit.get());
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
+ });
+ it('can create the new grafana-managed rule with only notifications step advanced mode', async () => {
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+
+ const { user } = renderRuleEditor();
+
+ await user.type(await ui.inputs.name.find(), 'my great new rule');
+
+ await selectFolderAndGroup(user);
+
+ await user.click(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Notification).get()); // switch notifications step to advanced mode
+
+ // save and check what was sent to backend
+ await user.click(ui.buttons.saveAndExit.get());
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
+ });
+ it('can create the new grafana-managed rule with only query step advanced mode', async () => {
+ const contactPointName = 'lotsa-emails';
+ const capture = captureRequests((r) => r.method === 'POST' && r.url.includes('/api/ruler/'));
+
+ const { user } = renderRuleEditor();
+
+ await user.type(await ui.inputs.name.find(), 'my great new rule');
+
+ await selectFolderAndGroup(user);
+ await selectContactPoint(user, contactPointName);
+
+ await user.click(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Query).get()); // switch query step to advanced mode
+
+ // save and check what was sent to backend
+ await user.click(ui.buttons.saveAndExit.get());
+ const requests = await capture;
+ const serializedRequests = await serializeRequests(requests);
+ expect(serializedRequests).toMatchSnapshot();
+ });
+ it('switch modes are intiallized depending on the local storage - 1', async () => {
+ localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'false');
+ localStorage.setItem(MANUAL_ROUTING_KEY, 'true');
+
+ const { user } = renderRuleEditor();
+ await selectFolderAndGroup(user);
+
+ expect(ui.inputs.switchModeAdvanced(GrafanaRuleFormStep.Query).get()).toBeInTheDocument();
+ expect(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Notification).get()).toBeInTheDocument();
+ });
+ it('switch modes are intiallized depending on the local storage - 2', async () => {
+ localStorage.setItem(SIMPLIFIED_QUERY_EDITOR_KEY, 'true');
+ localStorage.setItem(MANUAL_ROUTING_KEY, 'false');
+
+ const { user } = renderRuleEditor();
+ await selectFolderAndGroup(user);
+
+ expect(ui.inputs.switchModeBasic(GrafanaRuleFormStep.Query).get()).toBeInTheDocument();
+ expect(ui.inputs.switchModeAdvanced(GrafanaRuleFormStep.Notification).get()).toBeInTheDocument();
+ });
+ });
+});
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap
index 5e8fc88cdb3..3cee4d53070 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/__snapshots__/SimplifiedRuleEditor.test.tsx.snap
@@ -51,43 +51,20 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
"annotations": {},
"for": "1m",
"grafana_alert": {
- "condition": "B",
+ "condition": "C",
"data": [
{
- "datasourceUid": "__expr__",
+ "datasourceUid": "prometheus",
"model": {
- "conditions": [
- {
- "evaluator": {
- "params": [],
- "type": "gt",
- },
- "operator": {
- "type": "and",
- },
- "query": {
- "params": [
- "A",
- ],
- },
- "reducer": {
- "params": [],
- "type": "last",
- },
- "type": "query",
- },
- ],
- "datasource": {
- "type": "__expr__",
- "uid": "__expr__",
- },
- "expression": "A",
- "reducer": "last",
+ "instant": true,
"refId": "A",
- "type": "reduce",
},
"queryType": "",
"refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
},
{
"datasourceUid": "__expr__",
@@ -95,9 +72,7 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
"conditions": [
{
"evaluator": {
- "params": [
- 0,
- ],
+ "params": [],
"type": "gt",
},
"operator": {
@@ -120,12 +95,50 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
"uid": "__expr__",
},
"expression": "A",
+ "reducer": "last",
"refId": "B",
- "type": "threshold",
+ "type": "reduce",
},
"queryType": "",
"refId": "B",
},
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ ],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "C",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "B",
+ "refId": "C",
+ "type": "threshold",
+ },
+ "queryType": "",
+ "refId": "C",
+ },
],
"exec_err_state": "Error",
"is_paused": false,
@@ -154,3 +167,693 @@ exports[`Can create a new grafana managed alert using simplified routing can cre
},
]
`;
+
+exports[`Can create a new grafana managed alert using simplified routing switch modes enabled can create the new grafana-managed rule with advanced modes 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {},
+ "for": "1m",
+ "grafana_alert": {
+ "condition": "C",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ ],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "C",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "B",
+ "refId": "C",
+ "type": "threshold",
+ },
+ "queryType": "",
+ "refId": "C",
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "metadata": {
+ "editor_settings": {
+ "simplified_notifications_section": false,
+ "simplified_query_and_expressions_section": false,
+ },
+ },
+ "no_data_state": "NoData",
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
+
+exports[`Can create a new grafana managed alert using simplified routing switch modes enabled can create the new grafana-managed rule with default modes 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {},
+ "for": "1m",
+ "grafana_alert": {
+ "condition": "C",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ ],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "C",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "B",
+ "refId": "C",
+ "type": "threshold",
+ },
+ "queryType": "",
+ "refId": "C",
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "metadata": {
+ "editor_settings": {
+ "simplified_notifications_section": true,
+ "simplified_query_and_expressions_section": true,
+ },
+ },
+ "no_data_state": "NoData",
+ "notification_settings": {
+ "receiver": "lotsa-emails",
+ },
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
+
+exports[`Can create a new grafana managed alert using simplified routing switch modes enabled can create the new grafana-managed rule with only notifications step advanced mode 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {},
+ "for": "1m",
+ "grafana_alert": {
+ "condition": "C",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ ],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "C",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "B",
+ "refId": "C",
+ "type": "threshold",
+ },
+ "queryType": "",
+ "refId": "C",
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "metadata": {
+ "editor_settings": {
+ "simplified_notifications_section": false,
+ "simplified_query_and_expressions_section": true,
+ },
+ },
+ "no_data_state": "NoData",
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
+
+exports[`Can create a new grafana managed alert using simplified routing switch modes enabled can create the new grafana-managed rule with only query step advanced mode 1`] = `
+[
+ {
+ "body": {
+ "interval": "1m",
+ "name": "grafana-group-1",
+ "rules": [
+ {
+ "annotations": {
+ "summary": "Test alert",
+ },
+ "for": "5m",
+ "grafana_alert": {
+ "condition": "A",
+ "data": [
+ {
+ "datasourceUid": "datasource-uid",
+ "model": {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "datasource-uid",
+ },
+ "expression": "vector(1)",
+ "queryType": "alerting",
+ "refId": "A",
+ },
+ "queryType": "alerting",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 1000,
+ "to": 2000,
+ },
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "namespace_uid": "uuid020c61ef",
+ "no_data_state": "NoData",
+ "rule_group": "grafana-group-1",
+ "title": "Grafana-rule",
+ "uid": "4d7125fee983",
+ },
+ "labels": {
+ "region": "nasa",
+ "severity": "critical",
+ },
+ },
+ {
+ "annotations": {},
+ "for": "1m",
+ "grafana_alert": {
+ "condition": "C",
+ "data": [
+ {
+ "datasourceUid": "prometheus",
+ "model": {
+ "instant": true,
+ "refId": "A",
+ },
+ "queryType": "",
+ "refId": "A",
+ "relativeTimeRange": {
+ "from": 600,
+ "to": 0,
+ },
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "B",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "A",
+ "reducer": "last",
+ "refId": "B",
+ "type": "reduce",
+ },
+ "queryType": "",
+ "refId": "B",
+ },
+ {
+ "datasourceUid": "__expr__",
+ "model": {
+ "conditions": [
+ {
+ "evaluator": {
+ "params": [
+ 0,
+ ],
+ "type": "gt",
+ },
+ "operator": {
+ "type": "and",
+ },
+ "query": {
+ "params": [
+ "C",
+ ],
+ },
+ "reducer": {
+ "params": [],
+ "type": "last",
+ },
+ "type": "query",
+ },
+ ],
+ "datasource": {
+ "type": "__expr__",
+ "uid": "__expr__",
+ },
+ "expression": "B",
+ "refId": "C",
+ "type": "threshold",
+ },
+ "queryType": "",
+ "refId": "C",
+ },
+ ],
+ "exec_err_state": "Error",
+ "is_paused": false,
+ "metadata": {
+ "editor_settings": {
+ "simplified_notifications_section": true,
+ "simplified_query_and_expressions_section": false,
+ },
+ },
+ "no_data_state": "NoData",
+ "notification_settings": {
+ "receiver": "lotsa-emails",
+ },
+ "title": "my great new rule",
+ },
+ "labels": {},
+ },
+ ],
+ },
+ "headers": [
+ [
+ "content-type",
+ "application/json",
+ ],
+ [
+ "accept",
+ "application/json, text/plain, */*",
+ ],
+ ],
+ "method": "POST",
+ "url": "http://localhost/api/ruler/grafana/api/v1/rules/uuid020c61ef?subtype=cortex",
+ },
+]
+`;
diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
index c701e2a41ec..124bd7e78a2 100644
--- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
@@ -93,7 +93,14 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
label: t('share-dashboard.menu.invite-user-title', 'Invite new member'),
renderCondition: !!config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd),
onClick: () => {
- window.open(config.externalUserMngLinkUrl, '_blank');
+ const url = new URL(config.externalUserMngLinkUrl);
+
+ // Add query parameters to track conversion
+ url.searchParams.append('src', 'grafananet');
+ url.searchParams.append('cnt', 'share-invite');
+
+ // Open the modified URL
+ window.open(url.toString(), '_blank');
},
renderDividerAbove: true,
component: () => ,
diff --git a/public/test/helpers/alertingRuleEditor.tsx b/public/test/helpers/alertingRuleEditor.tsx
index 465b2ef0dc3..0a2a8634ce8 100644
--- a/public/test/helpers/alertingRuleEditor.tsx
+++ b/public/test/helpers/alertingRuleEditor.tsx
@@ -1,15 +1,20 @@
-import { Routes, Route } from 'react-router-dom-v5-compat';
+import { Route, Routes } from 'react-router-dom-v5-compat';
import { render } from 'test/test-utils';
import { byRole, byTestId, byText } from 'testing-library-selector';
import { selectors } from '@grafana/e2e-selectors';
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
import RuleEditor from 'app/features/alerting/unified/RuleEditor';
+export enum GrafanaRuleFormStep {
+ Query = 2,
+ Notification = 5,
+}
export const ui = {
loadingIndicator: byText('Loading rule...'),
inputs: {
name: byRole('textbox', { name: 'name' }),
+ metric: byRole('textbox', { name: 'metric' }),
alertType: byTestId('alert-type-picker'),
dataSource: byTestId(selectors.components.DataSourcePicker.inputV2),
folder: byTestId('folder-picker'),
@@ -26,6 +31,8 @@ export const ui = {
contactPoint: byTestId('contact-point-picker'),
routingOptions: byText(/muting, grouping and timings \(optional\)/i),
},
+ switchModeBasic: (stepNo: GrafanaRuleFormStep) => byTestId(`advanced-switch-${stepNo}-basic`),
+ switchModeAdvanced: (stepNo: GrafanaRuleFormStep) => byTestId(`advanced-switch-${stepNo}-advanced`),
},
buttons: {
saveAndExit: byRole('button', { name: 'Save rule and exit' }),
@@ -34,8 +41,7 @@ export const ui = {
addLabel: byRole('button', { name: /Add label/ }),
},
};
-
-export function renderRuleEditor(identifier?: string, recording = false) {
+export function renderRuleEditor(identifier?: string, recording?: 'recording' | 'grafana-recording') {
return render(
<>
@@ -46,9 +52,7 @@ export function renderRuleEditor(identifier?: string, recording = false) {
>,
{
historyOptions: {
- initialEntries: [
- identifier ? `/alerting/${identifier}/edit` : `/alerting/new/${recording ? 'recording' : 'alerting'}`,
- ],
+ initialEntries: [identifier ? `/alerting/${identifier}/edit` : `/alerting/new/${recording ?? 'alerting'}`],
},
}
);
diff --git a/yarn.lock b/yarn.lock
index f9eaf5f8f3e..96e3935d1cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2691,17 +2691,17 @@ __metadata:
languageName: node
linkType: hard
-"@floating-ui/react@npm:0.27.2":
- version: 0.27.2
- resolution: "@floating-ui/react@npm:0.27.2"
+"@floating-ui/react@npm:0.27.3":
+ version: 0.27.3
+ resolution: "@floating-ui/react@npm:0.27.3"
dependencies:
"@floating-ui/react-dom": "npm:^2.1.2"
- "@floating-ui/utils": "npm:^0.2.8"
+ "@floating-ui/utils": "npm:^0.2.9"
tabbable: "npm:^6.0.0"
peerDependencies:
react: ">=17.0.0"
react-dom: ">=17.0.0"
- checksum: 10/c9ff0d7216cc7b51f066e8927852ba014bf51c79e48a1837f99760750df37fc3ed010dcdc89e57e673e2740e3bd2b91ccdcc6ccd13ec927950110a18b6b9cefa
+ checksum: 10/91eef58241b2225420fa73dfc501934463310489e08f2a0681b320e0c639c9ee59044ab66645f2901b74a88328208ff4cc4afabe93917a7a28f9d18037965cb6
languageName: node
linkType: hard
@@ -2719,10 +2719,10 @@ __metadata:
languageName: node
linkType: hard
-"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.8":
- version: 0.2.8
- resolution: "@floating-ui/utils@npm:0.2.8"
- checksum: 10/3e3ea3b2de06badc4baebdf358b3dbd77ccd9474a257a6ef237277895943db2acbae756477ec64de65a2a1436d94aea3107129a1feeef6370675bf2b161c1abc
+"@floating-ui/utils@npm:^0.2.0, @floating-ui/utils@npm:^0.2.8, @floating-ui/utils@npm:^0.2.9":
+ version: 0.2.9
+ resolution: "@floating-ui/utils@npm:0.2.9"
+ checksum: 10/0ca786347db3dd8d9034b86d1449fabb96642788e5900cc5f2aee433cd7b243efbcd7a165bead50b004ee3f20a90ddebb6a35296fc41d43cfd361b6f01b69ffb
languageName: node
linkType: hard
@@ -3668,7 +3668,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.13.5"
"@emotion/eslint-plugin": "npm:11.12.0"
- "@floating-ui/react": "npm:0.27.2"
+ "@floating-ui/react": "npm:0.27.3"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/experimental": "npm:2.1.6"
@@ -3969,7 +3969,7 @@ __metadata:
"@emotion/react": "npm:11.14.0"
"@emotion/serialize": "npm:1.3.3"
"@faker-js/faker": "npm:^9.0.0"
- "@floating-ui/react": "npm:0.27.2"
+ "@floating-ui/react": "npm:0.27.3"
"@grafana/data": "npm:11.5.0-pre"
"@grafana/e2e-selectors": "npm:11.5.0-pre"
"@grafana/faro-web-sdk": "npm:^1.3.6"
@@ -17612,7 +17612,7 @@ __metadata:
"@emotion/eslint-plugin": "npm:11.12.0"
"@emotion/react": "npm:11.14.0"
"@fingerprintjs/fingerprintjs": "npm:^3.4.2"
- "@floating-ui/react": "npm:0.27.2"
+ "@floating-ui/react": "npm:0.27.3"
"@formatjs/intl-durationformat": "npm:^0.7.0"
"@glideapps/glide-data-grid": "npm:^6.0.0"
"@grafana/aws-sdk": "npm:0.6.0"