diff --git a/public/app/features/explore-map/components/AssistantComponents/DebugAssistantContext.tsx b/public/app/features/explore-map/components/AssistantComponents/DebugAssistantContext.tsx index 4d2256ee6d0..4f19e208174 100644 --- a/public/app/features/explore-map/components/AssistantComponents/DebugAssistantContext.tsx +++ b/public/app/features/explore-map/components/AssistantComponents/DebugAssistantContext.tsx @@ -11,20 +11,15 @@ export const DebugAssistantContext: React.FC = () => { const pageContext = usePageContext(); useEffect(() => { - // eslint-disable-next-line no-console - console.group('🔍 Assistant Debug Info'); - // eslint-disable-next-line no-console - console.log('Current URL:', window.location.pathname); - // eslint-disable-next-line no-console - console.log('Registered Components:', Object.keys(pageComponents)); - // eslint-disable-next-line no-console - console.log('Component Details:', pageComponents); - // eslint-disable-next-line no-console - console.log('Page Context Items:', pageContext.length); - // eslint-disable-next-line no-console - console.log('Page Context:', pageContext); - // eslint-disable-next-line no-console - console.groupEnd(); + // Debug logging disabled to reduce console noise + // Uncomment if needed for assistant debugging + // console.group('🔍 Assistant Debug Info'); + // console.log('Current URL:', window.location.pathname); + // console.log('Registered Components:', Object.keys(pageComponents)); + // console.log('Component Details:', pageComponents); + // console.log('Page Context Items:', pageContext.length); + // console.log('Page Context:', pageContext); + // console.groupEnd(); }, [pageComponents, pageContext]); return null; diff --git a/public/app/features/explore-map/components/ConfirmDeleteFrameDialog.tsx b/public/app/features/explore-map/components/ConfirmDeleteFrameDialog.tsx new file mode 100644 index 00000000000..bcb2d855bfc --- /dev/null +++ b/public/app/features/explore-map/components/ConfirmDeleteFrameDialog.tsx @@ -0,0 +1,71 @@ +import { useCallback, useState } from 'react'; + +import { Checkbox, ConfirmModal } from '@grafana/ui'; +import { useDispatch, useSelector } from 'app/types/store'; + +import { splitClose } from '../../explore/state/main'; +import { removeFrame } from '../state/crdtSlice'; +import { selectPanelsInFrame, selectPanels } from '../state/selectors'; + +interface ConfirmDeleteFrameDialogProps { + frameId: string; + frameTitle: string; + panelCount: number; + onClose: () => void; +} + +export function ConfirmDeleteFrameDialog({ frameId, frameTitle, panelCount, onClose }: ConfirmDeleteFrameDialogProps) { + const dispatch = useDispatch(); + const panelsInFrame = useSelector((state) => selectPanelsInFrame(state.exploreMapCRDT, frameId)); + const allPanels = useSelector((state) => selectPanels(state.exploreMapCRDT)); + const [deletePanels, setDeletePanels] = useState(false); + + const handleConfirm = useCallback(() => { + // If deleting panels, clean up Explore state first + if (deletePanels) { + for (const panelId of panelsInFrame) { + const panel = allPanels[panelId]; + if (panel) { + dispatch(splitClose(panel.exploreId)); + } + } + } + + dispatch( + removeFrame({ + frameId, + deletePanels, + }) + ); + onClose(); + }, [dispatch, frameId, deletePanels, onClose, panelsInFrame, allPanels]); + + return ( + +

+ Are you sure you want to delete the frame "{frameTitle}"? +

+ {panelCount > 0 && ( + <> +

+ This frame contains {panelCount} panel{panelCount > 1 ? 's' : ''}. +

+ setDeletePanels(e.currentTarget.checked)} + /> + + )} + + } + confirmText="Delete" + onConfirm={handleConfirm} + onDismiss={onClose} + /> + ); +} diff --git a/public/app/features/explore-map/components/ExploreMapCanvas.tsx b/public/app/features/explore-map/components/ExploreMapCanvas.tsx index 21a16e951b2..817947c8632 100644 --- a/public/app/features/explore-map/components/ExploreMapCanvas.tsx +++ b/public/app/features/explore-map/components/ExploreMapCanvas.tsx @@ -10,10 +10,11 @@ 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, selectViewport, selectCursors, selectSelectedPanelIds, selectMapUid } from '../state/selectors'; +import { selectPanels, selectFrames, selectViewport, selectCursors, selectSelectedPanelIds, selectMapUid } from '../state/selectors'; import { EdgeCursorIndicator } from './EdgeCursorIndicator'; import { ExploreMapComment } from './ExploreMapComment'; +import { ExploreMapFrame } from './ExploreMapFrame'; import { ExploreMapPanelContainer } from './ExploreMapPanelContainer'; import { UserCursor } from './UserCursor'; @@ -36,6 +37,7 @@ export function ExploreMapCanvas() { const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const panels = useSelector((state) => selectPanels(state.exploreMapCRDT)); + const frames = useSelector((state) => selectFrames(state.exploreMapCRDT)); const viewport = useSelector((state) => selectViewport(state.exploreMapCRDT)); const cursors = useSelector((state) => selectCursors(state.exploreMapCRDT)); const selectedPanelIds = useSelector((state) => selectSelectedPanelIds(state.exploreMapCRDT)); @@ -322,6 +324,11 @@ export function ExploreMapCanvas() { role="button" tabIndex={0} > + {/* Render frames first (lower z-index) */} + {Object.values(frames).map((frame) => ( + + ))} + {/* Render panels on top */} {Object.values(panels).map((panel) => { return ; })} diff --git a/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx b/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx index 5757efec3b7..2531d2b7d41 100644 --- a/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx +++ b/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx @@ -13,8 +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 } from '../state/crdtSlice'; -import { selectPanels, selectMapUid } from '../state/selectors'; +import { addPanel, addFrame } from '../state/crdtSlice'; +import { selectPanels, selectMapUid, selectViewport, selectSelectedPanelIds } from '../state/selectors'; import { AddPanelAction } from './AssistantComponents'; export function ExploreMapFloatingToolbar() { @@ -29,6 +29,8 @@ export function ExploreMapFloatingToolbar() { // Get canvas state for assistant context const panels = useSelector((state) => selectPanels(state.exploreMapCRDT)); const mapUid = useSelector((state) => selectMapUid(state.exploreMapCRDT)); + const viewport = useSelector((state) => selectViewport(state.exploreMapCRDT)); + const selectedPanelIds = useSelector((state) => selectSelectedPanelIds(state.exploreMapCRDT)); const handleAddPanel = useCallback(() => { dispatch( @@ -99,6 +101,61 @@ export function ExploreMapFloatingToolbar() { setIsOpen(false); }, [dispatch, currentUsername]); + const handleAddFrame = useCallback(() => { + // Get selected panels that are not already in a frame + const selectedUnframedPanels = selectedPanelIds + .map((id) => panels[id]) + .filter((panel) => panel && !panel.frameId); + + let position: { x: number; y: number; width: number; height: number }; + + if (selectedUnframedPanels.length > 0) { + // Calculate bounds around selected unframed panels + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + for (const panel of selectedUnframedPanels) { + minX = Math.min(minX, panel.position.x); + minY = Math.min(minY, panel.position.y); + maxX = Math.max(maxX, panel.position.x + panel.position.width); + maxY = Math.max(maxY, panel.position.y + panel.position.height); + } + + // Add padding around the panels + const padding = 50; + position = { + x: minX - padding, + y: minY - padding, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; + } else { + // No selected panels, position at viewport center + const viewportSize = { width: window.innerWidth, height: window.innerHeight }; + const canvasCenterX = (-viewport.panX + viewportSize.width / 2) / viewport.zoom; + const canvasCenterY = (-viewport.panY + viewportSize.height / 2) / viewport.zoom; + + const frameWidth = 800; + const frameHeight = 600; + + position = { + x: canvasCenterX - frameWidth / 2, + y: canvasCenterY - frameHeight / 2, + width: frameWidth, + height: frameHeight, + }; + } + + dispatch( + addFrame({ + position, + createdBy: currentUsername, + }) + ); + }, [dispatch, currentUsername, selectedPanelIds, panels, viewport]); + // Build context for assistant const canvasContext = useMemo(() => { const panelsArray = Object.values(panels); @@ -245,6 +302,9 @@ CRITICAL: /> + {isAssistantAvailable && Object.keys(panels).length > 0 && (