Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scopes: Add ScopesBridge object #990

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions packages/scenes/src/components/SceneApp/SceneApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,33 @@ import { renderSceneComponentWithRouteProps } from './utils';
* Responsible for top level pages routing
*/
export class SceneApp extends SceneObjectBase<SceneAppState> implements DataRequestEnricher {
protected _renderBeforeActivation = true;

public enrichDataRequest() {
return {
app: this.state.name || 'app',
};
}

public static Component = ({ model }: SceneComponentProps<SceneApp>) => {
const { pages } = model.useState();
const { pages, scopesBridge } = model.useState();

return (
<SceneAppContext.Provider value={model}>
<Switch>
{pages.map((page) => (
<Route
key={page.state.url}
exact={false}
path={page.state.url}
render={(props) => renderSceneComponentWithRouteProps(page, props)}
></Route>
))}
</Switch>
</SceneAppContext.Provider>
<>
{scopesBridge && <scopesBridge.Component model={scopesBridge} />}
<SceneAppContext.Provider value={model}>
<Switch>
{pages.map((page) => (
<Route
key={page.state.url}
exact={false}
path={page.state.url}
render={(props) => renderSceneComponentWithRouteProps(page, props)}
></Route>
))}
</Switch>
</SceneAppContext.Provider>
</>
);
};
}
Expand Down
23 changes: 23 additions & 0 deletions packages/scenes/src/components/SceneApp/SceneAppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,11 +18,32 @@ export class SceneAppPage extends SceneObjectBase<SceneAppPageState> implements
public static Component = SceneAppPageRenderer;
private _sceneCache = new Map<string, EmbeddedScene>();
private _drilldownCache = new Map<string, SceneAppPageLike>();
private _scopesBridge: SceneScopesBridge | undefined;

public constructor(state: SceneAppPageState) {
super(state);

this.addActivationHandler(this._activationHandler);
}

private _activationHandler = () => {
if (!this.state.useScopes) {
return;
}

this._scopesBridge = sceneGraph.getScopesBridge(this);

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) {
this.setState({ initializedScene: scene });
}
Expand Down
5 changes: 5 additions & 0 deletions packages/scenes/src/components/SceneApp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends { [K in keyof Params]?: string } = {}> {
params: Params;
Expand All @@ -15,6 +16,7 @@ export interface SceneAppState extends SceneObjectState {
pages: SceneAppPageLike[];
name?: string;
urlSyncOptions?: SceneUrlSyncOptions;
scopesBridge?: SceneScopesBridge;
}

export interface SceneAppRoute {
Expand Down Expand Up @@ -69,6 +71,9 @@ export interface SceneAppPageState extends SceneObjectState {
getFallbackPage?: () => SceneAppPageLike;

layout?: PageLayoutType;

// Whether to use scopes for this page
useScopes?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this as a page level prop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking that some pages might not want scopes to be shown for various reasons 🤔

}

export interface SceneAppPageLike extends SceneObject<SceneAppPageState>, DataRequestEnricher {
Expand Down
126 changes: 126 additions & 0 deletions packages/scenes/src/core/SceneScopesBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { isEqual } from 'lodash';
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 { SceneObjectUrlSyncConfig } from '../services/SceneObjectUrlSyncConfig';

export class SceneScopesBridge extends SceneObjectBase implements SceneObjectWithUrlSync {
static Component = SceneScopesBridgeRenderer;

protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['scopes'] });

protected _renderBeforeActivation = true;

private _contextSubject = new BehaviorSubject<ScopesContextValue | undefined>(undefined);

private _pendingScopes: string[] | null = null;

public getUrlState(): SceneObjectUrlValues {
return {
scopes: this._pendingScopes ?? (this.context?.state.value ?? []).map((scope: 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 ?? [];
}

public subscribeToValue(cb: (newScopes: Scope[], prevScopes: Scope[]) => void): Unsubscribable {
return this.contextObservable
.pipe(
filter((context) => !!context && !context.state.loading),
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 isLoading(): boolean {
return this.context?.state.loading ?? false;
}

public subscribeToLoading(cb: (loading: boolean) => void): Unsubscribable {
return this.contextObservable
.pipe(
filter((context) => !!context),
pairwise(),
map(
([prevContext, newContext]) =>
[prevContext?.state.loading ?? false, newContext?.state.loading ?? false] as [boolean, boolean]
),
filter(([prevLoading, newLoading]) => prevLoading !== newLoading)
)
.subscribe(([_prevLoading, newLoading]) => {
cb(newLoading);
});
}

public setEnabled(enabled: boolean) {
this.context?.setEnabled(enabled);
}

public setReadOnly(readOnly: boolean) {
this.context?.setReadOnly(readOnly);
}

public updateContext(newContext: ScopesContextValue | undefined) {
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) {
this.forceRender();
}
}
}

private get context(): ScopesContextValue | undefined {
return this._contextSubject.getValue();
}

private get contextObservable(): Observable<ScopesContextValue | undefined> {
return this._contextSubject.asObservable();
}
}

function SceneScopesBridgeRenderer({ model }: SceneComponentProps<SceneScopesBridge>) {
const context = useScopes();

useEffect(() => {
model.updateContext(context);
}, [context, model]);

return null;
}
2 changes: 2 additions & 0 deletions packages/scenes/src/core/sceneGraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
findDescendents,
getQueryController,
getUrlSyncManager,
getScopesBridge,
} from './sceneGraph';

export const sceneGraph = {
Expand All @@ -34,4 +35,5 @@ export const sceneGraph = {
findDescendents,
getQueryController,
getUrlSyncManager,
getScopesBridge,
};
8 changes: 8 additions & 0 deletions packages/scenes/src/core/sceneGraph/sceneGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions packages/scenes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ 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 const sceneUtils = {
getUrlWithAppState,
Expand Down
Loading
Loading