rework some stuff to make selecting the correct transform easier

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