From a22c1ae4242134e73bae2dbe2230658d9becc50a Mon Sep 17 00:00:00 2001
From: Andres Martinez Gotor
Date: Mon, 1 Jul 2024 10:53:16 +0200
Subject: [PATCH 1/9] Chore: Remove provisional APIVersion from plugin info
(#89831)
---
.../developers/plugins/plugin.schema.json | 5 --
pkg/api/dtos/plugins.go | 2 -
pkg/api/plugins.go | 2 -
.../manager/pipeline/validation/steps.go | 35 ---------
.../manager/pipeline/validation/steps_test.go | 75 -------------------
pkg/plugins/plugins.go | 3 -
.../datasources/service/datasource.go | 9 +--
.../datasources/service/datasource_test.go | 28 +++----
.../pluginsintegration/pipeline/pipeline.go | 1 -
.../plugincontext/base_plugincontext.go | 1 -
.../plugincontext/plugincontext_test.go | 12 +--
.../api/plugins/data/expectedListResp.json | 3 +-
.../grafana-testdata-datasource/plugin.json | 1 -
13 files changed, 20 insertions(+), 157 deletions(-)
delete mode 100644 pkg/plugins/manager/pipeline/validation/steps_test.go
diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json
index 647a3fe485d..71e3b963214 100644
--- a/docs/sources/developers/plugins/plugin.schema.json
+++ b/docs/sources/developers/plugins/plugin.schema.json
@@ -582,11 +582,6 @@
},
"required": ["extensionPointId", "title", "type"]
}
- },
- "apiVersion": {
- "type": "string",
- "description": "[internal only] The API version for the plugin. Used for Datasource API servers. This metadata is temporary and will be removed in the future.",
- "pattern": "^v([\\d]+)(?:(alpha|beta)([\\d]+))?$"
}
}
}
diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go
index 1d377476174..7d768e059e4 100644
--- a/pkg/api/dtos/plugins.go
+++ b/pkg/api/dtos/plugins.go
@@ -28,7 +28,6 @@ type PluginSetting struct {
SignatureType plugins.SignatureType `json:"signatureType"`
SignatureOrg string `json:"signatureOrg"`
AngularDetected bool `json:"angularDetected"`
- APIVersion string `json:"apiVersion"`
}
type PluginListItem struct {
@@ -50,7 +49,6 @@ type PluginListItem struct {
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
AngularDetected bool `json:"angularDetected"`
IAM *pfs.IAM `json:"iam,omitempty"`
- APIVersion string `json:"apiVersion"`
}
type PluginList []PluginListItem
diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go
index 739141e78a3..ffe15c59357 100644
--- a/pkg/api/plugins.go
+++ b/pkg/api/plugins.go
@@ -142,7 +142,6 @@ func (hs *HTTPServer) GetPluginList(c *contextmodel.ReqContext) response.Respons
SignatureOrg: pluginDef.SignatureOrg,
AccessControl: pluginsMetadata[pluginDef.ID],
AngularDetected: pluginDef.Angular.Detected,
- APIVersion: pluginDef.APIVersion,
}
if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagExternalServiceAccounts) {
@@ -209,7 +208,6 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
SignatureOrg: plugin.SignatureOrg,
SecureJsonFields: map[string]bool{},
AngularDetected: plugin.Angular.Detected,
- APIVersion: plugin.APIVersion,
}
if plugin.IsApp() {
diff --git a/pkg/plugins/manager/pipeline/validation/steps.go b/pkg/plugins/manager/pipeline/validation/steps.go
index faf436c8798..10af0cce5e4 100644
--- a/pkg/plugins/manager/pipeline/validation/steps.go
+++ b/pkg/plugins/manager/pipeline/validation/steps.go
@@ -3,8 +3,6 @@ package validation
import (
"context"
"errors"
- "fmt"
- "regexp"
"slices"
"time"
@@ -117,36 +115,3 @@ func (a *AngularDetector) Validate(ctx context.Context, p *plugins.Plugin) error
p.Angular.HideDeprecation = slices.Contains(a.cfg.HideAngularDeprecation, p.ID)
return nil
}
-
-// APIVersionValidation implements a ValidateFunc for validating plugin API versions.
-type APIVersionValidation struct {
-}
-
-// APIVersionValidationStep returns a new ValidateFunc for validating plugin signatures.
-func APIVersionValidationStep() ValidateFunc {
- sv := &APIVersionValidation{}
- return sv.Validate
-}
-
-// Validate validates the plugin signature. If a signature error is encountered, the error is recorded with the
-// pluginerrs.ErrorTracker.
-func (v *APIVersionValidation) Validate(ctx context.Context, p *plugins.Plugin) error {
- if p.APIVersion != "" {
- if !p.Backend {
- return fmt.Errorf("plugin %s has an API version but is not a backend plugin", p.ID)
- }
- // Eventually, all backend plugins will be supported
- if p.Type != plugins.TypeDataSource {
- return fmt.Errorf("plugin %s has an API version but is not a datasource plugin", p.ID)
- }
- m, err := regexp.MatchString(`^v([\d]+)(?:(alpha|beta)([\d]+))?$`, p.APIVersion)
- if err != nil {
- return fmt.Errorf("failed to verify apiVersion %s: %v", p.APIVersion, err)
- }
- if !m {
- return fmt.Errorf("plugin %s has an invalid API version %s", p.ID, p.APIVersion)
- }
- }
-
- return nil
-}
diff --git a/pkg/plugins/manager/pipeline/validation/steps_test.go b/pkg/plugins/manager/pipeline/validation/steps_test.go
deleted file mode 100644
index 0eaabda84ee..00000000000
--- a/pkg/plugins/manager/pipeline/validation/steps_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-package validation
-
-import (
- "context"
- "testing"
-
- "github.com/grafana/grafana/pkg/plugins"
- "github.com/stretchr/testify/require"
-)
-
-func TestAPIVersionValidation(t *testing.T) {
- s := APIVersionValidationStep()
-
- tests := []struct {
- name string
- plugin *plugins.Plugin
- err bool
- }{
- {
- name: "valid plugin",
- plugin: &plugins.Plugin{
- JSONData: plugins.JSONData{
- Backend: true,
- Type: plugins.TypeDataSource,
- APIVersion: "v0alpha1",
- },
- },
- err: false,
- },
- {
- name: "invalid plugin - not backend",
- plugin: &plugins.Plugin{
- JSONData: plugins.JSONData{
- Backend: false,
- Type: plugins.TypeDataSource,
- APIVersion: "v0alpha1",
- },
- },
- err: true,
- },
- {
- name: "invalid plugin - not datasource",
- plugin: &plugins.Plugin{
- JSONData: plugins.JSONData{
- Backend: true,
- Type: plugins.TypeApp,
- APIVersion: "v0alpha1",
- },
- },
- err: true,
- },
- {
- name: "invalid plugin - invalid API version",
- plugin: &plugins.Plugin{
- JSONData: plugins.JSONData{
- Backend: true,
- Type: plugins.TypeDataSource,
- APIVersion: "invalid",
- },
- },
- err: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- err := s(context.Background(), tt.plugin)
- if tt.err {
- require.Error(t, err)
- } else {
- require.NoError(t, err)
- }
- })
- }
-}
diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go
index d96a09e0963..33c001ca626 100644
--- a/pkg/plugins/plugins.go
+++ b/pkg/plugins/plugins.go
@@ -128,9 +128,6 @@ type JSONData struct {
// App Service Auth Registration
IAM *pfs.IAM `json:"iam,omitempty"`
-
- // API Version: Temporary field while plugins don't expose a OpenAPI schema
- APIVersion string `json:"apiVersion,omitempty"`
}
func ReadPluginJSON(reader io.Reader) (JSONData, error) {
diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go
index 3e4fd62fea7..e731776656c 100644
--- a/pkg/services/datasources/service/datasource.go
+++ b/pkg/services/datasources/service/datasource.go
@@ -322,10 +322,7 @@ func (s *Service) prepareInstanceSettings(ctx context.Context, settings *backend
}
// When the APIVersion is set, the client must also implement AdmissionHandler
- if p.APIVersion == "" {
- if settings.APIVersion != "" {
- return nil, fmt.Errorf("invalid request apiVersion (datasource does not have one configured)")
- }
+ if settings.APIVersion == "" {
return settings, nil // NOOP
}
@@ -367,7 +364,7 @@ func (s *Service) prepareInstanceSettings(ctx context.Context, settings *backend
if err != nil {
if errors.Is(err, plugins.ErrMethodNotImplemented) {
return nil, errutil.Internal("plugin.unimplemented").
- Errorf("plugin (%s) with apiVersion=%s must implement ValidateAdmission", p.ID, p.APIVersion)
+ Errorf("plugin (%s) with apiVersion=%s must implement ValidateAdmission", p.ID, settings.APIVersion)
}
return nil, err
}
@@ -388,7 +385,7 @@ func (s *Service) prepareInstanceSettings(ctx context.Context, settings *backend
if err != nil {
if errors.Is(err, plugins.ErrMethodNotImplemented) {
return nil, errutil.Internal("plugin.unimplemented").
- Errorf("plugin (%s) with apiVersion=%s must implement MutateAdmission", p.ID, p.APIVersion)
+ Errorf("plugin (%s) with apiVersion=%s must implement MutateAdmission", p.ID, settings.APIVersion)
}
return nil, err
}
diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go
index 0fa7457351b..a4c6afdc2db 100644
--- a/pkg/services/datasources/service/datasource_test.go
+++ b/pkg/services/datasources/service/datasource_test.go
@@ -110,10 +110,9 @@ func TestService_AddDataSource(t *testing.T) {
dsService.pluginStore = &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{
- ID: "test",
- Type: plugins.TypeDataSource,
- Name: "test",
- APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed
+ ID: "test",
+ Type: plugins.TypeDataSource,
+ Name: "test",
},
}},
}
@@ -150,10 +149,9 @@ func TestService_AddDataSource(t *testing.T) {
dsService.pluginStore = &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{
- ID: "test",
- Type: plugins.TypeDataSource,
- Name: "test",
- APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed
+ ID: "test",
+ Type: plugins.TypeDataSource,
+ Name: "test",
},
}},
}
@@ -200,10 +198,9 @@ func TestService_AddDataSource(t *testing.T) {
dsService.pluginStore = &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{
- ID: "test",
- Type: plugins.TypeDataSource,
- Name: "test",
- APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed
+ ID: "test",
+ Type: plugins.TypeDataSource,
+ Name: "test",
},
}},
}
@@ -491,10 +488,9 @@ func TestService_UpdateDataSource(t *testing.T) {
dsService.pluginStore = &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{{
JSONData: plugins.JSONData{
- ID: "test",
- Type: plugins.TypeDataSource,
- Name: "test",
- APIVersion: "v0alpha1", // When a value exists in plugin.json, the callbacks will be executed
+ ID: "test",
+ Type: plugins.TypeDataSource,
+ Name: "test",
},
}},
}
diff --git a/pkg/services/pluginsintegration/pipeline/pipeline.go b/pkg/services/pluginsintegration/pipeline/pipeline.go
index 749ccd3d0c1..f45cb3bfd76 100644
--- a/pkg/services/pluginsintegration/pipeline/pipeline.go
+++ b/pkg/services/pluginsintegration/pipeline/pipeline.go
@@ -54,7 +54,6 @@ func ProvideValidationStage(cfg *config.PluginManagementCfg, sv signature.Valida
SignatureValidationStep(sv),
validation.ModuleJSValidationStep(),
validation.AngularDetectionStep(cfg, ai),
- validation.APIVersionValidationStep(),
},
})
}
diff --git a/pkg/services/pluginsintegration/plugincontext/base_plugincontext.go b/pkg/services/pluginsintegration/plugincontext/base_plugincontext.go
index 48c179bb29d..3abe9d92a9b 100644
--- a/pkg/services/pluginsintegration/plugincontext/base_plugincontext.go
+++ b/pkg/services/pluginsintegration/plugincontext/base_plugincontext.go
@@ -43,7 +43,6 @@ func (p *BaseProvider) GetBasePluginContext(ctx context.Context, plugin pluginst
pCtx := backend.PluginContext{
PluginID: plugin.ID,
PluginVersion: plugin.Info.Version,
- APIVersion: plugin.APIVersion,
}
if user != nil && !user.IsNil() {
pCtx.OrgID = user.GetOrgID()
diff --git a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go
index 3aa3709295f..feec2e53db0 100644
--- a/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go
+++ b/pkg/services/pluginsintegration/plugincontext/plugincontext_test.go
@@ -26,17 +26,15 @@ import (
func TestGet(t *testing.T) {
const (
- pluginID = "plugin-id"
- alias = "alias"
- apiVersion = "v0alpha1"
+ pluginID = "plugin-id"
+ alias = "alias"
)
preg := registry.NewInMemory()
require.NoError(t, preg.Add(context.Background(), &plugins.Plugin{
JSONData: plugins.JSONData{
- ID: pluginID,
- AliasIDs: []string{alias},
- APIVersion: apiVersion,
+ ID: pluginID,
+ AliasIDs: []string{alias},
},
}))
@@ -61,7 +59,6 @@ func TestGet(t *testing.T) {
pCtx, err := pcp.Get(context.Background(), tc.input, identity, identity.OrgID)
require.NoError(t, err)
require.Equal(t, pluginID, pCtx.PluginID)
- require.Equal(t, apiVersion, pCtx.APIVersion)
require.NotNil(t, pCtx.GrafanaConfig)
})
@@ -75,7 +72,6 @@ func TestGet(t *testing.T) {
})
require.NoError(t, err)
require.Equal(t, pluginID, pCtx.PluginID)
- require.Equal(t, apiVersion, pCtx.APIVersion)
require.NotNil(t, pCtx.GrafanaConfig)
})
})
diff --git a/pkg/tests/api/plugins/data/expectedListResp.json b/pkg/tests/api/plugins/data/expectedListResp.json
index 3985342cb8c..26e4bb233a9 100644
--- a/pkg/tests/api/plugins/data/expectedListResp.json
+++ b/pkg/tests/api/plugins/data/expectedListResp.json
@@ -1671,8 +1671,7 @@
"signature": "internal",
"signatureType": "",
"signatureOrg": "",
- "angularDetected": false,
- "apiVersion": "v0alpha1"
+ "angularDetected": false
},
{
"name": "Text",
diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/plugin.json b/public/app/plugins/datasource/grafana-testdata-datasource/plugin.json
index 55807a26991..903222c9fef 100644
--- a/public/app/plugins/datasource/grafana-testdata-datasource/plugin.json
+++ b/public/app/plugins/datasource/grafana-testdata-datasource/plugin.json
@@ -10,7 +10,6 @@
"alerting": true,
"annotations": true,
"backend": true,
- "apiVersion": "v0alpha1",
"queryOptions": {
"minInterval": true,
From c168f2acec6996ef6c86c45c4e906a08187b8eac Mon Sep 17 00:00:00 2001
From: Pepe Cano <825430+ppcano@users.noreply.github.com>
Date: Mon, 1 Jul 2024 11:35:12 +0200
Subject: [PATCH 2/9] Alerting docs: Update `View alert groups` (#89461)
* Alerting docs: Update `View alert groups`
Rename to `View the status of notifications` and extend on this topic
* Update docs/sources/alerting/manage-notifications/view-alert-groups.md
Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
* Update docs/sources/alerting/manage-notifications/view-alert-groups.md
Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
* Update docs/sources/alerting/manage-notifications/view-alert-groups.md
Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
* Use `1` always for numbered list
* refer to `grouping settings of notification policies`
* Update `View notification errors` with latest instructions
---------
Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com>
---
.../group-alert-notifications.md | 2 +-
.../manage-notifications/view-alert-groups.md | 78 +++++++++++++++----
.../view-notification-errors.md | 44 -----------
3 files changed, 62 insertions(+), 62 deletions(-)
delete mode 100644 docs/sources/alerting/manage-notifications/view-notification-errors.md
diff --git a/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md b/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md
index eecbad7f530..5c812545f5e 100644
--- a/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md
+++ b/docs/sources/alerting/fundamentals/notifications/group-alert-notifications.md
@@ -39,7 +39,7 @@ Grouping in Grafana Alerting allows you to batch relevant alerts together into a
Grouping combines similar alert instances within a specific period into a single notification, reducing alert noise.
-In the notification policy, you can configure how to group multiple alerts into a single notification:
+In the [notification policy](ref:notification-policies), you can configure how to group multiple alerts into a single notification:
- The `Group by` option specifies the criteria for grouping incoming alerts within the policy. The default is by alert rule.
- [Timing options](#timing-options) determine when and how often to send the notification.
diff --git a/docs/sources/alerting/manage-notifications/view-alert-groups.md b/docs/sources/alerting/manage-notifications/view-alert-groups.md
index f050af7373f..47d3f7bee5f 100644
--- a/docs/sources/alerting/manage-notifications/view-alert-groups.md
+++ b/docs/sources/alerting/manage-notifications/view-alert-groups.md
@@ -4,49 +4,93 @@ aliases:
- ../../alerting/alert-groups/filter-alerts/ # /docs/grafana//alerting/alert-groups/filter-alerts/
- ../../alerting/alert-groups/view-alert-grouping/ # /docs/grafana//alerting/alert-groups/view-alert-grouping/
- ../../alerting/unified-alerting/alert-groups/ # /docs/grafana//alerting/unified-alerting/alert-groups/
+ - ../../alerting/manage-notifications/view-notification-errors/ # /docs/grafana//alerting/manage-notifications/view-notification-errors/
canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/view-alert-groups/
-description: Alert groups
+description: The Groups view lists grouped alerts that are actively triggering notifications.
keywords:
- grafana
- alerting
- alerts
+ - errors
+ - notifications
- groups
labels:
products:
- cloud
- enterprise
- oss
-title: View and filter by alert groups
+title: View the status of notifications
weight: 800
+refs:
+ alertmanager:
+ - pattern: /docs/grafana/
+ destination: /docs/grafana//alerting/set-up/configure-alertmanager/
+ - pattern: /docs/grafana-cloud/
+ destination: /docs/grafana-cloud/alerting-and-irm/alerting/set-up/configure-alertmanager/
+ grouping:
+ - pattern: /docs/grafana/
+ destination: /docs/grafana//alerting/fundamentals/notifications/group-alert-notifications/
+ - pattern: /docs/grafana-cloud/
+ destination: /docs/grafana-cloud/alerting-and-irm/alerting/fundamentals/notifications/group-alert-notifications/
---
-# View and filter by alert groups
+# View the status of notifications
-Alert groups show grouped alerts from an Alertmanager instance. By default, alert rules are grouped by the label keys for the default policy in notification policies. Grouping common alert rules into a single alert group prevents duplicate alert rules from being fired.
+The Groups view page lists grouped alerts that are actively triggering notifications.
-You can view alert groups and also filter for alert rules that match specific criteria.
+By default, Grafana Alerting groups similar firing alerts (or alert instances) to prevent notification overload. For details on how notification grouping works, refer to [Group alert notifications](ref:grouping).
-## View alert groups
+In the Groups view, you can see alert groups, check the state of their notifications, and also filter for alert instances that match specific criteria. This view is useful for debugging and verifying your grouping settings of notification policies.
+
+## View alert groups and notification state
To view alert groups, complete the following steps.
-1. In the left-side menu, click **Alerts & IRM** and then **Alerting**.
-1. Click **Groups** to view the list of existing groups.
-1. From the **Alertmanager** dropdown, select an external Alertmanager as your data source. By default, the `Grafana` Alertmanager is selected.
-1. From **Custom group by** dropdown, select a combination of labels to view a grouping other than the default. This is useful for debugging and verifying your grouping of notification policies.
+1. Click **Alerts & IRM** -> **Alerting**.
+1. Click **Groups** to view the list of groups firing notifications.
+
+ By default, alert groups are grouped by the notification policies grouping.
+
+ Each group displays its label set, contact point, and the number of alert instances (or alerts).
+
+ Then, click on a group to access its alert instances. You can find alert instances by their label set and view their notification state.
+
+### Notification states
+
+The notification state of an alert instance can be in one of the following states:
+
+- **Unprocessed**: The alert is received but its notification has not been processed yet.
+- **Suppressed**: The alert has been silenced.
+- **Active**: The alert notification has been handled. The alert is still firing and continues to be managed.
+
+### Filter alerts
+
+You can filter by label, state, or Alertmanager:
+
+- **By label**: In **Search**, enter an existing label to view alerts matching the label. For example, `environment=production,region=~US|EU,severity!=warning`.
+
+- **By state**: In **States**, select from Active, Suppressed, or Unprocessed states to view alerts matching your selected state. All other alerts are hidden.
+
+- **By Alertmanager**: In the **Alertmanager** dropdown, select an [external Alertmanager](ref:alertmanager) to view only alert groups for that specific Alertmanager. By default, the `Grafana` Alertmanager is selected.
+
+### Custom group
+
+From **Custom group by** dropdown, select a combination of labels to view a grouping other than the default. This helps validate the [grouping settings of your notification policies](ref:grouping).
If an alert does not contain labels specified either in the grouping of the default policy or the custom grouping, then the alert is added to a catch all group with a header of `No grouping`.
-## Filter alerts
+## View notification errors
+
+{{% admonition type="note" %}}
-You can filter by label or state.
+Notification errors are only available with [pre-configured Grafana Alertmanagers](ref:alertmanager).
-### Search by label
+{{% /admonition %}}
-In **Search**, enter an existing label to view alerts matching the label.
+Notification errors provide information about why they failed to be sent or were not received.
-For example, `environment=production,region=~US|EU,severity!=warning`.
+To view notification errors, navigate to **Alerts & IRM** -> **Alerting** -> **Contact points**.
-### Filter by state
+Each contact point displays a message about the status of their latest notification deliveries.
-In **States**, select from Active, Suppressed, or Unprocessed states to view alerts matching your selected state. All other alerts are hidden.
+If a contact point is failing, a red message indicates that there are errors delivering notifications. Hover over the error message to see the notification error details.
diff --git a/docs/sources/alerting/manage-notifications/view-notification-errors.md b/docs/sources/alerting/manage-notifications/view-notification-errors.md
deleted file mode 100644
index 9caf6d6e186..00000000000
--- a/docs/sources/alerting/manage-notifications/view-notification-errors.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-canonical: https://grafana.com/docs/grafana/latest/alerting/manage-notifications/view-notification-errors/
-description: View notification errors and understand why they failed to be sent or were not received
-keywords:
- - grafana
- - alerting
- - notification
- - errors
- - contact points
-labels:
- products:
- - cloud
- - enterprise
- - oss
-title: View notification errors
-weight: 900
----
-
-# View notification errors
-
-View notification errors and understand why they failed to be sent or were not received.
-
-**Note:**
-This feature only works if you are using Grafana Alertmanager.
-
-To view notification errors, complete the following steps.
-
-1. Navigate to Alerting -> Contact points.
-
- If any contact points are failing, a message at the right-hand corner of the screen alerts the user to the fact that there are errors and how many.
-
-2. Click on the contact point to view the details of errors for each contact point.
-
- Error details are displayed if you hover over the Error icon.
-
- If a contact point has more than one integration, you see all errors for each of the integrations listed.
-
-3. In the Health column, check the status of the notification.
-
- This can be either OK, No attempts, or Error.
-
-## Useful links
-
-[Receivers API](https://editor.swagger.io/?url=https://raw.githubusercontent.com/grafana/grafana/main/pkg/services/ngalert/api/tooling/post.json)
From c0058f9c7e390d8a196f5b375382334287633ea9 Mon Sep 17 00:00:00 2001
From: Ashley Harrison
Date: Mon, 1 Jul 2024 11:28:39 +0100
Subject: [PATCH 3/9] Page: Add `bodyScrolling` feature toggle (#89895)
add bodyScrolling feature toggle
---
.../grafana-data/src/types/featureToggles.gen.ts | 1 +
pkg/services/featuremgmt/registry.go | 11 +++++++++++
pkg/services/featuremgmt/toggles_gen.csv | 1 +
pkg/services/featuremgmt/toggles_gen.go | 4 ++++
pkg/services/featuremgmt/toggles_gen.json | 15 +++++++++++++++
5 files changed, 32 insertions(+)
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 2b56f82d2bb..2726e598147 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -199,4 +199,5 @@ export interface FeatureToggles {
alertingApiServer?: boolean;
dashboardRestoreUI?: boolean;
cloudWatchRoundUpEndTime?: boolean;
+ bodyScrolling?: boolean;
}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 059cceee86b..3a0764fbb83 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1360,6 +1360,17 @@ var (
Owner: awsDatasourcesSquad,
Expression: "true",
},
+ {
+ Name: "bodyScrolling",
+ Description: "Adjusts Page to make body the scrollable element",
+ Stage: FeatureStageExperimental,
+ Owner: grafanaFrontendPlatformSquad,
+ Expression: "false", // enabled by default
+ FrontendOnly: true,
+ AllowSelfServe: false,
+ HideFromDocs: true,
+ HideFromAdminPage: true,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 3bb0bf05cac..71699a4e6ec 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -180,3 +180,4 @@ passScopeToDashboardApi,experimental,@grafana/dashboards-squad,false,false,false
alertingApiServer,experimental,@grafana/alerting-squad,false,true,false
dashboardRestoreUI,experimental,@grafana/grafana-frontend-platform,false,false,false
cloudWatchRoundUpEndTime,GA,@grafana/aws-datasources,false,false,false
+bodyScrolling,experimental,@grafana/grafana-frontend-platform,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 75ecaa053b5..ea33898217c 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -730,4 +730,8 @@ const (
// FlagCloudWatchRoundUpEndTime
// Round up end time for metric queries to the next minute to avoid missing data
FlagCloudWatchRoundUpEndTime = "cloudWatchRoundUpEndTime"
+
+ // FlagBodyScrolling
+ // Adjusts Page to make body the scrollable element
+ FlagBodyScrolling = "bodyScrolling"
)
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 10c6014ea06..99143386068 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -473,6 +473,21 @@
"codeowner": "@grafana/partner-datasources"
}
},
+ {
+ "metadata": {
+ "name": "bodyScrolling",
+ "resourceVersion": "1719825052257",
+ "creationTimestamp": "2024-07-01T09:10:52Z"
+ },
+ "spec": {
+ "description": "Adjusts Page to make body the scrollable element",
+ "stage": "experimental",
+ "codeowner": "@grafana/grafana-frontend-platform",
+ "frontend": true,
+ "hideFromAdminPage": true,
+ "hideFromDocs": true
+ }
+ },
{
"metadata": {
"name": "cachingOptimizeSerializationMemoryUsage",
From 4c9fef6183eb4e5faea6287ac66f8a4b76f5b42a Mon Sep 17 00:00:00 2001
From: Bogdan Matei
Date: Mon, 1 Jul 2024 13:46:45 +0300
Subject: [PATCH 4/9] Scopes: Persist selected scopes when searching (#89758)
---
.../scene/Scopes/ScopesFiltersScene.tsx | 21 +-
.../scene/Scopes/ScopesInput.tsx | 70 +++--
.../scene/Scopes/ScopesScene.test.tsx | 297 ++++++++++++------
.../scene/Scopes/ScopesTree.tsx | 81 +++++
.../scene/Scopes/ScopesTreeHeadline.tsx | 42 +++
.../scene/Scopes/ScopesTreeItem.tsx | 143 +++++++++
.../scene/Scopes/ScopesTreeLevel.tsx | 185 -----------
.../scene/Scopes/ScopesTreeLoading.tsx | 29 ++
.../scene/Scopes/ScopesTreeSearch.tsx | 53 ++++
.../dashboard-scene/scene/Scopes/api.ts | 5 +-
.../scene/Scopes/testUtils.tsx | 83 +++--
.../dashboard-scene/scene/Scopes/types.ts | 9 +
public/locales/en-US/grafana.json | 6 +-
public/locales/pseudo-LOCALE/grafana.json | 6 +-
14 files changed, 672 insertions(+), 358 deletions(-)
create mode 100644 public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx
create mode 100644 public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx
create mode 100644 public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx
delete mode 100644 public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx
create mode 100644 public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx
create mode 100644 public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
index 7af747fc9d8..c693b626299 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesFiltersScene.tsx
@@ -17,9 +17,9 @@ import { t, Trans } from 'app/core/internationalization';
import { ScopesInput } from './ScopesInput';
import { ScopesScene } from './ScopesScene';
-import { ScopesTreeLevel } from './ScopesTreeLevel';
+import { ScopesTree } from './ScopesTree';
import { fetchNodes, fetchScope, fetchSelectedScopes } from './api';
-import { NodesMap, SelectedScope, TreeScope } from './types';
+import { NodeReason, NodesMap, SelectedScope, TreeScope } from './types';
import { getBasicScope } from './utils';
export interface ScopesFiltersSceneState extends SceneObjectState {
@@ -47,6 +47,7 @@ export class ScopesFiltersScene extends SceneObjectBase
nodes: {
'': {
name: '',
+ reason: NodeReason.Result,
nodeType: 'container',
title: '',
isExpandable: true,
@@ -119,7 +120,19 @@ export class ScopesFiltersScene extends SceneObjectBase
})
)
.subscribe((childNodes) => {
- currentNode.nodes = childNodes;
+ const persistedNodes = this.state.treeScopes
+ .map(({ path }) => path[path.length - 1])
+ .filter((nodeName) => nodeName in currentNode.nodes && !(nodeName in childNodes))
+ .reduce((acc, nodeName) => {
+ acc[nodeName] = {
+ ...currentNode.nodes[nodeName],
+ reason: NodeReason.Persisted,
+ };
+
+ return acc;
+ }, {});
+
+ currentNode.nodes = { ...persistedNodes, ...childNodes };
this.setState({ nodes });
@@ -284,7 +297,7 @@ export function ScopesFiltersSceneRenderer({ model }: SceneComponentProps
) : (
- {
+ setIsTooltipVisible(false);
+ }, [scopes]);
+
const scopesPaths = useMemo(() => {
const pathsTitles = scopes.map(({ scope, path }) => {
let currentLevel = nodes;
@@ -64,7 +69,7 @@ export function ScopesInput({
const groupedByPath = groupBy(pathsTitles, ([path]) => path);
- return Object.entries(groupedByPath)
+ const scopesPaths = Object.entries(groupedByPath)
.map(([path, pathScopes]) => {
const scopesTitles = pathScopes.map(([, scopeTitle]) => scopeTitle).join(', ');
@@ -75,41 +80,44 @@ export function ScopesInput({
{path}
));
+
+ return <>{scopesPaths}>;
}, [nodes, scopes, styles]);
const scopesTitles = useMemo(() => scopes.map(({ scope }) => scope.spec.title).join(', '), [scopes]);
- const input = (
- 0 && !isDisabled ? (
- onRemoveAllClick()}
- />
- ) : undefined
- }
- onClick={() => {
- if (!isDisabled) {
- onInputClick();
+ const input = useMemo(
+ () => (
+ 0 && !isDisabled ? (
+ onRemoveAllClick()}
+ />
+ ) : undefined
}
- }}
- />
+ onMouseOver={() => setIsTooltipVisible(true)}
+ onMouseOut={() => setIsTooltipVisible(false)}
+ onClick={() => {
+ if (!isDisabled) {
+ onInputClick();
+ }
+ }}
+ />
+ ),
+ [isDisabled, isLoading, onInputClick, onRemoveAllClick, scopes, scopesTitles]
);
- if (scopes.length === 0) {
- return input;
- }
-
return (
- {scopesPaths}>} interactive={true}>
+
{input}
);
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx
index 84cea5de184..0f4fb4d188e 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesScene.test.tsx
@@ -10,47 +10,54 @@ import { ScopesFiltersScene } from './ScopesFiltersScene';
import { ScopesScene } from './ScopesScene';
import {
buildTestScene,
- fetchSuggestedDashboardsSpy,
fetchNodesSpy,
fetchScopeSpy,
fetchSelectedScopesSpy,
- getApplicationsClustersExpand,
- getApplicationsClustersSelect,
- getApplicationsClustersSlothClusterNorthSelect,
- getApplicationsClustersSlothClusterSouthSelect,
- getApplicationsExpand,
- getApplicationsSearch,
- getApplicationsSlothPictureFactorySelect,
- getApplicationsSlothPictureFactoryTitle,
- getApplicationsSlothVoteTrackerSelect,
- getFiltersApply,
- getFiltersCancel,
- getFiltersInput,
- getClustersExpand,
- getClustersSelect,
- getClustersSlothClusterNorthRadio,
- getClustersSlothClusterSouthRadio,
+ fetchSuggestedDashboardsSpy,
getDashboard,
getDashboardsContainer,
getDashboardsExpand,
getDashboardsSearch,
+ getFiltersApply,
+ getFiltersCancel,
+ getFiltersInput,
getMock,
+ getNotFoundForFilter,
+ getNotFoundForFilterClear,
+ getNotFoundForScope,
+ getNotFoundNoScopes,
+ getPersistedApplicationsSlothPictureFactorySelect,
+ getPersistedApplicationsSlothPictureFactoryTitle,
+ getPersistedApplicationsSlothVoteTrackerTitle,
+ getResultApplicationsClustersExpand,
+ getResultApplicationsClustersSelect,
+ getResultApplicationsClustersSlothClusterNorthSelect,
+ getResultApplicationsClustersSlothClusterSouthSelect,
+ getResultApplicationsExpand,
+ getResultApplicationsSlothPictureFactorySelect,
+ getResultApplicationsSlothPictureFactoryTitle,
+ getResultApplicationsSlothVoteTrackerSelect,
+ getResultApplicationsSlothVoteTrackerTitle,
+ getResultClustersExpand,
+ getResultClustersSelect,
+ getResultClustersSlothClusterEastRadio,
+ getResultClustersSlothClusterNorthRadio,
+ getResultClustersSlothClusterSouthRadio,
+ getTreeHeadline,
+ getTreeSearch,
mocksScopes,
queryAllDashboard,
- queryFiltersApply,
- queryApplicationsClustersTitle,
- queryApplicationsSlothPictureFactoryTitle,
- queryApplicationsSlothVoteTrackerTitle,
queryDashboard,
queryDashboardsContainer,
queryDashboardsExpand,
- renderDashboard,
- getNotFoundForScope,
queryDashboardsSearch,
- getNotFoundForFilter,
- getClustersSlothClusterEastRadio,
- getNotFoundForFilterClear,
- getNotFoundNoScopes,
+ queryFiltersApply,
+ queryPersistedApplicationsSlothPictureFactoryTitle,
+ queryPersistedApplicationsSlothVoteTrackerTitle,
+ queryResultApplicationsClustersTitle,
+ queryResultApplicationsSlothPictureFactoryTitle,
+ queryResultApplicationsSlothVoteTrackerTitle,
+ renderDashboard,
} from './testUtils';
jest.mock('@grafana/runtime', () => ({
@@ -106,15 +113,15 @@ describe('ScopesScene', () => {
describe('Tree', () => {
it('Navigates through scopes nodes', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsClustersExpand());
- await userEvents.click(getApplicationsExpand());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsClustersExpand());
+ await userEvents.click(getResultApplicationsExpand());
});
it('Fetches scope details on select', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await waitFor(() => expect(fetchScopeSpy).toHaveBeenCalledTimes(1));
});
@@ -126,77 +133,167 @@ describe('ScopesScene', () => {
])
);
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- expect(getApplicationsSlothVoteTrackerSelect()).toBeChecked();
- expect(getApplicationsSlothPictureFactorySelect()).toBeChecked();
+ await userEvents.click(getResultApplicationsExpand());
+ expect(getResultApplicationsSlothVoteTrackerSelect()).toBeChecked();
+ expect(getResultApplicationsSlothPictureFactorySelect()).toBeChecked();
});
it('Can select scopes from same level', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
- await userEvents.click(getApplicationsClustersSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('slothVoteTracker, slothPictureFactory, Cluster Index Helper');
});
it('Can select a node from an inner level', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
- await userEvents.click(getApplicationsClustersExpand());
- await userEvents.click(getApplicationsClustersSlothClusterNorthSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsClustersExpand());
+ await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('slothClusterNorth');
});
it('Can select a node from an upper level', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toBe('Cluster Index Helper');
});
it('Respects only one select per container', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersExpand());
- await userEvents.click(getClustersSlothClusterNorthRadio());
- expect(getClustersSlothClusterNorthRadio().checked).toBe(true);
- expect(getClustersSlothClusterSouthRadio().checked).toBe(false);
- await userEvents.click(getClustersSlothClusterSouthRadio());
- expect(getClustersSlothClusterNorthRadio().checked).toBe(false);
- expect(getClustersSlothClusterSouthRadio().checked).toBe(true);
+ await userEvents.click(getResultClustersExpand());
+ await userEvents.click(getResultClustersSlothClusterNorthRadio());
+ expect(getResultClustersSlothClusterNorthRadio().checked).toBe(true);
+ expect(getResultClustersSlothClusterSouthRadio().checked).toBe(false);
+ await userEvents.click(getResultClustersSlothClusterSouthRadio());
+ expect(getResultClustersSlothClusterNorthRadio().checked).toBe(false);
+ expect(getResultClustersSlothClusterSouthRadio().checked).toBe(true);
});
it('Search works', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.type(getApplicationsSearch(), 'Clusters');
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.type(getTreeSearch(), 'Clusters');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
- expect(queryApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
- expect(queryApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
- expect(getApplicationsClustersSelect()).toBeInTheDocument();
- await userEvents.clear(getApplicationsSearch());
- await userEvents.type(getApplicationsSearch(), 'sloth');
+ expect(queryResultApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
+ expect(queryResultApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
+ expect(getResultApplicationsClustersSelect()).toBeInTheDocument();
+ await userEvents.clear(getTreeSearch());
+ await userEvents.type(getTreeSearch(), 'sloth');
await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
- expect(getApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
- expect(getApplicationsSlothVoteTrackerSelect()).toBeInTheDocument();
- expect(queryApplicationsClustersTitle()).not.toBeInTheDocument();
+ expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(getResultApplicationsSlothVoteTrackerSelect()).toBeInTheDocument();
+ expect(queryResultApplicationsClustersTitle()).not.toBeInTheDocument();
});
it('Opens to a selected scope', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getClustersExpand());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultClustersExpand());
+ await userEvents.click(getFiltersApply());
+ await userEvents.click(getFiltersInput());
+ expect(queryResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ });
+
+ it('Persists a scope', async () => {
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.type(getTreeSearch(), 'slothVoteTracker');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ expect(getPersistedApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(queryPersistedApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
+ expect(queryResultApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
+ expect(getResultApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
+ });
+
+ it('Does not persist a retrieved scope', async () => {
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.type(getTreeSearch(), 'slothPictureFactory');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ expect(queryPersistedApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
+ expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ });
+
+ it('Removes persisted nodes', async () => {
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.type(getTreeSearch(), 'slothVoteTracker');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ await userEvents.clear(getTreeSearch());
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
+ expect(queryPersistedApplicationsSlothPictureFactoryTitle()).not.toBeInTheDocument();
+ expect(queryPersistedApplicationsSlothVoteTrackerTitle()).not.toBeInTheDocument();
+ expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(getResultApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
+ });
+
+ it('Persists nodes from search', async () => {
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.type(getTreeSearch(), 'sloth');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.type(getTreeSearch(), 'slothunknown');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(4));
+ expect(getPersistedApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(getPersistedApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
+ await userEvents.clear(getTreeSearch());
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(5));
+ expect(getResultApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ expect(getResultApplicationsSlothVoteTrackerTitle()).toBeInTheDocument();
+ });
+
+ it('Selects a persisted scope', async () => {
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.type(getTreeSearch(), 'slothVoteTracker');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
+ expect(getFiltersInput().value).toBe('slothPictureFactory, slothVoteTracker');
+ });
+
+ it('Deselects a persisted scope', async () => {
await userEvents.click(getFiltersInput());
- expect(queryApplicationsSlothPictureFactoryTitle()).toBeInTheDocument();
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
+ await userEvents.type(getTreeSearch(), 'slothVoteTracker');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getFiltersApply());
+ expect(getFiltersInput().value).toBe('slothPictureFactory, slothVoteTracker');
+ await userEvents.click(getFiltersInput());
+ await userEvents.click(getPersistedApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getFiltersApply());
+ expect(getFiltersInput().value).toBe('slothVoteTracker');
+ });
+
+ it('Shows the proper headline', async () => {
+ await userEvents.click(getFiltersInput());
+ expect(getTreeHeadline()).toHaveTextContent('Recommended');
+ await userEvents.type(getTreeSearch(), 'Applications');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(2));
+ expect(getTreeHeadline()).toHaveTextContent('Results');
+ await userEvents.type(getTreeSearch(), 'unknown');
+ await waitFor(() => expect(fetchNodesSpy).toHaveBeenCalledTimes(3));
+ expect(getTreeHeadline()).toHaveTextContent('No results found for your query');
});
});
@@ -208,7 +305,7 @@ describe('ScopesScene', () => {
it('Fetches scope details on save', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchSelectedScopesSpy).toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual(
@@ -218,7 +315,7 @@ describe('ScopesScene', () => {
it("Doesn't save the scopes on close", async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersCancel());
await waitFor(() => expect(fetchSelectedScopesSpy).not.toHaveBeenCalled());
expect(filtersScene.getSelectedScopes()).toEqual([]);
@@ -226,7 +323,7 @@ describe('ScopesScene', () => {
it('Shows selected scopes', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersSelect());
+ await userEvents.click(getResultClustersSelect());
await userEvents.click(getFiltersApply());
expect(getFiltersInput().value).toEqual('Cluster Index Helper');
});
@@ -240,8 +337,8 @@ describe('ScopesScene', () => {
it('Does not fetch dashboards list when the list is not expanded', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).not.toHaveBeenCalled());
});
@@ -249,16 +346,16 @@ describe('ScopesScene', () => {
it('Fetches dashboards list when the list is expanded', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
});
it('Fetches dashboards list when the list is expanded after scope selection', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await userEvents.click(getDashboardsExpand());
await waitFor(() => expect(fetchSuggestedDashboardsSpy).toHaveBeenCalled());
@@ -267,22 +364,22 @@ describe('ScopesScene', () => {
it('Shows dashboards for multiple scopes', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(queryDashboard('3')).not.toBeInTheDocument();
expect(queryDashboard('4')).not.toBeInTheDocument();
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
expect(getDashboard('3')).toBeInTheDocument();
expect(getDashboard('4')).toBeInTheDocument();
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
expect(queryDashboard('1')).not.toBeInTheDocument();
expect(queryDashboard('2')).not.toBeInTheDocument();
@@ -293,8 +390,8 @@ describe('ScopesScene', () => {
it('Filters the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
expect(getDashboard('1')).toBeInTheDocument();
expect(getDashboard('2')).toBeInTheDocument();
@@ -305,10 +402,10 @@ describe('ScopesScene', () => {
it('Deduplicates the dashboards list', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsClustersExpand());
- await userEvents.click(getApplicationsClustersSlothClusterNorthSelect());
- await userEvents.click(getApplicationsClustersSlothClusterSouthSelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsClustersExpand());
+ await userEvents.click(getResultApplicationsClustersSlothClusterNorthSelect());
+ await userEvents.click(getResultApplicationsClustersSlothClusterSouthSelect());
await userEvents.click(getFiltersApply());
expect(queryAllDashboard('5')).toHaveLength(1);
expect(queryAllDashboard('6')).toHaveLength(1);
@@ -325,8 +422,8 @@ describe('ScopesScene', () => {
it('Does not show the input when there are no dashboards found for scope', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getClustersExpand());
- await userEvents.click(getClustersSlothClusterEastRadio());
+ await userEvents.click(getResultClustersExpand());
+ await userEvents.click(getResultClustersSlothClusterEastRadio());
await userEvents.click(getFiltersApply());
expect(getNotFoundForScope()).toBeInTheDocument();
expect(queryDashboardsSearch()).not.toBeInTheDocument();
@@ -335,8 +432,8 @@ describe('ScopesScene', () => {
it('Does show the input and a message when there are no dashboards found for filter', async () => {
await userEvents.click(getDashboardsExpand());
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await userEvents.type(getDashboardsSearch(), 'unknown');
expect(queryDashboardsSearch()).toBeInTheDocument();
@@ -380,8 +477,8 @@ describe('ScopesScene', () => {
describe('Enrichers', () => {
it('Data requests', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
@@ -391,7 +488,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
@@ -403,7 +500,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
const queryRunner = sceneGraph.findObject(dashboardScene, (o) => o.state.key === 'data-query-runner')!;
@@ -415,8 +512,8 @@ describe('ScopesScene', () => {
it('Filters requests', async () => {
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsExpand());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsExpand());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
@@ -425,7 +522,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothVoteTrackerSelect());
+ await userEvents.click(getResultApplicationsSlothVoteTrackerSelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
@@ -436,7 +533,7 @@ describe('ScopesScene', () => {
});
await userEvents.click(getFiltersInput());
- await userEvents.click(getApplicationsSlothPictureFactorySelect());
+ await userEvents.click(getResultApplicationsSlothPictureFactorySelect());
await userEvents.click(getFiltersApply());
await waitFor(() => {
expect(dashboardScene.enrichFiltersRequest().scopes).toEqual(
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx
new file mode 100644
index 00000000000..6f97a7e9b83
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTree.tsx
@@ -0,0 +1,81 @@
+import { groupBy } from 'lodash';
+import { useMemo } from 'react';
+
+import { ScopesTreeHeadline } from './ScopesTreeHeadline';
+import { ScopesTreeItem } from './ScopesTreeItem';
+import { ScopesTreeLoading } from './ScopesTreeLoading';
+import { ScopesTreeSearch } from './ScopesTreeSearch';
+import { NodeReason, NodesMap, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
+
+export interface ScopesTreeProps {
+ nodes: NodesMap;
+ nodePath: string[];
+ loadingNodeName: string | undefined;
+ scopes: TreeScope[];
+ onNodeUpdate: OnNodeUpdate;
+ onNodeSelectToggle: OnNodeSelectToggle;
+}
+
+export function ScopesTree({
+ nodes,
+ nodePath,
+ loadingNodeName,
+ scopes,
+ onNodeUpdate,
+ onNodeSelectToggle,
+}: ScopesTreeProps) {
+ const nodeId = nodePath[nodePath.length - 1];
+ const node = nodes[nodeId];
+ const childNodes = Object.values(node.nodes);
+ const isNodeLoading = loadingNodeName === nodeId;
+ const scopeNames = scopes.map(({ scopeName }) => scopeName);
+ const anyChildExpanded = childNodes.some(({ isExpanded }) => isExpanded);
+ const groupedNodes = useMemo(() => groupBy(childNodes, 'reason'), [childNodes]);
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx
new file mode 100644
index 00000000000..8dda39062b8
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeHeadline.tsx
@@ -0,0 +1,42 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+import { Trans } from 'app/core/internationalization';
+
+import { Node } from './types';
+
+export interface ScopesTreeHeadlineProps {
+ anyChildExpanded: boolean;
+ query: string;
+ resultsNodes: Node[];
+}
+
+export function ScopesTreeHeadline({ anyChildExpanded, query, resultsNodes }: ScopesTreeHeadlineProps) {
+ const styles = useStyles2(getStyles);
+
+ if (anyChildExpanded) {
+ return null;
+ }
+
+ return (
+
+ {!query ? (
+ Recommended
+ ) : resultsNodes.length === 0 ? (
+ No results found for your query
+ ) : (
+ Results
+ )}
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ container: css({
+ color: theme.colors.text.secondary,
+ margin: theme.spacing(1, 0),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx
new file mode 100644
index 00000000000..611e0851a68
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeItem.tsx
@@ -0,0 +1,143 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Checkbox, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { ScopesTree } from './ScopesTree';
+import { Node, OnNodeSelectToggle, OnNodeUpdate, TreeScope } from './types';
+
+export interface ScopesTreeItemProps {
+ anyChildExpanded: boolean;
+ isNodeLoading: boolean;
+ loadingNodeName: string | undefined;
+ node: Node;
+ nodePath: string[];
+ nodes: Node[];
+ scopeNames: string[];
+ scopes: TreeScope[];
+ type: 'persisted' | 'result';
+ onNodeUpdate: OnNodeUpdate;
+ onNodeSelectToggle: OnNodeSelectToggle;
+}
+
+export function ScopesTreeItem({
+ anyChildExpanded,
+ loadingNodeName,
+ node,
+ nodePath,
+ nodes,
+ scopeNames,
+ scopes,
+ type,
+ onNodeSelectToggle,
+ onNodeUpdate,
+}: ScopesTreeItemProps) {
+ const styles = useStyles2(getStyles);
+
+ return (
+
+ {nodes.map((childNode) => {
+ const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
+
+ if (anyChildExpanded && !childNode.isExpanded) {
+ return null;
+ }
+
+ const childNodePath = [...nodePath, childNode.name];
+
+ const radioName = childNodePath.join('.');
+
+ return (
+
+
+ {childNode.isSelectable && !childNode.isExpanded ? (
+ node.disableMultiSelect ? (
+ {
+ onNodeSelectToggle(childNodePath);
+ }}
+ />
+ ) : (
+ {
+ onNodeSelectToggle(childNodePath);
+ }}
+ />
+ )
+ ) : null}
+
+ {childNode.isExpandable ? (
+
+ ) : (
+ {childNode.title}
+ )}
+
+
+
+ {childNode.isExpanded && (
+
+ )}
+
+
+ );
+ })}
+
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ title: css({
+ alignItems: 'center',
+ display: 'flex',
+ gap: theme.spacing(1),
+ fontSize: theme.typography.pxToRem(14),
+ lineHeight: theme.typography.pxToRem(22),
+ padding: theme.spacing(0.5, 0),
+
+ '& > label': css({
+ gap: 0,
+ }),
+ }),
+ expand: css({
+ alignItems: 'center',
+ background: 'none',
+ border: 0,
+ display: 'flex',
+ gap: theme.spacing(1),
+ margin: 0,
+ padding: 0,
+ }),
+ children: css({
+ paddingLeft: theme.spacing(4),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx
deleted file mode 100644
index 8d57b74d825..00000000000
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLevel.tsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import { css } from '@emotion/css';
-import { debounce } from 'lodash';
-import { useEffect, useMemo, useState } from 'react';
-import Skeleton from 'react-loading-skeleton';
-
-import { GrafanaTheme2 } from '@grafana/data';
-import { Checkbox, FilterInput, Icon, RadioButtonDot, useStyles2 } from '@grafana/ui';
-import { t, Trans } from 'app/core/internationalization';
-
-import { NodesMap, TreeScope } from './types';
-
-export interface ScopesTreeLevelProps {
- nodes: NodesMap;
- nodePath: string[];
- loadingNodeName: string | undefined;
- scopes: TreeScope[];
- onNodeUpdate: (path: string[], isExpanded: boolean, query: string) => void;
- onNodeSelectToggle: (path: string[]) => void;
-}
-
-export function ScopesTreeLevel({
- nodes,
- nodePath,
- loadingNodeName,
- scopes,
- onNodeUpdate,
- onNodeSelectToggle,
-}: ScopesTreeLevelProps) {
- const styles = useStyles2(getStyles);
-
- const nodeId = nodePath[nodePath.length - 1];
- const node = nodes[nodeId];
- const childNodes = node.nodes;
- const childNodesArr = Object.values(childNodes);
- const isNodeLoading = loadingNodeName === nodeId;
-
- const scopeNames = scopes.map(({ scopeName }) => scopeName);
- const anyChildExpanded = childNodesArr.some(({ isExpanded }) => isExpanded);
-
- const [queryValue, setQueryValue] = useState(node.query);
- useEffect(() => {
- setQueryValue(node.query);
- }, [node.query]);
- const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
-
- return (
- <>
- {!anyChildExpanded && (
- {
- setQueryValue(value);
- onQueryUpdate(nodePath, true, value);
- }}
- />
- )}
-
- {!anyChildExpanded && !node.query && (
-
- Recommended
-
- )}
-
-
- {isNodeLoading &&
}
-
- {!isNodeLoading &&
- childNodesArr.map((childNode) => {
- const isSelected = childNode.isSelectable && scopeNames.includes(childNode.linkId!);
-
- if (anyChildExpanded && !childNode.isExpanded && !isSelected) {
- return null;
- }
-
- const childNodePath = [...nodePath, childNode.name];
-
- const radioName = childNodePath.join('.');
-
- return (
-
-
- {childNode.isSelectable && !childNode.isExpanded ? (
- node.disableMultiSelect ? (
- {
- onNodeSelectToggle(childNodePath);
- }}
- />
- ) : (
- {
- onNodeSelectToggle(childNodePath);
- }}
- />
- )
- ) : null}
-
- {childNode.isExpandable ? (
-
- ) : (
- {childNode.title}
- )}
-
-
-
- {childNode.isExpanded && (
-
- )}
-
-
- );
- })}
-
- >
- );
-}
-
-const getStyles = (theme: GrafanaTheme2) => {
- return {
- searchInput: css({
- margin: theme.spacing(1, 0),
- }),
- headline: css({
- color: theme.colors.text.secondary,
- margin: theme.spacing(1, 0),
- }),
- loader: css({
- margin: theme.spacing(0.5, 0),
- }),
- itemTitle: css({
- alignItems: 'center',
- display: 'flex',
- gap: theme.spacing(1),
- fontSize: theme.typography.pxToRem(14),
- lineHeight: theme.typography.pxToRem(22),
- padding: theme.spacing(0.5, 0),
-
- '& > label': css({
- gap: 0,
- }),
- }),
- itemExpand: css({
- alignItems: 'center',
- background: 'none',
- border: 0,
- display: 'flex',
- gap: theme.spacing(1),
- margin: 0,
- padding: 0,
- }),
- itemChildren: css({
- paddingLeft: theme.spacing(4),
- }),
- };
-};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx
new file mode 100644
index 00000000000..9a27b6a2311
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeLoading.tsx
@@ -0,0 +1,29 @@
+import { css } from '@emotion/css';
+import { ReactNode } from 'react';
+import Skeleton from 'react-loading-skeleton';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { useStyles2 } from '@grafana/ui';
+
+export interface ScopesTreeLoadingProps {
+ children: ReactNode;
+ isNodeLoading: boolean;
+}
+
+export function ScopesTreeLoading({ children, isNodeLoading }: ScopesTreeLoadingProps) {
+ const styles = useStyles2(getStyles);
+
+ if (isNodeLoading) {
+ return ;
+ }
+
+ return children;
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ loader: css({
+ margin: theme.spacing(0.5, 0),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
new file mode 100644
index 00000000000..cc9f4a61d8c
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
@@ -0,0 +1,53 @@
+import { css } from '@emotion/css';
+import { debounce } from 'lodash';
+import { useEffect, useMemo, useState } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { FilterInput, useStyles2 } from '@grafana/ui';
+import { t } from 'app/core/internationalization';
+
+import { OnNodeUpdate } from './types';
+
+export interface ScopesTreeSearchProps {
+ anyChildExpanded: boolean;
+ nodePath: string[];
+ query: string;
+ onNodeUpdate: OnNodeUpdate;
+}
+
+export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpdate }: ScopesTreeSearchProps) {
+ const styles = useStyles2(getStyles);
+
+ const [queryValue, setQueryValue] = useState(query);
+
+ useEffect(() => {
+ setQueryValue(query);
+ }, [query]);
+
+ const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
+
+ if (anyChildExpanded) {
+ return null;
+ }
+
+ return (
+ {
+ setQueryValue(value);
+ onQueryUpdate(nodePath, true, value);
+ }}
+ />
+ );
+}
+
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ input: css({
+ margin: theme.spacing(1, 0),
+ }),
+ };
+};
diff --git a/public/app/features/dashboard-scene/scene/Scopes/api.ts b/public/app/features/dashboard-scene/scene/Scopes/api.ts
index 07df4877a54..d1deda21d73 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/api.ts
+++ b/public/app/features/dashboard-scene/scene/Scopes/api.ts
@@ -1,8 +1,8 @@
-import { Scope, ScopeSpec, ScopeNode, ScopeDashboardBinding } from '@grafana/data';
+import { Scope, ScopeDashboardBinding, ScopeNode, ScopeSpec } from '@grafana/data';
import { config, getBackendSrv } from '@grafana/runtime';
import { ScopedResourceClient } from 'app/features/apiserver/client';
-import { NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types';
+import { NodeReason, NodesMap, SelectedScope, SuggestedDashboard, TreeScope } from './types';
import { getBasicScope, mergeScopes } from './utils';
const group = 'scope.grafana.app';
@@ -37,6 +37,7 @@ export async function fetchNodes(parent: string, query: string): Promise `scopes-tree-${nodeId}-search`,
- select: (nodeId: string) => `scopes-tree-${nodeId}-checkbox`,
- radio: (nodeId: string) => `scopes-tree-${nodeId}-radio`,
- expand: (nodeId: string) => `scopes-tree-${nodeId}-expand`,
- title: (nodeId: string) => `scopes-tree-${nodeId}-title`,
+ search: 'scopes-tree-search',
+ headline: 'scopes-tree-headline',
+ select: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-checkbox`,
+ radio: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-radio`,
+ expand: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-expand`,
+ title: (nodeId: string, type: 'result' | 'persisted') => `scopes-tree-${type}-${nodeId}-title`,
},
filters: {
input: 'scopes-filters-input',
@@ -359,36 +360,50 @@ export const getNotFoundForScope = () => screen.getByTestId(selectors.dashboards
export const getNotFoundForFilter = () => screen.getByTestId(selectors.dashboards.notFoundForFilter);
export const getNotFoundForFilterClear = () => screen.getByTestId(selectors.dashboards.notFoundForFilterClear);
-export const getApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications'));
-export const getApplicationsSearch = () => screen.getByTestId(selectors.tree.search('applications'));
-export const queryApplicationsSlothPictureFactoryTitle = () =>
- screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory'));
-export const getApplicationsSlothPictureFactoryTitle = () =>
- screen.getByTestId(selectors.tree.title('applications-slothPictureFactory'));
-export const getApplicationsSlothPictureFactorySelect = () =>
- screen.getByTestId(selectors.tree.select('applications-slothPictureFactory'));
-export const queryApplicationsSlothVoteTrackerTitle = () =>
- screen.queryByTestId(selectors.tree.title('applications-slothVoteTracker'));
-export const getApplicationsSlothVoteTrackerSelect = () =>
- screen.getByTestId(selectors.tree.select('applications-slothVoteTracker'));
-export const queryApplicationsClustersTitle = () => screen.queryByTestId(selectors.tree.title('applications.clusters'));
-export const getApplicationsClustersSelect = () => screen.getByTestId(selectors.tree.select('applications.clusters'));
-export const getApplicationsClustersExpand = () => screen.getByTestId(selectors.tree.expand('applications.clusters'));
-export const queryApplicationsClustersSlothClusterNorthTitle = () =>
- screen.queryByTestId(selectors.tree.title('applications.clusters-slothClusterNorth'));
-export const getApplicationsClustersSlothClusterNorthSelect = () =>
- screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterNorth'));
-export const getApplicationsClustersSlothClusterSouthSelect = () =>
- screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterSouth'));
+export const getTreeSearch = () => screen.getByTestId(selectors.tree.search);
+export const getTreeHeadline = () => screen.getByTestId(selectors.tree.headline);
+export const getResultApplicationsExpand = () => screen.getByTestId(selectors.tree.expand('applications', 'result'));
+export const queryResultApplicationsSlothPictureFactoryTitle = () =>
+ screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory', 'result'));
+export const getResultApplicationsSlothPictureFactoryTitle = () =>
+ screen.getByTestId(selectors.tree.title('applications-slothPictureFactory', 'result'));
+export const getResultApplicationsSlothPictureFactorySelect = () =>
+ screen.getByTestId(selectors.tree.select('applications-slothPictureFactory', 'result'));
+export const queryPersistedApplicationsSlothPictureFactoryTitle = () =>
+ screen.queryByTestId(selectors.tree.title('applications-slothPictureFactory', 'persisted'));
+export const getPersistedApplicationsSlothPictureFactoryTitle = () =>
+ screen.getByTestId(selectors.tree.title('applications-slothPictureFactory', 'persisted'));
+export const getPersistedApplicationsSlothPictureFactorySelect = () =>
+ screen.getByTestId(selectors.tree.select('applications-slothPictureFactory', 'persisted'));
+export const queryResultApplicationsSlothVoteTrackerTitle = () =>
+ screen.queryByTestId(selectors.tree.title('applications-slothVoteTracker', 'result'));
+export const getResultApplicationsSlothVoteTrackerTitle = () =>
+ screen.getByTestId(selectors.tree.title('applications-slothVoteTracker', 'result'));
+export const getResultApplicationsSlothVoteTrackerSelect = () =>
+ screen.getByTestId(selectors.tree.select('applications-slothVoteTracker', 'result'));
+export const queryPersistedApplicationsSlothVoteTrackerTitle = () =>
+ screen.queryByTestId(selectors.tree.title('applications-slothVoteTracker', 'persisted'));
+export const getPersistedApplicationsSlothVoteTrackerTitle = () =>
+ screen.getByTestId(selectors.tree.title('applications-slothVoteTracker', 'persisted'));
+export const queryResultApplicationsClustersTitle = () =>
+ screen.queryByTestId(selectors.tree.title('applications.clusters', 'result'));
+export const getResultApplicationsClustersSelect = () =>
+ screen.getByTestId(selectors.tree.select('applications.clusters', 'result'));
+export const getResultApplicationsClustersExpand = () =>
+ screen.getByTestId(selectors.tree.expand('applications.clusters', 'result'));
+export const getResultApplicationsClustersSlothClusterNorthSelect = () =>
+ screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterNorth', 'result'));
+export const getResultApplicationsClustersSlothClusterSouthSelect = () =>
+ screen.getByTestId(selectors.tree.select('applications.clusters-slothClusterSouth', 'result'));
-export const getClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters'));
-export const getClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters'));
-export const getClustersSlothClusterNorthRadio = () =>
- screen.getByTestId(selectors.tree.radio('clusters-slothClusterNorth'));
-export const getClustersSlothClusterSouthRadio = () =>
- screen.getByTestId(selectors.tree.radio('clusters-slothClusterSouth'));
-export const getClustersSlothClusterEastRadio = () =>
- screen.getByTestId(selectors.tree.radio('clusters-slothClusterEast'));
+export const getResultClustersSelect = () => screen.getByTestId(selectors.tree.select('clusters', 'result'));
+export const getResultClustersExpand = () => screen.getByTestId(selectors.tree.expand('clusters', 'result'));
+export const getResultClustersSlothClusterNorthRadio = () =>
+ screen.getByTestId(selectors.tree.radio('clusters-slothClusterNorth', 'result'));
+export const getResultClustersSlothClusterSouthRadio = () =>
+ screen.getByTestId(selectors.tree.radio('clusters-slothClusterSouth', 'result'));
+export const getResultClustersSlothClusterEastRadio = () =>
+ screen.getByTestId(selectors.tree.radio('clusters-slothClusterEast', 'result'));
export function buildTestScene(overrides: Partial = {}) {
return new DashboardScene({
diff --git a/public/app/features/dashboard-scene/scene/Scopes/types.ts b/public/app/features/dashboard-scene/scene/Scopes/types.ts
index 5dc7cfc1e5d..40d3cbbefae 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/types.ts
+++ b/public/app/features/dashboard-scene/scene/Scopes/types.ts
@@ -1,7 +1,13 @@
import { Scope, ScopeDashboardBinding, ScopeNodeSpec } from '@grafana/data';
+export enum NodeReason {
+ Persisted,
+ Result,
+}
+
export interface Node extends ScopeNodeSpec {
name: string;
+ reason: NodeReason;
isExpandable: boolean;
isSelectable: boolean;
isExpanded: boolean;
@@ -26,3 +32,6 @@ export interface SuggestedDashboard {
dashboardTitle: string;
items: ScopeDashboardBinding[];
}
+
+export type OnNodeUpdate = (path: string[], isExpanded: boolean, query: string) => void;
+export type OnNodeSelectToggle = (path: string[]) => void;
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 70a894d96fd..92d54e9c3ae 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1698,7 +1698,11 @@
"tree": {
"collapse": "Collapse",
"expand": "Expand",
- "headline": "Recommended",
+ "headline": {
+ "noResults": "No results found for your query",
+ "recommended": "Recommended",
+ "results": "Results"
+ },
"search": "Search"
}
},
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index 3d992a19570..9a3df4cb747 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1698,7 +1698,11 @@
"tree": {
"collapse": "Cőľľäpşę",
"expand": "Ēχpäʼnđ",
- "headline": "Ŗęčőmmęʼnđęđ",
+ "headline": {
+ "noResults": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy",
+ "recommended": "Ŗęčőmmęʼnđęđ",
+ "results": "Ŗęşūľŧş"
+ },
"search": "Ŝęäřčĥ"
}
},
From 852d032e1ae1f7c989d8b2ec7d8e05bf2a54928e Mon Sep 17 00:00:00 2001
From: Alex Khomenko
Date: Mon, 1 Jul 2024 14:28:49 +0300
Subject: [PATCH 5/9] App events: Add "info" variant (#89903)
* App events: Add info notification type
* Add info hook
* Revert state
* Use info alert
---
.../grafana-data/src/types/legacyEvents.ts | 1 +
.../components/AppChrome/AppChromeService.tsx | 2 +-
.../AppNotifications/AppNotificationList.tsx | 2 ++
public/app/core/copy/appNotification.ts | 20 ++++++++++++++++---
4 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/packages/grafana-data/src/types/legacyEvents.ts b/packages/grafana-data/src/types/legacyEvents.ts
index 8154a322159..0ba9e15a932 100644
--- a/packages/grafana-data/src/types/legacyEvents.ts
+++ b/packages/grafana-data/src/types/legacyEvents.ts
@@ -13,6 +13,7 @@ export const AppEvents = {
alertSuccess: eventFactory('alert-success'),
alertWarning: eventFactory('alert-warning'),
alertError: eventFactory('alert-error'),
+ alertInfo: eventFactory('alert-info'),
};
export const PanelEvents = {
diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx
index 7a062e1152f..5c1d88ebb41 100644
--- a/public/app/core/components/AppChrome/AppChromeService.tsx
+++ b/public/app/core/components/AppChrome/AppChromeService.tsx
@@ -214,7 +214,7 @@ export class AppChromeService {
const { kioskMode, searchBarHidden } = this.state.getValue();
if (searchBarHidden || kioskMode === KioskMode.TV) {
- appEvents.emit(AppEvents.alertSuccess, [t('navigation.kiosk.tv-alert', 'Press ESC to exit kiosk mode')]);
+ appEvents.emit(AppEvents.alertInfo, [t('navigation.kiosk.tv-alert', 'Press ESC to exit kiosk mode')]);
return KioskMode.Full;
}
diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx
index 7b989214d45..edb686f0d55 100644
--- a/public/app/core/components/AppNotifications/AppNotificationList.tsx
+++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx
@@ -10,6 +10,7 @@ import { useSelector, useDispatch } from 'app/types';
import {
createErrorNotification,
+ createInfoNotification,
createSuccessNotification,
createWarningNotification,
} from '../../copy/appNotification';
@@ -25,6 +26,7 @@ export function AppNotificationList() {
appEvents.on(AppEvents.alertWarning, (payload) => dispatch(notifyApp(createWarningNotification(...payload))));
appEvents.on(AppEvents.alertSuccess, (payload) => dispatch(notifyApp(createSuccessNotification(...payload))));
appEvents.on(AppEvents.alertError, (payload) => dispatch(notifyApp(createErrorNotification(...payload))));
+ appEvents.on(AppEvents.alertInfo, (payload) => dispatch(notifyApp(createInfoNotification(...payload))));
}, [dispatch]);
const onClearAppNotification = (id: string) => {
diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts
index 485cdc6d4c6..60e766d48a2 100644
--- a/public/app/core/copy/appNotification.ts
+++ b/public/app/core/copy/appNotification.ts
@@ -1,4 +1,4 @@
-import { useMemo } from 'react';
+import { useMemo, ReactElement } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { getMessageFromError } from 'app/core/utils/errors';
@@ -40,7 +40,7 @@ export const createErrorNotification = (
title: string,
text: string | Error = '',
traceId?: string,
- component?: React.ReactElement
+ component?: ReactElement
): AppNotification => {
return {
...defaultErrorNotification,
@@ -64,12 +64,23 @@ export const createWarningNotification = (title: string, text = '', traceId?: st
showing: true,
});
-/** Hook for showing toast notifications with varying severity (success, warning error).
+export const createInfoNotification = (title: string, text = '', traceId?: string): AppNotification => ({
+ severity: AppNotificationSeverity.Info,
+ icon: 'info-circle',
+ title,
+ text,
+ id: uuidv4(),
+ timestamp: Date.now(),
+ showing: true,
+});
+
+/** Hook for showing toast notifications with varying severity (success, warning, error, info).
* @example
* const notifyApp = useAppNotification();
* notifyApp.success('Success!', 'Some additional text');
* notifyApp.warning('Warning!');
* notifyApp.error('Error!');
+ * notifyApp.info('Info text');
*/
export function useAppNotification() {
const dispatch = useDispatch();
@@ -84,6 +95,9 @@ export function useAppNotification() {
error: (title: string, text = '', traceId?: string) => {
dispatch(notifyApp(createErrorNotification(title, text, traceId)));
},
+ info: (title: string, text = '') => {
+ dispatch(notifyApp(createInfoNotification(title, text)));
+ },
}),
[dispatch]
);
From 751e6739f33f85e246416703a6ae268ea009286e Mon Sep 17 00:00:00 2001
From: Juan Cabanas
Date: Mon, 1 Jul 2024 09:01:14 -0300
Subject: [PATCH 6/9] ShareDrawer: Add menu item click tracking (#89860)
---
.../sharing/ShareButton/ShareMenu.tsx | 17 ++++++++++++++++-
.../dashboard-scene/sharing/ShareModal.tsx | 5 ++++-
.../dashboard-scene/utils/interactions.ts | 2 +-
.../components/ShareModal/ShareModal.tsx | 2 +-
.../SharePublicDashboard.test.tsx | 7 ++-----
5 files changed, 24 insertions(+), 9 deletions(-)
diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
index 250f723d41f..c5a88434302 100644
--- a/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareButton/ShareMenu.tsx
@@ -8,7 +8,9 @@ import { contextSrv } from 'app/core/core';
import { t } from 'app/core/internationalization';
import { isPublicDashboardsEnabled } from '../../../dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils';
+import { getTrackingSource, shareDashboardType } from '../../../dashboard/components/ShareModal/utils';
import { DashboardScene } from '../../scene/DashboardScene';
+import { DashboardInteractions } from '../../utils/interactions';
import { ShareDrawer } from '../ShareDrawer/ShareDrawer';
import { SceneShareDrawerState } from '../types';
@@ -21,6 +23,7 @@ const newShareButtonSelector = e2eSelectors.pages.Dashboard.DashNav.newShareButt
type CustomDashboardDrawer = new (...args: SceneShareDrawerState[]) => SceneObject;
export interface ShareDrawerMenuItem {
+ shareId: string;
testId: string;
label: string;
description?: string;
@@ -52,6 +55,7 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
const menuItems: ShareDrawerMenuItem[] = [];
menuItems.push({
+ shareId: shareDashboardType.link,
testId: newShareButtonSelector.shareInternally,
icon: 'building',
label: t('share-dashboard.menu.share-internally-title', 'Share internally'),
@@ -62,6 +66,7 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
});
menuItems.push({
+ shareId: shareDashboardType.publicDashboard,
testId: newShareButtonSelector.shareExternally,
icon: 'share-alt',
label: t('share-dashboard.menu.share-externally-title', 'Share externally'),
@@ -74,6 +79,7 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
customShareDrawerItem.forEach((d) => menuItems.push(d));
menuItems.push({
+ shareId: shareDashboardType.snapshot,
testId: newShareButtonSelector.shareSnapshot,
icon: 'camera',
label: t('share-dashboard.menu.share-snapshot-title', 'Share snapshot'),
@@ -86,6 +92,15 @@ export default function ShareMenu({ dashboard, panel }: { dashboard: DashboardSc
return menuItems.filter((item) => item.renderCondition);
}, [onMenuItemClick, dashboard, panel]);
+ const onClick = (item: ShareDrawerMenuItem) => {
+ DashboardInteractions.sharingCategoryClicked({
+ item: item.shareId,
+ shareResource: getTrackingSource(panel?.getRef()),
+ });
+
+ item.onClick(dashboard);
+ };
+
return (
diff --git a/public/app/features/dashboard-scene/sharing/ShareModal.tsx b/public/app/features/dashboard-scene/sharing/ShareModal.tsx
index d015501e143..916eddc9dde 100644
--- a/public/app/features/dashboard-scene/sharing/ShareModal.tsx
+++ b/public/app/features/dashboard-scene/sharing/ShareModal.tsx
@@ -91,7 +91,10 @@ export class ShareModal extends SceneObjectBase implements Moda
};
onChangeTab: ComponentProps['onChangeTab'] = (tab) => {
- DashboardInteractions.sharingTabChanged({ item: tab.value, shareResource: getTrackingSource(this.state.panelRef) });
+ DashboardInteractions.sharingCategoryClicked({
+ item: tab.value,
+ shareResource: getTrackingSource(this.state.panelRef),
+ });
this.setState({ activeTab: tab.value });
};
}
diff --git a/public/app/features/dashboard-scene/utils/interactions.ts b/public/app/features/dashboard-scene/utils/interactions.ts
index 85c4b332f82..3086ec1defb 100644
--- a/public/app/features/dashboard-scene/utils/interactions.ts
+++ b/public/app/features/dashboard-scene/utils/interactions.ts
@@ -44,7 +44,7 @@ export const DashboardInteractions = {
},
// Sharing interactions:
- sharingTabChanged: (properties?: Record) => {
+ sharingCategoryClicked: (properties?: Record) => {
reportDashboardInteraction('sharing_category_clicked', properties);
},
shareLinkCopied: (properties?: Record) => {
diff --git a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx
index 937ae1af519..9e3bc7117c8 100644
--- a/public/app/features/dashboard/components/ShareModal/ShareModal.tsx
+++ b/public/app/features/dashboard/components/ShareModal/ShareModal.tsx
@@ -102,7 +102,7 @@ class UnthemedShareModal extends React.Component {
onSelectTab: React.ComponentProps['onChangeTab'] = (t) => {
this.setState((prevState) => ({ ...prevState, activeTab: t.value }));
- DashboardInteractions.sharingTabChanged({
+ DashboardInteractions.sharingCategoryClicked({
item: t.value,
shareResource: getTrackingSource(this.props.panel),
});
diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
index faff169478a..c19af9d767f 100644
--- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
+++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.test.tsx
@@ -353,11 +353,11 @@ describe('SharePublic - Report interactions', () => {
});
it('reports interaction when public dashboard tab is clicked', async () => {
+ jest.spyOn(DashboardInteractions, 'sharingCategoryClicked');
await renderSharePublicDashboard();
await waitFor(() => {
- expect(DashboardInteractions.sharingTabChanged).toHaveBeenCalledTimes(1);
- expect(DashboardInteractions.sharingTabChanged).lastCalledWith({
+ expect(DashboardInteractions.sharingCategoryClicked).lastCalledWith({
item: shareDashboardType.publicDashboard,
shareResource: 'dashboard',
});
@@ -374,7 +374,6 @@ describe('SharePublic - Report interactions', () => {
await userEvent.click(screen.getByTestId(selectors.EnableTimeRangeSwitch));
await waitFor(() => {
- expect(reportInteraction).toHaveBeenCalledTimes(1);
expect(reportInteraction).toHaveBeenLastCalledWith('dashboards_sharing_public_time_picker_clicked', {
enabled: !pubdashResponse.timeSelectionEnabled,
});
@@ -391,7 +390,6 @@ describe('SharePublic - Report interactions', () => {
await userEvent.click(screen.getByTestId(selectors.EnableAnnotationsSwitch));
await waitFor(() => {
- expect(reportInteraction).toHaveBeenCalledTimes(1);
expect(reportInteraction).toHaveBeenLastCalledWith('dashboards_sharing_public_annotations_clicked', {
enabled: !pubdashResponse.annotationsEnabled,
});
@@ -405,7 +403,6 @@ describe('SharePublic - Report interactions', () => {
await userEvent.click(screen.getByTestId(selectors.PauseSwitch));
await waitFor(() => {
- expect(reportInteraction).toHaveBeenCalledTimes(1);
expect(reportInteraction).toHaveBeenLastCalledWith('dashboards_sharing_public_pause_clicked', {
paused: pubdashResponse.isEnabled,
});
From 71d31397e5598de990d8070f0039e0f19894f51b Mon Sep 17 00:00:00 2001
From: Gabriel MABILLE
Date: Mon, 1 Jul 2024 14:39:51 +0200
Subject: [PATCH 7/9] Fix flaky tests (#89910)
---
pkg/services/ngalert/state/historian/loki_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go
index 97c37b3dd02..297282df0d4 100644
--- a/pkg/services/ngalert/state/historian/loki_test.go
+++ b/pkg/services/ngalert/state/historian/loki_test.go
@@ -856,7 +856,7 @@ func TestGetFolderUIDsForFilter(t *testing.T) {
result, err := loki.getFolderUIDsForFilter(context.Background(), models.HistoryQuery{OrgID: orgID, SignedInUser: usr})
assert.NoError(t, err)
- assert.EqualValues(t, folders, result)
+ assert.ElementsMatch(t, folders, result)
assert.Len(t, ac.Calls, len(folders)+1)
assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName)
From 559738ce6ae1bd27e2885ae295dbf2920fec1f2a Mon Sep 17 00:00:00 2001
From: Yuri Tseretyan
Date: Mon, 1 Jul 2024 09:59:06 -0400
Subject: [PATCH 8/9] Alerting: Fix flaky test in historian (#89913)
---
pkg/services/ngalert/state/historian/loki_test.go | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go
index 297282df0d4..e2ef7b44f80 100644
--- a/pkg/services/ngalert/state/historian/loki_test.go
+++ b/pkg/services/ngalert/state/historian/loki_test.go
@@ -861,11 +861,16 @@ func TestGetFolderUIDsForFilter(t *testing.T) {
assert.Len(t, ac.Calls, len(folders)+1)
assert.Equal(t, "CanReadAllRules", ac.Calls[0].MethodName)
assert.Equal(t, usr, ac.Calls[0].Arguments[1])
- for i, folderUID := range folders {
- assert.Equal(t, "HasAccessInFolder", ac.Calls[i+1].MethodName)
- assert.Equal(t, usr, ac.Calls[i+1].Arguments[1])
- assert.Equal(t, folderUID, ac.Calls[i+1].Arguments[2].(models.Namespaced).GetNamespaceUID())
+
+ var called []string
+ for _, call := range ac.Calls[1:] {
+ if !assert.Equal(t, "HasAccessInFolder", call.MethodName) {
+ continue
+ }
+ assert.Equal(t, usr, call.Arguments[1])
+ called = append(called, call.Arguments[2].(models.Namespaced).GetNamespaceUID())
}
+ assert.ElementsMatch(t, folders, called)
t.Run("should fail if no folders to read", func(t *testing.T) {
loki := createLoki(ac)
From 86466aec61f92985faaddcb3d998c9c1c36033f9 Mon Sep 17 00:00:00 2001
From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
Date: Mon, 1 Jul 2024 17:00:40 +0200
Subject: [PATCH 9/9] Gops: Update texts in main irm page (#89810)
Update texts in main irm page
---
public/app/features/gops/configuration-tracker/irmHooks.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/public/app/features/gops/configuration-tracker/irmHooks.ts b/public/app/features/gops/configuration-tracker/irmHooks.ts
index f182633940f..14fe5246d52 100644
--- a/public/app/features/gops/configuration-tracker/irmHooks.ts
+++ b/public/app/features/gops/configuration-tracker/irmHooks.ts
@@ -340,7 +340,7 @@ export const useGetConfigurationForUI = ({
function getConnectDataSourceConfiguration() {
const description = dataSourceCompatibleWithAlerting
? 'You have connected a datasource.'
- : 'Connect at least one data source to start receiving data.';
+ : 'Connect at least one data source to start receiving data';
const actionButtonTitle = dataSourceCompatibleWithAlerting ? 'View' : 'Connect';
return {
id: ConfigurationStepsEnum.CONNECT_DATASOURCE,
@@ -356,8 +356,8 @@ export const useGetConfigurationForUI = ({
id: ConfigurationStepsEnum.ESSENTIALS,
title: 'Essentials',
titleIcon: 'star',
- description: 'Configure the features you need to start using Grafana IRM workflows',
- actionButtonTitle: 'Start',
+ description: 'Set up the necessary features to start using Grafana IRM workflows',
+ actionButtonTitle: stepsDone === totalStepsToDo ? 'View' : 'Configure',
stepsDone,
totalStepsToDo,
},