diff --git a/.github/commands.json b/.github/commands.json
index 29106576726..0db8a607b53 100644
--- a/.github/commands.json
+++ b/.github/commands.json
@@ -1226,5 +1226,13 @@
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/69"
}
+ },
+ {
+ "type": "label",
+ "name": "area/suggestions",
+ "action": "addToProject",
+ "addToProject": {
+ "url": "https://github.com/orgs/grafana/projects/56"
+ }
}
]
diff --git a/.github/pr-commands.json b/.github/pr-commands.json
index 8b4472fe02a..ab42e204fce 100644
--- a/.github/pr-commands.json
+++ b/.github/pr-commands.json
@@ -469,5 +469,15 @@
"addToProject": {
"url": "https://github.com/orgs/grafana/projects/190"
}
+ },
+ {
+ "type": "changedfiles",
+ "matches": [
+ "public/app/features/panel/suggestions/**/*",
+ "public/app/plugins/panel/**/suggestions.ts",
+ "packages/grafana-data/src/types/suggestions*"
+ ],
+ "action": "updateLabel",
+ "addLabel": "area/suggestions"
}
]
diff --git a/.github/workflows/auto-triager/labels.txt b/.github/workflows/auto-triager/labels.txt
index 18ef2df93d8..451719ee0f7 100644
--- a/.github/workflows/auto-triager/labels.txt
+++ b/.github/workflows/auto-triager/labels.txt
@@ -85,6 +85,7 @@ area/scenes
area/search
area/security
area/streaming
+area/suggestions
area/templating/repeating
area/tooltip
area/transformations
diff --git a/apps/iam/go.sum b/apps/iam/go.sum
index 0a50abe77dd..0b22bce3774 100644
--- a/apps/iam/go.sum
+++ b/apps/iam/go.sum
@@ -853,8 +853,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
-github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388 h1:ZGYKVzeRlBWSf1e17Q8ME2WT1WmEKP8g0tMFfaBjgfI=
-github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
+github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg=
+github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
diff --git a/conf/defaults.ini b/conf/defaults.ini
index a72b0183290..c2d7e4da3b6 100644
--- a/conf/defaults.ini
+++ b/conf/defaults.ini
@@ -1747,9 +1747,8 @@ enabled = true
#################################### Short Links #############################
[short_links]
-# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is 7 days. Maximum value is 365.
-# 0 means they will be deleted approximately every 10 minutes. A negative value (such as -1) will disable expiration.
-expire_time = 7
+# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365.
+expire_time = -1
#################################### Internal Grafana Metrics ############
# Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId
diff --git a/conf/sample.ini b/conf/sample.ini
index d6397f894e4..d1d50f0a72a 100644
--- a/conf/sample.ini
+++ b/conf/sample.ini
@@ -1689,8 +1689,8 @@ default_datasource_uid =
#################################### Short Links #############################
[short_links]
-# Short links which are never accessed will be deleted as cleanup. Time is in days. Default is 7 days. Max is 365. 0 means they will be deleted approximately every 10 minutes.
-;expire_time = 7
+# Short links that are never accessed will be deleted as cleanup. Time is set up in days. The default is -1 (never expire). Maximum value is 365.
+;expire_time = -1
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP URL /metrics and /metrics/plugins/:pluginId
diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md
index 13c2b77fd2f..1a3e4aea652 100644
--- a/docs/sources/setup-grafana/configure-grafana/_index.md
+++ b/docs/sources/setup-grafana/configure-grafana/_index.md
@@ -2142,17 +2142,13 @@ Configures settings around the short link feature.
#### `expire_time`
-Short links that are never accessed are considered expired or stale and are deleted as cleanup.
+Short links that are never accessed are considered expired or stale and can be deleted as cleanup.
Set the expiration time in days.
-The default is `7` days.
+The default is `-1` days (never expire).
The maximum is `365` days.
-A setting above the maximum uses the value `365` instead.
-Setting `0` means the short links are cleaned up approximately every 10 minutes.
-A negative value such as `-1` disables expiry.
-{{< admonition type="caution" >}}
-Short links without an expiration increase the size of the database and can't be deleted. Grafana recommends setting a duration based on your specific use case
-{{< /admonition >}}
+A setting above the maximum uses the value `365` instead.
+A negative value such as `-1` disables expiry.
diff --git a/go.mod b/go.mod
index 526ca9abc27..785d5c39043 100644
--- a/go.mod
+++ b/go.mod
@@ -250,6 +250,7 @@ require (
github.com/grafana/grafana/apps/plugins v0.0.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana/apps/preferences v0.0.0 // @grafana/grafana-app-platform-squad
github.com/grafana/grafana/apps/provisioning v0.0.0 // @grafana/grafana-app-platform-squad
+ github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 // @grafana/grafana-search-and-storage
github.com/grafana/grafana/apps/scope v0.0.0 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana/apps/secret v0.0.0 // @grafana/grafana-operator-experience-squad
github.com/grafana/grafana/apps/shorturl v0.0.0 // @grafana/sharing-squad
diff --git a/go.sum b/go.sum
index ae0854810ad..f4f91335d2b 100644
--- a/go.sum
+++ b/go.sum
@@ -1653,8 +1653,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.284.0 h1:1bK7eWsnPBLUWDcWJWe218Ik5ad
github.com/grafana/grafana-plugin-sdk-go v0.284.0/go.mod h1:lHPniaSxq3SL5MxDIPy04TYB1jnTp/ivkYO+xn5Rz3E=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b h1:6Bo65etvjQ4tStkaA5+N3A3ENbO4UAWj53TxF6g2Hdk=
github.com/grafana/grafana/apps/example v0.0.0-20251027162426-edef69fdc82b/go.mod h1:6+wASOCN8LWt6FJ8dc0oODUBIEY5XHaE6ABi8g0mR+k=
-github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388 h1:ZGYKVzeRlBWSf1e17Q8ME2WT1WmEKP8g0tMFfaBjgfI=
-github.com/grafana/grafana/apps/quotas v0.0.0-20251209171149-4b999cd94388/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
+github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2 h1:rDPMdshj3QMvpXn+wK4T8awF9n2sd8i4YRiGqX2xTvg=
+github.com/grafana/grafana/apps/quotas v0.0.0-20251209183543-1013d74f13f2/go.mod h1:M7bV60iRB61y0ISPG1HX/oNLZtlh0ZF22rUYwNkAKjo=
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao=
github.com/grafana/grafana/pkg/semconv v0.0.0-20250804150913-990f1c69ecc2 h1:A65jWgLk4Re28gIuZcpC0aTh71JZ0ey89hKGE9h543s=
diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go
index b9da9e6f2c5..85d8bc4e4f1 100644
--- a/pkg/registry/apis/iam/register.go
+++ b/pkg/registry/apis/iam/register.go
@@ -90,11 +90,11 @@ func RegisterAPIService(
builder := &IdentityAccessManagementAPIBuilder{
store: store,
- userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation),
- saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation),
- legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation),
- teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation),
- ssoLegacyStore: sso.NewLegacyStore(ssoService),
+ userLegacyStore: user.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
+ saLegacyStore: serviceaccount.NewLegacyStore(store, accessClient, enableAuthnMutation, tracing),
+ legacyTeamStore: team.NewLegacyStore(store, legacyAccessClient, enableAuthnMutation, tracing),
+ teamBindingLegacyStore: teambinding.NewLegacyBindingStore(store, enableAuthnMutation, tracing),
+ ssoLegacyStore: sso.NewLegacyStore(ssoService, tracing),
coreRolesStorage: coreRolesStorage,
rolesStorage: rolesStorage,
resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider),
@@ -114,7 +114,7 @@ func RegisterAPIService(
dual: dual,
unified: unified,
userSearchClient: resource.NewSearchClient(dualwrite.NewSearchAdapter(dual), iamv0.UserResourceInfo.GroupResource(),
- unified, user.NewUserLegacySearchClient(userService), features),
+ unified, user.NewUserLegacySearchClient(userService, tracing), features),
teamSearch: NewTeamSearchHandler(tracing, dual, team.NewLegacyTeamSearchClient(teamService), unified, features),
}
apiregistration.RegisterAPI(builder)
diff --git a/pkg/registry/apis/iam/serviceaccount/store.go b/pkg/registry/apis/iam/serviceaccount/store.go
index 2980322cb27..db4709a4e2e 100644
--- a/pkg/registry/apis/iam/serviceaccount/store.go
+++ b/pkg/registry/apis/iam/serviceaccount/store.go
@@ -5,6 +5,7 @@ import (
"fmt"
"strings"
+ "go.opentelemetry.io/otel/trace"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,14 +36,15 @@ var (
var resource = iamv0alpha1.ServiceAccountResourceInfo
-func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore {
- return &LegacyStore{store, ac, enableAuthnMutation}
+func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore {
+ return &LegacyStore{store, ac, enableAuthnMutation, tracer}
}
type LegacyStore struct {
store legacy.LegacyIdentityStore
ac claims.AccessClient
enableAuthnMutation bool
+ tracer trace.Tracer
}
// DeleteCollection implements rest.CollectionDeleter.
@@ -52,6 +54,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res
// Delete implements rest.GracefulDeleter.
func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "serviceaccount.Delete")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
}
@@ -95,6 +100,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda
// Create implements rest.Creater.
func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "serviceaccount.Create")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create")
}
@@ -165,6 +173,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
}
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "serviceaccount.List")
+ defer span.End()
+
res, err := common.List(
ctx, resource, s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.ServiceAccount], error) {
@@ -228,6 +239,9 @@ func extractPluginNameFromTitle(title string) string {
}
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "serviceaccount.Get")
+ defer span.End()
+
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
diff --git a/pkg/registry/apis/iam/sso/store.go b/pkg/registry/apis/iam/sso/store.go
index c9965adc152..9ab09fa6ef4 100644
--- a/pkg/registry/apis/iam/sso/store.go
+++ b/pkg/registry/apis/iam/sso/store.go
@@ -7,6 +7,7 @@ import (
commonv1 "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
+ "go.opentelemetry.io/otel/trace"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -32,12 +33,13 @@ var (
var resource = iamv0.SSOSettingResourceInfo
-func NewLegacyStore(service ssosettings.Service) *LegacyStore {
- return &LegacyStore{service}
+func NewLegacyStore(service ssosettings.Service, tracer trace.Tracer) *LegacyStore {
+ return &LegacyStore{service, tracer}
}
type LegacyStore struct {
service ssosettings.Service
+ tracer trace.Tracer
}
// Destroy implements rest.Storage.
@@ -71,6 +73,9 @@ func (s *LegacyStore) NewList() runtime.Object {
// List implements rest.Lister.
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "sso.List")
+ defer span.End()
+
ns, _ := request.NamespaceInfoFrom(ctx, false)
settings, err := s.service.List(ctx)
@@ -88,6 +93,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
// Get implements rest.Getter.
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "sso.Get")
+ defer span.End()
+
ns, _ := request.NamespaceInfoFrom(ctx, false)
setting, err := s.service.GetForProviderWithRedactedSecrets(ctx, name)
@@ -112,6 +120,9 @@ func (s *LegacyStore) Update(
_ bool,
_ *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "sso.Update")
+ defer span.End()
+
const created = false
ident, err := identity.GetRequester(ctx)
if err != nil {
@@ -148,6 +159,9 @@ func (s *LegacyStore) Delete(
_ rest.ValidateObjectFunc,
options *metav1.DeleteOptions,
) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "sso.Delete")
+ defer span.End()
+
obj, err := s.Get(ctx, name, nil)
if err != nil {
return obj, false, err
diff --git a/pkg/registry/apis/iam/team/store.go b/pkg/registry/apis/iam/team/store.go
index e048fdb7a6d..c667c8ec374 100644
--- a/pkg/registry/apis/iam/team/store.go
+++ b/pkg/registry/apis/iam/team/store.go
@@ -5,6 +5,7 @@ import (
"fmt"
"strconv"
+ "go.opentelemetry.io/otel/trace"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -35,14 +36,15 @@ var (
var resource = iamv0alpha1.TeamResourceInfo
-func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore {
- return &LegacyStore{store, ac, enableAuthnMutation}
+func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore {
+ return &LegacyStore{store, ac, enableAuthnMutation, tracer}
}
type LegacyStore struct {
store legacy.LegacyIdentityStore
ac claims.AccessClient
enableAuthnMutation bool
+ tracer trace.Tracer
}
func (s *LegacyStore) New() runtime.Object {
@@ -74,6 +76,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res
// Delete implements rest.GracefulDeleter.
func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "team.Delete")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
}
@@ -112,6 +117,9 @@ func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation
// Update implements rest.Updater.
func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "team.Update")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update")
}
@@ -161,6 +169,9 @@ func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.Upda
}
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "team.List")
+ defer span.End()
+
res, err := common.List(
ctx, resource, s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.Team], error) {
@@ -197,6 +208,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
}
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "team.Get")
+ defer span.End()
+
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
@@ -219,6 +233,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
}
func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "team.Create")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create")
}
diff --git a/pkg/registry/apis/iam/teambinding/store.go b/pkg/registry/apis/iam/teambinding/store.go
index eaaa4766e39..6f6fd4ce162 100644
--- a/pkg/registry/apis/iam/teambinding/store.go
+++ b/pkg/registry/apis/iam/teambinding/store.go
@@ -6,6 +6,7 @@ import (
"strconv"
"time"
+ "go.opentelemetry.io/otel/trace"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,13 +36,14 @@ var (
_ rest.CollectionDeleter = (*LegacyBindingStore)(nil)
)
-func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool) *LegacyBindingStore {
- return &LegacyBindingStore{store, enableAuthnMutation}
+func NewLegacyBindingStore(store legacy.LegacyIdentityStore, enableAuthnMutation bool, tracer trace.Tracer) *LegacyBindingStore {
+ return &LegacyBindingStore{store, enableAuthnMutation, tracer}
}
type LegacyBindingStore struct {
store legacy.LegacyIdentityStore
enableAuthnMutation bool
+ tracer trace.Tracer
}
// Destroy implements rest.Storage.
@@ -73,6 +75,9 @@ func (l *LegacyBindingStore) ConvertToTable(ctx context.Context, object runtime.
}
func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
+ ctx, span := l.tracer.Start(ctx, "teambinding.Update")
+ defer span.End()
+
if !l.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "update")
}
@@ -125,6 +130,9 @@ func (l *LegacyBindingStore) Update(ctx context.Context, name string, objInfo re
}
func (l *LegacyBindingStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
+ ctx, span := l.tracer.Start(ctx, "teambinding.Delete")
+ defer span.End()
+
if !l.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "delete")
}
@@ -160,6 +168,9 @@ func (l *LegacyBindingStore) DeleteCollection(ctx context.Context, deleteValidat
}
func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
+ ctx, span := l.tracer.Start(ctx, "teambinding.Create")
+ defer span.End()
+
if !l.enableAuthnMutation {
return nil, apierrors.NewMethodNotSupported(bindingResource.GroupResource(), "create")
}
@@ -230,6 +241,9 @@ func (l *LegacyBindingStore) Create(ctx context.Context, obj runtime.Object, cre
// Get implements rest.Getter.
func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+ ctx, span := l.tracer.Start(ctx, "teambinding.Get")
+ defer span.End()
+
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
@@ -254,6 +268,9 @@ func (l *LegacyBindingStore) Get(ctx context.Context, name string, options *meta
// List implements rest.Lister.
func (l *LegacyBindingStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
+ ctx, span := l.tracer.Start(ctx, "teambinding.List")
+ defer span.End()
+
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
diff --git a/pkg/registry/apis/iam/user/legacy_search.go b/pkg/registry/apis/iam/user/legacy_search.go
index 534a4f016eb..7fb6c13f7a6 100644
--- a/pkg/registry/apis/iam/user/legacy_search.go
+++ b/pkg/registry/apis/iam/user/legacy_search.go
@@ -6,6 +6,7 @@ import (
"log/slog"
"math"
+ "go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -27,13 +28,15 @@ type UserLegacySearchClient struct {
resourcepb.ResourceIndexClient
userService user.Service
log *slog.Logger
+ tracer trace.Tracer
}
// NewUserLegacySearchClient creates a new UserLegacySearchClient.
-func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient {
+func NewUserLegacySearchClient(userService user.Service, tracer trace.Tracer) *UserLegacySearchClient {
return &UserLegacySearchClient{
userService: userService,
log: slog.Default().With("logger", "legacy-user-search-client"),
+ tracer: tracer,
}
}
@@ -41,6 +44,9 @@ func NewUserLegacySearchClient(userService user.Service) *UserLegacySearchClient
// It only supports exact matching for title, login, or email.
// FIXME: This implementation only supports a single field query and will be extended in the future.
func (c *UserLegacySearchClient) Search(ctx context.Context, req *resourcepb.ResourceSearchRequest, _ ...grpc.CallOption) (*resourcepb.ResourceSearchResponse, error) {
+ ctx, span := c.tracer.Start(ctx, "user.Search")
+ defer span.End()
+
signedInUser, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
diff --git a/pkg/registry/apis/iam/user/legacy_search_test.go b/pkg/registry/apis/iam/user/legacy_search_test.go
index b8a754f34ce..6bd49185786 100644
--- a/pkg/registry/apis/iam/user/legacy_search_test.go
+++ b/pkg/registry/apis/iam/user/legacy_search_test.go
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/identity"
+ "github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
res "github.com/grafana/grafana/pkg/storage/unified/resource"
@@ -17,7 +18,7 @@ import (
func TestUserLegacySearchClient_Search(t *testing.T) {
t.Run("should return error if no query fields are provided", func(t *testing.T) {
mockUserService := usertest.NewMockService(t)
- client := NewUserLegacySearchClient(mockUserService)
+ client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService())
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
@@ -66,7 +67,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockUserService := usertest.NewMockService(t)
- client := NewUserLegacySearchClient(mockUserService)
+ client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService())
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
req := &resourcepb.ResourceSearchRequest{
Limit: 10,
@@ -125,7 +126,7 @@ func TestUserLegacySearchClient_Search(t *testing.T) {
t.Run("title should have precedence over login and email", func(t *testing.T) {
mockUserService := usertest.NewMockService(t)
- client := NewUserLegacySearchClient(mockUserService)
+ client := NewUserLegacySearchClient(mockUserService, tracing.NewNoopTracerService())
ctx := identity.WithRequester(context.Background(), &user.SignedInUser{OrgID: 1, UserID: 1})
req := &resourcepb.ResourceSearchRequest{
Options: &resourcepb.ListOptions{
diff --git a/pkg/registry/apis/iam/user/store.go b/pkg/registry/apis/iam/user/store.go
index 803b2972b85..97daee3af18 100644
--- a/pkg/registry/apis/iam/user/store.go
+++ b/pkg/registry/apis/iam/user/store.go
@@ -5,6 +5,7 @@ import (
"fmt"
"time"
+ "go.opentelemetry.io/otel/trace"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@@ -36,18 +37,22 @@ var (
var resource = iamv0alpha1.UserResourceInfo
-func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool) *LegacyStore {
- return &LegacyStore{store, ac, enableAuthnMutation}
+func NewLegacyStore(store legacy.LegacyIdentityStore, ac claims.AccessClient, enableAuthnMutation bool, tracer trace.Tracer) *LegacyStore {
+ return &LegacyStore{store, ac, enableAuthnMutation, tracer}
}
type LegacyStore struct {
store legacy.LegacyIdentityStore
ac claims.AccessClient
enableAuthnMutation bool
+ tracer trace.Tracer
}
// Update implements rest.Updater.
func (s *LegacyStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "user.Update")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "update")
}
@@ -105,6 +110,9 @@ func (s *LegacyStore) DeleteCollection(ctx context.Context, deleteValidation res
// Delete implements rest.GracefulDeleter.
func (s *LegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
+ ctx, span := s.tracer.Start(ctx, "user.Delete")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, false, apierrors.NewMethodNotSupported(resource.GroupResource(), "delete")
}
@@ -171,6 +179,9 @@ func (s *LegacyStore) ConvertToTable(ctx context.Context, object runtime.Object,
}
func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "user.List")
+ defer span.End()
+
res, err := common.List(
ctx, resource, s.ac, common.PaginationFromListOptions(options),
func(ctx context.Context, ns claims.NamespaceInfo, p common.Pagination) (*common.ListResponse[iamv0alpha1.User], error) {
@@ -206,6 +217,9 @@ func (s *LegacyStore) List(ctx context.Context, options *internalversion.ListOpt
}
func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "user.Get")
+ defer span.End()
+
ns, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
@@ -229,6 +243,9 @@ func (s *LegacyStore) Get(ctx context.Context, name string, options *metav1.GetO
// Create implements rest.Creater.
func (s *LegacyStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
+ ctx, span := s.tracer.Start(ctx, "user.Create")
+ defer span.End()
+
if !s.enableAuthnMutation {
return nil, apierrors.NewMethodNotSupported(resource.GroupResource(), "create")
}
diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go
index f6c0b3d3f19..cc59427da6f 100644
--- a/pkg/setting/setting.go
+++ b/pkg/setting/setting.go
@@ -1330,7 +1330,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error {
cfg.QueryHistoryEnabled = queryHistory.Key("enabled").MustBool(true)
shortLinks := iniFile.Section("short_links")
- cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(7)
+ cfg.ShortLinkExpiration = shortLinks.Key("expire_time").MustInt(-1)
if cfg.ShortLinkExpiration > 365 {
cfg.Logger.Warn("short_links expire_time must be less than 366 days. Setting to 365 days")
diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go
index bdfb2e8c7ca..c4052938603 100644
--- a/pkg/storage/unified/resource/server.go
+++ b/pkg/storage/unified/resource/server.go
@@ -1618,6 +1618,7 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) {
))
if s.overridesService == nil {
+ s.log.FromContext(ctx).Debug("overrides service not configured, skipping quota check", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource)
return
}
@@ -1632,6 +1633,12 @@ func (s *server) checkQuota(ctx context.Context, nsr NamespacedResource) {
s.log.FromContext(ctx).Error("failed to get resource stats for quota checking", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "error", err)
return
}
+ if len(stats) > 0 {
+ s.log.FromContext(ctx).Debug("stats found", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "count", stats[0].Count)
+ } else {
+ s.log.FromContext(ctx).Debug("no stats found for resource", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource)
+ }
+
if len(stats) > 0 && stats[0].Count >= int64(quota.Limit) {
s.log.FromContext(ctx).Info("Quota exceeded on create", "namespace", nsr.Namespace, "group", nsr.Group, "resource", nsr.Resource, "quota", quota.Limit, "count", stats[0].Count, "stats_resource", stats[0].Resource)
}
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index ea232baffcc..2cbda7827e3 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -236,6 +236,10 @@ export class KeybindingSrv {
appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl }));
});
+ this.bind('t =', () => {
+ appEvents.publish(new ZoomOutEvent({ scale: 0.5, updateUrl }));
+ });
+
this.bind('t -', () => {
appEvents.publish(new ZoomOutEvent({ scale: 2, updateUrl }));
});
diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts
index 9a70858f39b..8772d49f138 100644
--- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts
+++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.test.ts
@@ -269,6 +269,13 @@ describe('setupKeyboardShortcuts', () => {
expect(tPlusBinding).toBeDefined();
});
+ it('should setup t = zoom in shortcut', () => {
+ setupKeyboardShortcuts(mockScene);
+
+ const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't =');
+ expect(tEqualsBinding).toBeDefined();
+ });
+
it('should setup t - zoom out shortcut with keypress type', () => {
setupKeyboardShortcuts(mockScene);
@@ -302,9 +309,11 @@ describe('setupKeyboardShortcuts', () => {
setupKeyboardShortcuts(mockScene);
const tPlusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't +');
+ const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't =');
const tMinusBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't -');
expect(tPlusBinding).toBeUndefined();
+ expect(tEqualsBinding).toBeUndefined();
expect(tMinusBinding).toBeUndefined();
});
});
@@ -364,6 +373,28 @@ describe('setupKeyboardShortcuts', () => {
expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds
});
+ it('should zoom in (scale 0.5) when t = is pressed', () => {
+ setupKeyboardShortcuts(mockScene);
+
+ const tEqualsBinding = mockKeybindingSet.addBinding.mock.calls.find((call) => call[0].key === 't =');
+ const handler = tEqualsBinding![0].onTrigger;
+
+ handler();
+
+ // Scale 0.5 should result in 3 hour span (half of 6)
+ expect(mockTimeRange.onTimeRangeChange).toHaveBeenCalledWith(
+ expect.objectContaining({
+ from: expect.any(Object),
+ to: expect.any(Object),
+ raw: expect.any(Object),
+ })
+ );
+
+ const call = mockTimeRange.onTimeRangeChange.mock.calls[0][0];
+ const newSpan = call.to.valueOf() - call.from.valueOf();
+ expect(newSpan).toBe(3 * 60 * 60 * 1000); // 3 hours in milliseconds
+ });
+
it('should keep center point when zooming in', () => {
setupKeyboardShortcuts(mockScene);
diff --git a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts
index 1113b8a8a8c..f1878aeb466 100644
--- a/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts
+++ b/public/app/features/dashboard-scene/scene/keyboardShortcuts.ts
@@ -138,6 +138,13 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
},
});
+ keybindings.addBinding({
+ key: 't =',
+ onTrigger: () => {
+ handleZoom(scene, 0.5);
+ },
+ });
+
keybindings.addBinding({
key: 't -',
type: 'keypress', // NOTE: Because some browsers/OS identify minus symbol differently.