mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
1dbbbd9ca7
commit
b700de8122
@ -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 |
|
||||
|
@ -140,6 +140,7 @@ export interface FeatureToggles {
|
||||
dashboardSceneForViewers?: boolean;
|
||||
dashboardSceneSolo?: boolean;
|
||||
dashboardScene?: boolean;
|
||||
dashboardNewLayouts?: boolean;
|
||||
panelFilterVariable?: boolean;
|
||||
pdfTables?: boolean;
|
||||
ssoSettingsApi?: boolean;
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
@ -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": {
|
||||
|
@ -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 })],
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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} />;
|
||||
};
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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()];
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user