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

UrlSync: Variable changes adds browser history steps #882

Open
wants to merge 12 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
4 changes: 2 additions & 2 deletions packages/scenes-app/src/demos/urlSyncTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import {
SceneTimeRange,
SceneVariableSet,
VariableValueSelectors,
getUrlSyncManager,
} from '@grafana/scenes';
import { getQueryRunnerWithRandomWalkQuery } from './utils';
import { Button, Stack } from '@grafana/ui';
import { NewSceneObjectAddedEvent } from '@grafana/scenes/src/services/UrlSyncManager';

export function getUrlSyncTest(defaults: SceneAppPageState) {
return new SceneAppPage({
Expand Down Expand Up @@ -102,7 +102,7 @@ class DynamicSubScene extends SceneObjectBase<DynamicSubSceneState> {

private addScene() {
const scene = buildNewSubScene();
getUrlSyncManager().handleNewObject(scene);
this.publishEvent(new NewSceneObjectAddedEvent(scene), true);
this.setState({ scene });
}

Expand Down
4 changes: 4 additions & 0 deletions packages/scenes-app/src/pages/DemoListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ import { css } from '@emotion/css';
function getDemoSceneApp() {
return new SceneApp({
name: 'scenes-demos-app',
urlSyncOptions: {
updateUrlOnInit: true,
createBrowserHistorySteps: true,
},
pages: [
new SceneAppPage({
title: 'Demos',
Expand Down
8 changes: 5 additions & 3 deletions packages/scenes-react/src/contexts/SceneContextObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
SceneObjectState,
SceneVariable,
SceneVariableSet,
getUrlSyncManager,
NewSceneObjectAddedEvent,
} from '@grafana/scenes';
import { writeSceneLog } from '../utils';

Expand All @@ -23,7 +23,7 @@ export class SceneContextObject extends SceneObjectBase<SceneContextObjectState>
}

public addToScene(obj: SceneObject) {
getUrlSyncManager().handleNewObject(obj);
this.publishEvent(new NewSceneObjectAddedEvent(obj), true);

this.setState({ children: [...this.state.children, obj] });
writeSceneLog('SceneContext', `Adding to scene: ${obj.constructor.name} key: ${obj.state.key}`);
Expand Down Expand Up @@ -54,7 +54,7 @@ export class SceneContextObject extends SceneObjectBase<SceneContextObjectState>
public addVariable(variable: SceneVariable) {
let set = this.state.$variables as SceneVariableSet;

getUrlSyncManager().handleNewObject(variable);
this.publishEvent(new NewSceneObjectAddedEvent(set), true);

if (set) {
set.setState({ variables: [...set.state.variables, variable] });
Expand All @@ -72,6 +72,8 @@ export class SceneContextObject extends SceneObjectBase<SceneContextObjectState>
}

public addChildContext(ctx: SceneContextObject) {
this.publishEvent(new NewSceneObjectAddedEvent(ctx), true);

this.setState({ childContexts: [...(this.state.childContexts ?? []), ctx] });

writeSceneLog('SceneContext', `Adding child context: ${ctx.constructor.name} key: ${ctx.state.key}`);
Expand Down
15 changes: 6 additions & 9 deletions packages/scenes-react/src/contexts/SceneContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import {
SceneTimeRangeState,
SceneTimeRange,
behaviors,
UrlSyncContextProvider,
getUrlSyncManager,
} from '@grafana/scenes';
import { SceneTimeRangeState, SceneTimeRange, behaviors, UrlSyncContextProvider } from '@grafana/scenes';

import { SceneContextObject, SceneContextObjectState } from './SceneContextObject';

Expand Down Expand Up @@ -51,7 +45,6 @@ export function SceneContextProvider({ children, timeRange, withQueryController
const childContext = new SceneContextObject(state);

if (parentContext) {
getUrlSyncManager().handleNewObject(childContext);
parentContext.addChildContext(childContext);
}

Expand Down Expand Up @@ -79,5 +72,9 @@ export function SceneContextProvider({ children, timeRange, withQueryController
}

// For root context we wrap the provider in a UrlSyncWrapper that handles the hook that updates state on location changes
return <UrlSyncContextProvider scene={childContext}>{innerProvider}</UrlSyncContextProvider>;
return (
<UrlSyncContextProvider scene={childContext} updateUrlOnInit={true} createBrowserHistorySteps={true}>
{innerProvider}
</UrlSyncContextProvider>
);
}
26 changes: 15 additions & 11 deletions packages/scenes/src/components/SceneApp/SceneApp.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { createContext } from 'react';
import { Route, Switch } from 'react-router-dom';

import { DataRequestEnricher, SceneComponentProps } from '../../core/types';
Expand All @@ -20,20 +20,24 @@ export class SceneApp extends SceneObjectBase<SceneAppState> implements DataRequ
const { pages } = model.useState();

return (
<Switch>
{pages.map((page) => (
<Route
key={page.state.url}
exact={false}
path={page.state.url}
render={(props) => renderSceneComponentWithRouteProps(page, props)}
></Route>
))}
</Switch>
<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>
);
};
}

export const SceneAppContext = createContext<SceneApp | null>(null);

const sceneAppCache = new Map<object, SceneApp>();

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/scenes/src/components/SceneApp/SceneAppPageView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NavModelItem, UrlQueryMap } from '@grafana/data';
import { PluginPage } from '@grafana/runtime';
import React, { useEffect, useLayoutEffect } from 'react';
import React, { useContext, useEffect, useLayoutEffect } from 'react';

import { RouteComponentProps } from 'react-router-dom';
import { SceneObject } from '../../core/types';
Expand All @@ -9,6 +9,7 @@ import { SceneAppPage } from './SceneAppPage';
import { SceneAppDrilldownView, SceneAppPageLike } from './types';
import { getUrlWithAppState, renderSceneComponentWithRouteProps, useAppQueryParams } from './utils';
import { useUrlSync } from '../../services/useUrlSync';
import { SceneAppContext } from './SceneApp';

export interface Props {
page: SceneAppPageLike;
Expand All @@ -21,8 +22,9 @@ export function SceneAppPageView({ page, routeProps }: Props) {
const containerState = containerPage.useState();
const params = useAppQueryParams();
const scene = page.getScene(routeProps.match);
const appContext = useContext(SceneAppContext);
const isInitialized = containerState.initializedScene === scene;
const {layout} = page.state;
const { layout } = page.state;

useLayoutEffect(() => {
// Before rendering scene components, we are making sure the URL sync is enabled for.
Expand All @@ -36,7 +38,7 @@ export function SceneAppPageView({ page, routeProps }: Props) {
return () => containerPage.setState({ initializedScene: undefined });
}, [containerPage]);

const urlSyncInitialized = useUrlSync(containerPage);
const urlSyncInitialized = useUrlSync(containerPage, appContext?.state.urlSyncOptions);

if (!isInitialized && !urlSyncInitialized) {
return null;
Expand Down
5 changes: 3 additions & 2 deletions packages/scenes/src/components/SceneApp/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentType } from 'react';
import { DataRequestEnricher, SceneObject, SceneObjectState } from '../../core/types';
import { DataRequestEnricher, SceneObject, SceneObjectState, SceneUrlSyncOptions } from '../../core/types';
import { EmbeddedScene } from '../EmbeddedScene';
import { IconName, PageLayoutType } from '@grafana/data';

Expand All @@ -14,6 +14,7 @@ export interface SceneAppState extends SceneObjectState {
// Array of SceneAppPage objects that are considered app's top level pages
pages: SceneAppPageLike[];
name?: string;
urlSyncOptions?: SceneUrlSyncOptions;
}

export interface SceneAppRoute {
Expand Down Expand Up @@ -67,7 +68,7 @@ export interface SceneAppPageState extends SceneObjectState {
*/
getFallbackPage?: () => SceneAppPageLike;

layout?: PageLayoutType
layout?: PageLayoutType;
}

export interface SceneAppPageLike extends SceneObject<SceneAppPageState>, DataRequestEnricher {
Expand Down
4 changes: 4 additions & 0 deletions packages/scenes/src/core/SceneTimeRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,8 @@ export class SceneTimeRange extends SceneObjectBase<SceneTimeRangeState> impleme

this.setState(update);
}

public shouldCreateHistoryStep(values: SceneObjectUrlValues): boolean {
return true;
}
}
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 @@ -13,6 +13,7 @@ import {
interpolate,
getAncestor,
getQueryController,
getUrlSyncManager,
} from './sceneGraph';

export const sceneGraph = {
Expand All @@ -30,4 +31,5 @@ export const sceneGraph = {
findAllObjects,
getAncestor,
getQueryController,
getUrlSyncManager,
};
18 changes: 18 additions & 0 deletions packages/scenes/src/core/sceneGraph/sceneGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getClosest } from './utils';
import { SceneQueryControllerLike, isQueryController } from '../../behaviors/SceneQueryController';
import { VariableInterpolation } from '@grafana/runtime';
import { QueryVariable } from '../../variables/variants/query/QueryVariable';
import { UrlSyncManagerLike } from '../../services/UrlSyncManager';

/**
* Get the closest node with variables
Expand Down Expand Up @@ -269,3 +270,20 @@ export function getQueryController(sceneObject: SceneObject): SceneQueryControll

return undefined;
}

/**
* Returns the closest SceneObject that has a state property with the
* name urlSyncManager that is of type UrlSyncManager
*/
export function getUrlSyncManager(sceneObject: SceneObject): UrlSyncManagerLike | undefined {
let parent: SceneObject | undefined = sceneObject;

while (parent) {
if ('urlSyncManager' in parent.state) {
return parent.state.urlSyncManager as UrlSyncManagerLike;
}
parent = parent.parent;
}

return undefined;
}
22 changes: 19 additions & 3 deletions packages/scenes/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ export interface SceneTimeRangeState extends SceneObjectState {
/**
* When set, the time range will invalidate relative ranges after the specified interval has elapsed
*/
afterMs?: number
afterMs?: number;
/**
* When set, the time range will invalidate relative ranges after the specified percentage of the current interval has elapsed.
* If both invalidate values are set, the smaller value will be used for the given interval.
*/
percent?: number
}
percent?: number;
};
}

export interface SceneTimeRangeLike extends SceneObject<SceneTimeRangeState> {
Expand All @@ -179,12 +179,14 @@ export function isSceneObject(obj: any): obj is SceneObject {
export interface SceneObjectWithUrlSync extends SceneObject {
getUrlState(): SceneObjectUrlValues;
updateFromUrl(values: SceneObjectUrlValues): void;
shouldCreateHistoryStep?(values: SceneObjectUrlValues): boolean;
}

export interface SceneObjectUrlSyncHandler {
getKeys(): string[];
getUrlState(): SceneObjectUrlValues;
updateFromUrl(values: SceneObjectUrlValues): void;
shouldCreateHistoryStep?(values: SceneObjectUrlValues): boolean;
}

export interface DataRequestEnricher {
Expand Down Expand Up @@ -276,3 +278,17 @@ export interface SceneDataQuery extends DataQuery {
// Opt this query out of time window comparison
timeRangeCompare?: boolean;
}

export interface SceneUrlSyncOptions {
/**
* This will update the url to contain all scene url state
* when the scene is initialized. Important for browser history "back" actions.
*/
updateUrlOnInit?: boolean;
/**
* This is only supported by some objects if they implement
* shouldCreateHistoryStep where they can control what changes
* url changes should add a new browser history entry.
*/
createBrowserHistorySteps?: boolean;
}
2 changes: 1 addition & 1 deletion packages/scenes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export { AdHocFiltersVariable } from './variables/adhoc/AdHocFiltersVariable';
export { GroupByVariable } from './variables/groupby/GroupByVariable';
export { type MacroVariableConstructor } from './variables/macros/types';

export { type UrlSyncManagerLike, UrlSyncManager, getUrlSyncManager } from './services/UrlSyncManager';
export { type UrlSyncManagerLike, UrlSyncManager, NewSceneObjectAddedEvent } from './services/UrlSyncManager';
export { useUrlSync } from './services/useUrlSync';
export { UrlSyncContextProvider } from './services/UrlSyncContextProvider';
export { SceneObjectUrlSyncConfig } from './services/SceneObjectUrlSyncConfig';
Expand Down
4 changes: 4 additions & 0 deletions packages/scenes/src/services/SceneObjectUrlSyncConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ export class SceneObjectUrlSyncConfig implements SceneObjectUrlSyncHandler {
public updateFromUrl(values: SceneObjectUrlValues): void {
this._sceneObject.updateFromUrl(values);
}

public shouldCreateHistoryStep(values: SceneObjectUrlValues): boolean {
return this._sceneObject.shouldCreateHistoryStep?.(values) ?? false;
}
}
13 changes: 9 additions & 4 deletions packages/scenes/src/services/UrlSyncContextProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SceneObject } from '../core/types';
import { SceneObject, SceneUrlSyncOptions } from '../core/types';
import { useUrlSync } from './useUrlSync';

export interface UrlSyncContextProviderProps {
export interface UrlSyncContextProviderProps extends SceneUrlSyncOptions {
scene: SceneObject;
children: React.ReactNode;
}
Expand All @@ -10,8 +10,13 @@ export interface UrlSyncContextProviderProps {
* Right now this is actually not defining a context, but think it might in the future (with UrlSyncManager as the context value)
*/

export function UrlSyncContextProvider({ children, scene }: UrlSyncContextProviderProps) {
const isInitialized = useUrlSync(scene);
export function UrlSyncContextProvider({
children,
scene,
updateUrlOnInit,
createBrowserHistorySteps,
}: UrlSyncContextProviderProps) {
const isInitialized = useUrlSync(scene, { updateUrlOnInit, createBrowserHistorySteps });

if (!isInitialized) {
return null;
Expand Down
Loading
Loading