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 {
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 (
<>
<DetailViewHeader selectedItem={selectedItem} panel={panel} />
<ScrollContainer>
<Container>
<PanelDataTransformationsTabRendered model={transformsTab} />
<PanelDataTransformationsTabRendered model={transformsTab} selectedIdx={selectedItem.index} />
</Container>
</ScrollContainer>
</>
@@ -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<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> {
static Component = PanelDataPaneRendered;
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['tab'] });
@@ -131,11 +128,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
}
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<PanelDataPane>) {
// 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<PanelDataPane>) {
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<PanelDataPane>) {
queryRunner.runQueries();
// Select the new expression
setSelectedId(expressionSelectedId(nextRefId));
setSelectedId(queryItemId(newExpression));
}
},
[queryRunner]
@@ -268,15 +245,9 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
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<PanelDataPane>) {
// Select the newly added transformation
setTimeout(() => {
setSelectedId(transformSelectedId(transformations.length));
setSelectedId(transformItemId(transformations.length));
}, 100);
}
},
@@ -319,7 +290,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
queryRunner.runQueries();
// Select the new query
setSelectedId(querySelectedId(newRefId));
setSelectedId(queryItemId(duplicatedQuery));
}
}
},
@@ -335,7 +306,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
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<PanelDataPane>) {
queryRunner.setState({ queries: newQueries });
queryRunner.runQueries();
setSelectedId(expressionSelectedId(newRefId));
setSelectedId(queryItemId(duplicatedExpression));
}
}
},
@@ -396,7 +367,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
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<PanelDataPane>) {
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<PanelDataPane>) {
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}
@@ -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<PanelDataTransformationsTab>) {
export function PanelDataTransformationsTabRendered({
model,
selectedIdx,
}: SceneComponentProps<PanelDataTransformationsTab> & { 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<DataTransformerConfig[]>(() => {
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<boolean>(false);
@@ -159,7 +157,12 @@ export function PanelDataTransformationsTabRendered({ model }: SceneComponentPro
return (
<>
<TransformationsEditor data={sourceData.data} transformations={transformations} model={model} />
<TransformationsEditor
data={sourceData.data}
transformations={transformations}
model={model}
selectedIdx={selectedIdx}
/>
{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}
></TransformationOperationRows>
{provided.placeholder}
</div>
@@ -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 */}
<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';
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 { 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 (
<TransformationOperationRow
index={i}
id={`${t.id}`}
key={`${t.id}`}
data={data}
configs={configs}
uiConfig={uiConfig}
onRemove={onRemove}
onChange={onChange}
/>
);
})}
</>
);
};
return (
<TransformationOperationRow
index={selectedIdx}
id={`${t.id}`}
key={`${t.id}`}
data={data}
configs={configs}
uiConfig={uiConfig}
onRemove={onRemove}
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';