chore: alex - fix transformation enabled/disabled, sections!

This commit is contained in:
Alex Spencer
2025-12-02 15:14:25 -08:00
parent 2734cb55b6
commit 96e0f4718b
3 changed files with 134 additions and 49 deletions
@@ -137,13 +137,16 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
const dataTransformer = panel.state.$data instanceof SceneDataTransformer ? panel.state.$data : null;
const transformerState = dataTransformer?.useState();
// Build combined items list
const items: QueryTransformItem[] = useMemo(() => {
const result: QueryTransformItem[] = [];
// Build separate lists for queries/expressions and transformations
const { dataSourceItems, transformItems, allItems } = useMemo(() => {
const dataSourceItems: QueryTransformItem[] = [];
const transformItems: QueryTransformItem[] = [];
// Add queries and expressions
const queries = queryRunnerState?.queries;
for (let i = 0; i < (queries?.length ?? 0); i++) {
const query = queries![i];
result.push({
dataSourceItems.push({
id: queryItemId(query),
type: isExpressionQuery(query) ? 'expression' : 'query',
data: query,
@@ -151,11 +154,12 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
});
}
// Add transformations
const transformations = transformerState?.transformations;
for (let i = 0; i < (transformations?.length ?? 0); i++) {
const transform = transformations![i];
if (isDataTransformerConfig(transform)) {
result.push({
transformItems.push({
id: transformItemId(i),
type: 'transform',
data: transform,
@@ -164,20 +168,24 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
}
}
return result;
return {
dataSourceItems,
transformItems,
allItems: [...dataSourceItems, ...transformItems],
};
}, [queryRunnerState?.queries, transformerState?.transformations]);
// Auto-select first item if nothing is selected
const effectiveSelectedId = useMemo(() => {
if (selectedId === null && items.length > 0) {
return items[0].id;
if (selectedId === null && allItems.length > 0) {
return allItems[0].id;
}
return selectedId;
}, [selectedId, items]);
}, [selectedId, allItems]);
const selectedItem = useMemo(() => {
return items.find((item) => item.id === effectiveSelectedId);
}, [items, effectiveSelectedId]);
return allItems.find((item) => item.id === effectiveSelectedId);
}, [allItems, effectiveSelectedId]);
const handleSelect = useCallback((id: string) => {
setSelectedId(id);
@@ -406,6 +414,22 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
[tabs, selectedId]
);
const handleToggleTransformVisibility = useCallback(
(index: number) => {
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.map((t, i) =>
i === index ? { ...t, disabled: t.disabled ? undefined : true } : t
);
transformsTab.onChangeTransformations(newTransformations);
}
},
[tabs]
);
// Get data for transformations drawer
const sourceData = queryRunner?.useState();
const series = sourceData?.data?.series || [];
@@ -421,7 +445,9 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
<div {...containerProps} className={cx(containerProps.className, styles.unifiedLayout)}>
<div {...primaryProps} className={cx(primaryProps.className, styles.leftPane)}>
<QueryTransformList
items={items}
allItems={allItems}
dataSourceItems={dataSourceItems}
transformItems={transformItems}
selectedId={effectiveSelectedId}
onSelect={handleSelect}
onAddQuery={handleAddQuery}
@@ -435,6 +461,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
onRemoveExpression={handleRemoveExpression}
onToggleExpressionVisibility={handleToggleExpressionVisibility}
onRemoveTransform={handleRemoveTransform}
onToggleTransformVisibility={handleToggleTransformVisibility}
/>
</div>
<div
@@ -470,9 +497,9 @@ export function shouldShowAlertingTab(pluginId: string) {
return isGraph || isTimeseries;
}
// Left pane sizing: card width + padding (16px left + 48px right = 64px)
const LEFT_PANE_MIN = 180 + 64; // 244px (180px card + padding)
const LEFT_PANE_MAX = 240 + 64; // 304px (240px card + padding)
// Left pane sizing: cards grow from 180px-300px, plus content padding (32px + 48px = 80px)
const LEFT_PANE_MIN = 180 + 80; // 260px (180px card min + 80px padding)
const LEFT_PANE_MAX = 300 + 80; // 380px (300px card max + 80px padding)
function getStyles(theme: GrafanaTheme2) {
return {
@@ -496,9 +523,9 @@ function getStyles(theme: GrafanaTheme2) {
cursor: 'col-resize',
}),
leftPane: css({
// !important to override useSplitter's inline styles
// !important on minWidth to override useSplitter's inline minWidth: 'min-content'
minWidth: `${LEFT_PANE_MIN}px !important`,
maxWidth: `${LEFT_PANE_MAX}px !important`,
maxWidth: `${LEFT_PANE_MAX}px`,
overflow: 'hidden',
}),
};
@@ -48,7 +48,9 @@ export const QueryTransformCard = memo(
return undefined;
}, [type, item]);
const isHidden = (type === 'query' || type === 'expression') && 'hide' in item && item.hide;
const isHidden =
((type === 'query' || type === 'expression') && 'hide' in item && item.hide) ||
(type === 'transform' && 'disabled' in item && item.disabled);
const icon = type === 'query' ? 'database' : type === 'expression' ? 'code' : 'pivot';
const typeLabel = type === 'query' ? 'Query' : type === 'expression' ? 'Expression' : 'Transformation';
const name = getName();
@@ -90,15 +92,19 @@ export const QueryTransformCard = memo(
<span className={styles.typeLabel}>{typeLabel}</span>
</div>
<Stack gap={0.25}>
{(type === 'query' || type === 'expression') && onToggleVisibility && (
{onToggleVisibility && (
<IconButton
name={isHidden ? 'eye-slash' : 'eye'}
size="sm"
variant="secondary"
tooltip={
isHidden
? t('dashboard-scene.query-transform-card.show-response', 'Show response')
: t('dashboard-scene.query-transform-card.hide-response', 'Hide response')
? type === 'transform'
? t('dashboard-scene.query-transform-card.enable-transform', 'Enable transformation')
: t('dashboard-scene.query-transform-card.show-response', 'Show response')
: type === 'transform'
? t('dashboard-scene.query-transform-card.disable-transform', 'Disable transformation')
: t('dashboard-scene.query-transform-card.hide-response', 'Hide response')
}
onClick={(e) => handleAction(e, onToggleVisibility)}
className={styles.actionButton}
@@ -161,7 +167,7 @@ const getStyles = (theme: GrafanaTheme2, colors: ReturnType<typeof usePanelDataP
background: theme.colors.background.secondary,
width: '100%',
minWidth: 180,
maxWidth: 240,
maxWidth: 300,
'&:hover': {
borderColor: theme.colors.border.strong,
},
@@ -19,7 +19,9 @@ export interface QueryTransformItem {
}
interface QueryTransformListProps {
items: QueryTransformItem[];
allItems: QueryTransformItem[];
dataSourceItems: QueryTransformItem[];
transformItems: QueryTransformItem[];
selectedId: string | null;
onSelect: (id: string) => void;
onAddQuery: () => void;
@@ -32,11 +34,14 @@ interface QueryTransformListProps {
onRemoveExpression?: (index: number) => void;
onToggleExpressionVisibility?: (index: number) => void;
onRemoveTransform?: (index: number) => void;
onToggleTransformVisibility?: (index: number) => void;
}
export const QueryTransformList = memo(
({
items,
dataSourceItems,
transformItems,
allItems,
selectedId,
onSelect,
onAddQuery,
@@ -49,13 +54,14 @@ export const QueryTransformList = memo(
onRemoveExpression,
onToggleExpressionVisibility,
onRemoveTransform,
onToggleTransformVisibility,
}: QueryTransformListProps) => {
const styles = useStyles2(getStyles);
const allConnections = useMemo(() => {
const conns: Array<{ from: string; to: string }> = [];
items.forEach((item) => {
allItems.forEach((item) => {
if (item.type === 'expression' && 'expression' in item.data && 'refId' in item.data) {
const expr = item.data;
@@ -78,7 +84,7 @@ export const QueryTransformList = memo(
});
return conns;
}, [items]);
}, [allItems]);
// Filter connections to only show for selected card
const visibleConnections = useMemo(() => {
@@ -87,7 +93,7 @@ export const QueryTransformList = memo(
}
// Find the item to get its refId
const activeItem = items.find((item) => item.id === selectedId);
const activeItem = allItems.find((item) => item.id === selectedId);
if (!activeItem || !('refId' in activeItem.data)) {
return [];
}
@@ -96,7 +102,7 @@ export const QueryTransformList = memo(
// Show connections where this card is involved (either as source or destination)
return allConnections.filter((conn) => conn.from === activeRefId || conn.to === activeRefId);
}, [allConnections, selectedId, items]);
}, [allConnections, selectedId, allItems]);
const getHandlers = (item: QueryTransformItem) => {
switch (item.type) {
@@ -118,23 +124,24 @@ export const QueryTransformList = memo(
return {
onDuplicate: undefined,
onRemove: onRemoveTransform ? () => onRemoveTransform(item.index) : undefined,
onToggleVisibility: undefined,
onToggleVisibility: onToggleTransformVisibility ? () => onToggleTransformVisibility(item.index) : undefined,
};
}
};
const stats = useMemo(() => {
const totalCards = items.length;
const queries = items.filter((item) => item.type === 'query' || item.type === 'expression');
const hiddenQueries = queries.filter((item) => 'hide' in item.data && item.data.hide);
const visibleQueries = queries.length - hiddenQueries.length;
const totalCards = allItems.length;
const hiddenQueries = dataSourceItems.filter((item) => 'hide' in item.data && item.data.hide);
const disabledTransforms = transformItems.filter((item) => 'disabled' in item.data && item.data.disabled);
const hiddenTotal = hiddenQueries.length + disabledTransforms.length;
const visibleTotal = totalCards - hiddenTotal;
return {
totalCards,
visibleQueries,
hiddenQueries: hiddenQueries.length,
visibleQueries: visibleTotal,
hiddenQueries: hiddenTotal,
};
}, [items]);
}, [allItems, dataSourceItems, transformItems]);
return (
<div className={styles.container}>
@@ -149,18 +156,46 @@ export const QueryTransformList = memo(
<div className={styles.scrollWrapper}>
<ScrollContainer data-scrollcontainer>
<div className={styles.content}>
<Stack direction="column" gap={2}>
{items.map((item) => (
<QueryTransformCard
key={item.id}
item={item.data}
type={item.type}
index={item.index}
isSelected={selectedId === item.id}
onClick={() => onSelect(item.id)}
{...getHandlers(item)}
/>
))}
<Stack direction="column" gap={3}>
{/* Data Sources Section (Queries + Expressions) */}
{dataSourceItems.length > 0 && (
<Stack direction="column" gap={2}>
<div className={styles.sectionLabel}>
{t('dashboard-scene.query-transform-list.queries-expressions', 'Queries & Expressions')}
</div>
{dataSourceItems.map((item) => (
<QueryTransformCard
key={item.id}
item={item.data}
type={item.type}
index={item.index}
isSelected={selectedId === item.id}
onClick={() => onSelect(item.id)}
{...getHandlers(item)}
/>
))}
</Stack>
)}
{/* Transformations Section */}
{transformItems.length > 0 && (
<Stack direction="column" gap={2}>
<div className={styles.sectionLabel}>
{t('dashboard-scene.query-transform-list.transformations', 'Transformations')}
</div>
{transformItems.map((item) => (
<QueryTransformCard
key={item.id}
item={item.data}
type={item.type}
index={item.index}
isSelected={selectedId === item.id}
onClick={() => onSelect(item.id)}
{...getHandlers(item)}
/>
))}
</Stack>
)}
<AddDataItemMenu
onAddQuery={onAddQuery}
@@ -226,13 +261,30 @@ const getStyles = (theme: GrafanaTheme2) => {
textTransform: 'uppercase',
color: theme.colors.text.primary,
}),
sectionLabel: css({
fontFamily: "'CommitMono', monospace",
fontSize: theme.typography.bodySmall.fontSize,
color: theme.colors.text.maxContrast,
textTransform: 'uppercase',
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
marginLeft: theme.spacing(-2),
marginRight: theme.spacing(-2),
'&::before, &::after': {
content: '""',
flex: 1,
height: '1px',
background: theme.colors.border.weak,
},
}),
scrollWrapper: css({
flex: 1,
minHeight: 0,
position: 'relative',
}),
content: css({
padding: theme.spacing(2),
padding: theme.spacing(4),
paddingRight: theme.spacing(6),
}),
footer: css({