Merge remote-tracking branch 'origin/main' into grafana-git-ui-sync
This commit is contained in:
28 files changed
+1966
-191
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <div id="grafana-portal-container" className={styles.grafanaPortalContainer} />;
|
||||
return (
|
||||
<div
|
||||
id="grafana-portal-container"
|
||||
data-testid={selectors.components.Portal.container}
|
||||
className={styles.grafanaPortalContainer}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: {
|
||||
"*": {"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }) => <div>{actions}</div>,
|
||||
}));
|
||||
|
||||
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<keyof FeatureToggles>, 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<keyof FeatureToggles>, 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'
|
||||
);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
+259
@@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
+168
@@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
+124
-42
@@ -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 }) => <div>{actions}</div>,
|
||||
@@ -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(
|
||||
<AlertmanagerProvider alertmanagerSourceName={GRAFANA_DATASOURCE_NAME} accessType="notification">
|
||||
<Routes>
|
||||
<Route path={'/alerting/new/:type'} element={<RuleEditor />} />
|
||||
<Route path={'/alerting/:id/edit'} element={<RuleEditor />} />
|
||||
</Routes>
|
||||
</AlertmanagerProvider>,
|
||||
{ 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
+737
-34
@@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -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: () => <Icon name="external-link-alt" className={styles.inviteUserItemIcon} />,
|
||||
|
||||
@@ -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(
|
||||
<>
|
||||
<AppNotificationList />
|
||||
@@ -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'}`],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user