2025-04-01 10:38:02 +09:00

333 lines
9.3 KiB
TypeScript

import * as H from 'history';
import { debounce } from 'lodash';
import { NavIndex, PanelPlugin } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import {
PanelBuilders,
SceneDataTransformer,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
SceneObjectStateChangedEvent,
SceneQueryRunner,
sceneUtils,
VizPanel,
} from '@grafana/scenes';
import { Panel } from '@grafana/schema/dist/esm/index.gen';
import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions';
import { getLastUsedDatasourceFromStorage } from 'app/features/dashboard/utils/dashboard';
import { saveLibPanel } from 'app/features/library-panels/state/api';
import { DashboardSceneChangeTracker } from '../saving/DashboardSceneChangeTracker';
import { getPanelChanges } from '../saving/getDashboardChanges';
import { DashboardLayoutItem, isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { vizPanelToPanel } from '../serialization/transformSceneToSaveModel';
import {
activateSceneObjectAndParentTree,
getDashboardSceneFor,
getLibraryPanelBehavior,
getPanelIdForVizPanel,
} from '../utils/utils';
import { DataProviderSharer } from './PanelDataPane/DataProviderSharer';
import { PanelDataPane } from './PanelDataPane/PanelDataPane';
import { PanelEditorRenderer } from './PanelEditorRenderer';
import { PanelOptionsPane } from './PanelOptionsPane';
export interface PanelEditorState extends SceneObjectState {
isNewPanel: boolean;
isDirty?: boolean;
optionsPane?: PanelOptionsPane;
dataPane?: PanelDataPane;
panelRef: SceneObjectRef<VizPanel>;
showLibraryPanelSaveModal?: boolean;
showLibraryPanelUnlinkModal?: boolean;
tableView?: VizPanel;
pluginLoadErrror?: string;
/**
* Waiting for library panel or panel plugin to load
*/
isInitializing?: boolean;
}
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
static Component = PanelEditorRenderer;
private _layoutItemState?: SceneObjectState;
private _layoutItem: DashboardLayoutItem;
private _originalSaveModel!: Panel;
private _changesHaveBeenMade = false;
public constructor(state: PanelEditorState) {
super(state);
const panel = this.state.panelRef.resolve();
const layoutItem = panel.parent;
if (!layoutItem || !isDashboardLayoutItem(layoutItem)) {
throw new Error('Panel must have a parent of type DashboardLayoutItem');
}
this._layoutItem = layoutItem;
this.setOriginalState(this.state.panelRef);
this.addActivationHandler(this._activationHandler.bind(this));
}
private _activationHandler() {
const panel = this.state.panelRef.resolve();
const deactivateParents = activateSceneObjectAndParentTree(panel);
this.waitForPlugin();
return () => {
this._layoutItem.editingCompleted?.(this.state.isDirty || this._changesHaveBeenMade);
if (deactivateParents) {
deactivateParents();
}
};
}
private waitForPlugin(retry = 0) {
const panel = this.getPanel();
const plugin = panel.getPlugin();
if (!plugin || plugin.meta.id !== panel.state.pluginId) {
if (retry < 100) {
setTimeout(() => this.waitForPlugin(retry + 1), retry * 10);
} else {
this.setState({ pluginLoadErrror: 'Failed to load panel plugin' });
}
return;
}
this.gotPanelPlugin(plugin);
}
private setOriginalState(panelRef: SceneObjectRef<VizPanel>) {
const panel = panelRef.resolve();
this._originalSaveModel = vizPanelToPanel(panel);
this._layoutItemState = sceneUtils.cloneSceneObjectState(this._layoutItem.state);
}
/**
* Useful for testing to turn on debounce
*/
public debounceSaveModelDiff = true;
/**
* Subscribe to state changes and check if the save model has changed
*/
private _setupChangeDetection() {
const panel = this.state.panelRef.resolve();
const performSaveModelDiff = () => {
const { hasChanges } = getPanelChanges(this._originalSaveModel, vizPanelToPanel(panel));
this.setState({ isDirty: hasChanges });
};
const performSaveModelDiffDebounced = this.debounceSaveModelDiff
? debounce(performSaveModelDiff, 250)
: performSaveModelDiff;
const handleStateChange = (event: SceneObjectStateChangedEvent) => {
if (DashboardSceneChangeTracker.isUpdatingPersistedState(event)) {
performSaveModelDiffDebounced();
}
};
// Subscribe to state changes on the parent (layout item) so we do not miss state changes on the layout item
this._subs.add(this._layoutItem.subscribeToEvent(SceneObjectStateChangedEvent, handleStateChange));
}
public getPanel(): VizPanel {
return this.state.panelRef?.resolve();
}
private gotPanelPlugin(plugin: PanelPlugin) {
const panel = this.getPanel();
// First time initialization
if (this.state.isInitializing) {
this.setOriginalState(this.state.panelRef);
this._layoutItem.editingStarted?.();
this._setupChangeDetection();
this._updateDataPane(plugin);
// Listen for panel plugin changes
this._subs.add(
panel.subscribeToState((n, p) => {
if (n.pluginId !== p.pluginId) {
this.waitForPlugin();
}
})
);
// Setup options pane
this.setState({
optionsPane: new PanelOptionsPane({
panelRef: this.state.panelRef,
searchQuery: '',
listMode: OptionFilter.All,
}),
isInitializing: false,
});
} else {
// plugin changed after first time initialization
// Just update data pane
this._updateDataPane(plugin);
}
}
private _updateDataPane(plugin: PanelPlugin) {
const skipDataQuery = plugin.meta.skipDataQuery;
const panel = this.state.panelRef.resolve();
if (skipDataQuery) {
if (this.state.dataPane) {
locationService.partial({ tab: null }, true);
this.setState({ dataPane: undefined });
}
// clean up data provider when switching from data to non data panel
if (panel.state.$data) {
panel.setState({
$data: undefined,
});
}
}
if (!skipDataQuery) {
if (!this.state.dataPane) {
this.setState({ dataPane: PanelDataPane.createFor(this.getPanel()) });
}
// add data provider when switching from non data to data panel
if (!panel.state.$data) {
let ds = getLastUsedDatasourceFromStorage(getDashboardSceneFor(this).state.uid!)?.datasourceUid;
if (!ds) {
ds = config.defaultDatasource;
}
panel.setState({
$data: new SceneDataTransformer({
$data: new SceneQueryRunner({
datasource: {
uid: ds,
},
queries: [{ refId: 'A' }],
}),
transformations: [],
}),
});
}
}
}
public getUrlKey() {
return this.getPanelId().toString();
}
public getPanelId() {
return getPanelIdForVizPanel(this.state.panelRef.resolve());
}
public getPageNav(location: H.Location, navIndex: NavIndex) {
const dashboard = getDashboardSceneFor(this);
return {
text: 'Edit panel',
parentItem: dashboard.getPageNav(location, navIndex),
};
}
public onDiscard = () => {
this.setState({ isDirty: false });
const panel = this.state.panelRef.resolve();
if (this.state.isNewPanel) {
getDashboardSceneFor(this).removePanel(panel);
} else {
// Revert any layout element changes
this._layoutItem!.setState(this._layoutItemState!);
}
locationService.partial({ editPanel: null });
};
public dashboardSaved() {
this.setOriginalState(this.state.panelRef);
this.setState({ isDirty: false });
// Remember that we have done changes
this._changesHaveBeenMade = true;
}
public onSaveLibraryPanel = () => {
this.setState({ showLibraryPanelSaveModal: true });
};
public onConfirmSaveLibraryPanel = () => {
saveLibPanel(this.state.panelRef.resolve());
this.setState({ isDirty: false });
locationService.partial({ editPanel: null });
};
public onDismissLibraryPanelSaveModal = () => {
this.setState({ showLibraryPanelSaveModal: false });
};
public onUnlinkLibraryPanel = () => {
this.setState({ showLibraryPanelUnlinkModal: true });
};
public onDismissUnlinkLibraryPanelModal = () => {
this.setState({ showLibraryPanelUnlinkModal: false });
};
public onConfirmUnlinkLibraryPanel = () => {
const libPanelBehavior = getLibraryPanelBehavior(this.getPanel());
if (!libPanelBehavior) {
return;
}
libPanelBehavior.unlink();
this.setState({ showLibraryPanelUnlinkModal: false });
};
public onToggleTableView = () => {
if (this.state.tableView) {
this.setState({ tableView: undefined });
return;
}
const panel = this.state.panelRef.resolve();
const dataProvider = panel.state.$data;
if (!dataProvider) {
return;
}
this.setState({
tableView: PanelBuilders.table()
.setTitle('')
.setOption('showTypeIcons', true)
.setOption('showHeader', true)
.setData(new DataProviderSharer({ source: dataProvider.getRef() }))
.build(),
});
};
}
export function buildPanelEditScene(panel: VizPanel, isNewPanel = false): PanelEditor {
return new PanelEditor({
isInitializing: true,
panelRef: panel.getRef(),
isNewPanel,
});
}