Actions: Improve drag and drop experience (#94263)

* feat(actions): improve dnd ux

---------

Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
This commit is contained in:
Ihor Yeromin 2024-10-23 09:24:06 +02:00 committed by GitHub
parent 8072286daf
commit b189743ca0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 103 additions and 128 deletions

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
import { cloneDeep } from 'lodash';
import { ReactNode, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { DataFrame, DataLink, GrafanaTheme2, VariableSuggestion } from '@grafana/data';
@ -91,49 +91,40 @@ export const DataLinksInlineEditor = ({
onChange(update);
};
const renderFirstLink = (linkJSX: ReactNode, key: string) => {
if (showOneClick) {
return (
<div className={styles.oneClickOverlay} key={key}>
return (
<div className={styles.container}>
{/* one-link placeholder */}
{showOneClick && linksSafe.length > 0 && (
<div className={styles.oneClickOverlay}>
<span className={styles.oneClickSpan}>
<Trans i18nKey="grafana-ui.data-links-inline-editor.one-click-link">One-click link</Trans>
</span>
{linkJSX}
</div>
);
}
return linkJSX;
};
)}
return (
<>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sortable-links" direction="vertical">
{(provided) => (
<div className={styles.wrapper} ref={provided.innerRef} {...provided.droppableProps}>
<div
className={styles.wrapper}
ref={provided.innerRef}
{...provided.droppableProps}
style={{ paddingTop: showOneClick && linksSafe.length > 0 ? '28px' : '0px' }}
>
{linksSafe.map((link, idx) => {
const key = `${link.title}/${idx}`;
const linkJSX = (
<div className={styles.itemWrapper} key={key}>
<DataLinksListItem
key={key}
index={idx}
link={link}
onChange={onDataLinkChange}
onEdit={() => setEditIndex(idx)}
onRemove={() => onDataLinkRemove(idx)}
data={data}
itemKey={key}
/>
</div>
return (
<DataLinksListItem
key={key}
index={idx}
link={link}
onChange={onDataLinkChange}
onEdit={() => setEditIndex(idx)}
onRemove={() => onDataLinkRemove(idx)}
data={data}
itemKey={key}
/>
);
if (idx === 0) {
return renderFirstLink(linkJSX, key);
}
return linkJSX;
})}
{provided.placeholder}
</div>
@ -164,22 +155,27 @@ export const DataLinksInlineEditor = ({
<Button size="sm" icon="plus" onClick={onDataLinkAdd} variant="secondary" className={styles.button}>
<Trans i18nKey="grafana-ui.data-links-inline-editor.add-link">Add link</Trans>
</Button>
</>
</div>
);
};
const getDataLinksInlineEditorStyles = (theme: GrafanaTheme2) => ({
container: css({
position: 'relative',
}),
wrapper: css({
marginBottom: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
}),
oneClickOverlay: css({
height: 'auto',
border: `2px dashed ${theme.colors.text.link}`,
fontSize: 10,
color: theme.colors.text.primary,
marginBottom: theme.spacing(1),
position: 'absolute',
width: '100%',
height: '92px',
}),
oneClickSpan: css({
padding: 10,
@ -187,9 +183,6 @@ const getDataLinksInlineEditorStyles = (theme: GrafanaTheme2) => ({
marginBottom: -10,
display: 'inline-block',
}),
itemWrapper: css({
padding: '4px 8px 8px 8px',
}),
button: css({
marginLeft: theme.spacing(1),
}),

View File

@ -5,10 +5,9 @@ import { DataFrame, DataLink, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { isCompactUrl } from '../../../utils';
import { Trans } from '../../../utils/i18n';
import { FieldValidationMessage } from '../../Forms/FieldValidationMessage';
import { Icon } from '../../Icon/Icon';
import { IconButton } from '../../IconButton/IconButton';
import { Tooltip } from '../../Tooltip/Tooltip';
export interface DataLinksListItemProps {
index: number;
@ -33,40 +32,33 @@ export const DataLinksListItem = ({ link, onEdit, onRemove, index, itemKey }: Da
return (
<Draggable key={itemKey} draggableId={itemKey} index={index}>
{(provided) => (
<>
<div
className={cx(styles.wrapper, styles.dragRow)}
ref={provided.innerRef}
{...provided.draggableProps}
key={index}
>
<div className={styles.linkDetails}>
<div className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}>
{hasTitle ? title : 'Data link title not provided'}
</div>
<div
className={cx(styles.wrapper, styles.dragRow)}
ref={provided.innerRef}
{...provided.draggableProps}
key={index}
>
<div className={styles.linkDetails}>
<div className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}>
{hasTitle ? title : 'Data link title not provided'}
</div>
<Tooltip content={'Explore data link may not work in the future. Please edit.'} show={isCompactExploreUrl}>
<div
className={cx(styles.url, !hasUrl && styles.notConfigured, isCompactExploreUrl && styles.errored)}
title={url}
>
{hasUrl ? url : 'Data link url not provided'}
</div>
{isCompactExploreUrl && (
<FieldValidationMessage>
<Trans i18nKey="grafana-ui.data-links-list-item.compact-explore-disclaimer">
Explore data link may not work in the future. Please edit.
</Trans>
</FieldValidationMessage>
)}
</div>
<div className={styles.icons}>
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit data link" />
<IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove data link" />
<div className={styles.dragIcon} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" />
</div>
</Tooltip>
</div>
<div className={styles.icons}>
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit data link" />
<IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove data link" />
<div className={styles.dragIcon} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" />
</div>
</div>
</>
</div>
)}
</Draggable>
);
@ -79,7 +71,6 @@ const getDataLinkListItemStyles = (theme: GrafanaTheme2) => {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
padding: '5px 0 5px 10px',
borderRadius: theme.shape.radius.default,
background: theme.colors.background.secondary,
@ -112,6 +103,7 @@ const getDataLinkListItemStyles = (theme: GrafanaTheme2) => {
}),
dragRow: css({
position: 'relative',
margin: '8px',
}),
icons: css({
display: 'flex',

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd';
import { cloneDeep } from 'lodash';
import { ReactNode, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { Action, DataFrame, GrafanaTheme2, defaultActionConfig, VariableSuggestion } from '@grafana/data';
import { Button } from '@grafana/ui/src/components/Button';
@ -90,46 +90,39 @@ export const ActionsInlineEditor = ({
onChange(update);
};
const renderFirstAction = (actionsJSX: ReactNode, key: string) => {
if (showOneClick) {
return (
<div className={styles.oneClickOverlay} key={key}>
<span className={styles.oneClickSpan}>One-click action</span> {actionsJSX}
</div>
);
}
return actionsJSX;
};
return (
<>
<div className={styles.container}>
{/* one-link placeholder */}
{showOneClick && actionsSafe.length > 0 && (
<div className={styles.oneClickOverlay}>
<span className={styles.oneClickSpan}>One-click link</span>
</div>
)}
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="sortable-actions" direction="vertical">
{(provided) => (
<div className={styles.wrapper} ref={provided.innerRef} {...provided.droppableProps}>
<div
className={styles.wrapper}
ref={provided.innerRef}
{...provided.droppableProps}
style={{ paddingTop: showOneClick && actionsSafe.length > 0 ? '28px' : '0px' }}
>
{actionsSafe.map((action, idx) => {
const key = `${action.title}/${idx}`;
const actionsJSX = (
<div className={styles.itemWrapper} key={key}>
<ActionListItem
key={key}
index={idx}
action={action}
onChange={onActionChange}
onEdit={() => setEditIndex(idx)}
onRemove={() => onActionRemove(idx)}
data={data}
itemKey={key}
/>
</div>
return (
<ActionListItem
key={key}
index={idx}
action={action}
onChange={onActionChange}
onEdit={() => setEditIndex(idx)}
onRemove={() => onActionRemove(idx)}
data={data}
itemKey={key}
/>
);
if (idx === 0) {
return renderFirstAction(actionsJSX, key);
}
return actionsJSX;
})}
{provided.placeholder}
</div>
@ -160,22 +153,27 @@ export const ActionsInlineEditor = ({
<Button size="sm" icon="plus" onClick={onActionAdd} variant="secondary" className={styles.button}>
Add action
</Button>
</>
</div>
);
};
const getActionsInlineEditorStyle = (theme: GrafanaTheme2) => ({
container: css({
position: 'relative',
}),
wrapper: css({
marginBottom: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
}),
oneClickOverlay: css({
height: 'auto',
border: `2px dashed ${theme.colors.text.link}`,
fontSize: 10,
color: theme.colors.text.primary,
marginBottom: theme.spacing(1),
position: 'absolute',
width: '100%',
height: '89px',
}),
oneClickSpan: css({
padding: 10,

View File

@ -26,27 +26,25 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act
return (
<Draggable key={itemKey} draggableId={itemKey} index={index}>
{(provided) => (
<>
<div
className={cx(styles.wrapper, styles.dragRow)}
ref={provided.innerRef}
{...provided.draggableProps}
key={index}
>
<div className={styles.linkDetails}>
<div className={cx(styles.url, !hasTitle && styles.notConfigured)}>
{hasTitle ? title : 'Action title not provided'}
</div>
</div>
<div className={styles.icons}>
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit action" />
<IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove action" />
<div className={styles.dragIcon} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" />
</div>
<div
className={cx(styles.wrapper, styles.dragRow)}
ref={provided.innerRef}
{...provided.draggableProps}
key={index}
>
<div className={styles.linkDetails}>
<div className={cx(styles.url, !hasTitle && styles.notConfigured)}>
{hasTitle ? title : 'Action title not provided'}
</div>
</div>
</>
<div className={styles.icons}>
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit action" />
<IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove action" />
<div className={styles.dragIcon} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" />
</div>
</div>
</div>
)}
</Draggable>
);
@ -59,7 +57,6 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
padding: '5px 0 5px 10px',
borderRadius: theme.shape.radius.default,
background: theme.colors.background.secondary,
@ -100,6 +97,7 @@ const getActionListItemStyles = (theme: GrafanaTheme2) => {
}),
dragRow: css({
position: 'relative',
margin: '8px',
}),
icons: css({
display: 'flex',

View File

@ -1084,9 +1084,6 @@
"add-link": "Add link",
"one-click-link": "One-click link"
},
"data-links-list-item": {
"compact-explore-disclaimer": "Explore data link may not work in the future. Please edit."
},
"data-source-http-settings": {
"access-help": "Help <1></1>",
"access-help-details": "Access mode controls how requests to the data source will be handled.<1> <1>Server</1></1> should be the preferred way if nothing else is stated.",

View File

@ -1084,9 +1084,6 @@
"add-link": "Åđđ ľįʼnĸ",
"one-click-link": "Øʼnę-čľįčĸ ľįʼnĸ"
},
"data-links-list-item": {
"compact-explore-disclaimer": "Ēχpľőřę đäŧä ľįʼnĸ mäy ʼnőŧ ŵőřĸ įʼn ŧĥę ƒūŧūřę. Pľęäşę ęđįŧ."
},
"data-source-http-settings": {
"access-help": "Ħęľp <1></1>",
"access-help-details": "Åččęşş mőđę čőʼnŧřőľş ĥőŵ řęqūęşŧş ŧő ŧĥę đäŧä şőūřčę ŵįľľ þę ĥäʼnđľęđ.<1> <1>Ŝęřvęř</1></1> şĥőūľđ þę ŧĥę přęƒęřřęđ ŵäy įƒ ʼnőŧĥįʼnģ ęľşę įş şŧäŧęđ.",