diff --git a/docusaurus/docs/scene-app.md b/docusaurus/docs/scene-app.md
index 91a5bae96..3ca0d20d1 100644
--- a/docusaurus/docs/scene-app.md
+++ b/docusaurus/docs/scene-app.md
@@ -24,6 +24,10 @@ Define a new Scenes app using the `SceneApp` object :
function getSceneApp() {
return new SceneApp({
pages: [],
+ urlSyncOptions: {
+ updateUrlOnInit: true,
+ createBrowserHistorySteps: true
+ }
});
}
```
diff --git a/docusaurus/docs/url-sync.md b/docusaurus/docs/url-sync.md
new file mode 100644
index 000000000..008026933
--- /dev/null
+++ b/docusaurus/docs/url-sync.md
@@ -0,0 +1,92 @@
+---
+id: url-sync
+title: Url sync
+---
+
+Scenes comes with a URL sync system that enables two way syncing of scene object state to URL.
+
+## UrlSyncContextProvider
+
+To enable URL sync you have to wrap your root scene in a UrlSyncContextProvider
+
+```tsx
+
+```
+
+## SceneApp
+
+For scene apps that use SceenApp the url sync initialized for you, but you can still set url sync options on the SceneApp state.
+
+```tsx
+function getSceneApp() {
+ return new SceneApp({
+ pages: [],
+ urlSyncOptions: {
+ updateUrlOnInit: true,
+ createBrowserHistorySteps: true
+ }
+ });
+}
+```
+
+## SceneObjectUrlSyncHandler
+
+A scene objects that set's its `_urlSync` property will have the option to sync part of it's state to / from the URL.
+
+This property has this interface type:
+
+```tsx
+export interface SceneObjectUrlSyncHandler {
+ getKeys(): string[];
+ getUrlState(): SceneObjectUrlValues;
+ updateFromUrl(values: SceneObjectUrlValues): void;
+ shouldCreateHistoryStep?(values: SceneObjectUrlValues): boolean;
+}
+```
+
+The current behavior of updateFromUrl is a bit strange in that it will only pass on URL values that are different compared to what is returned by
+getUrlState.
+
+## Browser history
+
+If createBrowserHistorySteps is enabled then for state changes where shouldCreateHistoryStep return true new browser history states will be returned.
+
+## SceneObjectUrlSyncConfig
+
+This class implements the SceneObjectUrlSyncHandler interface and is a utility class to make it a bit easier for scene objects to implement
+url sync behavior.
+
+
+Example:
+
+```tsx
+export class SomeObject extends SceneObjectBase {
+ protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['from', 'to'] });
+
+ public getUrlState() {
+ return { from: this.state.from, to: this.state.to };
+ }
+
+ public updateFromUrl(values: SceneObjectUrlValues) {
+ const update: Partial = {};
+
+ if (typeof values.from === 'string') {
+ update.from = values.from;
+ }
+
+ if (typeof values.to === 'string') {
+ update.to = values.to;
+ }
+
+ this.setState(update);
+ }
+
+ onUserUpdate(from: string, to: string) {
+ // For state actions that should add browser history wrap them in this callback
+ this._urlSync.performBrowserHistoryAction(() => {
+ this.setState({from, to})
+ })
+ }
+}
+```
+
diff --git a/docusaurus/website/sidebars.js b/docusaurus/website/sidebars.js
index 23399f365..c72c8fa00 100644
--- a/docusaurus/website/sidebars.js
+++ b/docusaurus/website/sidebars.js
@@ -58,6 +58,7 @@ const sidebars = {
'advanced-behaviors',
'advanced-custom-datasource',
'advanced-time-range-comparison',
+ 'url-sync',
],
},
{
@@ -65,13 +66,10 @@ const sidebars = {
label: '@grafana/scenes-ml',
collapsible: true,
collapsed: false,
- items: [
- 'getting-started',
- 'baselines-and-forecasts',
- 'outlier-detection',
- 'changepoint-detection',
- ].map(id => `scenes-ml/${id}`),
- }
+ items: ['getting-started', 'baselines-and-forecasts', 'outlier-detection', 'changepoint-detection'].map(
+ (id) => `scenes-ml/${id}`
+ ),
+ },
],
};
module.exports = sidebars;
diff --git a/packages/scenes-app/src/demos/urlSyncTest.tsx b/packages/scenes-app/src/demos/urlSyncTest.tsx
index 29d97a3d0..3140bd1c4 100644
--- a/packages/scenes-app/src/demos/urlSyncTest.tsx
+++ b/packages/scenes-app/src/demos/urlSyncTest.tsx
@@ -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({
@@ -102,7 +102,7 @@ class DynamicSubScene extends SceneObjectBase {
private addScene() {
const scene = buildNewSubScene();
- getUrlSyncManager().handleNewObject(scene);
+ this.publishEvent(new NewSceneObjectAddedEvent(scene), true);
this.setState({ scene });
}
diff --git a/packages/scenes-app/src/pages/DemoListPage.tsx b/packages/scenes-app/src/pages/DemoListPage.tsx
index a601444b5..9fc19ea1c 100644
--- a/packages/scenes-app/src/pages/DemoListPage.tsx
+++ b/packages/scenes-app/src/pages/DemoListPage.tsx
@@ -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',
diff --git a/packages/scenes-react/src/contexts/SceneContextObject.tsx b/packages/scenes-react/src/contexts/SceneContextObject.tsx
index 9ace0c2dc..efad0a575 100644
--- a/packages/scenes-react/src/contexts/SceneContextObject.tsx
+++ b/packages/scenes-react/src/contexts/SceneContextObject.tsx
@@ -4,7 +4,7 @@ import {
SceneObjectState,
SceneVariable,
SceneVariableSet,
- getUrlSyncManager,
+ NewSceneObjectAddedEvent,
} from '@grafana/scenes';
import { writeSceneLog } from '../utils';
@@ -23,7 +23,7 @@ export class SceneContextObject extends SceneObjectBase
}
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}`);
@@ -54,7 +54,7 @@ export class SceneContextObject extends SceneObjectBase
public addVariable(variable: SceneVariable) {
let set = this.state.$variables as SceneVariableSet;
- getUrlSyncManager().handleNewObject(variable);
+ this.publishEvent(new NewSceneObjectAddedEvent(variable), true);
if (set) {
set.setState({ variables: [...set.state.variables, variable] });
@@ -72,6 +72,8 @@ export class SceneContextObject extends SceneObjectBase
}
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}`);
diff --git a/packages/scenes-react/src/contexts/SceneContextProvider.tsx b/packages/scenes-react/src/contexts/SceneContextProvider.tsx
index 3193f934d..9858bccff 100644
--- a/packages/scenes-react/src/contexts/SceneContextProvider.tsx
+++ b/packages/scenes-react/src/contexts/SceneContextProvider.tsx
@@ -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';
@@ -51,7 +45,6 @@ export function SceneContextProvider({ children, timeRange, withQueryController
const childContext = new SceneContextObject(state);
if (parentContext) {
- getUrlSyncManager().handleNewObject(childContext);
parentContext.addChildContext(childContext);
}
@@ -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 {innerProvider};
+ return (
+
+ {innerProvider}
+
+ );
}
diff --git a/packages/scenes/src/components/SceneApp/SceneApp.tsx b/packages/scenes/src/components/SceneApp/SceneApp.tsx
index e5491358d..fc4583fc4 100644
--- a/packages/scenes/src/components/SceneApp/SceneApp.tsx
+++ b/packages/scenes/src/components/SceneApp/SceneApp.tsx
@@ -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';
@@ -20,20 +20,24 @@ export class SceneApp extends SceneObjectBase implements DataRequ
const { pages } = model.useState();
return (
-
- {pages.map((page) => (
- renderSceneComponentWithRouteProps(page, props)}
- >
- ))}
-
+
+
+ {pages.map((page) => (
+ renderSceneComponentWithRouteProps(page, props)}
+ >
+ ))}
+
+
);
};
}
+export const SceneAppContext = createContext(null);
+
const sceneAppCache = new Map