diff --git a/public/app/features/explore-map/components/ExploreMapCanvas.tsx b/public/app/features/explore-map/components/ExploreMapCanvas.tsx index 046f87f8c66..1b2e6677467 100644 --- a/public/app/features/explore-map/components/ExploreMapCanvas.tsx +++ b/public/app/features/explore-map/components/ExploreMapCanvas.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from 'react-zoom-pan-pinch'; import { GrafanaTheme2 } from '@grafana/data'; @@ -38,11 +38,17 @@ export function ExploreMapCanvas() { const mapUid = useSelector((state) => selectMapUid(state.exploreMapCRDT)); // Initialize cursor sync - const { updatePosition } = useCursorSync({ + const { updatePosition, updatePositionImmediate } = useCursorSync({ mapUid: mapUid || '', enabled: !!mapUid, }); + // Send selection update immediately when selection changes + // Use position (0, 0) since we only care about the selection, not cursor position + useEffect(() => { + updatePositionImmediate(0, 0, selectedPanelIds); + }, [selectedPanelIds, updatePositionImmediate]); + const handleCanvasClick = useCallback( (e: React.MouseEvent) => { // Don't deselect if we just finished a selection drag @@ -124,7 +130,8 @@ export function ExploreMapCanvas() { const canvasY = Math.max(0, Math.min(10000, screenY / scale)); // Update cursor position for all sessions (using actual canvas coordinates, clamped to bounds) - updatePosition(canvasX, canvasY); + // Include currently selected panel IDs for collaborative selection visualization + updatePosition(canvasX, canvasY, selectedPanelIds); // Handle selection rectangle if dragging (using canvas coordinates) if (isSelecting && selectionRect) { @@ -135,7 +142,7 @@ export function ExploreMapCanvas() { }); } }, - [isSelecting, selectionRect, updatePosition, contextTransformRef, viewport] + [isSelecting, selectionRect, updatePosition, contextTransformRef, viewport, selectedPanelIds] ); const handleCanvasMouseUp = useCallback( diff --git a/public/app/features/explore-map/components/ExploreMapPanelContainer.tsx b/public/app/features/explore-map/components/ExploreMapPanelContainer.tsx index 54fca72aa5b..0aff4f621ad 100644 --- a/public/app/features/explore-map/components/ExploreMapPanelContainer.tsx +++ b/public/app/features/explore-map/components/ExploreMapPanelContainer.tsx @@ -4,7 +4,7 @@ import { Rnd, RndDragCallback, RndResizeCallback } from 'react-rnd'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { Button, Tooltip, useStyles2 } from '@grafana/ui'; +import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui'; import { useDispatch, useSelector } from 'app/types/store'; import { splitClose } from '../../explore/state/main'; @@ -19,7 +19,7 @@ import { updatePanelPosition, updatePanelSize, } from '../state/crdtSlice'; -import { selectSelectedPanelIds, selectViewport } from '../state/selectors'; +import { selectSelectedPanelIds, selectViewport, selectCursors } from '../state/selectors'; import { ExploreMapPanel } from '../state/types'; import { ExploreMapPanelContent } from './ExploreMapPanelContent'; @@ -36,8 +36,14 @@ function ExploreMapPanelContainerComponent({ panel }: ExploreMapPanelContainerPr const selectedPanelIds = useSelector((state) => selectSelectedPanelIds(state.exploreMapCRDT)); const viewport = useSelector((state) => selectViewport(state.exploreMapCRDT)); + const cursors = useSelector((state) => selectCursors(state.exploreMapCRDT)); const isSelected = selectedPanelIds.includes(panel.id); + // Find all users who have this panel selected (excluding current user) + const remoteSelectingUsers = Object.values(cursors).filter( + (cursor) => cursor.selectedPanelIds?.includes(panel.id) + ); + // Get the active drag info from a parent context (if any panel is being dragged) const activeDragInfo = useSelector((state) => state.exploreMapCRDT.local.activeDrag); @@ -217,40 +223,70 @@ function ExploreMapPanelContainerComponent({ panel }: ExploreMapPanelContainerPr minWidth={300} minHeight={200} > -