mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 15:18:01 +08:00
New panel edit: data links tweaks (#22304)
* Add basic layout components * Add some love to data links in new panel edit * fix prettier
This commit is contained in:
@ -0,0 +1,51 @@
|
||||
import { DataFrame, DataLink, VariableSuggestion } from '@grafana/data';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { DataLinkEditor } from '../DataLinkEditor';
|
||||
import { HorizontalGroup } from '../../Layout/Layout';
|
||||
import Forms from '../../Forms';
|
||||
|
||||
interface DataLinkEditorModalContentProps {
|
||||
link: DataLink;
|
||||
index: number;
|
||||
data: DataFrame[];
|
||||
suggestions: VariableSuggestion[];
|
||||
onChange: (index: number, ink: DataLink) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = ({
|
||||
link,
|
||||
index,
|
||||
suggestions,
|
||||
onChange,
|
||||
onClose,
|
||||
}) => {
|
||||
const [dirtyLink, setDirtyLink] = useState(link);
|
||||
return (
|
||||
<>
|
||||
<DataLinkEditor
|
||||
value={dirtyLink}
|
||||
index={index}
|
||||
isLast={false}
|
||||
suggestions={suggestions}
|
||||
onChange={(index, link) => {
|
||||
setDirtyLink(link);
|
||||
}}
|
||||
onRemove={() => {}}
|
||||
/>
|
||||
<HorizontalGroup>
|
||||
<Forms.Button
|
||||
onClick={() => {
|
||||
onChange(index, dirtyLink);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Forms.Button>
|
||||
<Forms.Button variant="secondary" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Forms.Button>
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,134 @@
|
||||
import { DataFrame, DataLink, GrafanaTheme, VariableSuggestion } from '@grafana/data';
|
||||
import React, { useState } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import Forms from '../../Forms';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { Modal } from '../../Modal/Modal';
|
||||
import { FullWidthButtonContainer } from '../../Button/FullWidthButtonContainer';
|
||||
import { selectThemeVariant, stylesFactory, useTheme } from '../../../themes';
|
||||
import { DataLinksListItem } from './DataLinksListItem';
|
||||
import { DataLinkEditorModalContent } from './DataLinkEditorModalContent';
|
||||
|
||||
interface DataLinksInlineEditorProps {
|
||||
links: DataLink[];
|
||||
onChange: (links: DataLink[]) => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
data: DataFrame[];
|
||||
}
|
||||
|
||||
export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ links, onChange, suggestions, data }) => {
|
||||
const theme = useTheme();
|
||||
const [editIndex, setEditIndex] = useState();
|
||||
const isEditing = editIndex !== null && editIndex !== undefined;
|
||||
const styles = getDataLinksInlineEditorStyles(theme);
|
||||
|
||||
const onDataLinkChange = (index: number, link: DataLink) => {
|
||||
const update = cloneDeep(links);
|
||||
update[index] = link;
|
||||
onChange(update);
|
||||
};
|
||||
|
||||
const onDataLinkAdd = () => {
|
||||
let update = cloneDeep(links);
|
||||
if (update) {
|
||||
update.push({
|
||||
title: '',
|
||||
url: '',
|
||||
});
|
||||
} else {
|
||||
update = [
|
||||
{
|
||||
title: '',
|
||||
url: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
setEditIndex(update.length - 1);
|
||||
onChange(update);
|
||||
};
|
||||
|
||||
const onDataLinkRemove = (index: number) => {
|
||||
const update = cloneDeep(links);
|
||||
update.splice(index, 1);
|
||||
onChange(update);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{links && (
|
||||
<div className={styles.wrapper}>
|
||||
{links.map((l, i) => {
|
||||
return (
|
||||
<DataLinksListItem
|
||||
key={`${l.title}/${i}`}
|
||||
index={i}
|
||||
link={l}
|
||||
onChange={onDataLinkChange}
|
||||
onEdit={() => setEditIndex(i)}
|
||||
onRemove={() => onDataLinkRemove(i)}
|
||||
data={data}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<Modal
|
||||
title="Edit data link"
|
||||
isOpen={isEditing}
|
||||
onDismiss={() => {
|
||||
setEditIndex(null);
|
||||
}}
|
||||
>
|
||||
<DataLinkEditorModalContent
|
||||
index={editIndex}
|
||||
link={links[editIndex]}
|
||||
data={data}
|
||||
onChange={onDataLinkChange}
|
||||
onClose={() => setEditIndex(null)}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<FullWidthButtonContainer>
|
||||
<Forms.Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
|
||||
Add data link
|
||||
</Forms.Button>
|
||||
</FullWidthButtonContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDataLinksInlineEditorStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.gray85,
|
||||
dark: theme.colors.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
const shadow = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.gray85,
|
||||
dark: theme.colors.black,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border: 1px dashed ${borderColor};
|
||||
margin-bottom: ${theme.spacing.md};
|
||||
transition: box-shadow 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px ${shadow};
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
@ -0,0 +1,98 @@
|
||||
import React, { FC } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { DataFrame, DataLink, GrafanaTheme, VariableSuggestion } from '@grafana/data';
|
||||
import { selectThemeVariant, stylesFactory, useTheme } from '../../../themes';
|
||||
import { HorizontalGroup } from '../../Layout/Layout';
|
||||
import { Icon } from '../../Icon/Icon';
|
||||
|
||||
interface DataLinksListItemProps {
|
||||
index: number;
|
||||
link: DataLink;
|
||||
data: DataFrame[];
|
||||
onChange: (index: number, link: DataLink) => void;
|
||||
onEdit: () => void;
|
||||
onRemove: () => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
isEditing?: boolean;
|
||||
}
|
||||
|
||||
export const DataLinksListItem: FC<DataLinksListItemProps> = ({
|
||||
index,
|
||||
link,
|
||||
data,
|
||||
onChange,
|
||||
suggestions,
|
||||
isEditing,
|
||||
onEdit,
|
||||
onRemove,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const styles = getDataLinkListItemStyles(theme);
|
||||
|
||||
const hasTitle = link.title.trim() !== '';
|
||||
const hasUrl = link.url.trim() !== '';
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div>
|
||||
<div className={cx(!hasTitle && styles.notConfigured)}>{hasTitle ? link.title : 'No data link provided'}</div>
|
||||
<div className={cx(!hasUrl && styles.notConfigured, styles.url)}>{hasUrl ? link.url : 'No url provided'}</div>
|
||||
</div>
|
||||
|
||||
<HorizontalGroup>
|
||||
<div onClick={onEdit} className={styles.action}>
|
||||
<Icon name="pencil" />
|
||||
</div>
|
||||
<div onClick={onRemove} className={cx(styles.action, styles.remove)}>
|
||||
<Icon name="trash" />
|
||||
</div>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getDataLinkListItemStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const borderColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.gray85,
|
||||
dark: theme.colors.dark9,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
const bg = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.white,
|
||||
dark: theme.colors.dark1,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
border-bottom: 1px dashed ${borderColor};
|
||||
padding: ${theme.spacing.sm};
|
||||
transition: background 0.5s cubic-bezier(0.19, 1, 0.22, 1);
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: ${bg};
|
||||
}
|
||||
`,
|
||||
action: css`
|
||||
cursor: pointer;
|
||||
`,
|
||||
|
||||
notConfigured: css`
|
||||
font-style: italic;
|
||||
`,
|
||||
url: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
`,
|
||||
remove: css`
|
||||
color: ${theme.colors.red88};
|
||||
`,
|
||||
};
|
||||
});
|
@ -1,18 +1,6 @@
|
||||
import {
|
||||
FieldOverrideContext,
|
||||
FieldConfigEditorProps,
|
||||
DataLink,
|
||||
FieldOverrideEditorProps,
|
||||
DataFrame,
|
||||
} from '@grafana/data';
|
||||
import React, { FC, useState } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import Forms from '../Forms';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
import { DataLinkEditor } from '../DataLinks/DataLinkEditor';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { VariableSuggestion } from '@grafana/data';
|
||||
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
|
||||
import { FieldOverrideContext, FieldConfigEditorProps, DataLink, FieldOverrideEditorProps } from '@grafana/data';
|
||||
import React from 'react';
|
||||
import { DataLinksInlineEditor } from '../DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
|
||||
|
||||
export interface DataLinksFieldConfigSettings {}
|
||||
|
||||
@ -29,45 +17,13 @@ export const DataLinksValueEditor: React.FC<FieldConfigEditorProps<DataLink[], D
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
const onDataLinkChange = (index: number, link: DataLink) => {
|
||||
const links = cloneDeep(value);
|
||||
links[index] = link;
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
const onDataLinkAdd = () => {
|
||||
const links = cloneDeep(value) || [];
|
||||
|
||||
links.push({
|
||||
title: '',
|
||||
url: '',
|
||||
});
|
||||
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value &&
|
||||
value.map((l, i) => {
|
||||
return (
|
||||
<DataLinksListItem
|
||||
key={`${l.title}/${i}`}
|
||||
index={i}
|
||||
link={l}
|
||||
onChange={onDataLinkChange}
|
||||
data={context.data}
|
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<FullWidthButtonContainer>
|
||||
<Forms.Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
|
||||
Add data link
|
||||
</Forms.Button>
|
||||
</FullWidthButtonContainer>
|
||||
</>
|
||||
<DataLinksInlineEditor
|
||||
links={value}
|
||||
onChange={onChange}
|
||||
data={context.data}
|
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -75,156 +31,13 @@ export const DataLinksOverrideEditor: React.FC<FieldOverrideEditorProps<DataLink
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
item,
|
||||
}) => {
|
||||
const onDataLinkChange = (index: number, link: DataLink) => {
|
||||
const links = cloneDeep(value);
|
||||
links[index] = link;
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
const onDataLinkAdd = () => {
|
||||
let links = cloneDeep(value);
|
||||
if (links) {
|
||||
links.push({
|
||||
title: '',
|
||||
url: '',
|
||||
});
|
||||
} else {
|
||||
links = [
|
||||
{
|
||||
title: '',
|
||||
url: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
onChange(links);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{value &&
|
||||
value.map((l, i) => {
|
||||
return (
|
||||
<DataLinksListItem
|
||||
key={`${l.title}/${i}`}
|
||||
index={i}
|
||||
link={l}
|
||||
onChange={onDataLinkChange}
|
||||
data={context.data}
|
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<Forms.Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
|
||||
Create data link
|
||||
</Forms.Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DataLinksListItemProps {
|
||||
index: number;
|
||||
link: DataLink;
|
||||
data: DataFrame[];
|
||||
onChange: (index: number, link: DataLink) => void;
|
||||
suggestions: VariableSuggestion[];
|
||||
}
|
||||
|
||||
const DataLinksListItem: FC<DataLinksListItemProps> = ({ index, link, data, onChange, suggestions }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const style = () => {
|
||||
return {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`,
|
||||
action: css`
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
`,
|
||||
noTitle: css`
|
||||
font-style: italic;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
const styles = style();
|
||||
const hasTitle = link.title.trim() !== '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={cx(!hasTitle && styles.noTitle)}>{hasTitle ? link.title : 'Edit data link'}</div>
|
||||
<div>
|
||||
<Forms.Button size="sm" icon="fa fa-pencil" variant="link" onClick={() => setIsEditing(true)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isEditing && (
|
||||
<Modal
|
||||
title="Edit data link"
|
||||
isOpen={isEditing}
|
||||
onDismiss={() => {
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
<DataLinkEditorModalContent
|
||||
index={index}
|
||||
link={link}
|
||||
data={data}
|
||||
onChange={onChange}
|
||||
onClose={() => setIsEditing(false)}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface DataLinkEditorModalContentProps {
|
||||
link: DataLink;
|
||||
index: number;
|
||||
data: DataFrame[];
|
||||
suggestions: VariableSuggestion[];
|
||||
onChange: (index: number, ink: DataLink) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = ({
|
||||
link,
|
||||
index,
|
||||
data,
|
||||
suggestions,
|
||||
onChange,
|
||||
onClose,
|
||||
}) => {
|
||||
const [dirtyLink, setDirtyLink] = useState(link);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataLinkEditor
|
||||
value={dirtyLink}
|
||||
index={index}
|
||||
isLast={false}
|
||||
suggestions={suggestions}
|
||||
onChange={(index, link) => {
|
||||
setDirtyLink(link);
|
||||
}}
|
||||
onRemove={() => {}}
|
||||
/>
|
||||
<Forms.Button
|
||||
onClick={() => {
|
||||
onChange(index, dirtyLink);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Forms.Button>
|
||||
<Forms.Button onClick={() => onClose()}>Cancel</Forms.Button>
|
||||
</>
|
||||
<DataLinksInlineEditor
|
||||
links={value}
|
||||
onChange={onChange}
|
||||
data={context.data}
|
||||
suggestions={context.getSuggestions ? context.getSuggestions() : []}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { css } from 'emotion';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory, useTheme } from '../../themes';
|
||||
|
||||
enum Orientation {
|
||||
Horizontal,
|
||||
|
Reference in New Issue
Block a user