mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
16aa4376ac
commit
80e80221b9
@ -4556,7 +4556,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/sandbox/TestStuffPage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/scenes/components/SceneFlexLayout.tsx:5381": [
|
||||
"public/app/features/scenes/components/layout/SceneFlexLayout.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/scenes/core/SceneComponentWrapper.tsx:5381": [
|
||||
@ -4570,9 +4570,10 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
],
|
||||
"public/app/features/scenes/core/SceneTimeRange.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
import { NestedScene } from './NestedScene';
|
||||
import { Scene } from './Scene';
|
||||
import { SceneCanvasText } from './SceneCanvasText';
|
||||
import { SceneFlexLayout } from './SceneFlexLayout';
|
||||
import { SceneFlexLayout } from './layout/SceneFlexLayout';
|
||||
|
||||
function setup() {
|
||||
const scene = new Scene({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Scene } from './Scene';
|
||||
import { SceneFlexLayout } from './SceneFlexLayout';
|
||||
import { SceneFlexLayout } from './layout/SceneFlexLayout';
|
||||
|
||||
describe('Scene', () => {
|
||||
it('Simple scene', () => {
|
||||
|
18
public/app/features/scenes/components/SceneDragHandle.tsx
Normal file
18
public/app/features/scenes/components/SceneDragHandle.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon } from '@grafana/ui';
|
||||
|
||||
export function SceneDragHandle({ layoutKey, className }: { layoutKey: string; className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={`${className} grid-drag-handle-${layoutKey}`}
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
cursor: 'move',
|
||||
}}
|
||||
>
|
||||
<Icon name="draggabledots" />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -8,6 +8,8 @@ import { Field, PanelChrome, Input } from '@grafana/ui';
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||
import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
|
||||
|
||||
import { SceneDragHandle } from './SceneDragHandle';
|
||||
|
||||
export interface VizPanelState extends SceneLayoutChildState {
|
||||
title?: string;
|
||||
pluginId: string;
|
||||
@ -33,8 +35,11 @@ export class VizPanel extends SceneObjectBase<VizPanelState> {
|
||||
}
|
||||
|
||||
function ScenePanelRenderer({ model }: SceneComponentProps<VizPanel>) {
|
||||
const { title, pluginId, options, fieldConfig } = model.useState();
|
||||
const { title, pluginId, options, fieldConfig, ...state } = model.useState();
|
||||
const { data } = model.getData().useState();
|
||||
const layout = model.getLayout();
|
||||
const isDraggable = layout.state.isDraggable ? state.isDraggable : false;
|
||||
const dragHandle = <SceneDragHandle layoutKey={layout.state.key!} />;
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
@ -44,7 +49,7 @@ function ScenePanelRenderer({ model }: SceneComponentProps<VizPanel>) {
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelChrome title={title} width={width} height={height}>
|
||||
<PanelChrome title={title} width={width} height={height} leftItems={isDraggable ? [dragHandle] : undefined}>
|
||||
{(innerWidth, innerHeight) => (
|
||||
<>
|
||||
<PanelRenderer
|
||||
|
@ -2,8 +2,8 @@ import React, { CSSProperties } from 'react';
|
||||
|
||||
import { Field, RadioButtonGroup } from '@grafana/ui';
|
||||
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||
import { SceneObjectSize, SceneLayoutState, SceneComponentProps, SceneLayoutChild } from '../core/types';
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||
import { SceneComponentProps, SceneLayoutChild, SceneLayoutState, SceneObjectSize } from '../../core/types';
|
||||
|
||||
export type FlexLayoutDirection = 'column' | 'row';
|
||||
|
@ -0,0 +1,238 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||
import { SceneComponentProps, SceneLayoutChildState } from '../../core/types';
|
||||
import { Scene } from '../Scene';
|
||||
|
||||
import { SceneGridLayout, SceneGridRow } from './SceneGridLayout';
|
||||
|
||||
// Mocking AutoSizer to allow testing of the SceneGridLayout component rendering
|
||||
jest.mock(
|
||||
'react-virtualized-auto-sizer',
|
||||
() =>
|
||||
({ children }: { children: (args: { width: number; height: number }) => React.ReactNode }) =>
|
||||
children({ height: 600, width: 600 })
|
||||
);
|
||||
|
||||
class TestObject extends SceneObjectBase<SceneLayoutChildState> {
|
||||
public static Component = (m: SceneComponentProps<TestObject>) => {
|
||||
return <div data-testid="test-object">TestObject</div>;
|
||||
};
|
||||
}
|
||||
|
||||
describe('SceneGridLayout', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render all grid children', async () => {
|
||||
const scene = new Scene({
|
||||
title: 'Grid test',
|
||||
layout: new SceneGridLayout({
|
||||
children: [
|
||||
new TestObject({ size: { x: 0, y: 0, width: 12, height: 5 } }),
|
||||
new TestObject({ size: { x: 0, y: 5, width: 12, height: 5 } }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
render(<scene.Component model={scene} />);
|
||||
|
||||
expect(screen.queryAllByTestId('test-object')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should not render children of a collapsed row', async () => {
|
||||
const scene = new Scene({
|
||||
title: 'Grid test',
|
||||
layout: new SceneGridLayout({
|
||||
children: [
|
||||
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 12, height: 5 } }),
|
||||
new TestObject({ key: 'b', size: { x: 0, y: 5, width: 12, height: 5 } }),
|
||||
new SceneGridRow({
|
||||
title: 'Row A',
|
||||
key: 'Row A',
|
||||
isCollapsed: true,
|
||||
size: { y: 10 },
|
||||
children: [new TestObject({ key: 'c', size: { x: 0, y: 11, width: 12, height: 5 } })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
render(<scene.Component model={scene} />);
|
||||
|
||||
expect(screen.queryAllByTestId('test-object')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render children of an expanded row', async () => {
|
||||
const scene = new Scene({
|
||||
title: 'Grid test',
|
||||
layout: new SceneGridLayout({
|
||||
children: [
|
||||
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 12, height: 5 } }),
|
||||
new TestObject({ key: 'b', size: { x: 0, y: 5, width: 12, height: 5 } }),
|
||||
new SceneGridRow({
|
||||
title: 'Row A',
|
||||
key: 'Row A',
|
||||
isCollapsed: false,
|
||||
size: { y: 10 },
|
||||
children: [new TestObject({ key: 'c', size: { x: 0, y: 11, width: 12, height: 5 } })],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
render(<scene.Component model={scene} />);
|
||||
|
||||
expect(screen.queryAllByTestId('test-object')).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when moving a panel', () => {
|
||||
it('shoud update layout children placement and order ', () => {
|
||||
const layout = new SceneGridLayout({
|
||||
children: [
|
||||
new TestObject({ key: 'a', size: { x: 0, y: 0, width: 1, height: 1 } }),
|
||||
new TestObject({ key: 'b', size: { x: 1, y: 0, width: 1, height: 1 } }),
|
||||
new TestObject({ key: 'c', size: { x: 0, y: 1, width: 1, height: 1 } }),
|
||||
],
|
||||
});
|
||||
layout.onDragStop(
|
||||
[
|
||||
{ i: 'b', x: 0, y: 0, w: 1, h: 1 },
|
||||
{
|
||||
i: 'a',
|
||||
x: 0,
|
||||
y: 1,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
{
|
||||
i: 'c',
|
||||
x: 0,
|
||||
y: 2,
|
||||
w: 1,
|
||||
h: 1,
|
||||
},
|
||||
],
|
||||
// @ts-expect-error
|
||||
{},
|
||||
{ i: 'b', x: 0, y: 0, w: 1, h: 1 },
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
);
|
||||
|
||||
expect(layout.state.children[0].state.key).toEqual('b');
|
||||
expect(layout.state.children[0].state.size).toEqual({ x: 0, y: 0, width: 1, height: 1 });
|
||||
expect(layout.state.children[1].state.key).toEqual('a');
|
||||
expect(layout.state.children[1].state.size).toEqual({ x: 0, y: 1, width: 1, height: 1 });
|
||||
expect(layout.state.children[2].state.key).toEqual('c');
|
||||
expect(layout.state.children[2].state.size).toEqual({ x: 0, y: 2, width: 1, height: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using rows', () => {
|
||||
it('should update objects relations when moving object out of a row', () => {
|
||||
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 1, width: 1, height: 1 } });
|
||||
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 1, width: 1, height: 1 } });
|
||||
|
||||
const sourceRow = new SceneGridRow({
|
||||
title: 'Row A',
|
||||
key: 'row-a',
|
||||
children: [rowAChild1, rowAChild2],
|
||||
size: { y: 0 },
|
||||
});
|
||||
|
||||
const layout = new SceneGridLayout({
|
||||
children: [sourceRow],
|
||||
});
|
||||
|
||||
const updatedLayout = layout.moveChildTo(rowAChild1, layout);
|
||||
|
||||
expect(updatedLayout.length).toEqual(2);
|
||||
|
||||
// the source row should be cloned and with children updated
|
||||
expect(updatedLayout[0].state.key).toEqual(sourceRow.state.key);
|
||||
expect(updatedLayout[0]).not.toEqual(sourceRow);
|
||||
expect((updatedLayout[0] as SceneGridRow).state.children.length).toEqual(1);
|
||||
expect((updatedLayout[0] as SceneGridRow).state.children).not.toContain(rowAChild1);
|
||||
|
||||
// the moved child should be cloned in the root
|
||||
expect(updatedLayout[1].state.key).toEqual(rowAChild1.state.key);
|
||||
expect(updatedLayout[1]).not.toEqual(rowAChild1);
|
||||
});
|
||||
it('should update objects relations when moving objects between rows', () => {
|
||||
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 0, width: 1, height: 1 } });
|
||||
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 0, width: 1, height: 1 } });
|
||||
|
||||
const sourceRow = new SceneGridRow({
|
||||
title: 'Row A',
|
||||
key: 'row-a',
|
||||
children: [rowAChild1, rowAChild2],
|
||||
});
|
||||
|
||||
const targetRow = new SceneGridRow({
|
||||
title: 'Row B',
|
||||
key: 'row-b',
|
||||
children: [],
|
||||
});
|
||||
|
||||
const panelOutsideARow = new TestObject({ key: 'a', size: { x: 0, y: 0, width: 1, height: 1 } });
|
||||
const layout = new SceneGridLayout({
|
||||
children: [panelOutsideARow, sourceRow, targetRow],
|
||||
});
|
||||
|
||||
const updatedLayout = layout.moveChildTo(rowAChild1, targetRow);
|
||||
|
||||
expect(updatedLayout[0]).toEqual(panelOutsideARow);
|
||||
|
||||
// the source row should be cloned and with children updated
|
||||
expect(updatedLayout[1].state.key).toEqual(sourceRow.state.key);
|
||||
expect(updatedLayout[1]).not.toEqual(sourceRow);
|
||||
expect((updatedLayout[1] as SceneGridRow).state.children.length).toEqual(1);
|
||||
|
||||
// the target row should be cloned and with children updated
|
||||
expect(updatedLayout[2].state.key).toEqual(targetRow.state.key);
|
||||
expect(updatedLayout[2]).not.toEqual(targetRow);
|
||||
expect((updatedLayout[2] as SceneGridRow).state.children.length).toEqual(1);
|
||||
|
||||
// the moved object should be cloned and added to the target row
|
||||
const movedObject = (updatedLayout[2] as SceneGridRow).state.children[0];
|
||||
expect(movedObject.state.key).toEqual('row-a-child1');
|
||||
expect(movedObject).not.toEqual(rowAChild1);
|
||||
});
|
||||
|
||||
it('should update position of objects when row is expanded', () => {
|
||||
const rowAChild1 = new TestObject({ key: 'row-a-child1', size: { x: 0, y: 1, width: 1, height: 1 } });
|
||||
const rowAChild2 = new TestObject({ key: 'row-a-child2', size: { x: 1, y: 1, width: 1, height: 1 } });
|
||||
|
||||
const rowA = new SceneGridRow({
|
||||
title: 'Row A',
|
||||
key: 'row-a',
|
||||
children: [rowAChild1, rowAChild2],
|
||||
size: { y: 0 },
|
||||
isCollapsed: true,
|
||||
});
|
||||
|
||||
const panelOutsideARow = new TestObject({ key: 'outsider', size: { x: 0, y: 1, width: 1, height: 1 } });
|
||||
|
||||
const rowBChild1 = new TestObject({ key: 'row-b-child1', size: { x: 0, y: 3, width: 1, height: 1 } });
|
||||
const rowB = new SceneGridRow({
|
||||
title: 'Row B',
|
||||
key: 'row-b',
|
||||
children: [rowBChild1],
|
||||
size: { y: 2 },
|
||||
isCollapsed: false,
|
||||
});
|
||||
|
||||
const layout = new SceneGridLayout({
|
||||
children: [rowA, panelOutsideARow, rowB],
|
||||
});
|
||||
|
||||
layout.toggleRow(rowA);
|
||||
|
||||
expect(panelOutsideARow.state!.size!.y).toEqual(2);
|
||||
expect(rowB.state!.size!.y).toEqual(3);
|
||||
expect(rowBChild1.state!.size!.y).toEqual(4);
|
||||
});
|
||||
});
|
||||
});
|
493
public/app/features/scenes/components/layout/SceneGridLayout.tsx
Normal file
493
public/app/features/scenes/components/layout/SceneGridLayout.tsx
Normal file
@ -0,0 +1,493 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import ReactGridLayout from 'react-grid-layout';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, useStyles2 } from '@grafana/ui';
|
||||
import { DEFAULT_PANEL_SPAN, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneLayoutChild,
|
||||
SceneLayoutChildState,
|
||||
SceneLayoutState,
|
||||
SceneObject,
|
||||
SceneObjectSize,
|
||||
} from '../../core/types';
|
||||
import { SceneDragHandle } from '../SceneDragHandle';
|
||||
|
||||
interface SceneGridLayoutState extends SceneLayoutState {}
|
||||
|
||||
export class SceneGridLayout extends SceneObjectBase<SceneGridLayoutState> {
|
||||
public static Component = SceneGridLayoutRenderer;
|
||||
|
||||
private _skipOnLayoutChange = false;
|
||||
|
||||
public constructor(state: SceneGridLayoutState) {
|
||||
super({
|
||||
isDraggable: true,
|
||||
...state,
|
||||
children: sortChildrenByPosition(state.children),
|
||||
});
|
||||
}
|
||||
|
||||
public toggleRow(row: SceneGridRow) {
|
||||
const isCollapsed = row.state.isCollapsed;
|
||||
|
||||
if (!isCollapsed) {
|
||||
row.setState({ isCollapsed: true });
|
||||
// To force re-render
|
||||
this.setState({});
|
||||
return;
|
||||
}
|
||||
|
||||
const rowChildren = row.state.children;
|
||||
|
||||
if (rowChildren.length === 0) {
|
||||
row.setState({ isCollapsed: false });
|
||||
this.setState({});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ok we are expanding row. We need to update row children y pos (incase they are incorrect) and push items below down
|
||||
// Code copied from DashboardModel toggleRow()
|
||||
|
||||
const rowY = row.state.size?.y!;
|
||||
const firstPanelYPos = rowChildren[0].state.size?.y ?? rowY;
|
||||
const yDiff = firstPanelYPos - (rowY + 1);
|
||||
|
||||
// y max will represent the bottom y pos after all panels have been added
|
||||
// needed to know home much panels below should be pushed down
|
||||
let yMax = rowY;
|
||||
|
||||
for (const panel of rowChildren) {
|
||||
// set the y gridPos if it wasn't already set
|
||||
const newSize = { ...panel.state.size };
|
||||
newSize.y = newSize.y ?? rowY;
|
||||
// make sure y is adjusted (in case row moved while collapsed)
|
||||
newSize.y -= yDiff;
|
||||
if (newSize.y > panel.state.size?.y!) {
|
||||
panel.setState({ size: newSize });
|
||||
}
|
||||
// update insert post and y max
|
||||
yMax = Math.max(yMax, Number(newSize.y!) + Number(newSize.height!));
|
||||
}
|
||||
|
||||
const pushDownAmount = yMax - rowY - 1;
|
||||
|
||||
// push panels below down
|
||||
for (const child of this.state.children) {
|
||||
if (child.state.size?.y! > rowY) {
|
||||
this.pushChildDown(child, pushDownAmount);
|
||||
}
|
||||
|
||||
if (child instanceof SceneGridRow && child !== row) {
|
||||
for (const rowChild of child.state.children) {
|
||||
if (rowChild.state.size?.y! > rowY) {
|
||||
this.pushChildDown(rowChild, pushDownAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
row.setState({ isCollapsed: false });
|
||||
// Trigger re-render
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
public onLayoutChange = (layout: ReactGridLayout.Layout[]) => {
|
||||
if (this._skipOnLayoutChange) {
|
||||
// Layout has been updated by other RTL handler already
|
||||
this._skipOnLayoutChange = false;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of layout) {
|
||||
const child = this.getSceneLayoutChild(item.i);
|
||||
|
||||
const nextSize = {
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
width: item.w,
|
||||
height: item.h,
|
||||
};
|
||||
|
||||
if (!isItemSizeEqual(child.state.size!, nextSize)) {
|
||||
child.setState({
|
||||
size: {
|
||||
...child.state.size,
|
||||
...nextSize,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ children: sortChildrenByPosition(this.state.children) });
|
||||
};
|
||||
|
||||
/**
|
||||
* Will also scan row children and return child of the row
|
||||
*/
|
||||
public getSceneLayoutChild(key: string) {
|
||||
for (const child of this.state.children) {
|
||||
if (child.state.key === key) {
|
||||
return child;
|
||||
}
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
for (const rowChild of child.state.children) {
|
||||
if (rowChild.state.key === key) {
|
||||
return rowChild;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Scene layout child not found for GridItem');
|
||||
}
|
||||
|
||||
public onResizeStop: ReactGridLayout.ItemCallback = (_, o, n) => {
|
||||
const child = this.getSceneLayoutChild(n.i);
|
||||
child.setState({
|
||||
size: {
|
||||
...child.state.size,
|
||||
width: n.w,
|
||||
height: n.h,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private pushChildDown(child: SceneLayoutChild, amount: number) {
|
||||
child.setState({
|
||||
size: {
|
||||
...child.state.size,
|
||||
y: child.state.size?.y! + amount,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We assume the layout array is storted according to y pos, and walk upwards until we find a row.
|
||||
* If it is collapsed there is no row to add it to. The default is then to return the SceneGridLayout itself
|
||||
*/
|
||||
private findGridItemSceneParent(layout: ReactGridLayout.Layout[], startAt: number): SceneGridRow | SceneGridLayout {
|
||||
for (let i = startAt; i >= 0; i--) {
|
||||
const gridItem = layout[i];
|
||||
const sceneChild = this.getSceneLayoutChild(gridItem.i);
|
||||
|
||||
if (sceneChild instanceof SceneGridRow) {
|
||||
// the closest row is collapsed return null
|
||||
if (sceneChild.state.isCollapsed) {
|
||||
return this;
|
||||
}
|
||||
|
||||
return sceneChild;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This likely needs a slighltly different approach. Where we clone or deactivate or and re-activate the moved child
|
||||
*/
|
||||
public moveChildTo(child: SceneLayoutChild, target: SceneGridLayout | SceneGridRow) {
|
||||
const currentParent = child.parent!;
|
||||
let rootChildren = this.state.children;
|
||||
const newChild = child.clone({ key: child.state.key });
|
||||
|
||||
// Remove from current parent row
|
||||
if (currentParent instanceof SceneGridRow) {
|
||||
const newRow = currentParent.clone({
|
||||
children: currentParent.state.children.filter((c) => c.state.key !== child.state.key),
|
||||
});
|
||||
|
||||
// new children with new row
|
||||
rootChildren = rootChildren.map((c) => (c === currentParent ? newRow : c));
|
||||
|
||||
// if target is also a row
|
||||
if (target instanceof SceneGridRow) {
|
||||
const targetRow = target.clone({ children: [...target.state.children, newChild] });
|
||||
rootChildren = rootChildren.map((c) => (c === target ? targetRow : c));
|
||||
} else {
|
||||
// target is the main grid
|
||||
rootChildren = [...rootChildren, newChild];
|
||||
}
|
||||
} else {
|
||||
// current parent is the main grid remove it from there
|
||||
rootChildren = rootChildren.filter((c) => c.state.key !== child.state.key);
|
||||
// Clone the target row and add the child
|
||||
const targetRow = target.clone({ children: [...target.state.children, newChild] });
|
||||
// Replace row with new row
|
||||
rootChildren = rootChildren.map((c) => (c === target ? targetRow : c));
|
||||
}
|
||||
|
||||
return rootChildren;
|
||||
}
|
||||
|
||||
public onDragStop: ReactGridLayout.ItemCallback = (gridLayout, o, updatedItem) => {
|
||||
const sceneChild = this.getSceneLayoutChild(updatedItem.i)!;
|
||||
|
||||
// Need to resort the grid layout based on new position (needed to to find the new parent)
|
||||
gridLayout = sortGridLayout(gridLayout);
|
||||
|
||||
// Update children positions if they have changed
|
||||
for (let i = 0; i < gridLayout.length; i++) {
|
||||
const gridItem = gridLayout[i];
|
||||
const child = this.getSceneLayoutChild(gridItem.i)!;
|
||||
const childSize = child.state.size!;
|
||||
|
||||
if (childSize?.x !== gridItem.x || childSize?.y !== gridItem.y) {
|
||||
child.setState({
|
||||
size: {
|
||||
...child.state.size,
|
||||
x: gridItem.x,
|
||||
y: gridItem.y,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the parent if the child if it has moved to a row or back to the grid
|
||||
const indexOfUpdatedItem = gridLayout.findIndex((item) => item.i === updatedItem.i);
|
||||
const newParent = this.findGridItemSceneParent(gridLayout, indexOfUpdatedItem - 1);
|
||||
let newChildren = this.state.children;
|
||||
|
||||
if (newParent !== sceneChild.parent) {
|
||||
newChildren = this.moveChildTo(sceneChild, newParent);
|
||||
}
|
||||
|
||||
this.setState({ children: sortChildrenByPosition(newChildren) });
|
||||
this._skipOnLayoutChange = true;
|
||||
};
|
||||
|
||||
private toGridCell(child: SceneLayoutChild): ReactGridLayout.Layout {
|
||||
const size = child.state.size!;
|
||||
|
||||
let x = size.x ?? 0;
|
||||
let y = size.y ?? 0;
|
||||
const w = Number.isInteger(Number(size.width)) ? Number(size.width) : DEFAULT_PANEL_SPAN;
|
||||
const h = Number.isInteger(Number(size.height)) ? Number(size.height) : DEFAULT_PANEL_SPAN;
|
||||
|
||||
let isDraggable = Boolean(child.state.isDraggable);
|
||||
let isResizable = Boolean(child.state.isResizable);
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
isDraggable = child.state.isCollapsed ? true : false;
|
||||
isResizable = false;
|
||||
}
|
||||
|
||||
return { i: child.state.key!, x, y, h, w, isResizable, isDraggable };
|
||||
}
|
||||
|
||||
public buildGridLayout(width: number): ReactGridLayout.Layout[] {
|
||||
let cells: ReactGridLayout.Layout[] = [];
|
||||
|
||||
for (const child of this.state.children) {
|
||||
cells.push(this.toGridCell(child));
|
||||
|
||||
if (child instanceof SceneGridRow && !child.state.isCollapsed) {
|
||||
for (const rowChild of child.state.children) {
|
||||
cells.push(this.toGridCell(rowChild));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by position
|
||||
cells = sortGridLayout(cells);
|
||||
|
||||
if (width < 768) {
|
||||
// We should not persist the mobile layout
|
||||
this._skipOnLayoutChange = true;
|
||||
return cells.map((cell) => ({ ...cell, w: 24 }));
|
||||
}
|
||||
|
||||
this._skipOnLayoutChange = false;
|
||||
|
||||
return cells;
|
||||
}
|
||||
}
|
||||
|
||||
function SceneGridLayoutRenderer({ model }: SceneComponentProps<SceneGridLayout>) {
|
||||
const { children } = model.useState();
|
||||
validateChildrenSize(children);
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => {
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layout = model.buildGridLayout(width);
|
||||
|
||||
return (
|
||||
/**
|
||||
* The children is using a width of 100% so we need to guarantee that it is wrapped
|
||||
* in an element that has the calculated size given by the AutoSizer. The AutoSizer
|
||||
* has a width of 0 and will let its content overflow its div.
|
||||
*/
|
||||
<div style={{ width: `${width}px`, height: '100%' }}>
|
||||
<ReactGridLayout
|
||||
width={width}
|
||||
/*
|
||||
Disable draggable if mobile device, solving an issue with unintentionally
|
||||
moving panels. https://github.com/grafana/grafana/issues/18497
|
||||
theme.breakpoints.md = 769
|
||||
*/
|
||||
isDraggable={width > 768}
|
||||
isResizable={false}
|
||||
containerPadding={[0, 0]}
|
||||
useCSSTransforms={false}
|
||||
margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]}
|
||||
cols={GRID_COLUMN_COUNT}
|
||||
rowHeight={GRID_CELL_HEIGHT}
|
||||
draggableHandle={`.grid-drag-handle-${model.state.key}`}
|
||||
// @ts-ignore: ignoring for now until we make the size type numbers-only
|
||||
layout={layout}
|
||||
onDragStop={model.onDragStop}
|
||||
onResizeStop={model.onResizeStop}
|
||||
onLayoutChange={model.onLayoutChange}
|
||||
isBounded={false}
|
||||
>
|
||||
{layout.map((gridItem) => {
|
||||
const sceneChild = model.getSceneLayoutChild(gridItem.i)!;
|
||||
return (
|
||||
<div key={sceneChild.state.key} style={{ display: 'flex' }}>
|
||||
<sceneChild.Component model={sceneChild} key={sceneChild.state.key} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ReactGridLayout>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
|
||||
interface SceneGridRowState extends SceneLayoutChildState {
|
||||
title: string;
|
||||
isCollapsible?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
children: Array<SceneObject<SceneLayoutChildState>>;
|
||||
}
|
||||
|
||||
export class SceneGridRow extends SceneObjectBase<SceneGridRowState> {
|
||||
public static Component = SceneGridRowRenderer;
|
||||
|
||||
public constructor(state: SceneGridRowState) {
|
||||
super({
|
||||
isResizable: false,
|
||||
isDraggable: true,
|
||||
isCollapsible: true,
|
||||
...state,
|
||||
size: {
|
||||
...state.size,
|
||||
x: 0,
|
||||
height: 1,
|
||||
width: GRID_COLUMN_COUNT,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public onCollapseToggle = () => {
|
||||
if (!this.state.isCollapsible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layout = this.parent;
|
||||
|
||||
if (!layout || !(layout instanceof SceneGridLayout)) {
|
||||
throw new Error('SceneGridRow must be a child of SceneGridLayout');
|
||||
}
|
||||
|
||||
layout.toggleRow(this);
|
||||
};
|
||||
}
|
||||
|
||||
function SceneGridRowRenderer({ model }: SceneComponentProps<SceneGridRow>) {
|
||||
const styles = useStyles2(getSceneGridRowStyles);
|
||||
const { isCollapsible, isCollapsed, isDraggable, title } = model.useState();
|
||||
const layout = model.getLayout();
|
||||
const dragHandle = <SceneDragHandle layoutKey={layout.state.key!} />;
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<div className={cx(styles.rowHeader, isCollapsed && styles.rowHeaderCollapsed)}>
|
||||
<div onClick={model.onCollapseToggle} className={styles.rowTitleWrapper}>
|
||||
{isCollapsible && <Icon name={isCollapsed ? 'angle-right' : 'angle-down'} />}
|
||||
<span className={styles.rowTitle}>{title}</span>
|
||||
</div>
|
||||
{isDraggable && isCollapsed && <div>{dragHandle}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getSceneGridRowStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
row: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
rowHeader: css({
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '8px',
|
||||
border: `1px solid transparent`,
|
||||
}),
|
||||
rowTitleWrapper: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
rowHeaderCollapsed: css({
|
||||
marginBottom: '0px',
|
||||
background: theme.colors.background.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.borderRadius(1),
|
||||
}),
|
||||
rowTitle: css({
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: theme.typography.h6.fontWeight,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
function validateChildrenSize(children: SceneLayoutChild[]) {
|
||||
if (
|
||||
children.find(
|
||||
(c) =>
|
||||
!c.state.size ||
|
||||
c.state.size.height === undefined ||
|
||||
c.state.size.width === undefined ||
|
||||
c.state.size.x === undefined ||
|
||||
c.state.size.y === undefined
|
||||
)
|
||||
) {
|
||||
throw new Error('All children must have a size specified');
|
||||
}
|
||||
}
|
||||
|
||||
function isItemSizeEqual(a: SceneObjectSize, b: SceneObjectSize) {
|
||||
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
|
||||
}
|
||||
|
||||
function sortChildrenByPosition(children: SceneLayoutChild[]) {
|
||||
return [...children].sort((a, b) => {
|
||||
return a.state.size?.y! - b.state.size?.y! || a.state.size?.x! - b.state.size?.x!;
|
||||
});
|
||||
}
|
||||
|
||||
function sortGridLayout(layout: ReactGridLayout.Layout[]) {
|
||||
return [...layout].sort((a, b) => a.y - b.y || a.x! - b.x);
|
||||
}
|
@ -4,9 +4,13 @@ import { SceneComponentEditingWrapper } from '../editor/SceneComponentEditWrappe
|
||||
|
||||
import { SceneComponentProps, SceneObject } from './types';
|
||||
|
||||
export function SceneComponentWrapper<T extends SceneObject>({ model, isEditing }: SceneComponentProps<T>) {
|
||||
export function SceneComponentWrapper<T extends SceneObject>({
|
||||
model,
|
||||
isEditing,
|
||||
...otherProps
|
||||
}: SceneComponentProps<T>) {
|
||||
const Component = (model as any).constructor['Component'] ?? EmptyRenderer;
|
||||
const inner = <Component model={model} isEditing={isEditing} />;
|
||||
const inner = <Component {...otherProps} model={model} isEditing={isEditing} />;
|
||||
|
||||
// Handle component activation state state
|
||||
useEffect(() => {
|
||||
|
@ -7,7 +7,15 @@ import { useForceUpdate } from '@grafana/ui';
|
||||
|
||||
import { SceneComponentWrapper } from './SceneComponentWrapper';
|
||||
import { SceneObjectStateChangedEvent } from './events';
|
||||
import { SceneDataState, SceneObject, SceneComponent, SceneEditor, SceneTimeRange, SceneObjectState } from './types';
|
||||
import {
|
||||
SceneDataState,
|
||||
SceneObject,
|
||||
SceneComponent,
|
||||
SceneEditor,
|
||||
SceneTimeRange,
|
||||
SceneObjectState,
|
||||
SceneLayoutState,
|
||||
} from './types';
|
||||
|
||||
export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObjectState>
|
||||
implements SceneObject<TState>
|
||||
@ -208,6 +216,21 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
throw new Error('No data found in scene tree');
|
||||
}
|
||||
|
||||
/**
|
||||
* Will walk up the scene object graph to the closest $layout scene object
|
||||
*/
|
||||
public getLayout(): SceneObject<SceneLayoutState> {
|
||||
if (this.constructor.name === 'SceneFlexLayout' || this.constructor.name === 'SceneGridLayout') {
|
||||
return this as SceneObject<SceneLayoutState>;
|
||||
}
|
||||
|
||||
if (this.parent) {
|
||||
return this.parent.getLayout();
|
||||
}
|
||||
|
||||
throw new Error('No layout found in scene tree');
|
||||
}
|
||||
|
||||
/**
|
||||
* Will walk up the scene object graph to the closest $editor scene object
|
||||
*/
|
||||
|
@ -13,9 +13,20 @@ export interface SceneObjectStatePlain {
|
||||
$variables?: SceneVariables;
|
||||
}
|
||||
|
||||
export interface SceneLayoutChildState extends SceneObjectStatePlain {
|
||||
export interface SceneLayoutChildSize {
|
||||
size?: SceneObjectSize;
|
||||
}
|
||||
export interface SceneLayoutChildInteractions {
|
||||
isDraggable?: boolean;
|
||||
isResizable?: boolean;
|
||||
isCollapsible?: boolean;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export interface SceneLayoutChildState
|
||||
extends SceneObjectStatePlain,
|
||||
SceneLayoutChildSize,
|
||||
SceneLayoutChildInteractions {}
|
||||
|
||||
export type SceneObjectState = SceneObjectStatePlain | SceneLayoutState | SceneLayoutChildState;
|
||||
|
||||
@ -84,6 +95,9 @@ export interface SceneObject<TState extends SceneObjectState = SceneObjectState>
|
||||
/** Get the closest node with time range */
|
||||
getTimeRange(): SceneTimeRange;
|
||||
|
||||
/** Get the closest layout node */
|
||||
getLayout(): SceneObject<SceneLayoutState>;
|
||||
|
||||
/** Returns a deep clone this object and all its children */
|
||||
clone(state?: Partial<TState>): this;
|
||||
|
||||
|
@ -2,11 +2,11 @@ import { getDefaultTimeRange } from '@grafana/data';
|
||||
|
||||
import { Scene } from '../components/Scene';
|
||||
import { SceneCanvasText } from '../components/SceneCanvasText';
|
||||
import { SceneFlexLayout } from '../components/SceneFlexLayout';
|
||||
import { ScenePanelRepeater } from '../components/ScenePanelRepeater';
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||
import { SceneToolbarInput } from '../components/SceneToolbarButton';
|
||||
import { VizPanel } from '../components/VizPanel';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { SceneEditManager } from '../editor/SceneEditManager';
|
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
|
||||
@ -18,12 +18,12 @@ export function getFlexLayoutTest(): Scene {
|
||||
direction: 'row',
|
||||
children: [
|
||||
new VizPanel({
|
||||
size: { minWidth: '70%' },
|
||||
pluginId: 'timeseries',
|
||||
title: 'Dynamic height and width',
|
||||
size: { minWidth: '70%' },
|
||||
}),
|
||||
|
||||
new SceneFlexLayout({
|
||||
// size: { width: 450 },
|
||||
direction: 'column',
|
||||
children: [
|
||||
new VizPanel({
|
||||
@ -35,15 +35,15 @@ export function getFlexLayoutTest(): Scene {
|
||||
title: 'Fill height',
|
||||
}),
|
||||
new SceneCanvasText({
|
||||
size: { ySizing: 'content' },
|
||||
text: 'Size to content',
|
||||
fontSize: 20,
|
||||
size: { ySizing: 'content' },
|
||||
align: 'center',
|
||||
}),
|
||||
new VizPanel({
|
||||
size: { height: 300 },
|
||||
pluginId: 'timeseries',
|
||||
title: 'Fixed height',
|
||||
size: { height: 300 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@ -92,6 +92,7 @@ export function getScenePanelRepeaterTest(): Scene {
|
||||
direction: 'column',
|
||||
children: [
|
||||
new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
size: { minHeight: 200 },
|
||||
children: [
|
||||
new VizPanel({
|
||||
|
76
public/app/features/scenes/scenes/grid.tsx
Normal file
76
public/app/features/scenes/scenes/grid.tsx
Normal file
@ -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;
|
||||
}
|
109
public/app/features/scenes/scenes/gridMultiTimeRange.tsx
Normal file
109
public/app/features/scenes/scenes/gridMultiTimeRange.tsx
Normal file
@ -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;
|
||||
}
|
120
public/app/features/scenes/scenes/gridMultiple.tsx
Normal file
120
public/app/features/scenes/scenes/gridMultiple.tsx
Normal file
@ -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;
|
||||
}
|
149
public/app/features/scenes/scenes/gridWithMultipleData.tsx
Normal file
149
public/app/features/scenes/scenes/gridWithMultipleData.tsx
Normal file
@ -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;
|
||||
}
|
97
public/app/features/scenes/scenes/gridWithRow.tsx
Normal file
97
public/app/features/scenes/scenes/gridWithRow.tsx
Normal file
@ -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;
|
||||
}
|
102
public/app/features/scenes/scenes/gridWithRows.tsx
Normal file
102
public/app/features/scenes/scenes/gridWithRows.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { getDefaultTimeRange } from '@grafana/data';
|
||||
|
||||
import { Scene } from '../components/Scene';
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||
import { VizPanel } from '../components/VizPanel';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { SceneEditManager } from '../editor/SceneEditManager';
|
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
|
||||
|
||||
export function getGridWithRowsTest(): Scene {
|
||||
const panel = new VizPanel({
|
||||
pluginId: 'timeseries',
|
||||
title: 'Fill height',
|
||||
});
|
||||
|
||||
const row1 = new SceneGridRow({
|
||||
title: 'Collapsible/draggable row with flex layout',
|
||||
size: { x: 0, y: 0, height: 10 },
|
||||
children: [
|
||||
new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
children: [
|
||||
new VizPanel({
|
||||
pluginId: 'timeseries',
|
||||
title: 'Fill height',
|
||||
}),
|
||||
new VizPanel({
|
||||
pluginId: 'timeseries',
|
||||
title: 'Fill height',
|
||||
}),
|
||||
new VizPanel({
|
||||
pluginId: 'timeseries',
|
||||
title: 'Fill height',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const cell1 = new VizPanel({
|
||||
size: {
|
||||
x: 0,
|
||||
y: 10,
|
||||
width: 12,
|
||||
height: 20,
|
||||
},
|
||||
pluginId: 'timeseries',
|
||||
title: 'Cell 1',
|
||||
});
|
||||
|
||||
const cell2 = new VizPanel({
|
||||
isResizable: false,
|
||||
isDraggable: false,
|
||||
size: { x: 12, y: 20, width: 12, height: 10 },
|
||||
pluginId: 'timeseries',
|
||||
title: 'No resize/no drag',
|
||||
});
|
||||
|
||||
const row2 = new SceneGridRow({
|
||||
size: { x: 12, y: 10, height: 10, width: 12 },
|
||||
title: 'Row with a nested flex layout',
|
||||
children: [
|
||||
new SceneFlexLayout({
|
||||
children: [
|
||||
new SceneFlexLayout({
|
||||
direction: 'column',
|
||||
children: [panel, panel],
|
||||
}),
|
||||
new SceneFlexLayout({
|
||||
direction: 'column',
|
||||
children: [panel, panel],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
const scene = new Scene({
|
||||
title: 'Grid rows test',
|
||||
layout: new SceneGridLayout({
|
||||
children: [cell1, cell2, row1, row2],
|
||||
}),
|
||||
$editor: new SceneEditManager({}),
|
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
uid: 'gdev-testdata',
|
||||
type: 'testdata',
|
||||
},
|
||||
scenarioId: 'random_walk',
|
||||
},
|
||||
],
|
||||
}),
|
||||
actions: [new SceneTimePicker({})],
|
||||
});
|
||||
|
||||
return scene;
|
||||
}
|
@ -1,12 +1,28 @@
|
||||
import { Scene } from '../components/Scene';
|
||||
|
||||
import { getFlexLayoutTest, getScenePanelRepeaterTest } from './demo';
|
||||
import { getGridLayoutTest } from './grid';
|
||||
import { getGridWithMultipleTimeRanges } from './gridMultiTimeRange';
|
||||
import { getMultipleGridLayoutTest } from './gridMultiple';
|
||||
import { getGridWithMultipleData } from './gridWithMultipleData';
|
||||
import { getGridWithRowLayoutTest } from './gridWithRow';
|
||||
import { getNestedScene } from './nested';
|
||||
import { getSceneWithRows } from './sceneWithRows';
|
||||
import { getVariablesDemo } from './variablesDemo';
|
||||
|
||||
export function getScenes(): Scene[] {
|
||||
return [getFlexLayoutTest(), getScenePanelRepeaterTest(), getNestedScene(), getSceneWithRows(), getVariablesDemo()];
|
||||
return [
|
||||
getFlexLayoutTest(),
|
||||
getScenePanelRepeaterTest(),
|
||||
getNestedScene(),
|
||||
getSceneWithRows(),
|
||||
getGridLayoutTest(),
|
||||
getGridWithRowLayoutTest(),
|
||||
getGridWithMultipleData(),
|
||||
getGridWithMultipleTimeRanges(),
|
||||
getMultipleGridLayoutTest(),
|
||||
getVariablesDemo(),
|
||||
];
|
||||
}
|
||||
|
||||
const cache: Record<string, Scene> = {};
|
||||
|
@ -2,9 +2,9 @@ import { getDefaultTimeRange } from '@grafana/data';
|
||||
|
||||
import { NestedScene } from '../components/NestedScene';
|
||||
import { Scene } from '../components/Scene';
|
||||
import { SceneFlexLayout } from '../components/SceneFlexLayout';
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||
import { VizPanel } from '../components/VizPanel';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
|
||||
|
||||
@ -14,12 +14,12 @@ export function getNestedScene(): Scene {
|
||||
layout: new SceneFlexLayout({
|
||||
direction: 'column',
|
||||
children: [
|
||||
getInnerScene('Inner scene'),
|
||||
new VizPanel({
|
||||
key: '3',
|
||||
pluginId: 'timeseries',
|
||||
title: 'Panel 3',
|
||||
}),
|
||||
getInnerScene('Inner scene'),
|
||||
],
|
||||
}),
|
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
|
||||
@ -45,6 +45,7 @@ export function getInnerScene(title: string) {
|
||||
const scene = new NestedScene({
|
||||
title: title,
|
||||
canRemove: true,
|
||||
canCollapse: true,
|
||||
layout: new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
children: [
|
||||
|
@ -2,9 +2,9 @@ import { getDefaultTimeRange } from '@grafana/data';
|
||||
|
||||
import { NestedScene } from '../components/NestedScene';
|
||||
import { Scene } from '../components/Scene';
|
||||
import { SceneFlexLayout } from '../components/SceneFlexLayout';
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||
import { VizPanel } from '../components/VizPanel';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { SceneEditManager } from '../editor/SceneEditManager';
|
||||
|
||||
@ -19,6 +19,7 @@ export function getSceneWithRows(): Scene {
|
||||
new NestedScene({
|
||||
title: 'Overview',
|
||||
canCollapse: true,
|
||||
// size: { ySizing: 'content', xSizing: 'fill' },
|
||||
layout: new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
children: [
|
||||
@ -35,6 +36,7 @@ export function getSceneWithRows(): Scene {
|
||||
}),
|
||||
new NestedScene({
|
||||
title: 'More server details',
|
||||
// size: { ySizing: 'content', xSizing: 'fill' },
|
||||
canCollapse: true,
|
||||
layout: new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
|
@ -2,9 +2,9 @@ import { getDefaultTimeRange } from '@grafana/data';
|
||||
|
||||
import { Scene } from '../components/Scene';
|
||||
import { SceneCanvasText } from '../components/SceneCanvasText';
|
||||
import { SceneFlexLayout } from '../components/SceneFlexLayout';
|
||||
import { SceneSubMenu } from '../components/SceneSubMenu';
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
|
||||
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
||||
|
Loading…
Reference in New Issue
Block a user