Submit
Path:
~
/
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
dashboard-scene
/
scene
/
File Content:
DashboardScene.tsx
import * as H from 'history'; import { Unsubscribable } from 'rxjs'; import { AppEvents, CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { dataLayers, getUrlSyncManager, SceneDataLayers, SceneFlexLayout, sceneGraph, SceneGridItem, SceneGridLayout, SceneObject, SceneObjectBase, SceneObjectState, SceneObjectStateChangedEvent, SceneRefreshPicker, SceneTimeRange, sceneUtils, SceneVariable, SceneVariableDependencyConfigLike, VizPanel, } from '@grafana/scenes'; import { Dashboard, DashboardLink } from '@grafana/schema'; import appEvents from 'app/core/app_events'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { getNavModel } from 'app/core/selectors/navModel'; import store from 'app/core/store'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; import { PanelEditor } from '../panel-edit/PanelEditor'; import { SaveDashboardDrawer } from '../saving/SaveDashboardDrawer'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { gridItemToPanel } from '../serialization/transformSceneToSaveModel'; import { DecoratedRevisionModel } from '../settings/VersionsEditView'; import { DashboardEditView } from '../settings/utils'; import { historySrv } from '../settings/version-history'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { djb2Hash } from '../utils/djb2Hash'; import { getDashboardUrl } from '../utils/urlBuilders'; import { forceRenderChildren, getClosestVizPanel, getPanelIdForVizPanel, isPanelClone } from '../utils/utils'; import { DashboardControls } from './DashboardControls'; import { DashboardSceneUrlSync } from './DashboardSceneUrlSync'; import { PanelRepeaterGridItem } from './PanelRepeaterGridItem'; import { ViewPanelScene } from './ViewPanelScene'; import { setupKeyboardShortcuts } from './keyboardShortcuts'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links']; export interface DashboardSceneState extends SceneObjectState { /** The title */ title: string; /** The description */ description?: string; /** Tags */ tags?: string[]; /** Links */ links: DashboardLink[]; /** Is editable */ editable?: boolean; /** A uid when saved */ uid?: string; /** @deprecated */ id?: number | null; /** Layout of panels */ body: SceneObject; /** NavToolbar actions */ actions?: SceneObject[]; /** Fixed row at the top of the canvas with for example variables and time range controls */ controls?: SceneObject[]; /** True when editing */ isEditing?: boolean; /** True when user made a change */ isDirty?: boolean; /** meta flags */ meta: DashboardMeta; /** Version of the dashboard */ version?: number; /** Panel to inspect */ inspectPanelKey?: string; /** Panel to view in fullscreen */ viewPanelScene?: ViewPanelScene; /** Edit view */ editview?: DashboardEditView; /** Edit panel */ editPanel?: PanelEditor; /** Scene object that handles the current drawer or modal */ overlay?: SceneObject; } export class DashboardScene extends SceneObjectBase<DashboardSceneState> { static listenToChangesInProps = PERSISTED_PROPS; static Component = DashboardSceneRenderer; /** * Handles url sync */ protected _urlSync = new DashboardSceneUrlSync(this); /** * Get notified when variables change */ protected _variableDependency = new DashboardVariableDependency(); /** * State before editing started */ private _initialState?: DashboardSceneState; /** * The save model which the scene was originally created from */ private _initialSaveModel?: Dashboard; /** * Url state before editing started */ private _initialUrlState?: H.Location; /** * change tracking subscription */ private _changeTrackerSub?: Unsubscribable; public constructor(state: Partial<DashboardSceneState>) { super({ title: 'Dashboard', meta: {}, editable: true, body: state.body ?? new SceneFlexLayout({ children: [] }), links: state.links ?? [], ...state, }); this.addActivationHandler(() => this._activationHandler()); } private _activationHandler() { let prevSceneContext = window.__grafanaSceneContext; window.__grafanaSceneContext = this; if (this.state.isEditing) { this.startTrackingChanges(); } if (!this.state.meta.isEmbedded && this.state.uid) { dashboardWatcher.watch(this.state.uid); } const clearKeyBindings = setupKeyboardShortcuts(this); const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this); // @ts-expect-error getDashboardSrv().setCurrent(oldDashboardWrapper); // Deactivation logic return () => { window.__grafanaSceneContext = prevSceneContext; clearKeyBindings(); this.stopTrackingChanges(); this.stopUrlSync(); oldDashboardWrapper.destroy(); dashboardWatcher.leave(); }; } public startUrlSync() { if (!this.state.meta.isEmbedded) { getUrlSyncManager().initSync(this); } } public stopUrlSync() { getUrlSyncManager().cleanUp(this); } public onEnterEditMode = () => { // Save this state this._initialState = sceneUtils.cloneSceneObjectState(this.state); this._initialUrlState = locationService.getLocation(); // Switch to edit mode this.setState({ isEditing: true }); // Propagate change edit mode change to children 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() { if (this.state.body instanceof SceneGridLayout) { this.state.body.setState({ isDraggable: this.state.isEditing, isResizable: this.state.isEditing }); forceRenderChildren(this.state.body, true); } } public exitEditMode({ skipConfirm }: { skipConfirm: boolean }) { if (!this.canDiscard()) { console.error('Trying to discard back to a state that does not exist, initialState undefined'); return; } 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() { // No need to listen to changes anymore this.stopTrackingChanges(); // Stop url sync before updating url this.stopUrlSync(); // 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 }); // Update state and disable editing this.setState({ ...this._initialState, isEditing: false }); // and start url sync again this.startUrlSync(); // Disable grid dragging this.propagateEditModeChange(); } public canDiscard() { return this._initialState !== undefined; } 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; this.exitEditMode({ skipConfirm: false }); return true; }; public openSaveDrawer({ saveAsCopy }: { saveAsCopy?: boolean }) { if (!this.state.isEditing) { return; } this.setState({ overlay: new SaveDashboardDrawer({ dashboardRef: this.getRef(), saveAsCopy, }), }); } public getPageNav(location: H.Location, navIndex: NavIndex) { const { meta, viewPanelScene, editPanel } = this.state; let pageNav: NavModelItem = { text: this.state.title, url: getDashboardUrl({ uid: this.state.uid, slug: meta.slug, currentQueryParams: location.search, updateQuery: { viewPanel: null, inspect: null, editview: null, editPanel: null, tab: null }, }), }; const { folderUid } = meta; if (folderUid) { 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, }; } } if (viewPanelScene) { pageNav = { text: 'View panel', parentItem: pageNav, }; } if (editPanel) { pageNav = { text: 'Edit panel', parentItem: pageNav, }; } return pageNav; } /** * Returns the body (layout) or the full view panel */ public getBodyToRender(): SceneObject { return this.state.viewPanelScene ?? this.state.body; } private startTrackingChanges() { this._changeTrackerSub = this.subscribeToEvent( SceneObjectStateChangedEvent, (event: SceneObjectStateChangedEvent) => { if (event.payload.changedObject instanceof SceneRefreshPicker) { if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'intervals')) { this.setIsDirty(); } } 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(); } } if (event.payload.changedObject instanceof SceneGridItem) { this.setIsDirty(); } 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(); } if (event.payload.changedObject instanceof DashboardControls) { if (Object.prototype.hasOwnProperty.call(event.payload.partialUpdate, 'hideTimeControls')) { this.setIsDirty(); } } } ); } private setIsDirty() { if (!this.state.isDirty) { this.setState({ isDirty: true }); } } private stopTrackingChanges() { this._changeTrackerSub?.unsubscribe(); } public getInitialState(): DashboardSceneState | undefined { return this._initialState; } public addPanel(vizPanel: VizPanel): void { // TODO: need logic for adding a panel when other panels exist // This is the logic when dashboard is empty this.setState({ body: new SceneGridLayout({ children: [ new SceneGridItem({ height: 10, width: 10, x: 0.2, y: 0, body: vizPanel, }), ], }), }); } public duplicatePanel(vizPanel: VizPanel) { if (!vizPanel.parent) { return; } const gridItem = vizPanel.parent; if (!(gridItem instanceof SceneGridItem || PanelRepeaterGridItem)) { 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)); appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Click **Add panel** icon to paste.']); } public showModal(modal: SceneObject) { this.setState({ overlay: modal }); } public closeModal() { this.setState({ overlay: undefined }); } 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); } } public onOpenSettings = () => { locationService.partial({ editview: 'settings' }); }; public isEmpty = (): boolean => { const { body, viewPanelScene } = this.state; if (!!viewPanelScene) { return !!viewPanelScene.state.body; } if (body instanceof SceneFlexLayout || body instanceof SceneGridLayout) { return body.state.children.length === 0; } throw new Error('Invalid body type'); }; /** * Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request */ public enrichDataRequest(sceneObject: SceneObject): Partial<DataQueryRequest> { const panel = getClosestVizPanel(sceneObject); let panelId = 0; if (panel && panel.state.key) { if (isPanelClone(panel.state.key)) { panelId = djb2Hash(panel?.state.key); } else { panelId = getPanelIdForVizPanel(panel); } } return { app: CoreApp.Dashboard, dashboardUID: this.state.uid, panelId, panelPluginId: panel?.state.pluginId, }; } canEditDashboard() { const { meta } = this.state; return Boolean(meta.canEdit || meta.canMakeEditable); } public getInitialSaveModel() { return this._initialSaveModel; } /** Hacky temp function until we refactor transformSaveModelToScene a bit */ public setInitialSaveModel(saveModel: Dashboard) { this._initialSaveModel = saveModel; } } export class DashboardVariableDependency implements SceneVariableDependencyConfigLike { private _emptySet = new Set<string>(); getNames(): Set<string> { return this._emptySet; } public hasDependencyOn(): boolean { return false; } public variableUpdateCompleted(variable: SceneVariable, hasChanged: boolean) { if (hasChanged) { // Temp solution for some core panels (like dashlist) to know that variables have changed appEvents.publish(new VariablesChanged({ refreshAll: true, panelIds: [] })); } } }
Submit
FILE
FOLDER
INFO
Name
Size
Permission
Action
AlertStatesDataLayer.ts
6986 bytes
0644
DashboardAnnotationsDataLayer.test.ts
2000 bytes
0644
DashboardAnnotationsDataLayer.ts
1413 bytes
0644
DashboardControls.tsx
1283 bytes
0644
DashboardLinksControls.tsx
2088 bytes
0644
DashboardMacro.ts
966 bytes
0644
DashboardScene.test.tsx
9118 bytes
0644
DashboardScene.tsx
17603 bytes
0644
DashboardSceneRenderer.tsx
3280 bytes
0644
DashboardSceneUrlSync.test.ts
2767 bytes
0644
DashboardSceneUrlSync.ts
5530 bytes
0644
GoToSnapshotOriginButton.test.tsx
2303 bytes
0644
GoToSnapshotOriginButton.tsx
2007 bytes
0644
LibraryVizPanel.tsx
2245 bytes
0644
NavToolbarActions.test.tsx
2532 bytes
0644
NavToolbarActions.tsx
9842 bytes
0644
PanelLinks.tsx
1943 bytes
0644
PanelMenuBehavior.test.tsx
16261 bytes
0644
PanelMenuBehavior.tsx
14341 bytes
0644
PanelNotices.tsx
1237 bytes
0644
PanelRepeaterGridItem.test.tsx
3838 bytes
0644
PanelRepeaterGridItem.tsx
6699 bytes
0644
PanelTimeRange.test.tsx
2053 bytes
0644
PanelTimeRange.tsx
4071 bytes
0644
RowRepeaterBehavior.test.tsx
4316 bytes
0644
RowRepeaterBehavior.ts
5990 bytes
0644
ViewPanelScene.test.tsx
1875 bytes
0644
ViewPanelScene.tsx
2411 bytes
0644
keyboardShortcuts.ts
4630 bytes
0644
setDashboardPanelContext.test.ts
7884 bytes
0644
setDashboardPanelContext.ts
5957 bytes
0644
types.ts
353 bytes
0644
N4ST4R_ID | Naxtarrr