Transformations: enable transformations reordering (#27197)

* Transformations: enable queries reorder by drag and drop

* Satisfy ts

* Update unicons and replace ellipsis with draggabledot

* remove import

* Remove that snap

* Review

* review 2
This commit is contained in:
Dominik Prokop
2020-08-31 08:47:27 +02:00
committed by GitHub
parent 1a69bcfeff
commit 6dbb803b3f
18 changed files with 338 additions and 161 deletions

View File

@@ -7,7 +7,7 @@ describe('QueryOperationRow', () => {
it('renders', () => {
expect(() =>
shallow(
<QueryOperationRow>
<QueryOperationRow id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
)
@@ -20,7 +20,7 @@ describe('QueryOperationRow', () => {
// @ts-ignore strict null error, you shouldn't use promise like approach with act but I don't know what the intention is here
await act(async () => {
shallow(
<QueryOperationRow onOpen={onOpenSpy}>
<QueryOperationRow onOpen={onOpenSpy} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -32,7 +32,7 @@ describe('QueryOperationRow', () => {
const onOpenSpy = jest.fn();
const onCloseSpy = jest.fn();
const wrapper = mount(
<QueryOperationRow onOpen={onOpenSpy} onClose={onCloseSpy} isOpen={false}>
<QueryOperationRow onOpen={onOpenSpy} onClose={onCloseSpy} isOpen={false} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -60,7 +60,7 @@ describe('QueryOperationRow', () => {
it('should render title provided as element', () => {
const title = <div aria-label="test title">Test</div>;
const wrapper = shallow(
<QueryOperationRow title={title}>
<QueryOperationRow title={title} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -71,7 +71,7 @@ describe('QueryOperationRow', () => {
it('should render title provided as function', () => {
const title = () => <div aria-label="test title">Test</div>;
const wrapper = shallow(
<QueryOperationRow title={title}>
<QueryOperationRow title={title} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -87,7 +87,7 @@ describe('QueryOperationRow', () => {
return <div aria-label="test title">Test</div>;
};
shallow(
<QueryOperationRow title={title}>
<QueryOperationRow title={title} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -100,7 +100,7 @@ describe('QueryOperationRow', () => {
it('should render actions provided as element', () => {
const actions = <div aria-label="test actions">Test</div>;
const wrapper = shallow(
<QueryOperationRow actions={actions}>
<QueryOperationRow actions={actions} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -111,7 +111,7 @@ describe('QueryOperationRow', () => {
it('should render actions provided as function', () => {
const actions = () => <div aria-label="test actions">Test</div>;
const wrapper = shallow(
<QueryOperationRow actions={actions}>
<QueryOperationRow actions={actions} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);
@@ -127,7 +127,7 @@ describe('QueryOperationRow', () => {
return <div aria-label="test actions">Test</div>;
};
shallow(
<QueryOperationRow actions={actions}>
<QueryOperationRow actions={actions} id="test-id" index={0}>
<div>Test</div>
</QueryOperationRow>
);

View File

@@ -1,10 +1,13 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import { HorizontalGroup, Icon, renderOrCallToRender, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
import { useUpdateEffect } from 'react-use';
import { Draggable } from 'react-beautiful-dnd';
interface QueryOperationRowProps {
index: number;
id: string;
title?: ((props: { isOpen: boolean }) => React.ReactNode) | React.ReactNode;
headerElement?: React.ReactNode;
actions?:
@@ -14,6 +17,7 @@ interface QueryOperationRowProps {
onClose?: () => void;
children: React.ReactNode;
isOpen?: boolean;
draggable?: boolean;
}
export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
@@ -24,10 +28,16 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
onClose,
onOpen,
isOpen,
draggable,
index,
id,
}: QueryOperationRowProps) => {
const [isContentVisible, setIsContentVisible] = useState(isOpen !== undefined ? isOpen : true);
const theme = useTheme();
const styles = getQueryOperationRowStyles(theme);
const onRowToggle = useCallback(() => {
setIsContentVisible(!isContentVisible);
}, [isContentVisible, setIsContentVisible]);
useUpdateEffect(() => {
if (isContentVisible) {
@@ -54,24 +64,37 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
},
});
return (
const rowHeader = (
<div className={styles.header}>
<HorizontalGroup justify="space-between">
<div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title">
{draggable && (
<Icon title="Drag and drop to reorder" name="draggabledots" size="lg" className={styles.dragIcon} />
)}
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} />
{title && <span className={styles.title}>{titleElement}</span>}
{headerElement}
</div>
{actions && actionsElement}
</HorizontalGroup>
</div>
);
return draggable ? (
<Draggable draggableId={id} index={index}>
{provided => {
return (
<>
<div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}>
<div {...provided.dragHandleProps}>{rowHeader}</div>
{isContentVisible && <div className={styles.content}>{children}</div>}
</div>
</>
);
}}
</Draggable>
) : (
<div className={styles.wrapper}>
<div className={styles.header}>
<HorizontalGroup justify="space-between">
<div
className={styles.titleWrapper}
onClick={() => {
setIsContentVisible(!isContentVisible);
}}
aria-label="Query operation row title"
>
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} />
{title && <span className={styles.title}>{titleElement}</span>}
{headerElement}
</div>
{actions && actionsElement}
</HorizontalGroup>
</div>
{rowHeader}
{isContentVisible && <div className={styles.content}>{children}</div>}
</div>
);
@@ -92,6 +115,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
align-items: center;
justify-content: space-between;
`,
dragIcon: css`
opacity: 0.4;
cursor: drag;
`,
collapseIcon: css`
color: ${theme.colors.textWeak};
&:hover {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useMemo } from 'react';
import { css, cx } from 'emotion';
import { css } from 'emotion';
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
import {
DataFrame,
@@ -10,7 +10,7 @@ import {
TransformerUIProps,
getFieldDisplayName,
} from '@grafana/data';
import { stylesFactory, useTheme, Input, IconButton } from '@grafana/ui';
import { stylesFactory, useTheme, Input, IconButton, Icon } from '@grafana/ui';
import { OrganizeFieldsTransformerOptions } from '@grafana/data/src/transformations/transformers/organize';
import { createOrderFieldsComparer } from '@grafana/data/src/transformations/transformers/order';
@@ -135,7 +135,7 @@ const DraggableFieldName: React.FC<DraggableFieldProps> = ({
>
<div className="gf-form gf-form--grow">
<div className="gf-form-label gf-form-label--justify-left width-30">
<i className={cx('fa fa-ellipsis-v', styles.draggable)} />
<Icon name="draggabledots" title="Drag and drop to reorder" size="lg" className={styles.draggable} />
<IconButton
className={styles.toggle}
size="md"
@@ -168,8 +168,6 @@ const getFieldNameStyles = stylesFactory((theme: GrafanaTheme) => ({
color: ${theme.colors.textWeak};
`,
draggable: css`
padding: 0 ${theme.spacing.xs};
font-size: ${theme.typography.size.md};
opacity: 0.4;
&:hover {
color: ${theme.colors.textStrong};