Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
10 changes: 9 additions & 1 deletion packages/scenes-app/src/components/Routes/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import * as React from 'react';
import { Route, Routes } from 'react-router-dom';
import { Route, Routes, useLocation } from 'react-router-dom';
import { ROUTES } from '../../constants';
import { DemoListPage } from '../../pages/DemoListPage';
import GrafanaMonitoringApp from '../../monitoring-app/GrafanaMonitoringApp';
import { ReactDemoPage } from '../../react-demo/Home';
import { HomePage } from '../../home-demo/HomeApp';
import { SceneObjectBase } from '@grafana/scenes';

export function AppRoutes() {
const location = useLocation();
const params = new URLSearchParams(location.search);

if (params.get('renderBeforeActivation') === 'true') {
SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true;
}

return (
<Routes>
<Route path={`${ROUTES.Demos}/*`} Component={DemoListPage} />
Expand Down
141 changes: 141 additions & 0 deletions packages/scenes-app/src/demos/flickeringDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import React from 'react';
import {
EmbeddedScene,
PanelBuilders,
SceneAppPage,
SceneAppPageState,
SceneCSSGridLayout,
SceneFlexItem,
SceneFlexLayout,
SceneObjectBase,
SceneRefreshPicker,
SceneTimePicker,
SceneTimeRange,
SceneVariableSet,
VariableValueSelectors,
} from '@grafana/scenes';
import { getQueryRunnerWithRandomWalkQuery } from './utils';
import { Button, InlineSwitch } from '@grafana/ui';
import { useLocation } from 'react-router-dom';
import { locationUtil } from '@grafana/data';

export function getFlickeringDemo(defaults: SceneAppPageState) {
const layout = new SceneCSSGridLayout({
autoRows: 'auto',
children: [],
isLazy: true,
});

layout.setState({
children: [
PanelBuilders.timeseries()
.setTitle('Panel with explore button')
.setData(getQueryRunnerWithRandomWalkQuery())
.setHeaderActions(<SwitchPanelButton layout={layout} />)
.build(),
PanelBuilders.timeseries().setTitle('Panel below').setData(getQueryRunnerWithRandomWalkQuery()).build(),
],
});

return new SceneAppPage({
...defaults,
$timeRange: new SceneTimeRange(),
controls: [new RenderBeforeActivationSwitch({}), new SceneTimePicker({}), new SceneRefreshPicker({})],
tabs: [
new SceneAppPage({
title: 'Overview',
url: `${defaults.url}/overview`,
routePath: 'overview',
getScene: () => {
return new EmbeddedScene({
controls: [new VariableValueSelectors({})],
$variables: new SceneVariableSet({
variables: [],
}),
body: layout,
});
},
}),
new SceneAppPage({
title: 'Details',
url: `${defaults.url}/details`,
routePath: 'details',
getScene: () => {
return new EmbeddedScene({
body: new SceneFlexLayout({
direction: 'column',
children: [
new SceneFlexItem({
body: PanelBuilders.timeseries()
.setTitle('Panel with explore button')
.setData(getQueryRunnerWithRandomWalkQuery())
.build(),
}),
],
}),
});
},
}),
],
});
}

interface VizPanelExploreButtonProps {
layout: SceneCSSGridLayout;
}

let counter = 0;

function getNewPanel(layout: SceneCSSGridLayout) {
counter++;

if (counter % 2 === 0) {
return PanelBuilders.timeseries()
.setTitle(`Another panel ${counter}`)
.setData(getQueryRunnerWithRandomWalkQuery())
.setHeaderActions(<SwitchPanelButton layout={layout} />)
.build();
}

return PanelBuilders.gauge()
.setTitle(`Another panel ${counter}`)
.setData(getQueryRunnerWithRandomWalkQuery())
.setHeaderActions(<SwitchPanelButton layout={layout} />)
.build();
}

function SwitchPanelButton({ layout }: VizPanelExploreButtonProps) {
const onClick = () => {
layout.setState({ children: [getNewPanel(layout), ...layout.state.children.slice(1)] });
};

return (
<Button size="sm" variant="secondary" onClick={onClick}>
Switch
</Button>
);
}

export class RenderBeforeActivationSwitch extends SceneObjectBase {
public static Component = RenderBeforeActivationSwitchRenderer;
}

function RenderBeforeActivationSwitchRenderer() {
const location = useLocation();

const onToggle = (evt: React.ChangeEvent<HTMLInputElement>) => {
const url = locationUtil.getUrlForPartial(location, {
renderBeforeActivation: evt.currentTarget.checked ? 'true' : null,
});
window.location.href = locationUtil.assureBaseUrl(url);
};

return (
<InlineSwitch
label="Render before activation"
showLabel={true}
value={SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT}
onChange={onToggle}
/>
);
}
7 changes: 7 additions & 0 deletions packages/scenes-app/src/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { getSceneGraphEventsDemo } from './sceneGraphEvents';
import { getSeriesLimitTest } from './seriesLimit';
import { getScopesDemo } from './scopesDemo';
import { getVariableWithObjectValuesDemo } from './variableWithObjectValuesDemo';
import { getFlickeringDemo } from './flickeringDemo';

export interface DemoDescriptor {
title: string;
Expand Down Expand Up @@ -319,5 +320,11 @@ export function getDemos(): DemoDescriptor[] {
getPage: getVariableWithObjectValuesDemo,
getSourceCodeModule: () => import('!!raw-loader!../demos/variableWithObjectValuesDemo.tsx'),
},
{
title: 'Flickering demo',
description: 'Demo showing flickering panels',
getPage: getFlickeringDemo,
getSourceCodeModule: () => import('!!raw-loader!../demos/flickeringDemo.tsx'),
},
].sort((a, b) => a.title.localeCompare(b.title));
}
12 changes: 8 additions & 4 deletions packages/scenes-app/src/demos/scopesDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@ import {
SceneAppPageState,
SceneFlexItem,
SceneFlexLayout,
SceneObjectBase,
SceneVariableSet,
ScopesVariable,
VariableValueSelectors,
VizPanel,
} from '@grafana/scenes';
import { EmbeddedSceneWithContext } from '@grafana/scenes-react';
import { getEmbeddedSceneDefaults, getPromQueryInstant } from './utils';

SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT = true;

export function getScopesDemo(defaults: SceneAppPageState) {
return new SceneAppPage({
...defaults,
$variables: new SceneVariableSet({
variables: [new ScopesVariable({ enable: true }), new AdHocFiltersVariable({ layout: 'combobox' })],
}),
getScene: () => {
return new EmbeddedSceneWithContext({
...getEmbeddedSceneDefaults(),

$variables: new SceneVariableSet({
variables: [new ScopesVariable({ enable: true }), new AdHocFiltersVariable({ layout: 'combobox' })],
}),
key: 'Prometheus query that uses scopes',
controls: [new VariableValueSelectors({})],
body: new SceneFlexLayout({
direction: 'column',
children: [
Expand Down
3 changes: 2 additions & 1 deletion packages/scenes-react/src/components/RefreshPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useId } from 'react';
import { useSceneContext } from '../hooks/hooks';
import { SceneRefreshPicker, SceneRefreshPickerState } from '@grafana/scenes';
import { usePrevious } from 'react-use';
import { useAddToScene } from '../contexts/SceneContextObject';

export interface Props {
refresh?: string;
Expand All @@ -22,7 +23,7 @@ export function RefreshPicker(props: Props) {
});
}

useEffect(() => scene.addToScene(picker), [picker, scene]);
useAddToScene(picker, scene);

// Update options
useEffect(() => {
Expand Down
3 changes: 2 additions & 1 deletion packages/scenes-react/src/components/SceneFlexLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { useSceneContext } from '../hooks/hooks';
import { SceneFlexItem, type SceneFlexItemProps } from './SceneFlexItem';
import { SceneFlexLayoutContext } from './SceneFlexLayoutContext';
import { useAddToScene } from '../contexts/SceneContextObject';

export interface SceneFlexLayoutProps extends SceneFlexItemPlacement {
children: React.ReactNode;
Expand Down Expand Up @@ -43,7 +44,7 @@ export function SceneFlexLayout(props: SceneFlexLayoutProps) {
});
}

useEffect(() => scene.addToScene(layout), [layout, scene]);
useAddToScene(layout, scene);

// Keep layout placement props in sync (but do not touch children here).
useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/scenes-react/src/components/VizPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('VizPanel', () => {
it('Should render with titleItems', () => {
const scene = new SceneContextObject();
const viz = VizConfigBuilders.timeseries().build();
const titleItems = <div>Title Item</div>;
const titleItems = <div key="title-item">Title Item</div>;

const { rerender, unmount } = render(
<TestContextProvider value={scene}>
Expand Down
3 changes: 2 additions & 1 deletion packages/scenes-react/src/components/VizPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getPanelOptionsWithDefaults } from '@grafana/data';
import { PanelContext } from '@grafana/ui';
import { writeSceneLog } from '../utils';
import { useSceneContext } from '../hooks/hooks';
import { useAddToScene } from '../contexts/SceneContextObject';

export interface VizPanelProps {
title: string;
Expand Down Expand Up @@ -81,7 +82,7 @@ export function VizPanel(props: VizPanelProps) {
});
}

useEffect(() => scene.addToScene(panel), [panel, scene]);
useAddToScene(panel, scene);

// Update options
useEffect(() => {
Expand Down
34 changes: 34 additions & 0 deletions packages/scenes-react/src/contexts/SceneContextObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
NewSceneObjectAddedEvent,
} from '@grafana/scenes';
import { writeSceneLog } from '../utils';
import { useEffect } from 'react';

export interface SceneContextObjectState extends SceneObjectState {
childContexts?: SceneContextObject[];
Expand Down Expand Up @@ -87,3 +88,36 @@ export class SceneContextObject extends SceneObjectBase<SceneContextObjectState>
writeSceneLog('SceneContext', `Remvoing child context: ${ctx.constructor.name} key: ${ctx.state.key}`);
}
}

export function useAddToScene(obj: SceneObject, ctx: SceneContextObject) {
// Old behavior
if (!SceneObjectBase.RENDER_BEFORE_ACTIVATION_DEFAULT) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => ctx.addToScene(obj), [ctx, obj]);
return;
}

// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
const deactivate = obj.activate();

return () => {
writeSceneLog('SceneContext', `Removing from scene: ${obj.constructor.name} key: ${obj.state.key}`);
ctx.setState({ children: ctx.state.children.filter((x) => x !== obj) });

deactivate();
};
}, [ctx, obj]);

// Check if scene contains object instance
if (ctx.state.children.includes(obj)) {
return;
}

// This is technically a state change during render. We have to add it to the state tree right away in order to render the object on the first pass
// Should be ok as nothing subscribes to SceneContextObject state changes and the NewSceneObjectAddedEvent is syncing url state to obj state
ctx.publishEvent(new NewSceneObjectAddedEvent(obj), true);
ctx.setState({ children: [...ctx.state.children, obj] });

writeSceneLog('SceneContext', `Adding to scene: ${obj.constructor.name} key: ${obj.state.key}`);
}
3 changes: 2 additions & 1 deletion packages/scenes-react/src/hooks/useDataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useSceneContext } from './hooks';
import { useEffect, useId } from 'react';
import { isEqual } from 'lodash';
import { DataTransformerConfig } from '@grafana/schema';
import { useAddToScene } from '../contexts/SceneContextObject';

export interface UseDataTransformerOptions {
transformations: Array<DataTransformerConfig | CustomTransformerDefinition>;
Expand All @@ -28,7 +29,7 @@ export function useDataTransformer(options: UseDataTransformerOptions) {
});
}

useEffect(() => scene.addToScene(dataTransformer), [dataTransformer, scene]);
useAddToScene(dataTransformer, scene);

useEffect(() => {
if (!isEqual(dataTransformer.state.transformations, options.transformations)) {
Expand Down
5 changes: 3 additions & 2 deletions packages/scenes-react/src/hooks/useSceneObject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useId } from 'react';
import { useId } from 'react';
import { SceneObject, sceneGraph } from '@grafana/scenes';
import { useSceneContext } from './hooks';
import { CacheKey, SceneObjectConstructor, getSceneObjectCache } from '../caching/SceneObjectCache';
import { useAddToScene } from '../contexts/SceneContextObject';

export interface UseSceneObjectProps<T extends SceneObject> {
factory: (key: string) => T;
Expand Down Expand Up @@ -44,7 +45,7 @@ export function useSceneObject<T extends SceneObject>(options: UseSceneObjectPro
}
}

useEffect(() => scene.addToScene(obj), [obj, scene]);
useAddToScene(obj, scene);

return obj;
}
Loading
Loading