Scenes: Grid layout (#56737)

* WIP: First approach to scene grid layout

* Flex layout

* Grid layout rows

* Allow passing custom props to scene object renderers

* Allow nesting grid layouts

* Re-layout nested grid's enclosing grids

* Update public/app/features/scenes/components/layout/SceneGridLayout.tsx

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

* Review comments

* Got rid of flex & grid child layout objects

* WIP: Recreating rows behaviour (almost working)

* Major progress on rows

* remove nested grid example (not supported)

* Remove removal damn

* Trying to use children directly

* Ts fixes

* chore: Fix TS

* Fix issue when row bboxes when not updated on layout change

* Now the tricky part

* working

* Removing some code

* needs more work

* Getting some thing working

* Getting some thing working

* fix toggle row

* Starting to work

* Fix

* Yay it's working

* Updates

* Updates

* Added some sorting of children

* Updated comment

* Simplify sorting

* removed commented code

* Updated

* Pushed a fix so we can move a panel out from a row and into the parent grid

* simplify move logic

* Minor simplification

* Removed some unnesary code

* fixed comment

* Removed unnessary condition in findGridSceneParent

* remove unnessary if

* Simplify toGridCell

* removed duplicate if

* removed unused code

* Adds grid demo with different data scenarios

* Make it green

* Demo grid with multiple time ranges

* Move child atomically

* Add tests

* Cleanup

* Fix unused import

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
pull/58746/head
Dominik Prokop 3 years ago committed by GitHub
parent 16aa4376ac
commit 80e80221b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .betterer.results
  2. 2
      public/app/features/scenes/components/NestedScene.test.tsx
  3. 2
      public/app/features/scenes/components/Scene.test.tsx
  4. 18
      public/app/features/scenes/components/SceneDragHandle.tsx
  5. 9
      public/app/features/scenes/components/VizPanel.tsx
  6. 4
      public/app/features/scenes/components/layout/SceneFlexLayout.tsx
  7. 238
      public/app/features/scenes/components/layout/SceneGridLayout.test.tsx
  8. 493
      public/app/features/scenes/components/layout/SceneGridLayout.tsx
  9. 8
      public/app/features/scenes/core/SceneComponentWrapper.tsx
  10. 25
      public/app/features/scenes/core/SceneObjectBase.tsx
  11. 16
      public/app/features/scenes/core/types.ts
  12. 11
      public/app/features/scenes/scenes/demo.tsx
  13. 76
      public/app/features/scenes/scenes/grid.tsx
  14. 109
      public/app/features/scenes/scenes/gridMultiTimeRange.tsx
  15. 120
      public/app/features/scenes/scenes/gridMultiple.tsx
  16. 149
      public/app/features/scenes/scenes/gridWithMultipleData.tsx
  17. 97
      public/app/features/scenes/scenes/gridWithRow.tsx
  18. 102
      public/app/features/scenes/scenes/gridWithRows.tsx
  19. 18
      public/app/features/scenes/scenes/index.tsx
  20. 5
      public/app/features/scenes/scenes/nested.tsx
  21. 4
      public/app/features/scenes/scenes/sceneWithRows.tsx
  22. 2
      public/app/features/scenes/scenes/variablesDemo.tsx

@ -4556,7 +4556,7 @@ exports[`better eslint`] = {
"public/app/features/sandbox/TestStuffPage.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/scenes/components/SceneFlexLayout.tsx:5381": [
"public/app/features/scenes/components/layout/SceneFlexLayout.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/scenes/core/SceneComponentWrapper.tsx:5381": [
@ -4570,9 +4570,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/features/scenes/core/SceneTimeRange.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],

@ -4,7 +4,7 @@ import React from 'react';
import { NestedScene } from './NestedScene';
import { Scene } from './Scene';
import { SceneCanvasText } from './SceneCanvasText';
import { SceneFlexLayout } from './SceneFlexLayout';
import { SceneFlexLayout } from './layout/SceneFlexLayout';
function setup() {
const scene = new Scene({

@ -1,5 +1,5 @@
import { Scene } from './Scene';
import { SceneFlexLayout } from './SceneFlexLayout';
import { SceneFlexLayout } from './layout/SceneFlexLayout';
describe('Scene', () => {
it('Simple scene', () => {

@ -0,0 +1,18 @@
import React from 'react';
import { Icon } from '@grafana/ui';
export function SceneDragHandle({ layoutKey, className }: { layoutKey: string; className?: string }) {
return (
<div
className={`${className} grid-drag-handle-${layoutKey}`}
style={{
width: '20px',
height: '20px',
cursor: 'move',
}}
>
<Icon name="draggabledots" />
</div>
);
}

@ -8,6 +8,8 @@ import { Field, PanelChrome, Input } from '@grafana/ui';
import { SceneObjectBase } from '../core/SceneObjectBase';
import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
import { SceneDragHandle } from './SceneDragHandle';
export interface VizPanelState extends SceneLayoutChildState {
title?: string;
pluginId: string;
@ -33,8 +35,11 @@ export class VizPanel extends SceneObjectBase<VizPanelState> {
}
function ScenePanelRenderer({ model }: SceneComponentProps<VizPanel>) {
const { title, pluginId, options, fieldConfig } = model.useState();
const { title, pluginId, options, fieldConfig, ...state } = model.useState();
const { data } = model.getData().useState();
const layout = model.getLayout();
const isDraggable = layout.state.isDraggable ? state.isDraggable : false;
const dragHandle = <SceneDragHandle layoutKey={layout.state.key!} />;
return (
<AutoSizer>
@ -44,7 +49,7 @@ function ScenePanelRenderer({ model }: SceneComponentProps<VizPanel>) {
}
return (
<PanelChrome title={title} width={width} height={height}>
<PanelChrome title={title} width={width} height={height} leftItems={isDraggable ? [dragHandle] : undefined}>
{(innerWidth, innerHeight) => (
<>
<PanelRenderer

@ -2,8 +2,8 @@ import React, { CSSProperties } from 'react';
import { Field, RadioButtonGroup } from '@grafana/ui';
import { SceneObjectBase } from '../core/SceneObjectBase';
import { SceneObjectSize, SceneLayoutState, SceneComponentProps, SceneLayoutChild } from '../core/types';
import { SceneObjectBase } from '../../core/SceneObjectBase';
import { SceneComponentProps, SceneLayoutChild, SceneLayoutState, SceneObjectSize } from '../../core/types';
export type FlexLayoutDirection = 'column' | 'row';

@ -0,0 +1,238 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { SceneObjectBase } from '../../core/SceneObjectBase';
import { SceneComponentProps, SceneLayoutChildState } from '../../core/types';
import { Scene } from '../Scene';
import { SceneGridLayout, SceneGridRow } from './SceneGridLayout';
// Mocking AutoSizer to allow testing of the SceneGridLayout component rendering
jest.mock(
'react-virtualized-auto-sizer',
() =>
({ children }: { children: (args: { width: number; height: number }) => React.ReactNode }) =>
children({ height: 600, width: 600 })
);
class TestObject extends SceneObjectBase<SceneLayoutChildState> {
public static Component = (m: SceneComponentProps<TestObject>) => {
return <div data-testid="test-object">TestObject</div>;
};
}
describe('SceneGridLayout', () => {
describe('rendering', () => {
it('should render all grid children', async () => {
const scene = new Scene({
title: 'Grid test',
layout: new SceneGridLayout({
children: [
new TestObject({ size: { x: 0, y: 0, width: 12, height: 5 } }),
new TestObject({ size: { x: 0, y: 5, width: 12, height: 5 } }),
],
}),
});
render(<scene.Component model={scene} />);
expect(screen.queryAllByTestId('test-object')).toHaveLength(2);
});
it('should not render children of a collapsed row', async () => {
const scene = new Scene({
title: 'Grid test',
layout: new SceneGridLayout({
children: [
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 12, height: 5 } }),
new TestObject({ key: 'b', size: { x: 0, y: 5, width: 12, height: 5 } }),
new SceneGridRow({
title: 'Row A',
key: 'Row A',
isCollapsed: true,
size: { y: 10 },
children: [new TestObject({ key: 'c', size: { x: 0, y: 11, width: 12, height: 5 } })],
}),
],
}),
});
render(<scene.Component model={scene} />);
expect(screen.queryAllByTestId('test-object')).toHaveLength(2);
});
it('should render children of an expanded row', async () => {
const scene = new Scene({
title: 'Grid test',
layout: new SceneGridLayout({
children: [
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 12, height: 5 } }),
new TestObject({ key: 'b', size: { x: 0, y: 5, width: 12, height: 5 } }),
new SceneGridRow({
title: 'Row A',
key: 'Row A',
isCollapsed: false,
size: { y: 10 },
children: [new TestObject({ key: 'c', size: { x: 0, y: 11, width: 12, height: 5 } })],
}),
],
}),
});
render(<scene.Component model={scene} />);
expect(screen.queryAllByTestId('test-object')).toHaveLength(3);
});
});
describe('when moving a panel', () => {
it('shoud update layout children placement and order ', () => {
const layout = new SceneGridLayout({
children: [
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 1, height: 1 } }),
new TestObject({ key: 'b', size: { x: 1, y: 0, width: 1, height: 1 } }),
new TestObject({ key: 'c', size: { x: 0, y: 1, width: 1, height: 1 } }),
],
});
layout.onDragStop(
[
{ i: 'b', x: 0, y: 0, w: 1, h: 1 },
{
i: 'a',
x: 0,
y: 1,
w: 1,
h: 1,
},
{
i: 'c',
x: 0,
y: 2,
w: 1,
h: 1,
},
],
// @ts-expect-error
{},
{ i: 'b', x: 0, y: 0, w: 1, h: 1 },
{},
{},
{}
);
expect(layout.state.children[0].state.key).toEqual('b');
expect(layout.state.children[0].state.size).toEqual({ x: 0, y: 0, width: 1, height: 1 });
expect(layout.state.children[1].state.key).toEqual('a');
expect(layout.state.children[1].state.size).toEqual({ x: 0, y: 1, width: 1, height: 1 });
expect(layout.state.children[2].state.key).toEqual('c');
expect(layout.state.children[2].state.size).toEqual({ x: 0, y: 2, width: 1, height: 1 });
});
});
describe('when using rows', () => {
it('should update objects relations when moving object out of a row', () => {
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 1, width: 1, height: 1 } });
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 1, width: 1, height: 1 } });
const sourceRow = new SceneGridRow({
title: 'Row A',
key: 'row-a',
children: [rowAChild1, rowAChild2],
size: { y: 0 },
});
const layout = new SceneGridLayout({
children: [sourceRow],
});
const updatedLayout = layout.moveChildTo(rowAChild1, layout);
expect(updatedLayout.length).toEqual(2);
// the source row should be cloned and with children updated
expect(updatedLayout[0].state.key).toEqual(sourceRow.state.key);
expect(updatedLayout[0]).not.toEqual(sourceRow);
expect((updatedLayout[0] as SceneGridRow).state.children.length).toEqual(1);
expect((updatedLayout[0] as SceneGridRow).state.children).not.toContain(rowAChild1);
// the moved child should be cloned in the root
expect(updatedLayout[1].state.key).toEqual(rowAChild1.state.key);
expect(updatedLayout[1]).not.toEqual(rowAChild1);
});
it('should update objects relations when moving objects between rows', () => {
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 0, width: 1, height: 1 } });
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 0, width: 1, height: 1 } });
const sourceRow = new SceneGridRow({
title: 'Row A',
key: 'row-a',
children: [rowAChild1, rowAChild2],
});
const targetRow = new SceneGridRow({
title: 'Row B',
key: 'row-b',
children: [],
});
const panelOutsideARow = new TestObject({ key: 'a', size: { x: 0, y: 0, width: 1, height: 1 } });
const layout = new SceneGridLayout({
children: [panelOutsideARow, sourceRow, targetRow],
});
const updatedLayout = layout.moveChildTo(rowAChild1, targetRow);
expect(updatedLayout[0]).toEqual(panelOutsideARow);
// the source row should be cloned and with children updated
expect(updatedLayout[1].state.key).toEqual(sourceRow.state.key);
expect(updatedLayout[1]).not.toEqual(sourceRow);
expect((updatedLayout[1] as SceneGridRow).state.children.length).toEqual(1);
// the target row should be cloned and with children updated
expect(updatedLayout[2].state.key).toEqual(targetRow.state.key);
expect(updatedLayout[2]).not.toEqual(targetRow);
expect((updatedLayout[2] as SceneGridRow).state.children.length).toEqual(1);
// the moved object should be cloned and added to the target row
const movedObject = (updatedLayout[2] as SceneGridRow).state.children[0];
expect(movedObject.state.key).toEqual('row-a-child1');
expect(movedObject).not.toEqual(rowAChild1);
});
it('should update position of objects when row is expanded', () => {
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 1, width: 1, height: 1 } });
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 1, width: 1, height: 1 } });
const rowA = new SceneGridRow({
title: 'Row A',
key: 'row-a',
children: [rowAChild1, rowAChild2],
size: { y: 0 },
isCollapsed: true,
});
const panelOutsideARow = new TestObject({ key: 'outsider', size: { x: 0, y: 1, width: 1, height: 1 } });
const rowBChild1 = new TestObject({ key: 'row-b-child1', size: { x: 0, y: 3, width: 1, height: 1 } });
const rowB = new SceneGridRow({
title: 'Row B',
key: 'row-b',
children: [rowBChild1],
size: { y: 2 },
isCollapsed: false,
});
const layout = new SceneGridLayout({
children: [rowA, panelOutsideARow, rowB],
});
layout.toggleRow(rowA);
expect(panelOutsideARow.state!.size!.y).toEqual(2);
expect(rowB.state!.size!.y).toEqual(3);
expect(rowBChild1.state!.size!.y).toEqual(4);
});
});
});

@ -0,0 +1,493 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import ReactGridLayout from 'react-grid-layout';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, useStyles2 } from '@grafana/ui';
import { DEFAULT_PANEL_SPAN, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
import { SceneObjectBase } from '../../core/SceneObjectBase';
import {
SceneComponentProps,
SceneLayoutChild,
SceneLayoutChildState,
SceneLayoutState,
SceneObject,
SceneObjectSize,
} from '../../core/types';
import { SceneDragHandle } from '../SceneDragHandle';
interface SceneGridLayoutState extends SceneLayoutState {}
export class SceneGridLayout extends SceneObjectBase<SceneGridLayoutState> {
public static Component = SceneGridLayoutRenderer;
private _skipOnLayoutChange = false;
public constructor(state: SceneGridLayoutState) {
super({
isDraggable: true,
...state,
children: sortChildrenByPosition(state.children),
});
}
public toggleRow(row: SceneGridRow) {
const isCollapsed = row.state.isCollapsed;
if (!isCollapsed) {
row.setState({ isCollapsed: true });
// To force re-render
this.setState({});
return;
}
const rowChildren = row.state.children;
if (rowChildren.length === 0) {
row.setState({ isCollapsed: false });
this.setState({});
return;
}
// Ok we are expanding row. We need to update row children y pos (incase they are incorrect) and push items below down
// Code copied from DashboardModel toggleRow()
const rowY = row.state.size?.y!;
const firstPanelYPos = rowChildren[0].state.size?.y ?? rowY;
const yDiff = firstPanelYPos - (rowY + 1);
// y max will represent the bottom y pos after all panels have been added
// needed to know home much panels below should be pushed down
let yMax = rowY;
for (const panel of rowChildren) {
// set the y gridPos if it wasn't already set
const newSize = { ...panel.state.size };
newSize.y = newSize.y ?? rowY;
// make sure y is adjusted (in case row moved while collapsed)
newSize.y -= yDiff;
if (newSize.y > panel.state.size?.y!) {
panel.setState({ size: newSize });
}
// update insert post and y max
yMax = Math.max(yMax, Number(newSize.y!) + Number(newSize.height!));
}
const pushDownAmount = yMax - rowY - 1;
// push panels below down
for (const child of this.state.children) {
if (child.state.size?.y! > rowY) {
this.pushChildDown(child, pushDownAmount);
}
if (child instanceof SceneGridRow && child !== row) {
for (const rowChild of child.state.children) {
if (rowChild.state.size?.y! > rowY) {
this.pushChildDown(rowChild, pushDownAmount);
}
}
}
}
row.setState({ isCollapsed: false });
// Trigger re-render
this.setState({});
}
public onLayoutChange = (layout: ReactGridLayout.Layout[]) => {
if (this._skipOnLayoutChange) {
// Layout has been updated by other RTL handler already
this._skipOnLayoutChange = false;
return;
}
for (const item of layout) {
const child = this.getSceneLayoutChild(item.i);
const nextSize = {
x: item.x,
y: item.y,
width: item.w,
height: item.h,
};
if (!isItemSizeEqual(child.state.size!, nextSize)) {
child.setState({
size: {
...child.state.size,
...nextSize,
},
});
}
}
this.setState({ children: sortChildrenByPosition(this.state.children) });
};
/**
* Will also scan row children and return child of the row
*/
public getSceneLayoutChild(key: string) {
for (const child of this.state.children) {
if (child.state.key === key) {
return child;
}
if (child instanceof SceneGridRow) {
for (const rowChild of child.state.children) {
if (rowChild.state.key === key) {
return rowChild;
}
}
}
}
throw new Error('Scene layout child not found for GridItem');
}
public onResizeStop: ReactGridLayout.ItemCallback = (_, o, n) => {
const child = this.getSceneLayoutChild(n.i);
child.setState({
size: {
...child.state.size,
width: n.w,
height: n.h,
},
});
};
private pushChildDown(child: SceneLayoutChild, amount: number) {
child.setState({
size: {
...child.state.size,
y: child.state.size?.y! + amount,
},
});
}
/**
* We assume the layout array is storted according to y pos, and walk upwards until we find a row.
* If it is collapsed there is no row to add it to. The default is then to return the SceneGridLayout itself
*/
private findGridItemSceneParent(layout: ReactGridLayout.Layout[], startAt: number): SceneGridRow | SceneGridLayout {
for (let i = startAt; i >= 0; i--) {
const gridItem = layout[i];
const sceneChild = this.getSceneLayoutChild(gridItem.i);
if (sceneChild instanceof SceneGridRow) {
// the closest row is collapsed return null
if (sceneChild.state.isCollapsed) {
return this;
}
return sceneChild;
}
}
return this;
}
/**
* This likely needs a slighltly different approach. Where we clone or deactivate or and re-activate the moved child
*/
public moveChildTo(child: SceneLayoutChild, target: SceneGridLayout | SceneGridRow) {
const currentParent = child.parent!;
let rootChildren = this.state.children;
const newChild = child.clone({ key: child.state.key });
// Remove from current parent row
if (currentParent instanceof SceneGridRow) {
const newRow = currentParent.clone({
children: currentParent.state.children.filter((c) => c.state.key !== child.state.key),
});
// new children with new row
rootChildren = rootChildren.map((c) => (c === currentParent ? newRow : c));
// if target is also a row
if (target instanceof SceneGridRow) {
const targetRow = target.clone({ children: [...target.state.children, newChild] });
rootChildren = rootChildren.map((c) => (c === target ? targetRow : c));
} else {
// target is the main grid
rootChildren = [...rootChildren, newChild];
}
} else {
// current parent is the main grid remove it from there
rootChildren = rootChildren.filter((c) => c.state.key !== child.state.key);
// Clone the target row and add the child
const targetRow = target.clone({ children: [...target.state.children, newChild] });
// Replace row with new row
rootChildren = rootChildren.map((c) => (c === target ? targetRow : c));
}
return rootChildren;
}
public onDragStop: ReactGridLayout.ItemCallback = (gridLayout, o, updatedItem) => {
const sceneChild = this.getSceneLayoutChild(updatedItem.i)!;
// Need to resort the grid layout based on new position (needed to to find the new parent)
gridLayout = sortGridLayout(gridLayout);
// Update children positions if they have changed
for (let i = 0; i < gridLayout.length; i++) {
const gridItem = gridLayout[i];
const child = this.getSceneLayoutChild(gridItem.i)!;
const childSize = child.state.size!;
if (childSize?.x !== gridItem.x || childSize?.y !== gridItem.y) {
child.setState({
size: {
...child.state.size,
x: gridItem.x,
y: gridItem.y,
},
});
}
}
// Update the parent if the child if it has moved to a row or back to the grid
const indexOfUpdatedItem = gridLayout.findIndex((item) => item.i === updatedItem.i);
const newParent = this.findGridItemSceneParent(gridLayout, indexOfUpdatedItem - 1);
let newChildren = this.state.children;
if (newParent !== sceneChild.parent) {
newChildren = this.moveChildTo(sceneChild, newParent);
}
this.setState({ children: sortChildrenByPosition(newChildren) });
this._skipOnLayoutChange = true;
};
private toGridCell(child: SceneLayoutChild): ReactGridLayout.Layout {
const size = child.state.size!;
let x = size.x ?? 0;
let y = size.y ?? 0;
const w = Number.isInteger(Number(size.width)) ? Number(size.width) : DEFAULT_PANEL_SPAN;
const h = Number.isInteger(Number(size.height)) ? Number(size.height) : DEFAULT_PANEL_SPAN;
let isDraggable = Boolean(child.state.isDraggable);
let isResizable = Boolean(child.state.isResizable);
if (child instanceof SceneGridRow) {
isDraggable = child.state.isCollapsed ? true : false;
isResizable = false;
}
return { i: child.state.key!, x, y, h, w, isResizable, isDraggable };
}
public buildGridLayout(width: number): ReactGridLayout.Layout[] {
let cells: ReactGridLayout.Layout[] = [];
for (const child of this.state.children) {
cells.push(this.toGridCell(child));
if (child instanceof SceneGridRow && !child.state.isCollapsed) {
for (const rowChild of child.state.children) {
cells.push(this.toGridCell(rowChild));
}
}
}
// Sort by position
cells = sortGridLayout(cells);
if (width < 768) {
// We should not persist the mobile layout
this._skipOnLayoutChange = true;
return cells.map((cell) => ({ ...cell, w: 24 }));
}
this._skipOnLayoutChange = false;
return cells;
}
}
function SceneGridLayoutRenderer({ model }: SceneComponentProps<SceneGridLayout>) {
const { children } = model.useState();
validateChildrenSize(children);
return (
<AutoSizer disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
const layout = model.buildGridLayout(width);
return (
/**
* The children is using a width of 100% so we need to guarantee that it is wrapped
* in an element that has the calculated size given by the AutoSizer. The AutoSizer
* has a width of 0 and will let its content overflow its div.
*/
<div style={{ width: `${width}px`, height: '100%' }}>
<ReactGridLayout
width={width}
/*
Disable draggable if mobile device, solving an issue with unintentionally
moving panels. https://github.com/grafana/grafana/issues/18497
theme.breakpoints.md = 769
*/
isDraggable={width > 768}
isResizable={false}
containerPadding={[0, 0]}
useCSSTransforms={false}
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
cols={GRID_COLUMN_COUNT}
rowHeight={GRID_CELL_HEIGHT}
draggableHandle={`.grid-drag-handle-${model.state.key}`}
// @ts-ignore: ignoring for now until we make the size type numbers-only
layout={layout}
onDragStop={model.onDragStop}
onResizeStop={model.onResizeStop}
onLayoutChange={model.onLayoutChange}
isBounded={false}
>
{layout.map((gridItem) => {
const sceneChild = model.getSceneLayoutChild(gridItem.i)!;
return (
<div key={sceneChild.state.key} style={{ display: 'flex' }}>
<sceneChild.Component model={sceneChild} key={sceneChild.state.key} />
</div>
);
})}
</ReactGridLayout>
</div>
);
}}
</AutoSizer>
);
}
interface SceneGridRowState extends SceneLayoutChildState {
title: string;
isCollapsible?: boolean;
isCollapsed?: boolean;
children: Array<SceneObject<SceneLayoutChildState>>;
}
export class SceneGridRow extends SceneObjectBase<SceneGridRowState> {
public static Component = SceneGridRowRenderer;
public constructor(state: SceneGridRowState) {
super({
isResizable: false,
isDraggable: true,
isCollapsible: true,
...state,
size: {
...state.size,
x: 0,
height: 1,
width: GRID_COLUMN_COUNT,
},
});
}
public onCollapseToggle = () => {
if (!this.state.isCollapsible) {
return;
}
const layout = this.parent;
if (!layout || !(layout instanceof SceneGridLayout)) {
throw new Error('SceneGridRow must be a child of SceneGridLayout');
}
layout.toggleRow(this);
};
}
function SceneGridRowRenderer({ model }: SceneComponentProps<SceneGridRow>) {
const styles = useStyles2(getSceneGridRowStyles);
const { isCollapsible, isCollapsed, isDraggable, title } = model.useState();
const layout = model.getLayout();
const dragHandle = <SceneDragHandle layoutKey={layout.state.key!} />;
return (
<div className={styles.row}>
<div className={cx(styles.rowHeader, isCollapsed && styles.rowHeaderCollapsed)}>
<div onClick={model.onCollapseToggle} className={styles.rowTitleWrapper}>
{isCollapsible && <Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />}
<span className={styles.rowTitle}>{title}</span>
</div>
{isDraggable && isCollapsed && <div>{dragHandle}</div>}
</div>
</div>
);
}
const getSceneGridRowStyles = (theme: GrafanaTheme2) => {
return {
row: css({
width: '100%',
height: '100%',
position: 'relative',
zIndex: 0,
display: 'flex',
flexDirection: 'column',
}),
rowHeader: css({
width: '100%',
height: '30px',
display: 'flex',
justifyContent: 'space-between',
marginBottom: '8px',
border: `1px solid transparent`,
}),
rowTitleWrapper: css({
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
}),
rowHeaderCollapsed: css({
marginBottom: '0px',
background: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.borderRadius(1),
}),
rowTitle: css({
fontSize: theme.typography.h6.fontSize,
fontWeight: theme.typography.h6.fontWeight,
}),
};
};
function validateChildrenSize(children: SceneLayoutChild[]) {
if (
children.find(
(c) =>
!c.state.size ||
c.state.size.height === undefined ||
c.state.size.width === undefined ||
c.state.size.x === undefined ||
c.state.size.y === undefined
)
) {
throw new Error('All children must have a size specified');
}
}
function isItemSizeEqual(a: SceneObjectSize, b: SceneObjectSize) {
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}
function sortChildrenByPosition(children: SceneLayoutChild[]) {
return [...children].sort((a, b) => {
return a.state.size?.y! - b.state.size?.y! || a.state.size?.x! - b.state.size?.x!;
});
}
function sortGridLayout(layout: ReactGridLayout.Layout[]) {
return [...layout].sort((a, b) => a.y - b.y || a.x! - b.x);
}

@ -4,9 +4,13 @@ import { SceneComponentEditingWrapper } from '../editor/SceneComponentEditWrappe
import { SceneComponentProps, SceneObject } from './types';
export function SceneComponentWrapper<T extends SceneObject>({ model, isEditing }: SceneComponentProps<T>) {
export function SceneComponentWrapper<T extends SceneObject>({
model,
isEditing,
...otherProps
}: SceneComponentProps<T>) {
const Component = (model as any).constructor['Component'] ?? EmptyRenderer;
const inner = <Component model={model} isEditing={isEditing} />;
const inner = <Component {...otherProps} model={model} isEditing={isEditing} />;
// Handle component activation state state
useEffect(() => {

@ -7,7 +7,15 @@ import { useForceUpdate } from '@grafana/ui';
import { SceneComponentWrapper } from './SceneComponentWrapper';
import { SceneObjectStateChangedEvent } from './events';
import { SceneDataState, SceneObject, SceneComponent, SceneEditor, SceneTimeRange, SceneObjectState } from './types';
import {
SceneDataState,
SceneObject,
SceneComponent,
SceneEditor,
SceneTimeRange,
SceneObjectState,
SceneLayoutState,
} from './types';
export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObjectState>
implements SceneObject<TState>
@ -208,6 +216,21 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
throw new Error('No data found in scene tree');
}
/**
* Will walk up the scene object graph to the closest $layout scene object
*/
public getLayout(): SceneObject<SceneLayoutState> {
if (this.constructor.name === 'SceneFlexLayout' || this.constructor.name === 'SceneGridLayout') {
return this as SceneObject<SceneLayoutState>;
}
if (this.parent) {
return this.parent.getLayout();
}
throw new Error('No layout found in scene tree');
}
/**
* Will walk up the scene object graph to the closest $editor scene object
*/

@ -13,9 +13,20 @@ export interface SceneObjectStatePlain {
$variables?: SceneVariables;
}
export interface SceneLayoutChildState extends SceneObjectStatePlain {
export interface SceneLayoutChildSize {
size?: SceneObjectSize;
}
export interface SceneLayoutChildInteractions {
isDraggable?: boolean;
isResizable?: boolean;
isCollapsible?: boolean;
isCollapsed?: boolean;
}
export interface SceneLayoutChildState
extends SceneObjectStatePlain,
SceneLayoutChildSize,
SceneLayoutChildInteractions {}
export type SceneObjectState = SceneObjectStatePlain | SceneLayoutState | SceneLayoutChildState;
@ -84,6 +95,9 @@ export interface SceneObject<TState extends SceneObjectState = SceneObjectState>
/** Get the closest node with time range */
getTimeRange(): SceneTimeRange;
/** Get the closest layout node */
getLayout(): SceneObject<SceneLayoutState>;
/** Returns a deep clone this object and all its children */
clone(state?: Partial<TState>): this;

@ -2,11 +2,11 @@ import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneCanvasText } from '../components/SceneCanvasText';
import { SceneFlexLayout } from '../components/SceneFlexLayout';
import { ScenePanelRepeater } from '../components/ScenePanelRepeater';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { SceneToolbarInput } from '../components/SceneToolbarButton';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
@ -18,12 +18,12 @@ export function getFlexLayoutTest(): Scene {
direction: 'row',
children: [
new VizPanel({
size: { minWidth: '70%' },
pluginId: 'timeseries',
title: 'Dynamic height and width',
size: { minWidth: '70%' },
}),
new SceneFlexLayout({
// size: { width: 450 },
direction: 'column',
children: [
new VizPanel({
@ -35,15 +35,15 @@ export function getFlexLayoutTest(): Scene {
title: 'Fill height',
}),
new SceneCanvasText({
size: { ySizing: 'content' },
text: 'Size to content',
fontSize: 20,
size: { ySizing: 'content' },
align: 'center',
}),
new VizPanel({
size: { height: 300 },
pluginId: 'timeseries',
title: 'Fixed height',
size: { height: 300 },
}),
],
}),
@ -92,6 +92,7 @@ export function getScenePanelRepeaterTest(): Scene {
direction: 'column',
children: [
new SceneFlexLayout({
direction: 'row',
size: { minHeight: 200 },
children: [
new VizPanel({

@ -0,0 +1,76 @@
import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneGridLayout } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
export function getGridLayoutTest(): Scene {
const scene = new Scene({
title: 'Grid layout test',
layout: new SceneGridLayout({
children: [
new VizPanel({
isResizable: true,
isDraggable: true,
pluginId: 'timeseries',
title: 'Draggable and resizable',
size: {
x: 0,
y: 0,
width: 12,
height: 10,
},
}),
new VizPanel({
pluginId: 'timeseries',
title: 'No drag and no resize',
isResizable: false,
isDraggable: false,
size: { x: 12, y: 0, width: 12, height: 10 },
}),
new SceneFlexLayout({
direction: 'column',
isDraggable: true,
isResizable: true,
size: { x: 6, y: 11, width: 12, height: 10 },
children: [
new VizPanel({
size: { ySizing: 'fill' },
pluginId: 'timeseries',
title: 'Child of flex layout',
}),
new VizPanel({
size: { ySizing: 'fill' },
pluginId: 'timeseries',
title: 'Child of flex layout',
}),
],
}),
],
}),
$editor: new SceneEditManager({}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
},
],
}),
actions: [new SceneTimePicker({})],
});
return scene;
}

@ -0,0 +1,109 @@
import { dateTime, getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
export function getGridWithMultipleTimeRanges(): Scene {
const globalTimeRange = new SceneTimeRange(getDefaultTimeRange());
const now = dateTime();
const row1TimeRange = new SceneTimeRange({
from: dateTime(now).subtract(1, 'year'),
to: now,
raw: { from: 'now-1y', to: 'now' },
});
const scene = new Scene({
title: 'Grid with rows and different queries and time ranges',
layout: new SceneGridLayout({
children: [
new SceneGridRow({
$timeRange: row1TimeRange,
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk_table',
},
],
}),
title: 'Row A - has its own query, last year time range',
key: 'Row A',
isCollapsed: true,
size: { y: 0 },
children: [
new VizPanel({
pluginId: 'timeseries',
title: 'Row A Child1',
key: 'Row A Child1',
isResizable: true,
isDraggable: true,
size: { x: 0, y: 1, width: 12, height: 5 },
}),
new VizPanel({
pluginId: 'timeseries',
title: 'Row A Child2',
key: 'Row A Child2',
isResizable: true,
isDraggable: true,
size: { x: 0, y: 5, width: 6, height: 5 },
}),
],
}),
new VizPanel({
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
seriesCount: 10,
},
],
}),
isResizable: true,
isDraggable: true,
pluginId: 'timeseries',
title: 'Outsider, has its own query',
key: 'Outsider-own-query',
size: {
x: 0,
y: 12,
width: 6,
height: 10,
},
}),
],
}),
$editor: new SceneEditManager({}),
$timeRange: globalTimeRange,
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
},
],
}),
actions: [new SceneTimePicker({})],
});
return scene;
}

@ -0,0 +1,120 @@
import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneGridLayout } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
export function getMultipleGridLayoutTest(): Scene {
const scene = new Scene({
title: 'Multiple grid layouts test',
layout: new SceneFlexLayout({
children: [
new SceneGridLayout({
children: [
new VizPanel({
size: {
x: 0,
y: 0,
width: 12,
height: 10,
},
isDraggable: true,
isResizable: true,
pluginId: 'timeseries',
title: 'Dragabble and resizable',
}),
new VizPanel({
isResizable: false,
isDraggable: true,
size: { x: 12, y: 0, width: 12, height: 10 },
pluginId: 'timeseries',
title: 'Draggable only',
}),
new SceneFlexLayout({
isResizable: true,
isDraggable: true,
size: { x: 6, y: 11, width: 12, height: 10 },
direction: 'column',
children: [
new VizPanel({
size: { ySizing: 'fill' },
pluginId: 'timeseries',
title: 'Fill height',
}),
new VizPanel({
size: { ySizing: 'fill' },
pluginId: 'timeseries',
title: 'Fill height',
}),
],
}),
],
}),
new SceneGridLayout({
children: [
new VizPanel({
size: {
x: 0,
y: 0,
width: 12,
height: 10,
},
isDraggable: true,
pluginId: 'timeseries',
title: 'Fill height',
}),
new VizPanel({
isResizable: false,
isDraggable: true,
size: { x: 12, y: 0, width: 12, height: 10 },
pluginId: 'timeseries',
title: 'Fill height',
}),
new SceneFlexLayout({
size: { x: 6, y: 11, width: 12, height: 10 },
direction: 'column',
children: [
new VizPanel({
size: { ySizing: 'fill' },
isDraggable: true,
pluginId: 'timeseries',
title: 'Fill height',
}),
new VizPanel({
isDraggable: true,
size: { ySizing: 'fill' },
pluginId: 'timeseries',
title: 'Fill height',
}),
],
}),
],
}),
],
}),
$editor: new SceneEditManager({}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
},
],
}),
actions: [new SceneTimePicker({})],
});
return scene;
}

@ -0,0 +1,149 @@
import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
export function getGridWithMultipleData(): Scene {
const scene = new Scene({
title: 'Grid with rows and different queries',
layout: new SceneGridLayout({
children: [
new SceneGridRow({
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk_table',
},
],
}),
title: 'Row A - has its own query',
key: 'Row A',
isCollapsed: true,
size: { y: 0 },
children: [
new VizPanel({
pluginId: 'timeseries',
title: 'Row A Child1',
key: 'Row A Child1',
isResizable: true,
isDraggable: true,
size: { x: 0, y: 1, width: 12, height: 5 },
}),
new VizPanel({
pluginId: 'timeseries',
title: 'Row A Child2',
key: 'Row A Child2',
isResizable: true,
isDraggable: true,
size: { x: 0, y: 5, width: 6, height: 5 },
}),
],
}),
new SceneGridRow({
title: 'Row B - uses global query',
key: 'Row B',
isCollapsed: true,
size: { y: 1 },
children: [
new VizPanel({
pluginId: 'timeseries',
title: 'Row B Child1',
key: 'Row B Child1',
isResizable: false,
isDraggable: true,
size: { x: 0, y: 2, width: 12, height: 5 },
}),
new VizPanel({
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
seriesCount: 10,
},
],
}),
pluginId: 'timeseries',
title: 'Row B Child2 with data',
key: 'Row B Child2',
isResizable: false,
isDraggable: true,
size: { x: 0, y: 7, width: 6, height: 5 },
}),
],
}),
new VizPanel({
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
seriesCount: 10,
},
],
}),
isResizable: true,
isDraggable: true,
pluginId: 'timeseries',
title: 'Outsider, has its own query',
key: 'Outsider-own-query',
size: {
x: 0,
y: 12,
width: 6,
height: 10,
},
}),
new VizPanel({
isResizable: true,
isDraggable: true,
pluginId: 'timeseries',
title: 'Outsider, uses global query',
key: 'Outsider-global-query',
size: {
x: 6,
y: 12,
width: 12,
height: 10,
},
}),
],
}),
$editor: new SceneEditManager({}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
},
],
}),
actions: [new SceneTimePicker({})],
});
return scene;
}

@ -0,0 +1,97 @@
import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
export function getGridWithRowLayoutTest(): Scene {
const scene = new Scene({
title: 'Grid with row layout test',
layout: new SceneGridLayout({
children: [
new SceneGridRow({
title: 'Row A',
key: 'Row A',
isCollapsed: true,
size: { y: 0 },
children: [
new VizPanel({
pluginId: 'timeseries',
title: 'Row A Child1',
key: 'Row A Child1',
isResizable: true,
isDraggable: true,
size: { x: 0, y: 1, width: 12, height: 5 },
}),
new VizPanel({
pluginId: 'timeseries',
title: 'Row A Child2',
key: 'Row A Child2',
isResizable: true,
isDraggable: true,
size: { x: 0, y: 5, width: 6, height: 5 },
}),
],
}),
new SceneGridRow({
title: 'Row B',
key: 'Row B',
isCollapsed: true,
size: { y: 1 },
children: [
new VizPanel({
pluginId: 'timeseries',
title: 'Row B Child1',
key: 'Row B Child1',
isResizable: false,
isDraggable: true,
size: { x: 0, y: 2, width: 12, height: 5 },
}),
new VizPanel({
pluginId: 'timeseries',
title: 'Row B Child2',
key: 'Row B Child2',
isResizable: false,
isDraggable: true,
size: { x: 0, y: 7, width: 6, height: 5 },
}),
],
}),
new VizPanel({
isResizable: true,
isDraggable: true,
pluginId: 'timeseries',
title: 'Outsider',
key: 'Outsider',
size: {
x: 2,
y: 12,
width: 12,
height: 10,
},
}),
],
}),
$editor: new SceneEditManager({}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
},
],
}),
actions: [new SceneTimePicker({})],
});
return scene;
}

@ -0,0 +1,102 @@
import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
export function getGridWithRowsTest(): Scene {
const panel = new VizPanel({
pluginId: 'timeseries',
title: 'Fill height',
});
const row1 = new SceneGridRow({
title: 'Collapsible/draggable row with flex layout',
size: { x: 0, y: 0, height: 10 },
children: [
new SceneFlexLayout({
direction: 'row',
children: [
new VizPanel({
pluginId: 'timeseries',
title: 'Fill height',
}),
new VizPanel({
pluginId: 'timeseries',
title: 'Fill height',
}),
new VizPanel({
pluginId: 'timeseries',
title: 'Fill height',
}),
],
}),
],
});
const cell1 = new VizPanel({
size: {
x: 0,
y: 10,
width: 12,
height: 20,
},
pluginId: 'timeseries',
title: 'Cell 1',
});
const cell2 = new VizPanel({
isResizable: false,
isDraggable: false,
size: { x: 12, y: 20, width: 12, height: 10 },
pluginId: 'timeseries',
title: 'No resize/no drag',
});
const row2 = new SceneGridRow({
size: { x: 12, y: 10, height: 10, width: 12 },
title: 'Row with a nested flex layout',
children: [
new SceneFlexLayout({
children: [
new SceneFlexLayout({
direction: 'column',
children: [panel, panel],
}),
new SceneFlexLayout({
direction: 'column',
children: [panel, panel],
}),
],
}),
],
});
const scene = new Scene({
title: 'Grid rows test',
layout: new SceneGridLayout({
children: [cell1, cell2, row1, row2],
}),
$editor: new SceneEditManager({}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
$data: new SceneQueryRunner({
queries: [
{
refId: 'A',
datasource: {
uid: 'gdev-testdata',
type: 'testdata',
},
scenarioId: 'random_walk',
},
],
}),
actions: [new SceneTimePicker({})],
});
return scene;
}

@ -1,12 +1,28 @@
import { Scene } from '../components/Scene';
import { getFlexLayoutTest, getScenePanelRepeaterTest } from './demo';
import { getGridLayoutTest } from './grid';
import { getGridWithMultipleTimeRanges } from './gridMultiTimeRange';
import { getMultipleGridLayoutTest } from './gridMultiple';
import { getGridWithMultipleData } from './gridWithMultipleData';
import { getGridWithRowLayoutTest } from './gridWithRow';
import { getNestedScene } from './nested';
import { getSceneWithRows } from './sceneWithRows';
import { getVariablesDemo } from './variablesDemo';
export function getScenes(): Scene[] {
return [getFlexLayoutTest(), getScenePanelRepeaterTest(), getNestedScene(), getSceneWithRows(), getVariablesDemo()];
return [
getFlexLayoutTest(),
getScenePanelRepeaterTest(),
getNestedScene(),
getSceneWithRows(),
getGridLayoutTest(),
getGridWithRowLayoutTest(),
getGridWithMultipleData(),
getGridWithMultipleTimeRanges(),
getMultipleGridLayoutTest(),
getVariablesDemo(),
];
}
const cache: Record<string, Scene> = {};

@ -2,9 +2,9 @@ import { getDefaultTimeRange } from '@grafana/data';
import { NestedScene } from '../components/NestedScene';
import { Scene } from '../components/Scene';
import { SceneFlexLayout } from '../components/SceneFlexLayout';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
@ -14,12 +14,12 @@ export function getNestedScene(): Scene {
layout: new SceneFlexLayout({
direction: 'column',
children: [
getInnerScene('Inner scene'),
new VizPanel({
key: '3',
pluginId: 'timeseries',
title: 'Panel 3',
}),
getInnerScene('Inner scene'),
],
}),
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
@ -45,6 +45,7 @@ export function getInnerScene(title: string) {
const scene = new NestedScene({
title: title,
canRemove: true,
canCollapse: true,
layout: new SceneFlexLayout({
direction: 'row',
children: [

@ -2,9 +2,9 @@ import { getDefaultTimeRange } from '@grafana/data';
import { NestedScene } from '../components/NestedScene';
import { Scene } from '../components/Scene';
import { SceneFlexLayout } from '../components/SceneFlexLayout';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { VizPanel } from '../components/VizPanel';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { SceneEditManager } from '../editor/SceneEditManager';
@ -19,6 +19,7 @@ export function getSceneWithRows(): Scene {
new NestedScene({
title: 'Overview',
canCollapse: true,
// size: { ySizing: 'content', xSizing: 'fill' },
layout: new SceneFlexLayout({
direction: 'row',
children: [
@ -35,6 +36,7 @@ export function getSceneWithRows(): Scene {
}),
new NestedScene({
title: 'More server details',
// size: { ySizing: 'content', xSizing: 'fill' },
canCollapse: true,
layout: new SceneFlexLayout({
direction: 'row',

@ -2,9 +2,9 @@ import { getDefaultTimeRange } from '@grafana/data';
import { Scene } from '../components/Scene';
import { SceneCanvasText } from '../components/SceneCanvasText';
import { SceneFlexLayout } from '../components/SceneFlexLayout';
import { SceneSubMenu } from '../components/SceneSubMenu';
import { SceneTimePicker } from '../components/SceneTimePicker';
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
import { SceneTimeRange } from '../core/SceneTimeRange';
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';

Loading…
Cancel
Save