Add cursor modes
This commit is contained in:
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user