From 3348dd5717e0f289a94aabe5869f1a14d60e7a23 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 4 Dec 2024 15:41:18 +0200 Subject: [PATCH 01/11] ReactContexts: PoC --- .../scenes-app/src/components/App/App.tsx | 5 +- .../src/components/SomeReactContext.tsx | 20 ++++++++ packages/scenes-app/src/demos/index.ts | 2 + .../CustomObjectWithReactContext.tsx | 36 +++++++++++++ .../src/demos/reactContext/ReactContext.tsx | 16 ++++++ packages/scenes/src/core/ReactContexts.tsx | 50 +++++++++++++++++++ .../scenes/src/core/SceneComponentWrapper.tsx | 32 +++++++++++- packages/scenes/src/core/SceneObjectBase.tsx | 7 +++ packages/scenes/src/core/types.ts | 17 ++++++- packages/scenes/src/index.ts | 1 + 10 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 packages/scenes-app/src/components/SomeReactContext.tsx create mode 100644 packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx create mode 100644 packages/scenes-app/src/demos/reactContext/ReactContext.tsx create mode 100644 packages/scenes/src/core/ReactContexts.tsx diff --git a/packages/scenes-app/src/components/App/App.tsx b/packages/scenes-app/src/components/App/App.tsx index 7122d3923..63ae07bdd 100644 --- a/packages/scenes-app/src/components/App/App.tsx +++ b/packages/scenes-app/src/components/App/App.tsx @@ -2,12 +2,15 @@ import * as React from 'react'; import { AppRootProps } from '@grafana/data'; import { PluginPropsContext } from '../../utils/utils.plugin'; import { Routes } from '../Routes'; +import { SomeReactContextProvider } from '../SomeReactContext'; export class App extends React.PureComponent { render() { return ( - + + + ); } diff --git a/packages/scenes-app/src/components/SomeReactContext.tsx b/packages/scenes-app/src/components/SomeReactContext.tsx new file mode 100644 index 000000000..efe03d6ff --- /dev/null +++ b/packages/scenes-app/src/components/SomeReactContext.tsx @@ -0,0 +1,20 @@ +import React, { createContext, useMemo, useState } from 'react'; + +export interface SomeReactContextValue { + value: number; + setValue: (value: number) => void; +} + +export const SomeReactContext = createContext({ value: 0, setValue: () => undefined }); + +interface Props { + children: React.ReactNode; +} + +export const SomeReactContextProvider = ({ children }: Props) => { + const [value, setValue] = useState(0); + + const contextValue = useMemo(() => ({ value, setValue }), [value, setValue]); + + return {children}; +}; diff --git a/packages/scenes-app/src/demos/index.ts b/packages/scenes-app/src/demos/index.ts index d2296b432..e423adf42 100644 --- a/packages/scenes-app/src/demos/index.ts +++ b/packages/scenes-app/src/demos/index.ts @@ -40,6 +40,7 @@ import { getUrlSyncTest } from './urlSyncTest'; import { getMlDemo } from './ml'; import { getSceneGraphEventsDemo } from './sceneGraphEvents'; import { getSeriesLimitTest } from './seriesLimit'; +import { getReactContext } from './reactContext/ReactContext'; export interface DemoDescriptor { title: string; @@ -89,5 +90,6 @@ export function getDemos(): DemoDescriptor[] { { title: 'Machine Learning', getPage: getMlDemo }, { title: 'Events on the Scene Graph', getPage: getSceneGraphEventsDemo }, { title: 'Series limit', getPage: getSeriesLimitTest }, + { title: 'React Contexts', getPage: getReactContext }, ].sort((a, b) => a.title.localeCompare(b.title)); } diff --git a/packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx b/packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx new file mode 100644 index 000000000..f69c2dbf7 --- /dev/null +++ b/packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx @@ -0,0 +1,36 @@ +import React, { useContext } from 'react'; +import { ReactContexts, SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; +import { Button } from '@grafana/ui'; + +import { SomeReactContext } from '../../components/SomeReactContext'; + +export class CustomObjectWithReactContext extends SceneObjectBase { + static Component = ({ model }: SceneComponentProps) => { + const context = useContext(SomeReactContext); + + return ( + <> +
Value: {context?.value}
+
+ +
+ + ); + }; + + protected _reactContexts = new ReactContexts(this, [{ context: SomeReactContext }]); + + public constructor() { + super({}); + + this.addActivationHandler(() => { + console.log('Value on activation:', this.reactContexts?.getContext(SomeReactContext)?.value); + + this._subs.add( + this.reactContexts?.subscribeToContext(SomeReactContext, (newValue, prevValue) => { + console.log('Value changed:', newValue, prevValue); + }) + ); + }); + } +} diff --git a/packages/scenes-app/src/demos/reactContext/ReactContext.tsx b/packages/scenes-app/src/demos/reactContext/ReactContext.tsx new file mode 100644 index 000000000..a264fb4f5 --- /dev/null +++ b/packages/scenes-app/src/demos/reactContext/ReactContext.tsx @@ -0,0 +1,16 @@ +import { EmbeddedScene, SceneAppPage, SceneAppPageState } from '@grafana/scenes'; +import { getEmbeddedSceneDefaults } from '../utils'; +import { CustomObjectWithReactContext } from './CustomObjectWithReactContext'; + +export function getReactContext(defaults: SceneAppPageState) { + return new SceneAppPage({ + ...defaults, + subTitle: 'Example of custom object that is consuming a React Context', + getScene: () => { + return new EmbeddedScene({ + ...getEmbeddedSceneDefaults(), + body: new CustomObjectWithReactContext(), + }); + }, + }); +} diff --git a/packages/scenes/src/core/ReactContexts.tsx b/packages/scenes/src/core/ReactContexts.tsx new file mode 100644 index 000000000..de3d2419d --- /dev/null +++ b/packages/scenes/src/core/ReactContexts.tsx @@ -0,0 +1,50 @@ +import { Context } from 'react'; +import { ReactContextsHandler, ReactContextsHandlerEntry, SceneObject } from './types'; +import { BehaviorSubject, filter, map, pairwise, Unsubscribable } from 'rxjs'; + +export class ReactContexts implements ReactContextsHandler { + private _ctxMap: BehaviorSubject, any>>; + + public constructor(public _sceneObject: SceneObject, public _contexts: ReactContextsHandlerEntry[]) { + this._ctxMap = new BehaviorSubject(new WeakMap, any>()); + } + + public getContextsList(): ReactContextsHandlerEntry[] { + return this._contexts; + } + + public getContext(ctx: Context): T | undefined { + return this._ctxMap.getValue().get(ctx); + } + + public subscribeToContext(ctx: Context, cb: { (newCtxValue: T, prevCtxValue: T): void }): Unsubscribable { + return this._ctxMap + .pipe( + pairwise(), + filter(([prevCtxMap, newCtxMap]) => prevCtxMap.get(ctx) !== newCtxMap.get(ctx)), + map(([prevCtxMap, newCtxMap]) => [newCtxMap.get(ctx), prevCtxMap.get(ctx)]) + ) + .subscribe(([newValue, prevValue]) => { + cb(newValue, prevValue); + }); + } + + public useContext(ctx: Context): T { + return this._ctxMap.getValue().get(ctx); + } + + public updateContext(ctx: Context, ctxValue: T) { + const currentCtxMap = this._ctxMap.getValue(); + + if (currentCtxMap.get(ctx) !== ctxValue) { + const newCtxMap = this._contexts.reduce((acc, ctxEntry) => { + acc.set(ctxEntry.context, ctxEntry.context === ctx ? ctxValue : currentCtxMap.get(ctxEntry.context)); + + return acc; + }, new WeakMap, any>()); + + this._ctxMap.next(newCtxMap); + this._sceneObject.forceRender(); + } + } +} diff --git a/packages/scenes/src/core/SceneComponentWrapper.tsx b/packages/scenes/src/core/SceneComponentWrapper.tsx index 4a8840108..f5d1c103c 100644 --- a/packages/scenes/src/core/SceneComponentWrapper.tsx +++ b/packages/scenes/src/core/SceneComponentWrapper.tsx @@ -1,8 +1,29 @@ -import React, { useEffect, useState } from 'react'; +import React, { Context, useContext, useEffect, useState } from 'react'; import { SceneComponentProps, SceneObject } from './types'; -function SceneComponentWrapperWithoutMemo({ model, ...otherProps }: SceneComponentProps) { +function ContextsConsumer({ model }: SceneComponentProps) { + return model.reactContexts + ?.getContextsList() + .map((ctx, idx) => ); +} + +interface MySceneObjectContextConsumerProps { + ctx: Context; + model: SceneObject; +} + +function MySceneObjectContextConsumer({ ctx, model }: MySceneObjectContextConsumerProps) { + const ctxValue = useContext(ctx); + + useEffect(() => { + model.reactContexts?.updateContext(ctx, ctxValue); + }, [ctx, ctxValue, model]); + + return null; +} + +function ComponentRenderer({ model, ...otherProps }: SceneComponentProps) { const Component = (model as any).constructor['Component'] ?? EmptyRenderer; const [_, setValue] = useState(0); @@ -22,6 +43,13 @@ function SceneComponentWrapperWithoutMemo({ model, ...oth return ; } +function SceneComponentWrapperWithoutMemo({ model, ...otherProps }: SceneComponentProps) { + return [ + , + , + ]; +} + export const SceneComponentWrapper = React.memo(SceneComponentWrapperWithoutMemo); function EmptyRenderer(_: SceneComponentProps): React.ReactElement | null { diff --git a/packages/scenes/src/core/SceneObjectBase.tsx b/packages/scenes/src/core/SceneObjectBase.tsx index 6882ede9d..425b625c1 100644 --- a/packages/scenes/src/core/SceneObjectBase.tsx +++ b/packages/scenes/src/core/SceneObjectBase.tsx @@ -21,6 +21,7 @@ import { SceneObjectStateChangedEvent } from './events'; import { cloneSceneObject } from './sceneGraph/utils'; import { SceneVariableDependencyConfigLike } from '../variables/types'; import { SceneObjectRef } from './SceneObjectRef'; +import { ReactContexts } from './ReactContexts'; export abstract class SceneObjectBase implements SceneObject @@ -39,6 +40,7 @@ export abstract class SceneObjectBase /** This abstraction declares URL sync dependencies of a scene object. **/ readonly urlSync?: SceneObjectUrlSyncHandler; + /** The React contexts to which this object subscribes */ + readonly reactContexts?: ReactContextsHandler; + /** Subscribe to state changes */ subscribeToState(handler: SceneStateChangedHandler): Unsubscribable; @@ -300,3 +303,15 @@ export interface SceneUrlSyncOptions { */ createBrowserHistorySteps?: boolean; } + +export interface ReactContextsHandlerEntry { + context: Context; +} + +export interface ReactContextsHandler { + getContextsList(): ReactContextsHandlerEntry[]; + getContext(ctx: React.Context): T | undefined; + subscribeToContext(ctx: React.Context, cb: (newCtxValue: T, prevCtxValue: T) => void): Unsubscribable; + useContext(ctx: React.Context): T | undefined; + updateContext(ctx: React.Context, ctxValue: T): void; +} diff --git a/packages/scenes/src/index.ts b/packages/scenes/src/index.ts index 3b2c4a371..1bc7c51f2 100644 --- a/packages/scenes/src/index.ts +++ b/packages/scenes/src/index.ts @@ -27,6 +27,7 @@ export { SceneObjectBase, useSceneObjectState } from './core/SceneObjectBase'; export { SceneDataNode } from './core/SceneDataNode'; export { SceneTimeRange } from './core/SceneTimeRange'; export { SceneTimeZoneOverride } from './core/SceneTimeZoneOverride'; +export { ReactContexts } from './core/ReactContexts'; export { SceneQueryRunner, type QueryRunnerState } from './querying/SceneQueryRunner'; export { DataProviderProxy } from './querying/DataProviderProxy'; From d9be47d738ad9a1f6ff7e4380e79d244984479e0 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Wed, 4 Dec 2024 15:46:50 +0200 Subject: [PATCH 02/11] Remove unused `useContext` hook --- packages/scenes/src/core/ReactContexts.tsx | 4 ---- packages/scenes/src/core/types.ts | 1 - 2 files changed, 5 deletions(-) diff --git a/packages/scenes/src/core/ReactContexts.tsx b/packages/scenes/src/core/ReactContexts.tsx index de3d2419d..90b3678b6 100644 --- a/packages/scenes/src/core/ReactContexts.tsx +++ b/packages/scenes/src/core/ReactContexts.tsx @@ -29,10 +29,6 @@ export class ReactContexts implements ReactContextsHandler { }); } - public useContext(ctx: Context): T { - return this._ctxMap.getValue().get(ctx); - } - public updateContext(ctx: Context, ctxValue: T) { const currentCtxMap = this._ctxMap.getValue(); diff --git a/packages/scenes/src/core/types.ts b/packages/scenes/src/core/types.ts index 550ab263b..128e3e411 100644 --- a/packages/scenes/src/core/types.ts +++ b/packages/scenes/src/core/types.ts @@ -312,6 +312,5 @@ export interface ReactContextsHandler { getContextsList(): ReactContextsHandlerEntry[]; getContext(ctx: React.Context): T | undefined; subscribeToContext(ctx: React.Context, cb: (newCtxValue: T, prevCtxValue: T) => void): Unsubscribable; - useContext(ctx: React.Context): T | undefined; updateContext(ctx: React.Context, ctxValue: T): void; } From 4fe7683a37bcdcc12f1c813a3cc5aa9e26b366ba Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 5 Dec 2024 14:36:10 +0200 Subject: [PATCH 03/11] Refactor --- .../scenes-app/src/components/App/App.tsx | 5 +- .../src/components/SomeReactContext.tsx | 20 ---- packages/scenes-app/src/demos/index.ts | 2 - .../CustomObjectWithReactContext.tsx | 36 ------ .../src/demos/reactContext/ReactContext.tsx | 16 --- .../src/components/SceneApp/SceneApp.tsx | 29 ++--- .../src/components/SceneApp/SceneAppPage.tsx | 21 ++++ .../scenes/src/components/SceneApp/types.ts | 5 + packages/scenes/src/core/ReactContexts.tsx | 46 -------- .../scenes/src/core/SceneComponentWrapper.tsx | 32 +----- packages/scenes/src/core/SceneObjectBase.tsx | 7 -- packages/scenes/src/core/SceneScopesBridge.ts | 103 ++++++++++++++++++ packages/scenes/src/core/ScopesContext.ts | 41 +++++++ packages/scenes/src/core/sceneGraph/index.ts | 2 + .../scenes/src/core/sceneGraph/sceneGraph.ts | 8 ++ packages/scenes/src/core/types.ts | 16 +-- packages/scenes/src/index.ts | 3 +- .../scenes/src/querying/SceneQueryRunner.ts | 52 ++++++++- .../variables/adhoc/AdHocFilterRenderer.tsx | 10 +- .../variables/adhoc/AdHocFiltersVariable.tsx | 29 +++++ .../src/variables/groupby/GroupByVariable.tsx | 26 ++++- .../src/variables/variants/ScopesVariable.ts | 1 + 22 files changed, 309 insertions(+), 201 deletions(-) delete mode 100644 packages/scenes-app/src/components/SomeReactContext.tsx delete mode 100644 packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx delete mode 100644 packages/scenes-app/src/demos/reactContext/ReactContext.tsx delete mode 100644 packages/scenes/src/core/ReactContexts.tsx create mode 100644 packages/scenes/src/core/SceneScopesBridge.ts create mode 100644 packages/scenes/src/core/ScopesContext.ts create mode 100644 packages/scenes/src/variables/variants/ScopesVariable.ts diff --git a/packages/scenes-app/src/components/App/App.tsx b/packages/scenes-app/src/components/App/App.tsx index 63ae07bdd..7122d3923 100644 --- a/packages/scenes-app/src/components/App/App.tsx +++ b/packages/scenes-app/src/components/App/App.tsx @@ -2,15 +2,12 @@ import * as React from 'react'; import { AppRootProps } from '@grafana/data'; import { PluginPropsContext } from '../../utils/utils.plugin'; import { Routes } from '../Routes'; -import { SomeReactContextProvider } from '../SomeReactContext'; export class App extends React.PureComponent { render() { return ( - - - + ); } diff --git a/packages/scenes-app/src/components/SomeReactContext.tsx b/packages/scenes-app/src/components/SomeReactContext.tsx deleted file mode 100644 index efe03d6ff..000000000 --- a/packages/scenes-app/src/components/SomeReactContext.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, { createContext, useMemo, useState } from 'react'; - -export interface SomeReactContextValue { - value: number; - setValue: (value: number) => void; -} - -export const SomeReactContext = createContext({ value: 0, setValue: () => undefined }); - -interface Props { - children: React.ReactNode; -} - -export const SomeReactContextProvider = ({ children }: Props) => { - const [value, setValue] = useState(0); - - const contextValue = useMemo(() => ({ value, setValue }), [value, setValue]); - - return {children}; -}; diff --git a/packages/scenes-app/src/demos/index.ts b/packages/scenes-app/src/demos/index.ts index e423adf42..d2296b432 100644 --- a/packages/scenes-app/src/demos/index.ts +++ b/packages/scenes-app/src/demos/index.ts @@ -40,7 +40,6 @@ import { getUrlSyncTest } from './urlSyncTest'; import { getMlDemo } from './ml'; import { getSceneGraphEventsDemo } from './sceneGraphEvents'; import { getSeriesLimitTest } from './seriesLimit'; -import { getReactContext } from './reactContext/ReactContext'; export interface DemoDescriptor { title: string; @@ -90,6 +89,5 @@ export function getDemos(): DemoDescriptor[] { { title: 'Machine Learning', getPage: getMlDemo }, { title: 'Events on the Scene Graph', getPage: getSceneGraphEventsDemo }, { title: 'Series limit', getPage: getSeriesLimitTest }, - { title: 'React Contexts', getPage: getReactContext }, ].sort((a, b) => a.title.localeCompare(b.title)); } diff --git a/packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx b/packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx deleted file mode 100644 index f69c2dbf7..000000000 --- a/packages/scenes-app/src/demos/reactContext/CustomObjectWithReactContext.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React, { useContext } from 'react'; -import { ReactContexts, SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; -import { Button } from '@grafana/ui'; - -import { SomeReactContext } from '../../components/SomeReactContext'; - -export class CustomObjectWithReactContext extends SceneObjectBase { - static Component = ({ model }: SceneComponentProps) => { - const context = useContext(SomeReactContext); - - return ( - <> -
Value: {context?.value}
-
- -
- - ); - }; - - protected _reactContexts = new ReactContexts(this, [{ context: SomeReactContext }]); - - public constructor() { - super({}); - - this.addActivationHandler(() => { - console.log('Value on activation:', this.reactContexts?.getContext(SomeReactContext)?.value); - - this._subs.add( - this.reactContexts?.subscribeToContext(SomeReactContext, (newValue, prevValue) => { - console.log('Value changed:', newValue, prevValue); - }) - ); - }); - } -} diff --git a/packages/scenes-app/src/demos/reactContext/ReactContext.tsx b/packages/scenes-app/src/demos/reactContext/ReactContext.tsx deleted file mode 100644 index a264fb4f5..000000000 --- a/packages/scenes-app/src/demos/reactContext/ReactContext.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { EmbeddedScene, SceneAppPage, SceneAppPageState } from '@grafana/scenes'; -import { getEmbeddedSceneDefaults } from '../utils'; -import { CustomObjectWithReactContext } from './CustomObjectWithReactContext'; - -export function getReactContext(defaults: SceneAppPageState) { - return new SceneAppPage({ - ...defaults, - subTitle: 'Example of custom object that is consuming a React Context', - getScene: () => { - return new EmbeddedScene({ - ...getEmbeddedSceneDefaults(), - body: new CustomObjectWithReactContext(), - }); - }, - }); -} diff --git a/packages/scenes/src/components/SceneApp/SceneApp.tsx b/packages/scenes/src/components/SceneApp/SceneApp.tsx index fc4583fc4..6b047b848 100644 --- a/packages/scenes/src/components/SceneApp/SceneApp.tsx +++ b/packages/scenes/src/components/SceneApp/SceneApp.tsx @@ -17,21 +17,24 @@ export class SceneApp extends SceneObjectBase implements DataRequ } public static Component = ({ model }: SceneComponentProps) => { - const { pages } = model.useState(); + const { pages, scopesBridge } = model.useState(); return ( - - - {pages.map((page) => ( - renderSceneComponentWithRouteProps(page, props)} - > - ))} - - + <> + {scopesBridge && } + + + {pages.map((page) => ( + renderSceneComponentWithRouteProps(page, props)} + > + ))} + + + ); }; } diff --git a/packages/scenes/src/components/SceneApp/SceneAppPage.tsx b/packages/scenes/src/components/SceneApp/SceneAppPage.tsx index 9fc3cd83b..442ee04ae 100644 --- a/packages/scenes/src/components/SceneApp/SceneAppPage.tsx +++ b/packages/scenes/src/components/SceneApp/SceneAppPage.tsx @@ -8,6 +8,8 @@ import { SceneReactObject } from '../SceneReactObject'; import { SceneAppDrilldownViewRender, SceneAppPageView } from './SceneAppPageView'; import { SceneAppDrilldownView, SceneAppPageLike, SceneAppPageState, SceneRouteMatch } from './types'; import { renderSceneComponentWithRouteProps } from './utils'; +import { sceneGraph } from '../../core/sceneGraph'; +import { SceneScopesBridge } from '../../core/SceneScopesBridge'; /** * Responsible for page's drilldown & tabs routing @@ -16,11 +18,30 @@ export class SceneAppPage extends SceneObjectBase implements public static Component = SceneAppPageRenderer; private _sceneCache = new Map(); private _drilldownCache = new Map(); + private _scopesBridge: SceneScopesBridge | undefined; public constructor(state: SceneAppPageState) { super(state); + + this.addActivationHandler(this._activationHandler); } + private _activationHandler = () => { + this._scopesBridge = sceneGraph.getScopesBridge(this); + + if (this.state.useScopes) { + if (!this._scopesBridge) { + throw new Error('Use of scopes is enabled but no scopes bridge found'); + } + + this._scopesBridge.enable(); + + return () => { + this._scopesBridge?.disable(); + }; + } + }; + public initializeScene(scene: EmbeddedScene) { this.setState({ initializedScene: scene }); } diff --git a/packages/scenes/src/components/SceneApp/types.ts b/packages/scenes/src/components/SceneApp/types.ts index 054c8c72b..13cedcba4 100644 --- a/packages/scenes/src/components/SceneApp/types.ts +++ b/packages/scenes/src/components/SceneApp/types.ts @@ -2,6 +2,7 @@ import { ComponentType } from 'react'; import { DataRequestEnricher, SceneObject, SceneObjectState, SceneUrlSyncOptions } from '../../core/types'; import { EmbeddedScene } from '../EmbeddedScene'; import { IconName, PageLayoutType } from '@grafana/data'; +import { SceneScopesBridge } from '../../core/SceneScopesBridge'; export interface SceneRouteMatch { params: Params; @@ -15,6 +16,7 @@ export interface SceneAppState extends SceneObjectState { pages: SceneAppPageLike[]; name?: string; urlSyncOptions?: SceneUrlSyncOptions; + scopesBridge?: SceneScopesBridge; } export interface SceneAppRoute { @@ -69,6 +71,9 @@ export interface SceneAppPageState extends SceneObjectState { getFallbackPage?: () => SceneAppPageLike; layout?: PageLayoutType; + + // Whether to use scopes for this page + useScopes?: boolean; } export interface SceneAppPageLike extends SceneObject, DataRequestEnricher { diff --git a/packages/scenes/src/core/ReactContexts.tsx b/packages/scenes/src/core/ReactContexts.tsx deleted file mode 100644 index 90b3678b6..000000000 --- a/packages/scenes/src/core/ReactContexts.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Context } from 'react'; -import { ReactContextsHandler, ReactContextsHandlerEntry, SceneObject } from './types'; -import { BehaviorSubject, filter, map, pairwise, Unsubscribable } from 'rxjs'; - -export class ReactContexts implements ReactContextsHandler { - private _ctxMap: BehaviorSubject, any>>; - - public constructor(public _sceneObject: SceneObject, public _contexts: ReactContextsHandlerEntry[]) { - this._ctxMap = new BehaviorSubject(new WeakMap, any>()); - } - - public getContextsList(): ReactContextsHandlerEntry[] { - return this._contexts; - } - - public getContext(ctx: Context): T | undefined { - return this._ctxMap.getValue().get(ctx); - } - - public subscribeToContext(ctx: Context, cb: { (newCtxValue: T, prevCtxValue: T): void }): Unsubscribable { - return this._ctxMap - .pipe( - pairwise(), - filter(([prevCtxMap, newCtxMap]) => prevCtxMap.get(ctx) !== newCtxMap.get(ctx)), - map(([prevCtxMap, newCtxMap]) => [newCtxMap.get(ctx), prevCtxMap.get(ctx)]) - ) - .subscribe(([newValue, prevValue]) => { - cb(newValue, prevValue); - }); - } - - public updateContext(ctx: Context, ctxValue: T) { - const currentCtxMap = this._ctxMap.getValue(); - - if (currentCtxMap.get(ctx) !== ctxValue) { - const newCtxMap = this._contexts.reduce((acc, ctxEntry) => { - acc.set(ctxEntry.context, ctxEntry.context === ctx ? ctxValue : currentCtxMap.get(ctxEntry.context)); - - return acc; - }, new WeakMap, any>()); - - this._ctxMap.next(newCtxMap); - this._sceneObject.forceRender(); - } - } -} diff --git a/packages/scenes/src/core/SceneComponentWrapper.tsx b/packages/scenes/src/core/SceneComponentWrapper.tsx index f5d1c103c..4a8840108 100644 --- a/packages/scenes/src/core/SceneComponentWrapper.tsx +++ b/packages/scenes/src/core/SceneComponentWrapper.tsx @@ -1,29 +1,8 @@ -import React, { Context, useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { SceneComponentProps, SceneObject } from './types'; -function ContextsConsumer({ model }: SceneComponentProps) { - return model.reactContexts - ?.getContextsList() - .map((ctx, idx) => ); -} - -interface MySceneObjectContextConsumerProps { - ctx: Context; - model: SceneObject; -} - -function MySceneObjectContextConsumer({ ctx, model }: MySceneObjectContextConsumerProps) { - const ctxValue = useContext(ctx); - - useEffect(() => { - model.reactContexts?.updateContext(ctx, ctxValue); - }, [ctx, ctxValue, model]); - - return null; -} - -function ComponentRenderer({ model, ...otherProps }: SceneComponentProps) { +function SceneComponentWrapperWithoutMemo({ model, ...otherProps }: SceneComponentProps) { const Component = (model as any).constructor['Component'] ?? EmptyRenderer; const [_, setValue] = useState(0); @@ -43,13 +22,6 @@ function ComponentRenderer({ model, ...otherProps }: Scen return ; } -function SceneComponentWrapperWithoutMemo({ model, ...otherProps }: SceneComponentProps) { - return [ - , - , - ]; -} - export const SceneComponentWrapper = React.memo(SceneComponentWrapperWithoutMemo); function EmptyRenderer(_: SceneComponentProps): React.ReactElement | null { diff --git a/packages/scenes/src/core/SceneObjectBase.tsx b/packages/scenes/src/core/SceneObjectBase.tsx index 425b625c1..6882ede9d 100644 --- a/packages/scenes/src/core/SceneObjectBase.tsx +++ b/packages/scenes/src/core/SceneObjectBase.tsx @@ -21,7 +21,6 @@ import { SceneObjectStateChangedEvent } from './events'; import { cloneSceneObject } from './sceneGraph/utils'; import { SceneVariableDependencyConfigLike } from '../variables/types'; import { SceneObjectRef } from './SceneObjectRef'; -import { ReactContexts } from './ReactContexts'; export abstract class SceneObjectBase implements SceneObject @@ -40,7 +39,6 @@ export abstract class SceneObjectBase { + static Component = SceneScopesBridgeRenderer; + + protected _renderBeforeActivation = true; + + private _contextSubject = new BehaviorSubject(undefined); + + public constructor(state: SceneScopesBridgeState) { + super(state); + } + + public getValue(): Scope[] { + return this.context?.state.value ?? []; + } + + public subscribeToValue(cb: (newScopes: Scope[], prevScopes: Scope[]) => void): Unsubscribable { + return this.contextObservable + .pipe( + filter((context) => !!context && !context.state.isLoading), + pairwise(), + map( + ([prevContext, newContext]) => + [prevContext?.state.value ?? [], newContext?.state.value ?? []] as [Scope[], Scope[]] + ), + filter(([prevScopes, newScopes]) => !isEqual(prevScopes, newScopes)) + ) + .subscribe(([prevScopes, newScopes]) => { + cb(newScopes, prevScopes); + }); + } + + public getIsLoading(): boolean { + return this.context?.state.isLoading ?? false; + } + + public subscribeToIsLoading(cb: (isLoading: boolean) => void): Unsubscribable { + return this.contextObservable + .pipe( + filter((context) => !!context), + pairwise(), + map( + ([prevContext, newContext]) => + [prevContext?.state.isLoading ?? false, newContext?.state.isLoading ?? false] as [boolean, boolean] + ), + filter(([prevIsLoading, newIsLoading]) => prevIsLoading !== newIsLoading) + ) + .subscribe(([_prevIsLoading, newIsLoading]) => { + cb(newIsLoading); + }); + } + + public enable() { + this.context?.enable(); + } + + public disable() { + this.context?.disable(); + } + + public enterReadOnly() { + this.context?.enterReadOnly(); + } + + public exitReadOnly() { + this.context?.exitReadOnly(); + } + + public updateContext(newContext: ScopesContextValue | undefined) { + if (this.context !== newContext) { + this._contextSubject.next(newContext); + } + } + + private get context(): ScopesContextValue | undefined { + return this._contextSubject.getValue(); + } + + private get contextObservable(): Observable { + return this._contextSubject.asObservable(); + } +} + +function SceneScopesBridgeRenderer({ model }: SceneComponentProps) { + const context = useScopes(); + + useEffect(() => { + model.updateContext(context); + }, [context, model]); + + return null; +} diff --git a/packages/scenes/src/core/ScopesContext.ts b/packages/scenes/src/core/ScopesContext.ts new file mode 100644 index 000000000..8b22ea60f --- /dev/null +++ b/packages/scenes/src/core/ScopesContext.ts @@ -0,0 +1,41 @@ +import { createContext, useContext } from 'react'; +import { useObservable } from 'react-use'; +import { Observable, Subscription } from 'rxjs'; + +import { Scope } from '@grafana/data'; + +export interface ScopesContextValue { + state: { + isEnabled: boolean; + isLoading: boolean; + isReadOnly: boolean; + pendingScopes: string[] | null; + value: Scope[]; + }; + stateObservable: Observable; + setNewScopes: (scopeNames: string[] | null) => void; + setCurrentScopes: (scopes: Scope[]) => void; + enterLoadingMode: () => void; + exitLoadingMode: () => void; + enterReadOnly: () => void; + exitReadOnly: () => void; + enable: () => void; + disable: () => void; + subscribeToState: ( + cb: (newState: ScopesContextValue['state'], prevState: ScopesContextValue['state']) => void + ) => Subscription; +} + +export const ScopesContext = createContext(undefined); + +export function useScopes() { + const context = useContext(ScopesContext); + + useObservable(context?.stateObservable ?? new Observable(), context?.state); + + if (!context) { + return undefined; + } + + return context; +} diff --git a/packages/scenes/src/core/sceneGraph/index.ts b/packages/scenes/src/core/sceneGraph/index.ts index e038d45c6..b6a3de5b4 100644 --- a/packages/scenes/src/core/sceneGraph/index.ts +++ b/packages/scenes/src/core/sceneGraph/index.ts @@ -15,6 +15,7 @@ import { findDescendents, getQueryController, getUrlSyncManager, + getScopesBridge, } from './sceneGraph'; export const sceneGraph = { @@ -34,4 +35,5 @@ export const sceneGraph = { findDescendents, getQueryController, getUrlSyncManager, + getScopesBridge, }; diff --git a/packages/scenes/src/core/sceneGraph/sceneGraph.ts b/packages/scenes/src/core/sceneGraph/sceneGraph.ts index 72c5402d1..b2d6c6d7a 100644 --- a/packages/scenes/src/core/sceneGraph/sceneGraph.ts +++ b/packages/scenes/src/core/sceneGraph/sceneGraph.ts @@ -11,6 +11,7 @@ import { SceneQueryControllerLike, isQueryController } from '../../behaviors/Sce import { VariableInterpolation } from '@grafana/runtime'; import { QueryVariable } from '../../variables/variants/query/QueryVariable'; import { UrlSyncManagerLike } from '../../services/UrlSyncManager'; +import { SceneScopesBridge } from '../SceneScopesBridge'; /** * Get the closest node with variables @@ -300,3 +301,10 @@ export function getUrlSyncManager(sceneObject: SceneObject): UrlSyncManagerLike return undefined; } + +/** + * Will walk up the scene object graph to the closest $scopesBridge scene object + */ +export function getScopesBridge(sceneObject: SceneObject): SceneScopesBridge | undefined { + return (findObject(sceneObject, (s) => s instanceof SceneScopesBridge) as SceneScopesBridge) ?? undefined; +} diff --git a/packages/scenes/src/core/types.ts b/packages/scenes/src/core/types.ts index 128e3e411..666586c61 100644 --- a/packages/scenes/src/core/types.ts +++ b/packages/scenes/src/core/types.ts @@ -1,4 +1,4 @@ -import React, { Context } from 'react'; +import React from 'react'; import { MonoTypeOperatorFunction, Observable, Unsubscribable } from 'rxjs'; import { @@ -73,9 +73,6 @@ export interface SceneObject /** This abstraction declares URL sync dependencies of a scene object. **/ readonly urlSync?: SceneObjectUrlSyncHandler; - /** The React contexts to which this object subscribes */ - readonly reactContexts?: ReactContextsHandler; - /** Subscribe to state changes */ subscribeToState(handler: SceneStateChangedHandler): Unsubscribable; @@ -303,14 +300,3 @@ export interface SceneUrlSyncOptions { */ createBrowserHistorySteps?: boolean; } - -export interface ReactContextsHandlerEntry { - context: Context; -} - -export interface ReactContextsHandler { - getContextsList(): ReactContextsHandlerEntry[]; - getContext(ctx: React.Context): T | undefined; - subscribeToContext(ctx: React.Context, cb: (newCtxValue: T, prevCtxValue: T) => void): Unsubscribable; - updateContext(ctx: React.Context, ctxValue: T): void; -} diff --git a/packages/scenes/src/index.ts b/packages/scenes/src/index.ts index 1bc7c51f2..ec7f85b6f 100644 --- a/packages/scenes/src/index.ts +++ b/packages/scenes/src/index.ts @@ -27,7 +27,6 @@ export { SceneObjectBase, useSceneObjectState } from './core/SceneObjectBase'; export { SceneDataNode } from './core/SceneDataNode'; export { SceneTimeRange } from './core/SceneTimeRange'; export { SceneTimeZoneOverride } from './core/SceneTimeZoneOverride'; -export { ReactContexts } from './core/ReactContexts'; export { SceneQueryRunner, type QueryRunnerState } from './querying/SceneQueryRunner'; export { DataProviderProxy } from './querying/DataProviderProxy'; @@ -128,6 +127,8 @@ export { renderSelectForVariable } from './variables/components/VariableValueSel export { VizConfigBuilder } from './core/PanelBuilders/VizConfigBuilder'; export { VizConfigBuilders } from './core/PanelBuilders/VizConfigBuilders'; export { type VizConfig } from './core/PanelBuilders/types'; +export { SceneScopesBridge } from './core/SceneScopesBridge'; +export { ScopesContext, type ScopesContextValue, useScopes } from './core/ScopesContext'; export const sceneUtils = { getUrlWithAppState, diff --git a/packages/scenes/src/querying/SceneQueryRunner.ts b/packages/scenes/src/querying/SceneQueryRunner.ts index cfb27453c..71e3853d0 100644 --- a/packages/scenes/src/querying/SceneQueryRunner.ts +++ b/packages/scenes/src/querying/SceneQueryRunner.ts @@ -48,6 +48,7 @@ import { SceneVariable } from '../variables/types'; import { DataLayersMerger } from './DataLayersMerger'; import { interpolate } from '../core/sceneGraph/sceneGraph'; import { wrapInSafeSerializableSceneObject } from '../utils/wrapInSafeSerializableSceneObject'; +import { SceneScopesBridge } from '../core/SceneScopesBridge'; let counter = 100; @@ -111,6 +112,8 @@ export class SceneQueryRunner extends SceneObjectBase implemen private _dataLayersMerger = new DataLayersMerger(); private _timeSub?: Unsubscribable; private _timeSubRange?: SceneTimeRangeLike; + private _scopesSub?: Unsubscribable; + private _scopesSubBridge?: SceneScopesBridge; private _containerWidth?: number; private _variableValueRecorder = new VariableValueRecorder(); private _results = new ReplaySubject(1); @@ -140,6 +143,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen private _onActivate() { if (this.isQueryModeAuto()) { const timeRange = sceneGraph.getTimeRange(this); + const scopesBridge = sceneGraph.getScopesBridge(this); // Add subscriptions to any extra providers so that they rerun queries // when their state changes and they should rerun. @@ -154,6 +158,8 @@ export class SceneQueryRunner extends SceneObjectBase implemen ); } + this.subscribeToScopesChanges(scopesBridge); + this.subscribeToTimeRangeChanges(timeRange); if (this.shouldRunQueriesOnActivate()) { @@ -366,6 +372,28 @@ export class SceneQueryRunner extends SceneObjectBase implemen return Boolean(this.state._hasFetchedData); } + private subscribeToScopesChanges(scopesBridge: SceneScopesBridge | undefined) { + if (!scopesBridge) { + // Nothing to do, there's no scopes bridge + return; + } + + if (this._scopesSubBridge === scopesBridge) { + // Nothing to do, already subscribed + return; + } + + if (this._scopesSub) { + this._scopesSub.unsubscribe(); + } + + this._scopesSubBridge = scopesBridge; + + this._scopesSub = scopesBridge.subscribeToValue(() => { + this.runWithTimeRangeAndScopes(sceneGraph.getTimeRange(this), scopesBridge); + }); + } + private subscribeToTimeRangeChanges(timeRange: SceneTimeRangeLike) { if (this._timeSubRange === timeRange) { // Nothing to do, already subscribed @@ -378,17 +406,19 @@ export class SceneQueryRunner extends SceneObjectBase implemen this._timeSubRange = timeRange; this._timeSub = timeRange.subscribeToState(() => { - this.runWithTimeRange(timeRange); + this.runWithTimeRangeAndScopes(timeRange, sceneGraph.getScopesBridge(this)); }); } public runQueries() { const timeRange = sceneGraph.getTimeRange(this); + const scopesBridge = sceneGraph.getScopesBridge(this); if (this.isQueryModeAuto()) { this.subscribeToTimeRangeChanges(timeRange); + this.subscribeToScopesChanges(scopesBridge); } - this.runWithTimeRange(timeRange); + this.runWithTimeRangeAndScopes(timeRange, scopesBridge); } private getMaxDataPoints() { @@ -412,7 +442,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen }); } - private async runWithTimeRange(timeRange: SceneTimeRangeLike) { + private async runWithTimeRangeAndScopes(timeRange: SceneTimeRangeLike, scopesBridge: SceneScopesBridge | undefined) { // If no maxDataPoints specified we might need to wait for container width to be set from the outside if (!this.state.maxDataPoints && this.state.maxDataPointsFromWidth && !this._containerWidth) { return; @@ -433,6 +463,13 @@ export class SceneQueryRunner extends SceneObjectBase implemen return; } + // Skip executing queries if scopes are in loading state + if (scopesBridge?.getIsLoading()) { + writeSceneLog('SceneQueryRunner', 'Scopes are in loading state, skipping query execution'); + this.setState({ data: { ...(this.state.data ?? emptyPanelData), state: LoadingState.Loading } }); + return; + } + const { queries } = this.state; // Simple path when no queries exist @@ -448,7 +485,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen this.findAndSubscribeToAdHocFilters(datasource?.uid); const runRequest = getRunRequest(); - const { primary, secondaries, processors } = this.prepareRequests(timeRange, ds); + const { primary, secondaries, processors } = this.prepareRequests(timeRange, ds, scopesBridge); writeSceneLog('SceneQueryRunner', 'Starting runRequest', this.state.key); @@ -505,7 +542,11 @@ export class SceneQueryRunner extends SceneObjectBase implemen return clone; } - private prepareRequests(timeRange: SceneTimeRangeLike, ds: DataSourceApi): PreparedRequests { + private prepareRequests( + timeRange: SceneTimeRangeLike, + ds: DataSourceApi, + scopesBridge: SceneScopesBridge | undefined + ): PreparedRequests { const { minInterval, queries } = this.state; let request: DataQueryRequest = { @@ -526,6 +567,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen }, cacheTimeout: this.state.cacheTimeout, queryCachingTTL: this.state.queryCachingTTL, + scopes: scopesBridge?.getValue(), // This asks the scene root to provide context properties like app, panel and dashboardUID ...getEnrichedDataRequest(this), }; diff --git a/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx b/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx index be94f3daf..6832a44b3 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx @@ -28,6 +28,8 @@ const filterNoOp = () => true; export function AdHocFilterRenderer({ filter, model }: Props) { const styles = useStyles2(getStyles); + const { _isScopesLoading } = model.useState(); + const [keys, setKeys] = useState([]); const [values, setValues] = useState([]); const [isKeysLoading, setIsKeysLoading] = useState(false); @@ -139,8 +141,8 @@ export function AdHocFilterRenderer({ filter, model }: Props) { // there's a bug in react-select where the menu doesn't recalculate its position when the options are loaded asynchronously // see https://github.com/grafana/grafana/issues/63558 // instead, we explicitly control the menu visibility and prevent showing it until the options have fully loaded - isOpen={isValuesOpen && !isValuesLoading} - isLoading={isValuesLoading} + isOpen={isValuesOpen && !isValuesLoading && !_isScopesLoading} + isLoading={isValuesLoading || _isScopesLoading} openMenuOnFocus={true} onOpenMenu={async () => { setIsValuesLoading(true); @@ -187,8 +189,8 @@ export function AdHocFilterRenderer({ filter, model }: Props) { // there's a bug in react-select where the menu doesn't recalculate its position when the options are loaded asynchronously // see https://github.com/grafana/grafana/issues/63558 // instead, we explicitly control the menu visibility and prevent showing it until the options have fully loaded - isOpen={isKeysOpen && !isKeysLoading} - isLoading={isKeysLoading} + isOpen={isKeysOpen && !isKeysLoading && !_isScopesLoading} + isLoading={isKeysLoading || _isScopesLoading} onOpenMenu={async () => { setIsKeysOpen(true); setIsKeysLoading(true); diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index a05cb82c5..b3684d802 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -16,6 +16,7 @@ import { css } from '@emotion/css'; import { getEnrichedFiltersRequest } from '../getEnrichedFiltersRequest'; import { AdHocFiltersComboboxRenderer } from './AdHocFiltersCombobox/AdHocFiltersComboboxRenderer'; import { wrapInSafeSerializableSceneObject } from '../../utils/wrapInSafeSerializableSceneObject'; +import { SceneScopesBridge } from '../../core/SceneScopesBridge'; export interface AdHocFilterWithLabels extends AdHocVariableFilter { keyLabel?: string; @@ -103,6 +104,11 @@ export interface AdHocFiltersVariableState extends SceneVariableState { * @internal state of the new filter being added */ _wip?: AdHocFilterWithLabels; + + /** + * @internal flag for keeping track of scopes loading state + */ + _isScopesLoading?: boolean; } export type AdHocVariableExpressionBuilderFn = (filters: AdHocFilterWithLabels[]) => string; @@ -170,6 +176,7 @@ export class AdHocFiltersVariable private _scopedVars = { __sceneObject: wrapInSafeSerializableSceneObject(this) }; private _dataSourceSrv = getDataSourceSrv(); + private _scopesBridge: SceneScopesBridge | undefined; protected _urlSync = new AdHocFiltersVariableUrlSyncHandler(this); @@ -187,8 +194,20 @@ export class AdHocFiltersVariable if (this.state.applyMode === 'auto') { patchGetAdhocFilters(this); } + + this.addActivationHandler(this._activationHandler); } + private _activationHandler = () => { + this._scopesBridge = sceneGraph.getScopesBridge(this); + + if (this._scopesBridge) { + this._subs.add( + this._scopesBridge.subscribeToIsLoading((isLoading) => this.setState({ _isScopesLoading: isLoading })) + ); + } + }; + public setState(update: Partial): void { let filterExpressionChanged = false; @@ -283,6 +302,10 @@ export class AdHocFiltersVariable * Get possible keys given current filters. Do not call from plugins directly */ public async _getKeys(currentKey: string | null): Promise>> { + if (this._scopesBridge?.getIsLoading()) { + return []; + } + const override = await this.state.getTagKeysProvider?.(this, currentKey); if (override && override.replace) { @@ -305,6 +328,7 @@ export class AdHocFiltersVariable filters: otherFilters, queries, timeRange, + scopes: this._scopesBridge?.getValue(), ...getEnrichedFiltersRequest(this), }); @@ -329,6 +353,10 @@ export class AdHocFiltersVariable * Get possible key values for a specific key given current filters. Do not call from plugins directly */ public async _getValuesFor(filter: AdHocFilterWithLabels): Promise>> { + if (this._scopesBridge?.getIsLoading()) { + return []; + } + const override = await this.state.getTagValuesProvider?.(this, filter); if (override && override.replace) { @@ -352,6 +380,7 @@ export class AdHocFiltersVariable filters: otherFilters, timeRange, queries, + scopes: this._scopesBridge?.getValue(), ...getEnrichedFiltersRequest(this), }); diff --git a/packages/scenes/src/variables/groupby/GroupByVariable.tsx b/packages/scenes/src/variables/groupby/GroupByVariable.tsx index fd78ddc31..76e93afed 100644 --- a/packages/scenes/src/variables/groupby/GroupByVariable.tsx +++ b/packages/scenes/src/variables/groupby/GroupByVariable.tsx @@ -16,6 +16,7 @@ import { GroupByVariableUrlSyncHandler } from './GroupByVariableUrlSyncHandler'; import { getOptionSearcher } from '../components/getOptionSearcher'; import { getEnrichedFiltersRequest } from '../getEnrichedFiltersRequest'; import { wrapInSafeSerializableSceneObject } from '../../utils/wrapInSafeSerializableSceneObject'; +import { SceneScopesBridge } from '../../core/SceneScopesBridge'; export interface GroupByVariableState extends MultiValueVariableState { /** Defaults to "Group" */ @@ -50,6 +51,11 @@ export interface GroupByVariableState extends MultiValueVariableState { * Return replace: false if you want to combine the results with the default lookup */ getTagKeysProvider?: getTagKeysProvider; + + /** + * @internal flag for keeping track of scopes loading state + */ + _isScopesLoading?: boolean; } export type getTagKeysProvider = ( @@ -63,6 +69,8 @@ export class GroupByVariable extends MultiValueVariable { protected _urlSync: SceneObjectUrlSyncHandler = new GroupByVariableUrlSyncHandler(this); + private _scopesBridge: SceneScopesBridge | undefined; + public validateAndUpdate(): Observable { return this.getValueOptions({}).pipe( map((options) => { @@ -145,8 +153,16 @@ export class GroupByVariable extends MultiValueVariable { }); this.addActivationHandler(() => { + this._scopesBridge = sceneGraph.getScopesBridge(this); + allActiveGroupByVariables.add(this); + if (this._scopesBridge) { + this._subs.add( + this._scopesBridge.subscribeToIsLoading((isLoading) => this.setState({ _isScopesLoading: isLoading })) + ); + } + return () => allActiveGroupByVariables.delete(this); }); } @@ -155,6 +171,10 @@ export class GroupByVariable extends MultiValueVariable { * Get possible keys given current filters. Do not call from plugins directly */ public _getKeys = async (ds: DataSourceApi) => { + if (this._scopesBridge?.getIsLoading()) { + return []; + } + // TODO: provide current dimensions? const override = await this.state.getTagKeysProvider?.(this, null); @@ -178,6 +198,7 @@ export class GroupByVariable extends MultiValueVariable { filters: otherFilters, queries, timeRange, + scopes: this._scopesBridge?.getValue(), ...getEnrichedFiltersRequest(this), }); if (responseHasError(response)) { @@ -204,7 +225,7 @@ export class GroupByVariable extends MultiValueVariable { return { value: [], text: [] }; } } -export function GroupByVariableRenderer({ model }: SceneComponentProps) { +export function GroupByVariableRenderer({ model }: SceneComponentProps) { const { value, text, @@ -214,6 +235,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps>>(() => { @@ -281,7 +303,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps { diff --git a/packages/scenes/src/variables/variants/ScopesVariable.ts b/packages/scenes/src/variables/variants/ScopesVariable.ts new file mode 100644 index 000000000..0f28c983e --- /dev/null +++ b/packages/scenes/src/variables/variants/ScopesVariable.ts @@ -0,0 +1 @@ +export class ScopesVariable {} From fc6363a60dd3f19935c265607c9674a42e32f43b Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 5 Dec 2024 14:37:30 +0200 Subject: [PATCH 04/11] Refactor --- packages/scenes/src/components/SceneApp/SceneApp.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/scenes/src/components/SceneApp/SceneApp.tsx b/packages/scenes/src/components/SceneApp/SceneApp.tsx index 6b047b848..862396ec6 100644 --- a/packages/scenes/src/components/SceneApp/SceneApp.tsx +++ b/packages/scenes/src/components/SceneApp/SceneApp.tsx @@ -10,6 +10,8 @@ import { renderSceneComponentWithRouteProps } from './utils'; * Responsible for top level pages routing */ export class SceneApp extends SceneObjectBase implements DataRequestEnricher { + protected _renderBeforeActivation = true; + public enrichDataRequest() { return { app: this.state.name || 'app', From f5c0b2337f162f71a5f7baf6c89089f2c5d39fcd Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 5 Dec 2024 14:39:01 +0200 Subject: [PATCH 05/11] Refactor --- packages/scenes/src/variables/variants/ScopesVariable.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/scenes/src/variables/variants/ScopesVariable.ts diff --git a/packages/scenes/src/variables/variants/ScopesVariable.ts b/packages/scenes/src/variables/variants/ScopesVariable.ts deleted file mode 100644 index 0f28c983e..000000000 --- a/packages/scenes/src/variables/variants/ScopesVariable.ts +++ /dev/null @@ -1 +0,0 @@ -export class ScopesVariable {} From ad7c523ec57bc408aeee6f465c90724616529773 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 5 Dec 2024 17:58:54 +0200 Subject: [PATCH 06/11] Refactor --- packages/scenes/src/core/SceneScopesBridge.ts | 47 ++++++++++++++++--- packages/scenes/src/core/ScopesContext.ts | 27 +++++------ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/packages/scenes/src/core/SceneScopesBridge.ts b/packages/scenes/src/core/SceneScopesBridge.ts index 14fa8d18a..d727a262a 100644 --- a/packages/scenes/src/core/SceneScopesBridge.ts +++ b/packages/scenes/src/core/SceneScopesBridge.ts @@ -1,26 +1,48 @@ import { isEqual } from 'lodash'; -import { useEffect } from 'react'; import { BehaviorSubject, filter, map, Observable, pairwise, Unsubscribable } from 'rxjs'; import { Scope } from '@grafana/data'; import { SceneObjectBase } from './SceneObjectBase'; -import { SceneComponentProps, SceneObjectState } from './types'; +import { SceneComponentProps, SceneObjectState, SceneObjectUrlValues, SceneObjectWithUrlSync } from './types'; import { ScopesContextValue, useScopes } from './ScopesContext'; +import { SceneObjectUrlSyncConfig } from '../services/SceneObjectUrlSyncConfig'; export interface SceneScopesBridgeState extends SceneObjectState {} -export class SceneScopesBridge extends SceneObjectBase { +export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWithUrlSync { static Component = SceneScopesBridgeRenderer; + protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] }); + protected _renderBeforeActivation = true; private _contextSubject = new BehaviorSubject(undefined); + private _pendingScopes: string[] | null = null; + public constructor(state: SceneScopesBridgeState) { super(state); } + public getUrlState(): SceneObjectUrlValues { + return { + scopes: this._pendingScopes ?? (this.context?.state.value ?? []).map((scope) => scope.metadata.name), + }; + } + + public updateFromUrl(values: SceneObjectUrlValues) { + let scopes = values['scopes'] ?? []; + scopes = (Array.isArray(scopes) ? scopes : [scopes]).map(String); + + if (!this.context) { + this._pendingScopes = scopes; + return; + } + + this.context?.changeScopes(scopes); + } + public getValue(): Scope[] { return this.context?.state.value ?? []; } @@ -78,8 +100,21 @@ export class SceneScopesBridge extends SceneObjectBase { } public updateContext(newContext: ScopesContextValue | undefined) { - if (this.context !== newContext) { + if (this._pendingScopes && newContext) { + setTimeout(() => { + newContext?.changeScopes(this._pendingScopes!); + this._pendingScopes = null; + }); + } + + if (this.context !== newContext || this.context?.state !== newContext?.state) { + const shouldUpdate = this.context?.state.value !== newContext?.state.value; + this._contextSubject.next(newContext); + + if (shouldUpdate) { + setTimeout(() => this.forceRender()); + } } } @@ -95,9 +130,7 @@ export class SceneScopesBridge extends SceneObjectBase { function SceneScopesBridgeRenderer({ model }: SceneComponentProps) { const context = useScopes(); - useEffect(() => { - model.updateContext(context); - }, [context, model]); + model.updateContext(context); return null; } diff --git a/packages/scenes/src/core/ScopesContext.ts b/packages/scenes/src/core/ScopesContext.ts index 8b22ea60f..d2bf02940 100644 --- a/packages/scenes/src/core/ScopesContext.ts +++ b/packages/scenes/src/core/ScopesContext.ts @@ -1,6 +1,6 @@ import { createContext, useContext } from 'react'; import { useObservable } from 'react-use'; -import { Observable, Subscription } from 'rxjs'; +import { Observable } from 'rxjs'; import { Scope } from '@grafana/data'; @@ -9,21 +9,14 @@ export interface ScopesContextValue { isEnabled: boolean; isLoading: boolean; isReadOnly: boolean; - pendingScopes: string[] | null; value: Scope[]; }; stateObservable: Observable; - setNewScopes: (scopeNames: string[] | null) => void; - setCurrentScopes: (scopes: Scope[]) => void; - enterLoadingMode: () => void; - exitLoadingMode: () => void; + changeScopes: (scopeNames: string[]) => void; enterReadOnly: () => void; exitReadOnly: () => void; enable: () => void; disable: () => void; - subscribeToState: ( - cb: (newState: ScopesContextValue['state'], prevState: ScopesContextValue['state']) => void - ) => Subscription; } export const ScopesContext = createContext(undefined); @@ -33,9 +26,15 @@ export function useScopes() { useObservable(context?.stateObservable ?? new Observable(), context?.state); - if (!context) { - return undefined; - } - - return context; + return context + ? { + state: context.state, + stateObservable: context.stateObservable, + changeScopes: context.changeScopes, + enterReadOnly: context.enterReadOnly, + exitReadOnly: context.exitReadOnly, + enable: context.enable, + disable: context.disable, + } + : undefined; } From 26f1979b009a5d450fd381a053b731ee5812c27e Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 6 Dec 2024 11:37:27 +0200 Subject: [PATCH 07/11] Remove complexity --- packages/scenes/src/core/SceneScopesBridge.ts | 17 +++++++---------- .../variables/adhoc/AdHocFilterRenderer.tsx | 10 ++++------ .../variables/adhoc/AdHocFiltersVariable.tsx | 19 ------------------- .../src/variables/groupby/GroupByVariable.tsx | 18 +----------------- 4 files changed, 12 insertions(+), 52 deletions(-) diff --git a/packages/scenes/src/core/SceneScopesBridge.ts b/packages/scenes/src/core/SceneScopesBridge.ts index d727a262a..122fa5042 100644 --- a/packages/scenes/src/core/SceneScopesBridge.ts +++ b/packages/scenes/src/core/SceneScopesBridge.ts @@ -1,16 +1,15 @@ import { isEqual } from 'lodash'; +import { useEffect } from 'react'; import { BehaviorSubject, filter, map, Observable, pairwise, Unsubscribable } from 'rxjs'; import { Scope } from '@grafana/data'; import { SceneObjectBase } from './SceneObjectBase'; -import { SceneComponentProps, SceneObjectState, SceneObjectUrlValues, SceneObjectWithUrlSync } from './types'; +import { SceneComponentProps, SceneObjectUrlValues, SceneObjectWithUrlSync } from './types'; import { ScopesContextValue, useScopes } from './ScopesContext'; import { SceneObjectUrlSyncConfig } from '../services/SceneObjectUrlSyncConfig'; -export interface SceneScopesBridgeState extends SceneObjectState {} - -export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWithUrlSync { +export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWithUrlSync { static Component = SceneScopesBridgeRenderer; protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] }); @@ -21,10 +20,6 @@ export class SceneScopesBridge extends SceneObjectBase i private _pendingScopes: string[] | null = null; - public constructor(state: SceneScopesBridgeState) { - super(state); - } - public getUrlState(): SceneObjectUrlValues { return { scopes: this._pendingScopes ?? (this.context?.state.value ?? []).map((scope) => scope.metadata.name), @@ -113,7 +108,7 @@ export class SceneScopesBridge extends SceneObjectBase i this._contextSubject.next(newContext); if (shouldUpdate) { - setTimeout(() => this.forceRender()); + this.forceRender(); } } } @@ -130,7 +125,9 @@ export class SceneScopesBridge extends SceneObjectBase i function SceneScopesBridgeRenderer({ model }: SceneComponentProps) { const context = useScopes(); - model.updateContext(context); + useEffect(() => { + model.updateContext(context); + }, [context, model]); return null; } diff --git a/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx b/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx index 6832a44b3..be94f3daf 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFilterRenderer.tsx @@ -28,8 +28,6 @@ const filterNoOp = () => true; export function AdHocFilterRenderer({ filter, model }: Props) { const styles = useStyles2(getStyles); - const { _isScopesLoading } = model.useState(); - const [keys, setKeys] = useState([]); const [values, setValues] = useState([]); const [isKeysLoading, setIsKeysLoading] = useState(false); @@ -141,8 +139,8 @@ export function AdHocFilterRenderer({ filter, model }: Props) { // there's a bug in react-select where the menu doesn't recalculate its position when the options are loaded asynchronously // see https://github.com/grafana/grafana/issues/63558 // instead, we explicitly control the menu visibility and prevent showing it until the options have fully loaded - isOpen={isValuesOpen && !isValuesLoading && !_isScopesLoading} - isLoading={isValuesLoading || _isScopesLoading} + isOpen={isValuesOpen && !isValuesLoading} + isLoading={isValuesLoading} openMenuOnFocus={true} onOpenMenu={async () => { setIsValuesLoading(true); @@ -189,8 +187,8 @@ export function AdHocFilterRenderer({ filter, model }: Props) { // there's a bug in react-select where the menu doesn't recalculate its position when the options are loaded asynchronously // see https://github.com/grafana/grafana/issues/63558 // instead, we explicitly control the menu visibility and prevent showing it until the options have fully loaded - isOpen={isKeysOpen && !isKeysLoading && !_isScopesLoading} - isLoading={isKeysLoading || _isScopesLoading} + isOpen={isKeysOpen && !isKeysLoading} + isLoading={isKeysLoading} onOpenMenu={async () => { setIsKeysOpen(true); setIsKeysLoading(true); diff --git a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx index b3684d802..00da5991e 100644 --- a/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx +++ b/packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx @@ -104,11 +104,6 @@ export interface AdHocFiltersVariableState extends SceneVariableState { * @internal state of the new filter being added */ _wip?: AdHocFilterWithLabels; - - /** - * @internal flag for keeping track of scopes loading state - */ - _isScopesLoading?: boolean; } export type AdHocVariableExpressionBuilderFn = (filters: AdHocFilterWithLabels[]) => string; @@ -200,12 +195,6 @@ export class AdHocFiltersVariable private _activationHandler = () => { this._scopesBridge = sceneGraph.getScopesBridge(this); - - if (this._scopesBridge) { - this._subs.add( - this._scopesBridge.subscribeToIsLoading((isLoading) => this.setState({ _isScopesLoading: isLoading })) - ); - } }; public setState(update: Partial): void { @@ -302,10 +291,6 @@ export class AdHocFiltersVariable * Get possible keys given current filters. Do not call from plugins directly */ public async _getKeys(currentKey: string | null): Promise>> { - if (this._scopesBridge?.getIsLoading()) { - return []; - } - const override = await this.state.getTagKeysProvider?.(this, currentKey); if (override && override.replace) { @@ -353,10 +338,6 @@ export class AdHocFiltersVariable * Get possible key values for a specific key given current filters. Do not call from plugins directly */ public async _getValuesFor(filter: AdHocFilterWithLabels): Promise>> { - if (this._scopesBridge?.getIsLoading()) { - return []; - } - const override = await this.state.getTagValuesProvider?.(this, filter); if (override && override.replace) { diff --git a/packages/scenes/src/variables/groupby/GroupByVariable.tsx b/packages/scenes/src/variables/groupby/GroupByVariable.tsx index 76e93afed..1a9be8e19 100644 --- a/packages/scenes/src/variables/groupby/GroupByVariable.tsx +++ b/packages/scenes/src/variables/groupby/GroupByVariable.tsx @@ -51,11 +51,6 @@ export interface GroupByVariableState extends MultiValueVariableState { * Return replace: false if you want to combine the results with the default lookup */ getTagKeysProvider?: getTagKeysProvider; - - /** - * @internal flag for keeping track of scopes loading state - */ - _isScopesLoading?: boolean; } export type getTagKeysProvider = ( @@ -157,12 +152,6 @@ export class GroupByVariable extends MultiValueVariable { allActiveGroupByVariables.add(this); - if (this._scopesBridge) { - this._subs.add( - this._scopesBridge.subscribeToIsLoading((isLoading) => this.setState({ _isScopesLoading: isLoading })) - ); - } - return () => allActiveGroupByVariables.delete(this); }); } @@ -171,10 +160,6 @@ export class GroupByVariable extends MultiValueVariable { * Get possible keys given current filters. Do not call from plugins directly */ public _getKeys = async (ds: DataSourceApi) => { - if (this._scopesBridge?.getIsLoading()) { - return []; - } - // TODO: provide current dimensions? const override = await this.state.getTagKeysProvider?.(this, null); @@ -235,7 +220,6 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps>>(() => { @@ -303,7 +287,7 @@ export function GroupByVariableRenderer({ model }: SceneComponentProps { From c3a4b47784b4d0219136335d7bc0f87307b25163 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Mon, 9 Dec 2024 16:40:35 +0200 Subject: [PATCH 08/11] Update to latest `ScopesContext` --- packages/scenes/src/core/ScopesContext.ts | 53 ++++++++++++++++------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/scenes/src/core/ScopesContext.ts b/packages/scenes/src/core/ScopesContext.ts index d2bf02940..0c7faade6 100644 --- a/packages/scenes/src/core/ScopesContext.ts +++ b/packages/scenes/src/core/ScopesContext.ts @@ -4,24 +4,47 @@ import { Observable } from 'rxjs'; import { Scope } from '@grafana/data'; +export interface ScopesContextValueState { + drawerOpened: boolean; + enabled: boolean; + loading: boolean; + readOnly: boolean; + value: Scope[]; +} + export interface ScopesContextValue { - state: { - isEnabled: boolean; - isLoading: boolean; - isReadOnly: boolean; - value: Scope[]; - }; + /** + * Current state. + */ + state: ScopesContextValueState; + + /** + * Observable that emits the current state. + */ stateObservable: Observable; - changeScopes: (scopeNames: string[]) => void; - enterReadOnly: () => void; - exitReadOnly: () => void; - enable: () => void; - disable: () => void; + + /** + * Change the selected scopes. The service takes care about loading them and propagating the changes. + * @param scopeNames + */ + changeScopes(scopeNames: string[]): void; + + /** + * Set read-only mode. + * If `readOnly` is `true`, the selector will be set to read-only and the dashboards panel will be closed. + */ + setReadOnly(readOnly: boolean): void; + + /** + * Enable or disable the usage of scopes. + * This will hide the selector and the dashboards panel, and it will stop propagating the scopes to the query object. + */ + setEnabled(enabled: boolean): void; } export const ScopesContext = createContext(undefined); -export function useScopes() { +export function useScopes(): ScopesContextValue | undefined { const context = useContext(ScopesContext); useObservable(context?.stateObservable ?? new Observable(), context?.state); @@ -31,10 +54,8 @@ export function useScopes() { state: context.state, stateObservable: context.stateObservable, changeScopes: context.changeScopes, - enterReadOnly: context.enterReadOnly, - exitReadOnly: context.exitReadOnly, - enable: context.enable, - disable: context.disable, + setReadOnly: context.setReadOnly, + setEnabled: context.setEnabled, } : undefined; } From 691e0595f5eb98d415bc33014aafecde127475f5 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Mon, 9 Dec 2024 17:10:40 +0200 Subject: [PATCH 09/11] Refactors --- .../src/components/SceneApp/SceneAppPage.tsx | 8 +-- packages/scenes/src/core/SceneScopesBridge.ts | 37 +++++------ packages/scenes/src/core/ScopesContext.ts | 61 ------------------- packages/scenes/src/index.ts | 1 - .../scenes/src/querying/SceneQueryRunner.ts | 2 +- 5 files changed, 20 insertions(+), 89 deletions(-) delete mode 100644 packages/scenes/src/core/ScopesContext.ts diff --git a/packages/scenes/src/components/SceneApp/SceneAppPage.tsx b/packages/scenes/src/components/SceneApp/SceneAppPage.tsx index 442ee04ae..9dd6df251 100644 --- a/packages/scenes/src/components/SceneApp/SceneAppPage.tsx +++ b/packages/scenes/src/components/SceneApp/SceneAppPage.tsx @@ -27,17 +27,17 @@ export class SceneAppPage extends SceneObjectBase implements } private _activationHandler = () => { - this._scopesBridge = sceneGraph.getScopesBridge(this); - if (this.state.useScopes) { + this._scopesBridge = sceneGraph.getScopesBridge(this); + if (!this._scopesBridge) { throw new Error('Use of scopes is enabled but no scopes bridge found'); } - this._scopesBridge.enable(); + this._scopesBridge.setEnabled(true); return () => { - this._scopesBridge?.disable(); + this._scopesBridge?.setEnabled(false); }; } }; diff --git a/packages/scenes/src/core/SceneScopesBridge.ts b/packages/scenes/src/core/SceneScopesBridge.ts index 122fa5042..718898d31 100644 --- a/packages/scenes/src/core/SceneScopesBridge.ts +++ b/packages/scenes/src/core/SceneScopesBridge.ts @@ -3,10 +3,11 @@ import { useEffect } from 'react'; import { BehaviorSubject, filter, map, Observable, pairwise, Unsubscribable } from 'rxjs'; import { Scope } from '@grafana/data'; +// @ts-expect-error: TODO: Fix this once new runtime package is released +import { ScopesContextValue, useScopes } from '@grafana/runtime'; import { SceneObjectBase } from './SceneObjectBase'; import { SceneComponentProps, SceneObjectUrlValues, SceneObjectWithUrlSync } from './types'; -import { ScopesContextValue, useScopes } from './ScopesContext'; import { SceneObjectUrlSyncConfig } from '../services/SceneObjectUrlSyncConfig'; export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWithUrlSync { @@ -22,7 +23,7 @@ export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWit public getUrlState(): SceneObjectUrlValues { return { - scopes: this._pendingScopes ?? (this.context?.state.value ?? []).map((scope) => scope.metadata.name), + scopes: this._pendingScopes ?? (this.context?.state.value ?? []).map((scope: Scope) => scope.metadata.name), }; } @@ -45,7 +46,7 @@ export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWit public subscribeToValue(cb: (newScopes: Scope[], prevScopes: Scope[]) => void): Unsubscribable { return this.contextObservable .pipe( - filter((context) => !!context && !context.state.isLoading), + filter((context) => !!context && !context.state.loading), pairwise(), map( ([prevContext, newContext]) => @@ -58,40 +59,32 @@ export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWit }); } - public getIsLoading(): boolean { - return this.context?.state.isLoading ?? false; + public isLoading(): boolean { + return this.context?.state.loading ?? false; } - public subscribeToIsLoading(cb: (isLoading: boolean) => void): Unsubscribable { + public subscribeToLoading(cb: (loading: boolean) => void): Unsubscribable { return this.contextObservable .pipe( filter((context) => !!context), pairwise(), map( ([prevContext, newContext]) => - [prevContext?.state.isLoading ?? false, newContext?.state.isLoading ?? false] as [boolean, boolean] + [prevContext?.state.loading ?? false, newContext?.state.loading ?? false] as [boolean, boolean] ), - filter(([prevIsLoading, newIsLoading]) => prevIsLoading !== newIsLoading) + filter(([prevLoading, newLoading]) => prevLoading !== newLoading) ) - .subscribe(([_prevIsLoading, newIsLoading]) => { - cb(newIsLoading); + .subscribe(([_prevLoading, newLoading]) => { + cb(newLoading); }); } - public enable() { - this.context?.enable(); + public setEnabled(enabled: boolean) { + this.context?.setEnabled(enabled); } - public disable() { - this.context?.disable(); - } - - public enterReadOnly() { - this.context?.enterReadOnly(); - } - - public exitReadOnly() { - this.context?.exitReadOnly(); + public setReadOnly(readOnly: boolean) { + this.context?.setReadOnly(readOnly); } public updateContext(newContext: ScopesContextValue | undefined) { diff --git a/packages/scenes/src/core/ScopesContext.ts b/packages/scenes/src/core/ScopesContext.ts deleted file mode 100644 index 0c7faade6..000000000 --- a/packages/scenes/src/core/ScopesContext.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createContext, useContext } from 'react'; -import { useObservable } from 'react-use'; -import { Observable } from 'rxjs'; - -import { Scope } from '@grafana/data'; - -export interface ScopesContextValueState { - drawerOpened: boolean; - enabled: boolean; - loading: boolean; - readOnly: boolean; - value: Scope[]; -} - -export interface ScopesContextValue { - /** - * Current state. - */ - state: ScopesContextValueState; - - /** - * Observable that emits the current state. - */ - stateObservable: Observable; - - /** - * Change the selected scopes. The service takes care about loading them and propagating the changes. - * @param scopeNames - */ - changeScopes(scopeNames: string[]): void; - - /** - * Set read-only mode. - * If `readOnly` is `true`, the selector will be set to read-only and the dashboards panel will be closed. - */ - setReadOnly(readOnly: boolean): void; - - /** - * Enable or disable the usage of scopes. - * This will hide the selector and the dashboards panel, and it will stop propagating the scopes to the query object. - */ - setEnabled(enabled: boolean): void; -} - -export const ScopesContext = createContext(undefined); - -export function useScopes(): ScopesContextValue | undefined { - const context = useContext(ScopesContext); - - useObservable(context?.stateObservable ?? new Observable(), context?.state); - - return context - ? { - state: context.state, - stateObservable: context.stateObservable, - changeScopes: context.changeScopes, - setReadOnly: context.setReadOnly, - setEnabled: context.setEnabled, - } - : undefined; -} diff --git a/packages/scenes/src/index.ts b/packages/scenes/src/index.ts index ec7f85b6f..c489145f1 100644 --- a/packages/scenes/src/index.ts +++ b/packages/scenes/src/index.ts @@ -128,7 +128,6 @@ export { VizConfigBuilder } from './core/PanelBuilders/VizConfigBuilder'; export { VizConfigBuilders } from './core/PanelBuilders/VizConfigBuilders'; export { type VizConfig } from './core/PanelBuilders/types'; export { SceneScopesBridge } from './core/SceneScopesBridge'; -export { ScopesContext, type ScopesContextValue, useScopes } from './core/ScopesContext'; export const sceneUtils = { getUrlWithAppState, diff --git a/packages/scenes/src/querying/SceneQueryRunner.ts b/packages/scenes/src/querying/SceneQueryRunner.ts index 71e3853d0..8fa762a3c 100644 --- a/packages/scenes/src/querying/SceneQueryRunner.ts +++ b/packages/scenes/src/querying/SceneQueryRunner.ts @@ -464,7 +464,7 @@ export class SceneQueryRunner extends SceneObjectBase implemen } // Skip executing queries if scopes are in loading state - if (scopesBridge?.getIsLoading()) { + if (scopesBridge?.isLoading()) { writeSceneLog('SceneQueryRunner', 'Scopes are in loading state, skipping query execution'); this.setState({ data: { ...(this.state.data ?? emptyPanelData), state: LoadingState.Loading } }); return; From 492764b88af96a191ea7d46ab4a7a715faf212f8 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Mon, 9 Dec 2024 17:14:58 +0200 Subject: [PATCH 10/11] Fix tests --- .../src/querying/__snapshots__/SceneQueryRunner.test.ts.snap | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap b/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap index 43711e4aa..202d9a0be 100644 --- a/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap +++ b/packages/scenes/src/querying/__snapshots__/SceneQueryRunner.test.ts.snap @@ -22,6 +22,7 @@ exports[`SceneQueryRunner when running query should build DataQueryRequest objec "to": "now", }, "requestId": "SQR100", + "scopes": undefined, "startTime": 1689063488000, "targets": [ { @@ -57,6 +58,7 @@ exports[`SceneQueryRunner when running query should build DataQueryRequest objec "to": "now", }, "requestId": "SQR178", + "scopes": undefined, "startTime": 1689063488000, "targets": [ { From 6412637d4a1f3cc40e343d14e47a6af5a50f1646 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Mon, 9 Dec 2024 17:18:27 +0200 Subject: [PATCH 11/11] Fix typecheck --- .../src/components/SceneApp/SceneAppPage.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/scenes/src/components/SceneApp/SceneAppPage.tsx b/packages/scenes/src/components/SceneApp/SceneAppPage.tsx index 9dd6df251..71aef0047 100644 --- a/packages/scenes/src/components/SceneApp/SceneAppPage.tsx +++ b/packages/scenes/src/components/SceneApp/SceneAppPage.tsx @@ -27,19 +27,21 @@ export class SceneAppPage extends SceneObjectBase implements } private _activationHandler = () => { - if (this.state.useScopes) { - this._scopesBridge = sceneGraph.getScopesBridge(this); - - if (!this._scopesBridge) { - throw new Error('Use of scopes is enabled but no scopes bridge found'); - } + if (!this.state.useScopes) { + return; + } - this._scopesBridge.setEnabled(true); + this._scopesBridge = sceneGraph.getScopesBridge(this); - return () => { - this._scopesBridge?.setEnabled(false); - }; + if (!this._scopesBridge) { + throw new Error('Use of scopes is enabled but no scopes bridge found'); } + + this._scopesBridge.setEnabled(true); + + return () => { + this._scopesBridge?.setEnabled(false); + }; }; public initializeScene(scene: EmbeddedScene) {