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) { - +