From 70181997b5e5f6bead6af28d4f4008a81efbd9d9 Mon Sep 17 00:00:00 2001 From: Christopher Moyer <35463610+chri2547@users.noreply.github.com> Date: Wed, 26 Feb 2025 08:55:27 -0600 Subject: [PATCH 01/13] docs/clarifies name of connection tile (#101361) clarifies name of connection tile --- .../datasources/prometheus/configure-prometheus-data-source.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/datasources/prometheus/configure-prometheus-data-source.md b/docs/sources/datasources/prometheus/configure-prometheus-data-source.md index c8388c35821..98ef1608f60 100644 --- a/docs/sources/datasources/prometheus/configure-prometheus-data-source.md +++ b/docs/sources/datasources/prometheus/configure-prometheus-data-source.md @@ -55,7 +55,7 @@ To add the Prometheus data source, complete the following steps: 1. Click **Connections** in the left-side menu. 1. Under **Connections**, click **Add new connection**. 1. Enter `Prometheus` in the search bar. -1. Select **Prometheus**. +1. Click **Prometheus data source**. 1. Click **Add new data source** in the upper right. 1. Enter a name for the data source. From 4270bf742a859c9e8e866b29e863186bc3cff3bd Mon Sep 17 00:00:00 2001 From: Gilles De Mey Date: Wed, 26 Feb 2025 15:57:32 +0100 Subject: [PATCH 02/13] Alerting: Fix label overflow for history common labels (#101343) --- .../rules/state-history/LokiStateHistory.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx index 7e9a2caef07..76ba417dd20 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LokiStateHistory.tsx @@ -5,7 +5,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { DataFrame, GrafanaTheme2, TimeRange, dateTime } from '@grafana/data'; -import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Field, Icon, Input, Label, Stack, Text, Tooltip, useStyles2 } from '@grafana/ui'; import { stateHistoryApi } from '../../../api/stateHistoryApi'; import { combineMatcherStrings } from '../../../utils/alertmanager'; @@ -103,15 +103,15 @@ const LokiStateHistory = ({ ruleUID }: Props) => { {!isEmpty(commonLabels) && ( -
- - Common labels + + + Common labels - + - -
+ + )} {isEmpty(frameSubset) ? ( <> @@ -254,10 +254,6 @@ export const getStyles = (theme: GrafanaTheme2) => ({ color: theme.colors.warning.text, padding: theme.spacing(), }), - commonLabels: css({ - display: 'grid', - gridTemplateColumns: 'max-content auto', - }), // we need !important here to override the list item default styles highlightedLogRecord: css({ background: `${theme.colors.primary.transparent} !important`, From 8c935c8f4a003a6aea18e4c0acbdf2734a261ad7 Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Wed, 26 Feb 2025 16:17:35 +0100 Subject: [PATCH 03/13] [unistore] Add benchmark for write throughput (#101345) * Add generic benchmark * address comments --- pkg/storage/unified/sql/backend.go | 29 ++- .../unified/sql/test/benchmark_test.go | 41 +++++ pkg/storage/unified/testing/benchmark.go | 171 ++++++++++++++++++ 3 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 pkg/storage/unified/sql/test/benchmark_test.go create mode 100644 pkg/storage/unified/testing/benchmark.go diff --git a/pkg/storage/unified/sql/backend.go b/pkg/storage/unified/sql/backend.go index 024bc5ab7d0..c404e467eed 100644 --- a/pkg/storage/unified/sql/backend.go +++ b/pkg/storage/unified/sql/backend.go @@ -40,6 +40,9 @@ type BackendOptions struct { PollingInterval time.Duration WatchBufferSize int IsHA bool + + // testing + SimulatedNetworkLatency time.Duration // slows down the create transactions by a fixed amount } func NewBackend(opts BackendOptions) (Backend, error) { @@ -58,15 +61,16 @@ func NewBackend(opts BackendOptions) (Backend, error) { opts.WatchBufferSize = defaultWatchBufferSize } return &backend{ - isHA: opts.IsHA, - done: ctx.Done(), - cancel: cancel, - log: log.New("sql-resource-server"), - tracer: opts.Tracer, - dbProvider: opts.DBProvider, - pollingInterval: opts.PollingInterval, - watchBufferSize: opts.WatchBufferSize, - batchLock: &batchLock{running: make(map[string]bool)}, + isHA: opts.IsHA, + done: ctx.Done(), + cancel: cancel, + log: log.New("sql-resource-server"), + tracer: opts.Tracer, + dbProvider: opts.DBProvider, + pollingInterval: opts.PollingInterval, + watchBufferSize: opts.WatchBufferSize, + batchLock: &batchLock{running: make(map[string]bool)}, + simulatedNetworkLatency: opts.SimulatedNetworkLatency, }, nil } @@ -95,6 +99,9 @@ type backend struct { pollingInterval time.Duration watchBufferSize int notifier eventNotifier + + // testing + simulatedNetworkLatency time.Duration } func (b *backend) Init(ctx context.Context) error { @@ -249,7 +256,9 @@ func (b *backend) create(ctx context.Context, event resource.WriteEvent) (int64, return fmt.Errorf("update resource rv: %w", err) } newVersion = rv - + if b.simulatedNetworkLatency > 0 { + time.Sleep(b.simulatedNetworkLatency) + } return nil }) diff --git a/pkg/storage/unified/sql/test/benchmark_test.go b/pkg/storage/unified/sql/test/benchmark_test.go new file mode 100644 index 00000000000..9ac1713db66 --- /dev/null +++ b/pkg/storage/unified/sql/test/benchmark_test.go @@ -0,0 +1,41 @@ +package test + +import ( + "context" + "testing" + "time" + + infraDB "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/grafana/grafana/pkg/storage/unified/sql" + "github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl" + test "github.com/grafana/grafana/pkg/storage/unified/testing" + "github.com/stretchr/testify/require" +) + +func newTestBackend(b *testing.B) resource.StorageBackend { + dbstore := infraDB.InitTestDB(b) + eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil) + require.NoError(b, err) + require.NotNil(b, eDB) + + backend, err := sql.NewBackend(sql.BackendOptions{ + DBProvider: eDB, + IsHA: true, + SimulatedNetworkLatency: 5 * time.Millisecond, // to simulate some network latency + }) + require.NoError(b, err) + require.NotNil(b, backend) + err = backend.Init(context.Background()) + require.NoError(b, err) + return backend +} + +func BenchmarkSQLStorageBackend(b *testing.B) { + opts := test.DefaultBenchmarkOptions() + if infraDB.IsTestDbSQLite() { + opts.Concurrency = 1 // to avoid SQLite database is locked error + } + test.BenchmarkStorageBackend(b, newTestBackend(b), opts) +} diff --git a/pkg/storage/unified/testing/benchmark.go b/pkg/storage/unified/testing/benchmark.go new file mode 100644 index 00000000000..0f16e5a7d15 --- /dev/null +++ b/pkg/storage/unified/testing/benchmark.go @@ -0,0 +1,171 @@ +package test + +import ( + "context" + "fmt" + "sort" + "strings" + "sync" + "testing" + "time" + + "github.com/grafana/grafana/pkg/storage/unified/resource" + "github.com/stretchr/testify/require" +) + +// BenchmarkOptions configures the benchmark parameters +type BenchmarkOptions struct { + NumResources int // total number of resources to write + Concurrency int // number of concurrent writers + NumNamespaces int // number of different namespaces + NumGroups int // number of different groups + NumResourceTypes int // number of different resource types +} + +// DefaultBenchmarkOptions returns the default benchmark options +func DefaultBenchmarkOptions() *BenchmarkOptions { + return &BenchmarkOptions{ + NumResources: 1000, + Concurrency: 20, + NumNamespaces: 1, + NumGroups: 1, + NumResourceTypes: 1, + } +} + +// BenchmarkResult contains the benchmark metrics +type BenchmarkResult struct { + TotalDuration time.Duration + WriteCount int + Throughput float64 // writes per second + P50Latency time.Duration + P90Latency time.Duration + P99Latency time.Duration +} + +// runStorageBackendBenchmark runs a write throughput benchmark +func runStorageBackendBenchmark(ctx context.Context, backend resource.StorageBackend, opts *BenchmarkOptions) (*BenchmarkResult, error) { + if opts == nil { + opts = DefaultBenchmarkOptions() + } + + // Create channels for workers + jobs := make(chan int, opts.NumResources) + results := make(chan time.Duration, opts.NumResources) + errors := make(chan error, opts.NumResources) + + // Fill the jobs channel + for i := 0; i < opts.NumResources; i++ { + jobs <- i + } + close(jobs) + + var wg sync.WaitGroup + + // Initialize each group and resource type combination in the init namespace + namespace := "ns-init" + for g := 0; g < opts.NumGroups; g++ { + group := fmt.Sprintf("group-%d", g) + for r := 0; r < opts.NumResourceTypes; r++ { + resourceType := fmt.Sprintf("resource-%d", r) + _, err := writeEvent(ctx, backend, "init", resource.WatchEvent_ADDED, + WithNamespace(namespace), + WithGroup(group), + WithResource(resourceType), + WithValue([]byte("init"))) + if err != nil { + return nil, fmt.Errorf("failed to initialize backend: %w", err) + } + } + } + // Start workers + startTime := time.Now() + for workerID := 0; workerID < opts.Concurrency; workerID++ { + wg.Add(1) + go func() { + defer wg.Done() + for jobID := range jobs { + // Calculate a unique ID for this job that's guaranteed to be unique across all workers + uniqueID := jobID + + // Generate deterministic and unique resource details + namespace := fmt.Sprintf("ns-%d", uniqueID%opts.NumNamespaces) + group := fmt.Sprintf("group-%d", uniqueID%opts.NumGroups) + resourceType := fmt.Sprintf("resource-%d", uniqueID%opts.NumResourceTypes) + // Ensure name is unique by using the global uniqueID + name := fmt.Sprintf("item-%d", uniqueID) + + writeStart := time.Now() + _, err := writeEvent(ctx, backend, name, resource.WatchEvent_ADDED, + WithNamespace(namespace), + WithGroup(group), + WithResource(resourceType), + WithValue([]byte(strings.Repeat("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", 20)))) // ~1.21 KiB + + if err != nil { + errors <- err + return + } + + results <- time.Since(writeStart) + } + }() + } + + // Wait for all workers to complete + wg.Wait() + close(results) + close(errors) + + // Check for errors + if len(errors) > 0 { + return nil, <-errors // Return the first error encountered + } + + // Collect all latencies + latencies := make([]time.Duration, 0, opts.NumResources) + for latency := range results { + latencies = append(latencies, latency) + } + + // Sort latencies for percentile calculation + sort.Slice(latencies, func(i, j int) bool { + return latencies[i] < latencies[j] + }) + + totalDuration := time.Since(startTime) + throughput := float64(opts.NumResources) / totalDuration.Seconds() + + return &BenchmarkResult{ + TotalDuration: totalDuration, + WriteCount: opts.NumResources, + Throughput: throughput, + P50Latency: latencies[len(latencies)*50/100], + P90Latency: latencies[len(latencies)*90/100], + P99Latency: latencies[len(latencies)*99/100], + }, nil +} + +// BenchmarkStorageBackend runs a benchmark test for a storage backend implementation +func BenchmarkStorageBackend(b *testing.B, backend resource.StorageBackend, opts *BenchmarkOptions) { + ctx := context.Background() + + result, err := runStorageBackendBenchmark(ctx, backend, opts) + require.NoError(b, err) + + b.ReportMetric(result.Throughput, "writes/sec") + b.ReportMetric(float64(result.P50Latency.Milliseconds()), "p50-latency-ms") + b.ReportMetric(float64(result.P90Latency.Milliseconds()), "p90-latency-ms") + b.ReportMetric(float64(result.P99Latency.Milliseconds()), "p99-latency-ms") + + // Also log the results for better visibility + b.Logf("Benchmark Configuration: Workers=%d, Resources=%d, Namespaces=%d, Groups=%d, Resource Types=%d", opts.Concurrency, opts.NumResources, opts.NumNamespaces, opts.NumGroups, opts.NumResourceTypes) + b.Logf("") + b.Logf("Benchmark Results:") + b.Logf("Total Duration: %v", result.TotalDuration) + b.Logf("Write Count: %d", result.WriteCount) + b.Logf("Throughput: %.2f writes/sec", result.Throughput) + b.Logf("P50 Latency: %v", result.P50Latency) + b.Logf("P90 Latency: %v", result.P90Latency) + b.Logf("P99 Latency: %v", result.P99Latency) +} From 6614f5c3b2de7fc6d58e56ef57b660acd0cb32d3 Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Wed, 26 Feb 2025 16:08:20 +0000 Subject: [PATCH 04/13] Log Context: Unify pinnedLogs and pinnedRowId props (#101067) --- public/app/features/logs/components/LogRows.tsx | 3 +-- .../logs/components/log-context/LogRowContextModal.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/logs/components/LogRows.tsx b/public/app/features/logs/components/LogRows.tsx index 206e3b88023..f0e037c5665 100644 --- a/public/app/features/logs/components/LogRows.tsx +++ b/public/app/features/logs/components/LogRows.tsx @@ -61,7 +61,6 @@ export interface Props { permalinkedRowId?: string; scrollIntoView?: (element: HTMLElement) => void; isFilterLabelActive?: (key: string, value: string, refId?: string) => Promise; - pinnedRowId?: string; pinnedLogs?: string[]; /** * If false or undefined, the `contain:strict` css property will be added to the wrapping `` for performance reasons. @@ -305,7 +304,7 @@ export const LogRows = memo( onPinLine={props.onPinLine} onUnpinLine={props.onUnpinLine} pinLineButtonTooltipTitle={props.pinLineButtonTooltipTitle} - pinned={props.pinnedRowId === row.uid || pinnedLogs?.some((logId) => logId === row.rowId)} + pinned={pinnedLogs?.some((logId) => logId === row.rowId || logId === row.uid)} isFilterLabelActive={props.isFilterLabelActive} handleTextSelection={handleSelection} enableLogDetails={enableLogDetails} diff --git a/public/app/features/logs/components/log-context/LogRowContextModal.tsx b/public/app/features/logs/components/log-context/LogRowContextModal.tsx index 798e03ce065..60cb0d6a092 100644 --- a/public/app/features/logs/components/log-context/LogRowContextModal.tsx +++ b/public/app/features/logs/components/log-context/LogRowContextModal.tsx @@ -561,7 +561,7 @@ export const LogRowContextModal: React.FunctionComponent setSticky(false)} onPinLine={() => setSticky(true)} - pinnedRowId={sticky ? row.uid : undefined} + pinnedLogs={sticky ? [row.uid] : undefined} overflowingContent={true} scrollElement={null} /> From c3505f08646f51f7db3ee58ce1e78439d4417a58 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 26 Feb 2025 17:29:32 +0100 Subject: [PATCH 05/13] AuthZ: Make `NewGrpcTokenAuth` public (#101352) * AuthZ: Expose NewGrpcTokenAuth * Lint --- pkg/services/authz/rbac.go | 6 +++--- pkg/services/authz/token_auth.go | 3 ++- pkg/services/authz/zanzana.go | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index fcb398667ab..215bd3dbad6 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -33,8 +33,8 @@ import ( "github.com/grafana/grafana/pkg/storage/legacysql" ) -// `authzService` is hardcoded in authz-service -const authzServiceAudience = "authzService" +// AuthzServiceAudience is the audience for the authz service. +const AuthzServiceAudience = "authzService" // ProvideAuthZClient provides an AuthZ client and creates the AuthZ service. func ProvideAuthZClient( @@ -123,7 +123,7 @@ func newRemoteRBACClient(clientCfg *authzClientSettings, tracer tracing.Tracer) clientCfg.remoteAddress, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials( - newGRPCTokenAuth(authzServiceAudience, clientCfg.tokenNamespace, tokenClient), + NewGRPCTokenAuth(AuthzServiceAudience, clientCfg.tokenNamespace, tokenClient), ), ) if err != nil { diff --git a/pkg/services/authz/token_auth.go b/pkg/services/authz/token_auth.go index 1546379741e..6014d445df3 100644 --- a/pkg/services/authz/token_auth.go +++ b/pkg/services/authz/token_auth.go @@ -6,7 +6,8 @@ import ( "github.com/grafana/authlib/authn" ) -func newGRPCTokenAuth(audience, namespace string, tc authn.TokenExchanger) *tokenAuth { +// TODO: move this to authlib +func NewGRPCTokenAuth(audience, namespace string, tc authn.TokenExchanger) *tokenAuth { return &tokenAuth{audience, namespace, tc} } diff --git a/pkg/services/authz/zanzana.go b/pkg/services/authz/zanzana.go index 8d644167528..6067a232082 100644 --- a/pkg/services/authz/zanzana.go +++ b/pkg/services/authz/zanzana.go @@ -57,7 +57,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features // TODO: add TLS support grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials( - newGRPCTokenAuth(authzServiceAudience, fmt.Sprintf("stacks-%s", cfg.StackID), tokenClient), + NewGRPCTokenAuth(AuthzServiceAudience, fmt.Sprintf("stacks-%s", cfg.StackID), tokenClient), ), } @@ -172,7 +172,7 @@ func (z *Zanzana) start(ctx context.Context) error { authenticator := authnlib.NewAccessTokenAuthenticator( authnlib.NewAccessTokenVerifier( - authnlib.VerifierConfig{AllowedAudiences: []string{authzServiceAudience}}, + authnlib.VerifierConfig{AllowedAudiences: []string{AuthzServiceAudience}}, authnlib.NewKeyRetriever(authnlib.KeyRetrieverConfig{ SigningKeysURL: z.cfg.ZanzanaServer.SigningKeysURL, }), From b5faf5d9a12a68f2fe8063c1ecefe7e0e97ee64c Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Wed, 26 Feb 2025 17:33:05 +0100 Subject: [PATCH 06/13] Drilldown: Require `datasources:explore` RBAC action (#101366) Drilldown: Require `datasources:explore` acton --- pkg/api/accesscontrol.go | 3 --- pkg/api/api.go | 2 +- pkg/services/accesscontrol/models.go | 3 +-- pkg/services/navtree/navtreeimpl/navtree.go | 2 +- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 0f1c4efe772..0ad8d8eeedc 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -66,9 +66,6 @@ func (hs *HTTPServer) declareFixedRoles() error { { Action: ac.ActionDatasourcesExplore, }, - { - Action: ac.ActionDatasourcesDrilldown, - }, }, }, Grants: []string{string(org.RoleEditor)}, diff --git a/pkg/api/api.go b/pkg/api/api.go index 18e52419ece..61ced28f276 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -183,7 +183,7 @@ func (hs *HTTPServer) registerRoutes() { } r.Get("/explore", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) - r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesDrilldown)), hs.Index) + r.Get("/drilldown", authorize(ac.EvalPermission(ac.ActionDatasourcesExplore)), hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) r.Get("/playlists/*", reqSignedIn, hs.Index) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 4c15e3c5e56..dc12171eaa5 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -386,8 +386,7 @@ const ( ActionSettingsWrite = "settings:write" // Datasources actions - ActionDatasourcesExplore = "datasources:explore" - ActionDatasourcesDrilldown = "datasources:drilldown" + ActionDatasourcesExplore = "datasources:explore" // Global Scopes ScopeGlobalUsersAll = "global.users:*" diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 7158c8884dc..d84240556f8 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -130,7 +130,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere }) } - if hasAccess(ac.EvalPermission(ac.ActionDatasourcesDrilldown)) { + if hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { drilldownChildNavLinks := s.buildDrilldownNavLinks(c) treeRoot.AddSection(&navtree.NavLink{ Text: "Drilldown", From 32fde6dba4be14fc90d58b07d97d5b3585b50d86 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Wed, 26 Feb 2025 14:39:39 -0500 Subject: [PATCH 07/13] Alerting: Update scheduler to provide full specification to rule update channel (#101375) update scheduler's aler rule to accept regular Evaluation in update channel This makes it accept the full rule definition, which is required in reset state. --- pkg/services/ngalert/schedule/alert_rule.go | 33 +++++++------------ .../ngalert/schedule/alert_rule_test.go | 21 ++++++------ .../ngalert/schedule/recording_rule.go | 2 +- .../ngalert/schedule/recording_rule_test.go | 8 +++-- pkg/services/ngalert/schedule/registry.go | 5 --- pkg/services/ngalert/schedule/schedule.go | 7 ++-- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/pkg/services/ngalert/schedule/alert_rule.go b/pkg/services/ngalert/schedule/alert_rule.go index 85c254b0a59..771d32a2573 100644 --- a/pkg/services/ngalert/schedule/alert_rule.go +++ b/pkg/services/ngalert/schedule/alert_rule.go @@ -37,7 +37,7 @@ type Rule interface { // It has no effect if the rule has not yet been Run, or if the rule is Stopped. Eval(eval *Evaluation) (bool, *Evaluation) // Update sends a singal to change the definition of the rule. - Update(lastVersion RuleVersionAndPauseStatus) bool + Update(eval *Evaluation) bool // Type gives the type of the rule. Type() ngmodels.RuleType // Status indicates the status of the evaluating rule. @@ -59,7 +59,6 @@ func newRuleFactory( sender AlertsSender, stateManager *state.Manager, evalFactory eval.EvaluatorFactory, - ruleProvider ruleProvider, clock clock.Clock, rrCfg setting.RecordingRuleSettings, met *metrics.Scheduler, @@ -95,7 +94,6 @@ func newRuleFactory( sender, stateManager, evalFactory, - ruleProvider, clock, met, logger, @@ -109,15 +107,11 @@ func newRuleFactory( type evalAppliedFunc = func(ngmodels.AlertRuleKey, time.Time) type stopAppliedFunc = func(ngmodels.AlertRuleKey) -type ruleProvider interface { - get(ngmodels.AlertRuleKey) *ngmodels.AlertRule -} - type alertRule struct { key ngmodels.AlertRuleKeyWithGroup evalCh chan *Evaluation - updateCh chan RuleVersionAndPauseStatus + updateCh chan *Evaluation ctx context.Context stopFn util.CancelCauseFunc @@ -129,7 +123,6 @@ type alertRule struct { sender AlertsSender stateManager *state.Manager evalFactory eval.EvaluatorFactory - ruleProvider ruleProvider // Event hooks that are only used in tests. evalAppliedHook evalAppliedFunc @@ -149,7 +142,6 @@ func newAlertRule( sender AlertsSender, stateManager *state.Manager, evalFactory eval.EvaluatorFactory, - ruleProvider ruleProvider, clock clock.Clock, met *metrics.Scheduler, logger log.Logger, @@ -161,7 +153,7 @@ func newAlertRule( return &alertRule{ key: key, evalCh: make(chan *Evaluation), - updateCh: make(chan RuleVersionAndPauseStatus), + updateCh: make(chan *Evaluation), ctx: ctx, stopFn: stop, appURL: appURL, @@ -171,7 +163,6 @@ func newAlertRule( sender: sender, stateManager: stateManager, evalFactory: evalFactory, - ruleProvider: ruleProvider, evalAppliedHook: evalAppliedHook, stopAppliedHook: stopAppliedHook, metrics: met, @@ -220,7 +211,7 @@ func (a *alertRule) Eval(eval *Evaluation) (bool, *Evaluation) { } // update sends an instruction to the rule evaluation routine to update the scheduled rule to the specified version. The specified version must be later than the current version, otherwise no update will happen. -func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool { +func (a *alertRule) Update(eval *Evaluation) bool { // check if the channel is not empty. select { case <-a.updateCh: @@ -230,7 +221,7 @@ func (a *alertRule) Update(lastVersion RuleVersionAndPauseStatus) bool { } select { - case a.updateCh <- lastVersion: + case a.updateCh <- eval: return true case <-a.ctx.Done(): return false @@ -254,15 +245,16 @@ func (a *alertRule) Run() error { select { // used by external services (API) to notify that rule is updated. case ctx := <-a.updateCh: - if currentFingerprint == ctx.Fingerprint { + fp := ctx.Fingerprint() + if currentFingerprint == fp { a.logger.Info("Rule's fingerprint has not changed. Skip resetting the state", "currentFingerprint", currentFingerprint) continue } - a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.IsPaused, "fingerprint", ctx.Fingerprint) + a.logger.Info("Clearing the state of the rule because it was updated", "isPaused", ctx.rule.IsPaused, "fingerprint", fp) // clear the state. So the next evaluation will start from the scratch. - a.resetState(grafanaCtx, ctx.IsPaused) - currentFingerprint = ctx.Fingerprint + a.resetState(grafanaCtx, ctx.rule, ctx.rule.IsPaused) + currentFingerprint = fp // evalCh - used by the scheduler to signal that evaluation is needed. case ctx, ok := <-a.evalCh: if !ok { @@ -298,7 +290,7 @@ func (a *alertRule) Run() error { // lingers in DB and won't be cleaned up until next alert rule update. needReset = needReset || (currentFingerprint == 0 && isPaused) if needReset { - a.resetState(grafanaCtx, isPaused) + a.resetState(grafanaCtx, ctx.rule, isPaused) } currentFingerprint = f if isPaused { @@ -494,8 +486,7 @@ func (a *alertRule) expireAndSend(ctx context.Context, states []state.StateTrans } } -func (a *alertRule) resetState(ctx context.Context, isPaused bool) { - rule := a.ruleProvider.get(a.key.AlertRuleKey) +func (a *alertRule) resetState(ctx context.Context, rule *ngmodels.AlertRule, isPaused bool) { reason := ngmodels.StateReasonUpdated if isPaused { reason = ngmodels.StateReasonPaused diff --git a/pkg/services/ngalert/schedule/alert_rule_test.go b/pkg/services/ngalert/schedule/alert_rule_test.go index 02ea0dd5735..b9f66875c6b 100644 --- a/pkg/services/ngalert/schedule/alert_rule_test.go +++ b/pkg/services/ngalert/schedule/alert_rule_test.go @@ -43,7 +43,7 @@ func TestAlertRule(t *testing.T) { r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) resultCh := make(chan bool) go func() { - resultCh <- r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + resultCh <- r.Update(&Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()}) }() select { case <-r.updateCh: @@ -54,8 +54,8 @@ func TestAlertRule(t *testing.T) { }) t.Run("update should drop any concurrent sending to updateCh", func(t *testing.T) { r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) - version1 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} - version2 := RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false} + version1 := &Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()} + version2 := &Evaluation{rule: gen.With(gen.WithIsPaused(false)).GenerateRef()} wg := sync.WaitGroup{} wg.Add(1) @@ -178,7 +178,7 @@ func TestAlertRule(t *testing.T) { r := blankRuleForTests(context.Background(), models.GenerateRuleKeyWithGroup(1)) r.Stop(errRuleDeleted) require.ErrorIs(t, r.ctx.Err(), errRuleDeleted) - require.False(t, r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false})) + require.False(t, r.Update(&Evaluation{rule: gen.GenerateRef()})) }) t.Run("eval should do nothing", func(t *testing.T) { ruleSpec := gen.GenerateRef() @@ -234,7 +234,7 @@ func TestAlertRule(t *testing.T) { } switch rand.Intn(max) + 1 { case 1: - r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + r.Update(&Evaluation{rule: gen.GenerateRef()}) case 2: r.Eval(&Evaluation{ scheduledAt: time.Now(), @@ -284,7 +284,7 @@ func blankRuleForTests(ctx context.Context, key models.AlertRuleKeyWithGroup) *a Log: log.NewNopLogger(), } st := state.NewManager(managerCfg, state.NewNoopPersister()) - return newAlertRule(ctx, key, nil, false, 0, nil, st, nil, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) + return newAlertRule(ctx, key, nil, false, 0, nil, st, nil, nil, nil, log.NewNopLogger(), nil, nil, nil) } func TestRuleRoutine(t *testing.T) { @@ -572,7 +572,6 @@ func TestRuleRoutine(t *testing.T) { t.Run("when a message is sent to update channel", func(t *testing.T) { rule := gen.With(withQueryForState(t, eval.Normal)).GenerateRef() folderTitle := "folderName" - ruleFp := ruleWithFolder{rule, folderTitle}.Fingerprint() evalAppliedChan := make(chan time.Time) @@ -628,8 +627,8 @@ func TestRuleRoutine(t *testing.T) { require.Greaterf(t, expectedToBeSent, 0, "State manager was expected to return at least one state that can be expired") t.Run("should do nothing if version in channel is the same", func(t *testing.T) { - ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) - ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp, false}) // second time just to make sure that previous messages were handled + ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) + ruleInfo.Update(&Evaluation{rule: rule, folderTitle: folderTitle}) // second time just to make sure that previous messages were handled actualStates := sch.stateManager.GetStatesForRuleUID(rule.OrgID, rule.UID) require.Len(t, actualStates, len(states)) @@ -638,7 +637,7 @@ func TestRuleRoutine(t *testing.T) { }) t.Run("should clear the state and expire firing alerts if version in channel is greater", func(t *testing.T) { - ruleInfo.Update(RuleVersionAndPauseStatus{ruleFp + 1, false}) + ruleInfo.Update(&Evaluation{rule: models.CopyRule(rule, gen.WithTitle(util.GenerateShortUID())), folderTitle: folderTitle}) require.Eventually(t, func() bool { return len(sender.Calls()) > 0 @@ -905,7 +904,7 @@ func TestRuleRoutine(t *testing.T) { } func ruleFactoryFromScheduler(sch *schedule) ruleFactory { - return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, &sch.schedulableAlertRules, sch.clock, sch.rrCfg, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc) + return newRuleFactory(sch.appURL, sch.disableGrafanaFolder, sch.maxAttempts, sch.alertsSender, sch.stateManager, sch.evaluatorFactory, sch.clock, sch.rrCfg, sch.metrics, sch.log, sch.tracer, sch.recordingWriter, sch.evalAppliedFunc, sch.stopAppliedFunc) } func stateForRule(rule *models.AlertRule, ts time.Time, evalState eval.State) *state.State { diff --git a/pkg/services/ngalert/schedule/recording_rule.go b/pkg/services/ngalert/schedule/recording_rule.go index 954b0869a4a..6c3f733a5ca 100644 --- a/pkg/services/ngalert/schedule/recording_rule.go +++ b/pkg/services/ngalert/schedule/recording_rule.go @@ -113,7 +113,7 @@ func (r *recordingRule) Eval(eval *Evaluation) (bool, *Evaluation) { } } -func (r *recordingRule) Update(lastVersion RuleVersionAndPauseStatus) bool { +func (r *recordingRule) Update(_ *Evaluation) bool { return true } diff --git a/pkg/services/ngalert/schedule/recording_rule_test.go b/pkg/services/ngalert/schedule/recording_rule_test.go index 7e72453bb9f..c880b1d417f 100644 --- a/pkg/services/ngalert/schedule/recording_rule_test.go +++ b/pkg/services/ngalert/schedule/recording_rule_test.go @@ -17,6 +17,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/metrics" @@ -116,7 +117,10 @@ func TestRecordingRule(t *testing.T) { } switch rand.Intn(max) + 1 { case 1: - r.Update(RuleVersionAndPauseStatus{fingerprint(rand.Uint64()), false}) + r.Update(&Evaluation{ + rule: gen.GenerateRef(), + folderTitle: util.GenerateShortUID(), + }) case 2: r.Eval(&Evaluation{ scheduledAt: time.Now(), @@ -492,7 +496,7 @@ func TestRecordingRule_Integration(t *testing.T) { t.Run("status shows evaluation", func(t *testing.T) { status := process.(*recordingRule).Status() - //TODO: assert "error" to fix test, update to "nodata" in the future + // TODO: assert "error" to fix test, update to "nodata" in the future require.Equal(t, "error", status.Health) }) }) diff --git a/pkg/services/ngalert/schedule/registry.go b/pkg/services/ngalert/schedule/registry.go index 11f72229f51..8ef0c87ddde 100644 --- a/pkg/services/ngalert/schedule/registry.go +++ b/pkg/services/ngalert/schedule/registry.go @@ -87,11 +87,6 @@ func (r *ruleRegistry) keyMap() map[models.AlertRuleKey]struct{} { return definitionsIDs } -type RuleVersionAndPauseStatus struct { - Fingerprint fingerprint - IsPaused bool -} - type Evaluation struct { scheduledAt time.Time rule *models.AlertRule diff --git a/pkg/services/ngalert/schedule/schedule.go b/pkg/services/ngalert/schedule/schedule.go index 3bc21115ca0..0416fa2fb7e 100644 --- a/pkg/services/ngalert/schedule/schedule.go +++ b/pkg/services/ngalert/schedule/schedule.go @@ -302,7 +302,6 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. sch.alertsSender, sch.stateManager, sch.evaluatorFactory, - &sch.schedulableAlertRules, sch.clock, sch.rrCfg, sch.metrics, @@ -372,9 +371,9 @@ func (sch *schedule) processTick(ctx context.Context, dispatcherGroup *errgroup. // if we do not need to eval the rule, check the whether rule was just updated and if it was, notify evaluation routine about that logger.Debug("Rule has been updated. Notifying evaluation routine") go func(routine Rule, rule *ngmodels.AlertRule) { - routine.Update(RuleVersionAndPauseStatus{ - Fingerprint: ruleWithFolder{rule: rule, folderTitle: folderTitle}.Fingerprint(), - IsPaused: rule.IsPaused, + routine.Update(&Evaluation{ + rule: rule, + folderTitle: folderTitle, }) }(ruleRoutine, item) updatedRules = append(updatedRules, ngmodels.AlertRuleKeyWithVersion{ From 4c021aac7a9a8ff54927841f03d9be9f5dbd0000 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:51:10 -0600 Subject: [PATCH 08/13] Table: Remove actions from context menu (#101318) --- .../DataLinks/DataLinksContextMenu.test.tsx | 41 +------------------ .../DataLinks/DataLinksContextMenu.tsx | 13 ++---- .../src/components/Table/BarGaugeCell.tsx | 7 ++-- .../src/components/Table/DefaultCell.tsx | 8 ++-- .../src/components/Table/ImageCell.tsx | 6 +-- .../src/components/Table/JSONViewCell.tsx | 7 ++-- packages/grafana-ui/src/utils/dataLinks.ts | 13 +----- 7 files changed, 18 insertions(+), 77 deletions(-) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx index cb7870abfad..64d7049d8d4 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.test.tsx @@ -6,7 +6,7 @@ import { DataLinksContextMenu } from './DataLinksContextMenu'; const fakeAriaLabel = 'fake aria label'; describe('DataLinksContextMenu', () => { - it('renders context menu when there are more than one data links or actions', () => { + it('renders context menu when there are more than one data links', () => { render( [ @@ -23,7 +23,6 @@ describe('DataLinksContextMenu', () => { origin: {}, }, ]} - actions={[{ title: 'Action1', onClick: () => {} }]} > {() => { return
; @@ -35,43 +34,7 @@ describe('DataLinksContextMenu', () => { expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); }); - it('renders context menu when there are actions and one data link', () => { - render( - [ - { - href: '/link1', - title: 'Link1', - target: '_blank', - origin: {}, - }, - ]} - actions={[{ title: 'Action1', onClick: () => {} }]} - > - {() => { - return
; - }} - - ); - - expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument(); - expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); - }); - - it('renders context menu when there are only actions', () => { - render( - []} actions={[{ title: 'Action1', onClick: () => {} }]}> - {() => { - return
; - }} - - ); - - expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument(); - expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); - }); - - it('renders link when there is a single data link and no actions', () => { + it('renders link when there is a single data link', () => { render( [ diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 8633ade3318..928811bb173 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -2,11 +2,11 @@ import { css } from '@emotion/css'; import { CSSProperties } from 'react'; import * as React from 'react'; -import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { GrafanaTheme2, LinkModel } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { useStyles2 } from '../../themes'; -import { actionModelToContextMenuItems, linkModelToContextMenuItems } from '../../utils/dataLinks'; +import { linkModelToContextMenuItems } from '../../utils/dataLinks'; import { WithContextMenu } from '../ContextMenu/WithContextMenu'; import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; import { MenuItem } from '../Menu/MenuItem'; @@ -15,7 +15,6 @@ export interface DataLinksContextMenuProps { children: (props: DataLinksContextMenuApi) => JSX.Element; links: () => LinkModel[]; style?: CSSProperties; - actions?: ActionModel[]; } export interface DataLinksContextMenuApi { @@ -23,16 +22,12 @@ export interface DataLinksContextMenuApi { targetClassName?: string; } -export const DataLinksContextMenu = ({ children, links, actions, style }: DataLinksContextMenuProps) => { +export const DataLinksContextMenu = ({ children, links, style }: DataLinksContextMenuProps) => { const styles = useStyles2(getStyles); const itemsGroup: MenuItemsGroup[] = [ { items: linkModelToContextMenuItems(links), label: Boolean(links().length) ? 'Data links' : '' }, ]; - const hasActions = Boolean(actions?.length); - if (hasActions) { - itemsGroup.push({ items: actionModelToContextMenuItems(actions!), label: 'Actions' }); - } const linksCounter = itemsGroup[0].items.length; const renderMenuGroupItems = () => { @@ -59,7 +54,7 @@ export const DataLinksContextMenu = ({ children, links, actions, style }: DataLi cursor: 'context-menu', }); - if (linksCounter > 1 || hasActions) { + if (linksCounter > 1) { return ( {({ openMenu }) => { diff --git a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx index f22643b3ba8..a427451f0c8 100644 --- a/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx +++ b/packages/grafana-ui/src/components/Table/BarGaugeCell.tsx @@ -24,7 +24,7 @@ const defaultScale: ThresholdsConfig = { }; export const BarGaugeCell = (props: TableCellProps) => { - const { field, innerWidth, tableStyles, cell, cellProps, row, actions } = props; + const { field, innerWidth, tableStyles, cell, cellProps, row } = props; const displayValue = field.display!(cell.value); const cellOptions = getCellOptions(field); @@ -56,7 +56,6 @@ export const BarGaugeCell = (props: TableCellProps) => { }; const hasLinks = Boolean(getLinks().length); - const hasActions = Boolean(actions?.length); const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index); const renderComponent = (menuProps: DataLinksContextMenuApi) => { @@ -85,8 +84,8 @@ export const BarGaugeCell = (props: TableCellProps) => { return (
- {hasLinks || hasActions ? ( - + {hasLinks ? ( + {(api) => renderComponent(api)} ) : ( diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 652aae07f0c..2ce27572494 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -17,8 +17,7 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ import { getCellColors, getCellOptions } from './utils'; export const DefaultCell = (props: TableCellProps) => { - const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height, actions } = - props; + const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props; const inspectEnabled = Boolean(field.config.custom?.inspect); const displayValue = field.display!(cell.value); @@ -26,7 +25,6 @@ export const DefaultCell = (props: TableCellProps) => { const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; const cellOptions = getCellOptions(field); const hasLinks = Boolean(getCellLinks(field, row)?.length); - const hasActions = Boolean(actions?.length); const clearButtonStyle = useStyles2(clearLinkButtonStyles); let value: string | ReactElement; @@ -81,8 +79,8 @@ export const DefaultCell = (props: TableCellProps) => { return (
- {hasLinks || hasActions ? ( - getCellLinks(field, row) || []} actions={actions}> + {hasLinks ? ( + getCellLinks(field, row) || []}> {(api) => { if (api.openMenu) { return ( diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/ImageCell.tsx index 65fce6214d8..40f8b03dd29 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/ImageCell.tsx @@ -9,13 +9,12 @@ import { getCellOptions } from './utils'; const DATALINKS_HEIGHT_OFFSET = 10; export const ImageCell = (props: TableCellProps) => { - const { field, cell, tableStyles, row, cellProps, actions } = props; + const { field, cell, tableStyles, row, cellProps } = props; const cellOptions = getCellOptions(field); const { title, alt } = cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined }; const displayValue = field.display!(cell.value); const hasLinks = Boolean(getCellLinks(field, row)?.length); - const hasActions = Boolean(actions?.length); // The image element const img = ( @@ -32,11 +31,10 @@ export const ImageCell = (props: TableCellProps) => {
{/* If there are data links/actions, we render them with image */} {/* Otherwise we simply render the image */} - {hasLinks || hasActions ? ( + {hasLinks ? ( getCellLinks(field, row) || []} - actions={actions} > {(api) => { if (api.openMenu) { diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx index ea2a3092576..e5f9e5916a8 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx @@ -11,7 +11,7 @@ import { TableCellInspectorMode } from './TableCellInspector'; import { TableCellProps } from './types'; export function JSONViewCell(props: TableCellProps): JSX.Element { - const { cell, tableStyles, cellProps, field, row, actions } = props; + const { cell, tableStyles, cellProps, field, row } = props; const inspectEnabled = Boolean(field.config.custom?.inspect); const txt = css({ cursor: 'pointer', @@ -30,14 +30,13 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { } const hasLinks = Boolean(getCellLinks(field, row)?.length); - const hasActions = Boolean(actions?.length); const clearButtonStyle = useStyles2(clearLinkButtonStyles); return (
- {hasLinks || hasActions ? ( - getCellLinks(field, row) || []} actions={actions}> + {hasLinks ? ( + getCellLinks(field, row) || []}> {(api) => { if (api.openMenu) { return ( diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts index f889cf5a2a0..5d2a6db23fd 100644 --- a/packages/grafana-ui/src/utils/dataLinks.ts +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -1,4 +1,4 @@ -import { ActionModel, LinkModel } from '@grafana/data'; +import { LinkModel } from '@grafana/data'; import { MenuItemProps } from '../components/Menu/MenuItem'; @@ -18,14 +18,3 @@ export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem }; }); }; - -export const actionModelToContextMenuItems: (actions: ActionModel[]) => MenuItemProps[] = (actions) => { - return actions.map((action) => { - return { - label: action.title, - ariaLabel: action.title, - icon: 'record-audio', - onClick: action.onClick, - }; - }); -}; From 2b0029267cfbcc65793a4922919afc4a2748c0a0 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 17:15:01 -0600 Subject: [PATCH 09/13] Actions: Remove `vizActions` feature toggle (#100309) --- .../configure-grafana/feature-toggles/index.md | 1 - packages/grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 8 -------- pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/featuremgmt/toggles_gen.json | 3 ++- public/app/core/components/OptionsUI/registry.tsx | 4 +--- public/app/features/actions/ActionEditor.tsx | 6 +----- .../panel/canvas/editor/element/elementEditor.tsx | 7 +------ public/app/plugins/panel/canvas/editor/options.ts | 4 +--- public/app/plugins/panel/status-history/utils.ts | 5 ----- public/app/plugins/panel/table/TableCellOptionEditor.tsx | 9 +-------- public/app/plugins/panel/table/TablePanel.tsx | 4 ---- public/locales/en-US/grafana.json | 2 +- public/locales/pseudo-LOCALE/grafana.json | 2 +- 15 files changed, 9 insertions(+), 52 deletions(-) diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 699ecbe13c5..f40ad6a1766 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -130,7 +130,6 @@ Experimental features might be changed or removed without prior notice. | `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) | | `storage` | Configurable storage for dashboards, datasources, and resources | | `canvasPanelNesting` | Allow elements nesting | -| `vizActions` | Allow actions in visualizations | | `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables | | `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown | | `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 85ba3f238c5..87c5ff7c0d1 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -36,7 +36,6 @@ export interface FeatureToggles { autoMigrateStatPanel?: boolean; disableAngular?: boolean; canvasPanelNesting?: boolean; - vizActions?: boolean; disableSecretsCompatibility?: boolean; logRequestsInstrumentedAsUnknown?: boolean; grpcServer?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index a3a8a86191d..d84ae066775 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -150,14 +150,6 @@ var ( Owner: grafanaDatavizSquad, HideFromAdminPage: true, }, - { - Name: "vizActions", - Description: "Allow actions in visualizations", - Stage: FeatureStageExperimental, - FrontendOnly: true, - Owner: grafanaDatavizSquad, - HideFromAdminPage: true, - }, { Name: "disableSecretsCompatibility", Description: "Disable duplicated secret storage in legacy tables", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 065323e011e..feb0e5d5d4b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -17,7 +17,6 @@ autoMigrateWorldmapPanel,preview,@grafana/dataviz-squad,false,false,true autoMigrateStatPanel,preview,@grafana/dataviz-squad,false,false,true disableAngular,preview,@grafana/dataviz-squad,false,false,true canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true -vizActions,experimental,@grafana/dataviz-squad,false,false,true disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false grpcServer,preview,@grafana/search-and-storage,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b2d704e7a10..56c3a7d53da 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -79,10 +79,6 @@ const ( // Allow elements nesting FlagCanvasPanelNesting = "canvasPanelNesting" - // FlagVizActions - // Allow actions in visualizations - FlagVizActions = "vizActions" - // FlagDisableSecretsCompatibility // Disable duplicated secret storage in legacy tables FlagDisableSecretsCompatibility = "disableSecretsCompatibility" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 0d6059d5520..569c2cbfc1d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -4202,7 +4202,8 @@ "metadata": { "name": "vizActions", "resourceVersion": "1722461779830", - "creationTimestamp": "2024-09-09T14:11:55Z" + "creationTimestamp": "2024-09-09T14:11:55Z", + "deletionTimestamp": "2025-02-07T18:50:26Z" }, "spec": { "description": "Allow actions in visualizations", diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index 519a0f0c338..28fcc7ab9e6 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -30,7 +30,6 @@ import { DataLinksFieldConfigSettings, } from '@grafana/data'; import { actionsOverrideProcessor } from '@grafana/data/src/field/overrides/processors'; -import { config } from '@grafana/runtime'; import { FieldConfig } from '@grafana/schema'; import { RadioButtonGroup, TimeZonePicker, Switch } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker'; @@ -349,7 +348,7 @@ export const getAllStandardFieldConfigs = () => { category, }; - const dataLinksCategory = config.featureToggles.vizActions ? 'Data links and actions' : 'Data links'; + const dataLinksCategory = 'Data links and actions'; const links: FieldConfigPropertyItem = { id: 'links', @@ -379,7 +378,6 @@ export const getAllStandardFieldConfigs = () => { shouldApply: () => true, category: [dataLinksCategory], getItemsCount: (value) => (value ? value.length : 0), - showIf: () => config.featureToggles.vizActions, hideFromDefaults: true, }; diff --git a/public/app/features/actions/ActionEditor.tsx b/public/app/features/actions/ActionEditor.tsx index 4194ab2680c..81500b9332a 100644 --- a/public/app/features/actions/ActionEditor.tsx +++ b/public/app/features/actions/ActionEditor.tsx @@ -2,7 +2,6 @@ import { css } from '@emotion/css'; import { memo } from 'react'; import { Action, GrafanaTheme2, httpMethodOptions, HttpRequestMethod, VariableSuggestion } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { Switch } from '@grafana/ui/'; import { Field } from '@grafana/ui/src/components/Forms/Field'; import { InlineField } from '@grafana/ui/src/components/Forms/InlineField'; @@ -108,8 +107,6 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne value.fetch.method !== HttpRequestMethod.GET && value.fetch.headers?.some(([name, value]) => name === 'Content-Type' && value === 'application/json'); - const action = config.featureToggles.vizActions ? 'or action' : ''; - return (
@@ -147,8 +144,7 @@ export const ActionEditor = memo(({ index, value, onChange, suggestions, showOne label={t('grafana-ui.data-link-inline-editor.one-click', 'One click')} description={t( 'grafana-ui.action-editor.modal.one-click-description', - 'Only one link {{ action }} can have one click enabled at a time', - { action } + 'Only one link or action can have one click enabled at a time' )} > diff --git a/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx b/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx index 78562e83e88..bdad1130a50 100644 --- a/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/element/elementEditor.tsx @@ -1,7 +1,6 @@ import { get as lodashGet } from 'lodash'; import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders'; -import { config } from '@grafana/runtime'; import { CanvasElementOptions } from 'app/features/canvas/element'; import { canvasElementRegistry, @@ -67,8 +66,6 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions< const current = options?.type ? options.type : DEFAULT_CANVAS_ELEMENT_CONFIG.type; const layerTypes = getElementTypes(opts.scene.shouldShowAdvancedTypes, current).options; - const actionsEnabled = config.featureToggles.vizActions; - const isUnsupported = !opts.scene.shouldShowAdvancedTypes && !defaultElementItems.filter((item) => item.id === options?.type).length; @@ -123,9 +120,7 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions< } optionBuilder.addDataLinks(builder, ctx); - if (actionsEnabled) { - optionBuilder.addActions(builder, ctx); - } + optionBuilder.addActions(builder, ctx); }, }; } diff --git a/public/app/plugins/panel/canvas/editor/options.ts b/public/app/plugins/panel/canvas/editor/options.ts index 954478b88dd..221ab5f7979 100644 --- a/public/app/plugins/panel/canvas/editor/options.ts +++ b/public/app/plugins/panel/canvas/editor/options.ts @@ -2,7 +2,6 @@ import { capitalize } from 'lodash'; import { FieldType } from '@grafana/data'; import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin'; -import { config } from '@grafana/runtime'; import { ConnectionDirection } from 'app/features/canvas/element'; import { SVGElements } from 'app/features/canvas/runtime/element'; import { ColorDimensionEditor, ResourceDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors'; @@ -213,7 +212,7 @@ export const optionBuilder: OptionSuppliers = { addDataLinks: (builder, context) => { builder.addCustomEditor({ - category: config.featureToggles.vizActions ? ['Data links and actions'] : ['Data links'], + category: ['Data links and actions'], id: 'dataLinks', path: 'links', name: 'Links', @@ -230,7 +229,6 @@ export const optionBuilder: OptionSuppliers = { name: 'Actions', editor: ActionsEditor, settings: context.options, - showIf: () => config.featureToggles.vizActions, }); }, }; diff --git a/public/app/plugins/panel/status-history/utils.ts b/public/app/plugins/panel/status-history/utils.ts index 9651797df10..cd683b7129c 100644 --- a/public/app/plugins/panel/status-history/utils.ts +++ b/public/app/plugins/panel/status-history/utils.ts @@ -1,6 +1,5 @@ import { ActionModel, Field, InterpolateFunction, LinkModel } from '@grafana/data'; import { DataFrame } from '@grafana/data/'; -import { config } from '@grafana/runtime'; import { getActions } from 'app/features/actions/utils'; export const getDataLinks = (field: Field, rowIdx: number) => { @@ -32,10 +31,6 @@ export const getFieldActions = ( replaceVars: InterpolateFunction, rowIndex: number ) => { - if (!config.featureToggles?.vizActions) { - return []; - } - const actions: Array> = []; const actionLookup = new Set(); diff --git a/public/app/plugins/panel/table/TableCellOptionEditor.tsx b/public/app/plugins/panel/table/TableCellOptionEditor.tsx index cff753a5344..39b2059d760 100644 --- a/public/app/plugins/panel/table/TableCellOptionEditor.tsx +++ b/public/app/plugins/panel/table/TableCellOptionEditor.tsx @@ -3,7 +3,6 @@ import { merge } from 'lodash'; import { useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { config } from '@grafana/runtime'; import { TableCellOptions } from '@grafana/schema'; import { Field, Select, TableCellDisplayMode, useStyles2 } from '@grafana/ui'; @@ -91,15 +90,9 @@ let cellDisplayModeOptions: Array> = [ { value: { type: TableCellDisplayMode.DataLinks }, label: 'Data links' }, { value: { type: TableCellDisplayMode.JSONView }, label: 'JSON View' }, { value: { type: TableCellDisplayMode.Image }, label: 'Image' }, + { value: { type: TableCellDisplayMode.Actions }, label: 'Actions' }, ]; -if (config.featureToggles.vizActions) { - cellDisplayModeOptions = [ - ...cellDisplayModeOptions, - { value: { type: TableCellDisplayMode.Actions }, label: 'Actions' }, - ]; -} - const getStyles = (theme: GrafanaTheme2) => ({ fixBottomMargin: css({ marginBottom: theme.spacing(-2), diff --git a/public/app/plugins/panel/table/TablePanel.tsx b/public/app/plugins/panel/table/TablePanel.tsx index 3945829809b..527389c9e90 100644 --- a/public/app/plugins/panel/table/TablePanel.tsx +++ b/public/app/plugins/panel/table/TablePanel.tsx @@ -152,10 +152,6 @@ const getCellActions = ( rowIndex: number, replaceVariables: InterpolateFunction | undefined ) => { - if (!config.featureToggles?.vizActions) { - return []; - } - const actions: Array> = []; const actionLookup = new Set(); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 366b60a9da0..2a6465e6357 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1703,7 +1703,7 @@ "action-query-params": "Query parameters", "action-title": "Title", "action-title-placeholder": "Action title", - "one-click-description": "Only one link {{ action }} can have one click enabled at a time" + "one-click-description": "Only one link or action can have one click enabled at a time" } }, "alert": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 23e1bebb722..3eea1ed364a 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1703,7 +1703,7 @@ "action-query-params": "Qūęřy päřämęŧęřş", "action-title": "Ŧįŧľę", "action-title-placeholder": "Åčŧįőʼn ŧįŧľę", - "one-click-description": "Øʼnľy őʼnę ľįʼnĸ {{ action }} čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę" + "one-click-description": "Øʼnľy őʼnę ľįʼnĸ őř äčŧįőʼn čäʼn ĥävę őʼnę čľįčĸ ęʼnäþľęđ äŧ ä ŧįmę" } }, "alert": { From e0a4a69c2eebf5832fd88a3f537849aafd5b5208 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:35:30 -0500 Subject: [PATCH 10/13] Docs: Add actions to visualizations (#100684) --- .../configure-data-links/index.md | 113 ++++++++++++++---- .../visualizations/bar-chart/index.md | 2 +- .../visualizations/bar-gauge/index.md | 2 +- .../visualizations/candlestick/index.md | 2 +- .../visualizations/gauge/index.md | 2 +- .../visualizations/geomap/index.md | 2 +- .../visualizations/heatmap/index.md | 2 +- .../visualizations/histogram/index.md | 2 +- .../visualizations/pie-chart/index.md | 2 +- .../visualizations/stat/index.md | 2 +- .../visualizations/state-timeline/index.md | 2 +- .../visualizations/status-history/index.md | 2 +- .../visualizations/table/index.md | 4 +- .../visualizations/time-series/index.md | 2 +- .../visualizations/trend/index.md | 2 +- .../visualizations/xy-chart/index.md | 2 +- .../visualizations/datalink-options-1.md | 13 +- .../visualizations/datalink-options-2.md | 24 +++- .../visualizations/datalink-options-3.md | 36 ++++++ .../shared/visualizations/datalink-options.md | 11 +- 20 files changed, 183 insertions(+), 46 deletions(-) create mode 100644 docs/sources/shared/visualizations/datalink-options-3.md diff --git a/docs/sources/panels-visualizations/configure-data-links/index.md b/docs/sources/panels-visualizations/configure-data-links/index.md index 0245bc75eb5..2b057694e6c 100644 --- a/docs/sources/panels-visualizations/configure-data-links/index.md +++ b/docs/sources/panels-visualizations/configure-data-links/index.md @@ -18,11 +18,16 @@ labels: - cloud - enterprise - oss -menuTitle: Configure data links -title: Configure data links +menuTitle: Configure data links and actions +title: Configure data links and actions description: Configure data links to create links between dashboards and link to external resources weight: 80 refs: + api-settings: + - pattern: /docs/grafana/ + destination: /docs/grafana//panels-visualizations/visualizations/canvas/#button-api-options + - pattern: /docs/grafana-cloud/ + destination: /docs/grafana/latest/panels-visualizations/visualizations/canvas/#button-api-options global-variables: - pattern: /docs/grafana/ destination: /docs/grafana//dashboards/variables/add-template-variables/#__from-and-__to @@ -125,9 +130,14 @@ refs: destination: /docs/grafana-cloud/visualizations/panels-visualizations/visualizations/bar-chart/ --- -# Configure data links +# Configure data links and actions -Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. For example, if your visualization shows four servers, you can add a data link to one or two of them. +_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls. +In both cases, you can carry out these tasks while maintaining the context of the source panel. + +## Data links + +With data links, you can create links that include the series name or even the value under the cursor. For example, if your visualization shows four servers, you can add a data link to one or two of them. The link itself is accessible in different ways depending on the visualization. For the time series visualization you need to click a data point or line: @@ -139,7 +149,7 @@ For visualizations like stat, gauge, or bar gauge you can click anywhere on the If there's only one data link in the visualization, clicking anywhere on the visualization opens the link rather than the context menu. -## Supported visualizations +### Supported visualizations You can configure data links for the following visualizations: @@ -164,11 +174,32 @@ You can configure data links for the following visualizations: {{< /column-list >}} -## Data link variables +## Actions -Variables in data links let you send people to a detailed dashboard with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection. +Using actions, you can trigger processes like starting or shutting down a server, directly from a dashboard panel. [API settings](ref:api-settings) are configured in the **Add action** dialog box. You can also pass variables in the API editor. -To see a list of available variables, enter `$` in the data link **URL** field. +### Supported visualizations + +You can configure actions for the following visualizations: + +{{< column-list >}} + +- [Bar chart](ref:bar-chart) +- [Candlestick](ref:candlestick) +- [State timeline](ref:state-timeline) +- [Status history](ref:status-history) +- [Table](ref:table) +- [Time series](ref:time-series) +- [Trend](ref:trend) +- [XY chart](ref:xy-chart) + +{{< /column-list >}} + +## Data link and action variables {#data-link-variables} + +Variables in data links and actions let you send people to a detailed dashboard or trigger an API call with preserved data filters. For example, you could use variables to specify a label, time range, series, or variable selection. + +To see a list of available variables, enter `$` in the data link or action **URL** field. {{% admonition type="note" %}} These variables changed in 6.4 so if you have an older version of Grafana, then use the version picker to select docs for an older version of Grafana. @@ -176,11 +207,11 @@ These variables changed in 6.4 so if you have an older version of Grafana, then Azure Monitor, [CloudWatch](ref:cloudwatch), and [Google Cloud Monitoring](ref:google-cloud-monitoring) have pre-configured data links called _deep links_. -You can also use template variables in your data links URLs. For more information, refer to [Templates and variables](ref:templates-and-variables). +You can also use template variables in your data links or actions URLs. For more information, refer to [Templates and variables](ref:templates-and-variables). ### Time range panel variables -These variables allow you to include the current time range in the data link URL: +These variables allow you to include the current time range in the data link or action URL: | Variable | Description | | ------------------ | ------------------------------------------------------------------------ | @@ -188,7 +219,7 @@ These variables allow you to include the current time range in the data link URL | `__from` | For more information, refer to [Global variables](ref:global-variables). | | `__to` | For more information, refer to [Global variables](ref:global-variables). | -When you create data links using time range variables like `__url_time_range` in the URL, you have to form the query parameter syntax yourself; that is, you must format the URL by appending query parameters using the question mark (`?`) and ampersand (`&`) syntax. These characters aren't automatically generated. +When you create data links and actions using time range variables like `__url_time_range` in the URL, you have to form the query parameter syntax yourself; that is, you must format the URL by appending query parameters using the question mark (`?`) and ampersand (`&`) syntax. These characters aren't automatically generated. ### Series variables @@ -219,9 +250,9 @@ Value-specific variables are available under `__value` namespace: | `__value.text` | Text representation of a value | | `__value.calc` | Calculation name if the value is result of calculation | -Using value-specific variables in data links can show different results depending on the set option of Tooltip mode. +Using value-specific variables in data links and actions can show different results depending on the set option of Tooltip mode. -When you create data links using time range variables like `__value.time` in the URL, you have to form the query parameter syntax yourself; that is, you must add the question mark (`?`) and ampersand (`&`). These characters aren't automatically generated. +When you create data links and actions using time range variables like `__value.time` in the URL, you have to form the query parameter syntax yourself; that is, you must add the question mark (`?`) and ampersand (`&`). These characters aren't automatically generated. ### Data variables @@ -247,20 +278,29 @@ When linking to another dashboard that uses template variables, select variable If you want to add all of the current dashboard's variables to the URL, then use `${__all_variables}`. -## Add a data link +## Add data links or actions {#add-a-data-link} + +The following tasks describe how to configure data links and actions. + +{{< tabs >}} +{{< tab-content name="Add data links" >}} +To add a data link, follow these steps: 1. Navigate to the panel to which you want to add the data link. 1. Hover over any part of the panel to display the menu icon in the upper-right corner. 1. Click the menu icon and select **Edit** to open the panel editor. -1. In the panel edit pane, scroll down to the **Data links** section and expand it. +1. Scroll down to the **Data links and actions** section and expand it. 1. Click **+ Add link**. -1. In the dialog box that opens, enter a **Title**. This is a human-readable label for the link displayed in the UI. This is a required field. -1. Enter the **URL** or variable to which you want to link. This is a required field. +1. In the dialog box that opens, enter a **Title**. - To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. + This is a human-readable label for the link displayed in the UI. This is a required field. + +1. Enter the **URL** to which you want to link. + + To add a data link variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. This is a required field. 1. If you want the link to open in a new tab, toggle the **Open in a new tab** switch. -1. If you want the data link to open with a single click, toggle the **One click** switch. +1. If you want the data link to open with a single click on the visualization, toggle the **One click** switch. Only one data link can have **One click** enabled at a time. **One click** is only supported for some visualizations. @@ -268,4 +308,37 @@ If you want to add all of the current dashboard's variables to the URL, then use 1. Click **Save dashboard**. 1. Click **Back to dashboard** and then **Exit edit**. -If you add multiple data links, you can control the order in which they appear in the visualization. To do this, click and drag the data link to the desired position. + {{< /tab-content >}} + {{< tab-content name="Add actions" >}} + + {{< admonition type="note">}} + Actions are not supported for all visualizations. For the list of supported visualizations, refer to [Supported visualizations](#supported-visualizations-1). + {{< /admonition >}} + + To add an action, by follow these steps: + +1. Navigate to the panel to which you want to add the action. +1. Hover over any part of the panel to display the menu icon in the upper-right corner. +1. Click the menu icon and select **Edit** to open the panel editor. +1. Scroll down to the **Data links and actions** section and expand it. +1. Click **+ Add action**. +1. In the dialog box that opens, define the API call settings: + + | Option | Description | + | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | + | Title | A human-readable label for the action that's displayed in the UI. | + | Confirmation message | A descriptive prompt to confirm or cancel the action. | + | Method | Select from **POST**, **PUT**, or **GET**. | + | URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | + | Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | + | Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | + | Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | + | Body | The body of the request. | + +1. Click **Save** to save changes and close the dialog box. +1. Click **Save dashboard**. +1. Click **Back to dashboard** and then **Exit edit**. + {{< /tab-content >}} + {{< /tabs >}} + +If you add multiple data links or actions, you can control the order in which they appear in the visualization. To do this, click and drag the data link or action to the desired position. diff --git a/docs/sources/panels-visualizations/visualizations/bar-chart/index.md b/docs/sources/panels-visualizations/visualizations/bar-chart/index.md index c9aaf77688b..6de1af95513 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-chart/index.md @@ -199,7 +199,7 @@ You can set standard min/max options to define hard limits of the Y-axis. For mo {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md index cc70b2bbb20..3fd9b5c24ea 100644 --- a/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/bar-gauge/index.md @@ -211,7 +211,7 @@ This option only applies when bar size is set to manual. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/candlestick/index.md b/docs/sources/panels-visualizations/visualizations/candlestick/index.md index c673509a446..bbaccb65254 100644 --- a/docs/sources/panels-visualizations/visualizations/candlestick/index.md +++ b/docs/sources/panels-visualizations/visualizations/candlestick/index.md @@ -136,7 +136,7 @@ The candlestick visualization is based on the time series visualization. It can {{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/gauge/index.md b/docs/sources/panels-visualizations/visualizations/gauge/index.md index fb6b224a12e..fbb1cb922ae 100644 --- a/docs/sources/panels-visualizations/visualizations/gauge/index.md +++ b/docs/sources/panels-visualizations/visualizations/gauge/index.md @@ -171,7 +171,7 @@ Adjust the sizes of the gauge text. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/geomap/index.md b/docs/sources/panels-visualizations/visualizations/geomap/index.md index b42d1f78a6c..64b20d33dd7 100644 --- a/docs/sources/panels-visualizations/visualizations/geomap/index.md +++ b/docs/sources/panels-visualizations/visualizations/geomap/index.md @@ -711,7 +711,7 @@ Displays debug information in the upper right corner. This can be useful for deb {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/heatmap/index.md b/docs/sources/panels-visualizations/visualizations/heatmap/index.md index 99bd1f53e68..a51479e4e7a 100644 --- a/docs/sources/panels-visualizations/visualizations/heatmap/index.md +++ b/docs/sources/panels-visualizations/visualizations/heatmap/index.md @@ -208,7 +208,7 @@ Choose whether you want to display the heatmap legend on the visualization by to Set the color used to show exemplar data. -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/histogram/index.md b/docs/sources/panels-visualizations/visualizations/histogram/index.md index 70fcc675d02..f005c8ad7cb 100644 --- a/docs/sources/panels-visualizations/visualizations/histogram/index.md +++ b/docs/sources/panels-visualizations/visualizations/histogram/index.md @@ -157,7 +157,7 @@ Choose from the following: {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md index 83efab500f9..679f4e61f3b 100644 --- a/docs/sources/panels-visualizations/visualizations/pie-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/pie-chart/index.md @@ -174,7 +174,7 @@ Use these settings to define how the legend appears in your visualization. For m {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/stat/index.md b/docs/sources/panels-visualizations/visualizations/stat/index.md index 8eca8f63b29..e1ad7fada4a 100644 --- a/docs/sources/panels-visualizations/visualizations/stat/index.md +++ b/docs/sources/panels-visualizations/visualizations/stat/index.md @@ -172,7 +172,7 @@ Adjust the sizes of the gauge text. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md index cc861506203..192bcb8fdb1 100644 --- a/docs/sources/panels-visualizations/visualizations/state-timeline/index.md +++ b/docs/sources/panels-visualizations/visualizations/state-timeline/index.md @@ -157,7 +157,7 @@ The **Page size** option lets you paginate the state timeline visualization to l {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/status-history/index.md b/docs/sources/panels-visualizations/visualizations/status-history/index.md index 998ff72e251..0382deffd77 100644 --- a/docs/sources/panels-visualizations/visualizations/status-history/index.md +++ b/docs/sources/panels-visualizations/visualizations/status-history/index.md @@ -143,7 +143,7 @@ Controls the opacity of state regions. {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index c53a0f7adff..96c95f437d2 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -362,9 +362,9 @@ If you want to apply this setting to only some fields instead of all fields, you {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions -{{< docs/shared lookup="visualizations/datalink-options-1.md" source="grafana" version="" >}} +{{< docs/shared lookup="visualizations/datalink-options-3.md" source="grafana" version="" >}} ### Value mappings diff --git a/docs/sources/panels-visualizations/visualizations/time-series/index.md b/docs/sources/panels-visualizations/visualizations/time-series/index.md index 2031d0db76a..7b3dec5e5bf 100644 --- a/docs/sources/panels-visualizations/visualizations/time-series/index.md +++ b/docs/sources/panels-visualizations/visualizations/time-series/index.md @@ -362,7 +362,7 @@ Set the position of the bar relative to a data point. In the examples below, **S {{< docs/shared lookup="visualizations/standard-options.md" source="grafana" version="" >}} -### Data links +### Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/trend/index.md b/docs/sources/panels-visualizations/visualizations/trend/index.md index 49032640568..e8cc7fa8d30 100644 --- a/docs/sources/panels-visualizations/visualizations/trend/index.md +++ b/docs/sources/panels-visualizations/visualizations/trend/index.md @@ -60,7 +60,7 @@ For example, you could represent engine power and torque versus speed where spee {{< docs/shared lookup="visualizations/tooltip-options-2.md" source="grafana" version="" >}} -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/panels-visualizations/visualizations/xy-chart/index.md b/docs/sources/panels-visualizations/visualizations/xy-chart/index.md index 8e25e2e2dfa..83becc1b7df 100644 --- a/docs/sources/panels-visualizations/visualizations/xy-chart/index.md +++ b/docs/sources/panels-visualizations/visualizations/xy-chart/index.md @@ -308,7 +308,7 @@ You can customize the following standard options: To learn more, refer to [Configure standard options](ref:configure-standard-options). -## Data links +## Data links and actions {{< docs/shared lookup="visualizations/datalink-options-2.md" source="grafana" version="" >}} diff --git a/docs/sources/shared/visualizations/datalink-options-1.md b/docs/sources/shared/visualizations/datalink-options-1.md index debfdcccbf9..5cfe706abf6 100644 --- a/docs/sources/shared/visualizations/datalink-options-1.md +++ b/docs/sources/shared/visualizations/datalink-options-1.md @@ -1,12 +1,16 @@ --- -title: Data link options +title: Data links and actions options comments: | - This file is used in the following visualizations: bar gauge, gauge, pie chart, stat, table + This file is used in the following visualizations: bar gauge, gauge, pie chart, stat --- Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. -To learn more, refer to [Configure data links](../../configure-data-links/). +To learn more, refer to [Configure data links and actions](../../configure-data-links/). + +{{< admonition type="note" >}} +Actions are not supported for this visualization. +{{< /admonition >}} For each data link, set the following options: @@ -14,4 +18,5 @@ For each data link, set the following options: - **URL** - **Open in new tab** -Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links. +Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. +If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links. diff --git a/docs/sources/shared/visualizations/datalink-options-2.md b/docs/sources/shared/visualizations/datalink-options-2.md index ec8502a1ae5..b74b753b4e6 100644 --- a/docs/sources/shared/visualizations/datalink-options-2.md +++ b/docs/sources/shared/visualizations/datalink-options-2.md @@ -1,10 +1,11 @@ --- -title: Data link options +title: Data links and actions options comments: | This file is used in the following visualizations: bar chart, candlestick, state timeline, status history, time series, trend, xy chart --- -Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. +_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls. +In both cases, you can carry out these tasks while maintaining the context of the source panel. For each data link, set the following options: @@ -13,4 +14,21 @@ For each data link, set the following options: - **Open in new tab** - **One click** - Opens the data link with a single click. Only one data link can have **One click** enabled at a time. -To learn more, refer to [Configure data links](../../configure-data-links/). +For each action, define the following API call settings: + + + +| Option | Description | +| -------------------- | ----------------------------------------------------------------- | +| Title | A human-readable label for the action that's displayed in the UI. | +| Confirmation message | A descriptive prompt to confirm or cancel the action. | +| Method | Select from **POST**, **PUT**, or **GET**. | +| URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | +| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | +| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | +| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | +| Body | The body of the request. | + + + +To learn more, refer to [Configure data links and actions](../../configure-data-links/). diff --git a/docs/sources/shared/visualizations/datalink-options-3.md b/docs/sources/shared/visualizations/datalink-options-3.md new file mode 100644 index 00000000000..738a5898596 --- /dev/null +++ b/docs/sources/shared/visualizations/datalink-options-3.md @@ -0,0 +1,36 @@ +--- +title: Data links and actions options +comments: | + This file is used in the following visualizations: table +--- + +_Data links_ allow you to link to other panels, dashboards, and external resources and _actions_ let you trigger basic, unauthenticated, API calls. +In both cases, you can carry out these tasks while maintaining the context of the source panel. + +For each data link, set the following options: + +- **Title** +- **URL** +- **Open in new tab** + +Data links for this visualization don't include the **One click** switch, however, if there's only one data link configured, that data link has single-click functionality. +If multiple data links are configured, then clicking the visualization opens a menu that displays all the data links. + +For each action, define the following API call settings: + + + +| Option | Description | +| -------------------- | ----------------------------------------------------------------- | +| Title | A human-readable label for the action that's displayed in the UI. | +| Confirmation message | A descriptive prompt to confirm or cancel the action. | +| Method | Select from **POST**, **PUT**, or **GET**. | +| URL | The request URL.

To add a variable, click in the **URL** field and enter `$` or press Ctrl+Space or Cmd+Space to see a list of available variables. | +| Query parameters | **Key** and **Value** pairs. Click the **+** icon to add as many key/value pairs as you need. | +| Headers | Comprised of **Key** and **Value** pairs and a **Content-Type**.

Click the **+** icon to add as many key/value pairs as you need. | +| Content-Type | Select from the following: **application/json**, **text/plain**, **application/XML**, and **application/x-www-form-urlencoded**. | +| Body | The body of the request. | + + + +To learn more, refer to [Configure data links and actions](../../configure-data-links/). diff --git a/docs/sources/shared/visualizations/datalink-options.md b/docs/sources/shared/visualizations/datalink-options.md index 6e567cfd464..131640d86ee 100644 --- a/docs/sources/shared/visualizations/datalink-options.md +++ b/docs/sources/shared/visualizations/datalink-options.md @@ -1,10 +1,15 @@ --- -title: Data link options +title: Data links and actions options comments: | This file is used in the following visualizations: geomap, heatmap, histogram --- -Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. You can create links that include the series name or even the value under the cursor. +Data links allow you to link to other panels, dashboards, and external resources while maintaining the context of the source panel. +You can create links that include the series name or even the value under the cursor. + +{{< admonition type="note" >}} +Actions are not supported for this visualization. +{{< /admonition >}} For each data link, set the following options: @@ -12,4 +17,4 @@ For each data link, set the following options: - **URL** - **Open in new tab** -To learn more, refer to [Configure data links](../../configure-data-links/). +To learn more, refer to [Configure data links and actions](../../configure-data-links/). From c605e4557d022fe01b5dac4ca5680389f6b1a328 Mon Sep 17 00:00:00 2001 From: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Date: Wed, 26 Feb 2025 19:37:51 -0500 Subject: [PATCH 11/13] Docs: add Actions cell type for table (#101380) --- .../visualizations/table/index.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/sources/panels-visualizations/visualizations/table/index.md b/docs/sources/panels-visualizations/visualizations/table/index.md index 96c95f437d2..e99622019d8 100644 --- a/docs/sources/panels-visualizations/visualizations/table/index.md +++ b/docs/sources/panels-visualizations/visualizations/table/index.md @@ -246,6 +246,7 @@ If you want to apply a cell type to only some fields instead of all fields, you | Data links | If you've configured data links, when the cell type is **Auto**, the cell text becomes clickable. If you change the cell type to **Data links**, the cell text reflects the titles of the configured data links. To control the application of data link text more granularly, use a **Cell option > Cell type > Data links** field override. | | [JSON View](#json-view) | Shows values formatted as code. | | [Image](#image) | If the field value is an image URL or a base64 encoded image, the table displays the image. | +| [Actions](#actions) | The cell displays a button that triggers a basic, unauthenticated API call when clicked. | ##### Sparkline @@ -333,6 +334,23 @@ Set the following options: - **Alt text** - Set the alternative text of an image. The text will be available for screen readers and in cases when images can't be loaded. - **Title text** - Set the text that's displayed when the image is hovered over with a cursor. +##### Actions + +The cell displays a button that triggers a basic, unauthenticated API call when clicked. +Configure the API call with the following options: + + +| Option | Description | +| ------- | ------------ | +| Endpoint | Enter the endpoint URL. | +| Method | Choose from **GET**, **POST**, and **PUT**. | +| Content-Type | Select an option in the drop-down list. Choose from: JSON, Text, JavaScript, HTML, XML, and x-www-form-urlencoded. | +| Query parameters | Enter as many **Key**, **Value** pairs as you need. | +| Header parameters | Enter as many **Key**, **Value** pairs as you need. | +| Payload | Enter the body of the API call. | + + + #### Wrap text {{< admonition type="note" >}} From 22a6dc6b52c40bbf0fff60abe3b20585622f473e Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Wed, 26 Feb 2025 20:36:09 -0600 Subject: [PATCH 12/13] Canvas: Fix no series timestamp (#101390) --- public/app/plugins/panel/canvas/components/CanvasTooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx index f2a5959edc6..6234a4a7693 100644 --- a/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx +++ b/public/app/plugins/panel/canvas/components/CanvasTooltip.tsx @@ -49,7 +49,7 @@ export const CanvasTooltip = ({ scene }: Props) => { } // Retrieve timestamp of the last data point if available - const timeField = scene.data?.series[0].fields?.find((field) => field.type === FieldType.time); + const timeField = scene.data?.series[0]?.fields?.find((field) => field.type === FieldType.time); const lastTimeValue = timeField?.values[timeField.values.length - 1]; const shouldDisplayTimeContentItem = timeField && lastTimeValue && element.data.field && getFieldDisplayName(timeField) !== element.data.field; From e8e79e9c79b5eacd88c2e6cb7461e3a84ce22507 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 27 Feb 2025 09:16:00 +0100 Subject: [PATCH 13/13] Plugins: Fix version tab breaking for non semantic version (#101225) --- .../components/VersionInstallButton.test.tsx | 40 ++++++++++++ .../admin/components/VersionInstallButton.tsx | 61 +++++++++++-------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx index 38511a2dfea..16ec1d3ca70 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.test.tsx @@ -177,6 +177,46 @@ describe('VersionInstallButton', () => { ); expect(screen.getByText('Downgrade')).not.toBeVisible(); }); + + it('should show the installation button if invalid semver version is provided', () => { + const version: Version = { + version: '1.0.a', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.1'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); + + it('should show the installation button if invalid semver installed version is provided', () => { + const version: Version = { + version: '1.0.0', + createdAt: '', + isCompatible: false, + grafanaDependency: null, + }; + const installedVersion = '1.0.a'; + renderWithStore( + {}} + /> + ); + expect(screen.getByText('Install')).toBeInTheDocument(); + }); }); function renderWithStore(component: JSX.Element) { diff --git a/public/app/features/plugins/admin/components/VersionInstallButton.tsx b/public/app/features/plugins/admin/components/VersionInstallButton.tsx index 1da8bfe01f7..f0bec011875 100644 --- a/public/app/features/plugins/admin/components/VersionInstallButton.tsx +++ b/public/app/features/plugins/admin/components/VersionInstallButton.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { useEffect, useState } from 'react'; -import { gt } from 'semver'; +import { gt, valid } from 'semver'; import { GrafanaTheme2 } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; @@ -14,6 +14,12 @@ import { Version } from '../types'; const PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME = 'plugins_upgrade_clicked'; const PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME = 'plugins_downgrade_clicked'; +enum InstallState { + INSTALL = 'Install', + UPGRADE = 'Upgrade', + DOWNGRADE = 'Downgrade', +} + interface Props { pluginId: string; version: Version; @@ -38,7 +44,7 @@ export const VersionInstallButton = ({ const [isModalOpen, setIsModalOpen] = useState(false); const styles = useStyles2(getStyles); - const isDowngrade = installedVersion && gt(installedVersion, version.version); + const installState = getInstallState(installedVersion, version.version); useEffect(() => { if (installedVersion === version.version) { @@ -61,7 +67,7 @@ export const VersionInstallButton = ({ schema_version: '1.0.0', }; - if (!installedVersion || gt(version.version, installedVersion)) { + if (installState === InstallState.UPGRADE) { reportInteraction(PLUGINS_VERSION_PAGE_UPGRADE_INTERACTION_EVENT_NAME, trackProps); } else { reportInteraction(PLUGINS_VERSION_PAGE_CHANGE_INTERACTION_EVENT_NAME, { @@ -76,7 +82,7 @@ export const VersionInstallButton = ({ }; const onInstallClick = () => { - if (isDowngrade) { + if (installState === InstallState.DOWNGRADE) { setIsModalOpen(true); } else { performInstallation(); @@ -91,24 +97,9 @@ export const VersionInstallButton = ({ setIsModalOpen(false); }; - let label = 'Downgrade'; - let hidden = false; const isPreinstalled = isPreinstalledPlugin(pluginId); - if (!installedVersion) { - label = 'Install'; - } else if (gt(version.version, installedVersion)) { - label = 'Upgrade'; - if (isPreinstalled.withVersion) { - // Hide button if the plugin is preinstalled with a specific version - hidden = true; - } - } else { - if (isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate)) { - // Hide the downgrade button if the plugin is preinstalled since it will be auto-updated - hidden = true; - } - } + const hidden = getButtonHiddenState(installState, isPreinstalled); return ( <> @@ -124,7 +115,7 @@ export const VersionInstallButton = ({ tooltip={tooltip} tooltipPlacement="bottom-start" > - {label} {isInstalling ? : getIcon(label)} + {installState} {isInstalling ? : getIcon(installState)} ; } - if (label === 'Upgrade') { + if (installState === InstallState.UPGRADE) { return ; } return ''; } +function getInstallState(installedVersion?: string, version?: string): InstallState { + if (!installedVersion || !version || !valid(installedVersion) || !valid(version)) { + return InstallState.INSTALL; + } + return gt(installedVersion, version) ? InstallState.DOWNGRADE : InstallState.UPGRADE; +} + +function getButtonHiddenState(installState: InstallState, isPreinstalled: { found: boolean; withVersion: boolean }) { + // Default state for initial install + if (installState === InstallState.INSTALL) { + return false; + } + + // Handle downgrade case + if (installState === InstallState.DOWNGRADE) { + return isPreinstalled.found && Boolean(config.featureToggles.preinstallAutoUpdate); + } + + // Handle upgrade case + return isPreinstalled.withVersion; +} + const getStyles = (theme: GrafanaTheme2) => ({ spinner: css({ marginLeft: theme.spacing(1),