diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 5e008a732e7..39299b42166 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -258,4 +258,6 @@ export interface FeatureToggles {
infinityRunQueriesInParallel?: boolean;
inviteUserExperimental?: boolean;
extraLanguages?: boolean;
+ noBackdropBlur?: boolean;
+ alertingMigrationUI?: boolean;
}
diff --git a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
index 12e094433d2..b8f4800fbae 100644
--- a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
+++ b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
@@ -14,6 +14,7 @@ import { getExtraStyles } from './extra';
import { getFilterTableStyles } from './filterTable';
import { getFontStyles } from './fonts';
import { getFormElementStyles } from './forms';
+import { getHacksStyles } from './hacks';
import { getJsonFormatterStyles } from './jsonFormatter';
import { getLegacySelectStyles } from './legacySelect';
import { getMarkdownStyles } from './markdownStyles';
@@ -24,9 +25,14 @@ import { getSlateStyles } from './slate';
import { getUplotStyles } from './uPlot';
import { getUtilityClassStyles } from './utilityClasses';
+interface GlobalStylesProps {
+ hackNoBackdropBlur?: boolean;
+}
+
/** @internal */
-export function GlobalStyles() {
+export function GlobalStyles(props: GlobalStylesProps) {
const theme = useTheme2();
+ const { hackNoBackdropBlur } = props;
return (
);
diff --git a/packages/grafana-ui/src/themes/GlobalStyles/hacks.ts b/packages/grafana-ui/src/themes/GlobalStyles/hacks.ts
new file mode 100644
index 00000000000..1e4fea76a6f
--- /dev/null
+++ b/packages/grafana-ui/src/themes/GlobalStyles/hacks.ts
@@ -0,0 +1,21 @@
+import { css } from '@emotion/react';
+
+export interface Hacks {
+ hackNoBackdropBlur?: boolean;
+}
+
+export function getHacksStyles(hacks: Hacks) {
+ return css([
+ /**
+ * Disables all backdrop blur effects to improve performance on extremely
+ * resource constrained devices.
+ *
+ * Controlled via the `noBackdropBlur` feature toggle in Grafana
+ */
+ hacks.hackNoBackdropBlur && {
+ '*, *:before, *:after': {
+ backdropFilter: 'none !important',
+ },
+ },
+ ]);
+}
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index d9053899ef8..a05f3956e1b 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1811,6 +1811,24 @@ var (
Owner: grafanaFrontendPlatformSquad,
FrontendOnly: true,
},
+ {
+ Name: "noBackdropBlur",
+ Description: "Disables backdrop blur",
+ Stage: FeatureStageExperimental,
+ Owner: grafanaFrontendPlatformSquad,
+ HideFromAdminPage: true,
+ HideFromDocs: true,
+ FrontendOnly: true,
+ },
+ {
+ Name: "alertingMigrationUI",
+ Description: "Enables the alerting migration UI, to migrate datasource-managed rules to Grafana-managed rules",
+ FrontendOnly: true,
+ Stage: FeatureStageExperimental,
+ Owner: grafanaAlertingSquad,
+ HideFromAdminPage: true,
+ HideFromDocs: true,
+ },
}
)
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index 4d6d6ca54bc..c3e15a6d655 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -239,3 +239,5 @@ grafanaManagedRecordingRulesDatasources,experimental,@grafana/alerting-squad,fal
infinityRunQueriesInParallel,privatePreview,@grafana/oss-big-tent,false,false,false
inviteUserExperimental,experimental,@grafana/sharing-squad,false,false,true
extraLanguages,experimental,@grafana/grafana-frontend-platform,false,false,true
+noBackdropBlur,experimental,@grafana/grafana-frontend-platform,false,false,true
+alertingMigrationUI,experimental,@grafana/alerting-squad,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 55b65a745c2..67ed9660c83 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -966,4 +966,12 @@ const (
// FlagExtraLanguages
// Enables additional languages
FlagExtraLanguages = "extraLanguages"
+
+ // FlagNoBackdropBlur
+ // Disables backdrop blur
+ FlagNoBackdropBlur = "noBackdropBlur"
+
+ // FlagAlertingMigrationUI
+ // Enables the alerting migration UI, to migrate datasource-managed rules to Grafana-managed rules
+ FlagAlertingMigrationUI = "alertingMigrationUI"
)
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 89f92814f9f..69911d2e36c 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -322,6 +322,21 @@
"frontend": true
}
},
+ {
+ "metadata": {
+ "name": "alertingMigrationUI",
+ "resourceVersion": "1741968018953",
+ "creationTimestamp": "2025-03-14T16:00:18Z"
+ },
+ "spec": {
+ "description": "Enables the alerting migration UI, to migrate datasource-managed rules to Grafana-managed rules",
+ "stage": "experimental",
+ "codeowner": "@grafana/alerting-squad",
+ "frontend": true,
+ "hideFromAdminPage": true,
+ "hideFromDocs": true
+ }
+ },
{
"metadata": {
"name": "alertingNoDataErrorExecution",
@@ -2913,6 +2928,21 @@
"hideFromDocs": true
}
},
+ {
+ "metadata": {
+ "name": "noBackdropBlur",
+ "resourceVersion": "1741879106163",
+ "creationTimestamp": "2025-03-13T15:18:26Z"
+ },
+ "spec": {
+ "description": "Disables backdrop blur",
+ "stage": "experimental",
+ "codeowner": "@grafana/grafana-frontend-platform",
+ "frontend": true,
+ "hideFromAdminPage": true,
+ "hideFromDocs": true
+ }
+ },
{
"metadata": {
"name": "nodeGraphDotLayout",
diff --git a/pkg/util/debouncer/debouncer.go b/pkg/util/debouncer/debouncer.go
new file mode 100644
index 00000000000..a6c0e7b22e3
--- /dev/null
+++ b/pkg/util/debouncer/debouncer.go
@@ -0,0 +1,310 @@
+package debouncer
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "time"
+
+ "github.com/grafana/dskit/instrument"
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+)
+
+var (
+ ErrBufferFull = errors.New("debouncer buffer full")
+)
+
+type ProcessFunc[T comparable] func(context.Context, T) error
+type ErrorFunc[T comparable] func(T, error)
+
+type metrics struct {
+ itemsAddedCounter prometheus.Counter
+ itemsDroppedCounter prometheus.Counter
+ itemsProcessedCounter prometheus.Counter
+ processingErrorsCounter prometheus.Counter
+ processingDurationHistogram prometheus.Histogram
+}
+
+func newMetrics(reg prometheus.Registerer, name string) *metrics {
+ return &metrics{
+ itemsAddedCounter: promauto.With(reg).NewCounter(prometheus.CounterOpts{
+ Name: "debouncer_items_added_total",
+ Help: "Total number of items added to the debouncer",
+ ConstLabels: prometheus.Labels{
+ "name": name,
+ },
+ }),
+ itemsDroppedCounter: promauto.With(reg).NewCounter(prometheus.CounterOpts{
+ Name: "debouncer_items_dropped_total",
+ Help: "Total number of items dropped due to a full buffer",
+ ConstLabels: prometheus.Labels{
+ "name": name,
+ },
+ }),
+ itemsProcessedCounter: promauto.With(reg).NewCounter(prometheus.CounterOpts{
+ Name: "debouncer_items_processed_total",
+ Help: "Total number of items processed by the debouncer",
+ ConstLabels: prometheus.Labels{
+ "name": name,
+ },
+ }),
+ processingErrorsCounter: promauto.With(reg).NewCounter(prometheus.CounterOpts{
+ Name: "debouncer_processing_errors_total",
+ Help: "Total number of errors during processing",
+ ConstLabels: prometheus.Labels{
+ "name": name,
+ },
+ }),
+ processingDurationHistogram: promauto.With(reg).NewHistogram(prometheus.HistogramOpts{
+ Name: "debouncer_processing_duration_seconds",
+ Help: "Time taken to process items",
+ Buckets: instrument.DefBuckets,
+ NativeHistogramBucketFactor: 1.1,
+ NativeHistogramMaxBucketNumber: 160,
+ NativeHistogramMinResetDuration: time.Hour,
+ ConstLabels: prometheus.Labels{
+ "name": name,
+ },
+ }),
+ }
+}
+
+// DebouncerOpts hold all the options to create a debouncer group.
+type DebouncerOpts[T comparable] struct {
+ // Name should be a unique name for this debouncer group. It is
+ // also used a name label value for the metrics.
+ Name string
+ // BufferSize is the maximum number of pending events to buffer.
+ BufferSize int
+
+ // ErrorHandler is the function that is called when a process for a given
+ // key returns an error while running.
+ ErrorHandler ErrorFunc[T]
+ // ProcessHandler is the function that is called once a process for a given
+ // key should be run.
+ ProcessHandler ProcessFunc[T]
+ // MinWait is the cooldown period after receiving an event. If another event with the
+ // same key arrives during this period, the timer resets and we wait another MinWait duration.
+ MinWait time.Duration
+ // MaxWait is the maximum time any event will wait before processing. Even if new events
+ // for the same key keep arriving, we guarantee processing after MaxWait from the first event.
+ MaxWait time.Duration
+ Reg prometheus.Registerer
+}
+
+type Group[T comparable] struct {
+ buffer chan T
+
+ // mutex protecting the debouncers map.
+ debouncersMu sync.Mutex
+ debouncers map[T]*debouncer[T]
+
+ wg sync.WaitGroup
+ ctx context.Context
+ cancel context.CancelFunc
+ errorHandler ErrorFunc[T]
+ processHandler ProcessFunc[T]
+ minWait time.Duration
+ maxWait time.Duration
+ metrics *metrics
+}
+
+// NewGroup creates a new debouncer group for processing events with unique keys.
+//
+// A debouncer group helps optimize expensive operations by:
+// 1. Grouping identical events that occur in rapid succession
+// 2. Processing each unique key only once after waiting periods expire
+//
+// Example usage:
+//
+// group := debouncer.NewGroup(DebouncerOpts[string]{
+// BufferSize: 1000,
+// ProcessHandler: func(ctx context.Context, key string) error {
+// // This is where you perform the expensive operation
+// return doSuperExpensiveCommand(key)
+// }
+// MinWait: time.Second * 10,
+// MaxWait: time.Minute,
+// })
+//
+// // Start the debouncer group.
+// group.Start(ctx)
+//
+// // Queue events
+// if err := group.Add("user-1"); err != nil {
+// // Do something with the error.
+// }
+// // Adding the same key resets MinWait but not MaxWait
+// if err := group.Add("user-1"); err != nil {
+// // Do something with the error.
+// }
+//
+// The event will be processed when either MinWait expires (after the most recent add)
+// or MaxWait expires (after the first add), whichever comes first.
+func NewGroup[T comparable](opts DebouncerOpts[T]) (*Group[T], error) {
+ if opts.BufferSize <= 0 {
+ opts.BufferSize = 100
+ }
+
+ if opts.MinWait <= 0 {
+ opts.MinWait = time.Minute
+ }
+ if opts.MaxWait <= 0 {
+ opts.MaxWait = 5 * time.Minute
+ }
+ if opts.MinWait > opts.MaxWait {
+ return nil, errors.New("minWait is bigger than maxWait")
+ }
+
+ if opts.ProcessHandler == nil {
+ return nil, errors.New("processHandler is required")
+ }
+
+ if opts.ErrorHandler == nil {
+ opts.ErrorHandler = func(_ T, _ error) {}
+ }
+
+ return &Group[T]{
+ buffer: make(chan T, opts.BufferSize),
+ debouncers: make(map[T]*debouncer[T]),
+ processHandler: opts.ProcessHandler,
+ errorHandler: opts.ErrorHandler,
+ minWait: opts.MinWait,
+ maxWait: opts.MaxWait,
+ metrics: newMetrics(opts.Reg, opts.Name),
+ }, nil
+}
+
+// Add will create a new debouncer for the given Key if it doesn't exist yet.
+// If a key has already a debouncer it will either reset the MinWait timer for
+// this key, or if they key is already running its process be no-op.
+func (g *Group[T]) Add(value T) error {
+ select {
+ case g.buffer <- value:
+ g.metrics.itemsAddedCounter.Inc()
+ return nil
+ default:
+ g.metrics.itemsDroppedCounter.Inc()
+ return ErrBufferFull
+ }
+}
+
+func (g *Group[T]) Start(ctx context.Context) {
+ g.ctx, g.cancel = context.WithCancel(ctx)
+ g.wg.Add(1)
+ go func() {
+ defer g.wg.Done()
+ for {
+ select {
+ case <-g.ctx.Done():
+ return
+ case value := <-g.buffer:
+ g.processValue(value)
+ }
+ }
+ }()
+}
+
+func (g *Group[T]) Stop() {
+ if g.cancel != nil {
+ g.cancel()
+ g.wg.Wait()
+ }
+}
+
+func (g *Group[T]) processValue(key T) {
+ g.debouncersMu.Lock()
+ deb, ok := g.debouncers[key]
+ if !ok {
+ deb = newDebouncer[T](g.minWait, g.maxWait, key, func(v T) {
+ g.processWithMetrics(g.ctx, v, g.processHandler)
+
+ g.debouncersMu.Lock()
+ defer g.debouncersMu.Unlock()
+ if current, exists := g.debouncers[key]; exists && current == deb {
+ delete(g.debouncers, key)
+ }
+ })
+ g.wg.Add(1)
+ go func() {
+ defer g.wg.Done()
+ deb.run(g.ctx)
+ }()
+ g.debouncers[key] = deb
+ }
+ g.debouncersMu.Unlock()
+
+ deb.reset()
+}
+
+func (g *Group[T]) processWithMetrics(ctx context.Context, value T, processFunc ProcessFunc[T]) {
+ timer := prometheus.NewTimer(g.metrics.processingDurationHistogram)
+ defer timer.ObserveDuration()
+ g.metrics.itemsProcessedCounter.Inc()
+
+ if err := processFunc(ctx, value); err != nil {
+ g.errorHandler(value, err)
+ g.metrics.processingErrorsCounter.Inc()
+ }
+}
+
+// debouncer handles debouncing for a specific key.
+type debouncer[T comparable] struct {
+ key T
+ resetChan chan struct{}
+ minWait time.Duration
+ maxWait time.Duration
+ processFunc func(T)
+}
+
+// newDebouncer creates a new key debouncer.
+func newDebouncer[T comparable](minWait, maxWait time.Duration, key T, processFunc func(T)) *debouncer[T] {
+ deb := &debouncer[T]{
+ key: key,
+ resetChan: make(chan struct{}, 1),
+ minWait: minWait,
+ maxWait: maxWait,
+ processFunc: processFunc,
+ }
+ return deb
+}
+
+// reset triggers a timer reset for the minWait.
+func (d *debouncer[T]) reset() {
+ select {
+ case d.resetChan <- struct{}{}:
+ // Value sent successfully.
+ default:
+ // Value was dropped. Is not an issue as
+ // a reset is already about to being processed
+ // or the process is being run.
+ }
+}
+
+// run manages the debouncing process for a specific key.
+func (d *debouncer[T]) run(ctx context.Context) {
+ // Create timers after getting the first updateChan.
+ minTimer := time.NewTimer(d.minWait)
+ maxTimer := time.NewTimer(d.maxWait)
+ defer func() {
+ minTimer.Stop()
+ maxTimer.Stop()
+ }()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-d.resetChan:
+ minTimer.Stop()
+ minTimer.Reset(d.minWait)
+ case <-minTimer.C:
+ d.processFunc(d.key)
+ return
+ case <-maxTimer.C:
+ d.processFunc(d.key)
+ return
+ }
+ }
+}
diff --git a/pkg/util/debouncer/debouncer_test.go b/pkg/util/debouncer/debouncer_test.go
new file mode 100644
index 00000000000..711c8761836
--- /dev/null
+++ b/pkg/util/debouncer/debouncer_test.go
@@ -0,0 +1,217 @@
+package debouncer
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/testutil"
+ "github.com/stretchr/testify/require"
+)
+
+func TestDebouncer(t *testing.T) {
+ t.Run("should process values after min wait", func(t *testing.T) {
+ var processedMu sync.Mutex
+ processedValues := make(map[string]int)
+
+ group, err := NewGroup(DebouncerOpts[string]{
+ BufferSize: 10,
+ ProcessHandler: func(ctx context.Context, value string) error {
+ processedMu.Lock()
+ processedValues[value]++
+ processedMu.Unlock()
+ return nil
+ },
+ MinWait: 10 * time.Millisecond,
+ MaxWait: 500 * time.Millisecond,
+ })
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ group.Start(ctx)
+
+ require.NoError(t, group.Add("key1"))
+ require.NoError(t, group.Add("key2"))
+ // Should be deduplicated.
+ require.NoError(t, group.Add("key1"))
+
+ require.Eventually(t, func() bool {
+ // We should have processed key1 and key2 exactly once.
+ processedMu.Lock()
+ if processedValues["key1"] == 1 && processedValues["key2"] == 1 {
+ return true
+ }
+ processedMu.Unlock()
+ return false
+ }, time.Millisecond*200, time.Millisecond*20)
+ })
+
+ t.Run("should process values after max wait", func(t *testing.T) {
+ processed := make(map[string]int, 1)
+
+ group, err := NewGroup(DebouncerOpts[string]{
+ BufferSize: 10,
+ ProcessHandler: func(ctx context.Context, value string) error {
+ processed[value]++
+ return nil
+ },
+ MinWait: 50 * time.Millisecond,
+ MaxWait: 500 * time.Millisecond,
+ })
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ group.Start(ctx)
+
+ ticker := time.NewTicker(time.Millisecond * 40)
+ defer ticker.Stop()
+
+ start := time.Now()
+
+ for counter := 0; counter < 25; counter++ {
+ <-ticker.C
+ _ = group.Add("key1")
+ if processed["key1"] == 1 {
+ break
+ }
+ }
+
+ require.WithinDuration(t, start.Add(time.Millisecond*500), time.Now(), time.Millisecond*100)
+ })
+
+ t.Run("should handle buffer full", func(t *testing.T) {
+ group, err := NewGroup(DebouncerOpts[string]{
+ BufferSize: 1,
+ ProcessHandler: func(ctx context.Context, value string) error { return nil },
+ MinWait: 10 * time.Millisecond,
+ MaxWait: 100 * time.Millisecond,
+ })
+ require.NoError(t, err)
+
+ require.NoError(t, group.Add("key1"))
+ // Buffer should be full by now as we are not reading from it yet.
+ require.ErrorIs(t, group.Add("key2"), ErrBufferFull)
+ })
+
+ t.Run("should track metrics", func(t *testing.T) {
+ var wg sync.WaitGroup
+
+ group, err := NewGroup(DebouncerOpts[string]{
+ BufferSize: 10,
+ ProcessHandler: func(ctx context.Context, value string) error {
+ wg.Done()
+ return nil
+ },
+ MinWait: 10 * time.Millisecond,
+ MaxWait: 100 * time.Millisecond,
+
+ Name: "test",
+ })
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ group.Start(ctx)
+
+ wg.Add(1)
+ require.NoError(t, group.Add("key1"))
+ require.NoError(t, group.Add("key1"))
+
+ wg.Wait()
+
+ require.Equal(t, float64(2), testutil.ToFloat64(group.metrics.itemsAddedCounter))
+ require.Equal(t, float64(1), testutil.ToFloat64(group.metrics.itemsProcessedCounter))
+ })
+
+ t.Run("should handle errors", func(t *testing.T) {
+ var (
+ wg sync.WaitGroup
+ errs = make(chan error, 10)
+ expectedErr = errors.New("test error")
+ )
+
+ group, err := NewGroup(DebouncerOpts[string]{
+ BufferSize: 10,
+ ProcessHandler: func(ctx context.Context, value string) error {
+ wg.Done()
+ return expectedErr
+ },
+ MinWait: 10 * time.Millisecond,
+ MaxWait: 100 * time.Millisecond,
+ Reg: prometheus.NewPedanticRegistry(),
+ Name: "test_errors",
+ ErrorHandler: func(_ string, err error) { errs <- err },
+ })
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ group.Start(ctx)
+
+ wg.Add(1)
+ require.NoError(t, group.Add("key1"))
+
+ wg.Wait()
+
+ select {
+ case err := <-errs:
+ require.Equal(t, expectedErr, err)
+ default:
+ t.Fatal("expected error")
+ }
+
+ require.Equal(t, float64(1), testutil.ToFloat64(group.metrics.processingErrorsCounter))
+ })
+
+ t.Run("should gracefully handle stops", func(t *testing.T) {
+ // Create a channel to signal when processing is done.
+ done := make(chan struct{})
+
+ group, err := NewGroup(DebouncerOpts[string]{
+ BufferSize: 10,
+ ProcessHandler: func(ctx context.Context, item string) error {
+ // Start a goroutine to wait for context cancellation.
+ go func() {
+ <-ctx.Done()
+ close(done)
+ }()
+ return nil
+ },
+ MinWait: 50 * time.Millisecond,
+ MaxWait: 500 * time.Millisecond,
+ })
+ require.NoError(t, err)
+
+ // Start the group with a context
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ group.Start(ctx)
+
+ // Send an item to trigger processing.
+ require.NoError(t, group.Add("key-1"))
+
+ // Give the group a moment to process the item.
+ time.Sleep(100 * time.Millisecond)
+
+ // Stop the group, which should cancel the context.
+ group.Stop()
+
+ // Wait for the done signal or timeout.
+ select {
+ case <-done:
+ // Success - the group was stopped and the context was canceled
+ case <-time.After(time.Second):
+ t.Fatal("Timed out waiting for group to stop")
+ }
+ })
+}
diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx
index fdbf6bf3b2d..fb241de0224 100644
--- a/public/app/AppWrapper.tsx
+++ b/public/app/AppWrapper.tsx
@@ -121,7 +121,7 @@ export class AppWrapper extends Component {
actions={[]}
options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }}
>
-
+
diff --git a/public/app/api/README.md b/public/app/api/README.md
index 356289f08a6..49da9eaad8a 100644
--- a/public/app/api/README.md
+++ b/public/app/api/README.md
@@ -24,7 +24,7 @@ Afterwards, you need to run the `TestIntegrationOpenAPIs` test. Note that it wil
### 2. Create the API definition
-In the `../public/app/features/{your_group_name}/api/` folder you have to create the `baseAPI.ts` file for your group. This file should have the following content:
+In the [`/public/app/api/clients`](/public/app/api/clients) folder, create a new folder and `baseAPI.ts` file for your group. This file should have the following content:
```jsx
import { createApi } from '@reduxjs/toolkit/query/react';
@@ -34,7 +34,7 @@ import { getAPIBaseURL } from 'app/api/utils';
export const BASE_URL = getAPIBaseURL('dashboard.grafana.app', 'v0alpha1');
-export const baseAPI = createApi({
+export const api = createApi({
reducerPath: 'dashboardAPI',
baseQuery: createBaseQuery({
baseURL: BASE_URL,
@@ -43,9 +43,9 @@ export const baseAPI = createApi({
});
```
-This is the API definition for the specific group you're working with, where `getAPIBaseURL` should have the proper `group` and `version` as parameters. The `reducePath` should also be modified to match `group + API`: `dashboard` will be `dashboardAPI`, `iam` will be `iamAPI` and so on.
+This is the API definition for the specific group you're working with, where `getAPIBaseURL` should have the proper `group` and `version` as parameters. The `reducerPath` needs to be unique. The convention is to use `API`: `dashboard` will be `dashboardAPI`, `iam` will be `iamAPI` and so on.
-### 3. Add the output information
+### 3. Add your new client to the generation script
Open [generate-rtk-apis.ts](scripts/generate-rtk-apis.ts) and add the following information:
@@ -54,7 +54,6 @@ Open [generate-rtk-apis.ts](scripts/generate-rtk-apis.ts) and add the following
| outputFile name | File that will be created after running the API Client Generation script. It is the key of the object. |
| apiFile | File with the group's API definition. |
| schemaFile | File with the schema that was automatically created in the second step. Although it is in openapi_snapshots, you should link the one saved in `data/openapi`. |
-| apiImport | Function name exported in the API definition (baseAPI.ts file). |
| filterEndpoints | The `operationId` of the particular route you want to work with. You can check the available operationIds in the specific group's spec file. As seen in the `migrate-to-cloud` one, it is an array |
| tag | Must be set to `true`, to automatically attach tags to endpoints. This is needed for proper cache invalidation. See more info in the [official documentation](https://redux-toolkit.js.org/rtk-query/usage/automated-refetching#:~:text=RTK%20Query%20uses,an%20active%20subscription.). |
@@ -65,16 +64,15 @@ Open [generate-rtk-apis.ts](scripts/generate-rtk-apis.ts) and add the following
In our example, the information added will be:
```jsx
-'../public/app/features/dashboard/api/endpoints.gen.ts': {
- apiFile: '../public/app/features/dashboard/api/baseAPI.ts',
+'../public/app/api/clients/dashboard/endpoints.gen.ts': {
+ apiFile: '../public/app/api/clients/dashboard/baseAPI.ts',
schemaFile: '../data/openapi/dashboard.grafana.app-v0alpha1.json',
- apiImport: 'baseAPI',
filterEndpoints: ['createDashboard', 'updateDashboard'],
tag: true,
},
```
-### 4. Run the API Client script
+### 4. Run the API client generation script
Then, we are ready to run the script to create the API client:
@@ -100,7 +98,7 @@ export { type Dashboard } from './endpoints.gen';
```
-There are some use cases where the hook will not work, and that is a clue to see if it needs to be modified. The hooks can be tweaked by using `enhanceEndpoints`.
+There are some use cases where the hook will not work out of the box, and that is a clue to see if it needs to be modified. The hooks can be tweaked by using `enhanceEndpoints`.
```jsx
export const dashboardsAPI = generatedApi.enhanceEndpoints({
diff --git a/public/app/routes/RoutesWrapper.tsx b/public/app/routes/RoutesWrapper.tsx
index 8a897553c7c..7e4701f040e 100644
--- a/public/app/routes/RoutesWrapper.tsx
+++ b/public/app/routes/RoutesWrapper.tsx
@@ -6,6 +6,7 @@ import { CompatRouter } from 'react-router-dom-v5-compat';
import { GrafanaTheme2 } from '@grafana/data/';
import {
+ config,
locationService,
LocationServiceProvider,
useChromeHeaderHeight,
@@ -114,7 +115,7 @@ export function ExperimentalSplitPaneRouterWrapper(props: RouterWrapperProps) {
-
+