From 80e80221b9e4f02fb09957eb0a5c01f9dc01c83c Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 15 Nov 2022 00:49:39 -0800 Subject: [PATCH] Scenes: Grid layout (#56737) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * 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 Co-authored-by: Ivan Ortega --- .betterer.results | 9 +- .../scenes/components/NestedScene.test.tsx | 2 +- .../features/scenes/components/Scene.test.tsx | 2 +- .../scenes/components/SceneDragHandle.tsx | 18 + .../features/scenes/components/VizPanel.tsx | 9 +- .../{ => layout}/SceneFlexLayout.tsx | 4 +- .../layout/SceneGridLayout.test.tsx | 238 +++++++++ .../components/layout/SceneGridLayout.tsx | 493 ++++++++++++++++++ .../scenes/core/SceneComponentWrapper.tsx | 8 +- .../features/scenes/core/SceneObjectBase.tsx | 25 +- public/app/features/scenes/core/types.ts | 16 +- public/app/features/scenes/scenes/demo.tsx | 11 +- public/app/features/scenes/scenes/grid.tsx | 76 +++ .../scenes/scenes/gridMultiTimeRange.tsx | 109 ++++ .../features/scenes/scenes/gridMultiple.tsx | 120 +++++ .../scenes/scenes/gridWithMultipleData.tsx | 149 ++++++ .../features/scenes/scenes/gridWithRow.tsx | 97 ++++ .../features/scenes/scenes/gridWithRows.tsx | 102 ++++ public/app/features/scenes/scenes/index.tsx | 18 +- public/app/features/scenes/scenes/nested.tsx | 5 +- .../features/scenes/scenes/sceneWithRows.tsx | 4 +- .../features/scenes/scenes/variablesDemo.tsx | 2 +- 22 files changed, 1493 insertions(+), 24 deletions(-) create mode 100644 public/app/features/scenes/components/SceneDragHandle.tsx rename public/app/features/scenes/components/{ => layout}/SceneFlexLayout.tsx (94%) create mode 100644 public/app/features/scenes/components/layout/SceneGridLayout.test.tsx create mode 100644 public/app/features/scenes/components/layout/SceneGridLayout.tsx create mode 100644 public/app/features/scenes/scenes/grid.tsx create mode 100644 public/app/features/scenes/scenes/gridMultiTimeRange.tsx create mode 100644 public/app/features/scenes/scenes/gridMultiple.tsx create mode 100644 public/app/features/scenes/scenes/gridWithMultipleData.tsx create mode 100644 public/app/features/scenes/scenes/gridWithRow.tsx create mode 100644 public/app/features/scenes/scenes/gridWithRows.tsx 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';