import * as H from 'history'; import { NavIndex } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { SceneGridItem, SceneGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; import { getPanelIdForVizPanel, getDashboardSceneFor } from '../utils/utils'; import { PanelDataPane } from './PanelDataPane/PanelDataPane'; import { PanelEditorRenderer } from './PanelEditorRenderer'; import { PanelOptionsPane } from './PanelOptionsPane'; import { VizPanelManager, VizPanelManagerState } from './VizPanelManager'; export interface PanelEditorState extends SceneObjectState { isDirty?: boolean; panelId: number; optionsPane: PanelOptionsPane; dataPane?: PanelDataPane; vizManager: VizPanelManager; } export class PanelEditor extends SceneObjectBase { private _initialRepeatOptions: Pick = {}; static Component = PanelEditorRenderer; private _discardChanges = false; public constructor(state: PanelEditorState) { super(state); const { repeat, repeatDirection, maxPerRow } = state.vizManager.state; this._initialRepeatOptions = { repeat, repeatDirection, maxPerRow, }; this.addActivationHandler(this._activationHandler.bind(this)); } private _activationHandler() { const panelManager = this.state.vizManager; const panel = panelManager.state.panel; this._subs.add( panelManager.subscribeToState((n, p) => { if (n.panel.state.pluginId !== p.panel.state.pluginId) { this._initDataPane(n.panel.state.pluginId); } }) ); this._initDataPane(panel.state.pluginId); return () => { if (!this._discardChanges) { this.commitChanges(); } }; } private _initDataPane(pluginId: string) { const skipDataQuery = config.panels[pluginId].skipDataQuery; if (skipDataQuery && this.state.dataPane) { locationService.partial({ tab: null }, true); this.setState({ dataPane: undefined }); } if (!skipDataQuery && !this.state.dataPane) { this.setState({ dataPane: new PanelDataPane(this.state.vizManager) }); } } public getUrlKey() { return this.state.panelId.toString(); } public getPageNav(location: H.Location, navIndex: NavIndex) { const dashboard = getDashboardSceneFor(this); return { text: 'Edit panel', parentItem: dashboard.getPageNav(location, navIndex), }; } public onDiscard = () => { this._discardChanges = true; locationService.partial({ editPanel: null }); }; public commitChanges() { const dashboard = getDashboardSceneFor(this); if (!dashboard.state.isEditing) { dashboard.onEnterEditMode(); } const panelManager = this.state.vizManager; const sourcePanel = panelManager.state.sourcePanel.resolve(); const sourcePanelParent = sourcePanel!.parent; const normalToRepeat = !this._initialRepeatOptions.repeat && panelManager.state.repeat; const repeatToNormal = this._initialRepeatOptions.repeat && !panelManager.state.repeat; if (sourcePanelParent instanceof SceneGridItem) { if (normalToRepeat) { this.replaceSceneGridItemWithPanelRepeater(sourcePanelParent); } else { panelManager.commitChanges(); } } else if (sourcePanelParent instanceof PanelRepeaterGridItem) { if (repeatToNormal) { this.replacePanelRepeaterWithGridItem(sourcePanelParent); } else { this.handleRepeatOptionChanges(sourcePanelParent); } } else { console.error('Unsupported scene object type'); } } private replaceSceneGridItemWithPanelRepeater(gridItem: SceneGridItem) { const gridLayout = gridItem.parent; if (!(gridLayout instanceof SceneGridLayout)) { console.error('Expected grandparent to be SceneGridLayout!'); return; } const panelManager = this.state.vizManager; const repeatDirection = panelManager.state.repeatDirection ?? 'h'; const repeater = new PanelRepeaterGridItem({ key: gridItem.state.key, x: gridItem.state.x, y: gridItem.state.y, width: repeatDirection === 'h' ? 24 : gridItem.state.width, height: gridItem.state.height, itemHeight: gridItem.state.height, source: panelManager.getPanelCloneWithData(), variableName: panelManager.state.repeat!, repeatedPanels: [], repeatDirection: panelManager.state.repeatDirection, maxPerRow: panelManager.state.maxPerRow, }); gridLayout.setState({ children: gridLayout.state.children.map((child) => (child.state.key === gridItem.state.key ? repeater : child)), }); } private replacePanelRepeaterWithGridItem(panelRepeater: PanelRepeaterGridItem) { const gridLayout = panelRepeater.parent; if (!(gridLayout instanceof SceneGridLayout)) { console.error('Expected grandparent to be SceneGridLayout!'); return; } const panelManager = this.state.vizManager; const panelClone = panelManager.getPanelCloneWithData(); const gridItem = new SceneGridItem({ key: panelRepeater.state.key, x: panelRepeater.state.x, y: panelRepeater.state.y, width: this._initialRepeatOptions.repeatDirection === 'h' ? 8 : panelRepeater.state.width, height: this._initialRepeatOptions.repeatDirection === 'v' ? 8 : panelRepeater.state.height, body: panelClone, }); gridLayout.setState({ children: gridLayout.state.children.map((child) => child.state.key === panelRepeater.state.key ? gridItem : child ), }); } private handleRepeatOptionChanges(panelRepeater: PanelRepeaterGridItem) { let width = panelRepeater.state.width ?? 1; let height = panelRepeater.state.height; const panelManager = this.state.vizManager; const horizontalToVertical = this._initialRepeatOptions.repeatDirection === 'h' && panelManager.state.repeatDirection === 'v'; const verticalToHorizontal = this._initialRepeatOptions.repeatDirection === 'v' && panelManager.state.repeatDirection === 'h'; if (horizontalToVertical) { width = Math.floor(width / (panelRepeater.state.maxPerRow ?? 1)); } else if (verticalToHorizontal) { width = 24; } panelRepeater.setState({ source: panelManager.getPanelCloneWithData(), repeatDirection: panelManager.state.repeatDirection, variableName: panelManager.state.repeat, maxPerRow: panelManager.state.maxPerRow, width, height, }); } } export function buildPanelEditScene(panel: VizPanel): PanelEditor { return new PanelEditor({ panelId: getPanelIdForVizPanel(panel), optionsPane: new PanelOptionsPane({}), vizManager: VizPanelManager.createFor(panel), }); }