diff --git a/apps/advisor/kinds/check.cue b/apps/advisor/kinds/check.cue index 4ab33fb05df..cf1cfd58d39 100644 --- a/apps/advisor/kinds/check.cue +++ b/apps/advisor/kinds/check.cue @@ -10,6 +10,12 @@ check: { frontend: false backend: true } + validation: { + operations: [ + "CREATE", + "UPDATE", + ] + } schema: { spec: { // Generic data input that a check can receive diff --git a/apps/advisor/pkg/apis/advisor_manifest.go b/apps/advisor/pkg/apis/advisor_manifest.go index 2e8bb06fb65..f62ea3addb8 100644 --- a/apps/advisor/pkg/apis/advisor_manifest.go +++ b/apps/advisor/pkg/apis/advisor_manifest.go @@ -27,7 +27,15 @@ var appManifestData = app.ManifestData{ Conversion: false, Versions: []app.ManifestKindVersion{ { - Name: "v0alpha1", + Name: "v0alpha1", + Admission: &app.AdmissionCapabilities{ + Validation: &app.ValidationCapability{ + Operations: []app.AdmissionOperation{ + app.AdmissionOperationCreate, + app.AdmissionOperationUpdate, + }, + }, + }, Schema: &versionSchemaCheckv0alpha1, }, }, diff --git a/apps/advisor/pkg/app/app.go b/apps/advisor/pkg/app/app.go index e4ccafbcaaf..42848758d5a 100644 --- a/apps/advisor/pkg/app/app.go +++ b/apps/advisor/pkg/app/app.go @@ -2,16 +2,44 @@ package app import ( "context" + "fmt" "github.com/grafana/grafana-app-sdk/app" + "github.com/grafana/grafana-app-sdk/k8s" "github.com/grafana/grafana-app-sdk/resource" "github.com/grafana/grafana-app-sdk/simple" advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checkregistry" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/klog/v2" ) +const ( + typeLabel = "advisor.grafana.app/type" + statusAnnotation = "advisor.grafana.app/status" +) + func New(cfg app.Config) (app.App, error) { + // Read config + checkRegistry, ok := cfg.SpecificConfig.(checkregistry.CheckService) + if !ok { + return nil, fmt.Errorf("invalid config type") + } + + // Prepare storage client + clientGenerator := k8s.NewClientRegistry(cfg.KubeConfig, k8s.ClientConfig{}) + client, err := clientGenerator.ClientFor(advisorv0alpha1.CheckKind()) + if err != nil { + return nil, err + } + + // Initialize checks + checkMap := map[string]checks.Check{} + for _, c := range checkRegistry.Checks() { + checkMap[c.Type()] = c + } + simpleConfig := simple.AppConfig{ Name: "advisor", KubeConfig: cfg.KubeConfig, @@ -23,6 +51,24 @@ func New(cfg app.Config) (app.App, error) { ManagedKinds: []simple.AppManagedKind{ { Kind: advisorv0alpha1.CheckKind(), + Validator: &simple.Validator{ + ValidateFunc: func(ctx context.Context, req *app.AdmissionRequest) error { + if req.Object != nil { + _, err := getCheck(req.Object, checkMap) + return err + } + return nil + }, + }, + Watcher: &simple.Watcher{ + AddFunc: func(ctx context.Context, obj resource.Object) error { + check, err := getCheck(obj, checkMap) + if err != nil { + return err + } + return processCheck(ctx, client, obj, check) + }, + }, }, }, } diff --git a/apps/advisor/pkg/app/authorizer.go b/apps/advisor/pkg/app/authorizer.go new file mode 100644 index 00000000000..67defbc85d9 --- /dev/null +++ b/apps/advisor/pkg/app/authorizer.go @@ -0,0 +1,31 @@ +package app + +import ( + "context" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func GetAuthorizer() authorizer.Authorizer { + return authorizer.AuthorizerFunc(func( + ctx context.Context, attr authorizer.Attributes, + ) (authorized authorizer.Decision, reason string, err error) { + if !attr.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + // require a user + u, err := identity.GetRequester(ctx) + if err != nil { + return authorizer.DecisionDeny, "valid user is required", err + } + + // check if is admin + if u.GetIsGrafanaAdmin() { + return authorizer.DecisionAllow, "", nil + } + + return authorizer.DecisionDeny, "forbidden", nil + }) +} diff --git a/apps/advisor/pkg/app/authorizer_test.go b/apps/advisor/pkg/app/authorizer_test.go new file mode 100644 index 00000000000..84414362fbd --- /dev/null +++ b/apps/advisor/pkg/app/authorizer_test.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/stretchr/testify/assert" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestGetAuthorizer(t *testing.T) { + tests := []struct { + name string + ctx context.Context + attr authorizer.Attributes + expectedDecision authorizer.Decision + expectedReason string + expectedErr error + }{ + { + name: "non-resource request", + ctx: context.TODO(), + attr: &mockAttributes{resourceRequest: false}, + expectedDecision: authorizer.DecisionNoOpinion, + expectedReason: "", + expectedErr: nil, + }, + { + name: "user is admin", + ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: true}), + attr: &mockAttributes{resourceRequest: true}, + expectedDecision: authorizer.DecisionAllow, + expectedReason: "", + expectedErr: nil, + }, + { + name: "user is not admin", + ctx: identity.WithRequester(context.TODO(), &mockUser{isGrafanaAdmin: false}), + attr: &mockAttributes{resourceRequest: true}, + expectedDecision: authorizer.DecisionDeny, + expectedReason: "forbidden", + expectedErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + auth := GetAuthorizer() + decision, reason, err := auth.Authorize(tt.ctx, tt.attr) + assert.Equal(t, tt.expectedDecision, decision) + assert.Equal(t, tt.expectedReason, reason) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +type mockAttributes struct { + authorizer.Attributes + resourceRequest bool +} + +func (m *mockAttributes) IsResourceRequest() bool { + return m.resourceRequest +} + +// Implement other methods of authorizer.Attributes as needed + +type mockUser struct { + identity.Requester + isGrafanaAdmin bool +} + +func (m *mockUser) GetIsGrafanaAdmin() bool { + return m.isGrafanaAdmin +} + +// Implement other methods of identity.Requester as needed diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go new file mode 100644 index 00000000000..0c375f035c5 --- /dev/null +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -0,0 +1,42 @@ +package checkregistry + +import ( + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" +) + +type CheckService interface { + Checks() []checks.Check +} + +type Service struct { + datasourceSvc datasources.DataSourceService + pluginStore pluginstore.Store + pluginContextProvider datasource.PluginContextWrapper + pluginClient plugins.Client +} + +func ProvideService(datasourceSvc datasources.DataSourceService, pluginStore pluginstore.Store, + pluginContextProvider datasource.PluginContextWrapper, pluginClient plugins.Client) *Service { + return &Service{ + datasourceSvc: datasourceSvc, + pluginStore: pluginStore, + pluginContextProvider: pluginContextProvider, + pluginClient: pluginClient, + } +} + +func (s *Service) Checks() []checks.Check { + return []checks.Check{ + datasourcecheck.New( + s.datasourceSvc, + s.pluginStore, + s.pluginContextProvider, + s.pluginClient, + ), + } +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go new file mode 100644 index 00000000000..7aede0c65ec --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -0,0 +1,96 @@ +package datasourcecheck + +import ( + "context" + "fmt" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" + "github.com/grafana/grafana/pkg/util" + "k8s.io/klog/v2" +) + +func New( + datasourceSvc datasources.DataSourceService, + pluginStore pluginstore.Store, + pluginContextProvider datasource.PluginContextWrapper, + pluginClient plugins.Client, +) checks.Check { + return &check{ + DatasourceSvc: datasourceSvc, + PluginStore: pluginStore, + PluginContextProvider: pluginContextProvider, + PluginClient: pluginClient, + } +} + +type check struct { + DatasourceSvc datasources.DataSourceService + PluginStore pluginstore.Store + PluginContextProvider datasource.PluginContextWrapper + PluginClient plugins.Client +} + +func (c *check) Type() string { + return "datasource" +} + +func (c *check) Run(ctx context.Context, obj *advisor.CheckSpec) (*advisor.CheckV0alpha1StatusReport, error) { + // Optionally read the check input encoded in the object + // fmt.Println(obj.Data) + + dss, err := c.DatasourceSvc.GetAllDataSources(ctx, &datasources.GetAllDataSourcesQuery{}) + if err != nil { + return nil, err + } + + dsErrs := []advisor.CheckV0alpha1StatusReportErrors{} + for _, ds := range dss { + // Data source UID validation + err := util.ValidateUID(ds.UID) + if err != nil { + dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ + Severity: advisor.CheckStatusSeverityLow, + Reason: fmt.Sprintf("Invalid UID: %s", ds.UID), + Action: "Change UID", + }) + } + + // Health check execution + pCtx, err := c.PluginContextProvider.PluginContextForDataSource(ctx, &backend.DataSourceInstanceSettings{ + Type: ds.Type, + UID: ds.UID, + APIVersion: ds.APIVersion, + }) + if err != nil { + klog.ErrorS(err, "Error creating plugin context", "datasource", ds.Name) + continue + } + req := &backend.CheckHealthRequest{ + PluginContext: pCtx, + Headers: map[string]string{}, + } + resp, err := c.PluginClient.CheckHealth(ctx, req) + if err != nil { + fmt.Println("Error checking health", err) + continue + } + if resp.Status != backend.HealthStatusOk { + dsErrs = append(dsErrs, advisor.CheckV0alpha1StatusReportErrors{ + Severity: advisor.CheckStatusSeverityHigh, + Reason: fmt.Sprintf("Health check failed: %s", ds.Name), + Action: "Check datasource", + }) + } + } + + return &advisor.CheckV0alpha1StatusReport{ + Count: int64(len(dss)), + Errors: dsErrs, + }, nil +} diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go new file mode 100644 index 00000000000..ee489483bd2 --- /dev/null +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -0,0 +1,114 @@ +package datasourcecheck + +import ( + "context" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + advisor "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry/apis/datasource" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/stretchr/testify/assert" +) + +func TestCheck_Run(t *testing.T) { + t.Run("should return no errors when all datasources are healthy", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + {UID: "valid-uid-2", Type: "mysql", Name: "MySQL"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(2), report.Count) + assert.Empty(t, report.Errors) + }) + + t.Run("should return errors when datasource UID is invalid", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "invalid uid", Type: "prometheus", Name: "Prometheus"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(1), report.Count) + assert.Len(t, report.Errors, 1) + assert.Equal(t, "Invalid UID: invalid uid", report.Errors[0].Reason) + }) + + t.Run("should return errors when datasource health check fails", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + } + + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + } + + report, err := check.Run(context.Background(), &advisor.CheckSpec{}) + + assert.NoError(t, err) + assert.Equal(t, int64(1), report.Count) + assert.Len(t, report.Errors, 1) + assert.Equal(t, "Health check failed: Prometheus", report.Errors[0].Reason) + }) +} + +type MockDatasourceSvc struct { + datasources.DataSourceService + + dss []*datasources.DataSource +} + +func (m *MockDatasourceSvc) GetAllDataSources(ctx context.Context, query *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) { + return m.dss, nil +} + +type MockPluginContextProvider struct { + datasource.PluginContextWrapper + + pCtx backend.PluginContext +} + +func (m *MockPluginContextProvider) PluginContextForDataSource(ctx context.Context, datasourceSettings *backend.DataSourceInstanceSettings) (backend.PluginContext, error) { + return m.pCtx, nil +} + +type MockPluginClient struct { + plugins.Client + + res *backend.CheckHealthResult +} + +func (m *MockPluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + return m.res, nil +} diff --git a/apps/advisor/pkg/app/checks/ifaces.go b/apps/advisor/pkg/app/checks/ifaces.go new file mode 100644 index 00000000000..008f5656298 --- /dev/null +++ b/apps/advisor/pkg/app/checks/ifaces.go @@ -0,0 +1,13 @@ +package checks + +import ( + "context" + + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" +) + +// Check defines the methods that a check must implement to be executed. +type Check interface { + Run(ctx context.Context, obj *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) + Type() string +} diff --git a/apps/advisor/pkg/app/utils.go b/apps/advisor/pkg/app/utils.go new file mode 100644 index 00000000000..4eae090198b --- /dev/null +++ b/apps/advisor/pkg/app/utils.go @@ -0,0 +1,95 @@ +package app + +import ( + "context" + "errors" + "fmt" + + claims "github.com/grafana/authlib/types" + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/user" +) + +func getCheck(obj resource.Object, checks map[string]checks.Check) (checks.Check, error) { + labels := obj.GetLabels() + objTypeLabel, ok := labels[typeLabel] + if !ok { + return nil, errors.New("missing check type as label") + } + c, ok := checks[objTypeLabel] + if !ok { + supportedTypes := "" + for k := range checks { + supportedTypes += k + ", " + } + return nil, fmt.Errorf("unknown check type %s. Supported types are: %s", objTypeLabel, supportedTypes) + } + + return c, nil +} + +func getStatusAnnotation(obj resource.Object) string { + return obj.GetAnnotations()[statusAnnotation] +} + +func setStatusAnnotation(ctx context.Context, client resource.Client, obj resource.Object, status string) error { + annotations := obj.GetAnnotations() + annotations[statusAnnotation] = status + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/metadata/annotations", + Value: annotations, + }}, + }, resource.PatchOptions{}, obj) +} + +func processCheck(ctx context.Context, client resource.Client, obj resource.Object, check checks.Check) error { + status := getStatusAnnotation(obj) + if status != "" { + // Check already processed + return nil + } + c, ok := obj.(*advisorv0alpha1.Check) + if !ok { + return fmt.Errorf("invalid object type") + } + // Populate ctx with the user that created the check + meta, err := utils.MetaAccessor(obj) + if err != nil { + return err + } + createdBy := meta.GetCreatedBy() + typ, uid, err := claims.ParseTypeID(createdBy) + if err != nil { + return err + } + ctx = identity.WithRequester(ctx, &user.SignedInUser{ + UserUID: uid, + FallbackType: typ, + }) + // Run the checks + report, err := check.Run(ctx, &c.Spec) + if err != nil { + setErr := setStatusAnnotation(ctx, client, obj, "error") + if setErr != nil { + return setErr + } + return err + } + err = setStatusAnnotation(ctx, client, obj, "processed") + if err != nil { + return err + } + return client.PatchInto(ctx, obj.GetStaticMetadata().Identifier(), resource.PatchRequest{ + Operations: []resource.PatchOperation{{ + Operation: resource.PatchOpAdd, + Path: "/status/report", + Value: *report, + }}, + }, resource.PatchOptions{}, obj) +} diff --git a/apps/advisor/pkg/app/utils_test.go b/apps/advisor/pkg/app/utils_test.go new file mode 100644 index 00000000000..6626f645eb7 --- /dev/null +++ b/apps/advisor/pkg/app/utils_test.go @@ -0,0 +1,124 @@ +package app + +import ( + "context" + "errors" + "testing" + + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/stretchr/testify/assert" +) + +func TestGetCheck(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetLabels(map[string]string{typeLabel: "testType"}) + + checkMap := map[string]checks.Check{ + "testType": &mockCheck{}, + } + + check, err := getCheck(obj, checkMap) + assert.NoError(t, err) + assert.NotNil(t, check) +} + +func TestGetCheck_MissingLabel(t *testing.T) { + obj := &advisorv0alpha1.Check{} + checkMap := map[string]checks.Check{} + + _, err := getCheck(obj, checkMap) + assert.Error(t, err) + assert.Equal(t, "missing check type as label", err.Error()) +} + +func TestGetCheck_UnknownType(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetLabels(map[string]string{typeLabel: "unknownType"}) + + checkMap := map[string]checks.Check{ + "testType": &mockCheck{}, + } + + _, err := getCheck(obj, checkMap) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown check type unknownType") +} + +func TestSetStatusAnnotation(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + client := &mockClient{} + ctx := context.TODO() + + err := setStatusAnnotation(ctx, client, obj, "processed") + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) +} + +func TestProcessCheck(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + check := &mockCheck{} + + err = processCheck(ctx, client, obj, check) + assert.NoError(t, err) + assert.Equal(t, "processed", obj.GetAnnotations()[statusAnnotation]) +} + +func TestProcessCheck_AlreadyProcessed(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{statusAnnotation: "processed"}) + client := &mockClient{} + ctx := context.TODO() + check := &mockCheck{} + + err := processCheck(ctx, client, obj, check) + assert.NoError(t, err) +} + +func TestProcessCheck_RunError(t *testing.T) { + obj := &advisorv0alpha1.Check{} + obj.SetAnnotations(map[string]string{}) + meta, err := utils.MetaAccessor(obj) + if err != nil { + t.Fatal(err) + } + meta.SetCreatedBy("user:1") + client := &mockClient{} + ctx := context.TODO() + + check := &mockCheck{ + err: errors.New("run error"), + } + + err = processCheck(ctx, client, obj, check) + assert.Error(t, err) + assert.Equal(t, "error", obj.GetAnnotations()[statusAnnotation]) +} + +type mockClient struct { + resource.Client +} + +func (m *mockClient) PatchInto(ctx context.Context, id resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions, obj resource.Object) error { + return nil +} + +type mockCheck struct { + checks.Check + err error +} + +func (m *mockCheck) Run(ctx context.Context, spec *advisorv0alpha1.CheckSpec) (*advisorv0alpha1.CheckV0alpha1StatusReport, error) { + return &advisorv0alpha1.CheckV0alpha1StatusReport{}, m.err +} diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index 1ef99435336..a6916eb1f83 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -149,6 +149,10 @@ The **Page size** option lets you paginate the state timeline visualization to l {{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" leveloffset="+1" >}} +### Axis options + +{{< docs/shared lookup="visualizations/axis-options-state-status.md" source="grafana" version="" leveloffset="+1" >}} + ### Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/status-history/index.md b/docs/sources/panels-visualizations/visualizations/status-history/index.md index b7825b94b82..3ca5df864e6 100644 --- a/docs/sources/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md @@ -131,6 +131,10 @@ Controls the opacity of state regions. {{< docs/shared lookup="visualizations/tooltip-options-1.md" source="grafana" version="" >}} +## Axis options + +{{< docs/shared lookup="visualizations/axis-options-state-status.md" source="grafana" version="" leveloffset="+1" >}} + ## Standard options {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index e7f21000554..e54509d5aeb 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -209,21 +209,7 @@ The following example shows three series: Min, Max, and Value. The Min and Max s ### Axis options -Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`. - -| Option | Description | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Time zone | Set the desired time zones to display along the x-axis. | -| [Placement](#placement) | Select the placement of the y-axis. | -| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. | -| Width | Set a fixed width of the axis. By default, Grafana dynamically calculates the width of an axis. By setting the width of the axis, data with different axes types can share the same display proportions. This setting makes it easier for you to compare more than one graph’s worth of data because the axes aren't shifted or stretched within visual proximity to each other. | -| Show grid lines | Set the axis grid line visibility.
| -| Color | Set the color of the axis. | -| Show border | Set the axis border visibility. | -| Scale | Set the y-axis values scale.
| -| Centered zero | Set the y-axis so it's centered on zero. | -| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. | -| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. | +{{< docs/shared lookup="visualizations/axis-options-all.md" source="grafana" version="" leveloffset="+1" >}} #### Placement diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 2bd6d4eef14..435c59f79ef 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -129,113 +129,111 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- [Experimental](https://grafana.com/docs/release-life-cycle/#experimental) features are early in their development lifecycle and so are not yet supported in Grafana Cloud. Experimental features might be changed or removed without prior notice. -| Feature toggle name | Description | -| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | -| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | -| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | -| `storage` | Configurable storage for dashboards, datasources, and resources | -| `canvasPanelNesting` | Allow elements nesting | -| `vizActions` | Allow actions in visualizations | -| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | -| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | -| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | -| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | -| `alertingBacktesting` | Rule backtesting API for alerting | -| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | -| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | -| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | -| `individualCookiePreferences` | Support overriding cookie preferences per user | -| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | -| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | -| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | -| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | -| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | -| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | -| `extraThemes` | Enables extra themes | -| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | -| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | -| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | -| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | -| `mlExpressions` | Enable support for Machine Learning in server-side expressions | -| `metricsSummary` | Enables metrics summary queries in the Tempo data source | -| `datasourceAPIServers` | Expose some datasources as apiservers. | -| `provisioning` | Next generation provisioning... and git | -| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | -| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | -| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | -| `libraryPanelRBAC` | Enables RBAC support for library panels | -| `wargamesTesting` | Placeholder feature flag for internal testing | -| `externalCorePlugins` | Allow core plugins to be loaded as external | -| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | -| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | -| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | -| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | -| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | -| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally | -| `kubernetesRestore` | Allow restoring objects in k8s | -| `kubernetesFolders` | Use the kubernetes API in the frontend for folders, and route /api/folders requests to k8s | -| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s | -| `grafanaAPIServerTestingWithExperimentalAPIs` | Facilitate integration testing of experimental APIs | -| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | -| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | -| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | -| `queryServiceFromUI` | Routes requests to the new query service | -| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | -| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | -| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | -| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | -| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | -| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | -| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | -| `dashboardNewLayouts` | Enables experimental new dashboard layouts | -| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | -| `tableSharedCrosshair` | Enables shared crosshair in table panel | -| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | -| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | -| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | -| `scopeApi` | In-development feature flag for the scope api using the app platform. | -| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | -| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | -| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | -| `expressionParser` | Enable new expression parser | -| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | -| `queryLibrary` | Enables Query Library feature in Explore | -| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | -| `alertingListViewV2` | Enables the new alert list view design | -| `dashboardRestore` | Enables deleted dashboard restore feature | -| `alertingCentralAlertHistory` | Enables the new central alert history. | -| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters | -| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | -| `dataplaneAggregator` | Enable grafana dataplane aggregator | -| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | -| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | -| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | -| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | -| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | -| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | -| `rolePickerDrawer` | Enables the new role picker drawer design | -| `pluginsSriChecks` | Enables SRI checks for plugin assets | -| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | -| `timeRangeProvider` | Enables time pickers sync | -| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | -| `playlistsReconciler` | Enables experimental reconciler for playlists | -| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | -| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | -| `enableSCIM` | Enables SCIM support for user and group management | -| `crashDetection` | Enables browser crash detection reporting to Faro. | -| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | -| `useV2DashboardsAPI` | Use the v2 kubernetes API in the frontend for dashboards | -| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | -| `investigationsBackend` | Enable the investigations backend API | -| `k8SFolderCounts` | Enable folder's api server counts | -| `k8SFolderMove` | Enable folder's api server move | -| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | -| `queryLibraryDashboards` | Enables Query Library feature in Dashboards | -| `grafanaAdvisor` | Enables Advisor app | -| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | -| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | +| Feature toggle name | Description | +| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | +| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | +| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | +| `storage` | Configurable storage for dashboards, datasources, and resources | +| `canvasPanelNesting` | Allow elements nesting | +| `vizActions` | Allow actions in visualizations | +| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | +| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | +| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | +| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query | +| `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | +| `lokiShardSplitting` | Use stream shards to split queries into smaller subqueries | +| `lokiQuerySplittingConfig` | Give users the option to configure split durations for Loki queries | +| `individualCookiePreferences` | Support overriding cookie preferences per user | +| `influxqlStreamingParser` | Enable streaming JSON parser for InfluxDB datasource InfluxQL query language | +| `lokiLogsDataplane` | Changes logs responses from Loki to be compliant with the dataplane specification. | +| `disableSSEDataplane` | Disables dataplane specific processing in server side expressions. | +| `alertStateHistoryLokiSecondary` | Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations. | +| `alertStateHistoryLokiPrimary` | Enable a remote Loki instance as the primary source for state history reads. | +| `alertStateHistoryLokiOnly` | Disable Grafana alerts from emitting annotations when a remote Loki instance is available. | +| `extraThemes` | Enables extra themes | +| `lokiPredefinedOperations` | Adds predefined query operations to Loki query editor | +| `frontendSandboxMonitorOnly` | Enables monitor only in the plugin frontend sandbox (if enabled) | +| `pluginsDetailsRightPanel` | Enables right panel for the plugins details page | +| `awsDatasourcesTempCredentials` | Support temporary security credentials in AWS plugins for Grafana Cloud customers | +| `mlExpressions` | Enable support for Machine Learning in server-side expressions | +| `metricsSummary` | Enables metrics summary queries in the Tempo data source | +| `datasourceAPIServers` | Expose some datasources as apiservers. | +| `provisioning` | Next generation provisioning... and git | +| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | +| `aiGeneratedDashboardChanges` | Enable AI powered features for dashboards to auto-summary changes when saving | +| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch. | +| `libraryPanelRBAC` | Enables RBAC support for library panels | +| `wargamesTesting` | Placeholder feature flag for internal testing | +| `externalCorePlugins` | Allow core plugins to be loaded as external | +| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins | +| `enableNativeHTTPHistogram` | Enables native HTTP Histograms | +| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) | +| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint | +| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | +| `kubernetesCliDashboards` | Use the k8s client to retrieve dashboards internally | +| `kubernetesRestore` | Allow restoring objects in k8s | +| `kubernetesFoldersServiceV2` | Use the Folders Service V2, and route Folder Service requests to k8s | +| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) | +| `queryService` | Register /apis/query.grafana.app/ -- will eventually replace /api/ds/query | +| `queryServiceRewrite` | Rewrite requests targeting /ds/query to the query service | +| `queryServiceFromUI` | Routes requests to the new query service | +| `cachingOptimizeSerializationMemoryUsage` | If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses. | +| `prometheusPromQAIL` | Prometheus and AI/ML to assist users in creating a query | +| `prometheusCodeModeMetricNamesSearch` | Enables search for metric names in Code Mode, to improve performance when working with an enormous number of metric names | +| `alertmanagerRemoteSecondary` | Enable Grafana to sync configuration and state with a remote Alertmanager. | +| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. | +| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. | +| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | +| `dashboardNewLayouts` | Enables experimental new dashboard layouts | +| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | +| `tableSharedCrosshair` | Enables shared crosshair in table panel | +| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend | +| `newFolderPicker` | Enables the nested folder picker without having nested folders enabled | +| `onPremToCloudMigrationsAuthApiMig` | Enables the use of auth api instead of gcom for internal token services. Requires `onPremToCloudMigrations` to be enabled in conjunction. | +| `scopeApi` | In-development feature flag for the scope api using the app platform. | +| `sqlExpressions` | Enables using SQL and DuckDB functions as Expressions. | +| `nodeGraphDotLayout` | Changed the layout algorithm for the node graph | +| `kubernetesAggregator` | Enable grafana's embedded kube-aggregator | +| `expressionParser` | Enable new expression parser | +| `disableNumericMetricsSortingInExpressions` | In server-side expressions, disable the sorting of numeric-kind metrics by their metric name or labels. | +| `queryLibrary` | Enables Query Library feature in Explore | +| `logsExploreTableDefaultVisualization` | Sets the logs table as default visualisation in logs explore | +| `alertingListViewV2` | Enables the new alert list view design | +| `dashboardRestore` | Enables deleted dashboard restore feature | +| `alertingCentralAlertHistory` | Enables the new central alert history. | +| `sqlQuerybuilderFunctionParameters` | Enables SQL query builder function parameters | +| `failWrongDSUID` | Throws an error if a datasource has an invalid UIDs | +| `dataplaneAggregator` | Enable grafana dataplane aggregator | +| `lokiSendDashboardPanelNames` | Send dashboard and panel names to Loki when querying | +| `alertingPrometheusRulesPrimary` | Uses Prometheus rules as the primary source of truth for ruler-enabled data sources | +| `exploreLogsShardSplitting` | Used in Explore Logs to split queries into multiple queries based on the number of shards | +| `exploreLogsAggregatedMetrics` | Used in Explore Logs to query by aggregated metrics | +| `exploreLogsLimitedTimeRange` | Used in Explore Logs to limit the time range | +| `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | +| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | +| `rolePickerDrawer` | Enables the new role picker drawer design | +| `pluginsSriChecks` | Enables SRI checks for plugin assets | +| `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | +| `timeRangeProvider` | Enables time pickers sync | +| `prometheusUsesCombobox` | Use new combobox component for Prometheus query editor | +| `playlistsReconciler` | Enables experimental reconciler for playlists | +| `prometheusSpecialCharsInLabelValues` | Adds support for quotes and special characters in label values for Prometheus queries | +| `enableExtensionsAdminPage` | Enables the extension admin page regardless of development mode | +| `enableSCIM` | Enables SCIM support for user and group management | +| `crashDetection` | Enables browser crash detection reporting to Faro. | +| `jaegerBackendMigration` | Enables querying the Jaeger data source without the proxy | +| `useV2DashboardsAPI` | Use the v2 kubernetes API in the frontend for dashboards | +| `unifiedHistory` | Displays the navigation history so the user can navigate back to previous pages | +| `investigationsBackend` | Enable the investigations backend API | +| `k8SFolderCounts` | Enable folder's api server counts | +| `k8SFolderMove` | Enable folder's api server move | +| `teamHttpHeadersMimir` | Enables LBAC for datasources for Mimir to apply LBAC filtering of metrics to the client requests for users in teams | +| `queryLibraryDashboards` | Enables Query Library feature in Dashboards | +| `grafanaAdvisor` | Enables Advisor app | +| `elasticsearchImprovedParsing` | Enables less memory intensive Elasticsearch result parsing | +| `datasourceConnectionsTab` | Shows defined connections for a data source in the plugins detail page | ## Development feature toggles diff --git a/docs/sources/shared/visualizations/axis-options-all.md b/docs/sources/shared/visualizations/axis-options-all.md new file mode 100644 index 00000000000..e2c833ab9e2 --- /dev/null +++ b/docs/sources/shared/visualizations/axis-options-all.md @@ -0,0 +1,21 @@ +--- +title: Axis options +comments: | + This file is used in the following visualizations: time series. +--- + +Options under the **Axis** section control how the x- and y-axes are rendered. Some options don't take effect until you click outside of the field option box you're editing. You can also press `Enter`. + +| Option | Description | +| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| Time zone | Set the desired time zones to display along the x-axis. | +| [Placement](#placement) | Select the placement of the y-axis. | +| Label | Set a y-axis text label. If you have more than one y-axis, then you can assign different labels using an override. | +| Width | Set a fixed width for the axis. By default, Grafana dynamically calculates the width of an axis. | +| Show grid lines | Set the axis grid line visibility.
| +| Color | Set the color of the axis. | +| Show border | Set the axis border visibility. | +| Scale | Set the y-axis values scale.
| +| Centered zero | Set the y-axis so it's centered on zero. | +| [Soft min](#soft-min-and-soft-max) | Set a soft min to better control the y-axis limits. zero. | +| [Soft max](#soft-min-and-soft-max) | Set a soft max to better control the y-axis limits. zero. | diff --git a/docs/sources/shared/visualizations/axis-options-state-status.md b/docs/sources/shared/visualizations/axis-options-state-status.md new file mode 100644 index 00000000000..8b1bacfd78c --- /dev/null +++ b/docs/sources/shared/visualizations/axis-options-state-status.md @@ -0,0 +1,10 @@ +--- +title: Axis options +comments: | + This file is used in the following visualizations: state timeline, status history. +--- + +| Option | Description | +| --------- | ------------------------------------------------------------------------------------------------ | +| Placement | Control the visibility of series names along the y-axis or time values along the x-axis. | +| Width | Set a fixed width for the axis. By default, Grafana dynamically calculates the width of an axis. | diff --git a/go.mod b/go.mod index e96a3f30fd1..0019d26e009 100644 --- a/go.mod +++ b/go.mod @@ -71,8 +71,7 @@ require ( github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 // @grafana/alerting-backend - github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team - github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 // @grafana/identity-access-team + github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 // @grafana/identity-access-team github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c // @grafana/identity-access-team github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 // @grafana/grafana-app-platform-squad github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics @@ -201,7 +200,7 @@ require ( ) require ( - github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f // @grafana/plugins-platform-backend + github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 // @grafana/plugins-platform-backend github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d // @grafana/alerting-backend github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d // @fcjack @matryer github.com/grafana/grafana/apps/playlist v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad @@ -212,7 +211,7 @@ require ( // This needs to be here for other projects that import grafana/grafana // For local development grafana/grafana will always use the local files // Check go.work file for details - github.com/grafana/grafana/pkg/promlib v0.0.7 // @grafana/oss-big-tent + github.com/grafana/grafana/pkg/promlib v0.0.8 // @grafana/oss-big-tent github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250121113133-e747350fee2d // @grafana/grafana-search-and-storage diff --git a/go.sum b/go.sum index c06f8cbfc3a..3290de746d4 100644 --- a/go.sum +++ b/go.sum @@ -1502,10 +1502,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c h1:duHQ8Bih3nt9p68aQdp+22a6mFBLpK4IOURhEq+Cvk8= -github.com/grafana/authlib v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= -github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk= -github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901 h1:nqV1YrtX+ZG+EYB5dcmFMWhg2Y038OMaAHAADbOC9RA= +github.com/grafana/authlib v0.0.0-20250123104008-e99947858901/go.mod h1:/gYfphsNu9v1qYWXxpv1NSvMEMSwvdf8qb8YlgwIRl8= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c h1:b0sPDtt33uFdmvUJjSCld3kwE2E49dUvevuUDSJsEuo= github.com/grafana/authlib/types v0.0.0-20250120145936-5f0e28e7a87c/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 h1:pIEPQPua6kCkMib/nOn//WLgtiA4urf2RPBViSF6CcE= @@ -1540,8 +1538,8 @@ github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.262.0 h1:R2DV6lwBQE5zaogxX3PorD9Seo8CXA8YuStf84oqwkk= github.com/grafana/grafana-plugin-sdk-go v0.262.0/go.mod h1:U43Cnrj/9DNYyvFcNdeUWNjMXTKNB0jcTcQGpWKd2gw= -github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f h1:c8IkbxPvM6+lscVOLgtbt8Gnro4Liltd+E2eqkoAeZA= -github.com/grafana/grafana/apps/advisor v0.0.0-20250121115006-c1eac9f9973f/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173 h1:uOM89HiWVVOTls0LrD4coHTckb2lA4U0sIJwCYdbhbw= +github.com/grafana/grafana/apps/advisor v0.0.0-20250123151950-b066a6313173/go.mod h1:goSDiy3jtC2cp8wjpPZdUHRENcoSUHae1/Px/MDfddA= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d h1:NRVOtiG1aUwOazBj9KM7X2o2shsM6TchqisezzoH1gw= github.com/grafana/grafana/apps/alerting/notifications v0.0.0-20250121113133-e747350fee2d/go.mod h1:AvleS6icyPmcBjihtx5jYEvdzLmHGBp66NuE0AMR57A= github.com/grafana/grafana/apps/investigation v0.0.0-20250121113133-e747350fee2d h1:oNc/aDfDucQxLbRZK25yz3Cwc+dGo1C0Xmm2LaliWUQ= @@ -1554,8 +1552,8 @@ github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d h github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250121113133-e747350fee2d/go.mod h1:PNmbi49lVrv2b2I8pdu46dTs2728lKEbnVuQD8I5MnM= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d h1:vVaLOibSj5lDwCb+pa0fly9uods8nTq1NJPYrTuPAGE= github.com/grafana/grafana/pkg/apiserver v0.0.0-20250121113133-e747350fee2d/go.mod h1:srJ6H0DL7c9Cz4CmK0h4AOlC1sm8UDwn9jBZ+RHxBt4= -github.com/grafana/grafana/pkg/promlib v0.0.7 h1:BdpanKOKnID/l1BJZLhE7TRNtmq7aOVdou1LBFWaMmU= -github.com/grafana/grafana/pkg/promlib v0.0.7/go.mod h1:rnwJXCA2xRwb7F27NB35iO/JsLL/H/+eVXECk/hrEhQ= +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-20250121113133-e747350fee2d h1:cYBjuhb3m5oC6Z00Kw8DdySFaNhwb38SMxx0oXnz5vQ= github.com/grafana/grafana/pkg/semconv v0.0.0-20250121113133-e747350fee2d/go.mod h1:tfLnBpPYgwrBMRz4EXqPCZJyCjEG4Ev37FSlXnocJ2c= github.com/grafana/grafana/pkg/storage/unified/apistore v0.0.0-20250121113133-e747350fee2d h1:4w4x8g6xCSRj06er81atNSQVL0fTSwLekDYsRxpz+M0= diff --git a/go.work.sum b/go.work.sum index fcfdff52765..d3788a596af 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1455,6 +1455,8 @@ github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9 github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/cel-go v0.22.0/go.mod h1:BuznPXXfQDpXKWQ9sPW3TzlAJN5zzFe+i9tIs0yC4s8= github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= github.com/google/go-jsonnet v0.18.0 h1:/6pTy6g+Jh1a1I2UMoAODkqELFiVIdOxbNwv0DDzoOg= github.com/google/go-jsonnet v0.18.0/go.mod h1:C3fTzyVJDslXdiTqw/bTFk7vSGyCtH3MGRbDfvEwGd0= github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= @@ -1497,17 +1499,19 @@ github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4 h1:616HUg7WVyLJLtVvk2pZ845M4Gk0fswgTwEZSuAZbJU= -github.com/grafana/alerting v0.0.0-20250117230852-a5e8136407d4/go.mod h1:QsnoKX/iYZxA4Cv+H+wC7uxutBD8qi8ZW5UJvD2TYmU= github.com/grafana/authlib v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:V63rh3udd7sqXJeaG+nGUmViwVnM/bY6t8U9Tols2GU= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335 h1:3DHH81RJCi8Bcgn2MdBh7vgWUshmAFjZzBCVuxiQ0uk= github.com/grafana/authlib/claims v0.0.0-20241202085737-df90af04f335/go.mod h1:r+F8H6awwjNQt/KPZ2GNwjk8TvsJ7/gxzkXN26GlL/A= github.com/grafana/authlib/types v0.0.0-20250120144156-d6737a7dc8f5/go.mod h1:qYjSd1tmJiuVoSICp7Py9/zD54O9uQQA3wuM6Gg4DFM= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2 h1:qhugDMdQ4Vp68H0tp/0iN17DM2ehRo1rLEdOFe/gB8I= github.com/grafana/cloudflare-go v0.0.0-20230110200409-c627cf6792f2/go.mod h1:w/aiO1POVIeXUQyl0VQSZjl5OAGDTL5aX+4v0RA1tcw= +github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752 h1:pIEPQPua6kCkMib/nOn//WLgtiA4urf2RPBViSF6CcE= +github.com/grafana/dashboard-linter v0.0.0-20241106223805-1e7999311752/go.mod h1:QRxC8NNAbpGx2vz9/XckJcWXtHU3My57byeudPrf0WU= github.com/grafana/go-gelf/v2 v2.0.1 h1:BOChP0h/jLeD+7F9mL7tq10xVkDG15he3T1zHuQaWak= github.com/grafana/go-gelf/v2 v2.0.1/go.mod h1:lexHie0xzYGwCgiRGcvZ723bSNyNI8ZRD4s0CLobh90= github.com/grafana/grafana-app-sdk v0.29.0/go.mod h1:XLt308EmK6kvqPlzjUyXxbwZKEk2vur/eiypUNDay5I= +github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70 h1:69GI3KsF851YnwYp6zHdsskcGp3ZnGsWc+ve8vMp1mc= +github.com/grafana/grafana-foundation-sdk/go v0.0.0-20241101005901-83e3491f2a70/go.mod h1:WtWosval1KCZP9BGa42b8aVoJmVXSg0EvQXi9LDSVZQ= github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0 h1:bjh0PVYSVVFxzINqPFYJmAmJNrWPgnVjuSdYJGHmtFU= github.com/grafana/tail v0.0.0-20230510142333-77b18831edf0/go.mod h1:7t5XR+2IA8P2qggOAHTj/GCZfoLBle3OvNSYh1VkRBU= diff --git a/package.json b/package.json index 712941452bd..f918c6bfae5 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@grafana/plugin-e2e": "^1.11.0", "@grafana/tsconfig": "^2.0.0", "@manypkg/get-packages": "^2.2.0", - "@playwright/test": "1.49.1", + "@playwright/test": "1.50.0", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "@react-types/button": "3.10.2", "@react-types/menu": "3.9.14", @@ -123,7 +123,7 @@ "@types/lodash": "4.17.14", "@types/logfmt": "^1.2.3", "@types/lucene": "^2", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/pluralize": "^0.0.33", @@ -228,7 +228,7 @@ "sass-loader": "16.0.4", "smtp-tester": "^2.1.0", "style-loader": "4.0.0", - "stylelint": "16.12.0", + "stylelint": "16.13.2", "stylelint-config-sass-guidelines": "12.1.0", "terser-webpack-plugin": "5.3.11", "testing-library-selector": "0.3.1", @@ -367,7 +367,7 @@ "react-draggable": "4.4.6", "react-dropzone": "^14.2.3", "react-grid-layout": "patch:react-grid-layout@npm%3A1.4.4#~/.yarn/patches/react-grid-layout-npm-1.4.4-4024c5395b.patch", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 06400ab720c..cecf1602206 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -65,7 +65,7 @@ "@rollup/plugin-node-resolve": "16.0.0", "@types/history": "4.7.11", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/papaparse": "5.3.15", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 0924a417231..7e9aa1052aa 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -501,6 +501,7 @@ export const getLinksSupplier = }); }, origin: field, + oneClick: link.oneClick ?? false, }; } else { linkModel = { @@ -508,6 +509,7 @@ export const getLinksSupplier = title: replaceVariables(link.title || '', dataLinkScopedVars), target: link.targetBlank ? '_blank' : undefined, origin: field, + oneClick: link.oneClick ?? false, }; } diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 476f2441ed7..14ee3ad4301 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -55,6 +55,8 @@ export interface DataLink { correlationData?: ExploreCorrelationHelperData; transformations?: DataLinkTransformationConfig[]; }; + + oneClick?: boolean; } /** @@ -98,6 +100,7 @@ export interface LinkModel { // When a click callback exists, this is passed the raw mouse|react event onClick?: (e: any, origin?: any) => void; + oneClick?: boolean; } /** diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e26b79a8056..97e532863af 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -112,9 +112,7 @@ export interface FeatureToggles { kubernetesDashboards?: boolean; kubernetesCliDashboards?: boolean; kubernetesRestore?: boolean; - kubernetesFolders?: boolean; kubernetesFoldersServiceV2?: boolean; - grafanaAPIServerTestingWithExperimentalAPIs?: boolean; datasourceQueryTypes?: boolean; queryService?: boolean; queryServiceRewrite?: boolean; diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 0b2c08dc592..0e70a96c6bd 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -40,7 +40,7 @@ }, "devDependencies": { "@rollup/plugin-node-resolve": "16.0.0", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/semver": "7.5.8", "esbuild": "0.24.2", "rimraf": "6.0.1", diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json index 9ff5b7b4f8b..1f3ca5b4913 100644 --- a/packages/grafana-flamegraph/package.json +++ b/packages/grafana-flamegraph/package.json @@ -68,7 +68,7 @@ "@types/d3": "^7", "@types/jest": "^29.5.4", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-virtualized-auto-sizer": "1.0.4", "@types/tinycolor2": "1.4.6", diff --git a/packages/grafana-icons/package.json b/packages/grafana-icons/package.json index d7b9d631c27..64befb9b038 100644 --- a/packages/grafana-icons/package.json +++ b/packages/grafana-icons/package.json @@ -45,7 +45,7 @@ "@svgr/plugin-prettier": "^8.1.0", "@svgr/plugin-svgo": "^8.1.0", "@types/babel__core": "^7", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "esbuild": "0.24.2", diff --git a/packages/grafana-o11y-ds-frontend/package.json b/packages/grafana-o11y-ds-frontend/package.json index 684b9aed7fc..933fa923886 100644 --- a/packages/grafana-o11y-ds-frontend/package.json +++ b/packages/grafana-o11y-ds-frontend/package.json @@ -36,7 +36,7 @@ "@testing-library/react": "16.1.0", "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/systemjs": "6.15.1", "jest": "^29.6.4", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index b2d54ad027b..80c1a9171e7 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -64,7 +64,7 @@ "monaco-promql": "1.7.4", "pluralize": "8.0.0", "prismjs": "1.29.0", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-select": "5.9.0", "react-use": "17.6.0", "react-window": "1.8.11", @@ -92,7 +92,7 @@ "@types/jest": "29.5.14", "@types/jquery": "3.5.32", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", diff --git a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts index 72c3097c014..a62b8181e42 100644 --- a/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statetimeline/panelcfg/x/StateTimelinePanelCfg_types.gen.ts @@ -43,7 +43,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts index 8b0b21a3723..1efeeb3dec4 100644 --- a/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts @@ -33,7 +33,7 @@ export const defaultOptions: Partial = { showValue: ui.VisibilityMode.Auto, }; -export interface FieldConfig extends ui.HideableFieldConfig { +export interface FieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { fillOpacity?: number; lineWidth?: number; } diff --git a/packages/grafana-sql/package.json b/packages/grafana-sql/package.json index cab5a2ac907..a6da2fa240c 100644 --- a/packages/grafana-sql/package.json +++ b/packages/grafana-sql/package.json @@ -41,7 +41,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "^29.5.4", "@types/lodash": "4.17.14", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/react": "18.3.18", "@types/react-dom": "18.3.5", "@types/react-virtualized-auto-sizer": "1.0.4", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 65f5cd996f3..1d2eb1a9bbb 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -92,7 +92,7 @@ "react-colorful": "5.6.1", "react-custom-scrollbars-2": "4.5.0", "react-dropzone": "14.3.5", - "react-highlight-words": "0.20.0", + "react-highlight-words": "0.21.0", "react-hook-form": "^7.49.2", "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", @@ -145,7 +145,7 @@ "@types/is-hotkey": "0.1.10", "@types/jest": "29.5.14", "@types/mock-raf": "1.0.6", - "@types/node": "22.10.7", + "@types/node": "22.10.9", "@types/prismjs": "1.26.5", "@types/react": "18.3.18", "@types/react-color": "3.0.13", diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx index a56cf853f1d..8099fb595d6 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.tsx @@ -74,11 +74,6 @@ export const MultiCombobox = (props: MultiComboboxPro return newItems; }, [baseItems, inputValue, enableAllOption, allOptionItem]); - const [isOpen, setIsOpen] = useState(false); - - const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); - - const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); const { measureRef, counterMeasureRef, suffixMeasureRef, shownItems } = useMeasureMulti( selectedItems, @@ -108,17 +103,41 @@ export const MultiCombobox = (props: MultiComboboxPro break; } }, + stateReducer: (state, actionAndChanges) => { + const { changes } = actionAndChanges; + return { + ...changes, + + /** + * TODO: Fix Hack! + * This prevents the menu from closing when the user unselects an item in the dropdown at the expense + * of breaking keyboard navigation. + * + * Downshift isn't really designed to keep selected items in the dropdown menu, so when you unselect an item + * in a multiselect, the stateReducer tries to move focus onto another item which causes the menu to be closed. + * This only seems to happen when you deselect the last item in the selectedItems list. + * + * Check out: + * - FunctionRemoveSelectedItem in the useMultipleSelection reducer https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/reducer.js#L75 + * - The activeIndex useEffect in useMultipleSelection https://github.com/downshift-js/downshift/blob/master/src/hooks/useMultipleSelection/index.js#L68-L72 + * + * Forcing the activeIndex to -999 both prevents the useEffect that changes the focus from triggering (value never changes) + * and prevents the if statement in useMultipleSelection from focusing anything. + */ + activeIndex: -999, + }; + }, }); const { //getToggleButtonProps, //getLabelProps, + isOpen, + highlightedIndex, getMenuProps, getInputProps, - highlightedIndex, getItemProps, } = useCombobox({ - isOpen, items, itemToString, inputValue, @@ -135,7 +154,6 @@ export const MultiCombobox = (props: MultiComboboxPro }; case useCombobox.stateChangeTypes.InputBlur: setInputValue(''); - setIsOpen(false); default: return changes; } @@ -175,14 +193,15 @@ export const MultiCombobox = (props: MultiComboboxPro case useCombobox.stateChangeTypes.InputChange: setInputValue(newInputValue ?? ''); break; - case useCombobox.stateChangeTypes.InputClick: - setIsOpen(true); default: break; } }, }); + const { inputRef: containerRef, floatingRef, floatStyles, scrollRef } = useComboboxFloat(items, isOpen); + const multiStyles = useStyles2(getMultiComboboxStyles, isOpen, invalid, disabled); + const virtualizerOptions = { count: items.length, getScrollElement: () => scrollRef.current, @@ -241,7 +260,6 @@ export const MultiCombobox = (props: MultiComboboxPro disabled, preventKeyAction: isOpen, placeholder: selectedItems.length > 0 ? undefined : placeholder, - onFocus: () => !disabled && setIsOpen(true), }) )} /> diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx index 98f5d67227f..cdd5e64eddf 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkEditor.tsx @@ -5,7 +5,7 @@ import { VariableSuggestion, GrafanaTheme2, DataLink } from '@grafana/data'; import { useStyles2 } from '../../themes/index'; import { isCompactUrl } from '../../utils/dataLinks'; -import { Trans } from '../../utils/i18n'; +import { t, Trans } from '../../utils/i18n'; import { Field } from '../Forms/Field'; import { Input } from '../Input/Input'; import { Switch } from '../Switch/Switch'; @@ -45,6 +45,10 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas onChange(index, { ...value, targetBlank: !value.targetBlank }); }; + const onOneClickChanged = () => { + onChange(index, { ...value, oneClick: !value.oneClick }); + }; + return (
@@ -63,6 +67,16 @@ export const DataLinkEditor = memo(({ index, value, onChange, suggestions, isLas + + + + {isLast && (
diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx index a431e241479..fa4e61589f3 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksInlineEditor.tsx @@ -51,6 +51,15 @@ export const DataLinksInlineEditor = ({ setIsNew(false); } } + + if (link.oneClick === true) { + linksSafe.forEach((link) => { + if (link.oneClick) { + link.oneClick = false; + } + }); + } + const update = cloneDeep(linksSafe); update[index] = link; onChange(update); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx index 939a4ec15b7..1c12a41dd2a 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksInlineEditor/DataLinksListItem.tsx @@ -5,6 +5,8 @@ import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../../themes'; import { isCompactUrl } from '../../../utils'; +import { t } from '../../../utils/i18n'; +import { Badge } from '../../Badge/Badge'; import { Icon } from '../../Icon/Icon'; import { IconButton } from '../../IconButton/IconButton'; import { Tooltip } from '../../Tooltip/Tooltip'; @@ -22,7 +24,7 @@ export interface DataLinksListItemProps { export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: DataLinksListItemProps) => { const styles = useStyles2(getDataLinkListItemStyles); - const { title = '', url = '' } = link; + const { title = '', url = '', oneClick = false } = link; const hasTitle = title.trim() !== ''; const hasUrl = url.trim() !== ''; @@ -52,6 +54,13 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da
+ {oneClick && ( + + )}
diff --git a/packages/grafana-ui/src/components/Input/Input.tsx b/packages/grafana-ui/src/components/Input/Input.tsx index 1240569cb57..09b113cf76b 100644 --- a/packages/grafana-ui/src/components/Input/Input.tsx +++ b/packages/grafana-ui/src/components/Input/Input.tsx @@ -64,12 +64,17 @@ export const Input = forwardRef((props, ref) => { const theme = useTheme2(); - const styles = getInputStyles({ theme, invalid: !!invalid, width: finalWidth }); + // Don't pass the width prop, as this causes an unnecessary amount of Emotion calls when auto sizing + const styles = getInputStyles({ theme, invalid: !!invalid }); const suffix = suffixProp || (loading && ); return ( -
+
{!!addonBefore &&
{addonBefore}
}
{prefix && ( @@ -125,7 +130,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: css({ label: 'input-wrapper', display: 'flex', - width: width ? theme.spacing(width) : '100%', + width: width ? theme.spacing(width) : '100%', // Not used in Input, as this causes performance issues with auto sizing height: theme.spacing(theme.components.height.md), borderRadius: theme.shape.radius.default, '&:hover': { diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index 05b05f7518d..c3c4a34eb64 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; -import { Button, DataLinkButton, Stack } from '..'; +import { Button, DataLinkButton, Icon, Stack } from '..'; import { useStyles2 } from '../../themes'; import { Trans } from '../../utils/i18n'; import { ActionButton } from '../Actions/ActionButton'; @@ -16,6 +16,21 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; const renderDataLinks = (dataLinks: LinkModel[], styles: ReturnType) => { + const oneClickLink = dataLinks.find((link) => link.oneClick === true); + + if (oneClickLink != null) { + return ( + + + + + Click to open {{ linkTitle: oneClickLink.title }} + + + + ); + } + return ( {dataLinks.map((link, i) => ( @@ -35,14 +50,15 @@ const renderActions = (actions: ActionModel[]) => { ); }; -export const VizTooltipFooter = ({ dataLinks, actions, annotate }: VizTooltipFooterProps) => { +export const VizTooltipFooter = ({ dataLinks, actions = [], annotate }: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); + const hasOneClickLink = dataLinks.some((link) => link.oneClick === true); return (
- {dataLinks?.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} - {actions && actions.length > 0 &&
{renderActions(actions)}
} - {annotate != null && ( + {dataLinks.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} + {!hasOneClickLink && actions.length > 0 &&
{renderActions(actions)}
} + {!hasOneClickLink && annotate != null && (