mirror of https://github.com/grafana/grafana
Scenes: Grid layout (#56737)
* WIP: First approach to scene grid layout * Flex layout * Grid layout rows * Allow passing custom props to scene object renderers * Allow nesting grid layouts * Re-layout nested grid's enclosing grids * Update public/app/features/scenes/components/layout/SceneGridLayout.tsx Co-authored-by: Torkel Ödegaard <torkel@grafana.com> * Review comments * Got rid of flex & grid child layout objects * WIP: Recreating rows behaviour (almost working) * Major progress on rows * remove nested grid example (not supported) * Remove removal damn * Trying to use children directly * Ts fixes * chore: Fix TS * Fix issue when row bboxes when not updated on layout change * Now the tricky part * working * Removing some code * needs more work * Getting some thing working * Getting some thing working * fix toggle row * Starting to work * Fix * Yay it's working * Updates * Updates * Added some sorting of children * Updated comment * Simplify sorting * removed commented code * Updated * Pushed a fix so we can move a panel out from a row and into the parent grid * simplify move logic * Minor simplification * Removed some unnesary code * fixed comment * Removed unnessary condition in findGridSceneParent * remove unnessary if * Simplify toGridCell * removed duplicate if * removed unused code * Adds grid demo with different data scenarios * Make it green * Demo grid with multiple time ranges * Move child atomically * Add tests * Cleanup * Fix unused import Co-authored-by: Torkel Ödegaard <torkel@grafana.com> Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>pull/58746/head
parent
16aa4376ac
commit
80e80221b9
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
|
||||
import { Icon } from '@grafana/ui'; |
||||
|
||||
export function SceneDragHandle({ layoutKey, className }: { layoutKey: string; className?: string }) { |
||||
return ( |
||||
<div |
||||
className={`${className} grid-drag-handle-${layoutKey}`} |
||||
style={{ |
||||
width: '20px', |
||||
height: '20px', |
||||
cursor: 'move', |
||||
}} |
||||
> |
||||
<Icon name="draggabledots" /> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,238 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import React from 'react'; |
||||
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase'; |
||||
import { SceneComponentProps, SceneLayoutChildState } from '../../core/types'; |
||||
import { Scene } from '../Scene'; |
||||
|
||||
import { SceneGridLayout, SceneGridRow } from './SceneGridLayout'; |
||||
|
||||
// Mocking AutoSizer to allow testing of the SceneGridLayout component rendering
|
||||
jest.mock( |
||||
'react-virtualized-auto-sizer', |
||||
() => |
||||
({ children }: { children: (args: { width: number; height: number }) => React.ReactNode }) => |
||||
children({ height: 600, width: 600 }) |
||||
); |
||||
|
||||
class TestObject extends SceneObjectBase<SceneLayoutChildState> { |
||||
public static Component = (m: SceneComponentProps<TestObject>) => { |
||||
return <div data-testid="test-object">TestObject</div>; |
||||
}; |
||||
} |
||||
|
||||
describe('SceneGridLayout', () => { |
||||
describe('rendering', () => { |
||||
it('should render all grid children', async () => { |
||||
const scene = new Scene({ |
||||
title: 'Grid test', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new TestObject({ size: { x: 0, y: 0, width: 12, height: 5 } }), |
||||
new TestObject({ size: { x: 0, y: 5, width: 12, height: 5 } }), |
||||
], |
||||
}), |
||||
}); |
||||
|
||||
render(<scene.Component model={scene} />); |
||||
|
||||
expect(screen.queryAllByTestId('test-object')).toHaveLength(2); |
||||
}); |
||||
|
||||
it('should not render children of a collapsed row', async () => { |
||||
const scene = new Scene({ |
||||
title: 'Grid test', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 12, height: 5 } }), |
||||
new TestObject({ key: 'b', size: { x: 0, y: 5, width: 12, height: 5 } }), |
||||
new SceneGridRow({ |
||||
title: 'Row A', |
||||
key: 'Row A', |
||||
isCollapsed: true, |
||||
size: { y: 10 }, |
||||
children: [new TestObject({ key: 'c', size: { x: 0, y: 11, width: 12, height: 5 } })], |
||||
}), |
||||
], |
||||
}), |
||||
}); |
||||
|
||||
render(<scene.Component model={scene} />); |
||||
|
||||
expect(screen.queryAllByTestId('test-object')).toHaveLength(2); |
||||
}); |
||||
|
||||
it('should render children of an expanded row', async () => { |
||||
const scene = new Scene({ |
||||
title: 'Grid test', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 12, height: 5 } }), |
||||
new TestObject({ key: 'b', size: { x: 0, y: 5, width: 12, height: 5 } }), |
||||
new SceneGridRow({ |
||||
title: 'Row A', |
||||
key: 'Row A', |
||||
isCollapsed: false, |
||||
size: { y: 10 }, |
||||
children: [new TestObject({ key: 'c', size: { x: 0, y: 11, width: 12, height: 5 } })], |
||||
}), |
||||
], |
||||
}), |
||||
}); |
||||
|
||||
render(<scene.Component model={scene} />); |
||||
|
||||
expect(screen.queryAllByTestId('test-object')).toHaveLength(3); |
||||
}); |
||||
}); |
||||
|
||||
describe('when moving a panel', () => { |
||||
it('shoud update layout children placement and order ', () => { |
||||
const layout = new SceneGridLayout({ |
||||
children: [ |
||||
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 1, height: 1 } }), |
||||
new TestObject({ key: 'b', size: { x: 1, y: 0, width: 1, height: 1 } }), |
||||
new TestObject({ key: 'c', size: { x: 0, y: 1, width: 1, height: 1 } }), |
||||
], |
||||
}); |
||||
layout.onDragStop( |
||||
[ |
||||
{ i: 'b', x: 0, y: 0, w: 1, h: 1 }, |
||||
{ |
||||
i: 'a', |
||||
x: 0, |
||||
y: 1, |
||||
w: 1, |
||||
h: 1, |
||||
}, |
||||
{ |
||||
i: 'c', |
||||
x: 0, |
||||
y: 2, |
||||
w: 1, |
||||
h: 1, |
||||
}, |
||||
], |
||||
// @ts-expect-error
|
||||
{}, |
||||
{ i: 'b', x: 0, y: 0, w: 1, h: 1 }, |
||||
{}, |
||||
{}, |
||||
{} |
||||
); |
||||
|
||||
expect(layout.state.children[0].state.key).toEqual('b'); |
||||
expect(layout.state.children[0].state.size).toEqual({ x: 0, y: 0, width: 1, height: 1 }); |
||||
expect(layout.state.children[1].state.key).toEqual('a'); |
||||
expect(layout.state.children[1].state.size).toEqual({ x: 0, y: 1, width: 1, height: 1 }); |
||||
expect(layout.state.children[2].state.key).toEqual('c'); |
||||
expect(layout.state.children[2].state.size).toEqual({ x: 0, y: 2, width: 1, height: 1 }); |
||||
}); |
||||
}); |
||||
|
||||
describe('when using rows', () => { |
||||
it('should update objects relations when moving object out of a row', () => { |
||||
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 1, width: 1, height: 1 } }); |
||||
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 1, width: 1, height: 1 } }); |
||||
|
||||
const sourceRow = new SceneGridRow({ |
||||
title: 'Row A', |
||||
key: 'row-a', |
||||
children: [rowAChild1, rowAChild2], |
||||
size: { y: 0 }, |
||||
}); |
||||
|
||||
const layout = new SceneGridLayout({ |
||||
children: [sourceRow], |
||||
}); |
||||
|
||||
const updatedLayout = layout.moveChildTo(rowAChild1, layout); |
||||
|
||||
expect(updatedLayout.length).toEqual(2); |
||||
|
||||
// the source row should be cloned and with children updated
|
||||
expect(updatedLayout[0].state.key).toEqual(sourceRow.state.key); |
||||
expect(updatedLayout[0]).not.toEqual(sourceRow); |
||||
expect((updatedLayout[0] as SceneGridRow).state.children.length).toEqual(1); |
||||
expect((updatedLayout[0] as SceneGridRow).state.children).not.toContain(rowAChild1); |
||||
|
||||
// the moved child should be cloned in the root
|
||||
expect(updatedLayout[1].state.key).toEqual(rowAChild1.state.key); |
||||
expect(updatedLayout[1]).not.toEqual(rowAChild1); |
||||
}); |
||||
it('should update objects relations when moving objects between rows', () => { |
||||
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 0, width: 1, height: 1 } }); |
||||
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 0, width: 1, height: 1 } }); |
||||
|
||||
const sourceRow = new SceneGridRow({ |
||||
title: 'Row A', |
||||
key: 'row-a', |
||||
children: [rowAChild1, rowAChild2], |
||||
}); |
||||
|
||||
const targetRow = new SceneGridRow({ |
||||
title: 'Row B', |
||||
key: 'row-b', |
||||
children: [], |
||||
}); |
||||
|
||||
const panelOutsideARow = new TestObject({ key: 'a', size: { x: 0, y: 0, width: 1, height: 1 } }); |
||||
const layout = new SceneGridLayout({ |
||||
children: [panelOutsideARow, sourceRow, targetRow], |
||||
}); |
||||
|
||||
const updatedLayout = layout.moveChildTo(rowAChild1, targetRow); |
||||
|
||||
expect(updatedLayout[0]).toEqual(panelOutsideARow); |
||||
|
||||
// the source row should be cloned and with children updated
|
||||
expect(updatedLayout[1].state.key).toEqual(sourceRow.state.key); |
||||
expect(updatedLayout[1]).not.toEqual(sourceRow); |
||||
expect((updatedLayout[1] as SceneGridRow).state.children.length).toEqual(1); |
||||
|
||||
// the target row should be cloned and with children updated
|
||||
expect(updatedLayout[2].state.key).toEqual(targetRow.state.key); |
||||
expect(updatedLayout[2]).not.toEqual(targetRow); |
||||
expect((updatedLayout[2] as SceneGridRow).state.children.length).toEqual(1); |
||||
|
||||
// the moved object should be cloned and added to the target row
|
||||
const movedObject = (updatedLayout[2] as SceneGridRow).state.children[0]; |
||||
expect(movedObject.state.key).toEqual('row-a-child1'); |
||||
expect(movedObject).not.toEqual(rowAChild1); |
||||
}); |
||||
|
||||
it('should update position of objects when row is expanded', () => { |
||||
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 1, width: 1, height: 1 } }); |
||||
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 1, width: 1, height: 1 } }); |
||||
|
||||
const rowA = new SceneGridRow({ |
||||
title: 'Row A', |
||||
key: 'row-a', |
||||
children: [rowAChild1, rowAChild2], |
||||
size: { y: 0 }, |
||||
isCollapsed: true, |
||||
}); |
||||
|
||||
const panelOutsideARow = new TestObject({ key: 'outsider', size: { x: 0, y: 1, width: 1, height: 1 } }); |
||||
|
||||
const rowBChild1 = new TestObject({ key: 'row-b-child1', size: { x: 0, y: 3, width: 1, height: 1 } }); |
||||
const rowB = new SceneGridRow({ |
||||
title: 'Row B', |
||||
key: 'row-b', |
||||
children: [rowBChild1], |
||||
size: { y: 2 }, |
||||
isCollapsed: false, |
||||
}); |
||||
|
||||
const layout = new SceneGridLayout({ |
||||
children: [rowA, panelOutsideARow, rowB], |
||||
}); |
||||
|
||||
layout.toggleRow(rowA); |
||||
|
||||
expect(panelOutsideARow.state!.size!.y).toEqual(2); |
||||
expect(rowB.state!.size!.y).toEqual(3); |
||||
expect(rowBChild1.state!.size!.y).toEqual(4); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,493 @@ |
||||
import { css, cx } from '@emotion/css'; |
||||
import React from 'react'; |
||||
import ReactGridLayout from 'react-grid-layout'; |
||||
import AutoSizer from 'react-virtualized-auto-sizer'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { Icon, useStyles2 } from '@grafana/ui'; |
||||
import { DEFAULT_PANEL_SPAN, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; |
||||
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase'; |
||||
import { |
||||
SceneComponentProps, |
||||
SceneLayoutChild, |
||||
SceneLayoutChildState, |
||||
SceneLayoutState, |
||||
SceneObject, |
||||
SceneObjectSize, |
||||
} from '../../core/types'; |
||||
import { SceneDragHandle } from '../SceneDragHandle'; |
||||
|
||||
interface SceneGridLayoutState extends SceneLayoutState {} |
||||
|
||||
export class SceneGridLayout extends SceneObjectBase<SceneGridLayoutState> { |
||||
public static Component = SceneGridLayoutRenderer; |
||||
|
||||
private _skipOnLayoutChange = false; |
||||
|
||||
public constructor(state: SceneGridLayoutState) { |
||||
super({ |
||||
isDraggable: true, |
||||
...state, |
||||
children: sortChildrenByPosition(state.children), |
||||
}); |
||||
} |
||||
|
||||
public toggleRow(row: SceneGridRow) { |
||||
const isCollapsed = row.state.isCollapsed; |
||||
|
||||
if (!isCollapsed) { |
||||
row.setState({ isCollapsed: true }); |
||||
// To force re-render
|
||||
this.setState({}); |
||||
return; |
||||
} |
||||
|
||||
const rowChildren = row.state.children; |
||||
|
||||
if (rowChildren.length === 0) { |
||||
row.setState({ isCollapsed: false }); |
||||
this.setState({}); |
||||
return; |
||||
} |
||||
|
||||
// Ok we are expanding row. We need to update row children y pos (incase they are incorrect) and push items below down
|
||||
// Code copied from DashboardModel toggleRow()
|
||||
|
||||
const rowY = row.state.size?.y!; |
||||
const firstPanelYPos = rowChildren[0].state.size?.y ?? rowY; |
||||
const yDiff = firstPanelYPos - (rowY + 1); |
||||
|
||||
// y max will represent the bottom y pos after all panels have been added
|
||||
// needed to know home much panels below should be pushed down
|
||||
let yMax = rowY; |
||||
|
||||
for (const panel of rowChildren) { |
||||
// set the y gridPos if it wasn't already set
|
||||
const newSize = { ...panel.state.size }; |
||||
newSize.y = newSize.y ?? rowY; |
||||
// make sure y is adjusted (in case row moved while collapsed)
|
||||
newSize.y -= yDiff; |
||||
if (newSize.y > panel.state.size?.y!) { |
||||
panel.setState({ size: newSize }); |
||||
} |
||||
// update insert post and y max
|
||||
yMax = Math.max(yMax, Number(newSize.y!) + Number(newSize.height!)); |
||||
} |
||||
|
||||
const pushDownAmount = yMax - rowY - 1; |
||||
|
||||
// push panels below down
|
||||
for (const child of this.state.children) { |
||||
if (child.state.size?.y! > rowY) { |
||||
this.pushChildDown(child, pushDownAmount); |
||||
} |
||||
|
||||
if (child instanceof SceneGridRow && child !== row) { |
||||
for (const rowChild of child.state.children) { |
||||
if (rowChild.state.size?.y! > rowY) { |
||||
this.pushChildDown(rowChild, pushDownAmount); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
row.setState({ isCollapsed: false }); |
||||
// Trigger re-render
|
||||
this.setState({}); |
||||
} |
||||
|
||||
public onLayoutChange = (layout: ReactGridLayout.Layout[]) => { |
||||
if (this._skipOnLayoutChange) { |
||||
// Layout has been updated by other RTL handler already
|
||||
this._skipOnLayoutChange = false; |
||||
return; |
||||
} |
||||
|
||||
for (const item of layout) { |
||||
const child = this.getSceneLayoutChild(item.i); |
||||
|
||||
const nextSize = { |
||||
x: item.x, |
||||
y: item.y, |
||||
width: item.w, |
||||
height: item.h, |
||||
}; |
||||
|
||||
if (!isItemSizeEqual(child.state.size!, nextSize)) { |
||||
child.setState({ |
||||
size: { |
||||
...child.state.size, |
||||
...nextSize, |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
this.setState({ children: sortChildrenByPosition(this.state.children) }); |
||||
}; |
||||
|
||||
/** |
||||
* Will also scan row children and return child of the row |
||||
*/ |
||||
public getSceneLayoutChild(key: string) { |
||||
for (const child of this.state.children) { |
||||
if (child.state.key === key) { |
||||
return child; |
||||
} |
||||
|
||||
if (child instanceof SceneGridRow) { |
||||
for (const rowChild of child.state.children) { |
||||
if (rowChild.state.key === key) { |
||||
return rowChild; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
throw new Error('Scene layout child not found for GridItem'); |
||||
} |
||||
|
||||
public onResizeStop: ReactGridLayout.ItemCallback = (_, o, n) => { |
||||
const child = this.getSceneLayoutChild(n.i); |
||||
child.setState({ |
||||
size: { |
||||
...child.state.size, |
||||
width: n.w, |
||||
height: n.h, |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
private pushChildDown(child: SceneLayoutChild, amount: number) { |
||||
child.setState({ |
||||
size: { |
||||
...child.state.size, |
||||
y: child.state.size?.y! + amount, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* We assume the layout array is storted according to y pos, and walk upwards until we find a row. |
||||
* If it is collapsed there is no row to add it to. The default is then to return the SceneGridLayout itself |
||||
*/ |
||||
private findGridItemSceneParent(layout: ReactGridLayout.Layout[], startAt: number): SceneGridRow | SceneGridLayout { |
||||
for (let i = startAt; i >= 0; i--) { |
||||
const gridItem = layout[i]; |
||||
const sceneChild = this.getSceneLayoutChild(gridItem.i); |
||||
|
||||
if (sceneChild instanceof SceneGridRow) { |
||||
// the closest row is collapsed return null
|
||||
if (sceneChild.state.isCollapsed) { |
||||
return this; |
||||
} |
||||
|
||||
return sceneChild; |
||||
} |
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
/** |
||||
* This likely needs a slighltly different approach. Where we clone or deactivate or and re-activate the moved child |
||||
*/ |
||||
public moveChildTo(child: SceneLayoutChild, target: SceneGridLayout | SceneGridRow) { |
||||
const currentParent = child.parent!; |
||||
let rootChildren = this.state.children; |
||||
const newChild = child.clone({ key: child.state.key }); |
||||
|
||||
// Remove from current parent row
|
||||
if (currentParent instanceof SceneGridRow) { |
||||
const newRow = currentParent.clone({ |
||||
children: currentParent.state.children.filter((c) => c.state.key !== child.state.key), |
||||
}); |
||||
|
||||
// new children with new row
|
||||
rootChildren = rootChildren.map((c) => (c === currentParent ? newRow : c)); |
||||
|
||||
// if target is also a row
|
||||
if (target instanceof SceneGridRow) { |
||||
const targetRow = target.clone({ children: [...target.state.children, newChild] }); |
||||
rootChildren = rootChildren.map((c) => (c === target ? targetRow : c)); |
||||
} else { |
||||
// target is the main grid
|
||||
rootChildren = [...rootChildren, newChild]; |
||||
} |
||||
} else { |
||||
// current parent is the main grid remove it from there
|
||||
rootChildren = rootChildren.filter((c) => c.state.key !== child.state.key); |
||||
// Clone the target row and add the child
|
||||
const targetRow = target.clone({ children: [...target.state.children, newChild] }); |
||||
// Replace row with new row
|
||||
rootChildren = rootChildren.map((c) => (c === target ? targetRow : c)); |
||||
} |
||||
|
||||
return rootChildren; |
||||
} |
||||
|
||||
public onDragStop: ReactGridLayout.ItemCallback = (gridLayout, o, updatedItem) => { |
||||
const sceneChild = this.getSceneLayoutChild(updatedItem.i)!; |
||||
|
||||
// Need to resort the grid layout based on new position (needed to to find the new parent)
|
||||
gridLayout = sortGridLayout(gridLayout); |
||||
|
||||
// Update children positions if they have changed
|
||||
for (let i = 0; i < gridLayout.length; i++) { |
||||
const gridItem = gridLayout[i]; |
||||
const child = this.getSceneLayoutChild(gridItem.i)!; |
||||
const childSize = child.state.size!; |
||||
|
||||
if (childSize?.x !== gridItem.x || childSize?.y !== gridItem.y) { |
||||
child.setState({ |
||||
size: { |
||||
...child.state.size, |
||||
x: gridItem.x, |
||||
y: gridItem.y, |
||||
}, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// Update the parent if the child if it has moved to a row or back to the grid
|
||||
const indexOfUpdatedItem = gridLayout.findIndex((item) => item.i === updatedItem.i); |
||||
const newParent = this.findGridItemSceneParent(gridLayout, indexOfUpdatedItem - 1); |
||||
let newChildren = this.state.children; |
||||
|
||||
if (newParent !== sceneChild.parent) { |
||||
newChildren = this.moveChildTo(sceneChild, newParent); |
||||
} |
||||
|
||||
this.setState({ children: sortChildrenByPosition(newChildren) }); |
||||
this._skipOnLayoutChange = true; |
||||
}; |
||||
|
||||
private toGridCell(child: SceneLayoutChild): ReactGridLayout.Layout { |
||||
const size = child.state.size!; |
||||
|
||||
let x = size.x ?? 0; |
||||
let y = size.y ?? 0; |
||||
const w = Number.isInteger(Number(size.width)) ? Number(size.width) : DEFAULT_PANEL_SPAN; |
||||
const h = Number.isInteger(Number(size.height)) ? Number(size.height) : DEFAULT_PANEL_SPAN; |
||||
|
||||
let isDraggable = Boolean(child.state.isDraggable); |
||||
let isResizable = Boolean(child.state.isResizable); |
||||
|
||||
if (child instanceof SceneGridRow) { |
||||
isDraggable = child.state.isCollapsed ? true : false; |
||||
isResizable = false; |
||||
} |
||||
|
||||
return { i: child.state.key!, x, y, h, w, isResizable, isDraggable }; |
||||
} |
||||
|
||||
public buildGridLayout(width: number): ReactGridLayout.Layout[] { |
||||
let cells: ReactGridLayout.Layout[] = []; |
||||
|
||||
for (const child of this.state.children) { |
||||
cells.push(this.toGridCell(child)); |
||||
|
||||
if (child instanceof SceneGridRow && !child.state.isCollapsed) { |
||||
for (const rowChild of child.state.children) { |
||||
cells.push(this.toGridCell(rowChild)); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Sort by position
|
||||
cells = sortGridLayout(cells); |
||||
|
||||
if (width < 768) { |
||||
// We should not persist the mobile layout
|
||||
this._skipOnLayoutChange = true; |
||||
return cells.map((cell) => ({ ...cell, w: 24 })); |
||||
} |
||||
|
||||
this._skipOnLayoutChange = false; |
||||
|
||||
return cells; |
||||
} |
||||
} |
||||
|
||||
function SceneGridLayoutRenderer({ model }: SceneComponentProps<SceneGridLayout>) { |
||||
const { children } = model.useState(); |
||||
validateChildrenSize(children); |
||||
|
||||
return ( |
||||
<AutoSizer disableHeight> |
||||
{({ width }) => { |
||||
if (width === 0) { |
||||
return null; |
||||
} |
||||
|
||||
const layout = model.buildGridLayout(width); |
||||
|
||||
return ( |
||||
/** |
||||
* The children is using a width of 100% so we need to guarantee that it is wrapped |
||||
* in an element that has the calculated size given by the AutoSizer. The AutoSizer |
||||
* has a width of 0 and will let its content overflow its div. |
||||
*/ |
||||
<div style={{ width: `${width}px`, height: '100%' }}> |
||||
<ReactGridLayout |
||||
width={width} |
||||
/* |
||||
Disable draggable if mobile device, solving an issue with unintentionally |
||||
moving panels. https://github.com/grafana/grafana/issues/18497
|
||||
theme.breakpoints.md = 769 |
||||
*/ |
||||
isDraggable={width > 768} |
||||
isResizable={false} |
||||
containerPadding={[0, 0]} |
||||
useCSSTransforms={false} |
||||
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]} |
||||
cols={GRID_COLUMN_COUNT} |
||||
rowHeight={GRID_CELL_HEIGHT} |
||||
draggableHandle={`.grid-drag-handle-${model.state.key}`} |
||||
// @ts-ignore: ignoring for now until we make the size type numbers-only
|
||||
layout={layout} |
||||
onDragStop={model.onDragStop} |
||||
onResizeStop={model.onResizeStop} |
||||
onLayoutChange={model.onLayoutChange} |
||||
isBounded={false} |
||||
> |
||||
{layout.map((gridItem) => { |
||||
const sceneChild = model.getSceneLayoutChild(gridItem.i)!; |
||||
return ( |
||||
<div key={sceneChild.state.key} style={{ display: 'flex' }}> |
||||
<sceneChild.Component model={sceneChild} key={sceneChild.state.key} /> |
||||
</div> |
||||
); |
||||
})} |
||||
</ReactGridLayout> |
||||
</div> |
||||
); |
||||
}} |
||||
</AutoSizer> |
||||
); |
||||
} |
||||
|
||||
interface SceneGridRowState extends SceneLayoutChildState { |
||||
title: string; |
||||
isCollapsible?: boolean; |
||||
isCollapsed?: boolean; |
||||
children: Array<SceneObject<SceneLayoutChildState>>; |
||||
} |
||||
|
||||
export class SceneGridRow extends SceneObjectBase<SceneGridRowState> { |
||||
public static Component = SceneGridRowRenderer; |
||||
|
||||
public constructor(state: SceneGridRowState) { |
||||
super({ |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
isCollapsible: true, |
||||
...state, |
||||
size: { |
||||
...state.size, |
||||
x: 0, |
||||
height: 1, |
||||
width: GRID_COLUMN_COUNT, |
||||
}, |
||||
}); |
||||
} |
||||
|
||||
public onCollapseToggle = () => { |
||||
if (!this.state.isCollapsible) { |
||||
return; |
||||
} |
||||
|
||||
const layout = this.parent; |
||||
|
||||
if (!layout || !(layout instanceof SceneGridLayout)) { |
||||
throw new Error('SceneGridRow must be a child of SceneGridLayout'); |
||||
} |
||||
|
||||
layout.toggleRow(this); |
||||
}; |
||||
} |
||||
|
||||
function SceneGridRowRenderer({ model }: SceneComponentProps<SceneGridRow>) { |
||||
const styles = useStyles2(getSceneGridRowStyles); |
||||
const { isCollapsible, isCollapsed, isDraggable, title } = model.useState(); |
||||
const layout = model.getLayout(); |
||||
const dragHandle = <SceneDragHandle layoutKey={layout.state.key!} />; |
||||
|
||||
return ( |
||||
<div className={styles.row}> |
||||
<div className={cx(styles.rowHeader, isCollapsed && styles.rowHeaderCollapsed)}> |
||||
<div onClick={model.onCollapseToggle} className={styles.rowTitleWrapper}> |
||||
{isCollapsible && <Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />} |
||||
<span className={styles.rowTitle}>{title}</span> |
||||
</div> |
||||
{isDraggable && isCollapsed && <div>{dragHandle}</div>} |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const getSceneGridRowStyles = (theme: GrafanaTheme2) => { |
||||
return { |
||||
row: css({ |
||||
width: '100%', |
||||
height: '100%', |
||||
position: 'relative', |
||||
zIndex: 0, |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
}), |
||||
rowHeader: css({ |
||||
width: '100%', |
||||
height: '30px', |
||||
display: 'flex', |
||||
justifyContent: 'space-between', |
||||
marginBottom: '8px', |
||||
border: `1px solid transparent`, |
||||
}), |
||||
rowTitleWrapper: css({ |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
cursor: 'pointer', |
||||
}), |
||||
rowHeaderCollapsed: css({ |
||||
marginBottom: '0px', |
||||
background: theme.colors.background.primary, |
||||
border: `1px solid ${theme.colors.border.weak}`, |
||||
borderRadius: theme.shape.borderRadius(1), |
||||
}), |
||||
rowTitle: css({ |
||||
fontSize: theme.typography.h6.fontSize, |
||||
fontWeight: theme.typography.h6.fontWeight, |
||||
}), |
||||
}; |
||||
}; |
||||
|
||||
function validateChildrenSize(children: SceneLayoutChild[]) { |
||||
if ( |
||||
children.find( |
||||
(c) => |
||||
!c.state.size || |
||||
c.state.size.height === undefined || |
||||
c.state.size.width === undefined || |
||||
c.state.size.x === undefined || |
||||
c.state.size.y === undefined |
||||
) |
||||
) { |
||||
throw new Error('All children must have a size specified'); |
||||
} |
||||
} |
||||
|
||||
function isItemSizeEqual(a: SceneObjectSize, b: SceneObjectSize) { |
||||
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height; |
||||
} |
||||
|
||||
function sortChildrenByPosition(children: SceneLayoutChild[]) { |
||||
return [...children].sort((a, b) => { |
||||
return a.state.size?.y! - b.state.size?.y! || a.state.size?.x! - b.state.size?.x!; |
||||
}); |
||||
} |
||||
|
||||
function sortGridLayout(layout: ReactGridLayout.Layout[]) { |
||||
return [...layout].sort((a, b) => a.y - b.y || a.x! - b.x); |
||||
} |
@ -0,0 +1,76 @@ |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
|
||||
import { Scene } from '../components/Scene'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout'; |
||||
import { SceneGridLayout } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneEditManager } from '../editor/SceneEditManager'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
export function getGridLayoutTest(): Scene { |
||||
const scene = new Scene({ |
||||
title: 'Grid layout test', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new VizPanel({ |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Draggable and resizable', |
||||
size: { |
||||
x: 0, |
||||
y: 0, |
||||
width: 12, |
||||
height: 10, |
||||
}, |
||||
}), |
||||
|
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'No drag and no resize', |
||||
isResizable: false, |
||||
isDraggable: false, |
||||
size: { x: 12, y: 0, width: 12, height: 10 }, |
||||
}), |
||||
|
||||
new SceneFlexLayout({ |
||||
direction: 'column', |
||||
isDraggable: true, |
||||
isResizable: true, |
||||
size: { x: 6, y: 11, width: 12, height: 10 }, |
||||
children: [ |
||||
new VizPanel({ |
||||
size: { ySizing: 'fill' }, |
||||
pluginId: 'timeseries', |
||||
title: 'Child of flex layout', |
||||
}), |
||||
new VizPanel({ |
||||
size: { ySizing: 'fill' }, |
||||
pluginId: 'timeseries', |
||||
title: 'Child of flex layout', |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}), |
||||
$editor: new SceneEditManager({}), |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
}, |
||||
], |
||||
}), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
return scene; |
||||
} |
@ -0,0 +1,109 @@ |
||||
import { dateTime, getDefaultTimeRange } from '@grafana/data'; |
||||
|
||||
import { Scene } from '../components/Scene'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneEditManager } from '../editor/SceneEditManager'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
export function getGridWithMultipleTimeRanges(): Scene { |
||||
const globalTimeRange = new SceneTimeRange(getDefaultTimeRange()); |
||||
|
||||
const now = dateTime(); |
||||
const row1TimeRange = new SceneTimeRange({ |
||||
from: dateTime(now).subtract(1, 'year'), |
||||
to: now, |
||||
raw: { from: 'now-1y', to: 'now' }, |
||||
}); |
||||
|
||||
const scene = new Scene({ |
||||
title: 'Grid with rows and different queries and time ranges', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new SceneGridRow({ |
||||
$timeRange: row1TimeRange, |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk_table', |
||||
}, |
||||
], |
||||
}), |
||||
title: 'Row A - has its own query, last year time range', |
||||
key: 'Row A', |
||||
isCollapsed: true, |
||||
size: { y: 0 }, |
||||
children: [ |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row A Child1', |
||||
key: 'Row A Child1', |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 1, width: 12, height: 5 }, |
||||
}), |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row A Child2', |
||||
key: 'Row A Child2', |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 5, width: 6, height: 5 }, |
||||
}), |
||||
], |
||||
}), |
||||
|
||||
new VizPanel({ |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
seriesCount: 10, |
||||
}, |
||||
], |
||||
}), |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Outsider, has its own query', |
||||
key: 'Outsider-own-query', |
||||
size: { |
||||
x: 0, |
||||
y: 12, |
||||
width: 6, |
||||
height: 10, |
||||
}, |
||||
}), |
||||
], |
||||
}), |
||||
$editor: new SceneEditManager({}), |
||||
$timeRange: globalTimeRange, |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
}, |
||||
], |
||||
}), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
return scene; |
||||
} |
@ -0,0 +1,120 @@ |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
|
||||
import { Scene } from '../components/Scene'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout'; |
||||
import { SceneGridLayout } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneEditManager } from '../editor/SceneEditManager'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
export function getMultipleGridLayoutTest(): Scene { |
||||
const scene = new Scene({ |
||||
title: 'Multiple grid layouts test', |
||||
layout: new SceneFlexLayout({ |
||||
children: [ |
||||
new SceneGridLayout({ |
||||
children: [ |
||||
new VizPanel({ |
||||
size: { |
||||
x: 0, |
||||
y: 0, |
||||
width: 12, |
||||
height: 10, |
||||
}, |
||||
isDraggable: true, |
||||
isResizable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Dragabble and resizable', |
||||
}), |
||||
new VizPanel({ |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
size: { x: 12, y: 0, width: 12, height: 10 }, |
||||
pluginId: 'timeseries', |
||||
title: 'Draggable only', |
||||
}), |
||||
new SceneFlexLayout({ |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 6, y: 11, width: 12, height: 10 }, |
||||
direction: 'column', |
||||
children: [ |
||||
new VizPanel({ |
||||
size: { ySizing: 'fill' }, |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
new VizPanel({ |
||||
size: { ySizing: 'fill' }, |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}), |
||||
|
||||
new SceneGridLayout({ |
||||
children: [ |
||||
new VizPanel({ |
||||
size: { |
||||
x: 0, |
||||
y: 0, |
||||
width: 12, |
||||
height: 10, |
||||
}, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
new VizPanel({ |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
size: { x: 12, y: 0, width: 12, height: 10 }, |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
new SceneFlexLayout({ |
||||
size: { x: 6, y: 11, width: 12, height: 10 }, |
||||
direction: 'column', |
||||
children: [ |
||||
new VizPanel({ |
||||
size: { ySizing: 'fill' }, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
new VizPanel({ |
||||
isDraggable: true, |
||||
size: { ySizing: 'fill' }, |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}), |
||||
|
||||
$editor: new SceneEditManager({}), |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
}, |
||||
], |
||||
}), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
return scene; |
||||
} |
@ -0,0 +1,149 @@ |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
|
||||
import { Scene } from '../components/Scene'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneEditManager } from '../editor/SceneEditManager'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
export function getGridWithMultipleData(): Scene { |
||||
const scene = new Scene({ |
||||
title: 'Grid with rows and different queries', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new SceneGridRow({ |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk_table', |
||||
}, |
||||
], |
||||
}), |
||||
title: 'Row A - has its own query', |
||||
key: 'Row A', |
||||
isCollapsed: true, |
||||
size: { y: 0 }, |
||||
children: [ |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row A Child1', |
||||
key: 'Row A Child1', |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 1, width: 12, height: 5 }, |
||||
}), |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row A Child2', |
||||
key: 'Row A Child2', |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 5, width: 6, height: 5 }, |
||||
}), |
||||
], |
||||
}), |
||||
new SceneGridRow({ |
||||
title: 'Row B - uses global query', |
||||
key: 'Row B', |
||||
isCollapsed: true, |
||||
size: { y: 1 }, |
||||
children: [ |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row B Child1', |
||||
key: 'Row B Child1', |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 2, width: 12, height: 5 }, |
||||
}), |
||||
new VizPanel({ |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
seriesCount: 10, |
||||
}, |
||||
], |
||||
}), |
||||
pluginId: 'timeseries', |
||||
title: 'Row B Child2 with data', |
||||
key: 'Row B Child2', |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 7, width: 6, height: 5 }, |
||||
}), |
||||
], |
||||
}), |
||||
new VizPanel({ |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
seriesCount: 10, |
||||
}, |
||||
], |
||||
}), |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Outsider, has its own query', |
||||
key: 'Outsider-own-query', |
||||
size: { |
||||
x: 0, |
||||
y: 12, |
||||
width: 6, |
||||
height: 10, |
||||
}, |
||||
}), |
||||
new VizPanel({ |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Outsider, uses global query', |
||||
key: 'Outsider-global-query', |
||||
size: { |
||||
x: 6, |
||||
y: 12, |
||||
width: 12, |
||||
height: 10, |
||||
}, |
||||
}), |
||||
], |
||||
}), |
||||
$editor: new SceneEditManager({}), |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
}, |
||||
], |
||||
}), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
return scene; |
||||
} |
@ -0,0 +1,97 @@ |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
|
||||
import { Scene } from '../components/Scene'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneEditManager } from '../editor/SceneEditManager'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
export function getGridWithRowLayoutTest(): Scene { |
||||
const scene = new Scene({ |
||||
title: 'Grid with row layout test', |
||||
layout: new SceneGridLayout({ |
||||
children: [ |
||||
new SceneGridRow({ |
||||
title: 'Row A', |
||||
key: 'Row A', |
||||
isCollapsed: true, |
||||
size: { y: 0 }, |
||||
children: [ |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row A Child1', |
||||
key: 'Row A Child1', |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 1, width: 12, height: 5 }, |
||||
}), |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row A Child2', |
||||
key: 'Row A Child2', |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 5, width: 6, height: 5 }, |
||||
}), |
||||
], |
||||
}), |
||||
new SceneGridRow({ |
||||
title: 'Row B', |
||||
key: 'Row B', |
||||
isCollapsed: true, |
||||
size: { y: 1 }, |
||||
children: [ |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row B Child1', |
||||
key: 'Row B Child1', |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 2, width: 12, height: 5 }, |
||||
}), |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Row B Child2', |
||||
key: 'Row B Child2', |
||||
isResizable: false, |
||||
isDraggable: true, |
||||
size: { x: 0, y: 7, width: 6, height: 5 }, |
||||
}), |
||||
], |
||||
}), |
||||
new VizPanel({ |
||||
isResizable: true, |
||||
isDraggable: true, |
||||
pluginId: 'timeseries', |
||||
title: 'Outsider', |
||||
key: 'Outsider', |
||||
size: { |
||||
x: 2, |
||||
y: 12, |
||||
width: 12, |
||||
height: 10, |
||||
}, |
||||
}), |
||||
], |
||||
}), |
||||
$editor: new SceneEditManager({}), |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
}, |
||||
], |
||||
}), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
return scene; |
||||
} |
@ -0,0 +1,102 @@ |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
|
||||
import { Scene } from '../components/Scene'; |
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout'; |
||||
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneEditManager } from '../editor/SceneEditManager'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
export function getGridWithRowsTest(): Scene { |
||||
const panel = new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}); |
||||
|
||||
const row1 = new SceneGridRow({ |
||||
title: 'Collapsible/draggable row with flex layout', |
||||
size: { x: 0, y: 0, height: 10 }, |
||||
children: [ |
||||
new SceneFlexLayout({ |
||||
direction: 'row', |
||||
children: [ |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
new VizPanel({ |
||||
pluginId: 'timeseries', |
||||
title: 'Fill height', |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}); |
||||
|
||||
const cell1 = new VizPanel({ |
||||
size: { |
||||
x: 0, |
||||
y: 10, |
||||
width: 12, |
||||
height: 20, |
||||
}, |
||||
pluginId: 'timeseries', |
||||
title: 'Cell 1', |
||||
}); |
||||
|
||||
const cell2 = new VizPanel({ |
||||
isResizable: false, |
||||
isDraggable: false, |
||||
size: { x: 12, y: 20, width: 12, height: 10 }, |
||||
pluginId: 'timeseries', |
||||
title: 'No resize/no drag', |
||||
}); |
||||
|
||||
const row2 = new SceneGridRow({ |
||||
size: { x: 12, y: 10, height: 10, width: 12 }, |
||||
title: 'Row with a nested flex layout', |
||||
children: [ |
||||
new SceneFlexLayout({ |
||||
children: [ |
||||
new SceneFlexLayout({ |
||||
direction: 'column', |
||||
children: [panel, panel], |
||||
}), |
||||
new SceneFlexLayout({ |
||||
direction: 'column', |
||||
children: [panel, panel], |
||||
}), |
||||
], |
||||
}), |
||||
], |
||||
}); |
||||
const scene = new Scene({ |
||||
title: 'Grid rows test', |
||||
layout: new SceneGridLayout({ |
||||
children: [cell1, cell2, row1, row2], |
||||
}), |
||||
$editor: new SceneEditManager({}), |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
$data: new SceneQueryRunner({ |
||||
queries: [ |
||||
{ |
||||
refId: 'A', |
||||
datasource: { |
||||
uid: 'gdev-testdata', |
||||
type: 'testdata', |
||||
}, |
||||
scenarioId: 'random_walk', |
||||
}, |
||||
], |
||||
}), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
return scene; |
||||
} |
Loading…
Reference in new issue