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
This commit is contained in:
Tobias Skarhed 2020-04-24 09:26:22 +02:00 committed by GitHub
parent 3fbdcf1070
commit e18e4cf015
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 113 additions and 184 deletions

View File

@ -1,11 +1,11 @@
import React, { ChangeEvent, useContext } from 'react'; import React, { ChangeEvent, useContext } from 'react';
import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data'; import { DataLink, VariableSuggestion, GrafanaTheme } from '@grafana/data';
import { FormField } from '../FormField/FormField'; import { Switch } from '../Switch/Switch';
import { Switch } from '../Forms/Legacy/Switch/Switch';
import { css } from 'emotion'; import { css } from 'emotion';
import { ThemeContext, stylesFactory } from '../../themes/index'; import { ThemeContext, stylesFactory } from '../../themes/index';
import { DataLinkInput } from './DataLinkInput'; import { DataLinkInput } from './DataLinkInput';
import { Icon } from '../Icon/Icon'; import { Field } from '../Forms/Field';
import { Input } from '../Input/Input';
interface DataLinkEditorProps { interface DataLinkEditorProps {
index: number; index: number;
@ -13,7 +13,6 @@ interface DataLinkEditorProps {
value: DataLink; value: DataLink;
suggestions: VariableSuggestion[]; suggestions: VariableSuggestion[];
onChange: (index: number, link: DataLink, callback?: () => void) => void; onChange: (index: number, link: DataLink, callback?: () => void) => void;
onRemove: (link: DataLink) => void;
} }
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
@ -28,7 +27,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
})); }));
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo( export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
({ index, value, onChange, onRemove, suggestions, isLast }) => { ({ index, value, onChange, suggestions, isLast }) => {
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
const styles = getStyles(theme); const styles = getStyles(theme);
@ -39,39 +38,24 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
onChange(index, { ...value, title: event.target.value }); onChange(index, { ...value, title: event.target.value });
}; };
const onRemoveClick = () => {
onRemove(value);
};
const onOpenInNewTabChanged = () => { const onOpenInNewTabChanged = () => {
onChange(index, { ...value, targetBlank: !value.targetBlank }); onChange(index, { ...value, targetBlank: !value.targetBlank });
}; };
return ( return (
<div className={styles.listItem}> <div className={styles.listItem}>
<div className="gf-form gf-form--inline"> <Field label="Title">
<FormField <Input value={value.title} onChange={onTitleChange} placeholder="Show details" />
className="gf-form--grow" </Field>
label="Title"
value={value.title} <Field label="URL">
onChange={onTitleChange} <DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />
inputWidth={0} </Field>
labelWidth={5}
placeholder="Show details" <Field label="Open in new tab">
/> <Switch checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
<Switch label="Open in new tab" checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} /> </Field>
<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%;
`}
/>
{isLast && ( {isLast && (
<div className={styles.infoText}> <div className={styles.infoText}>
With data links you can reference data variables like series name, labels and values. Type CMD+Space, With data links you can reference data variables like series name, labels and values. Type CMD+Space,

View File

@ -3,13 +3,15 @@ import usePrevious from 'react-use/lib/usePrevious';
import { DataLinkSuggestions } from './DataLinkSuggestions'; import { DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, makeValue } from '../../index'; import { ThemeContext, makeValue } from '../../index';
import { SelectionReference } from './SelectionReference'; 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 { Editor } from '@grafana/slate-react';
import { Value } from 'slate'; import { Value } from 'slate';
import Plain from 'slate-plain-serializer'; import Plain from 'slate-plain-serializer';
import { Popper as ReactPopper } from 'react-popper'; import { Popper as ReactPopper } from 'react-popper';
import { css } from 'emotion'; import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins'; import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate'; import { SCHEMA } from '../../utils/slate';
@ -33,6 +35,7 @@ const plugins = [
]; ];
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
input: getFormStyles(theme, { variant: 'primary', size: 'md', invalid: false }).input.input,
editor: css` editor: css`
.token.builtInVariable { .token.builtInVariable {
color: ${theme.palette.queryGreen}; color: ${theme.palette.queryGreen};
@ -41,12 +44,31 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
color: ${theme.colors.textBlue}; 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 // 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. // was used and changes to different state were propagated here.
export const DataLinkInput: React.FC<DataLinkInputProps> = memo( export const DataLinkInput: React.FC<DataLinkInputProps> = memo(
({ value, onChange, suggestions, placeholder = 'http://your-grafana.com/d/000000010/annotations' }) => { ({ value, onChange, suggestions, placeholder = 'http://your-grafana.com/d/000000010/annotations' }) => {
enableDatalinksPrismSyntax();
const editorRef = useRef<Editor>() as RefObject<Editor>; const editorRef = useRef<Editor>() as RefObject<Editor>;
const theme = useContext(ThemeContext); const theme = useContext(ThemeContext);
const styles = getStyles(theme); const styles = getStyles(theme);
@ -119,44 +141,52 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = memo(
}; };
return ( return (
<div className="slate-query-field__wrapper"> <div className={styles.wrapperOverrides}>
<div className="slate-query-field"> <div className="slate-query-field__wrapper">
{showingSuggestions && ( <div className="slate-query-field">
<Portal> {showingSuggestions && (
<ReactPopper <Portal>
referenceElement={selectionRef} <ReactPopper
placement="top-end" referenceElement={selectionRef}
modifiers={{ placement="top-end"
preventOverflow: { enabled: true, boundariesElement: 'window' }, modifiers={{
arrow: { enabled: false }, preventOverflow: { enabled: true, boundariesElement: 'window' },
offset: { offset: 250 }, // width of the suggestions menu arrow: { enabled: false },
}} offset: { offset: 250 }, // width of the suggestions menu
> }}
{({ ref, style, placement }) => { >
return ( {({ ref, style, placement }) => {
<div ref={ref} style={style} data-placement={placement}> return (
<DataLinkSuggestions <div ref={ref} style={style} data-placement={placement}>
suggestions={stateRef.current.suggestions} <DataLinkSuggestions
onSuggestionSelect={onVariableSelect} suggestions={stateRef.current.suggestions}
onClose={() => setShowingSuggestions(false)} onSuggestionSelect={onVariableSelect}
activeIndex={suggestionsIndex} onClose={() => setShowingSuggestions(false)}
/> activeIndex={suggestionsIndex}
</div> />
); </div>
}} );
</ReactPopper> }}
</Portal> </ReactPopper>
)} </Portal>
<Editor )}
schema={SCHEMA} <Editor
ref={editorRef} schema={SCHEMA}
placeholder={placeholder} ref={editorRef}
value={stateRef.current.linkUrl} placeholder={placeholder}
onChange={onUrlChange} value={stateRef.current.linkUrl}
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)} onChange={onUrlChange}
plugins={plugins} onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
className={styles.editor} plugins={plugins}
/> className={cx(
styles.editor,
styles.input,
css`
padding: 3px 8px;
`
)}
/>
</div>
</div> </div>
</div> </div>
); );

View File

@ -1,85 +0,0 @@
// 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';

View File

@ -31,7 +31,6 @@ export const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = (
onChange={(index, link) => { onChange={(index, link) => {
setDirtyLink(link); setDirtyLink(link);
}} }}
onRemove={() => {}}
/> />
<HorizontalGroup> <HorizontalGroup>
<Button <Button

View File

@ -93,7 +93,6 @@ export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
export * from './SingleStatShared/index'; export * from './SingleStatShared/index';
export { CallToActionCard } from './CallToActionCard/CallToActionCard'; export { CallToActionCard } from './CallToActionCard/CallToActionCard';
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu'; export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
export { DataLinkInput } from './DataLinks/DataLinkInput'; export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';

View File

@ -10,13 +10,13 @@ import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList'; import AppNotificationList from './components/AppNotifications/AppNotificationList';
import { import {
ColorPicker, ColorPicker,
DataLinksEditor,
DataSourceHttpSettings, DataSourceHttpSettings,
GraphContextMenu, GraphContextMenu,
SeriesColorPickerPopoverWithTheme, SeriesColorPickerPopoverWithTheme,
UnitPicker, UnitPicker,
Icon, Icon,
LegacyForms, LegacyForms,
DataLinksInlineEditor,
} from '@grafana/ui'; } from '@grafana/ui';
const { SecretFormField } = LegacyForms; const { SecretFormField } = LegacyForms;
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor'; 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 // We keep the drilldown terminology here because of as using data-* directive
// being in conflict with HTML data attributes // being in conflict with HTML data attributes
react2AngularDirective('drilldownLinksEditor', DataLinksEditor, [ react2AngularDirective('drilldownLinksEditor', DataLinksInlineEditor, [
'value', 'value',
'links',
'suggestions', 'suggestions',
['onChange', { watchDepth: 'reference', wrapApply: true }], ['onChange', { watchDepth: 'reference', wrapApply: true }],
]); ]);

View File

@ -37,7 +37,7 @@ export const DataLink = (props: Props) => {
return ( return (
<div className={className}> <div className={className}>
<div className={styles.firstRow}> <div className={styles.firstRow + ' gf-form'}>
<FormField <FormField
className={styles.nameField} className={styles.nameField}
labelWidth={6} labelWidth={6}
@ -59,27 +59,28 @@ export const DataLink = (props: Props) => {
}} }}
/> />
</div> </div>
<div className="gf-form">
<FormField <FormField
label="URL" label="URL"
labelWidth={6} labelWidth={6}
inputEl={ inputEl={
<DataLinkInput <DataLinkInput
placeholder={'http://example.com/${__value.raw}'} placeholder={'http://example.com/${__value.raw}'}
value={value.url || ''} value={value.url || ''}
onChange={newValue => onChange={newValue =>
onChange({ onChange({
...value, ...value,
url: newValue, url: newValue,
}) })
} }
suggestions={suggestions} suggestions={suggestions}
/> />
} }
className={css` className={css`
width: 100%; width: 100%;
`} `}
/> />
</div>
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
<drilldown-links-editor <drilldown-links-editor
value="ctrl.panel.options.dataLinks" links="ctrl.panel.options.dataLinks"
suggestions="ctrl.linkVariableSuggestions" suggestions="ctrl.linkVariableSuggestions"
on-change="ctrl.onDataLinksChange" on-change="ctrl.onDataLinksChange"
></drilldown-links-editor> ></drilldown-links-editor>