Commit 8759a912 by Torkel Ödegaard Committed by GitHub

PanelEdit: Drag and drop query order & UI tweaks (#27502)

* PanelEdit: Drag and drop query order & UI tweaks

* Fixed width of title issue

* added correct color and hover

* Updated e2e tests
parent 0c8390ce
...@@ -75,44 +75,6 @@ e2e.scenario({ ...@@ -75,44 +75,6 @@ e2e.scenario({
e2e().wait('@apiPostQuery'); e2e().wait('@apiPostQuery');
// Change order or query rows
// Check the order of the rows before
e2e.components.QueryEditorRows.rows()
.eq(0)
.within(() => {
e2e.components.QueryEditorRow.title('B').should('be.visible');
});
e2e.components.QueryEditorRows.rows()
.eq(1)
.within(() => {
e2e.components.QueryEditorRow.title('A').should('be.visible');
});
// Change so A is first
e2e.components.QueryEditorRow.actionButton('Move query up')
.eq(1)
.click();
e2e().wait('@apiPostQuery');
// Avoid flaky tests
// Maybe the virtual dom performs optimzations such as node position swapping, meaning 1 becomes 0 and it gets that element before the change because and never finds title 'A'
e2e().wait(250);
// Check the order of the rows after change
e2e.components.QueryEditorRows.rows()
.eq(0)
.within(() => {
e2e.components.QueryEditorRow.title('A').should('be.visible');
});
e2e.components.QueryEditorRows.rows()
.eq(1)
.within(() => {
e2e.components.QueryEditorRow.title('B').should('be.visible');
});
// Disable / enable row // Disable / enable row
expectInspectorResultAndClose(keys => { expectInspectorResultAndClose(keys => {
const length = keys.length; const length = keys.length;
...@@ -120,7 +82,7 @@ e2e.scenario({ ...@@ -120,7 +82,7 @@ e2e.scenario({
expect(keys[length - 1].innerText).equals('B:'); expect(keys[length - 1].innerText).equals('B:');
}); });
// Disable row with refId B // Disable row with refId A
e2e.components.QueryEditorRow.actionButton('Disable/enable query') e2e.components.QueryEditorRow.actionButton('Disable/enable query')
.eq(1) .eq(1)
.should('be.visible') .should('be.visible')
...@@ -130,7 +92,7 @@ e2e.scenario({ ...@@ -130,7 +92,7 @@ e2e.scenario({
expectInspectorResultAndClose(keys => { expectInspectorResultAndClose(keys => {
const length = keys.length; const length = keys.length;
expect(keys[length - 1].innerText).equals('A:'); expect(keys[length - 1].innerText).equals('B:');
}); });
// Enable row with refId B // Enable row with refId B
......
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { HorizontalGroup, Icon, renderOrCallToRender, stylesFactory, useTheme } from '@grafana/ui'; import { Icon, renderOrCallToRender, stylesFactory, useTheme } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion'; import { css } from 'emotion';
import { useUpdateEffect } from 'react-use'; import { useUpdateEffect } from 'react-use';
...@@ -66,33 +66,36 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({ ...@@ -66,33 +66,36 @@ export const QueryOperationRow: React.FC<QueryOperationRowProps> = ({
const rowHeader = ( const rowHeader = (
<div className={styles.header}> <div className={styles.header}>
<HorizontalGroup justify="space-between"> <div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title">
<div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title"> <Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} />
{draggable && ( {title && <span className={styles.title}>{titleElement}</span>}
<Icon title="Drag and drop to reorder" name="draggabledots" size="lg" className={styles.dragIcon} /> {headerElement}
)} </div>
<Icon name={isContentVisible ? 'angle-down' : 'angle-right'} className={styles.collapseIcon} /> {actions && actionsElement}
{title && <span className={styles.title}>{titleElement}</span>} {draggable && (
{headerElement} <Icon title="Drag and drop to reorder" name="draggabledots" size="lg" className={styles.dragIcon} />
</div> )}
{actions && actionsElement}
</HorizontalGroup>
</div> </div>
); );
return draggable ? (
<Draggable draggableId={id} index={index}> if (draggable) {
{provided => { return (
return ( <Draggable draggableId={id} index={index}>
<> {provided => {
<div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}> return (
<div {...provided.dragHandleProps}>{rowHeader}</div> <>
{isContentVisible && <div className={styles.content}>{children}</div>} <div ref={provided.innerRef} className={styles.wrapper} {...provided.draggableProps}>
</div> <div {...provided.dragHandleProps}>{rowHeader}</div>
</> {isContentVisible && <div className={styles.content}>{children}</div>}
); </div>
}} </>
</Draggable> );
) : ( }}
</Draggable>
);
}
return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{rowHeader} {rowHeader}
{isContentVisible && <div className={styles.content}>{children}</div>} {isContentVisible && <div className={styles.content}>{children}</div>}
...@@ -116,8 +119,11 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -116,8 +119,11 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
justify-content: space-between; justify-content: space-between;
`, `,
dragIcon: css` dragIcon: css`
opacity: 0.4;
cursor: drag; cursor: drag;
color: ${theme.colors.textWeak};
&:hover {
color: ${theme.colors.text};
}
`, `,
collapseIcon: css` collapseIcon: css`
color: ${theme.colors.textWeak}; color: ${theme.colors.textWeak};
...@@ -128,7 +134,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -128,7 +134,10 @@ const getQueryOperationRowStyles = stylesFactory((theme: GrafanaTheme) => {
titleWrapper: css` titleWrapper: css`
display: flex; display: flex;
align-items: center; align-items: center;
flex-grow: 1;
cursor: pointer; cursor: pointer;
overflow: hidden;
margin-right: ${theme.spacing.sm};
`, `,
title: css` title: css`
font-weight: ${theme.typography.weight.semibold}; font-weight: ${theme.typography.weight.semibold};
......
...@@ -27,7 +27,7 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp ...@@ -27,7 +27,7 @@ export const TransformationOperationRow: React.FC<TransformationOperationRowProp
const renderActions = ({ isOpen }: { isOpen: boolean }) => { const renderActions = ({ isOpen }: { isOpen: boolean }) => {
return ( return (
<HorizontalGroup align="center"> <HorizontalGroup align="center" width="auto">
<QueryOperationAction <QueryOperationAction
title="Debug" title="Debug"
disabled={!isOpen} disabled={!isOpen}
......
...@@ -37,7 +37,6 @@ interface Props { ...@@ -37,7 +37,6 @@ interface Props {
index: number; index: number;
onAddQuery: (query?: DataQuery) => void; onAddQuery: (query?: DataQuery) => void;
onRemoveQuery: (query: DataQuery) => void; onRemoveQuery: (query: DataQuery) => void;
onMoveQuery: (query: DataQuery, direction: number) => void;
onChange: (query: DataQuery) => void; onChange: (query: DataQuery) => void;
} }
...@@ -232,7 +231,7 @@ export class QueryEditorRow extends PureComponent<Props, State> { ...@@ -232,7 +231,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
const isDisabled = query.hide; const isDisabled = query.hide;
return ( return (
<HorizontalGroup> <HorizontalGroup width="auto">
{hasTextEditMode && ( {hasTextEditMode && (
<QueryOperationAction <QueryOperationAction
title="Toggle text edit mode" title="Toggle text edit mode"
...@@ -242,13 +241,6 @@ export class QueryEditorRow extends PureComponent<Props, State> { ...@@ -242,13 +241,6 @@ export class QueryEditorRow extends PureComponent<Props, State> {
}} }}
/> />
)} )}
<QueryOperationAction
title="Move query down"
icon="arrow-down"
onClick={() => this.props.onMoveQuery(query, 1)}
/>
<QueryOperationAction title="Move query up" icon="arrow-up" onClick={() => this.props.onMoveQuery(query, -1)} />
<QueryOperationAction title="Duplicate query" icon="copy" onClick={this.onCopyQuery} /> <QueryOperationAction title="Duplicate query" icon="copy" onClick={this.onCopyQuery} />
<QueryOperationAction <QueryOperationAction
title="Disable/enable query" title="Disable/enable query"
...@@ -297,6 +289,7 @@ export class QueryEditorRow extends PureComponent<Props, State> { ...@@ -297,6 +289,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
<div aria-label={selectors.components.QueryEditorRows.rows}> <div aria-label={selectors.components.QueryEditorRows.rows}>
<QueryOperationRow <QueryOperationRow
id={id} id={id}
draggable={true}
index={index} index={index}
title={this.renderTitle} title={this.renderTitle}
actions={this.renderActions} actions={this.renderActions}
......
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
// @ts-ignore ignoring this for now, otherwise we would have to extend _ interface with move
import _ from 'lodash';
// Types // Types
import { PanelModel } from '../state/PanelModel'; import { PanelModel } from '../state/PanelModel';
import { DataQuery, PanelData, DataSourceSelectItem } from '@grafana/data'; import { DataQuery, PanelData, DataSourceSelectItem } from '@grafana/data';
import { DashboardModel } from '../state/DashboardModel'; import { DashboardModel } from '../state/DashboardModel';
import { QueryEditorRow } from './QueryEditorRow'; import { QueryEditorRow } from './QueryEditorRow';
import { addQuery } from 'app/core/utils/query'; import { addQuery } from 'app/core/utils/query';
import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
interface Props { interface Props {
// The query configuration // The query configuration
...@@ -44,16 +42,6 @@ export class QueryEditorRows extends PureComponent<Props> { ...@@ -44,16 +42,6 @@ export class QueryEditorRows extends PureComponent<Props> {
panel.refresh(); panel.refresh();
}; };
onMoveQuery = (query: DataQuery, direction: number) => {
const { queries, onChangeQueries, panel } = this.props;
const index = _.indexOf(queries, query);
// @ts-ignore
_.move(queries, index, index + direction);
onChangeQueries(queries);
panel.refresh();
};
onChangeQuery(query: DataQuery, index: number) { onChangeQuery(query: DataQuery, index: number) {
const { queries, onChangeQueries } = this.props; const { queries, onChangeQueries } = this.props;
...@@ -76,24 +64,55 @@ export class QueryEditorRows extends PureComponent<Props> { ...@@ -76,24 +64,55 @@ export class QueryEditorRows extends PureComponent<Props> {
); );
} }
onDragEnd = (result: DropResult) => {
const { queries, onChangeQueries, panel } = this.props;
if (!result || !result.destination) {
return;
}
const startIndex = result.source.index;
const endIndex = result.destination.index;
if (startIndex === endIndex) {
return;
}
const update = Array.from(queries);
const [removed] = update.splice(startIndex, 1);
update.splice(endIndex, 0, removed);
onChangeQueries(update);
panel.refresh();
};
render() { render() {
const { props } = this; const { props } = this;
return props.queries.map((query, index) => ( return (
<QueryEditorRow <DragDropContext onDragEnd={this.onDragEnd}>
dataSourceValue={query.datasource || props.datasource.value} <Droppable droppableId="transformations-list" direction="vertical">
id={query.refId} {provided => {
index={index} return (
key={query.refId} <div ref={provided.innerRef} {...provided.droppableProps}>
panel={props.panel} {props.queries.map((query, index) => (
dashboard={props.dashboard} <QueryEditorRow
data={props.data} dataSourceValue={query.datasource || props.datasource.value}
query={query} id={query.refId}
onChange={query => this.onChangeQuery(query, index)} index={index}
onRemoveQuery={this.onRemoveQuery} key={query.refId}
onAddQuery={this.onAddQuery} panel={props.panel}
onMoveQuery={this.onMoveQuery} dashboard={props.dashboard}
inMixedMode={props.datasource.meta.mixed} data={props.data}
/> query={query}
)); onChange={query => this.onChangeQuery(query, index)}
onRemoveQuery={this.onRemoveQuery}
onAddQuery={this.onAddQuery}
inMixedMode={props.datasource.meta.mixed}
/>
))}
</div>
);
}}
</Droppable>
</DragDropContext>
);
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment