From 9002d98d02d161a177964e2eb3528c8182fe8a00 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 3 Dec 2025 16:29:26 -0500 Subject: [PATCH 1/4] more add button stuff --- .../PanelDataPane/AddDataItemMenu.tsx | 38 +++++++- .../PanelDataPane/QueryTransformList.tsx | 91 ++++++++++++------- 2 files changed, 95 insertions(+), 34 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx index 7f6b318f51e..c59708851b6 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/AddDataItemMenu.tsx @@ -1,8 +1,10 @@ -import { memo, useState } from 'react'; +import { css } from '@emotion/css'; +import { ComponentProps, memo, useState } from 'react'; +import { GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { Dropdown, IconButton, Menu } from '@grafana/ui'; +import { Button, Dropdown, IconButton, Menu, useStyles2 } from '@grafana/ui'; import { EXPRESSION_ICON_MAP } from 'app/features/expressions/consts'; import { ExpressionQueryType } from 'app/features/expressions/types'; @@ -14,6 +16,7 @@ interface AddDataItemMenuProps { index?: number; allowedTypes?: Array<'query' | 'transform' | 'expression'>; show?: boolean; + text?: string; } export const AddDataItemMenu = memo( @@ -23,15 +26,37 @@ export const AddDataItemMenu = memo( onAddExpression, onAddFromSavedQueries, index, + text, allowedTypes = ['query', 'expression', 'transform'], show = true, }: AddDataItemMenuProps) => { + const styles = useStyles2(getStyles); const [menuShown, setMenuShown] = useState(false); if (!show && !menuShown) { return; } + const renderButton = (onClick?: ComponentProps['onClick']) => { + return text ? ( + + ) : ( + + ); + }; + + if (allowedTypes.length === 1 && allowedTypes[0] === 'transform') { + return renderButton(() => onAddTransform(index)); + } + const expressionTypes = [ { type: ExpressionQueryType.math, label: t('dashboard-scene.add-data-item-menu.expression-math', 'Math') }, { type: ExpressionQueryType.reduce, label: t('dashboard-scene.add-data-item-menu.expression-reduce', 'Reduce') }, @@ -102,10 +127,17 @@ export const AddDataItemMenu = memo( return ( setMenuShown(shown)}> - + {renderButton()} ); } ); +const getStyles = (theme: GrafanaTheme2) => ({ + textButton: css({ + paddingLeft: 0, + fontFamily: theme.typography.fontFamilyMonospace, + }), +}); + AddDataItemMenu.displayName = 'AddDataItemMenu'; diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx index 53c4e260974..88cdd2c7ea7 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx @@ -191,11 +191,11 @@ export const QueryTransformList = memo(
{/* Data Sources Section (Queries + Expressions) */} - {dataSourceItems.length > 0 && ( - -
- {t('dashboard-scene.query-transform-list.queries-expressions', 'Queries & Expressions')} -
+ +
+ {t('dashboard-scene.query-transform-list.queries-expressions', 'Queries & Expressions')} +
+ {dataSourceItems.length > 0 ? ( {(provided, snapshot) => { // Check if dragging from transformations section @@ -206,7 +206,7 @@ export const QueryTransformList = memo(
{ + onMouseMove={(ev) => { const rect = ev.currentTarget.getBoundingClientRect(); const y = ev.clientY - rect.top; let hoveredIdx = Math.floor(y / CARD_HEIGHT); @@ -216,10 +216,16 @@ export const QueryTransformList = memo( if (hoveredIdx > dataSourceItems.length) { hoveredIdx = dataSourceItems.length; } - const hoveredId = hoveredIdx === dataSourceItems.length ? 'queries-last' : dataSourceItems[hoveredIdx].id; + const hoveredId = + hoveredIdx === dataSourceItems.length + ? 'queries-last' + : dataSourceItems[hoveredIdx].id; setHovered(hoveredId); }} - className={cx(styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined)} + className={cx( + styles.cardList, + isDraggingFromOtherSection ? styles.droppableInvalid : undefined + )} > {dataSourceItems.map((item, index) => ( @@ -246,7 +252,7 @@ export const QueryTransformList = memo(
)} -
+
@@ -263,7 +269,7 @@ export const QueryTransformList = memo(
-
+
@@ -279,16 +285,24 @@ export const QueryTransformList = memo( ); }} - - - )} + ) : ( + + )} + {/* Transformations Section */} - {transformItems.length > 0 && ( - -
- {t('dashboard-scene.query-transform-list.transformations', 'Transformations')} -
+ +
+ {t('dashboard-scene.query-transform-list.transformations', 'Transformations')} +
+ {transformItems.length > 0 ? ( {(provided, snapshot) => { // Check if dragging from data sources section @@ -299,18 +313,24 @@ export const QueryTransformList = memo(
{ + className={cx( + styles.cardList, + isDraggingFromOtherSection ? styles.droppableInvalid : undefined + )} + onMouseMove={(ev) => { const rect = ev.currentTarget.getBoundingClientRect(); const y = ev.clientY - rect.top; - let hoveredIdx = Math.floor(((y - 16 + (CARD_HEIGHT / 2)) / CARD_HEIGHT)); + let hoveredIdx = Math.floor((y - 16 + CARD_HEIGHT / 2) / CARD_HEIGHT); if (hoveredIdx < 0) { hoveredIdx = 0; } if (hoveredIdx > transformItems.length) { hoveredIdx = transformItems.length; } - const hoveredId = hoveredIdx === transformItems.length ? 'transformations-last' : transformItems[hoveredIdx].id; + const hoveredId = + hoveredIdx === transformItems.length + ? 'transformations-last' + : transformItems[hoveredIdx].id; setHovered(hoveredId); }} > @@ -339,7 +359,7 @@ export const QueryTransformList = memo(
)} -
+
@@ -355,7 +375,7 @@ export const QueryTransformList = memo( {provided.placeholder}
-
+
@@ -371,8 +391,17 @@ export const QueryTransformList = memo( ); }} - - )} + ) : ( + + )} +
@@ -519,7 +548,7 @@ const getStyles = (theme: GrafanaTheme2) => { cardContainerLast: css({ marginTop: theme.spacing(2), }), - floatingButton: css({ + addButtonFloating: css({ position: 'absolute', top: theme.spacing(-2), left: theme.spacing(-2.5), From 70558603728fc2208fd9ff4aff8c9c2342d71532 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 3 Dec 2025 16:31:22 -0500 Subject: [PATCH 2/4] delete console.log --- .../panel-edit/PanelDataPane/QueryTransformList.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx index 88cdd2c7ea7..07d91600fe8 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'; -import { memo, useEffect, useMemo, useState } from 'react'; +import { memo, useMemo, useState } from 'react'; import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -63,10 +63,6 @@ export const QueryTransformList = memo( const [isDragging, setIsDragging] = useState(false); const [hovered, setHovered] = useState(null); - useEffect(() => { - console.log('hovered changed:', hovered); - }, [hovered]); - const onDragStart = () => { setIsDragging(true); }; From 33fe608e54792ac1fc97cbea6a2f8f679591712d Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 3 Dec 2025 16:38:41 -0500 Subject: [PATCH 3/4] more add button cleanup --- .../PanelDataPane/QueryTransformList.tsx | 57 ++++++++----------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx index 07d91600fe8..e2e5f313b92 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/QueryTransformList.tsx @@ -1,6 +1,6 @@ import { css, cx } from '@emotion/css'; import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'; -import { memo, useMemo, useState } from 'react'; +import { HTMLAttributes, memo, useMemo, useState } from 'react'; import { DataTransformerConfig, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; @@ -170,6 +170,25 @@ export const QueryTransformList = memo( }; }, [allItems, dataSourceItems, transformItems]); + const cardListHoverHandlerFactory = ( + itemList: QueryTransformItem[], + lastItemId: string + ): HTMLAttributes['onMouseMove'] => { + return (ev) => { + const rect = ev.currentTarget.getBoundingClientRect(); + const y = ev.clientY - rect.top; + let hoveredIdx = Math.floor((y - 16 + CARD_HEIGHT / 2) / CARD_HEIGHT); + if (hoveredIdx < 0) { + hoveredIdx = 0; + } + if (hoveredIdx > itemList.length) { + hoveredIdx = itemList.length; + } + const hoveredId = hoveredIdx === itemList.length ? lastItemId : itemList[hoveredIdx].id; + setHovered(hoveredId); + }; + }; + return (
setHovered(null)}>
@@ -202,22 +221,7 @@ export const QueryTransformList = memo(
{ - const rect = ev.currentTarget.getBoundingClientRect(); - const y = ev.clientY - rect.top; - let hoveredIdx = Math.floor(y / CARD_HEIGHT); - if (hoveredIdx < 0) { - hoveredIdx = 0; - } - if (hoveredIdx > dataSourceItems.length) { - hoveredIdx = dataSourceItems.length; - } - const hoveredId = - hoveredIdx === dataSourceItems.length - ? 'queries-last' - : dataSourceItems[hoveredIdx].id; - setHovered(hoveredId); - }} + onMouseMove={cardListHoverHandlerFactory(dataSourceItems, 'queries-last')} className={cx( styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined @@ -272,7 +276,7 @@ export const QueryTransformList = memo( onAddTransform={onAddTransform} onAddExpression={onAddExpression} allowedTypes={['query', 'expression']} - index={transformItems.length} + index={dataSourceItems.length} show={!isDragging && hovered === 'queries-last'} />
@@ -313,22 +317,7 @@ export const QueryTransformList = memo( styles.cardList, isDraggingFromOtherSection ? styles.droppableInvalid : undefined )} - onMouseMove={(ev) => { - const rect = ev.currentTarget.getBoundingClientRect(); - const y = ev.clientY - rect.top; - let hoveredIdx = Math.floor((y - 16 + CARD_HEIGHT / 2) / CARD_HEIGHT); - if (hoveredIdx < 0) { - hoveredIdx = 0; - } - if (hoveredIdx > transformItems.length) { - hoveredIdx = transformItems.length; - } - const hoveredId = - hoveredIdx === transformItems.length - ? 'transformations-last' - : transformItems[hoveredIdx].id; - setHovered(hoveredId); - }} + onMouseMove={cardListHoverHandlerFactory(transformItems, 'transformations-last')} > {transformItems.map((item, index) => ( From 5e69b4dcc23efd08c661b974961fb8e21461429d Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Wed, 3 Dec 2025 16:52:06 -0500 Subject: [PATCH 4/4] clean up some state and selection stuff after removing a card --- .../PanelDataPane/PanelDataPane.tsx | 38 ++++++++----------- 1 file changed, 15 insertions(+), 23 deletions(-) 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 bd583382d60..5f81a998848 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx @@ -111,7 +111,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const transformer = transformsTab?.getDataTransformer(); const transformerState = transformer?.useState(); const queries = queryRunnerState?.queries; - const transformations = transformerState?.transformations; + const transformations = transformerState?.transformations?.filter(isDataTransformerConfig); // the selectedId is based on the refId of the query. refId is a user-editable property, so it can change, // which will break the selectId and result in the UI going into a deselected state. to avoid this, @@ -272,7 +272,8 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { // Clear selection if removing the selected query if (deletedQuery && selectedId === queryItemId(deletedQuery)) { - setSelectedId(null); + const prevQuery = newQueries?.[index - 1]; + setSelectedId(prevQuery ? queryItemId(prevQuery) : null); } } }, @@ -303,7 +304,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { let nextRefId = 'A'; const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; for (let i = 0; i < alphabet.length; i++) { - if (!queries?.some(q => q.refId === alphabet[i])) { + if (!queries?.some((q) => q.refId === alphabet[i])) { nextRefId = alphabet[i]; break; } @@ -345,7 +346,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { unsub.unsubscribe(); }); - const newTransformations = [...(transformations?.filter(isDataTransformerConfig) ?? [])]; + const newTransformations = [...(transformations ?? [])]; newTransformations.splice(selectedIndex, 0, newTransformation); transformsTab.onChangeTransformations(newTransformations); @@ -357,34 +358,28 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const handleRemoveTransform = useCallback( (index: number) => { if (transformsTab) { - const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter( - isDataTransformerConfig - ); - const newTransformations = transformations.filter((_, i) => i !== index); + const newTransformations = transformations?.filter((_, i) => i !== index) ?? []; transformsTab.onChangeTransformations(newTransformations); // Clear selection if removing the selected transformation if (selectedId === transformItemId(index)) { - setSelectedId(null); + const prevTransform = newTransformations[index - 1]; + setSelectedId(prevTransform ? transformItemId(index - 1) : null); } } }, - [transformsTab, selectedId] + [transformations, transformsTab, selectedId] ); const handleToggleTransformVisibility = useCallback( (index: number) => { 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 - ); + const newTransformations = + transformations?.map((t, i) => (i === index ? { ...t, disabled: t.disabled ? undefined : true } : t)) ?? []; transformsTab.onChangeTransformations(newTransformations); } }, - [transformsTab] + [transformations, transformsTab] ); const handleReorderDataSources = useCallback( @@ -403,16 +398,13 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { const handleReorderTransforms = useCallback( (startIndex: number, endIndex: number) => { if (transformsTab) { - const transformations = (transformsTab.getDataTransformer().state.transformations || []).filter( - isDataTransformerConfig - ); - const newTransformations = Array.from(transformations); + const newTransformations = [...(transformations ?? [])]; const [removed] = newTransformations.splice(startIndex, 1); newTransformations.splice(endIndex, 0, removed); transformsTab.onChangeTransformations(newTransformations); } }, - [transformsTab] + [transformations, transformsTab] ); // Get data for transformations drawer @@ -436,7 +428,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps) { selectedId={effectiveSelectedId} onSelect={handleSelect} onAddQuery={handleAddQuery} - onAddFromSavedQueries={index => setSavedQueriesDrawerState({ open: true, index: index ?? null })} + onAddFromSavedQueries={(index) => setSavedQueriesDrawerState({ open: true, index: index ?? null })} onAddTransform={(index) => setTransformDrawerState({ open: true, index: index ?? null })} onAddExpression={handleAddExpression} onDuplicateQuery={handleDuplicateQuery}