Commit e18e4cf0 by Tobias Skarhed Committed by GitHub

Forms migration: Data/Panel link editor (#23778)

* DataLink input to new form styles

* Make Angular work with inline editor

* Remove onRemove and desiableRemove

* Remove DataLinksEditor

* Change order of inputs

* Enable syntax highlight

* Fix datalinks for Elastic
parent 3fbdcf10
import React, { ChangeEvent, useContext } from 'react';
import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data';
import { FormField } from '../FormField/FormField';
import { Switch } from '../Forms/Legacy/Switch/Switch';
import { Switch } from '../Switch/Switch';
import { css } from 'emotion';
import { ThemeContext, stylesFactory } from '../../themes/index';
import { DataLinkInput } from './DataLinkInput';
import { Icon } from '../Icon/Icon';
import { Field } from '../Forms/Field';
import { Input } from '../Input/Input';
interface DataLinkEditorProps {
index: number;
......@@ -13,7 +13,6 @@ interface DataLinkEditorProps {
value: DataLink;
suggestions: VariableSuggestion[];
onChange: (index: number, link: DataLink, callback?: () => void) => void;
onRemove: (link: DataLink) => void;
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
......@@ -28,7 +27,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
}));
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
({ index, value, onChange, onRemove, suggestions, isLast }) => {
({ index, value, onChange, suggestions, isLast }) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
......@@ -39,39 +38,24 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
onChange(index, { ...value, title: event.target.value });
};
const onRemoveClick = () => {
onRemove(value);
};
const onOpenInNewTabChanged = () => {
onChange(index, { ...value, targetBlank: !value.targetBlank });
};
return (
<div className={styles.listItem}>
<div className="gf-form gf-form--inline">
<FormField
className="gf-form--grow"
label="Title"
value={value.title}
onChange={onTitleChange}
inputWidth={0}
labelWidth={5}
placeholder="Show details"
/>
<Switch label="Open in new tab" checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
<button className="gf-form-label gf-form-label--btn" onClick={onRemoveClick} title="Remove link">
<Icon name="times" />
</button>
</div>
<FormField
label="URL"
labelWidth={5}
inputEl={<DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />}
className={css`
width: 100%;
`}
/>
<Field label="Title">
<Input value={value.title} onChange={onTitleChange} placeholder="Show details" />
</Field>
<Field label="URL">
<DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />
</Field>
<Field label="Open in new tab">
<Switch checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
</Field>
{isLast && (
<div className={styles.infoText}>
With data links you can reference data variables like series name, labels and values. Type CMD+Space,
......
......@@ -3,13 +3,15 @@ import usePrevious from 'react-use/lib/usePrevious';
import { DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, makeValue } from '../../index';
import { SelectionReference } from './SelectionReference';
import { Portal } from '../index';
import { Portal, getFormStyles } from '../index';
// @ts-ignore
import Prism from 'prismjs';
import { Editor } from '@grafana/slate-react';
import { Value } from 'slate';
import Plain from 'slate-plain-serializer';
import { Popper as ReactPopper } from 'react-popper';
import { css } from 'emotion';
import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate';
......@@ -33,6 +35,7 @@ const plugins = [
];
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
input: getFormStyles(theme, { variant: 'primary', size: 'md', invalid: false }).input.input,
editor: css`
.token.builtInVariable {
color: ${theme.palette.queryGreen};
......@@ -41,12 +44,31 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
color: ${theme.colors.textBlue};
}
`,
// Wrapper with child selector needed.
// When classnames are appplied to the same element as the wrapper, it causes the suggestions to stop working
wrapperOverrides: css`
width: 100%;
> .slate-query-field__wrapper {
padding: 0;
background-color: transparent;
border: none;
}
`,
}));
export const enableDatalinksPrismSyntax = () => {
Prism.languages['links'] = {
builtInVariable: {
pattern: /(\${\S+?})/,
},
};
};
// This memoised also because rerendering the slate editor grabs focus which created problem in some cases this
// was used and changes to different state were propagated here.
export const DataLinkInput: React.FC<DataLinkInputProps> = memo(
({ value, onChange, suggestions, placeholder = 'http://your-grafana.com/d/000000010/annotations' }) => {
enableDatalinksPrismSyntax();
const editorRef = useRef<Editor>() as RefObject<Editor>;
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
......@@ -119,44 +141,52 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = memo(
};
return (
<div className="slate-query-field__wrapper">
<div className="slate-query-field">
{showingSuggestions && (
<Portal>
<ReactPopper
referenceElement={selectionRef}
placement="top-end"
modifiers={{
preventOverflow: { enabled: true, boundariesElement: 'window' },
arrow: { enabled: false },
offset: { offset: 250 }, // width of the suggestions menu
}}
>
{({ ref, style, placement }) => {
return (
<div ref={ref} style={style} data-placement={placement}>
<DataLinkSuggestions
suggestions={stateRef.current.suggestions}
onSuggestionSelect={onVariableSelect}
onClose={() => setShowingSuggestions(false)}
activeIndex={suggestionsIndex}
/>
</div>
);
}}
</ReactPopper>
</Portal>
)}
<Editor
schema={SCHEMA}
ref={editorRef}
placeholder={placeholder}
value={stateRef.current.linkUrl}
onChange={onUrlChange}
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
plugins={plugins}
className={styles.editor}
/>
<div className={styles.wrapperOverrides}>
<div className="slate-query-field__wrapper">
<div className="slate-query-field">
{showingSuggestions && (
<Portal>
<ReactPopper
referenceElement={selectionRef}
placement="top-end"
modifiers={{
preventOverflow: { enabled: true, boundariesElement: 'window' },
arrow: { enabled: false },
offset: { offset: 250 }, // width of the suggestions menu
}}
>
{({ ref, style, placement }) => {
return (
<div ref={ref} style={style} data-placement={placement}>
<DataLinkSuggestions
suggestions={stateRef.current.suggestions}
onSuggestionSelect={onVariableSelect}
onClose={() => setShowingSuggestions(false)}
activeIndex={suggestionsIndex}
/>
</div>
);
}}
</ReactPopper>
</Portal>
)}
<Editor
schema={SCHEMA}
ref={editorRef}
placeholder={placeholder}
value={stateRef.current.linkUrl}
onChange={onUrlChange}
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
plugins={plugins}
className={cx(
styles.editor,
styles.input,
css`
padding: 3px 8px;
`
)}
/>
</div>
</div>
</div>
);
......
// Libraries
import React, { FC } from 'react';
// @ts-ignore
import Prism from 'prismjs';
// Components
import { css } from 'emotion';
import { DataLink, VariableSuggestion } from '@grafana/data';
import { Button } from '../index';
import { DataLinkEditor } from './DataLinkEditor';
import { useTheme } from '../../themes/ThemeContext';
interface DataLinksEditorProps {
value?: DataLink[];
onChange: (links: DataLink[], callback?: () => void) => void;
suggestions: VariableSuggestion[];
maxLinks?: number;
}
export const enableDatalinksPrismSyntax = () => {
Prism.languages['links'] = {
builtInVariable: {
pattern: /(\${\S+?})/,
},
};
};
export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(
({ value = [], onChange, suggestions, maxLinks }) => {
const theme = useTheme();
enableDatalinksPrismSyntax();
const onAdd = () => {
onChange(value ? [...value, { url: '', title: '' }] : [{ url: '', title: '' }]);
};
const onLinkChanged = (linkIndex: number, newLink: DataLink, callback?: () => void) => {
onChange(
value.map((item, listIndex) => {
if (linkIndex === listIndex) {
return newLink;
}
return item;
}),
callback
);
};
const onRemove = (link: DataLink) => {
onChange(value.filter(item => item !== link));
};
return (
<>
{value && value.length > 0 && (
<div
className={css`
margin-bottom: ${theme.spacing.sm};
`}
>
{value.map((link, index) => (
<DataLinkEditor
key={index.toString()}
index={index}
isLast={index === value.length - 1}
value={link}
onChange={onLinkChanged}
onRemove={onRemove}
suggestions={suggestions}
/>
))}
</div>
)}
{(!value || (value && value.length < (maxLinks || Infinity))) && (
<Button variant="secondary" icon="plus" onClick={() => onAdd()}>
Add link
</Button>
)}
</>
);
}
);
DataLinksEditor.displayName = 'DataLinksEditor';
......@@ -31,7 +31,6 @@ export const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = (
onChange={(index, link) => {
setDirtyLink(link);
}}
onRemove={() => {}}
/>
<HorizontalGroup>
<Button
......
......@@ -93,7 +93,6 @@ export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
export * from './SingleStatShared/index';
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
......
......@@ -10,13 +10,13 @@ import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList';
import {
ColorPicker,
DataLinksEditor,
DataSourceHttpSettings,
GraphContextMenu,
SeriesColorPickerPopoverWithTheme,
UnitPicker,
Icon,
LegacyForms,
DataLinksInlineEditor,
} from '@grafana/ui';
const { SecretFormField } = LegacyForms;
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
......@@ -156,8 +156,9 @@ export function registerAngularDirectives() {
// We keep the drilldown terminology here because of as using data-* directive
// being in conflict with HTML data attributes
react2AngularDirective('drilldownLinksEditor', DataLinksEditor, [
react2AngularDirective('drilldownLinksEditor', DataLinksInlineEditor, [
'value',
'links',
'suggestions',
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
......
......@@ -37,7 +37,7 @@ export const DataLink = (props: Props) => {
return (
<div className={className}>
<div className={styles.firstRow}>
<div className={styles.firstRow + ' gf-form'}>
<FormField
className={styles.nameField}
labelWidth={6}
......@@ -59,27 +59,28 @@ export const DataLink = (props: Props) => {
}}
/>
</div>
<FormField
label="URL"
labelWidth={6}
inputEl={
<DataLinkInput
placeholder={'http://example.com/${__value.raw}'}
value={value.url || ''}
onChange={newValue =>
onChange({
...value,
url: newValue,
})
}
suggestions={suggestions}
/>
}
className={css`
width: 100%;
`}
/>
<div className="gf-form">
<FormField
label="URL"
labelWidth={6}
inputEl={
<DataLinkInput
placeholder={'http://example.com/${__value.raw}'}
value={value.url || ''}
onChange={newValue =>
onChange({
...value,
url: newValue,
})
}
suggestions={suggestions}
/>
}
className={css`
width: 100%;
`}
/>
</div>
</div>
);
};
<drilldown-links-editor
value="ctrl.panel.options.dataLinks"
links="ctrl.panel.options.dataLinks"
suggestions="ctrl.linkVariableSuggestions"
on-change="ctrl.onDataLinksChange"
></drilldown-links-editor>
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