diff --git a/.betterer.results b/.betterer.results
index 0e01318e04a..face19f7e0f 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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"],
diff --git a/public/app/features/scenes/components/NestedScene.test.tsx b/public/app/features/scenes/components/NestedScene.test.tsx
index 7a400e8447d..0b9d0b16355 100644
--- a/public/app/features/scenes/components/NestedScene.test.tsx
+++ b/public/app/features/scenes/components/NestedScene.test.tsx
@@ -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({
diff --git a/public/app/features/scenes/components/Scene.test.tsx b/public/app/features/scenes/components/Scene.test.tsx
index 6c616ff70f6..200210daeaf 100644
--- a/public/app/features/scenes/components/Scene.test.tsx
+++ b/public/app/features/scenes/components/Scene.test.tsx
@@ -1,5 +1,5 @@
import { Scene } from './Scene';
-import { SceneFlexLayout } from './SceneFlexLayout';
+import { SceneFlexLayout } from './layout/SceneFlexLayout';
describe('Scene', () => {
it('Simple scene', () => {
diff --git a/public/app/features/scenes/components/SceneDragHandle.tsx b/public/app/features/scenes/components/SceneDragHandle.tsx
new file mode 100644
index 00000000000..f30130b86fd
--- /dev/null
+++ b/public/app/features/scenes/components/SceneDragHandle.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+import { Icon } from '@grafana/ui';
+
+export function SceneDragHandle({ layoutKey, className }: { layoutKey: string; className?: string }) {
+ return (
+
+
+
+ );
+}
diff --git a/public/app/features/scenes/components/VizPanel.tsx b/public/app/features/scenes/components/VizPanel.tsx
index 764a5741c1f..375d4950c92 100644
--- a/public/app/features/scenes/components/VizPanel.tsx
+++ b/public/app/features/scenes/components/VizPanel.tsx
@@ -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 {
}
function ScenePanelRenderer({ model }: SceneComponentProps) {
- 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 = ;
return (
@@ -44,7 +49,7 @@ function ScenePanelRenderer({ model }: SceneComponentProps) {
}
return (
-
+
{(innerWidth, innerHeight) => (
<>
+ ({ children }: { children: (args: { width: number; height: number }) => React.ReactNode }) =>
+ children({ height: 600, width: 600 })
+);
+
+class TestObject extends SceneObjectBase {
+ public static Component = (m: SceneComponentProps) => {
+ return TestObject
;
+ };
+}
+
+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();
+
+ 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();
+
+ 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();
+
+ 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);
+ });
+ });
+});
diff --git a/public/app/features/scenes/components/layout/SceneGridLayout.tsx b/public/app/features/scenes/components/layout/SceneGridLayout.tsx
new file mode 100644
index 00000000000..645a110bcec
--- /dev/null
+++ b/public/app/features/scenes/components/layout/SceneGridLayout.tsx
@@ -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 {
+ 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) {
+ const { children } = model.useState();
+ validateChildrenSize(children);
+
+ return (
+
+ {({ 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.
+ */
+
+
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 (
+
+
+
+ );
+ })}
+
+
+ );
+ }}
+
+ );
+}
+
+interface SceneGridRowState extends SceneLayoutChildState {
+ title: string;
+ isCollapsible?: boolean;
+ isCollapsed?: boolean;
+ children: Array>;
+}
+
+export class SceneGridRow extends SceneObjectBase {
+ 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) {
+ const styles = useStyles2(getSceneGridRowStyles);
+ const { isCollapsible, isCollapsed, isDraggable, title } = model.useState();
+ const layout = model.getLayout();
+ const dragHandle = ;
+
+ return (
+
+
+
+ {isCollapsible && }
+ {title}
+
+ {isDraggable && isCollapsed &&
{dragHandle}
}
+
+
+ );
+}
+
+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);
+}
diff --git a/public/app/features/scenes/core/SceneComponentWrapper.tsx b/public/app/features/scenes/core/SceneComponentWrapper.tsx
index 74cfb834d6b..e4a6f71ece7 100644
--- a/public/app/features/scenes/core/SceneComponentWrapper.tsx
+++ b/public/app/features/scenes/core/SceneComponentWrapper.tsx
@@ -4,9 +4,13 @@ import { SceneComponentEditingWrapper } from '../editor/SceneComponentEditWrappe
import { SceneComponentProps, SceneObject } from './types';
-export function SceneComponentWrapper({ model, isEditing }: SceneComponentProps) {
+export function SceneComponentWrapper({
+ model,
+ isEditing,
+ ...otherProps
+}: SceneComponentProps) {
const Component = (model as any).constructor['Component'] ?? EmptyRenderer;
- const inner = ;
+ const inner = ;
// Handle component activation state state
useEffect(() => {
diff --git a/public/app/features/scenes/core/SceneObjectBase.tsx b/public/app/features/scenes/core/SceneObjectBase.tsx
index 8c4e029e16d..7c25982416e 100644
--- a/public/app/features/scenes/core/SceneObjectBase.tsx
+++ b/public/app/features/scenes/core/SceneObjectBase.tsx
@@ -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
implements SceneObject
@@ -208,6 +216,21 @@ export abstract class SceneObjectBase {
+ if (this.constructor.name === 'SceneFlexLayout' || this.constructor.name === 'SceneGridLayout') {
+ return this as SceneObject;
+ }
+
+ 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
*/
diff --git a/public/app/features/scenes/core/types.ts b/public/app/features/scenes/core/types.ts
index b531d6f95e6..86e678184b4 100644
--- a/public/app/features/scenes/core/types.ts
+++ b/public/app/features/scenes/core/types.ts
@@ -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
/** Get the closest node with time range */
getTimeRange(): SceneTimeRange;
+ /** Get the closest layout node */
+ getLayout(): SceneObject;
+
/** Returns a deep clone this object and all its children */
clone(state?: Partial): this;
diff --git a/public/app/features/scenes/scenes/demo.tsx b/public/app/features/scenes/scenes/demo.tsx
index 21066bf7007..f6acf02703d 100644
--- a/public/app/features/scenes/scenes/demo.tsx
+++ b/public/app/features/scenes/scenes/demo.tsx
@@ -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({
diff --git a/public/app/features/scenes/scenes/grid.tsx b/public/app/features/scenes/scenes/grid.tsx
new file mode 100644
index 00000000000..cefa6b61b9e
--- /dev/null
+++ b/public/app/features/scenes/scenes/grid.tsx
@@ -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;
+}
diff --git a/public/app/features/scenes/scenes/gridMultiTimeRange.tsx b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx
new file mode 100644
index 00000000000..f58854a99ce
--- /dev/null
+++ b/public/app/features/scenes/scenes/gridMultiTimeRange.tsx
@@ -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;
+}
diff --git a/public/app/features/scenes/scenes/gridMultiple.tsx b/public/app/features/scenes/scenes/gridMultiple.tsx
new file mode 100644
index 00000000000..bb4044c9e76
--- /dev/null
+++ b/public/app/features/scenes/scenes/gridMultiple.tsx
@@ -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;
+}
diff --git a/public/app/features/scenes/scenes/gridWithMultipleData.tsx b/public/app/features/scenes/scenes/gridWithMultipleData.tsx
new file mode 100644
index 00000000000..d492f81b057
--- /dev/null
+++ b/public/app/features/scenes/scenes/gridWithMultipleData.tsx
@@ -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;
+}
diff --git a/public/app/features/scenes/scenes/gridWithRow.tsx b/public/app/features/scenes/scenes/gridWithRow.tsx
new file mode 100644
index 00000000000..cbda038b18c
--- /dev/null
+++ b/public/app/features/scenes/scenes/gridWithRow.tsx
@@ -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;
+}
diff --git a/public/app/features/scenes/scenes/gridWithRows.tsx b/public/app/features/scenes/scenes/gridWithRows.tsx
new file mode 100644
index 00000000000..609373dd256
--- /dev/null
+++ b/public/app/features/scenes/scenes/gridWithRows.tsx
@@ -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;
+}
diff --git a/public/app/features/scenes/scenes/index.tsx b/public/app/features/scenes/scenes/index.tsx
index 0a1db6b2830..d8900a858b2 100644
--- a/public/app/features/scenes/scenes/index.tsx
+++ b/public/app/features/scenes/scenes/index.tsx
@@ -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 = {};
diff --git a/public/app/features/scenes/scenes/nested.tsx b/public/app/features/scenes/scenes/nested.tsx
index 77684fc4684..4b99f9bb497 100644
--- a/public/app/features/scenes/scenes/nested.tsx
+++ b/public/app/features/scenes/scenes/nested.tsx
@@ -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: [
diff --git a/public/app/features/scenes/scenes/sceneWithRows.tsx b/public/app/features/scenes/scenes/sceneWithRows.tsx
index a45282778d0..efcc172f053 100644
--- a/public/app/features/scenes/scenes/sceneWithRows.tsx
+++ b/public/app/features/scenes/scenes/sceneWithRows.tsx
@@ -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',
diff --git a/public/app/features/scenes/scenes/variablesDemo.tsx b/public/app/features/scenes/scenes/variablesDemo.tsx
index 45ae1149d8c..c6b4f1da6f3 100644
--- a/public/app/features/scenes/scenes/variablesDemo.tsx
+++ b/public/app/features/scenes/scenes/variablesDemo.tsx
@@ -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';