mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
8072286daf
commit
b189743ca0
@ -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),
|
||||
}),
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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.",
|
||||
|
@ -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ģ ęľşę įş şŧäŧęđ.",
|
||||
|
Loading…
Reference in New Issue
Block a user