chore: alex - debug mode

This commit is contained in:
Alex Spencer
2025-12-04 13:38:19 -08:00
parent 65efd85d64
commit 15bf6b45f9
9 changed files with 746 additions and 113 deletions
@@ -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<QueryLibraryViewRef>(null);
@@ -147,6 +151,8 @@ export const DetailView = memo(
panel={panel}
onRemoveTransform={onRemoveTransform}
onToggleTransformVisibility={onToggleTransformVisibility}
isDebugMode={isDebugMode}
debugPosition={debugPosition}
/>
<ScrollContainer>
<Container>
@@ -178,6 +184,8 @@ export const DetailView = memo(
onOpenQueryLibrary,
handleSelectQueryFromHeader,
handleSaveQueryFromHeader,
isDebugMode,
debugPosition,
]);
return <div className={styles.container}>{renderContent()}</div>;
@@ -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 (
<div className={styles.header}>
@@ -674,6 +693,8 @@ export const DetailViewHeader = (props: DetailViewHeaderProps) => {
onRemoveTransform={props.onRemoveTransform}
onToggleTransformVisibility={props.onToggleTransformVisibility}
onOpenQueryLibrary={props.onOpenQueryLibrary}
isDebugMode={props.isDebugMode}
debugPosition={props.debugPosition}
/>
);
};
@@ -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<VizPanel>;
transformPickerIndex?: number | null;
queryLibraryMode: QueryLibraryMode & { index: number | null };
isDebugMode?: boolean;
debugPosition?: number;
}
export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
@@ -71,6 +73,8 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
mode: 'browse',
index: null,
},
isDebugMode: false,
debugPosition: 0,
});
}
@@ -94,6 +98,10 @@ export class PanelDataPane extends SceneObjectBase<PanelDataPaneState> {
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<PanelDataPaneState> {
}
function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
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<PanelDataPane>) {
onQueryLibrarySave={handleQueryLibrarySave}
onQueryLibraryClose={handleQueryLibraryClose}
onOpenQueryLibrary={handleOpenQueryLibrary}
isDebugMode={isDebugMode}
debugPosition={debugPosition}
/>
</div>
</div>
@@ -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<PanelDat
[transformations, transformsTab]
);
const handleDebugStateChange = useCallback(
(isDebugMode: boolean, debugPosition: number) => {
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<PanelDat
onToggleTransformVisibility={handleToggleTransformVisibility}
onReorderDataSources={handleReorderDataSources}
onReorderTransforms={handleReorderTransforms}
onDebugStateChange={handleDebugStateChange}
onAddOrganizeFieldsTransform={() =>
handleAddTransform(
{ value: 'organize' },
@@ -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':
@@ -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<boolean>(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<Map<string, boolean>>(new Map());
// Capture current states before entering debug mode
const saveCurrentStates = useCallback(() => {
const states = new Map<string, boolean>();
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(
<span>{t('dashboard-scene.query-transform-list.header', 'Pipeline flow')}</span>
</Stack>
</div>
<Button size="sm" onClick={handleToggleAiMode} className={styles.aiModeButton}>
<Stack direction="row" gap={0.5} alignItems="center">
<Icon name={isAiMode ? 'times' : 'ai'} size="sm" />
{isAiMode
? t('dashboard-scene.query-transform-list.close', 'Close')
: t('dashboard-scene.query-transform-list.ai-mode', 'AI Mode')}
</Stack>
</Button>
<Stack direction="row" gap={0.5}>
<Button
variant="secondary"
fill="text"
size="sm"
onClick={handleToggleDebug}
className={cx(styles.debugButton, { [styles.debugButtonActive]: isDebugMode })}
tooltip={
isDebugMode
? t('dashboard-scene.query-transform-list.debug-mode-exit', 'Exit debug mode')
: t('dashboard-scene.query-transform-list.debug-mode-enter', 'Step through your pipeline')
}
>
<Icon name="bug" size="sm" />
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleToggleAiMode}
className={cx(isAiMode ? styles.aiModeButtonActive : styles.aiModeButtonInactive)}
tooltip={
isAiMode
? t('dashboard-scene.query-transform-list.ai-mode-exit', 'Exit AI mode')
: t('dashboard-scene.query-transform-list.ai-mode-enter', 'Supercharge your pipeline with AI')
}
>
<Stack direction="row" gap={0.5} alignItems="center">
<Icon name="ai" size="sm" className={isAiMode ? undefined : styles.aiModeIcon} />
<span className={isAiMode ? undefined : styles.aiModeText}>
{t('dashboard-scene.query-transform-list.ai-mode', 'AI')}
</span>
</Stack>
</Button>
</Stack>
</Stack>
</div>
{isAiMode && (
@@ -364,46 +460,95 @@ export const QueryTransformList = memo(
)}
>
<Stack direction="column" gap={2}>
{filteredDataSourceItems.map((item) => (
<div key={item.id} className={styles.cardContainer}>
<Draggable
isDragDisabled={viewingConnections}
draggableId={item.id}
index={item.index}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={snapshot.isDragging ? styles.dragging : undefined}
{filteredDataSourceItems.map((item, idx) => {
const globalIndex = allItems.findIndex((i) => i.id === item.id);
const isDebugDisabled = isDebugMode && globalIndex >= debugPosition;
const showDebugLineAfter = isDebugMode && globalIndex === debugPosition - 1;
return (
<>
<div
key={item.id}
className={cx(styles.cardContainer, {
[styles.cardDebugDisabled]: isDebugDisabled,
})}
>
<Draggable
isDragDisabled={viewingConnections || isDebugMode}
draggableId={item.id}
index={item.index}
>
<QueryTransformCard
item={item}
isSelected={
isAiMode
? selectedContextIds.includes(item.id)
: selectedId === item.id
}
onClick={() => handleCardClick(item.id)}
{...getHandlers(item)}
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={snapshot.isDragging ? styles.dragging : undefined}
>
<QueryTransformCard
item={item}
isSelected={
isAiMode
? selectedContextIds.includes(item.id)
: selectedId === item.id
}
onClick={() => handleCardClick(item.id)}
debugHiddenOverride={isItemHiddenByDebug(item.id)}
{...getHandlers(item)}
/>
</div>
)}
</Draggable>
<div className={styles.addButtonFloating}>
<AddDataItemMenu
onAddQuery={onAddQuery}
onAddTransform={onAddTransform}
onAddExpression={onAddExpression}
onAddFromSavedQueries={onAddFromSavedQueries}
index={item.index}
allowedTypes={['query', 'expression']}
show={canAdd && hovered === item.id}
/>
</div>
</div>
{showDebugLineAfter && (
<div
role="slider"
aria-label={t(
'dashboard-scene.query-transform-list.debug-position',
'Debug position'
)}
aria-valuemin={1}
aria-valuemax={allItems.length}
aria-valuenow={debugPosition}
tabIndex={0}
className={cx(styles.debugLine, {
[styles.debugLineDragging]: isDraggingDebugLine,
})}
style={{
transform: isDraggingDebugLine
? `translateY(${dragOffset}px)`
: undefined,
}}
onMouseDown={handleDebugLineMouseDown}
onKeyDown={(e) => {
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));
}
}}
>
<div className={styles.debugLineHandle}>
<Icon name="draggabledots" size="sm" />
</div>
</div>
)}
</Draggable>
<div className={styles.addButtonFloating}>
<AddDataItemMenu
onAddQuery={onAddQuery}
onAddTransform={onAddTransform}
onAddExpression={onAddExpression}
onAddFromSavedQueries={onAddFromSavedQueries}
index={item.index}
allowedTypes={['query', 'expression']}
show={canAdd && hovered === item.id}
/>
</div>
</div>
))}
</>
);
})}
{provided.placeholder}
</Stack>
@@ -465,42 +610,97 @@ export const QueryTransformList = memo(
onMouseMove={cardListHoverHandlerFactory(transformItems, 'transformations-last')}
>
<Stack direction="column" gap={2}>
{transformItems.map((item) => (
<div key={item.id} className={styles.cardContainer}>
<Draggable key={item.id} draggableId={item.id} index={item.index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={snapshot.isDragging ? styles.dragging : undefined}
{transformItems.map((item) => {
// 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 (
<>
<div
key={item.id}
className={cx(styles.cardContainer, {
[styles.cardDebugDisabled]: isDebugDisabled,
})}
>
<Draggable
key={item.id}
draggableId={item.id}
index={item.index}
isDragDisabled={isDebugMode}
>
<QueryTransformCard
item={item}
isSelected={
isAiMode
? selectedContextIds.includes(item.id)
: selectedId === item.id
}
onClick={() => handleCardClick(item.id)}
{...getHandlers(item)}
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={snapshot.isDragging ? styles.dragging : undefined}
>
<QueryTransformCard
item={item}
isSelected={
isAiMode
? selectedContextIds.includes(item.id)
: selectedId === item.id
}
onClick={() => handleCardClick(item.id)}
debugHiddenOverride={isItemHiddenByDebug(item.id)}
{...getHandlers(item)}
/>
</div>
)}
</Draggable>
<div className={styles.addButtonFloating}>
<AddDataItemMenu
onAddQuery={onAddQuery}
onAddTransform={onAddTransform}
onAddExpression={onAddExpression}
onAddFromSavedQueries={onAddFromSavedQueries}
index={item.index}
allowedTypes={['transform']}
show={canAdd && hovered === item.id}
/>
</div>
</div>
{showDebugLineAfter && (
<div
role="slider"
aria-label={t(
'dashboard-scene.query-transform-list.debug-position',
'Debug position'
)}
aria-valuemin={1}
aria-valuemax={allItems.length}
aria-valuenow={debugPosition}
tabIndex={0}
className={cx(styles.debugLine, {
[styles.debugLineDragging]: isDraggingDebugLine,
})}
style={{
transform: isDraggingDebugLine
? `translateY(${dragOffset}px)`
: undefined,
}}
onMouseDown={handleDebugLineMouseDown}
onKeyDown={(e) => {
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));
}
}}
>
<div className={styles.debugLineHandle}>
<Icon name="draggabledots" size="sm" />
</div>
</div>
)}
</Draggable>
<div className={styles.addButtonFloating}>
<AddDataItemMenu
onAddQuery={onAddQuery}
onAddTransform={onAddTransform}
onAddExpression={onAddExpression}
onAddFromSavedQueries={onAddFromSavedQueries}
index={item.index}
allowedTypes={['transform']}
show={canAdd && hovered === item.id}
/>
</div>
</div>
))}
</>
);
})}
{provided.placeholder}
</Stack>
<div className={cx(styles.cardContainer, styles.cardContainerLast)}>
@@ -560,7 +760,7 @@ export const QueryTransformList = memo(
QueryTransformList.displayName = 'QueryTransformList';
const getStyles = (theme: GrafanaTheme2) => {
const getStyles = (theme: GrafanaTheme2, colors: ReturnType<typeof usePanelDataPaneColors>) => {
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',
},
}),
};
};
@@ -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<string, boolean>,
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);
}
});
}
@@ -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,
};
}
+76 -21
View File
@@ -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",