From 15bf6b45f94bac87fb7409e9a3265387b26fd4eb Mon Sep 17 00:00:00 2001 From: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:38:19 -0800 Subject: [PATCH] chore: alex - debug mode --- .../panel-edit/PanelDataPane/DetailView.tsx | 8 + .../PanelDataPane/DetailViewHeader.tsx | 29 +- .../PanelDataPane/PanelDataPane.tsx | 22 +- .../PanelDataPane/PanelDataSidebar.tsx | 17 +- .../PanelDataPane/QueryTransformCard.tsx | 8 +- .../PanelDataPane/QueryTransformList.tsx | 437 ++++++++++++++---- .../PanelDataPane/debugModeHelpers.ts | 109 +++++ .../panel-edit/PanelDataPane/useDebugMode.ts | 132 ++++++ public/locales/en-US/grafana.json | 97 +++- 9 files changed, 746 insertions(+), 113 deletions(-) create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelDataPane/debugModeHelpers.ts create mode 100644 public/app/features/dashboard-scene/panel-edit/PanelDataPane/useDebugMode.ts diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx index 74e83c7055b..88ea2cde2cd 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx @@ -37,6 +37,8 @@ interface DetailViewProps { onQueryLibrarySave?: (name: string, description: string) => void; onQueryLibraryClose?: () => void; onOpenQueryLibrary?: (mode: 'browse' | 'save', index?: number) => void; + isDebugMode?: boolean; + debugPosition?: number; } export const DetailView = memo( @@ -56,6 +58,8 @@ export const DetailView = memo( onQueryLibrarySave, onQueryLibraryClose, onOpenQueryLibrary, + isDebugMode, + debugPosition, }: DetailViewProps) => { const styles = useStyles2(getStyles); const queryLibraryRef = useRef(null); @@ -147,6 +151,8 @@ export const DetailView = memo( panel={panel} onRemoveTransform={onRemoveTransform} onToggleTransformVisibility={onToggleTransformVisibility} + isDebugMode={isDebugMode} + debugPosition={debugPosition} /> @@ -178,6 +184,8 @@ export const DetailView = memo( onOpenQueryLibrary, handleSelectQueryFromHeader, handleSaveQueryFromHeader, + isDebugMode, + debugPosition, ]); return
{renderContent()}
; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx index 77f41374f40..b37013a197f 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailViewHeader.tsx @@ -45,6 +45,8 @@ interface ItemModeProps { onRemoveTransform?: (index: number) => void; onToggleTransformVisibility?: (index: number) => void; onOpenQueryLibrary?: (mode: 'browse' | 'save') => void; + isDebugMode?: boolean; + debugPosition?: number; queryLibraryMode?: never; onSelectQuery?: never; onClose?: never; @@ -150,6 +152,8 @@ function ItemHeader({ onRemoveTransform, onToggleTransformVisibility, onOpenQueryLibrary, + isDebugMode, + debugPosition, }: ItemModeProps) { const theme = useTheme2(); const config = useMemo(() => ITEM_CONFIG(theme)[selectedItem.type], [theme, selectedItem.type]); @@ -416,11 +420,26 @@ function ItemHeader({ } const allTransformations: DataTransformerConfig[] = transformations; - const currentIndex = selectedItem.index; + + // In debug mode, use debugPosition to determine which transformations to apply + // debugPosition is relative to all items (queries + transforms) + // We need to calculate the transformation index from it + let currentIndex = selectedItem.index; + if (isDebugMode && debugPosition !== undefined) { + // debugPosition is the number of enabled items + // To get transformation index: debugPosition - number_of_queries + // Number of queries = debugPosition - allTransformations.length (approximately, but we need to get it properly) + // Actually, let's get the number of queries from queryRunner + const numQueries = queryRunner.state.queries?.length || 0; + const maxTransformIndex = Math.max(0, (debugPosition || 0) - numQueries); + // Use the minimum of the calculated index and the current selected index + // This ensures we don't go beyond what's enabled in debug mode + currentIndex = Math.min(selectedItem.index, maxTransformIndex - 1); + } // Get transformations before and including current one - const inputTransforms = allTransformations.slice(0, currentIndex); - const outputTransforms = allTransformations.slice(currentIndex, currentIndex + 1); + const inputTransforms = allTransformations.slice(0, Math.max(0, currentIndex)); + const outputTransforms = allTransformations.slice(Math.max(0, currentIndex), Math.max(0, currentIndex) + 1); const ctx: DataTransformContext = { interpolate: (v: string) => getTemplateSrv().replace(v), @@ -442,7 +461,7 @@ function ItemHeader({ inputSubscription.unsubscribe(); outputSubscription.unsubscribe(); }; - }, [selectedItem, showDebug, panel]); + }, [selectedItem, showDebug, panel, isDebugMode, debugPosition]); return (
@@ -674,6 +693,8 @@ export const DetailViewHeader = (props: DetailViewHeaderProps) => { onRemoveTransform={props.onRemoveTransform} onToggleTransformVisibility={props.onToggleTransformVisibility} onOpenQueryLibrary={props.onOpenQueryLibrary} + isDebugMode={props.isDebugMode} + debugPosition={props.debugPosition} /> ); }; 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 2a46362f0e0..01a5d1cfc51 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -1,5 +1,5 @@ import { css, cx } from '@emotion/css'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -39,6 +39,8 @@ export interface PanelDataPaneState extends SceneObjectState { panelRef: SceneObjectRef; transformPickerIndex?: number | null; queryLibraryMode: QueryLibraryMode & { index: number | null }; + isDebugMode?: boolean; + debugPosition?: number; } export class PanelDataPane extends SceneObjectBase { @@ -71,6 +73,8 @@ export class PanelDataPane extends SceneObjectBase { mode: 'browse', index: null, }, + isDebugMode: false, + debugPosition: 0, }); } @@ -94,6 +98,10 @@ export class PanelDataPane extends SceneObjectBase { this.setState({ queryLibraryMode: mode }); }; + public setDebugState = (isDebugMode: boolean, debugPosition: number) => { + this.setState({ isDebugMode, debugPosition }); + }; + public getUrlState() { return { tab: this.state.tab }; } @@ -113,7 +121,15 @@ export class PanelDataPane extends SceneObjectBase { } function PanelDataPaneRendered({ model }: SceneComponentProps) { - const { tabs, selectedQueryTransform, panelRef, transformPickerIndex, queryLibraryMode } = model.useState(); + const { + tabs, + selectedQueryTransform, + panelRef, + transformPickerIndex, + queryLibraryMode, + isDebugMode = false, + debugPosition = 0, + } = model.useState(); const styles = useStyles2(getStyles); // Subscribe to query runner and tab state changes @@ -323,6 +339,8 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { onQueryLibrarySave={handleQueryLibrarySave} onQueryLibraryClose={handleQueryLibraryClose} onOpenQueryLibrary={handleOpenQueryLibrary} + isDebugMode={isDebugMode} + debugPosition={debugPosition} />
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataSidebar.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataSidebar.tsx index af1b2596141..d5db8402c49 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataSidebar.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataSidebar.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; -import { DataQuery, DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data'; +import { DataTransformerConfig, GrafanaTheme2, SelectableValue } from '@grafana/data'; import { t } from '@grafana/i18n'; import { SceneComponentProps, SceneDataQuery } from '@grafana/scenes'; import { Button, useStyles2 } from '@grafana/ui'; @@ -279,9 +279,17 @@ export function PanelDataSidebarRendered({ model }: SceneComponentProps { + model.setDebugState(isDebugMode, debugPosition); + }, + [model] + ); + // Get data for transformations drawer - const sourceData = queryRunner?.useState(); - const series = sourceData?.data?.series || []; + // Note: series data is not currently used in the sidebar but may be needed for future features + // const sourceData = queryRunner?.useState(); + // const series = sourceData?.data?.series || []; if (sidebarCollapsed) { return ( @@ -322,6 +330,7 @@ export function PanelDataSidebarRendered({ model }: SceneComponentProps handleAddTransform( { value: 'organize' }, 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 08fa0212a90..4e803bde5bc 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx @@ -19,6 +19,7 @@ interface QueryTransformCardProps { onDuplicate?: () => void; onRemove?: () => void; onToggleVisibility?: () => void; + debugHiddenOverride?: boolean | null; } export const QueryTransformCard = memo( @@ -29,6 +30,7 @@ export const QueryTransformCard = memo( onDuplicate, onRemove, onToggleVisibility, + debugHiddenOverride, }: QueryTransformCardProps) => { const colors = usePanelDataPaneColors(); const styles = useStyles2(getStyles, colors); @@ -45,10 +47,14 @@ export const QueryTransformCard = memo( return undefined; }, [type, data]); - const isHidden = + // Compute effective visibility: use debug override if present, otherwise use actual state + const actualHidden = ((type === 'query' || type === 'expression') && 'hide' in data && data.hide) || (type === 'transform' && 'disabled' in data && data.disabled); + const isHidden = + debugHiddenOverride !== null && debugHiddenOverride !== undefined ? debugHiddenOverride : actualHidden; + const typeLabel = useMemo(() => { switch (type) { case 'query': 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 454654cf15d..dacbf155f60 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'; -import { HTMLAttributes, memo, useCallback, useMemo, useState } from 'react'; +import { HTMLAttributes, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -11,7 +11,10 @@ import { AddDataItemMenu } from './AddDataItemMenu'; import { AiModeCard } from './AiModeCard'; import { ConnectionLines } from './ConnectionLines'; import { QueryTransformCard } from './QueryTransformCard'; +import { getItemHiddenState, restoreItemStates, syncItemsToDebugState } from './debugModeHelpers'; +import { usePanelDataPaneColors } from './theme'; import { QueryItem, QueryTransformItem, TransformItem } from './types'; +import { useDebugMode } from './useDebugMode'; const CARD_HEIGHT = 70; @@ -34,6 +37,7 @@ interface QueryTransformListProps { onReorderTransforms?: (startIndex: number, endIndex: number) => void; onAddOrganizeFieldsTransform?: () => void; onCollapseSidebar: () => void; + onDebugStateChange?: (isDebugMode: boolean, debugPosition: number) => void; } export const QueryTransformList = memo( @@ -56,8 +60,11 @@ export const QueryTransformList = memo( onReorderTransforms, onAddOrganizeFieldsTransform, onCollapseSidebar, + onDebugStateChange, }: QueryTransformListProps) => { - const styles = useStyles2(getStyles); + const colors = usePanelDataPaneColors(); + const styles = useStyles2(getStyles, colors); + const [isDragging, setIsDragging] = useState(false); const [isAiMode, setIsAiMode] = useState(false); const [isClosing, setIsClosing] = useState(false); @@ -66,6 +73,69 @@ export const QueryTransformList = memo( const [viewingConnections, setViewingConnections] = useState(false); const [collapsed, setCollapsed] = useState({ queries: false, transforms: false }); + // Debug mode via custom hook + const { + debugPosition, + setDebugPosition, + dragOffset, + handleDebugLineMouseDown, + isDebugMode, + isDraggingDebugLine, + isItemHiddenByDebug, + toggleDebugMode, + } = useDebugMode(allItems); + + // Store original states for restoration + const originalStatesRef = useRef>(new Map()); + + // Capture current states before entering debug mode + const saveCurrentStates = useCallback(() => { + const states = new Map(); + allItems.forEach((item) => { + const isHidden = getItemHiddenState(item); + states.set(item.id, isHidden); + }); + originalStatesRef.current = states; + }, [allItems]); + + // Update actual states to match debug position + const syncStatesToDebugPosition = useCallback(() => { + syncItemsToDebugState(allItems, isItemHiddenByDebug, onToggleQueryVisibility, onToggleTransformVisibility); + }, [allItems, isItemHiddenByDebug, onToggleQueryVisibility, onToggleTransformVisibility]); + + // Revert to original states + const restoreOriginalStates = useCallback(() => { + restoreItemStates(allItems, originalStatesRef.current, onToggleQueryVisibility, onToggleTransformVisibility); + originalStatesRef.current = new Map(); // Clear after restoring + }, [allItems, onToggleQueryVisibility, onToggleTransformVisibility]); + + // Handle state management + const handleToggleDebug = useCallback(() => { + if (!isDebugMode) { + // Save then sync + saveCurrentStates(); + toggleDebugMode(); + } else { + // Restore states + restoreOriginalStates(); + toggleDebugMode(); + } + }, [isDebugMode, saveCurrentStates, toggleDebugMode, restoreOriginalStates]); + + // Sync when debug position changes + const prevDebugPosition = useRef(debugPosition); + useEffect(() => { + if (isDebugMode && prevDebugPosition.current !== debugPosition) { + syncStatesToDebugPosition(); + } + prevDebugPosition.current = debugPosition; + }, [isDebugMode, debugPosition, syncStatesToDebugPosition]); + + // Notify parent of debug state changes + useEffect(() => { + onDebugStateChange?.(isDebugMode, debugPosition); + }, [isDebugMode, debugPosition, onDebugStateChange]); + const onDragStart = () => { setIsDragging(true); }; @@ -278,14 +348,40 @@ export const QueryTransformList = memo( {t('dashboard-scene.query-transform-list.header', 'Pipeline flow')} - + + + + {isAiMode && ( @@ -364,46 +460,95 @@ export const QueryTransformList = memo( )} > - {filteredDataSourceItems.map((item) => ( -
- - {(provided, snapshot) => ( -
{ + const globalIndex = allItems.findIndex((i) => i.id === item.id); + const isDebugDisabled = isDebugMode && globalIndex >= debugPosition; + const showDebugLineAfter = isDebugMode && globalIndex === debugPosition - 1; + + return ( + <> +
+ - handleCardClick(item.id)} - {...getHandlers(item)} + {(provided, snapshot) => ( +
+ handleCardClick(item.id)} + debugHiddenOverride={isItemHiddenByDebug(item.id)} + {...getHandlers(item)} + /> +
+ )} +
+
+
+
+ {showDebugLineAfter && ( +
{ + if (e.key === 'ArrowUp') { + e.preventDefault(); + setDebugPosition(Math.max(1, debugPosition - 1)); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + setDebugPosition(Math.min(allItems.length, debugPosition + 1)); + } + }} + > +
+ +
+
)} - -
- -
-
- ))} + + ); + })} {provided.placeholder} @@ -465,42 +610,97 @@ export const QueryTransformList = memo( onMouseMove={cardListHoverHandlerFactory(transformItems, 'transformations-last')} > - {transformItems.map((item) => ( -
- - {(provided, snapshot) => ( -
{ + // Find this item's position in the global allItems array + const globalIndex = allItems.findIndex((i) => i.id === item.id); + const isDebugDisabled = isDebugMode && globalIndex >= debugPosition; + const showDebugLineAfter = isDebugMode && globalIndex === debugPosition - 1; + + return ( + <> +
+ - handleCardClick(item.id)} - {...getHandlers(item)} + {(provided, snapshot) => ( +
+ handleCardClick(item.id)} + debugHiddenOverride={isItemHiddenByDebug(item.id)} + {...getHandlers(item)} + /> +
+ )} +
+
+
+
+ {showDebugLineAfter && ( +
{ + if (e.key === 'ArrowUp') { + e.preventDefault(); + setDebugPosition(Math.max(1, debugPosition - 1)); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + setDebugPosition(Math.min(allItems.length, debugPosition + 1)); + } + }} + > +
+ +
+
)} - -
- -
-
- ))} + + ); + })} {provided.placeholder}
@@ -560,7 +760,7 @@ export const QueryTransformList = memo( QueryTransformList.displayName = 'QueryTransformList'; -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2, colors: ReturnType) => { const headerHeight = 41; const footerHeight = 32; const barBase = { @@ -738,20 +938,43 @@ const getStyles = (theme: GrafanaTheme2) => { cardContainerLast: css({ marginTop: theme.spacing(2), }), + cardDebugDisabled: css({ + opacity: 0.4, + pointerEvents: 'none', + }), addButtonFloating: css({ position: 'absolute', top: theme.spacing(-2), left: theme.spacing(-2.5), }), - aiModeButton: css({ + aiModeButtonInactive: css({ + border: 'none', + borderRadius: theme.shape.radius.default, + fontFamily: theme.typography.fontFamilyMonospace, + fontWeight: theme.typography.fontWeightMedium, + textTransform: 'uppercase', + background: 'transparent', + }), + aiModeIcon: css({ + color: '#FF9830', + }), + aiModeText: css({ background: 'linear-gradient(90deg, #FF9830 0%, #B877D9 100%)', + WebkitBackgroundClip: 'text', + WebkitTextFillColor: 'transparent', + backgroundClip: 'text', + }), + aiModeButtonActive: css({ + background: 'linear-gradient(90deg, #FF9830 0%, #B877D9 100%)', + WebkitBackgroundClip: 'initial', + WebkitTextFillColor: 'initial', + backgroundClip: 'initial', border: 'none', borderRadius: theme.shape.radius.default, color: '#ffffff', fontWeight: theme.typography.fontWeightMedium, fontFamily: theme.typography.fontFamilyMonospace, textTransform: 'uppercase', - letterSpacing: '0.05em', '&:hover': { background: 'linear-gradient(90deg, #FFB050 0%, #C88FE5 100%)', boxShadow: '0 4px 12px rgba(255, 152, 48, 0.4)', @@ -761,5 +984,57 @@ const getStyles = (theme: GrafanaTheme2) => { boxShadow: '0 4px 12px rgba(255, 152, 48, 0.4)', }, }), + debugButton: css({ + border: 'none', + borderRadius: theme.shape.radius.default, + fontWeight: theme.typography.fontWeightMedium, + fontFamily: theme.typography.fontFamilyMonospace, + textTransform: 'uppercase', + letterSpacing: '0.05em', + }), + debugButtonActive: css({ + '&:focus, &:active, &:focus:active': { + background: '#441306', + }, + }), + debugLine: css({ + height: '4px', + background: colors.query.accent, + cursor: 'ns-resize', + position: 'relative', + marginTop: theme.spacing(1), + marginBottom: theme.spacing(1), + borderRadius: theme.shape.radius.default, + userSelect: 'none', + [theme.transitions.handleMotion('no-preference')]: { + transition: 'height 0.15s ease, transform 0.2s ease', + }, + '&:hover': { + height: '6px', + }, + }), + debugLineDragging: css({ + height: '6px', + [theme.transitions.handleMotion('no-preference')]: { + transition: 'height 0.15s ease', + }, + }), + debugLineHandle: css({ + position: 'absolute', + left: '50%', + top: '50%', + transform: 'translate(-50%, -50%)', + background: colors.query.accent, + borderRadius: theme.shape.radius.circle, + padding: theme.spacing(0.5), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.colors.text.primary, + cursor: 'grab', + '&:active': { + cursor: 'grabbing', + }, + }), }; }; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/debugModeHelpers.ts b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/debugModeHelpers.ts new file mode 100644 index 00000000000..31f81ec084f --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/debugModeHelpers.ts @@ -0,0 +1,109 @@ +/** + * Helper utilities for debug mode functionality in the panel data pane. + * + * Debug mode allows users to interactively enable/disable queries and transformations + * by dragging a visual line through the pipeline. This module provides utilities to: + * - Save original item states + * - Sync items to debug-computed states + * - Restore items to their original states + */ + +import { QueryTransformItem } from './types'; + +/** + * Get the current visibility/disabled state of an item. + * + * @param item - The query, expression, or transform item + * @returns true if the item is currently hidden/disabled, false otherwise + */ +export function getItemHiddenState(item: QueryTransformItem): boolean { + if (item.type === 'query' || item.type === 'expression') { + return (item.data && 'hide' in item.data && item.data.hide) || false; + } + if (item.type === 'transform') { + return (item.data && 'disabled' in item.data && item.data.disabled) || false; + } + return false; +} + +/** + * Get the appropriate toggle function for an item based on its type. + * + * @param item - The item to get the toggle function for + * @param onToggleQuery - Toggle function for queries/expressions + * @param onToggleTransform - Toggle function for transformations + * @returns The appropriate toggle function or undefined + */ +function getToggleFunction( + item: QueryTransformItem, + onToggleQuery?: (index: number) => void, + onToggleTransform?: (index: number) => void +): ((index: number) => void) | undefined { + if (item.type === 'query' || item.type === 'expression') { + return onToggleQuery; + } + if (item.type === 'transform') { + return onToggleTransform; + } + return undefined; +} + +/** + * Sync all items to match their debug-computed states. + * This toggles item visibility/disabled states to match what the debug line position dictates. + * + * @param items - All query, expression, and transform items + * @param isItemHiddenByDebug - Function that computes if an item should be hidden in debug mode + * @param onToggleQuery - Callback to toggle query/expression visibility + * @param onToggleTransform - Callback to toggle transformation disabled state + */ +export function syncItemsToDebugState( + items: QueryTransformItem[], + isItemHiddenByDebug: (itemId: string) => boolean | null, + onToggleQuery?: (index: number) => void, + onToggleTransform?: (index: number) => void +): void { + items.forEach((item) => { + const debugHidden = isItemHiddenByDebug(item.id); + if (debugHidden === null) { + return; + } + + const actuallyHidden = getItemHiddenState(item); + + if (debugHidden !== actuallyHidden) { + const toggleFn = getToggleFunction(item, onToggleQuery, onToggleTransform); + toggleFn?.(item.index); + } + }); +} + +/** + * Restore all items to their original states. + * This should be called when exiting debug mode to restore user settings. + * + * @param items - All query, expression, and transform items + * @param originalStates - Map of item IDs to their original hidden/disabled states + * @param onToggleQuery - Callback to toggle query/expression visibility + * @param onToggleTransform - Callback to toggle transformation disabled state + */ +export function restoreItemStates( + items: QueryTransformItem[], + originalStates: Map, + onToggleQuery?: (index: number) => void, + onToggleTransform?: (index: number) => void +): void { + items.forEach((item) => { + const originalState = originalStates.get(item.id); + if (originalState === undefined) { + return; + } + + const currentState = getItemHiddenState(item); + + if (originalState !== currentState) { + const toggleFn = getToggleFunction(item, onToggleQuery, onToggleTransform); + toggleFn?.(item.index); + } + }); +} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/useDebugMode.ts b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/useDebugMode.ts new file mode 100644 index 00000000000..90a838187de --- /dev/null +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/useDebugMode.ts @@ -0,0 +1,132 @@ +/** + * Custom hook for managing debug mode UI state in the panel data pane. + * + * This hook ONLY manages UI state (position, dragging, etc.). + * The parent component is responsible for saving/syncing/restoring actual item states. + * This keeps the hook pure and avoids side effects. + */ + +import { useCallback, useEffect, useState } from 'react'; + +import { QueryTransformItem } from './types'; + +// Approximate height of card + gap for position calculations +const CARD_WITH_GAP = 80; + +export interface UseDebugModeResult { + // State + isDebugMode: boolean; + debugPosition: number; + isDraggingDebugLine: boolean; + dragOffset: number; + + // Actions + toggleDebugMode: () => void; + setDebugPosition: (position: number) => void; + handleDebugLineMouseDown: (e: React.MouseEvent) => void; + isItemHiddenByDebug: (itemId: string) => boolean | null; +} + +export function useDebugMode(allItems: QueryTransformItem[]): UseDebugModeResult { + const [isDebugMode, setIsDebugMode] = useState(false); + const [debugPosition, setDebugPosition] = useState(allItems.length); + const [isDraggingDebugLine, setIsDraggingDebugLine] = useState(false); + const [dragStartY, setDragStartY] = useState(0); + const [dragStartPosition, setDragStartPosition] = useState(0); + const [dragOffset, setDragOffset] = useState(0); + + // Compute whether an item should be hidden by debug mode + const isItemHiddenByDebug = useCallback( + (itemId: string): boolean | null => { + if (!isDebugMode) { + return null; // Not in debug mode, use actual item state + } + + const globalIndex = allItems.findIndex((i) => i.id === itemId); + if (globalIndex === -1) { + return null; + } + + // Items at or after debugPosition are hidden + return globalIndex >= debugPosition; + }, + [isDebugMode, debugPosition, allItems] + ); + + const toggleDebugMode = useCallback(() => { + if (!isDebugMode) { + // When enabling debug mode, start with all items enabled + setDebugPosition(allItems.length); + } + setIsDebugMode(!isDebugMode); + }, [isDebugMode, allItems.length]); + + const handleDebugLineDrag = useCallback( + (e: MouseEvent) => { + if (!isDraggingDebugLine) { + return; + } + + // Calculate how far we've moved from the start + const deltaY = e.clientY - dragStartY; + setDragOffset(deltaY); + }, + [isDraggingDebugLine, dragStartY] + ); + + const handleDebugLineMouseDown = useCallback( + (e: React.MouseEvent) => { + setIsDraggingDebugLine(true); + setDragStartY(e.clientY); + setDragStartPosition(debugPosition); + setDragOffset(0); + }, + [debugPosition] + ); + + const handleDebugLineMouseUp = useCallback(() => { + setIsDraggingDebugLine(false); + + // Snap to nearest card position + const cardsMoved = Math.round(dragOffset / CARD_WITH_GAP); + let newPosition = dragStartPosition + cardsMoved; + + // Clamp between 1 and allItems.length + newPosition = Math.max(1, Math.min(allItems.length, newPosition)); + + setDebugPosition(newPosition); + setDragOffset(0); + }, [dragOffset, dragStartPosition, allItems.length]); + + // Add global mouse event listeners for dragging + useEffect(() => { + if (isDraggingDebugLine) { + document.addEventListener('mousemove', handleDebugLineDrag); + document.addEventListener('mouseup', handleDebugLineMouseUp); + return () => { + document.removeEventListener('mousemove', handleDebugLineDrag); + document.removeEventListener('mouseup', handleDebugLineMouseUp); + }; + } + return undefined; + }, [isDraggingDebugLine, handleDebugLineDrag, handleDebugLineMouseUp]); + + // Cleanup drag state when debug mode is disabled + useEffect(() => { + if (!isDebugMode && isDraggingDebugLine) { + setIsDraggingDebugLine(false); + setDragOffset(0); + } + }, [isDebugMode, isDraggingDebugLine]); + + return { + isDebugMode, + debugPosition, + isDraggingDebugLine, + dragOffset, + toggleDebugMode, + setDebugPosition, + handleDebugLineMouseDown, + isItemHiddenByDebug, + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 1bb30e49de9..6048caa9657 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3233,6 +3233,17 @@ "title-query-result": "Query result" } }, + "app": { + "features": { + "dashboardScene": { + "panelEdit": { + "panelDataPane": { + "expandSidebar": "Expand sidebar" + } + } + } + } + }, "app-chrome": { "skip-content-button": "Skip to main content", "top-bar": { @@ -5985,16 +5996,22 @@ }, "detail-view-header": { "actions": "Actions", + "debug": "Debug transformation", + "debug-transformation": "Debug transformation", "disable-transform": "Disable", "duplicate-query": "Duplicate", "edit-query-name": "Edit query name", "enable-transform": "Enable", "hide-response": "Hide response", + "input-data": "Input data", + "output-data": "Output data", "remove-query": "Remove", "remove-transform": "Remove", "run-query": "RUN QUERY", "save-query": "SAVE", - "show-response": "Show response" + "show-documentation": "Show documentation", + "show-response": "Show response", + "transformation-help": "Transformation help" }, "edit-link-view": { "edit-link-page-nav": { @@ -6286,15 +6303,28 @@ "disable-transform": "Disable transformation", "duplicate": "Duplicate query", "enable-transform": "Enable transformation", + "expression": { + "label": "Expression" + }, "hide-response": "Hide response", + "query": { + "label": "Query" + }, "remove-query": "Remove query", "remove-transform": "Remove transformation", - "show-response": "Show response" + "show-response": "Show response", + "transform": { + "label": "Transform" + } }, "query-transform-list": { "add": "Add", - "ai-mode": "AI Mode", - "close": "Close", + "ai-mode": "AI", + "ai-mode-enter": "Supercharge your pipeline with AI", + "ai-mode-exit": "Exit AI mode", + "debug-mode-enter": "Step through your pipeline", + "debug-mode-exit": "Exit debug mode", + "debug-position": "Debug position", "header": "Pipeline flow", "nodes": "nodes", "queries-expressions": "Queries & Expressions", @@ -6410,23 +6440,6 @@ "title-same-as-folder": "Dashboard name cannot be the same as the folder name", "title-validation-failed": "Dashboard title validation failed." }, - "saved-queries-drawer": { - "browse-tab": "Browse", - "description-label": "Description", - "description-placeholder": "Enter description...", - "name-label": "Name", - "name-placeholder": "Enter query name...", - "no-results": "No queries found", - "query-preview": "Query Preview", - "save-button": "Save Query", - "save-success": "Query saved successfully! (stub)", - "save-tab": "Save Current", - "save-title": "Save Query", - "search-placeholder": "Search saved queries...", - "stub-notice": "This is a stub implementation. Enable the queryLibrary feature toggle for full functionality.", - "title": "Saved Queries", - "use-query": "Use" - }, "scenes-new-rule-from-panel-button": { "body-no-alerting-capable-query-found": "Cannot create alerts from this panel because no query to an alerting capable datasource is found.", "new-alert-rule": "New alert rule", @@ -6488,6 +6501,13 @@ } } }, + "transformation-picker-view": { + "clear-search": "Clear search", + "close": "Close", + "no-results": "No transformations found", + "search-placeholder": "Search for transformation", + "title": "Add transformation" + }, "transformations-drawer": { "search-box-suffix": { "tooltip-clear-search": "Clear search" @@ -7431,6 +7451,9 @@ "pane": { "loading-placeholder": "Loading..." }, + "query-library": { + "default-title": "New query" + }, "queryless-apps-extensions": { "aria-label-go-queryless": "Go queryless" }, @@ -12414,6 +12437,38 @@ "title-data-source-help": "Data source help" } }, + "query-library": { + "empty-state": { + "message": "Start adding them from Explore or when editing a dashboard", + "title": "You haven't saved any queries yet" + }, + "filters": { + "search": "Search by..." + }, + "header": { + "close": "Close", + "delete-all": "Delete all", + "more": "More", + "save-query": "SAVE", + "select-query": "SELECT QUERY", + "title": "SAVED QUERIES" + }, + "item": { + "new": "New" + }, + "not-found": { + "message": "Try adjusting your search or filter criteria", + "title": "No results found" + }, + "query-details": { + "author": "Author", + "datasource": "Datasource", + "date-added": "Date added", + "description": "Description", + "make-query-visible": "Share query with all users", + "tags": "Tags" + } + }, "query-operation": { "header": { "collapse-row": "Collapse query row",