Files
grafana/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
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

215 lines
7.0 KiB
TypeScript

import { Unsubscribable } from 'rxjs';
import { AppEvents } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import {
SceneObjectBase,
SceneObjectState,
SceneObjectUrlSyncHandler,
SceneObjectUrlValues,
VizPanel,
} from '@grafana/scenes';
import appEvents from 'app/core/app_events';
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { createDashboardEditViewFor } from '../settings/utils';
import { findVizPanelByKey, getDashboardSceneFor, getLibraryPanel, isPanelClone } from '../utils/utils';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { LibraryVizPanel } from './LibraryVizPanel';
import { ViewPanelScene } from './ViewPanelScene';
import { DashboardRepeatsProcessedEvent } from './types';
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
private _eventSub?: Unsubscribable;
constructor(private _scene: DashboardScene) {}
getKeys(): string[] {
return ['inspect', 'viewPanel', 'editPanel', 'editview'];
}
getUrlState(): SceneObjectUrlValues {
const state = this._scene.state;
return {
inspect: state.inspectPanelKey,
viewPanel: state.viewPanelScene?.getUrlKey(),
editview: state.editview?.getUrlKey(),
editPanel: state.editPanel?.getUrlKey() || undefined,
};
}
updateFromUrl(values: SceneObjectUrlValues): void {
const { inspectPanelKey, viewPanelScene, isEditing, editPanel } = this._scene.state;
const update: Partial<DashboardSceneState> = {};
if (typeof values.editview === 'string' && this._scene.canEditDashboard()) {
update.editview = createDashboardEditViewFor(values.editview);
// If we are not in editing (for example after full page reload)
if (!isEditing) {
// Not sure what is best to do here.
// The reason for the timeout is for this change to happen after the url sync has completed
setTimeout(() => this._scene.onEnterEditMode());
}
} else if (values.hasOwnProperty('editview')) {
update.editview = undefined;
}
// Handle inspect object state
if (typeof values.inspect === 'string') {
let panel = findVizPanelByKey(this._scene, values.inspect);
if (!panel) {
appEvents.emit(AppEvents.alertError, ['Panel not found']);
locationService.partial({ inspect: null });
return;
}
if (getLibraryPanel(panel)) {
this._handleLibraryPanel(panel, (p) => {
if (p.state.key === undefined) {
// Inspect drawer require a panel key to be set
throw new Error('library panel key is undefined');
}
const drawer = new PanelInspectDrawer({
$behaviors: [new ResolveInspectPanelByKey({ panelKey: p.state.key })],
});
this._scene.setState({ overlay: drawer, inspectPanelKey: p.state.key });
});
return;
}
update.inspectPanelKey = values.inspect;
update.overlay = new PanelInspectDrawer({
$behaviors: [new ResolveInspectPanelByKey({ panelKey: values.inspect })],
});
} else if (inspectPanelKey) {
update.inspectPanelKey = undefined;
update.overlay = undefined;
}
// Handle view panel state
if (typeof values.viewPanel === 'string') {
const panel = findVizPanelByKey(this._scene, values.viewPanel);
if (!panel) {
// // If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet
if (isPanelClone(values.viewPanel)) {
this._handleViewRepeatClone(values.viewPanel);
return;
}
appEvents.emit(AppEvents.alertError, ['Panel not found']);
locationService.partial({ viewPanel: null });
return;
}
if (getLibraryPanel(panel)) {
this._handleLibraryPanel(panel, (p) => this._buildLibraryPanelViewScene(p));
return;
}
update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() });
} else if (viewPanelScene && values.viewPanel === null) {
update.viewPanelScene = undefined;
}
// Handle edit panel state
if (typeof values.editPanel === 'string') {
const panel = findVizPanelByKey(this._scene, values.editPanel);
if (!panel) {
console.warn(`Panel ${values.editPanel} not found`);
return;
}
// If we are not in editing (for example after full page reload)
if (!isEditing) {
this._scene.onEnterEditMode();
}
if (getLibraryPanel(panel)) {
this._handleLibraryPanel(panel, (p) => {
this._scene.setState({ editPanel: buildPanelEditScene(p) });
});
return;
}
update.editPanel = buildPanelEditScene(panel);
} else if (editPanel && values.editPanel === null) {
update.editPanel = undefined;
}
if (Object.keys(update).length > 0) {
this._scene.setState(update);
}
}
private _buildLibraryPanelViewScene(vizPanel: VizPanel) {
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: vizPanel.getRef() }) });
}
private _handleLibraryPanel(vizPanel: VizPanel, cb: (p: VizPanel) => void): void {
if (!(vizPanel.parent instanceof LibraryVizPanel)) {
throw new Error('Panel is not a child of a LibraryVizPanel');
}
const libraryPanel = vizPanel.parent;
if (libraryPanel.state.isLoaded) {
cb(vizPanel);
} else {
libraryPanel.subscribeToState((n) => {
cb(n.panel!);
});
libraryPanel.activate();
}
}
private _handleViewRepeatClone(viewPanel: string) {
if (!this._eventSub) {
this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
const panel = findVizPanelByKey(this._scene, viewPanel);
if (panel) {
this._eventSub?.unsubscribe();
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) });
}
});
}
}
}
interface ResolveInspectPanelByKeyState extends SceneObjectState {
panelKey: string;
}
class ResolveInspectPanelByKey extends SceneObjectBase<ResolveInspectPanelByKeyState> {
constructor(state: ResolveInspectPanelByKeyState) {
super(state);
this.addActivationHandler(this._onActivate);
}
private _onActivate = () => {
const parent = this.parent;
if (!parent || !(parent instanceof PanelInspectDrawer)) {
throw new Error('ResolveInspectPanelByKey must be attached to a PanelInspectDrawer');
}
const dashboard = getDashboardSceneFor(parent);
if (!dashboard) {
return;
}
const panelId = this.state.panelKey;
let panel = findVizPanelByKey(dashboard, panelId);
if (dashboard.state.editPanel) {
panel = dashboard.state.editPanel.state.vizManager.state.panel;
}
if (dashboard.state.viewPanelScene && dashboard.state.viewPanelScene.state.body) {
panel = dashboard.state.viewPanelScene.state.body;
}
if (panel) {
parent.setState({ panelRef: panel.getRef() });
}
};
}