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 19dbc5b957e..66cb7375489 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/DetailView.tsx @@ -59,14 +59,14 @@ export const DetailView = memo(({ selectedItem, panel, tabs }: DetailViewProps) ); } } else { - const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations); - if (transformsTab instanceof PanelDataTransformationsTab && 'id' in selectedItem.data) { + const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations); + if (transformsTab && 'id' in selectedItem.data) { return ( <> - + 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 7209b730971..798ebc3af85 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -32,6 +32,7 @@ import { PanelDataTransformationsTab } from './PanelDataTransformationsTab'; import { QueryTransformList, QueryTransformItem } from './QueryTransformList'; import { TransformationsDrawer } from './TransformationsDrawer'; import { PanelDataPaneTab, TabId } from './types'; +import { isDataTransformerConfig, queryItemId, transformItemId } from './utils'; export interface PanelDataPaneState extends SceneObjectState { tabs: PanelDataPaneTab[]; @@ -39,10 +40,6 @@ export interface PanelDataPaneState extends SceneObjectState { panelRef: SceneObjectRef; } -const querySelectedId = (refId: string) => `query-${refId}`; -const transformSelectedId = (index: number) => `transform-${index}`; -const expressionSelectedId = (refId: string) => `expression-${refId}`; - export class PanelDataPane extends SceneObjectBase { static Component = PanelDataPaneRendered; protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] }); @@ -131,11 +128,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { } if (updatedQuery) { - setSelectedId( - updatedQuery.datasource?.type === '__expr__' - ? expressionSelectedId(updatedQuery.refId) - : querySelectedId(updatedQuery.refId) - ); + setSelectedId(queryItemId(updatedQuery)); } }); }, [queryRunner]); @@ -147,45 +140,29 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { // Build combined items list const items: QueryTransformItem[] = useMemo(() => { const result: QueryTransformItem[] = []; - - // Add queries and expressions (using actual array index) - const queries = queryRunnerState?.queries || []; - queries.forEach((query, actualIndex) => { - if ('refId' in query) { - if (isExpressionQuery(query)) { - result.push({ - id: expressionSelectedId(query.refId), - type: 'expression', - data: query, - index: actualIndex, // Store actual index in queries array - }); - } else { - result.push({ - id: querySelectedId(query.refId), - type: 'query', - data: query, - index: actualIndex, // Store actual index in queries array - }); - } - } - }); - - // Add transformations - const rawTransformations = transformerState?.transformations || []; - const transformations = Array.isArray(rawTransformations) - ? rawTransformations.filter( - (t): t is DataTransformerConfig => - t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string' - ) - : []; - transformations.forEach((transform, index) => { + const queries = queryRunnerState?.queries; + for (let i = 0; i < (queries?.length ?? 0); i++) { + const query = queries![i]; result.push({ - id: `transform-${index}`, - type: 'transform', - data: transform, - index, + id: queryItemId(query), + type: isExpressionQuery(query) ? 'expression' : 'query', + data: query, + index: i, // Store actual index in queries array }); - }); + } + + const transformations = transformerState?.transformations; + for (let i = 0; i < (transformations?.length ?? 0); i++) { + const transform = transformations![i]; + if (isDataTransformerConfig(transform)) { + result.push({ + id: transformItemId(i), + type: 'transform', + data: transform, + index: i, + }); + } + } return result; }, [queryRunnerState?.queries, transformerState?.transformations]); @@ -214,7 +191,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { setTimeout(() => { const newQueries = getQueryRunnerFor(panel)?.state.queries || []; if (newQueries.length > 0) { - setSelectedId(querySelectedId(newQueries[newQueries.length - 1].refId)); + setSelectedId(queryItemId(newQueries[newQueries.length - 1])); } }, 100); } @@ -254,7 +231,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { queryRunner.runQueries(); // Select the new expression - setSelectedId(expressionSelectedId(nextRefId)); + setSelectedId(queryItemId(newExpression)); } }, [queryRunner] @@ -268,15 +245,9 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations); if (transformsTab instanceof PanelDataTransformationsTab) { - const transformer = transformsTab.getDataTransformer(); - const rawTransformations = transformer.state.transformations || []; - const transformations = Array.isArray(rawTransformations) - ? rawTransformations.filter( - (t): t is DataTransformerConfig => - t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string' - ) - : []; - + const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter( + isDataTransformerConfig + ); const newTransformation: DataTransformerConfig = { id: selected.value, options: {}, @@ -287,7 +258,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { // Select the newly added transformation setTimeout(() => { - setSelectedId(transformSelectedId(transformations.length)); + setSelectedId(transformItemId(transformations.length)); }, 100); } }, @@ -319,7 +290,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { queryRunner.runQueries(); // Select the new query - setSelectedId(querySelectedId(newRefId)); + setSelectedId(queryItemId(duplicatedQuery)); } } }, @@ -335,7 +306,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { queryRunner.runQueries(); // Clear selection if removing the selected query - if (selectedId === querySelectedId(queries[index]?.refId)) { + if (selectedId === queryItemId(queries[index])) { setSelectedId(null); } } @@ -380,7 +351,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { queryRunner.setState({ queries: newQueries }); queryRunner.runQueries(); - setSelectedId(expressionSelectedId(newRefId)); + setSelectedId(queryItemId(duplicatedExpression)); } } }, @@ -396,7 +367,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { queryRunner.runQueries(); const expressionToRemove = queries[index]; - if (expressionToRemove && selectedId === expressionSelectedId(expressionToRemove.refId)) { + if (expressionToRemove && selectedId === queryItemId(expressionToRemove)) { setSelectedId(null); } } @@ -418,21 +389,16 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const handleRemoveTransform = useCallback( (index: number) => { - const transformsTab = tabs.find((t) => t.tabId === TabId.Transformations); - if (transformsTab instanceof PanelDataTransformationsTab) { - const transformer = transformsTab.getDataTransformer(); - const rawTransformations = transformer.state.transformations || []; - const transformations = Array.isArray(rawTransformations) - ? rawTransformations.filter( - (t): t is DataTransformerConfig => - t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string' - ) - : []; + const transformsTab = tabs.find((t): t is PanelDataTransformationsTab => t.tabId === TabId.Transformations); + if (transformsTab) { + const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter( + isDataTransformerConfig + ); const newTransformations = transformations.filter((_, i) => i !== index); transformsTab.onChangeTransformations(newTransformations); // Clear selection if removing the selected transformation - if (selectedId === transformSelectedId(index)) { + if (selectedId === transformItemId(index)) { setSelectedId(null); } } @@ -464,6 +430,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { onDuplicateQuery={handleDuplicateQuery} onRemoveQuery={handleRemoveQuery} onToggleQueryVisibility={handleToggleQueryVisibility} + // TODO: can all the expression stuff just be handled with the query handlers since expressions are queries? onDuplicateExpression={handleDuplicateExpression} onRemoveExpression={handleRemoveExpression} onToggleExpressionVisibility={handleToggleExpressionVisibility} diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx index d58eedcd9d6..c6218be2123 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataTransformationsTab.tsx @@ -24,7 +24,7 @@ import { PanelDataPane } from './PanelDataPane'; import { PanelDataQueriesTab } from './PanelDataQueriesTab'; import { TransformationsDrawer } from './TransformationsDrawer'; import { PanelDataPaneTab, TabId, PanelDataTabHeaderProps } from './types'; -import { findSqlExpression, scrollToQueryRow } from './utils'; +import { findSqlExpression, isDataTransformerConfig, scrollToQueryRow } from './utils'; const SET_TIMEOUT = 750; @@ -67,19 +67,17 @@ export class PanelDataTransformationsTab } } -export function PanelDataTransformationsTabRendered({ model }: SceneComponentProps) { +export function PanelDataTransformationsTabRendered({ + model, + selectedIdx, +}: SceneComponentProps & { selectedIdx?: number }) { const sourceData = model.getQueryRunner().useState(); - const { data, transformations: transformsWrongType } = model.getDataTransformer().useState(); + const { data, transformations: rawTransformations } = model.getDataTransformer().useState(); // Type guard to ensure transformations are DataTransformerConfig[] const transformations = useMemo(() => { - return Array.isArray(transformsWrongType) - ? transformsWrongType.filter( - (t): t is DataTransformerConfig => - t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string' - ) - : []; - }, [transformsWrongType]); + return Array.isArray(rawTransformations) ? rawTransformations.filter(isDataTransformerConfig) : []; + }, [rawTransformations]); const [drawerOpen, setDrawerOpen] = useState(false); @@ -159,7 +157,12 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro return ( <> - + {transformationsDrawer} ); @@ -169,9 +172,10 @@ interface TransformationEditorProps { transformations: DataTransformerConfig[]; model: PanelDataTransformationsTab; data: PanelData; + selectedIdx?: number; } -function TransformationsEditor({ transformations, model, data }: TransformationEditorProps) { +function TransformationsEditor({ transformations, model, data, selectedIdx }: TransformationEditorProps) { const transformationEditorRows = transformations.map((t, i) => ({ id: `${i} - ${t.id}`, transformation: t })); const styles = useStyles2(getStyles); @@ -211,6 +215,7 @@ function TransformationsEditor({ transformations, model, data }: TransformationE }} configs={transformationEditorRows} data={data} + selectedIdx={selectedIdx} > {provided.placeholder} 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 cc371d91272..c5e433c8e0a 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformCard.tsx @@ -8,6 +8,8 @@ import { getDataSourceSrv } from '@grafana/runtime'; import { SceneDataQuery } from '@grafana/scenes'; import { Icon, IconButton, Stack, useStyles2 } from '@grafana/ui'; +import { queryItemId, transformItemId } from './utils'; + interface QueryTransformCardProps { item: SceneDataQuery | DataTransformerConfig; type: 'query' | 'transform' | 'expression'; @@ -69,7 +71,7 @@ export const QueryTransformCard = memo( onClick={onClick} onKeyDown={handleKeyDown} data-testid={`${type}-card-${index}`} - data-card-id={'refId' in item ? item.refId : `transform-${index}`} + data-card-id={'refId' in item ? queryItemId(item) : transformItemId(index)} > {/* Header with type and action icons */}
+ isExpressionQuery(query) ? `expression-${query.refId}` : `query-${query.refId}`; +export const transformItemId = (index: number) => `transform-${index}`; + +export const isDataTransformerConfig = ( + t: DataTransformerConfig | CustomTransformerDefinition +): t is DataTransformerConfig => t !== null && typeof t === 'object' && 'id' in t && typeof t.id === 'string'; diff --git a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRows.tsx b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRows.tsx index 142f32aaf8e..6b839708fc5 100644 --- a/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRows.tsx +++ b/public/app/features/dashboard/components/TransformationsEditor/TransformationOperationRows.tsx @@ -1,3 +1,5 @@ +import { memo } from 'react'; + import { DataTransformerConfig, standardTransformersRegistry } from '@grafana/data'; import { TransformationOperationRow } from './TransformationOperationRow'; @@ -9,36 +11,56 @@ interface TransformationOperationRowsProps { configs: TransformationsEditorTransformation[]; onRemove: (index: number) => void; onChange: (index: number, config: DataTransformerConfig) => void; + selectedIdx?: number; } -export const TransformationOperationRows = ({ - data, - onChange, - onRemove, - configs, -}: TransformationOperationRowsProps) => { - return ( - <> - {configs.map((t, i) => { - const uiConfig = standardTransformersRegistry.getIfExists(t.transformation.id); +export const TransformationOperationRows = memo( + ({ data, onChange, onRemove, configs, selectedIdx }: TransformationOperationRowsProps) => { + if (selectedIdx != null) { + const t = configs[selectedIdx]; + if (!t) { + return null; + } - if (!uiConfig) { - return null; - } + const uiConfig = standardTransformersRegistry.getIfExists(t.transformation.id); + if (!uiConfig) { + return null; + } - return ( - - ); - })} - - ); -}; + return ( + + ); + } + + return configs.map((t, i) => { + const uiConfig = standardTransformersRegistry.getIfExists(t.transformation.id); + if (!uiConfig) { + return null; + } + + return ( + + ); + }); + } +); + +TransformationOperationRows.displayName = 'TransformationOperationRows';