From 91ba4bc41990c32b5e1f5213aaf769bfc9b812dd Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 4 Dec 2025 12:09:43 +0000 Subject: [PATCH] Fix sticky note conflicts with frames --- .../components/ExploreMapCanvas.tsx | 7 +- .../components/ExploreMapFloatingToolbar.tsx | 22 +- .../components/ExploreMapStickyNote.tsx | 375 ++++++++++++++ public/app/features/explore-map/crdt/state.ts | 456 ++++++++++++++++++ public/app/features/explore-map/crdt/types.ts | 147 ++++++ .../features/explore-map/state/crdtSlice.ts | 177 +++++++ .../features/explore-map/state/selectors.ts | 11 + 7 files changed, 1193 insertions(+), 2 deletions(-) create mode 100644 public/app/features/explore-map/components/ExploreMapStickyNote.tsx diff --git a/public/app/features/explore-map/components/ExploreMapCanvas.tsx b/public/app/features/explore-map/components/ExploreMapCanvas.tsx index 817947c8632..88f752f736d 100644 --- a/public/app/features/explore-map/components/ExploreMapCanvas.tsx +++ b/public/app/features/explore-map/components/ExploreMapCanvas.tsx @@ -10,12 +10,13 @@ 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 } from '../state/selectors'; +import { selectPanels, selectFrames, selectViewport, selectCursors, selectSelectedPanelIds, selectMapUid, selectPostItNotes } from '../state/selectors'; import { EdgeCursorIndicator } from './EdgeCursorIndicator'; import { ExploreMapComment } from './ExploreMapComment'; import { ExploreMapFrame } from './ExploreMapFrame'; import { ExploreMapPanelContainer } from './ExploreMapPanelContainer'; +import { ExploreMapStickyNote } from './ExploreMapStickyNote'; import { UserCursor } from './UserCursor'; interface SelectionRect { @@ -38,6 +39,7 @@ export function ExploreMapCanvas() { const panels = useSelector((state) => selectPanels(state.exploreMapCRDT)); const frames = useSelector((state) => selectFrames(state.exploreMapCRDT)); + const postItNotes = useSelector((state) => selectPostItNotes(state.exploreMapCRDT)); const viewport = useSelector((state) => selectViewport(state.exploreMapCRDT)); const cursors = useSelector((state) => selectCursors(state.exploreMapCRDT)); const selectedPanelIds = useSelector((state) => selectSelectedPanelIds(state.exploreMapCRDT)); @@ -337,6 +339,9 @@ export function ExploreMapCanvas() { .map((info) => ( ))} + {Object.values(postItNotes).map((postIt) => ( + + ))} {selectionRect && (
{ + dispatch( + addPostItNote({ + viewportSize: { + width: window.innerWidth, + height: window.innerHeight, + }, + createdBy: currentUsername, + }) + ); + setIsOpen(false); + }, [dispatch, currentUsername]); // Build context for assistant const canvasContext = useMemo(() => { @@ -285,6 +300,11 @@ CRITICAL: logoAlt="Pyroscope" onClick={handleAddProfilesDrilldownPanel} /> + ); diff --git a/public/app/features/explore-map/components/ExploreMapStickyNote.tsx b/public/app/features/explore-map/components/ExploreMapStickyNote.tsx new file mode 100644 index 00000000000..0d8f3e1bbc7 --- /dev/null +++ b/public/app/features/explore-map/components/ExploreMapStickyNote.tsx @@ -0,0 +1,375 @@ +import { css, cx } from '@emotion/css'; +import React, { useCallback, useRef, useState } from 'react'; +import { Rnd, RndDragCallback, RndResizeCallback } from 'react-rnd'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { Button, TextArea, useStyles2 } from '@grafana/ui'; +import { contextSrv } from 'app/core/services/context_srv'; +import { useDispatch } from 'app/types/store'; + +import { + bringPostItNoteToFront, + removePostItNote, + updatePostItNoteColor, + updatePostItNotePosition, + updatePostItNoteSize, + updatePostItNoteText, +} from '../state/crdtSlice'; + +interface ExploreMapStickyNoteProps { + postIt: { + id: string; + position: { x: number; y: number; width: number; height: number; zIndex: number }; + text: string; + color: string; + createdBy?: string; + }; + zoom: number; +} + +export const STICKY_NOTE_COLORS = [ + { name: 'yellow', value: '#f1c40f' }, + { name: 'blue', value: '#3498db' }, + { name: 'green', value: '#2ecc71' }, + { name: 'purple', value: '#9b59b6' }, + { name: 'red', value: '#e74c3c' }, +]; + +export function ExploreMapStickyNote({ postIt, zoom }: ExploreMapStickyNoteProps) { + const styles = useStyles2(getStyles); + const dispatch = useDispatch(); + const rndRef = useRef(null); + const textAreaRef = useRef(null); + const [isEditing, setIsEditing] = useState(false); + const [editText, setEditText] = useState(postIt.text); + const [dragStartPos, setDragStartPos] = useState<{ x: number; y: number } | null>(null); + + const colorInfo = STICKY_NOTE_COLORS.find((c) => c.name === postIt.color) || STICKY_NOTE_COLORS.find((c) => c.name === 'purple') || STICKY_NOTE_COLORS[0]; + + const handleDragStart: RndDragCallback = useCallback( + (_e, data) => { + setDragStartPos({ x: data.x, y: data.y }); + dispatch(bringPostItNoteToFront({ postItId: postIt.id })); + }, + [dispatch, postIt.id] + ); + + const handleDragStop: RndDragCallback = useCallback( + (_e, data) => { + if (dragStartPos) { + const deltaX = data.x - dragStartPos.x; + const deltaY = data.y - dragStartPos.y; + + // Only update position if there was actual movement + const hasMoved = Math.abs(deltaX) > 0.5 || Math.abs(deltaY) > 0.5; + + if (hasMoved) { + dispatch( + updatePostItNotePosition({ + postItId: postIt.id, + x: data.x, + y: data.y, + }) + ); + } + } + setDragStartPos(null); + }, + [dispatch, postIt.id, dragStartPos] + ); + + const handleResizeStop: RndResizeCallback = useCallback( + (_e, _direction, ref, _delta, position) => { + const newWidth = ref.offsetWidth; + const newHeight = ref.offsetHeight; + + // Update position + dispatch( + updatePostItNotePosition({ + postItId: postIt.id, + x: position.x, + y: position.y, + }) + ); + + // Update size + dispatch( + updatePostItNoteSize({ + postItId: postIt.id, + width: newWidth, + height: newHeight, + }) + ); + }, + [dispatch, postIt.id] + ); + + const handleDoubleClick = useCallback(() => { + setIsEditing(true); + setEditText(postIt.text); + setTimeout(() => { + textAreaRef.current?.focus(); + textAreaRef.current?.select(); + }, 0); + }, [postIt.text]); + + const handleSaveText = useCallback(() => { + dispatch( + updatePostItNoteText({ + postItId: postIt.id, + text: editText, + }) + ); + setIsEditing(false); + }, [dispatch, postIt.id, editText]); + + const handleCancelEdit = useCallback(() => { + setEditText(postIt.text); + setIsEditing(false); + }, [postIt.text]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + handleCancelEdit(); + } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + handleSaveText(); + } + }, + [handleCancelEdit, handleSaveText] + ); + + const handleDelete = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (window.confirm(t('explore-map.sticky.delete-confirm', 'Delete this sticky note?'))) { + dispatch(removePostItNote({ postItId: postIt.id })); + } + }, + [dispatch, postIt.id] + ); + + const handleColorChange = useCallback( + (color: string) => { + dispatch( + updatePostItNoteColor({ + postItId: postIt.id, + color, + }) + ); + }, + [dispatch, postIt.id] + ); + + return ( + +
+ {isEditing ? ( +
+