diff --git a/public/app/features/explore-map/components/ExploreMapPanelContent.tsx b/public/app/features/explore-map/components/ExploreMapPanelContent.tsx
index 5ae9427f26d..d9134f5504f 100644
--- a/public/app/features/explore-map/components/ExploreMapPanelContent.tsx
+++ b/public/app/features/explore-map/components/ExploreMapPanelContent.tsx
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
+import { Trans } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { useDispatch, useSelector } from 'app/types/store';
@@ -15,6 +16,7 @@ interface ExploreMapPanelContentProps {
height: number;
}
+
export function ExploreMapPanelContent({ exploreId }: ExploreMapPanelContentProps) {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
@@ -26,17 +28,25 @@ export function ExploreMapPanelContent({ exploreId }: ExploreMapPanelContentProp
// Check if the explore pane exists in Redux
const explorePane = useSelector((state) => state.explore?.panes?.[exploreId]);
+ // Find the panel with this exploreId to get saved state
+ const panel = useSelector((state) =>
+ Object.values(state.exploreMap.panels).find((p) => p.exploreId === exploreId)
+ );
+
// Initialize Explore pane on mount
useEffect(() => {
const initializePane = async () => {
+ // Use saved state if available, otherwise defaults
+ const savedState = panel?.exploreState;
+
await dispatch(
initializeExplore({
exploreId,
- datasource: undefined,
- queries: [],
- range: DEFAULT_RANGE,
+ datasource: savedState?.datasourceUid,
+ queries: savedState?.queries || [],
+ range: savedState?.range || DEFAULT_RANGE,
eventBridge: eventBus,
- compact: false,
+ compact: savedState?.compact || false,
})
);
setIsInitialized(true);
@@ -48,13 +58,15 @@ export function ExploreMapPanelContent({ exploreId }: ExploreMapPanelContentProp
return () => {
eventBus.removeAllListeners();
};
- }, [dispatch, exploreId, eventBus]);
+ }, [dispatch, exploreId, eventBus, panel?.exploreState]);
// Wait for Redux state to be initialized
if (!isInitialized || !explorePane) {
return (
-
Initializing Explore...
+
+ Initializing Explore...
+
);
}
diff --git a/public/app/features/explore-map/hooks/useCanvasPersistence.ts b/public/app/features/explore-map/hooks/useCanvasPersistence.ts
index d061a257079..8330d7ee2a9 100644
--- a/public/app/features/explore-map/hooks/useCanvasPersistence.ts
+++ b/public/app/features/explore-map/hooks/useCanvasPersistence.ts
@@ -1,43 +1,105 @@
import { useEffect } from 'react';
+import { store } from '@grafana/data';
import { useDispatch, useSelector } from 'app/types/store';
import { loadCanvas } from '../state/exploreMapSlice';
-import { ExploreMapState } from '../state/types';
+import { ExploreMapState, SerializedExploreState } from '../state/types';
const STORAGE_KEY = 'grafana.exploreMap.state';
export function useCanvasPersistence() {
const dispatch = useDispatch();
const exploreMapState = useSelector((state) => state.exploreMap);
+ const exploreState = useSelector((state) => state.explore);
- // Load state from localStorage on mount
- // Note: Explore state is not persisted here - each panel will re-initialize
- // its Explore instance when ExploreMapPanelContent mounts
+ // Load state from storage on mount
useEffect(() => {
try {
- const savedState = localStorage.getItem(STORAGE_KEY);
+ const savedState = store.get(STORAGE_KEY);
if (savedState) {
const parsed: ExploreMapState = JSON.parse(savedState);
dispatch(loadCanvas(parsed));
}
} catch (error) {
- console.error('Failed to load canvas state from localStorage:', error);
+ console.error('Failed to load canvas state from storage:', error);
}
}, [dispatch]);
- // Save state to localStorage whenever it changes
+ // Save state to storage whenever it changes, including Explore state
useEffect(() => {
try {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(exploreMapState));
+ // Enrich exploreMapState with current Explore state for each panel
+ const enrichedState: ExploreMapState = {
+ ...exploreMapState,
+ cursors: {}, // Don't persist cursor state - it's ephemeral
+ panels: Object.fromEntries(
+ Object.entries(exploreMapState.panels).map(([panelId, panel]) => {
+ const explorePane = exploreState.panes?.[panel.exploreId];
+
+ let exploreStateToSave: SerializedExploreState | undefined = undefined;
+ if (explorePane) {
+ exploreStateToSave = {
+ queries: explorePane.queries,
+ datasourceUid: explorePane.datasourceInstance?.uid,
+ range: explorePane.range,
+ refreshInterval: explorePane.refreshInterval,
+ panelsState: explorePane.panelsState,
+ compact: explorePane.compact,
+ };
+ }
+
+ return [
+ panelId,
+ {
+ ...panel,
+ exploreState: exploreStateToSave,
+ },
+ ];
+ })
+ ),
+ };
+
+ store.set(STORAGE_KEY, JSON.stringify(enrichedState));
} catch (error) {
- console.error('Failed to save canvas state to localStorage:', error);
+ console.error('Failed to save canvas state to storage:', error);
}
- }, [exploreMapState]);
+ }, [exploreMapState, exploreState]);
const exportCanvas = () => {
try {
- const dataStr = JSON.stringify(exploreMapState, null, 2);
+ // Enrich with Explore state before exporting
+ const enrichedState: ExploreMapState = {
+ ...exploreMapState,
+ cursors: {}, // Don't export cursor state - it's ephemeral
+ panels: Object.fromEntries(
+ Object.entries(exploreMapState.panels).map(([panelId, panel]) => {
+ const explorePane = exploreState.panes?.[panel.exploreId];
+
+ let exploreStateToSave: SerializedExploreState | undefined = undefined;
+ if (explorePane) {
+ exploreStateToSave = {
+ queries: explorePane.queries,
+ datasourceUid: explorePane.datasourceInstance?.uid,
+ range: explorePane.range,
+ refreshInterval: explorePane.refreshInterval,
+ panelsState: explorePane.panelsState,
+ compact: explorePane.compact,
+ };
+ }
+
+ return [
+ panelId,
+ {
+ ...panel,
+ exploreState: exploreStateToSave,
+ },
+ ];
+ })
+ ),
+ };
+
+ const dataStr = JSON.stringify(enrichedState, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileDefaultName = `explore-map-${new Date().toISOString()}.json`;
@@ -59,7 +121,10 @@ export function useCanvasPersistence() {
input.accept = 'application/json';
input.onchange = (e: Event) => {
- const file = (e.target as HTMLInputElement).files?.[0];
+ if (!(e.target instanceof HTMLInputElement)) {
+ return;
+ }
+ const file = e.target.files?.[0];
if (!file) {
return;
}
@@ -67,8 +132,11 @@ export function useCanvasPersistence() {
const reader = new FileReader();
reader.onload = (event) => {
try {
- const content = event.target?.result as string;
- const parsed: ExploreMapState = JSON.parse(content);
+ const result = event.target?.result;
+ if (typeof result !== 'string') {
+ throw new Error('Invalid file content');
+ }
+ const parsed: ExploreMapState = JSON.parse(result);
dispatch(loadCanvas(parsed));
alert('Canvas imported successfully!');
} catch (error) {
diff --git a/public/app/features/explore-map/hooks/useMockCursors.ts b/public/app/features/explore-map/hooks/useMockCursors.ts
index 9569e261dd0..eee33f4437c 100644
--- a/public/app/features/explore-map/hooks/useMockCursors.ts
+++ b/public/app/features/explore-map/hooks/useMockCursors.ts
@@ -16,11 +16,13 @@ interface MockUser {
pauseUntil: number;
}
-const MOCK_USERS: Array> = [
+const ENABLE_MOCK_USERS = false;
+
+let MOCK_USERS: Array> = ENABLE_MOCK_USERS ? [
{ userId: 'mock-1', userName: 'Christian', color: '#FF6B6B', speed: 8 },
{ userId: 'mock-2', userName: 'Ryan', color: '#4ECDC4', speed: 6 },
{ userId: 'mock-3', userName: 'Marc', color: '#45B7D1', speed: 10 },
-];
+] : [];
// Constrain cursors to upper left area of canvas
const MOVEMENT_AREA = {
diff --git a/public/app/features/explore-map/state/exploreMapSlice.ts b/public/app/features/explore-map/state/exploreMapSlice.ts
index 52619213d08..4707427f64e 100644
--- a/public/app/features/explore-map/state/exploreMapSlice.ts
+++ b/public/app/features/explore-map/state/exploreMapSlice.ts
@@ -3,7 +3,14 @@ import { v4 as uuidv4 } from 'uuid';
import { generateExploreId } from 'app/core/utils/explore';
-import { CanvasViewport, ExploreMapState, initialExploreMapState, PanelPosition, UserCursor } from './types';
+import {
+ CanvasViewport,
+ ExploreMapState,
+ initialExploreMapState,
+ PanelPosition,
+ SerializedExploreState,
+ UserCursor,
+} from './types';
const exploreMapSlice = createSlice({
name: 'exploreMap',
@@ -90,12 +97,23 @@ const exploreMapSlice = createSlice({
y: sourcePanel.position.y + 30,
zIndex: state.nextZIndex,
},
+ exploreState: sourcePanel.exploreState,
};
state.nextZIndex++;
state.selectedPanelId = newPanelId;
}
},
+ savePanelExploreState: (
+ state,
+ action: PayloadAction<{ panelId: string; exploreState: SerializedExploreState }>
+ ) => {
+ const panel = state.panels[action.payload.panelId];
+ if (panel) {
+ panel.exploreState = action.payload.exploreState;
+ }
+ },
+
loadCanvas: (state, action: PayloadAction) => {
return action.payload;
},
@@ -119,6 +137,7 @@ export const {
updateViewport,
resetCanvas,
duplicatePanel,
+ savePanelExploreState,
loadCanvas,
updateCursor,
removeCursor,
diff --git a/public/app/features/explore-map/state/types.ts b/public/app/features/explore-map/state/types.ts
index d677ec1ce00..5809afabf1f 100644
--- a/public/app/features/explore-map/state/types.ts
+++ b/public/app/features/explore-map/state/types.ts
@@ -1,3 +1,5 @@
+import { DataQuery, TimeRange, ExplorePanelsState } from '@grafana/data';
+
export interface PanelPosition {
x: number;
y: number;
@@ -6,10 +8,20 @@ export interface PanelPosition {
zIndex: number;
}
+export interface SerializedExploreState {
+ queries: DataQuery[];
+ datasourceUid?: string;
+ range: TimeRange;
+ refreshInterval?: string;
+ panelsState?: ExplorePanelsState;
+ compact?: boolean;
+}
+
export interface ExploreMapPanel {
id: string;
exploreId: string;
position: PanelPosition;
+ exploreState?: SerializedExploreState;
}
export interface CanvasViewport {