From b489c4360a0e6ec1aea20b3a863ef5820246df86 Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Tue, 2 Dec 2025 11:10:38 -0800 Subject: [PATCH] chore: disabled card state + connection lines ONLY when selected --- .../PanelDataPane/ConnectionLines.tsx | 113 +++++++----------- .../PanelDataPane/QueryTransformCard.tsx | 99 ++++++++------- .../PanelDataPane/QueryTransformList.tsx | 55 ++++----- 3 files changed, 110 insertions(+), 157 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 9525af48ed2..1cec3635538 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/ConnectionLines.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/ConnectionLines.tsx @@ -18,44 +18,32 @@ export function ConnectionLines({ connections }: ConnectionLinesProps) { const [positions, setPositions] = useState>(new Map()); const containerRef = useRef(null); - // Update positions when cards change useLayoutEffect(() => { const updatePositions = () => { const newPositions = new Map(); - - // Find all cards by their data-card-id attribute const cards = document.querySelectorAll('[data-card-id]'); cards.forEach((element) => { const cardId = element.getAttribute('data-card-id'); if (cardId) { - const rect = element.getBoundingClientRect(); - newPositions.set(cardId, rect); + newPositions.set(cardId, element.getBoundingClientRect()); } }); setPositions(newPositions); }; - // Delay to ensure cards are rendered const timeoutId = setTimeout(updatePositions, 100); - - // Update on resize and scroll window.addEventListener('resize', updatePositions); const container = containerRef.current?.parentElement; if (container) { - // Watch for scroll events const scrollContainer = container.querySelector('[data-scrollcontainer]'); scrollContainer?.addEventListener('scroll', updatePositions); - // Watch for DOM changes - const observer = new MutationObserver(() => { - setTimeout(updatePositions, 50); - }); + const observer = new MutationObserver(() => setTimeout(updatePositions, 50)); observer.observe(container, { childList: true, subtree: true }); - // Watch for container resize (splitter changes) const resizeObserver = new ResizeObserver(updatePositions); resizeObserver.observe(container); @@ -80,76 +68,42 @@ export function ConnectionLines({ connections }: ConnectionLinesProps) { return ; } - // Group connections by expression (each "to" card gets its own lane) - const lanesByExpression = new Map>(); - - connections.forEach((conn) => { - if (!lanesByExpression.has(conn.to)) { - lanesByExpression.set(conn.to, new Set()); - } - const lane = lanesByExpression.get(conn.to)!; - lane.add(conn.from); // Add the referenced query - lane.add(conn.to); // Add the expression itself + const cardIds = new Set(); + connections.forEach(({ from, to }) => { + cardIds.add(from); + cardIds.add(to); }); - // Convert to array of lanes - const activeLanes = Array.from(lanesByExpression.values()); + const swimlaneX = containerRect.width - 32; - // Find the rightmost edge of all cards - let maxCardRight = 0; - positions.forEach((rect) => { - const cardRight = rect.right - containerRect.left; - if (cardRight > maxCardRight) { - maxCardRight = cardRight; + const cardPositions: number[] = []; + cardIds.forEach((cardId) => { + const cardRect = positions.get(cardId); + if (cardRect) { + cardPositions.push(cardRect.top + cardRect.height / 2 - containerRect.top); } }); - const laneSpacing = 16; // Spacing between lanes (must match QueryTransformList.tsx) - const baseOffset = 24; // Offset from rightmost card (must match QueryTransformList.tsx) + if (cardPositions.length === 0) { + return ; + } + + const minY = Math.min(...cardPositions); + const maxY = Math.max(...cardPositions); return ( - {activeLanes.map((lane, laneIndex) => { - // Calculate Y positions for cards in this lane - const laneYPositions: number[] = []; - lane.forEach((cardId) => { + + + {Array.from(cardIds).map((cardId) => { const cardRect = positions.get(cardId); - if (cardRect) { - laneYPositions.push(cardRect.top + cardRect.height / 2 - containerRect.top); + if (!cardRect) { + return null; } - }); - - if (laneYPositions.length === 0) { - return null; - } - - // Calculate swimlane X position for this lane (start from rightmost card edge) - const swimlaneX = maxCardRight + baseOffset + laneIndex * laneSpacing; - - // Find min/max Y to draw the line only between connected points - const minY = Math.min(...laneYPositions); - const maxY = Math.max(...laneYPositions); - - return ( - - {/* Vertical swimlane line */} - - - {/* Connection points for cards in this lane */} - {Array.from(lane).map((cardId) => { - const cardRect = positions.get(cardId); - - if (!cardRect) { - return null; - } - - const pointY = cardRect.top + cardRect.height / 2 - containerRect.top; - - return ; - })} - - ); - })} + const pointY = cardRect.top + cardRect.height / 2 - containerRect.top; + return ; + })} + ); } @@ -165,6 +119,19 @@ const getStyles = (theme: GrafanaTheme2) => ({ zIndex: 10, overflow: 'visible', }), + connectionGroup: css({ + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + animation: 'fadeIn 0.2s ease-in-out', + '@keyframes fadeIn': { + from: { + opacity: 0, + }, + to: { + opacity: 1, + }, + }, + }, + }), swimlane: css({ stroke: theme.colors.text.maxContrast, strokeWidth: 2, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx index 73d3f538997..cc371d91272 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx @@ -1,4 +1,5 @@ import { css } from '@emotion/css'; +import clsx from 'clsx'; import { memo, useMemo } from 'react'; import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data'; @@ -64,7 +65,7 @@ export const QueryTransformCard = memo(
{typeLabel}
-
- - {(type === 'query' || type === 'expression') && onToggleVisibility && ( - handleAction(e, onToggleVisibility)} - className={styles.actionButton} - /> - )} - {(type === 'query' || type === 'expression') && onDuplicate && ( - handleAction(e, onDuplicate)} - className={styles.actionButton} - /> - )} - {onRemove && ( - handleAction(e, onRemove)} - className={styles.actionButton} - /> - )} - -
+ + {(type === 'query' || type === 'expression') && onToggleVisibility && ( + handleAction(e, onToggleVisibility)} + className={styles.actionButton} + /> + )} + {(type === 'query' || type === 'expression') && onDuplicate && ( + handleAction(e, onDuplicate)} + className={styles.actionButton} + /> + )} + {onRemove && ( + handleAction(e, onRemove)} + className={styles.actionButton} + /> + )} + {/* Content: Name */} @@ -145,8 +144,8 @@ export const QueryTransformCard = memo( QueryTransformCard.displayName = 'QueryTransformCard'; const getStyles = (theme: GrafanaTheme2) => { - const actionsClass = 'actions-container'; const selectedClass = 'card-selected'; + const hiddenClass = 'card-hidden'; return { card: css({ @@ -157,9 +156,6 @@ const getStyles = (theme: GrafanaTheme2) => { overflow: 'hidden', background: theme.colors.background.primary, width: '100%', - [`&:hover .${actionsClass}`]: { - opacity: 1, - }, '&:hover': { borderColor: theme.colors.border.strong, }, @@ -171,8 +167,12 @@ const getStyles = (theme: GrafanaTheme2) => { borderColor: theme.colors.primary.border, boxShadow: `0 0 0 1px ${theme.colors.primary.border}`, }, + [`&.${hiddenClass}`]: { + opacity: 0.5, + }, }), cardSelected: selectedClass, + cardHidden: hiddenClass, headerQuery: css({ display: 'flex', alignItems: 'center', @@ -220,11 +220,6 @@ const getStyles = (theme: GrafanaTheme2) => { textTransform: 'uppercase', letterSpacing: '0.05em', }), - actions: css({ - opacity: 0, - flexShrink: 0, - }), - actionsClass, actionButton: css({ '&:hover': { background: theme.colors.action.hover, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx index 7f3e04f8c8a..b21d52eb195 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx @@ -51,8 +51,7 @@ export const QueryTransformList = memo( }: QueryTransformListProps) => { const styles = useStyles2(getStyles); - // Detect connections between items - const connections = useMemo(() => { + const allConnections = useMemo(() => { const conns: Array<{ from: string; to: string }> = []; items.forEach((item) => { @@ -64,19 +63,15 @@ export const QueryTransformList = memo( const expressionString = expr.expression; if (expressionType === 'math') { - // Math expressions: parse $A, $B, etc. const matches = expressionString.matchAll(/\$(\w+)/g); for (const match of matches) { - const refId = match[1]; - conns.push({ from: refId, to: expr.refId }); + conns.push({ from: match[1], to: expr.refId }); } } else if (expressionType === 'reduce' || expressionType === 'resample' || expressionType === 'threshold') { - // Reduce/Resample/Threshold: expression field is a single refId if (expressionString) { conns.push({ from: expressionString, to: expr.refId }); } } - // TODO: Handle 'sql' and 'classic_conditions' types if needed } } }); @@ -84,6 +79,24 @@ export const QueryTransformList = memo( return conns; }, [items]); + // Filter connections to only show for selected card + const visibleConnections = useMemo(() => { + if (!selectedId) { + return []; + } + + // Find the item to get its refId + const activeItem = items.find((item) => item.id === selectedId); + if (!activeItem || !('refId' in activeItem.data)) { + return []; + } + + const activeRefId = activeItem.data.refId; + + // Show connections where this card is involved (either as source or destination) + return allConnections.filter((conn) => conn.from === activeRefId || conn.to === activeRefId); + }, [allConnections, selectedId, items]); + const getHandlers = (item: QueryTransformItem) => { switch (item.type) { case 'query': @@ -109,22 +122,11 @@ export const QueryTransformList = memo( } }; - // Calculate number of lanes for dynamic padding - const lanesByExpression = useMemo(() => { - const lanes = new Map>(); - connections.forEach((conn) => { - if (!lanes.has(conn.to)) { - lanes.set(conn.to, new Set()); - } - }); - return lanes.size; - }, [connections]); - return (
- + -
0 ? styles.contentWithConnections(lanesByExpression) : styles.content}> +
{items.map((item) => ( { - const laneSpacing = 16; // Must match ConnectionLines.tsx - const baseOffset = 24; // Must match ConnectionLines.tsx - const extraPadding = 8; // Extra breathing room beyond the last lane - return { container: css({ position: 'relative', @@ -170,15 +168,8 @@ const getStyles = (theme: GrafanaTheme2) => { content: css({ position: 'relative', padding: theme.spacing(2), + paddingRight: theme.spacing(6), // Extra space for the connection line zIndex: 1, }), - contentWithConnections: (numLanes: number) => - css({ - position: 'relative', - padding: theme.spacing(2), - // Calculate exact space needed: base offset + (lanes * spacing) + extra padding - paddingRight: baseOffset + numLanes * laneSpacing + extraPadding, - zIndex: 1, - }), }; };