From 0425e940dd7d79e522046f73cebd9583b1a17d1d Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:19:40 -0800 Subject: [PATCH] chore: alex - drag n drop, connection line fix --- .../PanelDataPane/ConnectionLines.tsx | 77 ++++-- .../PanelDataPane/PanelDataPane.tsx | 37 ++- .../PanelDataPane/QueryTransformList.tsx | 233 ++++++++++++++---- 3 files changed, 269 insertions(+), 78 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/ConnectionLines.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/ConnectionLines.tsx index d54f0e56cc1..5a3f3f32767 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/ConnectionLines.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/ConnectionLines.tsx @@ -11,57 +11,84 @@ interface Connection { interface ConnectionLinesProps { connections: Connection[]; + isDragging?: boolean; } -export function ConnectionLines({ connections }: ConnectionLinesProps) { +export function ConnectionLines({ connections, isDragging = false }: ConnectionLinesProps) { const styles = useStyles2(getStyles); const [positions, setPositions] = useState>(new Map()); const containerRef = useRef(null); useLayoutEffect(() => { + let rafId: number | null = null; + let isUpdating = false; + const updatePositions = () => { - const newPositions = new Map(); - const cards = document.querySelectorAll('[data-card-id]'); + if (isUpdating) { + return; + } - cards.forEach((element) => { - const cardId = element.getAttribute('data-card-id'); - if (cardId) { - newPositions.set(cardId, element.getBoundingClientRect()); - } + isUpdating = true; + rafId = requestAnimationFrame(() => { + const newPositions = new Map(); + const cards = document.querySelectorAll('[data-card-id]'); + + cards.forEach((element) => { + const cardId = element.getAttribute('data-card-id'); + if (cardId) { + newPositions.set(cardId, element.getBoundingClientRect()); + } + }); + + setPositions(newPositions); + isUpdating = false; }); - - setPositions(newPositions); }; - const timeoutId = setTimeout(updatePositions, 100); - window.addEventListener('resize', updatePositions); + // Initial update and update when drag ends + if (!isDragging) { + updatePositions(); + } const container = containerRef.current?.parentElement; if (container) { - const scrollContainer = container.querySelector('[data-scrollcontainer]'); - scrollContainer?.addEventListener('scroll', updatePositions); - - const observer = new MutationObserver(() => setTimeout(updatePositions, 50)); + // Only observe mutations when not dragging (for card add/remove/reorder) + let mutationTimeout: number | null = null; + const observer = new MutationObserver(() => { + if (isDragging) { + return; + } + if (mutationTimeout) { + clearTimeout(mutationTimeout); + } + mutationTimeout = window.setTimeout(updatePositions, 100); + }); observer.observe(container, { childList: true, subtree: true }); + // Track container resize (from splitter drag) const resizeObserver = new ResizeObserver(updatePositions); resizeObserver.observe(container); return () => { - clearTimeout(timeoutId); - window.removeEventListener('resize', updatePositions); - scrollContainer?.removeEventListener('scroll', updatePositions); + if (rafId) { + cancelAnimationFrame(rafId); + } + if (mutationTimeout) { + clearTimeout(mutationTimeout); + } observer.disconnect(); resizeObserver.disconnect(); }; } return () => { - clearTimeout(timeoutId); - window.removeEventListener('resize', updatePositions); + if (rafId) { + cancelAnimationFrame(rafId); + } }; - }, [connections]); + }, [connections, isDragging]); + // Get container rect from the SVG's parent (contentWrapper) const containerRect = containerRef.current?.parentElement?.getBoundingClientRect(); if (!containerRect || connections.length === 0) { @@ -79,7 +106,7 @@ export function ConnectionLines({ connections }: ConnectionLinesProps) { refIds.add(to); }); - const swimlaneX = containerRect.width - 24; + const swimlaneX = containerRect.width - 40; // Adjusted for increased right padding (64px) const cardPositions: number[] = []; refIds.forEach((refId) => { @@ -118,8 +145,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ position: 'absolute', top: 0, left: 0, - width: '100%', - height: '100%', + right: 0, + bottom: 0, pointerEvents: 'none', zIndex: 10, overflow: 'visible', diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index ba611463ed3..65a17a73df1 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -430,6 +430,35 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { [tabs] ); + const handleReorderDataSources = useCallback( + (startIndex: number, endIndex: number) => { + if (queryRunner) { + const queries = queryRunner.state.queries || []; + const newQueries = Array.from(queries); + const [removed] = newQueries.splice(startIndex, 1); + newQueries.splice(endIndex, 0, removed); + queryRunner.setState({ queries: newQueries }); + } + }, + [queryRunner] + ); + + const handleReorderTransforms = useCallback( + (startIndex: number, endIndex: number) => { + const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations); + if (transformsTab) { + const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter( + isDataTransformerConfig + ); + const newTransformations = Array.from(transformations); + const [removed] = newTransformations.splice(startIndex, 1); + newTransformations.splice(endIndex, 0, removed); + transformsTab.onChangeTransformations(newTransformations); + } + }, + [tabs] + ); + // Get data for transformations drawer const sourceData = queryRunner?.useState(); const series = sourceData?.data?.series || []; @@ -462,6 +491,8 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { onToggleExpressionVisibility={handleToggleExpressionVisibility} onRemoveTransform={handleRemoveTransform} onToggleTransformVisibility={handleToggleTransformVisibility} + onReorderDataSources={handleReorderDataSources} + onReorderTransforms={handleReorderTransforms} />
void; onRemoveTransform?: (index: number) => void; onToggleTransformVisibility?: (index: number) => void; + onReorderDataSources?: (startIndex: number, endIndex: number) => void; + onReorderTransforms?: (startIndex: number, endIndex: number) => void; } export const QueryTransformList = memo( @@ -55,8 +58,43 @@ export const QueryTransformList = memo( onToggleExpressionVisibility, onRemoveTransform, onToggleTransformVisibility, + onReorderDataSources, + onReorderTransforms, }: QueryTransformListProps) => { const styles = useStyles2(getStyles); + const [isDragging, setIsDragging] = useState(false); + + const onDragStart = () => { + setIsDragging(true); + }; + + const onDragEnd = (result: DropResult) => { + // Defer state updates until after drop animation + setTimeout(() => { + setIsDragging(false); + + if (!result.destination) { + return; + } + + const startIndex = result.source.index; + const endIndex = result.destination.index; + + if (startIndex === endIndex) { + return; + } + + // Handle reordering based on droppable ID + if (result.source.droppableId === 'data-sources' && result.destination.droppableId === 'data-sources') { + onReorderDataSources?.(startIndex, endIndex); + } else if ( + result.source.droppableId === 'transformations' && + result.destination.droppableId === 'transformations' + ) { + onReorderTransforms?.(startIndex, endIndex); + } + }, 0); + }; const allConnections = useMemo(() => { const conns: Array<{ from: string; to: string }> = []; @@ -152,57 +190,119 @@ export const QueryTransformList = memo(
-
-
- - {/* Data Sources Section (Queries + Expressions) */} - {dataSourceItems.length > 0 && ( - -
- {t('dashboard-scene.query-transform-list.queries-expressions', 'Queries & Expressions')} -
- {dataSourceItems.map((item) => ( - onSelect(item.id)} - {...getHandlers(item)} - /> - ))} -
- )} +
+ + +
+ + {/* Data Sources Section (Queries + Expressions) */} + {dataSourceItems.length > 0 && ( + +
+ {t('dashboard-scene.query-transform-list.queries-expressions', 'Queries & Expressions')} +
+ + {(provided, snapshot) => { + // Check if dragging from transformations section + const isDraggingFromOtherSection = + isDragging && snapshot.draggingFromThisWith === null && snapshot.isDraggingOver; - {/* Transformations Section */} - {transformItems.length > 0 && ( - -
- {t('dashboard-scene.query-transform-list.transformations', 'Transformations')} -
- {transformItems.map((item) => ( - onSelect(item.id)} - {...getHandlers(item)} - /> - ))} -
- )} + return ( +
+ + {dataSourceItems.map((item, index) => ( + + {(provided, snapshot) => ( +
+ onSelect(item.id)} + {...getHandlers(item)} + /> +
+ )} +
+ ))} + {provided.placeholder} +
+
+ ); + }} +
+
+ )} - -
+ {/* Transformations Section */} + {transformItems.length > 0 && ( + +
+ {t('dashboard-scene.query-transform-list.transformations', 'Transformations')} +
+ + {(provided, snapshot) => { + // Check if dragging from data sources section + const isDraggingFromOtherSection = + isDragging && snapshot.draggingFromThisWith === null && snapshot.isDraggingOver; + + return ( +
+ + {transformItems.map((item, index) => ( + + {(provided, snapshot) => ( +
+ onSelect(item.id)} + {...getHandlers(item)} + /> +
+ )} +
+ ))} + {provided.placeholder} +
+
+ ); + }} +
+
+ )} + + + +
+
@@ -270,7 +370,7 @@ const getStyles = (theme: GrafanaTheme2) => { gap: theme.spacing(1), marginLeft: theme.spacing(-2), marginRight: theme.spacing(-2), - '&::before, &::after': { + '&::after': { content: '""', flex: 1, height: '1px', @@ -280,10 +380,43 @@ const getStyles = (theme: GrafanaTheme2) => { scrollWrapper: css({ flex: 1, minHeight: 0, + }), + contentWrapper: css({ position: 'relative', + minHeight: '100%', }), content: css({ - padding: `${theme.spacing(2)} ${theme.spacing(6)} ${theme.spacing(4)} ${theme.spacing(4)}`, + padding: `${theme.spacing(2)} ${theme.spacing(8)} ${theme.spacing(4)} ${theme.spacing(6)}`, + position: 'relative', + }), + dragging: css({ + opacity: 0.8, + cursor: 'grabbing !important', + // Use GPU-accelerated properties only + willChange: 'transform', + }), + droppableActive: css({ + // Minimal styling for performance + }), + droppableInvalid: css({ + position: 'relative', + cursor: 'not-allowed', + '&::after': { + content: '""', + position: 'absolute', + top: -4, + left: -4, + right: -4, + bottom: -4, + background: theme.colors.error.transparent, + borderRadius: theme.shape.radius.default, + pointerEvents: 'none', + zIndex: 0, + }, + '& > *': { + position: 'relative', + zIndex: 1, + }, }), footer: css({ ...barBase,