mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 01:23:32 -06:00
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:
parent
3fbdcf1070
commit
e18e4cf015
@ -1,11 +1,11 @@
|
||||
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>
|
||||
);
|
||||
|
@ -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';
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
<drilldown-links-editor
|
||||
value="ctrl.panel.options.dataLinks"
|
||||
links="ctrl.panel.options.dataLinks"
|
||||
suggestions="ctrl.linkVariableSuggestions"
|
||||
on-change="ctrl.onDataLinksChange"
|
||||
></drilldown-links-editor>
|
||||
|
Loading…
Reference in New Issue
Block a user