diff --git a/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx b/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx index 30cbe797561..d090ad0447f 100644 --- a/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx +++ b/public/app/features/explore-map/components/ExploreMapFloatingToolbar.tsx @@ -1,9 +1,9 @@ import { css } from '@emotion/css'; -import { useCallback } from 'react'; +import { useCallback, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; -import { Button, useStyles2 } from '@grafana/ui'; +import { Trans, t } from '@grafana/i18n'; +import { Button, ButtonGroup, Dropdown, Menu, MenuItem, useStyles2 } from '@grafana/ui'; import { useDispatch } from 'app/types/store'; import { addPanel } from '../state/crdtSlice'; @@ -11,6 +11,7 @@ import { addPanel } from '../state/crdtSlice'; export function ExploreMapFloatingToolbar() { const styles = useStyles2(getStyles); const dispatch = useDispatch(); + const [isOpen, setIsOpen] = useState(false); const handleAddPanel = useCallback(() => { dispatch( @@ -21,13 +22,51 @@ export function ExploreMapFloatingToolbar() { }, }) ); + setIsOpen(false); }, [dispatch]); + const handleAddTracesDrilldownPanel = useCallback(() => { + dispatch( + addPanel({ + viewportSize: { + width: window.innerWidth, + height: window.innerHeight, + }, + kind: 'traces-drilldown', + }) + ); + setIsOpen(false); + }, [dispatch]); + + const MenuActions = () => ( + + + + + ); + return (
- + + + +
); } diff --git a/public/app/features/explore-map/components/ExploreMapPanelContent.tsx b/public/app/features/explore-map/components/ExploreMapPanelContent.tsx index f3953aedf79..3d667033645 100644 --- a/public/app/features/explore-map/components/ExploreMapPanelContent.tsx +++ b/public/app/features/explore-map/components/ExploreMapPanelContent.tsx @@ -14,6 +14,8 @@ import { usePanelStateSync } from '../hooks/usePanelStateSync'; // import { useExploreStateSync } from '../hooks/useExploreStateSync'; import { selectPanels } from '../state/selectors'; +import { ExploreMapTracesDrilldownPanel } from './ExploreMapTracesDrilldownPanel'; + interface ExploreMapPanelContentProps { panelId: string; exploreId: string; @@ -118,8 +120,14 @@ export function ExploreMapPanelContent({ panelId, exploreId, width, height }: Ex return panels[panelId]; }); - // Initialize Explore pane on mount + // Initialize Explore pane on mount (only for standard Explore panels) useEffect(() => { + if (panel?.mode === 'traces-drilldown') { + // Traces drilldown panels don't need Explore initialization + setIsInitialized(true); + return; + } + const initializePane = async () => { // Use saved state if available, otherwise defaults const savedState = panel?.exploreState; @@ -133,7 +141,7 @@ export function ExploreMapPanelContent({ panelId, exploreId, width, height }: Ex queries: savedState?.queries || [], range: savedState?.range || DEFAULT_RANGE, eventBridge: eventBus, - compact: savedState?.compact || false, + compact: savedState?.compact ?? false, }) ); setIsInitialized(true); @@ -146,7 +154,11 @@ export function ExploreMapPanelContent({ panelId, exploreId, width, height }: Ex eventBus.removeAllListeners(); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dispatch, exploreId, eventBus]); + }, [dispatch, exploreId, eventBus, panel?.exploreState, panel?.mode]); + + if (panel?.mode === 'traces-drilldown') { + return ; + } // Wait for Redux state to be initialized if (!isInitialized) { diff --git a/public/app/features/explore-map/components/ExploreMapTracesDrilldownPanel.tsx b/public/app/features/explore-map/components/ExploreMapTracesDrilldownPanel.tsx new file mode 100644 index 00000000000..f6a34c00343 --- /dev/null +++ b/public/app/features/explore-map/components/ExploreMapTracesDrilldownPanel.tsx @@ -0,0 +1,150 @@ +import { css } from '@emotion/css'; +import { useEffect, useRef, useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { Trans, t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; +import { useStyles2 } from '@grafana/ui'; +import { useDispatch, useSelector } from 'app/types/store'; + +import { updatePanelIframeUrl } from '../state/crdtSlice'; +import { selectPanels } from '../state/selectors'; + +interface ExploreMapTracesDrilldownPanelProps { + exploreId: string; + width: number; + height: number; +} + +export function ExploreMapTracesDrilldownPanel({ exploreId, width, height }: ExploreMapTracesDrilldownPanelProps) { + const styles = useStyles2(getStyles); + const dispatch = useDispatch(); + const [isInitialized, setIsInitialized] = useState(false); + const iframeRef = useRef(null); + + // Find the panel with this exploreId to get saved state + const panel = useSelector((state) => { + const panels = selectPanels(state.exploreMapCRDT); + return Object.values(panels).find((p) => p.exploreId === exploreId); + }); + + // Build iframe URL for traces drilldown - use saved URL if available, otherwise default + // IMPORTANT: Only compute this once on mount to prevent iframe reloads + const [tracesDrilldownUrl] = useState(() => { + // If we have a saved iframe URL from previous session, use it + if (panel?.mode === 'traces-drilldown' && panel?.iframeUrl) { + return panel.iframeUrl; + } + // Otherwise, construct the default URL + const origin = window.location.origin; + const subUrl = config.appSubUrl || ''; + const appPath = '/a/grafana-exploretraces-app'; + return `${origin}${subUrl}${appPath}`; + }); + + // Initialize immediately for traces drilldown panels + useEffect(() => { + setIsInitialized(true); + }, []); + + // Auto-save iframe URL changes for traces-drilldown panels + useEffect(() => { + if (!isInitialized) { + return; + } + + const panelId = panel?.id; + if (!panelId) { + return; + } + + let lastSavedUrl = panel?.iframeUrl || ''; + + const checkAndSaveUrl = () => { + const iframe = iframeRef.current; + if (!iframe) { + return; + } + + try { + const currentUrl = iframe.contentWindow?.location.href; + if (currentUrl && currentUrl !== lastSavedUrl) { + lastSavedUrl = currentUrl; + dispatch( + updatePanelIframeUrl({ + panelId, + iframeUrl: currentUrl, + }) + ); + } else if (!currentUrl) { + console.log('No URL detected'); + } + } catch (e) { + console.error('Cannot read iframe URL:', e); + } + }; + + // Start checking periodically (every 2 seconds) + const intervalId = setInterval(checkAndSaveUrl, 2000); + + // Also check immediately + setTimeout(checkAndSaveUrl, 100); + + return () => { + clearInterval(intervalId); + }; + // Intentionally not including panel.iframeUrl to avoid re-creating interval on every save + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isInitialized, panel?.id, dispatch]); + + if (!isInitialized) { + return ( +
+
+ Initializing... +
+
+ ); + } + + return ( +
+