Merge branch 'data-manipulation-improvements' of https://github.com/grafana/grafana into data-manipulation-improvements
This commit is contained in:
@@ -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<typeof Button>['onClick']) => {
|
||||
return text ? (
|
||||
<Button onClick={onClick} className={styles.textButton} size="md" variant="primary" icon="plus" fill="text">
|
||||
{text}
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
name="plus"
|
||||
size="xs"
|
||||
variant="primary"
|
||||
tooltip={t('dashboard-scene.add-data-item-menu.add-button', 'Add')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Dropdown overlay={menu} placement="top-end" onVisibleChange={(shown) => setMenuShown(shown)}>
|
||||
<IconButton name="plus" size="xs" variant="primary" tooltip={t('dashboard-scene.add-data-item-menu.add-button', 'Add')} />
|
||||
{renderButton()}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
textButton: css({
|
||||
paddingLeft: 0,
|
||||
fontFamily: theme.typography.fontFamilyMonospace,
|
||||
}),
|
||||
});
|
||||
|
||||
AddDataItemMenu.displayName = 'AddDataItemMenu';
|
||||
|
||||
@@ -111,7 +111,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
|
||||
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<PanelDataPane>) {
|
||||
|
||||
// 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<PanelDataPane>) {
|
||||
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<PanelDataPane>) {
|
||||
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<PanelDataPane>) {
|
||||
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
|
||||
|
||||
+64
-57
@@ -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<HTMLDivElement>['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 (
|
||||
<div className={styles.container} onMouseLeave={() => setHovered(null)}>
|
||||
<div className={styles.header}>
|
||||
@@ -299,11 +318,11 @@ export const QueryTransformList = memo(
|
||||
>
|
||||
<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>
|
||||
<Stack direction="column" gap={2}>
|
||||
<div className={styles.sectionLabel}>
|
||||
{t('dashboard-scene.query-transform-list.queries-expressions', 'Queries & Expressions')}
|
||||
</div>
|
||||
{dataSourceItems.length > 0 ? (
|
||||
<Droppable droppableId="data-sources">
|
||||
{(provided, snapshot) => {
|
||||
// Check if dragging from transformations section
|
||||
@@ -314,22 +333,7 @@ export const QueryTransformList = memo(
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.droppableProps}
|
||||
onMouseMove={(ev) => {
|
||||
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(
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<div className={styles.floatingButton}>
|
||||
<div className={styles.addButtonFloating}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
@@ -370,7 +374,7 @@ export const QueryTransformList = memo(
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
index={index}
|
||||
allowedTypes={['query', 'expression']}
|
||||
show={hovered === item.id}
|
||||
show={!isDragging && hovered === item.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -379,15 +383,15 @@ export const QueryTransformList = memo(
|
||||
</Stack>
|
||||
|
||||
<div className={cx(styles.cardContainer, styles.cardContainerLast)}>
|
||||
<div className={styles.floatingButton}>
|
||||
<div className={styles.addButtonFloating}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
allowedTypes={['query', 'expression']}
|
||||
index={transformItems.length}
|
||||
show={hovered === 'queries-last'}
|
||||
index={dataSourceItems.length}
|
||||
show={!isDragging && hovered === 'queries-last'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -395,15 +399,24 @@ export const QueryTransformList = memo(
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
</Stack>
|
||||
)}
|
||||
) : (
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
allowedTypes={['query', 'expression']}
|
||||
text={t('dashboard-scene.query-transform-list.add', 'Add')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Transformations Section */}
|
||||
{transformItems.length > 0 && (
|
||||
<Stack direction="column" gap={2}>
|
||||
<div className={styles.sectionLabel}>
|
||||
{t('dashboard-scene.query-transform-list.transformations', 'Transformations')}
|
||||
</div>
|
||||
<Stack direction="column" gap={2}>
|
||||
<div className={styles.sectionLabel}>
|
||||
{t('dashboard-scene.query-transform-list.transformations', 'Transformations')}
|
||||
</div>
|
||||
{transformItems.length > 0 ? (
|
||||
<Droppable droppableId="transformations">
|
||||
{(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')}
|
||||
>
|
||||
<Stack direction="column" gap={2}>
|
||||
{transformItems.map((item, index) => (
|
||||
@@ -462,7 +460,7 @@ export const QueryTransformList = memo(
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
<div className={styles.floatingButton}>
|
||||
<div className={styles.addButtonFloating}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddTransform={onAddTransform}
|
||||
@@ -470,7 +468,7 @@ export const QueryTransformList = memo(
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
index={index}
|
||||
allowedTypes={['transform']}
|
||||
show={hovered === item.id}
|
||||
show={!isDragging && hovered === item.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -478,7 +476,7 @@ export const QueryTransformList = memo(
|
||||
{provided.placeholder}
|
||||
</Stack>
|
||||
<div className={cx(styles.cardContainer, styles.cardContainerLast)}>
|
||||
<div className={styles.floatingButton}>
|
||||
<div className={styles.addButtonFloating}>
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
@@ -486,7 +484,7 @@ export const QueryTransformList = memo(
|
||||
onAddExpression={onAddExpression}
|
||||
allowedTypes={['transform']}
|
||||
index={transformItems.length}
|
||||
show={hovered === 'transformations-last'}
|
||||
show={!isDragging && hovered === 'transformations-last'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,8 +492,17 @@ export const QueryTransformList = memo(
|
||||
);
|
||||
}}
|
||||
</Droppable>
|
||||
</Stack>
|
||||
)}
|
||||
) : (
|
||||
<AddDataItemMenu
|
||||
onAddQuery={onAddQuery}
|
||||
onAddFromSavedQueries={onAddFromSavedQueries}
|
||||
onAddTransform={onAddTransform}
|
||||
onAddExpression={onAddExpression}
|
||||
allowedTypes={['transform']}
|
||||
text={t('dashboard-scene.query-transform-list.add', 'Add')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</DragDropContext>
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user