chore: alex - debug mode
This commit is contained in:
@@ -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':
|
||||
|
||||
+356
-81
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user