WIP: GraCoCa - Vibe coding session #1

This commit is contained in:
Christian Simon
2025-11-27 17:07:01 +00:00
parent 8e4be891c5
commit adbbe76dc7
17 changed files with 906 additions and 0 deletions
+2
View File
@@ -404,6 +404,7 @@
"react-redux": "9.2.0",
"react-resizable": "3.0.5",
"react-responsive-carousel": "^3.2.23",
"react-rnd": "10.4.13",
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"react-router-dom-v5-compat": "^6.26.1",
@@ -416,6 +417,7 @@
"react-virtualized-auto-sizer": "1.0.26",
"react-window": "1.8.11",
"react-window-infinite-loader": "1.0.10",
"react-zoom-pan-pinch": "3.6.1",
"reduce-reducers": "^1.0.4",
"redux": "5.0.1",
"redux-thunk": "3.1.0",
+1
View File
@@ -41,6 +41,7 @@ const (
NavIDRoot = "root"
NavIDDashboards = "dashboards/browse"
NavIDExplore = "explore"
NavIDExploreMap = "explore-map"
NavIDDrilldown = "drilldown"
NavIDAdaptiveTelemetry = "adaptive-telemetry"
NavIDCfg = "cfg" // NavIDCfg is the id for org configuration navigation node
@@ -134,6 +134,17 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
})
}
if s.cfg.ExploreEnabled && hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) {
treeRoot.AddSection(&navtree.NavLink{
Text: "Explore Map",
Id: navtree.NavIDExploreMap,
SubTitle: "Explore your data on an open canvas",
Icon: "apps",
SortWeight: navtree.WeightExplore + 1,
Url: s.cfg.AppSubURL + "/explore-map",
})
}
if hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) {
treeRoot.AddSection(&navtree.NavLink{
Text: "Drilldown",
+2
View File
@@ -15,6 +15,7 @@ import panelEditorReducers from 'app/features/dashboard/components/PanelEditor/s
import dashboardReducers from 'app/features/dashboard/state/reducers';
import dataSourcesReducers from 'app/features/datasources/state/reducers';
import exploreReducers from 'app/features/explore/state/main';
import exploreMapReducers from 'app/features/explore-map/state/reducers';
import invitesReducers from 'app/features/invites/state/reducers';
import importDashboardReducers from 'app/features/manage-dashboards/state/reducers';
import organizationReducers from 'app/features/org/state/reducers';
@@ -36,6 +37,7 @@ const rootReducers = {
...teamsReducers,
...dashboardReducers,
...exploreReducers,
...exploreMapReducers,
...dataSourcesReducers,
...usersReducers,
...serviceAccountsReducer,
@@ -60,6 +60,8 @@ export function getNavTitle(navId: string | undefined) {
return t('nav.scenes.title', 'Scenes');
case 'explore':
return t('nav.explore.title', 'Explore');
case 'explore-map':
return t('nav.explore-map.title', 'Explore Map');
case 'drilldown':
return t('nav.drilldown.title', 'Drilldown');
case 'alerting':
@@ -0,0 +1,58 @@
import { css } from '@emotion/css';
import { useEffect, useRef } from 'react';
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { ErrorBoundaryAlert, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { ExploreMapCanvas } from './components/ExploreMapCanvas';
import { ExploreMapToolbar } from './components/ExploreMapToolbar';
import { TransformProvider } from './context/TransformContext';
import { useCanvasPersistence } from './hooks/useCanvasPersistence';
export default function ExploreMapPage(props: GrafanaRouteComponentProps) {
const styles = useStyles2(getStyles);
const { chrome } = useGrafana();
const navModel = useNavModel('explore-map');
const transformRef = useRef<ReactZoomPanPinchRef>(null);
// Initialize canvas persistence
useCanvasPersistence();
useEffect(() => {
chrome.update({
sectionNav: navModel,
});
}, [chrome, navModel]);
return (
<ErrorBoundaryAlert>
<TransformProvider value={{ transformRef }}>
<div className={styles.pageWrapper}>
<h1 className="sr-only">
<Trans i18nKey="nav.explore-map.title">Explore Map</Trans>
</h1>
<ExploreMapToolbar />
<ExploreMapCanvas />
</div>
</TransformProvider>
</ErrorBoundaryAlert>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
pageWrapper: css({
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
backgroundColor: theme.colors.background.primary,
}),
};
};
@@ -0,0 +1,109 @@
import { css } from '@emotion/css';
import { useCallback, useRef } from 'react';
import { TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { useDispatch, useSelector } from 'app/types/store';
import { useTransformContext } from '../context/TransformContext';
import { selectPanel, updateViewport } from '../state/exploreMapSlice';
import { ExploreMapPanelContainer } from './ExploreMapPanelContainer';
export function ExploreMapCanvas() {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const canvasRef = useRef<HTMLDivElement>(null);
const { transformRef: contextTransformRef } = useTransformContext();
const panels = useSelector((state) => state.exploreMap.panels);
const viewport = useSelector((state) => state.exploreMap.viewport);
const handleCanvasClick = useCallback(
(e: React.MouseEvent) => {
// Only deselect if clicking directly on canvas (not on panels)
if (e.target === e.currentTarget) {
dispatch(selectPanel({ panelId: undefined }));
}
},
[dispatch]
);
const handleTransformChange = useCallback(
(ref: any) => {
dispatch(
updateViewport({
zoom: ref.state.scale,
panX: ref.state.positionX,
panY: ref.state.positionY,
})
);
},
[dispatch]
);
return (
<div className={styles.canvasWrapper}>
<TransformWrapper
ref={contextTransformRef as any}
initialScale={viewport.zoom}
initialPositionX={viewport.panX}
initialPositionY={viewport.panY}
minScale={0.1}
maxScale={4}
limitToBounds={false}
centerOnInit={false}
panning={{
disabled: false,
excluded: ['panel-drag-handle', 'react-rnd'],
}}
onTransformed={handleTransformChange}
doubleClick={{ disabled: true }}
wheel={{ step: 0.1 }}
>
<TransformComponent wrapperClass={styles.transformWrapper} contentClass={styles.transformContent}>
<div ref={canvasRef} className={styles.canvas} onClick={handleCanvasClick}>
{Object.values(panels).map((panel) => (
<ExploreMapPanelContainer key={panel.id} panel={panel} />
))}
</div>
</TransformComponent>
</TransformWrapper>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
canvasWrapper: css({
position: 'relative',
flex: 1,
overflow: 'hidden',
backgroundColor: theme.colors.background.canvas,
}),
transformWrapper: css({
width: '100%',
height: '100%',
cursor: 'grab',
'&:active': {
cursor: 'grabbing',
},
}),
transformContent: css({
width: '100%',
height: '100%',
}),
canvas: css({
position: 'relative',
width: '5000px',
height: '5000px',
backgroundImage: `
linear-gradient(${theme.colors.border.weak} 1px, transparent 1px),
linear-gradient(90deg, ${theme.colors.border.weak} 1px, transparent 1px)
`,
backgroundSize: '20px 20px',
backgroundPosition: '-1px -1px',
}),
};
};
@@ -0,0 +1,180 @@
import { css, cx } from '@emotion/css';
import { useCallback, useRef } from 'react';
import { Rnd } from 'react-rnd';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui';
import { useDispatch, useSelector } from 'app/types/store';
import { splitClose } from '../../explore/state/main';
import {
bringPanelToFront,
duplicatePanel,
removePanel,
selectPanel,
updatePanelPosition,
} from '../state/exploreMapSlice';
import { ExploreMapPanel } from '../state/types';
import { ExploreMapPanelContent } from './ExploreMapPanelContent';
interface ExploreMapPanelContainerProps {
panel: ExploreMapPanel;
}
export function ExploreMapPanelContainer({ panel }: ExploreMapPanelContainerProps) {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const rndRef = useRef<Rnd>(null);
const selectedPanelId = useSelector((state) => state.exploreMap.selectedPanelId);
const isSelected = selectedPanelId === panel.id;
const handleDragStop = useCallback(
(e: any, data: any) => {
dispatch(
updatePanelPosition({
panelId: panel.id,
position: { x: data.x, y: data.y },
})
);
},
[dispatch, panel.id]
);
const handleResizeStop = useCallback(
(e: any, direction: any, ref: any, delta: any, position: any) => {
const newWidth = ref.offsetWidth;
const newHeight = ref.offsetHeight;
dispatch(
updatePanelPosition({
panelId: panel.id,
position: {
x: position.x,
y: position.y,
width: newWidth,
height: newHeight,
},
})
);
// Trigger resize event for Explore components
window.dispatchEvent(new Event('resize'));
},
[dispatch, panel.id]
);
const handleMouseDown = useCallback(() => {
dispatch(selectPanel({ panelId: panel.id }));
dispatch(bringPanelToFront({ panelId: panel.id }));
}, [dispatch, panel.id]);
const handleRemove = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
// Clean up Explore state first
dispatch(splitClose(panel.exploreId));
// Then remove panel
dispatch(removePanel({ panelId: panel.id }));
},
[dispatch, panel.id, panel.exploreId]
);
const handleDuplicate = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
dispatch(duplicatePanel({ panelId: panel.id }));
},
[dispatch, panel.id]
);
return (
<Rnd
ref={rndRef}
position={{ x: panel.position.x, y: panel.position.y }}
size={{ width: panel.position.width, height: panel.position.height }}
onDragStop={handleDragStop}
onResizeStop={handleResizeStop}
onMouseDown={handleMouseDown}
bounds="parent"
dragHandleClassName="panel-drag-handle"
className={cx(styles.panelContainer, { [styles.selectedPanel]: isSelected })}
style={{ zIndex: panel.position.zIndex }}
minWidth={300}
minHeight={200}
>
<div className={styles.panel}>
<div className={cx(styles.panelHeader, 'panel-drag-handle')}>
<div className={styles.panelTitle}>Explore Panel {panel.id.slice(0, 8)}</div>
<div className={styles.panelActions}>
<Button
icon="copy"
variant="secondary"
size="sm"
fill="text"
onClick={handleDuplicate}
tooltip="Duplicate panel"
/>
<Button icon="times" variant="secondary" size="sm" fill="text" onClick={handleRemove} tooltip="Remove" />
</div>
</div>
<div className={styles.panelContent}>
<ExploreMapPanelContent
exploreId={panel.exploreId}
width={panel.position.width}
height={panel.position.height - 36}
/>
</div>
</div>
</Rnd>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
panelContainer: css({
cursor: 'default',
}),
panel: css({
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.colors.background.primary,
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
overflow: 'hidden',
boxShadow: theme.shadows.z2,
}),
selectedPanel: css({
'& > div': {
border: `2px solid ${theme.colors.primary.border}`,
},
}),
panelHeader: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(1, 1.5),
backgroundColor: theme.colors.background.secondary,
borderBottom: `1px solid ${theme.colors.border.weak}`,
cursor: 'move',
minHeight: '36px',
}),
panelTitle: css({
fontSize: theme.typography.body.fontSize,
fontWeight: theme.typography.fontWeightMedium,
userSelect: 'none',
}),
panelActions: css({
display: 'flex',
gap: theme.spacing(0.5),
}),
panelContent: css({
flex: 1,
overflow: 'hidden',
position: 'relative',
}),
};
};
@@ -0,0 +1,92 @@
import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react';
import { EventBusSrv, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { useDispatch, useSelector } from 'app/types/store';
import { ExplorePaneContainer } from '../../explore/ExplorePaneContainer';
import { DEFAULT_RANGE } from '../../explore/state/constants';
import { initializeExplore } from '../../explore/state/explorePane';
interface ExploreMapPanelContentProps {
exploreId: string;
width: number;
height: number;
}
export function ExploreMapPanelContent({ exploreId }: ExploreMapPanelContentProps) {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const [isInitialized, setIsInitialized] = useState(false);
// Create scoped event bus for this panel
const eventBus = useMemo(() => new EventBusSrv(), []);
// Check if the explore pane exists in Redux
const explorePane = useSelector((state) => state.explore?.panes?.[exploreId]);
// Initialize Explore pane on mount
useEffect(() => {
const initializePane = async () => {
await dispatch(
initializeExplore({
exploreId,
datasource: undefined,
queries: [],
range: DEFAULT_RANGE,
eventBridge: eventBus,
compact: false,
})
);
setIsInitialized(true);
};
initializePane();
// Cleanup on unmount
return () => {
eventBus.removeAllListeners();
};
}, [dispatch, exploreId, eventBus]);
// Wait for Redux state to be initialized
if (!isInitialized || !explorePane) {
return (
<div className={styles.container}>
<div className={styles.loading}>Initializing Explore...</div>
</div>
);
}
return (
<div className={styles.container}>
<ExplorePaneContainer exploreId={exploreId} />
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
width: '100%',
height: '100%',
overflow: 'hidden',
position: 'relative',
// Override Explore styles to fit in panel
'& .explore-container': {
padding: 0,
height: '100%',
},
}),
loading: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
color: theme.colors.text.secondary,
fontSize: theme.typography.body.fontSize,
}),
};
};
@@ -0,0 +1,110 @@
import { css } from '@emotion/css';
import { useCallback } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, ButtonGroup, ToolbarButton, useStyles2 } from '@grafana/ui';
import { useDispatch, useSelector } from 'app/types/store';
import { useTransformContext } from '../context/TransformContext';
import { useCanvasPersistence } from '../hooks/useCanvasPersistence';
import { addPanel, resetCanvas } from '../state/exploreMapSlice';
export function ExploreMapToolbar() {
const styles = useStyles2(getStyles);
const dispatch = useDispatch();
const { exportCanvas, importCanvas } = useCanvasPersistence();
const { transformRef } = useTransformContext();
const panelCount = useSelector((state) => Object.keys(state.exploreMap.panels).length);
const viewport = useSelector((state) => state.exploreMap.viewport);
const handleAddPanel = useCallback(() => {
dispatch(addPanel({}));
}, [dispatch]);
const handleResetCanvas = useCallback(() => {
if (confirm('Are you sure you want to clear all panels? This cannot be undone.')) {
dispatch(resetCanvas());
}
}, [dispatch]);
const handleZoomIn = useCallback(() => {
if (transformRef?.current) {
transformRef.current.zoomIn(0.2);
}
}, [transformRef]);
const handleZoomOut = useCallback(() => {
if (transformRef?.current) {
transformRef.current.zoomOut(0.2);
}
}, [transformRef]);
const handleResetZoom = useCallback(() => {
if (transformRef?.current) {
transformRef.current.resetTransform();
}
}, [transformRef]);
const handleExport = useCallback(() => {
exportCanvas();
}, [exportCanvas]);
const handleImport = useCallback(() => {
importCanvas();
}, [importCanvas]);
return (
<div className={styles.toolbar}>
<div className={styles.toolbarSection}>
<Button icon="plus" onClick={handleAddPanel} variant="primary">
Add Panel
</Button>
<span className={styles.panelCount}>{panelCount} panels</span>
</div>
<div className={styles.toolbarSection}>
<ButtonGroup>
<ToolbarButton icon="search-minus" onClick={handleZoomOut} tooltip="Zoom out" />
<ToolbarButton onClick={handleResetZoom} tooltip="Reset zoom">
{Math.round(viewport.zoom * 100)}%
</ToolbarButton>
<ToolbarButton icon="search-plus" onClick={handleZoomIn} tooltip="Zoom in" />
</ButtonGroup>
</div>
<div className={styles.toolbarSection}>
<ButtonGroup>
<ToolbarButton icon="save" onClick={handleExport} tooltip="Export canvas" />
<ToolbarButton icon="upload" onClick={handleImport} tooltip="Import canvas" />
<ToolbarButton icon="trash-alt" onClick={handleResetCanvas} tooltip="Clear all panels" variant="destructive" />
</ButtonGroup>
</div>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
toolbar: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(1, 2),
backgroundColor: theme.colors.background.secondary,
borderBottom: `1px solid ${theme.colors.border.weak}`,
minHeight: '48px',
gap: theme.spacing(2),
}),
toolbarSection: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
}),
panelCount: css({
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.secondary,
marginLeft: theme.spacing(1),
}),
};
};
@@ -0,0 +1,14 @@
import { createContext, useContext } from 'react';
import { ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
interface TransformContextType {
transformRef: React.RefObject<ReactZoomPanPinchRef> | null;
}
const TransformContext = createContext<TransformContextType>({ transformRef: null });
export const useTransformContext = () => {
return useContext(TransformContext);
};
export const TransformProvider = TransformContext.Provider;
@@ -0,0 +1,93 @@
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'app/types/store';
import { loadCanvas } from '../state/exploreMapSlice';
import { ExploreMapState } from '../state/types';
const STORAGE_KEY = 'grafana.exploreMap.state';
export function useCanvasPersistence() {
const dispatch = useDispatch();
const exploreMapState = useSelector((state) => state.exploreMap);
// Load state from localStorage on mount
// Note: Explore state is not persisted here - each panel will re-initialize
// its Explore instance when ExploreMapPanelContent mounts
useEffect(() => {
try {
const savedState = localStorage.getItem(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);
}
}, [dispatch]);
// Save state to localStorage whenever it changes
useEffect(() => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(exploreMapState));
} catch (error) {
console.error('Failed to save canvas state to localStorage:', error);
}
}, [exploreMapState]);
const exportCanvas = () => {
try {
const dataStr = JSON.stringify(exploreMapState, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const exportFileDefaultName = `explore-map-${new Date().toISOString()}.json`;
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', exportFileDefaultName);
linkElement.click();
} catch (error) {
console.error('Failed to export canvas:', error);
alert('Failed to export canvas. Check console for details.');
}
};
const importCanvas = () => {
try {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (event) => {
try {
const content = event.target?.result as string;
const parsed: ExploreMapState = JSON.parse(content);
dispatch(loadCanvas(parsed));
alert('Canvas imported successfully!');
} catch (error) {
console.error('Failed to parse imported canvas:', error);
alert('Failed to import canvas. Invalid file format.');
}
};
reader.readAsText(file);
};
input.click();
} catch (error) {
console.error('Failed to import canvas:', error);
alert('Failed to import canvas. Check console for details.');
}
};
return {
exportCanvas,
importCanvas,
};
}
@@ -0,0 +1,117 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { v4 as uuidv4 } from 'uuid';
import { generateExploreId } from 'app/core/utils/explore';
import { CanvasViewport, ExploreMapState, initialExploreMapState, PanelPosition } from './types';
const exploreMapSlice = createSlice({
name: 'exploreMap',
initialState: initialExploreMapState,
reducers: {
addPanel: (state, action: PayloadAction<{ position?: Partial<PanelPosition> }>) => {
const panelId = uuidv4();
const exploreId = generateExploreId();
const defaultPosition: PanelPosition = {
x: 100 + Object.keys(state.panels).length * 50,
y: 100 + Object.keys(state.panels).length * 50,
width: 600,
height: 400,
zIndex: state.nextZIndex,
};
state.panels[panelId] = {
id: panelId,
exploreId: exploreId,
position: { ...defaultPosition, ...action.payload.position },
};
state.nextZIndex++;
state.selectedPanelId = panelId;
},
removePanel: (state, action: PayloadAction<{ panelId: string }>) => {
delete state.panels[action.payload.panelId];
if (state.selectedPanelId === action.payload.panelId) {
state.selectedPanelId = undefined;
}
},
updatePanelPosition: (
state,
action: PayloadAction<{ panelId: string; position: Partial<PanelPosition> }>
) => {
const panel = state.panels[action.payload.panelId];
if (panel) {
panel.position = { ...panel.position, ...action.payload.position };
}
},
bringPanelToFront: (state, action: PayloadAction<{ panelId: string }>) => {
const panel = state.panels[action.payload.panelId];
if (panel) {
panel.position.zIndex = state.nextZIndex;
state.nextZIndex++;
}
},
selectPanel: (state, action: PayloadAction<{ panelId?: string }>) => {
state.selectedPanelId = action.payload.panelId;
if (action.payload.panelId) {
const panel = state.panels[action.payload.panelId];
if (panel) {
panel.position.zIndex = state.nextZIndex;
state.nextZIndex++;
}
}
},
updateViewport: (state, action: PayloadAction<Partial<CanvasViewport>>) => {
state.viewport = { ...state.viewport, ...action.payload };
},
resetCanvas: (state) => {
state.panels = {};
state.selectedPanelId = undefined;
state.nextZIndex = 1;
state.viewport = initialExploreMapState.viewport;
},
duplicatePanel: (state, action: PayloadAction<{ panelId: string }>) => {
const sourcePanel = state.panels[action.payload.panelId];
if (sourcePanel) {
const newPanelId = uuidv4();
const newExploreId = generateExploreId();
state.panels[newPanelId] = {
id: newPanelId,
exploreId: newExploreId,
position: {
...sourcePanel.position,
x: sourcePanel.position.x + 30,
y: sourcePanel.position.y + 30,
zIndex: state.nextZIndex,
},
};
state.nextZIndex++;
state.selectedPanelId = newPanelId;
}
},
loadCanvas: (state, action: PayloadAction<ExploreMapState>) => {
return action.payload;
},
},
});
export const {
addPanel,
removePanel,
updatePanelPosition,
bringPanelToFront,
selectPanel,
updateViewport,
resetCanvas,
duplicatePanel,
loadCanvas,
} = exploreMapSlice.actions;
export const exploreMapReducer = exploreMapSlice.reducer;
@@ -0,0 +1,5 @@
import { exploreMapReducer } from './exploreMapSlice';
export default {
exploreMap: exploreMapReducer,
};
@@ -0,0 +1,37 @@
export interface PanelPosition {
x: number;
y: number;
width: number;
height: number;
zIndex: number;
}
export interface ExploreMapPanel {
id: string;
exploreId: string;
position: PanelPosition;
}
export interface CanvasViewport {
zoom: number;
panX: number;
panY: number;
}
export interface ExploreMapState {
viewport: CanvasViewport;
panels: Record<string, ExploreMapPanel>;
selectedPanelId?: string;
nextZIndex: number;
}
export const initialExploreMapState: ExploreMapState = {
viewport: {
zoom: 1,
panX: 0,
panY: 0,
},
panels: {},
selectedPanelId: undefined,
nextZIndex: 1,
};
+10
View File
@@ -170,6 +170,16 @@ export function getAppRoutes(): RouteDescriptor[] {
: import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage')
),
},
{
path: '/explore-map',
pageClass: 'page-explore-map',
roles: () => contextSrv.evaluatePermission([AccessControlAction.DataSourcesExplore]),
component: SafeDynamicImport(() =>
config.exploreEnabled
? import(/* webpackChunkName: "explore-map" */ 'app/features/explore-map/ExploreMapPage')
: import(/* webpackChunkName: "explore-feature-toggle-page" */ 'app/features/explore/FeatureTogglePage')
),
},
{
path: '/drilldown',
component: () => <NavLandingPage navId="drilldown" />,
+63
View File
@@ -14005,6 +14005,13 @@ __metadata:
languageName: node
linkType: hard
"clsx@npm:^1.1.1":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
checksum: 10/5ded6f61f15f1fa0350e691ccec43a28b12fb8e64c8e94715f2a937bc3722d4c3ed41d6e945c971fc4dcc2a7213a43323beaf2e1c28654af63ba70c9968a8643
languageName: node
linkType: hard
"clsx@npm:^2.0.0, clsx@npm:^2.1.1":
version: 2.1.1
resolution: "clsx@npm:2.1.1"
@@ -19489,6 +19496,7 @@ __metadata:
react-refresh: "npm:0.14.0"
react-resizable: "npm:3.0.5"
react-responsive-carousel: "npm:^3.2.23"
react-rnd: "npm:10.4.13"
react-router: "npm:5.3.4"
react-router-dom: "npm:5.3.4"
react-router-dom-v5-compat: "npm:^6.26.1"
@@ -19502,6 +19510,7 @@ __metadata:
react-virtualized-auto-sizer: "npm:1.0.26"
react-window: "npm:1.8.11"
react-window-infinite-loader: "npm:1.0.10"
react-zoom-pan-pinch: "npm:3.6.1"
reduce-reducers: "npm:^1.0.4"
redux: "npm:5.0.1"
redux-mock-store: "npm:1.5.5"
@@ -28122,6 +28131,16 @@ __metadata:
languageName: node
linkType: hard
"re-resizable@npm:6.10.0":
version: 6.10.0
resolution: "re-resizable@npm:6.10.0"
peerDependencies:
react: ^16.13.1 || ^17.0.0 || ^18.0.0
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
checksum: 10/303e582feffdfd3e491e2b51dc75c8641c726882e2d8e3ec249b2bc1d23bfffa73bd4a982ed5456bcab9ec25f52b5430e8c632f32647295ed773679691a961a2
languageName: node
linkType: hard
"re-resizable@npm:6.11.2":
version: 6.11.2
resolution: "re-resizable@npm:6.11.2"
@@ -28307,6 +28326,19 @@ __metadata:
languageName: node
linkType: hard
"react-draggable@npm:4.4.6":
version: 4.4.6
resolution: "react-draggable@npm:4.4.6"
dependencies:
clsx: "npm:^1.1.1"
prop-types: "npm:^15.8.1"
peerDependencies:
react: ">= 16.3.0"
react-dom: ">= 16.3.0"
checksum: 10/51b9ac7f913797fc1cebc30ae383f346883033c45eb91e9b0b92e9ebd224bb1545b4ae2391825b649b798cc711a38351a5f41be24d949c64c6703ebc24eba661
languageName: node
linkType: hard
"react-draggable@npm:4.5.0, react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5":
version: 4.5.0
resolution: "react-draggable@npm:4.5.0"
@@ -28671,6 +28703,20 @@ __metadata:
languageName: node
linkType: hard
"react-rnd@npm:10.4.13":
version: 10.4.13
resolution: "react-rnd@npm:10.4.13"
dependencies:
re-resizable: "npm:6.10.0"
react-draggable: "npm:4.4.6"
tslib: "npm:2.6.2"
peerDependencies:
react: ">=16.3.0"
react-dom: ">=16.3.0"
checksum: 10/3a343d71117c37e105286eeee38e244d9dc62e0e4b2efa929128995056408cf7fda15f5100f66b8548370ae63f886b94c7851e862fd90dd0b543ec382f43c4f1
languageName: node
linkType: hard
"react-router-dom-v5-compat@npm:^6.26.1":
version: 6.26.1
resolution: "react-router-dom-v5-compat@npm:6.26.1"
@@ -28987,6 +29033,16 @@ __metadata:
languageName: node
linkType: hard
"react-zoom-pan-pinch@npm:3.6.1":
version: 3.6.1
resolution: "react-zoom-pan-pinch@npm:3.6.1"
peerDependencies:
react: "*"
react-dom: "*"
checksum: 10/9146aa5c427dd6d0c8a4ebe3db0c720718eef6262d1b4b36033ee433bc76a9c84e30ca91311211ab95446305d3e2813d9abc576d093efbf5562be984431896cb
languageName: node
linkType: hard
"react@npm:18.3.1":
version: 18.3.1
resolution: "react@npm:18.3.1"
@@ -32660,6 +32716,13 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:2.6.2":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca
languageName: node
linkType: hard
"tslib@npm:2.6.3":
version: 2.6.3
resolution: "tslib@npm:2.6.3"