2023-07-12 13:37:26 +02:00
|
|
|
import * as H from 'history';
|
2023-08-29 14:17:55 +02:00
|
|
|
import { Unsubscribable } from 'rxjs';
|
2023-07-12 13:37:26 +02:00
|
|
|
|
2024-02-13 14:15:55 -03:00
|
|
|
import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
|
|
|
|
|
import { locationService } from '@grafana/runtime';
|
2023-07-06 11:21:03 +02:00
|
|
|
import {
|
2024-02-12 16:06:46 +02:00
|
|
|
dataLayers,
|
2023-07-06 11:21:03 +02:00
|
|
|
getUrlSyncManager,
|
2024-02-12 16:06:46 +02:00
|
|
|
SceneDataLayers,
|
2023-09-14 12:17:04 +02:00
|
|
|
SceneFlexLayout,
|
2024-02-07 06:32:08 -07:00
|
|
|
sceneGraph,
|
2023-07-06 11:21:03 +02:00
|
|
|
SceneGridItem,
|
2023-08-24 07:26:23 +02:00
|
|
|
SceneGridLayout,
|
2024-02-28 11:13:01 +02:00
|
|
|
SceneGridRow,
|
2023-07-06 11:21:03 +02:00
|
|
|
SceneObject,
|
|
|
|
|
SceneObjectBase,
|
|
|
|
|
SceneObjectState,
|
|
|
|
|
SceneObjectStateChangedEvent,
|
2023-12-01 16:04:56 +01:00
|
|
|
SceneRefreshPicker,
|
|
|
|
|
SceneTimeRange,
|
2023-08-29 14:17:55 +02:00
|
|
|
sceneUtils,
|
2023-11-08 14:08:59 +01:00
|
|
|
SceneVariable,
|
|
|
|
|
SceneVariableDependencyConfigLike,
|
2024-02-07 06:32:08 -07:00
|
|
|
VizPanel,
|
2023-07-06 11:21:03 +02:00
|
|
|
} from '@grafana/scenes';
|
2024-01-29 12:04:45 +01:00
|
|
|
import { Dashboard, DashboardLink } from '@grafana/schema';
|
2023-11-08 14:08:59 +01:00
|
|
|
import appEvents from 'app/core/app_events';
|
2024-02-07 06:32:08 -07:00
|
|
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
2023-11-02 20:02:25 +01:00
|
|
|
import { getNavModel } from 'app/core/selectors/navModel';
|
2024-02-07 06:32:08 -07:00
|
|
|
import store from 'app/core/store';
|
2023-10-13 16:24:04 +02:00
|
|
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
2024-02-28 11:13:01 +02:00
|
|
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
2024-02-05 15:25:12 +01:00
|
|
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
2023-11-08 14:08:59 +01:00
|
|
|
import { VariablesChanged } from 'app/features/variables/types';
|
2024-01-29 12:04:45 +01:00
|
|
|
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types';
|
2024-02-13 14:15:55 -03:00
|
|
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
2023-07-06 11:21:03 +02:00
|
|
|
|
2024-01-17 05:53:53 -08:00
|
|
|
import { PanelEditor } from '../panel-edit/PanelEditor';
|
2024-01-29 12:04:45 +01:00
|
|
|
import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer';
|
2023-08-25 14:11:47 +02:00
|
|
|
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
2024-02-28 11:13:01 +02:00
|
|
|
import { buildGridItemForPanel, transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
2024-02-07 06:32:08 -07:00
|
|
|
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
|
2024-01-19 10:58:20 +02:00
|
|
|
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
|
2023-11-20 18:19:30 +01:00
|
|
|
import { DashboardEditView } from '../settings/utils';
|
2024-01-19 10:58:20 +02:00
|
|
|
import { historySrv } from '../settings/version-history';
|
2023-10-13 16:24:04 +02:00
|
|
|
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
2024-02-28 11:13:01 +02:00
|
|
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
2023-12-01 10:07:55 +01:00
|
|
|
import { djb2Hash } from '../utils/djb2Hash';
|
2023-10-20 15:22:56 +02:00
|
|
|
import { getDashboardUrl } from '../utils/urlBuilders';
|
2024-02-26 14:48:27 +02:00
|
|
|
import {
|
|
|
|
|
NEW_PANEL_HEIGHT,
|
|
|
|
|
NEW_PANEL_WIDTH,
|
|
|
|
|
forceRenderChildren,
|
|
|
|
|
getClosestVizPanel,
|
2024-02-28 11:13:01 +02:00
|
|
|
getDefaultRow,
|
2024-02-26 14:48:27 +02:00
|
|
|
getDefaultVizPanel,
|
|
|
|
|
getPanelIdForVizPanel,
|
2024-02-28 11:13:01 +02:00
|
|
|
getVizPanelKeyForPanelId,
|
2024-02-26 14:48:27 +02:00
|
|
|
isPanelClone,
|
|
|
|
|
} from '../utils/utils';
|
2023-07-06 11:21:03 +02:00
|
|
|
|
2023-12-15 11:52:34 +01:00
|
|
|
import { DashboardControls } from './DashboardControls';
|
2023-08-29 14:17:55 +02:00
|
|
|
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
2024-02-07 06:32:08 -07:00
|
|
|
import { PanelRepeaterGridItem } from './PanelRepeaterGridItem';
|
2023-11-30 11:20:15 +01:00
|
|
|
import { ViewPanelScene } from './ViewPanelScene';
|
2023-10-20 15:22:56 +02:00
|
|
|
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
2023-08-29 14:17:55 +02:00
|
|
|
|
2024-01-16 12:24:54 +01:00
|
|
|
export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links'];
|
2023-12-01 16:04:56 +01:00
|
|
|
|
2023-07-06 11:21:03 +02:00
|
|
|
export interface DashboardSceneState extends SceneObjectState {
|
2023-10-13 16:24:04 +02:00
|
|
|
/** The title */
|
2022-11-17 16:15:51 +01:00
|
|
|
title: string;
|
2023-12-01 16:04:56 +01:00
|
|
|
/** The description */
|
|
|
|
|
description?: string;
|
2023-11-28 12:26:09 +01:00
|
|
|
/** Tags */
|
|
|
|
|
tags?: string[];
|
2023-11-29 15:01:40 +01:00
|
|
|
/** Links */
|
2024-01-16 12:24:54 +01:00
|
|
|
links: DashboardLink[];
|
2023-12-01 16:04:56 +01:00
|
|
|
/** Is editable */
|
|
|
|
|
editable?: boolean;
|
2023-10-13 16:24:04 +02:00
|
|
|
/** A uid when saved */
|
2023-01-17 18:02:46 +01:00
|
|
|
uid?: string;
|
2023-10-13 16:24:04 +02:00
|
|
|
/** @deprecated */
|
|
|
|
|
id?: number | null;
|
|
|
|
|
/** Layout of panels */
|
2023-01-17 18:02:46 +01:00
|
|
|
body: SceneObject;
|
2023-10-13 16:24:04 +02:00
|
|
|
/** NavToolbar actions */
|
2022-11-17 16:15:51 +01:00
|
|
|
actions?: SceneObject[];
|
2023-10-13 16:24:04 +02:00
|
|
|
/** Fixed row at the top of the canvas with for example variables and time range controls */
|
2024-02-20 08:43:02 +01:00
|
|
|
controls?: DashboardControls;
|
2023-10-13 16:24:04 +02:00
|
|
|
/** True when editing */
|
2023-07-06 11:21:03 +02:00
|
|
|
isEditing?: boolean;
|
2023-10-13 16:24:04 +02:00
|
|
|
/** True when user made a change */
|
2023-07-06 11:21:03 +02:00
|
|
|
isDirty?: boolean;
|
2023-09-14 12:17:04 +02:00
|
|
|
/** meta flags */
|
|
|
|
|
meta: DashboardMeta;
|
2024-01-17 17:14:29 +02:00
|
|
|
/** Version of the dashboard */
|
|
|
|
|
version?: number;
|
2023-08-30 10:09:47 +02:00
|
|
|
/** Panel to inspect */
|
2023-09-05 13:51:46 +02:00
|
|
|
inspectPanelKey?: string;
|
2023-11-30 11:20:15 +01:00
|
|
|
/** Panel to view in fullscreen */
|
|
|
|
|
viewPanelScene?: ViewPanelScene;
|
2023-11-20 18:19:30 +01:00
|
|
|
/** Edit view */
|
|
|
|
|
editview?: DashboardEditView;
|
2024-01-17 05:53:53 -08:00
|
|
|
/** Edit panel */
|
|
|
|
|
editPanel?: PanelEditor;
|
2023-09-19 16:02:21 +02:00
|
|
|
/** Scene object that handles the current drawer or modal */
|
|
|
|
|
overlay?: SceneObject;
|
2024-02-28 11:13:01 +02:00
|
|
|
/** True when a user copies a panel in the dashboard */
|
|
|
|
|
hasCopiedPanel?: boolean;
|
2024-02-23 15:03:35 -07:00
|
|
|
isEmpty?: boolean;
|
2022-11-17 16:15:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
2023-12-01 16:04:56 +01:00
|
|
|
static listenToChangesInProps = PERSISTED_PROPS;
|
2023-07-06 09:22:02 +02:00
|
|
|
static Component = DashboardSceneRenderer;
|
2022-11-17 16:15:51 +01:00
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
/**
|
|
|
|
|
* Handles url sync
|
|
|
|
|
*/
|
2023-07-12 13:37:26 +02:00
|
|
|
protected _urlSync = new DashboardSceneUrlSync(this);
|
2023-11-08 14:08:59 +01:00
|
|
|
/**
|
|
|
|
|
* Get notified when variables change
|
|
|
|
|
*/
|
|
|
|
|
protected _variableDependency = new DashboardVariableDependency();
|
|
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
/**
|
|
|
|
|
* State before editing started
|
|
|
|
|
*/
|
|
|
|
|
private _initialState?: DashboardSceneState;
|
2024-01-29 12:04:45 +01:00
|
|
|
/**
|
|
|
|
|
* The save model which the scene was originally created from
|
|
|
|
|
*/
|
|
|
|
|
private _initialSaveModel?: Dashboard;
|
2023-08-29 14:17:55 +02:00
|
|
|
/**
|
|
|
|
|
* Url state before editing started
|
|
|
|
|
*/
|
2023-11-20 18:19:30 +01:00
|
|
|
private _initialUrlState?: H.Location;
|
2023-08-29 14:17:55 +02:00
|
|
|
/**
|
|
|
|
|
* change tracking subscription
|
|
|
|
|
*/
|
|
|
|
|
private _changeTrackerSub?: Unsubscribable;
|
2023-07-12 13:37:26 +02:00
|
|
|
|
2023-09-14 12:17:04 +02:00
|
|
|
public constructor(state: Partial<DashboardSceneState>) {
|
|
|
|
|
super({
|
|
|
|
|
title: 'Dashboard',
|
|
|
|
|
meta: {},
|
2023-12-01 16:04:56 +01:00
|
|
|
editable: true,
|
2023-09-14 12:17:04 +02:00
|
|
|
body: state.body ?? new SceneFlexLayout({ children: [] }),
|
2024-01-16 12:24:54 +01:00
|
|
|
links: state.links ?? [],
|
2024-02-28 11:13:01 +02:00
|
|
|
hasCopiedPanel: store.exists(LS_PANEL_COPY_KEY),
|
2023-09-14 12:17:04 +02:00
|
|
|
...state,
|
|
|
|
|
});
|
2022-11-17 16:15:51 +01:00
|
|
|
|
2023-09-11 13:51:05 +02:00
|
|
|
this.addActivationHandler(() => this._activationHandler());
|
2023-03-17 06:50:37 -07:00
|
|
|
}
|
2022-11-17 16:15:51 +01:00
|
|
|
|
2023-09-11 13:51:05 +02:00
|
|
|
private _activationHandler() {
|
2024-02-07 06:32:08 -07:00
|
|
|
let prevSceneContext = window.__grafanaSceneContext;
|
|
|
|
|
|
2023-10-04 13:21:01 +02:00
|
|
|
window.__grafanaSceneContext = this;
|
|
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
if (this.state.isEditing) {
|
|
|
|
|
this.startTrackingChanges();
|
2023-07-06 11:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
2024-02-05 15:25:12 +01:00
|
|
|
if (!this.state.meta.isEmbedded && this.state.uid) {
|
|
|
|
|
dashboardWatcher.watch(this.state.uid);
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-20 15:22:56 +02:00
|
|
|
const clearKeyBindings = setupKeyboardShortcuts(this);
|
2023-10-13 16:24:04 +02:00
|
|
|
const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this);
|
|
|
|
|
|
|
|
|
|
// @ts-expect-error
|
|
|
|
|
getDashboardSrv().setCurrent(oldDashboardWrapper);
|
|
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
// Deactivation logic
|
|
|
|
|
return () => {
|
2024-02-07 06:32:08 -07:00
|
|
|
window.__grafanaSceneContext = prevSceneContext;
|
2023-10-20 15:22:56 +02:00
|
|
|
clearKeyBindings();
|
2023-08-29 14:17:55 +02:00
|
|
|
this.stopTrackingChanges();
|
|
|
|
|
this.stopUrlSync();
|
2023-10-13 16:24:04 +02:00
|
|
|
oldDashboardWrapper.destroy();
|
2024-02-05 15:25:12 +01:00
|
|
|
dashboardWatcher.leave();
|
2023-08-29 14:17:55 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public startUrlSync() {
|
2024-01-31 11:33:46 +01:00
|
|
|
if (!this.state.meta.isEmbedded) {
|
|
|
|
|
getUrlSyncManager().initSync(this);
|
|
|
|
|
}
|
2023-07-06 11:21:03 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
public stopUrlSync() {
|
|
|
|
|
getUrlSyncManager().cleanUp(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public onEnterEditMode = () => {
|
|
|
|
|
// Save this state
|
|
|
|
|
this._initialState = sceneUtils.cloneSceneObjectState(this.state);
|
2023-11-20 18:19:30 +01:00
|
|
|
this._initialUrlState = locationService.getLocation();
|
2023-08-29 14:17:55 +02:00
|
|
|
|
|
|
|
|
// Switch to edit mode
|
2023-07-06 11:21:03 +02:00
|
|
|
this.setState({ isEditing: true });
|
2023-08-24 07:26:23 +02:00
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
// Propagate change edit mode change to children
|
2024-01-29 12:04:45 +01:00
|
|
|
this.propagateEditModeChange();
|
|
|
|
|
this.startTrackingChanges();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
public saveCompleted(saveModel: Dashboard, result: SaveDashboardResponseDTO, folderUid?: string) {
|
|
|
|
|
this._initialSaveModel = {
|
|
|
|
|
...saveModel,
|
|
|
|
|
id: result.id,
|
|
|
|
|
uid: result.uid,
|
|
|
|
|
version: result.version,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.stopTrackingChanges();
|
|
|
|
|
this.setState({
|
|
|
|
|
version: result.version,
|
|
|
|
|
isDirty: false,
|
|
|
|
|
uid: result.uid,
|
|
|
|
|
id: result.id,
|
|
|
|
|
meta: {
|
|
|
|
|
...this.state.meta,
|
|
|
|
|
uid: result.uid,
|
|
|
|
|
url: result.url,
|
|
|
|
|
slug: result.slug,
|
|
|
|
|
folderUid: folderUid,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
this.startTrackingChanges();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private propagateEditModeChange() {
|
2023-08-24 07:26:23 +02:00
|
|
|
if (this.state.body instanceof SceneGridLayout) {
|
2024-01-29 12:04:45 +01:00
|
|
|
this.state.body.setState({ isDraggable: this.state.isEditing, isResizable: this.state.isEditing });
|
2023-08-24 07:26:23 +02:00
|
|
|
forceRenderChildren(this.state.body, true);
|
|
|
|
|
}
|
2024-01-29 12:04:45 +01:00
|
|
|
}
|
2023-01-17 18:02:46 +01:00
|
|
|
|
2024-02-05 16:08:12 +01:00
|
|
|
public exitEditMode({ skipConfirm }: { skipConfirm: boolean }) {
|
2024-01-31 11:33:46 +01:00
|
|
|
if (!this.canDiscard()) {
|
|
|
|
|
console.error('Trying to discard back to a state that does not exist, initialState undefined');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-05 16:08:12 +01:00
|
|
|
if (!this.state.isDirty || skipConfirm) {
|
|
|
|
|
this.exitEditModeConfirmed();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appEvents.publish(
|
|
|
|
|
new ShowConfirmModalEvent({
|
|
|
|
|
title: 'Discard changes to dashboard?',
|
|
|
|
|
text: `You have unsaved changes to this dashboard. Are you sure you want to discard them?`,
|
|
|
|
|
icon: 'trash-alt',
|
|
|
|
|
yesText: 'Discard',
|
|
|
|
|
onConfirm: this.exitEditModeConfirmed.bind(this),
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private exitEditModeConfirmed() {
|
2023-08-29 14:17:55 +02:00
|
|
|
// No need to listen to changes anymore
|
|
|
|
|
this.stopTrackingChanges();
|
|
|
|
|
// Stop url sync before updating url
|
|
|
|
|
this.stopUrlSync();
|
2024-01-17 05:53:53 -08:00
|
|
|
|
|
|
|
|
// Now we can update urls
|
|
|
|
|
// We are updating url and removing editview and editPanel.
|
|
|
|
|
// The initial url may be including edit view, edit panel or inspect query params if the user pasted the url,
|
|
|
|
|
// hence we need to cleanup those query params to get back to the dashboard view. Otherwise url sync can trigger overlays.
|
|
|
|
|
locationService.replace(
|
|
|
|
|
locationUtil.getUrlForPartial(this._initialUrlState!, {
|
|
|
|
|
editPanel: null,
|
|
|
|
|
editview: null,
|
|
|
|
|
inspect: null,
|
|
|
|
|
inspectTab: null,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// locationService.replace({ pathname: this._initialUrlState?.pathname, search: this._initialUrlState?.search });
|
2023-08-29 14:17:55 +02:00
|
|
|
// Update state and disable editing
|
|
|
|
|
this.setState({ ...this._initialState, isEditing: false });
|
|
|
|
|
// and start url sync again
|
|
|
|
|
this.startUrlSync();
|
2023-08-24 07:26:23 +02:00
|
|
|
// Disable grid dragging
|
2024-01-29 12:04:45 +01:00
|
|
|
this.propagateEditModeChange();
|
2024-02-05 16:08:12 +01:00
|
|
|
}
|
2023-07-12 13:37:26 +02:00
|
|
|
|
2024-01-31 11:33:46 +01:00
|
|
|
public canDiscard() {
|
|
|
|
|
return this._initialState !== undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-19 10:58:20 +02:00
|
|
|
public onRestore = async (version: DecoratedRevisionModel): Promise<boolean> => {
|
|
|
|
|
const versionRsp = await historySrv.restoreDashboard(version.uid, version.version);
|
|
|
|
|
|
|
|
|
|
if (!Number.isInteger(versionRsp.version)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const dashboardDTO: DashboardDTO = {
|
|
|
|
|
dashboard: new DashboardModel(version.data),
|
|
|
|
|
meta: this.state.meta,
|
|
|
|
|
};
|
|
|
|
|
const dashScene = transformSaveModelToScene(dashboardDTO);
|
|
|
|
|
const newState = sceneUtils.cloneSceneObjectState(dashScene.state);
|
|
|
|
|
newState.version = versionRsp.version;
|
|
|
|
|
|
|
|
|
|
this._initialState = newState;
|
2024-02-05 16:08:12 +01:00
|
|
|
this.exitEditMode({ skipConfirm: false });
|
2024-01-19 10:58:20 +02:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-29 12:04:45 +01:00
|
|
|
public openSaveDrawer({ saveAsCopy }: { saveAsCopy?: boolean }) {
|
|
|
|
|
if (!this.state.isEditing) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
overlay: new SaveDashboardDrawer({
|
|
|
|
|
dashboardRef: this.getRef(),
|
|
|
|
|
saveAsCopy,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-08-29 14:17:55 +02:00
|
|
|
|
2023-11-02 20:02:25 +01:00
|
|
|
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
2024-01-17 05:53:53 -08:00
|
|
|
const { meta, viewPanelScene, editPanel } = this.state;
|
2023-11-02 20:02:25 +01:00
|
|
|
|
2023-07-12 13:37:26 +02:00
|
|
|
let pageNav: NavModelItem = {
|
|
|
|
|
text: this.state.title,
|
2023-09-11 13:51:05 +02:00
|
|
|
url: getDashboardUrl({
|
|
|
|
|
uid: this.state.uid,
|
2024-01-17 06:58:57 -08:00
|
|
|
slug: meta.slug,
|
2023-09-11 13:51:05 +02:00
|
|
|
currentQueryParams: location.search,
|
2024-01-17 05:53:53 -08:00
|
|
|
updateQuery: { viewPanel: null, inspect: null, editview: null, editPanel: null, tab: null },
|
2023-09-11 13:51:05 +02:00
|
|
|
}),
|
2023-07-12 13:37:26 +02:00
|
|
|
};
|
|
|
|
|
|
2023-11-22 15:22:00 +00:00
|
|
|
const { folderUid } = meta;
|
2023-11-02 20:02:25 +01:00
|
|
|
|
|
|
|
|
if (folderUid) {
|
2023-11-22 15:22:00 +00:00
|
|
|
const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
|
|
|
|
|
// If the folder hasn't loaded (maybe user doesn't have permission on it?) then
|
|
|
|
|
// don't show the "page not found" breadcrumb
|
|
|
|
|
if (folderNavModel.id !== 'not-found') {
|
|
|
|
|
pageNav = {
|
|
|
|
|
...pageNav,
|
|
|
|
|
parentItem: folderNavModel,
|
|
|
|
|
};
|
2023-11-02 20:02:25 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-30 11:20:15 +01:00
|
|
|
if (viewPanelScene) {
|
2023-07-12 13:37:26 +02:00
|
|
|
pageNav = {
|
|
|
|
|
text: 'View panel',
|
|
|
|
|
parentItem: pageNav,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 05:53:53 -08:00
|
|
|
if (editPanel) {
|
|
|
|
|
pageNav = {
|
|
|
|
|
text: 'Edit panel',
|
|
|
|
|
parentItem: pageNav,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-12 13:37:26 +02:00
|
|
|
return pageNav;
|
|
|
|
|
}
|
2023-08-25 14:11:47 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns the body (layout) or the full view panel
|
|
|
|
|
*/
|
2023-11-30 11:20:15 +01:00
|
|
|
public getBodyToRender(): SceneObject {
|
|
|
|
|
return this.state.viewPanelScene ?? this.state.body;
|
2023-08-25 14:11:47 +02:00
|
|
|
}
|
2023-07-12 13:37:26 +02:00
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
private startTrackingChanges() {
|
|
|
|
|
this._changeTrackerSub = this.subscribeToEvent(
|
|
|
|
|
SceneObjectStateChangedEvent,
|
|
|
|
|
(event: SceneObjectStateChangedEvent) => {
|
2023-12-01 16:04:56 +01:00
|
|
|
if (event.payload.changedObject instanceof SceneRefreshPicker) {
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'intervals')) {
|
|
|
|
|
this.setIsDirty();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-12 16:06:46 +02:00
|
|
|
if (event.payload.changedObject instanceof SceneDataLayers) {
|
|
|
|
|
this.setIsDirty();
|
|
|
|
|
}
|
|
|
|
|
if (event.payload.changedObject instanceof dataLayers.AnnotationsDataLayer) {
|
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'data')) {
|
|
|
|
|
this.setIsDirty();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-29 14:17:55 +02:00
|
|
|
if (event.payload.changedObject instanceof SceneGridItem) {
|
2023-09-14 12:17:04 +02:00
|
|
|
this.setIsDirty();
|
2023-08-29 14:17:55 +02:00
|
|
|
}
|
2023-12-01 16:04:56 +01:00
|
|
|
if (event.payload.changedObject instanceof DashboardScene) {
|
|
|
|
|
if (Object.keys(event.payload.partialUpdate).some((key) => PERSISTED_PROPS.includes(key))) {
|
|
|
|
|
this.setIsDirty();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (event.payload.changedObject instanceof SceneTimeRange) {
|
|
|
|
|
this.setIsDirty();
|
|
|
|
|
}
|
2023-12-15 11:52:34 +01:00
|
|
|
if (event.payload.changedObject instanceof DashboardControls) {
|
|
|
|
|
if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'hideTimeControls')) {
|
|
|
|
|
this.setIsDirty();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-29 14:17:55 +02:00
|
|
|
}
|
|
|
|
|
);
|
2023-07-12 13:37:26 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-14 12:17:04 +02:00
|
|
|
private setIsDirty() {
|
|
|
|
|
if (!this.state.isDirty) {
|
|
|
|
|
this.setState({ isDirty: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
private stopTrackingChanges() {
|
|
|
|
|
this._changeTrackerSub?.unsubscribe();
|
2023-07-12 13:37:26 +02:00
|
|
|
}
|
|
|
|
|
|
2023-08-29 14:17:55 +02:00
|
|
|
public getInitialState(): DashboardSceneState | undefined {
|
|
|
|
|
return this._initialState;
|
2023-07-12 13:37:26 +02:00
|
|
|
}
|
2023-09-19 16:02:21 +02:00
|
|
|
|
2024-02-28 11:13:01 +02:00
|
|
|
public addRow(row: SceneGridRow) {
|
2024-02-26 14:48:27 +02:00
|
|
|
if (!(this.state.body instanceof SceneGridLayout)) {
|
|
|
|
|
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sceneGridLayout = this.state.body;
|
|
|
|
|
|
2024-02-28 11:13:01 +02:00
|
|
|
// find all panels until the first row and put them into the newly created row. If there are no other rows,
|
|
|
|
|
// add all panels to the row. If there are no panels just create an empty row
|
|
|
|
|
const indexTillNextRow = sceneGridLayout.state.children.findIndex((child) => child instanceof SceneGridRow);
|
|
|
|
|
const rowChildren = sceneGridLayout.state.children
|
|
|
|
|
.splice(0, indexTillNextRow === -1 ? sceneGridLayout.state.children.length : indexTillNextRow)
|
|
|
|
|
.map((child) => child.clone());
|
|
|
|
|
|
|
|
|
|
if (rowChildren) {
|
|
|
|
|
row.setState({
|
|
|
|
|
children: rowChildren,
|
2024-02-26 14:48:27 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-28 11:13:01 +02:00
|
|
|
sceneGridLayout.setState({
|
|
|
|
|
children: [row, ...sceneGridLayout.state.children],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public addPanel(vizPanel: VizPanel): void {
|
|
|
|
|
if (!(this.state.body instanceof SceneGridLayout)) {
|
|
|
|
|
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sceneGridLayout = this.state.body;
|
|
|
|
|
|
|
|
|
|
const panelId = getPanelIdForVizPanel(vizPanel);
|
2024-02-26 14:48:27 +02:00
|
|
|
const newGridItem = new SceneGridItem({
|
|
|
|
|
height: NEW_PANEL_HEIGHT,
|
|
|
|
|
width: NEW_PANEL_WIDTH,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
body: vizPanel,
|
2024-02-28 11:13:01 +02:00
|
|
|
key: `grid-item-${panelId}`,
|
2024-02-26 14:48:27 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sceneGridLayout.setState({
|
|
|
|
|
children: [newGridItem, ...sceneGridLayout.state.children],
|
2024-02-15 09:40:58 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 06:32:08 -07:00
|
|
|
public duplicatePanel(vizPanel: VizPanel) {
|
|
|
|
|
if (!vizPanel.parent) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const gridItem = vizPanel.parent;
|
|
|
|
|
|
2024-02-28 11:13:01 +02:00
|
|
|
if (!(gridItem instanceof SceneGridItem || gridItem instanceof PanelRepeaterGridItem)) {
|
2024-02-07 06:32:08 -07:00
|
|
|
console.error('Trying to duplicate a panel in a layout that is not SceneGridItem or PanelRepeaterGridItem');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let panelState;
|
|
|
|
|
let panelData;
|
|
|
|
|
if (gridItem instanceof PanelRepeaterGridItem) {
|
|
|
|
|
const { key, ...gridRepeaterSourceState } = sceneUtils.cloneSceneObjectState(gridItem.state.source.state);
|
|
|
|
|
panelState = { ...gridRepeaterSourceState };
|
|
|
|
|
panelData = sceneGraph.getData(gridItem.state.source).clone();
|
|
|
|
|
} else {
|
|
|
|
|
const { key, ...gridItemPanelState } = sceneUtils.cloneSceneObjectState(vizPanel.state);
|
|
|
|
|
panelState = { ...gridItemPanelState };
|
|
|
|
|
panelData = sceneGraph.getData(vizPanel).clone();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// when we duplicate a panel we don't want to clone the alert state
|
|
|
|
|
delete panelData.state.data?.alertState;
|
|
|
|
|
|
|
|
|
|
const { key: gridItemKey, ...gridItemToDuplicateState } = sceneUtils.cloneSceneObjectState(gridItem.state);
|
|
|
|
|
|
|
|
|
|
const newGridItem = new SceneGridItem({
|
|
|
|
|
...gridItemToDuplicateState,
|
|
|
|
|
body: new VizPanel({ ...panelState, $data: panelData }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!(this.state.body instanceof SceneGridLayout)) {
|
|
|
|
|
console.error('Trying to duplicate a panel in a layout that is not SceneGridLayout ');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sceneGridLayout = this.state.body;
|
|
|
|
|
|
|
|
|
|
sceneGridLayout.setState({
|
|
|
|
|
children: [...sceneGridLayout.state.children, newGridItem],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public copyPanel(vizPanel: VizPanel) {
|
|
|
|
|
if (!vizPanel.parent) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const gridItem = vizPanel.parent;
|
|
|
|
|
|
|
|
|
|
const jsonData = gridItemToPanel(gridItem);
|
|
|
|
|
|
|
|
|
|
store.set(LS_PANEL_COPY_KEY, JSON.stringify(jsonData));
|
2024-02-28 11:13:01 +02:00
|
|
|
appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Use **Paste panel** toolbar action to paste.']);
|
|
|
|
|
this.setState({ hasCopiedPanel: true });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public pastePanel() {
|
|
|
|
|
if (!(this.state.body instanceof SceneGridLayout)) {
|
|
|
|
|
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const jsonData = store.get(LS_PANEL_COPY_KEY);
|
|
|
|
|
const jsonObj = JSON.parse(jsonData);
|
|
|
|
|
const panelModel = new PanelModel(jsonObj);
|
|
|
|
|
|
|
|
|
|
const gridItem = buildGridItemForPanel(panelModel);
|
|
|
|
|
const sceneGridLayout = this.state.body;
|
|
|
|
|
|
|
|
|
|
if (!(gridItem instanceof SceneGridItem) && !(gridItem instanceof PanelRepeaterGridItem)) {
|
|
|
|
|
throw new Error('Cannot paste invalid grid item');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const panelId = dashboardSceneGraph.getNextPanelId(this);
|
|
|
|
|
|
|
|
|
|
if (gridItem instanceof SceneGridItem && gridItem.state.body) {
|
|
|
|
|
gridItem.state.body.setState({
|
|
|
|
|
key: getVizPanelKeyForPanelId(panelId),
|
|
|
|
|
});
|
|
|
|
|
} else if (gridItem instanceof PanelRepeaterGridItem) {
|
|
|
|
|
gridItem.state.source.setState({
|
|
|
|
|
key: getVizPanelKeyForPanelId(panelId),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gridItem.setState({
|
|
|
|
|
height: NEW_PANEL_HEIGHT,
|
|
|
|
|
width: NEW_PANEL_WIDTH,
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
key: `grid-item-${panelId}`,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
sceneGridLayout.setState({
|
|
|
|
|
children: [gridItem, ...sceneGridLayout.state.children],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.setState({ hasCopiedPanel: false });
|
|
|
|
|
store.delete(LS_PANEL_COPY_KEY);
|
2024-02-07 06:32:08 -07:00
|
|
|
}
|
|
|
|
|
|
2023-09-19 16:02:21 +02:00
|
|
|
public showModal(modal: SceneObject) {
|
|
|
|
|
this.setState({ overlay: modal });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public closeModal() {
|
|
|
|
|
this.setState({ overlay: undefined });
|
|
|
|
|
}
|
2023-09-29 13:19:03 +02:00
|
|
|
|
2023-11-02 20:02:25 +01:00
|
|
|
public async onStarDashboard() {
|
|
|
|
|
const { meta, uid } = this.state;
|
|
|
|
|
if (!uid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const result = await getDashboardSrv().starDashboard(uid, Boolean(meta.isStarred));
|
|
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
|
meta: {
|
|
|
|
|
...meta,
|
|
|
|
|
isStarred: result,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Failed to star dashboard', err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-20 18:19:30 +01:00
|
|
|
public onOpenSettings = () => {
|
|
|
|
|
locationService.partial({ editview: 'settings' });
|
|
|
|
|
};
|
|
|
|
|
|
2024-02-28 11:13:01 +02:00
|
|
|
public onCreateNewRow() {
|
|
|
|
|
const row = getDefaultRow(this);
|
|
|
|
|
|
|
|
|
|
this.addRow(row);
|
|
|
|
|
|
|
|
|
|
return getPanelIdForVizPanel(row);
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 14:48:27 +02:00
|
|
|
public onCreateNewPanel(): number {
|
|
|
|
|
const vizPanel = getDefaultVizPanel(this);
|
|
|
|
|
|
|
|
|
|
this.addPanel(vizPanel);
|
|
|
|
|
|
|
|
|
|
return getPanelIdForVizPanel(vizPanel);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-29 13:19:03 +02:00
|
|
|
/**
|
|
|
|
|
* Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request
|
|
|
|
|
*/
|
|
|
|
|
public enrichDataRequest(sceneObject: SceneObject): Partial<DataQueryRequest> {
|
|
|
|
|
const panel = getClosestVizPanel(sceneObject);
|
2023-12-01 10:07:55 +01:00
|
|
|
let panelId = 0;
|
|
|
|
|
|
|
|
|
|
if (panel && panel.state.key) {
|
|
|
|
|
if (isPanelClone(panel.state.key)) {
|
|
|
|
|
panelId = djb2Hash(panel?.state.key);
|
|
|
|
|
} else {
|
|
|
|
|
panelId = getPanelIdForVizPanel(panel);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-09-29 13:19:03 +02:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
app: CoreApp.Dashboard,
|
|
|
|
|
dashboardUID: this.state.uid,
|
2023-12-01 10:07:55 +01:00
|
|
|
panelId,
|
2024-02-21 09:38:42 +01:00
|
|
|
panelPluginId: panel?.state.pluginId,
|
2023-09-29 13:19:03 +02:00
|
|
|
};
|
|
|
|
|
}
|
2023-10-13 11:42:42 +02:00
|
|
|
|
|
|
|
|
canEditDashboard() {
|
2023-11-02 20:02:25 +01:00
|
|
|
const { meta } = this.state;
|
|
|
|
|
|
|
|
|
|
return Boolean(meta.canEdit || meta.canMakeEditable);
|
2023-10-13 11:42:42 +02:00
|
|
|
}
|
2024-01-29 12:04:45 +01:00
|
|
|
|
|
|
|
|
public getInitialSaveModel() {
|
|
|
|
|
return this._initialSaveModel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Hacky temp function until we refactor transformSaveModelToScene a bit */
|
|
|
|
|
public setInitialSaveModel(saveModel: Dashboard) {
|
|
|
|
|
this._initialSaveModel = saveModel;
|
|
|
|
|
}
|
2023-01-17 18:02:46 +01:00
|
|
|
}
|
2023-11-08 14:08:59 +01:00
|
|
|
|
|
|
|
|
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
|
|
|
|
|
private _emptySet = new Set<string>();
|
|
|
|
|
|
|
|
|
|
getNames(): Set<string> {
|
|
|
|
|
return this._emptySet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public hasDependencyOn(): boolean {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-22 19:22:04 +01:00
|
|
|
public variableUpdateCompleted(variable: SceneVariable, hasChanged: boolean) {
|
|
|
|
|
if (hasChanged) {
|
2023-11-08 14:08:59 +01:00
|
|
|
// Temp solution for some core panels (like dashlist) to know that variables have changed
|
|
|
|
|
appEvents.publish(new VariablesChanged({ refreshAll: true, panelIds: [] }));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|