Dashboard: New layouts feature toggle and basic skeleton for a responsive grid layout (#94805)

* Wip

* Update

* Fix adding new panels

* reuse fromVizPanels

* Fixes

* Update

* Update

* Update

* Fixes

* Update
This commit is contained in:
Torkel Ödegaard 2024-10-23 10:55:45 +02:00 committed by GitHub
parent 1dbbbd9ca7
commit b700de8122
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 514 additions and 46 deletions

View File

@ -179,6 +179,7 @@ Experimental features might be changed or removed without prior notice.
| `alertmanagerRemotePrimary` | Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager. |
| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. |
| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe |
| `dashboardNewLayouts` | Enables experimental new dashboard layouts |
| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes |
| `tableSharedCrosshair` | Enables shared crosshair in table panel |
| `kubernetesFeatureToggles` | Use the kubernetes API for feature toggle management in the frontend |

View File

@ -140,6 +140,7 @@ export interface FeatureToggles {
dashboardSceneForViewers?: boolean;
dashboardSceneSolo?: boolean;
dashboardScene?: boolean;
dashboardNewLayouts?: boolean;
panelFilterVariable?: boolean;
pdfTables?: boolean;
ssoSettingsApi?: boolean;

View File

@ -913,6 +913,13 @@ var (
Owner: grafanaDashboardsSquad,
Expression: "true", // enabled by default
},
{
Name: "dashboardNewLayouts",
Description: "Enables experimental new dashboard layouts",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "panelFilterVariable",
Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard",

View File

@ -121,6 +121,7 @@ extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,t
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true
dashboardScene,GA,@grafana/dashboards-squad,false,false,true
dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,true
panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true
pdfTables,preview,@grafana/sharing-squad,false,false,false
ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
121 dashboardSceneForViewers GA @grafana/dashboards-squad false false true
122 dashboardSceneSolo GA @grafana/dashboards-squad false false true
123 dashboardScene GA @grafana/dashboards-squad false false true
124 dashboardNewLayouts experimental @grafana/dashboards-squad false false true
125 panelFilterVariable experimental @grafana/dashboards-squad false false true
126 pdfTables preview @grafana/sharing-squad false false false
127 ssoSettingsApi GA @grafana/identity-access-team false false false

View File

@ -495,6 +495,10 @@ const (
// Enables dashboard rendering using scenes for all roles
FlagDashboardScene = "dashboardScene"
// FlagDashboardNewLayouts
// Enables experimental new dashboard layouts
FlagDashboardNewLayouts = "dashboardNewLayouts"
// FlagPanelFilterVariable
// Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard
FlagPanelFilterVariable = "panelFilterVariable"

View File

@ -820,6 +820,22 @@
"expression": "true"
}
},
{
"metadata": {
"name": "dashboardNewLayouts",
"resourceVersion": "1729671312626",
"creationTimestamp": "2024-10-16T08:44:05Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-10-23 08:15:12.626632 +0000 UTC"
}
},
"spec": {
"description": "Enables experimental new dashboard layouts",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"frontend": true
}
},
{
"metadata": {
"name": "dashboardRestore",
@ -857,10 +873,10 @@
{
"metadata": {
"name": "dashboardScene",
"resourceVersion": "1727354524763",
"resourceVersion": "1729671397794",
"creationTimestamp": "2023-11-13T08:51:21Z",
"annotations": {
"grafana.app/updatedTimestamp": "2024-09-26 12:42:04.763233 +0000 UTC"
"grafana.app/updatedTimestamp": "2024-10-23 08:16:37.794144 +0000 UTC"
}
},
"spec": {

View File

@ -23,8 +23,7 @@ export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawer
public onAddLibraryPanel = (panelInfo: LibraryPanel) => {
const dashboard = getDashboardSceneFor(this);
const newPanel = getDefaultVizPanel(dashboard);
const newPanel = getDefaultVizPanel();
newPanel.setState({
$behaviors: [new LibraryPanelBehavior({ uid: panelInfo.uid, name: panelInfo.name })],

View File

@ -55,7 +55,6 @@ import {
getDashboardSceneFor,
getDefaultVizPanel,
getPanelIdForVizPanel,
getVizPanelKeyForPanelId,
isPanelClone,
} from '../utils/utils';
@ -460,10 +459,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
this.onEnterEditMode();
}
const panelId = dashboardSceneGraph.getNextPanelId(this);
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
vizPanel.clearParent();
this.state.body.addPanel(vizPanel);
}
@ -508,12 +503,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
const jsonObj = JSON.parse(jsonData);
const panelModel = new PanelModel(jsonObj);
const gridItem = buildGridItemForPanel(panelModel);
const panelId = dashboardSceneGraph.getNextPanelId(this);
const panel = gridItem.state.body;
panel.setState({ key: getVizPanelKeyForPanelId(panelId) });
panel.clearParent();
this.addPanel(panel);
store.delete(LS_PANEL_COPY_KEY);
@ -580,13 +571,17 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}
public onCreateNewPanel(): VizPanel {
const vizPanel = getDefaultVizPanel(this);
const vizPanel = getDefaultVizPanel();
this.addPanel(vizPanel);
return vizPanel;
}
public switchLayout(layout: DashboardLayoutManager) {
this.setState({ body: layout });
}
/**
* Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request
*/

View File

@ -47,14 +47,13 @@ describe('DefaultGridLayoutManager', () => {
const vizPanel = new VizPanel({
title: 'Panel Title',
key: 'panel-55',
pluginId: 'timeseries',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
});
manager.addPanel(vizPanel);
const panel = findVizPanelByKey(manager, 'panel-55')!;
const panel = findVizPanelByKey(manager, vizPanel.state.key)!;
const gridItem = panel.parent as DashboardGridItem;
expect(panel).toBeDefined();

View File

@ -1,3 +1,4 @@
import { config } from '@grafana/runtime';
import {
SceneObjectState,
SceneGridLayout,
@ -8,19 +9,24 @@ import {
sceneUtils,
SceneComponentProps,
} from '@grafana/scenes';
import { Button } from '@grafana/ui';
import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { Trans } from 'app/core/internationalization';
import { DashboardInteractions } from '../../utils/interactions';
import {
forceRenderChildren,
getPanelIdForVizPanel,
NEW_PANEL_HEIGHT,
NEW_PANEL_WIDTH,
getVizPanelKeyForPanelId,
getDefaultVizPanel,
} from '../../utils/utils';
import { DashboardGridItem } from '../DashboardGridItem';
import { RowRepeaterBehavior } from '../RowRepeaterBehavior';
import { LayoutEditChrome } from '../layouts-shared/LayoutEditChrome';
import { RowActions } from '../row-actions/RowActions';
import { DashboardLayoutManager } from '../types';
import { DashboardLayoutManager, LayoutEditorProps, LayoutRegistryItem } from '../types';
interface DefaultGridLayoutManagerState extends SceneObjectState {
grid: SceneGridLayout;
@ -38,17 +44,12 @@ export class DefaultGridLayoutManager
forceRenderChildren(this.state.grid, true);
}
/**
* Removes the first panel
*/
public cleanUpStateFromExplore(): void {
this.state.grid.setState({
children: this.state.grid.state.children.slice(1),
});
}
public addPanel(vizPanel: VizPanel): void {
const panelId = getPanelIdForVizPanel(vizPanel);
const panelId = this.getNextPanelId();
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
vizPanel.clearParent();
const newGridItem = new DashboardGridItem({
height: NEW_PANEL_HEIGHT,
width: NEW_PANEL_WIDTH,
@ -309,6 +310,29 @@ export class DefaultGridLayoutManager
});
}
public getDescriptor(): LayoutRegistryItem {
return DefaultGridLayoutManager.getDescriptor();
}
public static getDescriptor(): LayoutRegistryItem {
return {
name: 'Default grid',
description: 'The default grid layout',
id: 'default-grid',
createFromLayout: DefaultGridLayoutManager.createFromLayout,
};
}
/**
* Handle switching to the manual grid layout from other layouts
* @param currentLayout
* @returns
*/
public static createFromLayout(currentLayout: DashboardLayoutManager): DefaultGridLayoutManager {
const panels = currentLayout.getVizPanels();
return DefaultGridLayoutManager.fromVizPanels(panels);
}
/**
* For simple test grids
* @param panels
@ -321,6 +345,8 @@ export class DefaultGridLayoutManager
let currentX = 0;
for (let panel of panels) {
panel.clearParent();
children.push(
new DashboardGridItem({
key: `griditem-${getPanelIdForVizPanel(panel)}`,
@ -349,7 +375,48 @@ export class DefaultGridLayoutManager
});
}
public renderEditor() {
return <DefaultGridLayoutEditor layoutManager={this} />;
}
public static Component = ({ model }: SceneComponentProps<DefaultGridLayoutManager>) => {
return <model.state.grid.Component model={model.state.grid} />;
if (!config.featureToggles.dashboardNewLayouts) {
return <model.state.grid.Component model={model.state.grid} />;
}
return (
<LayoutEditChrome layoutManager={model}>
<model.state.grid.Component model={model.state.grid} />
</LayoutEditChrome>
);
};
}
function DefaultGridLayoutEditor({ layoutManager }: LayoutEditorProps<DefaultGridLayoutManager>) {
return (
<>
<Button
fill="outline"
icon="plus"
onClick={() => {
const vizPanel = getDefaultVizPanel();
layoutManager.addPanel(vizPanel);
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
}}
>
<Trans i18nKey="dashboard.add-menu.visualization">Visualization</Trans>
</Button>
<Button
fill="outline"
icon="plus"
onClick={() => {
layoutManager.addNewRow!();
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' });
}}
>
<Trans i18nKey="dashboard.add-menu.row">Row</Trans>
</Button>
</>
);
}

View File

@ -0,0 +1,62 @@
import { SceneObjectState, VizPanel, SceneObjectBase, SceneObject, SceneComponentProps } from '@grafana/scenes';
import { Switch } from '@grafana/ui';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
export interface ResponsiveGridItemState extends SceneObjectState {
body: VizPanel;
hideWhenNoData?: boolean;
}
export class ResponsiveGridItem extends SceneObjectBase<ResponsiveGridItemState> {
public constructor(state: ResponsiveGridItemState) {
super(state);
this.addActivationHandler(() => this._activationHandler());
}
private _activationHandler() {
if (!this.state.hideWhenNoData) {
return;
}
// TODO add hide when no data logic (in a behavior probably)
}
public toggleHideWhenNoData() {
this.setState({ hideWhenNoData: !this.state.hideWhenNoData });
}
/**
* DashboardLayoutElement interface
*/
public isDashboardLayoutElement: true = true;
public getOptions?(): OptionsPaneItemDescriptor[] {
const model = this;
return [
new OptionsPaneItemDescriptor({
title: 'Hide when no data',
render: function renderTransparent() {
const { hideWhenNoData } = model.state;
return <Switch value={hideWhenNoData} id="hide-when-no-data" onChange={() => model.toggleHideWhenNoData()} />;
},
}),
];
}
public setBody(body: SceneObject): void {
if (body instanceof VizPanel) {
this.setState({ body });
}
}
public getVizPanel() {
return this.state.body;
}
public static Component = ({ model }: SceneComponentProps<ResponsiveGridItem>) => {
const { body } = model.useState();
return <body.Component model={body} />;
};
}

View File

@ -0,0 +1,181 @@
import { SelectableValue } from '@grafana/data';
import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { Button, Field, Select } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DashboardInteractions } from '../../utils/interactions';
import { getDefaultVizPanel, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
import { LayoutEditChrome } from '../layouts-shared/LayoutEditChrome';
import { DashboardLayoutManager, LayoutRegistryItem, LayoutEditorProps } from '../types';
import { ResponsiveGridItem } from './ResponsiveGridItem';
interface ResponsiveGridLayoutManagerState extends SceneObjectState {
layout: SceneCSSGridLayout;
}
export class ResponsiveGridLayoutManager
extends SceneObjectBase<ResponsiveGridLayoutManagerState>
implements DashboardLayoutManager
{
public editModeChanged(isEditing: boolean): void {}
public addPanel(vizPanel: VizPanel): void {
const panelId = this.getNextPanelId();
vizPanel.setState({ key: getVizPanelKeyForPanelId(panelId) });
vizPanel.clearParent();
this.state.layout.setState({
children: [new ResponsiveGridItem({ body: vizPanel }), ...this.state.layout.state.children],
});
}
public addNewRow(): void {
throw new Error('Method not implemented.');
}
public getNextPanelId(): number {
let max = 0;
for (const child of this.state.layout.state.children) {
if (child instanceof VizPanel) {
let panelId = getPanelIdForVizPanel(child);
if (panelId > max) {
max = panelId;
}
}
}
return max;
}
public removePanel(panel: VizPanel) {
const element = panel.parent;
this.state.layout.setState({ children: this.state.layout.state.children.filter((child) => child !== element) });
}
public duplicatePanel(panel: VizPanel): void {
throw new Error('Method not implemented.');
}
public getVizPanels(): VizPanel[] {
const panels: VizPanel[] = [];
for (const child of this.state.layout.state.children) {
if (child instanceof ResponsiveGridItem) {
panels.push(child.state.body);
}
}
return panels;
}
public renderEditor() {
return <AutomaticGridEditor layoutManager={this} />;
}
public getDescriptor(): LayoutRegistryItem {
return ResponsiveGridLayoutManager.getDescriptor();
}
public static getDescriptor(): LayoutRegistryItem {
return {
name: 'Responsive grid',
description: 'CSS layout that adjusts to the available space',
id: 'responsive-grid',
createFromLayout: ResponsiveGridLayoutManager.createFromLayout,
};
}
public static createEmpty() {
return new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ children: [] }) });
}
public static createFromLayout(layout: DashboardLayoutManager): ResponsiveGridLayoutManager {
const panels = layout.getVizPanels();
const children: ResponsiveGridItem[] = [];
for (let panel of panels) {
children.push(new ResponsiveGridItem({ body: panel.clone() }));
}
return new ResponsiveGridLayoutManager({
layout: new SceneCSSGridLayout({
children,
templateColumns: 'repeat(auto-fit, minmax(400px, auto))',
autoRows: 'minmax(300px, auto)',
}),
});
}
public static Component = ({ model }: SceneComponentProps<ResponsiveGridLayoutManager>) => {
return (
<LayoutEditChrome layoutManager={model}>
<model.state.layout.Component model={model.state.layout} />
</LayoutEditChrome>
);
};
}
function AutomaticGridEditor({ layoutManager }: LayoutEditorProps<ResponsiveGridLayoutManager>) {
const cssLayout = layoutManager.state.layout;
const { templateColumns, autoRows } = cssLayout.useState();
const rowOptions: Array<SelectableValue<string>> = [];
const sizes = [100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 650];
const colOptions: Array<SelectableValue<string>> = [
{ label: `1 column`, value: `1fr` },
{ label: `2 columns`, value: `1fr 1fr` },
{ label: `3 columns`, value: `1fr 1fr 1fr` },
];
for (const size of sizes) {
colOptions.push({ label: `Min: ${size}px`, value: `repeat(auto-fit, minmax(${size}px, auto))` });
}
for (const size of sizes) {
rowOptions.push({ label: `Min: ${size}px`, value: `minmax(${size}px, auto)` });
}
for (const size of sizes) {
rowOptions.push({ label: `Fixed: ${size}px`, value: `${size}px` });
}
const onColumnsChange = (value: SelectableValue<string>) => {
cssLayout.setState({ templateColumns: value.value });
};
const onRowsChange = (value: SelectableValue<string>) => {
cssLayout.setState({ autoRows: value.value });
};
return (
<>
<Field label="Columns">
<Select
options={colOptions}
value={String(templateColumns)}
onChange={onColumnsChange}
allowCustomValue={true}
/>
</Field>
<Field label="Row height">
<Select options={rowOptions} value={String(autoRows)} onChange={onRowsChange} />
</Field>
<Button
fill="outline"
icon="plus"
onClick={() => {
const vizPanel = getDefaultVizPanel();
layoutManager.addPanel(vizPanel);
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
}}
>
<Trans i18nKey="dashboard.add-menu.visualization">Visualization</Trans>
</Button>
</>
);
}

View File

@ -0,0 +1,106 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Field, Select } from '@grafana/ui';
import { getDashboardSceneFor } from '../../utils/utils';
import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types';
import { layoutRegistry } from './layoutRegistry';
interface Props {
layoutManager: DashboardLayoutManager;
children: React.ReactNode;
}
export function LayoutEditChrome({ layoutManager, children }: Props) {
const styles = useStyles2(getStyles);
const { isEditing } = getDashboardSceneFor(layoutManager).useState();
const layouts = layoutRegistry.list();
const options = layouts.map((layout) => ({
label: layout.name,
value: layout,
}));
const currentLayoutId = layoutManager.getDescriptor().id;
const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId);
return (
<div className={styles.wrapper}>
{isEditing && (
<div className={styles.editHeader}>
<Field label="Layout type">
<Select
options={options}
value={currentLayoutOption}
onChange={(option) => changeLayoutTo(layoutManager, option.value!)}
/>
</Field>
{layoutManager.renderEditor?.()}
</div>
)}
{children}
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
editHeader: css({
width: '100%',
display: 'flex',
gap: theme.spacing(1),
padding: theme.spacing(0, 1, 0.5, 1),
margin: theme.spacing(0, 0, 1, 0),
alignItems: 'flex-end',
borderBottom: `1px solid ${theme.colors.border.weak}`,
paddingBottom: theme.spacing(1),
'&:hover, &:focus-within': {
'& > div': {
opacity: 1,
},
},
'& > div': {
marginBottom: 0,
marginRight: theme.spacing(1),
},
}),
wrapper: css({
display: 'flex',
flexDirection: 'column',
flex: '1 1 0',
width: '100%',
minHeight: 0,
}),
icon: css({
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
background: 'transparent',
border: 'none',
gap: theme.spacing(1),
}),
rowTitle: css({}),
rowActions: css({
display: 'flex',
opacity: 0,
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: 'opacity 200ms ease-in',
},
'&:hover, &:focus-within': {
opacity: 1,
},
}),
};
}
function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) {
const layoutParent = currentLayout.parent;
if (layoutParent && isLayoutParent(layoutParent)) {
layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout));
}
}

View File

@ -0,0 +1,9 @@
import { Registry } from '@grafana/data';
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
import { LayoutRegistryItem } from '../types';
export const layoutRegistry: Registry<LayoutRegistryItem> = new Registry<LayoutRegistryItem>(() => {
return [DefaultGridLayoutManager.getDescriptor(), ResponsiveGridLayoutManager.getDescriptor()];
});

View File

@ -1,5 +1,6 @@
import { BusEventWithPayload, RegistryItem } from '@grafana/data';
import { SceneObject, VizPanel } from '@grafana/scenes';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
/**
* A scene object that usually wraps an underlying layout
@ -11,18 +12,12 @@ export interface DashboardLayoutManager extends SceneObject {
* @param isEditing
*/
editModeChanged(isEditing: boolean): void;
/**
* We should be able to figure out how to add the explore panel in a way that leaves the
* initialSaveModel clean from it so we can leverage the default discard changes logic.
* Then we can get rid of this.
*/
cleanUpStateFromExplore?(): void;
/**
* Not sure we will need this in the long run, we should be able to handle this inside internally
*/
getNextPanelId(): number;
/**
* Remove an elemenet / panel
* Remove an element / panel
* @param element
*/
removePanel(panel: VizPanel): void;
@ -52,6 +47,14 @@ export interface DashboardLayoutManager extends SceneObject {
* For dynamic panels that need to be viewed in isolation (SoloRoute)
*/
activateRepeaters?(): void;
/**
* Get's the layout descriptor (which has the name and id)
*/
getDescriptor(): LayoutRegistryItem;
/**
* Renders options and layout actions
*/
renderEditor?(): React.ReactNode;
}
/**
@ -85,6 +88,33 @@ export function isLayoutParent(obj: SceneObject): obj is LayoutParent {
return 'switchLayout' in obj;
}
/**
* Abstraction to handle editing of different layout elements (wrappers for VizPanels and other objects)
* Also useful to when rendering / viewing an element outside it's layout scope
*/
export interface DashboardLayoutElement extends SceneObject {
/**
* Marks this object as a layout element
*/
isDashboardLayoutElement: true;
/**
* Return layout elements options (like repeat, repeat direction, etc for the default DashboardGridItem)
*/
getOptions?(): OptionsPaneItemDescriptor[];
/**
* Used by panel edit to commit changes
*/
setBody(body: SceneObject): void;
/**
* Only implemented by elements that wrap VizPanels
*/
getVizPanel?(): VizPanel;
}
export function isDashboardLayoutElement(obj: SceneObject): obj is DashboardLayoutElement {
return 'isDashboardLayoutElement' in obj;
}
export interface DashboardRepeatsProcessedEventPayload {
source: SceneObject;
}

View File

@ -46,16 +46,11 @@ export function getCursorSync(scene: DashboardScene) {
return;
}
export function getNextPanelId(dashboard: DashboardScene): number {
return dashboard.state.body.getNextPanelId();
}
export const dashboardSceneGraph = {
getTimePicker,
getRefreshPicker,
getPanelLinks,
getVizPanels,
getDataLayers,
getNextPanelId,
getCursorSync,
};

View File

@ -19,8 +19,6 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { dashboardSceneGraph } from './dashboardSceneGraph';
export const NEW_PANEL_HEIGHT = 8;
export const NEW_PANEL_WIDTH = 12;
@ -216,12 +214,9 @@ export function isPanelClone(key: string) {
return key.includes('clone');
}
export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel {
const panelId = dashboardSceneGraph.getNextPanelId(dashboard);
export function getDefaultVizPanel(): VizPanel {
return new VizPanel({
title: 'Panel Title',
key: getVizPanelKeyForPanelId(panelId),
pluginId: 'timeseries',
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
hoverHeaderOffset: 0,