Files
grafana/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
Oscar Kilhed 0b2640e9ff Dashboard scenes: Editing library panels. (#83223)
* wip

* Refactor find panel by key

* clean up lint, make isLoading optional

* change library panel so that the dashboard key is attached to the panel instead of the library panel

* do not reload everything when the library panel is already loaded

* Progress on library panel options in options pane

* We can skip building the edit scene until we have the library panel loaded

* undo changes to findLibraryPanelbyKey, changes not necessary when the panel has the findable id instead of the library panel

* fix undo

* make sure the save model gets the id from the panel and not the library panel

* remove non necessary links and data providers from dummy loading panel

* change library panel so that the dashboard key is attached to the panel instead of the library panel

* make sure the save model gets the id from the panel and not the library panel

* do not reload everything when the library panel is already loaded

* Fix merge issue

* Clean up

* lint cleanup

* wip saving

* working save

* use title from panel model

* move library panel api functions

* fix issue from merge

* Add confirm save modal. Update library panel to response from save request. Add library panel information box to panel options

* Better naming

* Remove library panel from viz panel state, use sourcePanel.parent instead. Fix edited by time formatting

* Add tests for editing library panels

* implement changed from review feedback

* minor refactor from feedback
2024-03-11 20:48:27 +01:00

227 lines
7.3 KiB
TypeScript

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 { LibraryVizPanel } from '../scene/LibraryVizPanel';
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
import { getDashboardSceneFor, getPanelIdForVizPanel } 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;
showLibraryPanelSaveModal?: boolean;
}
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
private _initialRepeatOptions: Pick<VizPanelManagerState, 'repeat' | 'repeatDirection' | 'maxPerRow'> = {};
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 LibraryVizPanel) {
// Library panels handled separately
return;
} else 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,
});
}
public onSaveLibraryPanel = () => {
this.setState({ showLibraryPanelSaveModal: true });
};
public onConfirmSaveLibraryPanel = () => {
this.state.vizManager.commitChanges();
locationService.partial({ editPanel: null });
};
public onDismissLibraryPanelModal = () => {
this.setState({ showLibraryPanelSaveModal: false });
};
}
export function buildPanelEditScene(panel: VizPanel): PanelEditor {
return new PanelEditor({
panelId: getPanelIdForVizPanel(panel),
optionsPane: new PanelOptionsPane({}),
vizManager: VizPanelManager.createFor(panel),
});
}