Add cursor modes

This commit is contained in:
Aleksandar Petrov
2025-12-04 09:15:58 -04:00
parent fff727a632
commit 39ff7d4b20
5 changed files with 121 additions and 7 deletions
@@ -10,7 +10,7 @@ import { useTransformContext } from '../context/TransformContext';
import { useCursorSync } from '../hooks/useCursorSync';
import { useCursorViewportTracking } from '../hooks/useCursorViewportTracking';
import { selectPanel as selectPanelCRDT, updateViewport as updateViewportCRDT, selectMultiplePanels as selectMultiplePanelsCRDT } from '../state/crdtSlice';
import { selectPanels, selectFrames, selectViewport, selectCursors, selectSelectedPanelIds, selectMapUid, selectPostItNotes } from '../state/selectors';
import { selectPanels, selectFrames, selectViewport, selectCursors, selectSelectedPanelIds, selectMapUid, selectPostItNotes, selectCursorMode } from '../state/selectors';
import { EdgeCursorIndicator } from './EdgeCursorIndicator';
import { ExploreMapComment } from './ExploreMapComment';
@@ -46,6 +46,7 @@ export function ExploreMapCanvas() {
const cursors = useSelector((state) => selectCursors(state.exploreMapCRDT));
const selectedPanelIds = useSelector((state) => selectSelectedPanelIds(state.exploreMapCRDT));
const mapUid = useSelector((state) => selectMapUid(state.exploreMapCRDT));
const cursorMode = useSelector((state) => selectCursorMode(state.exploreMapCRDT));
// Initialize cursor sync
const { updatePosition, updatePositionImmediate } = useCursorSync({
@@ -131,6 +132,11 @@ export function ExploreMapCanvas() {
const handleCanvasMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
// Don't allow selection in hand mode (panning mode)
if (cursorMode === 'hand') {
return;
}
// Only start selection if clicking directly on canvas (not on panels)
if (e.target !== e.currentTarget) {
return;
@@ -166,7 +172,7 @@ export function ExploreMapCanvas() {
});
setIsSelecting(true);
},
[contextTransformRef, viewport]
[contextTransformRef, viewport, cursorMode]
);
const handleCanvasMouseMove = useCallback(
@@ -325,7 +331,7 @@ export function ExploreMapCanvas() {
panning={{
disabled: false,
excluded: ['panel-drag-handle', 'react-rnd'],
allowLeftClickPan: false,
allowLeftClickPan: cursorMode === 'hand',
allowRightClickPan: false,
allowMiddleClickPan: true,
}}
@@ -333,7 +339,10 @@ export function ExploreMapCanvas() {
doubleClick={{ disabled: true }}
wheel={{ step: 0.1 }}
>
<TransformComponent wrapperClass={styles.transformWrapper} contentClass={styles.transformContent}>
<TransformComponent
wrapperClass={`${styles.transformWrapper} ${cursorMode === 'hand' ? styles.handCursor : styles.pointerCursor}`}
contentClass={styles.transformContent}
>
<div
ref={canvasRef}
className={styles.canvas}
@@ -403,6 +412,11 @@ const getStyles = (theme: GrafanaTheme2) => {
transformWrapper: css({
width: '100%',
height: '100%',
}),
pointerCursor: css({
cursor: 'default',
}),
handCursor: css({
cursor: 'grab',
'&:active': {
cursor: 'grabbing',
@@ -13,9 +13,8 @@ import prometheusLogoSvg from 'app/plugins/datasource/prometheus/img/prometheus_
import tempoLogoSvg from 'app/plugins/datasource/tempo/img/tempo_logo.svg';
import { useDispatch, useSelector } from 'app/types/store';
import { addPanel, addFrame, addPostItNote } from '../state/crdtSlice';
import { selectPanels, selectMapUid, selectViewport, selectSelectedPanelIds } from '../state/selectors';
import { selectPanels, selectMapUid } from '../state/selectors';
import { addPanel, addFrame, addPostItNote, setCursorMode } from '../state/crdtSlice';
import { selectPanels, selectMapUid, selectViewport, selectSelectedPanelIds, selectCursorMode } from '../state/selectors';
import { AddPanelAction } from './AssistantComponents';
@@ -33,6 +32,7 @@ export function ExploreMapFloatingToolbar() {
const mapUid = useSelector((state) => selectMapUid(state.exploreMapCRDT));
const viewport = useSelector((state) => selectViewport(state.exploreMapCRDT));
const selectedPanelIds = useSelector((state) => selectSelectedPanelIds(state.exploreMapCRDT));
const cursorMode = useSelector((state) => selectCursorMode(state.exploreMapCRDT));
const handleAddPanel = useCallback(() => {
dispatch(
@@ -171,6 +171,14 @@ export function ExploreMapFloatingToolbar() {
setIsOpen(false);
}, [dispatch, currentUsername]);
const handleSetPointerMode = useCallback(() => {
dispatch(setCursorMode({ mode: 'pointer' }));
}, [dispatch]);
const handleSetHandMode = useCallback(() => {
dispatch(setCursorMode({ mode: 'hand' }));
}, [dispatch]);
// Build context for assistant
const canvasContext = useMemo(() => {
const panelsArray = Object.values(panels);
@@ -310,6 +318,38 @@ CRITICAL:
return (
<div className={styles.floatingToolbar}>
<div className={styles.cursorModeGroup}>
<button
className={`${styles.cursorModeButton} ${cursorMode === 'pointer' ? styles.cursorModeButtonActive : ''}`}
onClick={handleSetPointerMode}
title="Pointer mode (V)"
aria-label="Pointer mode"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className={styles.cursorIcon}>
<path
d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87a.5.5 0 0 0 .35-.85L6.35 2.85a.5.5 0 0 0-.85.35Z"
fill="currentColor"
/>
</svg>
</button>
<button
className={`${styles.cursorModeButton} ${cursorMode === 'hand' ? styles.cursorModeButtonActive : ''}`}
onClick={handleSetHandMode}
title="Hand mode (H)"
aria-label="Hand mode"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" className={styles.cursorIcon}>
<path
d="M11.5 8.5V4a1.5 1.5 0 0 0-3 0v6.5m3-2V5a1.5 1.5 0 0 1 3 0v5m0-2.5V5a1.5 1.5 0 0 1 3 0v7m0-2.5V7a1.5 1.5 0 0 1 3 0v8.5c0 2.5-2 4.5-4.5 4.5h-2.5C11 20 9 18.5 9 16v-3.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
<div className={styles.toolbarDivider} />
<ButtonGroup>
<Button icon="plus" onClick={handleAddPanel} variant="primary">
<Trans i18nKey="explore-map.toolbar.add-panel">Add panel</Trans>
@@ -368,6 +408,45 @@ const getStyles = (theme: GrafanaTheme2) => {
boxShadow: theme.shadows.z3,
zIndex: 1000,
}),
cursorModeGroup: css({
display: 'flex',
gap: '2px',
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.radius.default,
padding: '2px',
}),
cursorModeButton: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '40px',
height: '40px',
border: 'none',
borderRadius: theme.shape.radius.default,
backgroundColor: 'transparent',
color: theme.colors.text.secondary,
cursor: 'pointer',
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: theme.colors.action.hover,
color: theme.colors.text.primary,
},
}),
cursorModeButtonActive: css({
backgroundColor: theme.colors.background.primary,
color: theme.colors.text.primary,
boxShadow: theme.shadows.z1,
}),
cursorIcon: css({
width: '20px',
height: '20px',
}),
toolbarDivider: css({
width: '1px',
height: '32px',
backgroundColor: theme.colors.border.weak,
margin: `0 ${theme.spacing(1)}`,
}),
};
};
@@ -41,6 +41,7 @@ export interface ExploreMapCRDTState {
viewport: CanvasViewport;
selectedPanelIds: string[];
cursors: Record<string, UserCursor>;
cursorMode: 'pointer' | 'hand';
isOnline: boolean;
isSyncing: boolean;
activeDrag?: {
@@ -80,6 +81,7 @@ export function createInitialCRDTState(mapUid?: string): ExploreMapCRDTState {
viewport: initialViewport,
selectedPanelIds: [],
cursors: {},
cursorMode: 'pointer',
isOnline: false,
isSyncing: false,
activeDrag: undefined,
@@ -1102,6 +1104,13 @@ const crdtSlice = createSlice({
delete state.local.cursors[action.payload.sessionId];
},
/**
* Set cursor mode (pointer or hand)
*/
setCursorMode: (state, action: PayloadAction<{ mode: 'pointer' | 'hand' }>) => {
state.local.cursorMode = action.payload.mode;
},
/**
* Set online status
*/
@@ -1194,6 +1203,7 @@ export const {
selectMultiplePanels,
updateCursor,
removeCursor,
setCursorMode,
setOnlineStatus,
setSyncingStatus,
setActiveDrag,
@@ -294,6 +294,13 @@ export const selectCursors = (state: ExploreMapCRDTState) => {
return state.local.cursors;
};
/**
* Select cursor mode
*/
export const selectCursorMode = (state: ExploreMapCRDTState) => {
return state.local.cursorMode;
};
/**
* Select online status
*/
@@ -76,6 +76,8 @@ export interface UserCursor {
selectedPanelIds: string[];
}
export type CursorMode = 'pointer' | 'hand';
export interface ExploreMapState {
uid?: string;
title?: string;
@@ -85,6 +87,7 @@ export interface ExploreMapState {
selectedPanelIds: string[];
nextZIndex: number;
cursors: Record<string, UserCursor>;
cursorMode: CursorMode;
crdtState?: any; // Raw CRDT state JSON for proper sync
}
@@ -105,4 +108,5 @@ export const initialExploreMapState: ExploreMapState = {
selectedPanelIds: [],
nextZIndex: 1,
cursors: {},
cursorMode: 'pointer',
};