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/PanelDataPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx index f93eb89d4df..4c21c61f6c2 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); } } }, @@ -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 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 5ef6aa32623..1d5abaf0acb 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'; @@ -239,6 +239,25 @@ export const QueryTransformList = memo( } }; + 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)}>
@@ -299,11 +318,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 @@ -314,22 +333,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 @@ -362,7 +366,7 @@ export const QueryTransformList = memo(
)} -
+
@@ -379,15 +383,15 @@ export const QueryTransformList = memo(
-
+
@@ -395,15 +399,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 @@ -418,22 +431,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) => ( @@ -462,7 +460,7 @@ export const QueryTransformList = memo(
)} -
+
@@ -478,7 +476,7 @@ export const QueryTransformList = memo( {provided.placeholder}
-
+
@@ -494,8 +492,17 @@ export const QueryTransformList = memo( ); }} - - )} + ) : ( + + )} +
@@ -699,7 +706,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),