Graph: Add data links feature (click on graph) (#17267)

* WIP: initial panel links editor

* WIP: Added dashboard migration to new panel drilldown link schema

* Make link_srv interpolate new variables

* Fix failing tests

* Drilldown: Add context menu to graph viz (#17284)

* Add simple context menu for adding graph annotations and showing drilldown links

* Close graph context menu when user start scrolling

* Move context menu component to grafana/ui

* Make graph context menu appear on click, use cmd/ctrl click for quick annotations

* Move graph context menu controller to separate file

* Drilldown: datapoint variables interpolation (#17328)

* Add simple context menu for adding graph annotations and showing drilldown links

* Close graph context menu when user start scrolling

* Move context menu component to grafana/ui

* Make graph context menu appear on click, use cmd/ctrl click for quick annotations

* Add util for absolute time range transformation

* Add series name and datapoint timestamp interpolation

* Rename drilldown link variables tot snake case, use const values instead of strings in tests

* Bring LinkSrv.getPanelLinkAnchorInfo for compatibility reasons and add deprecation warning

* Rename seriesLabel to seriesName

* Drilldown: use separate editors for panel and series links (#17355)

* Use correct target ini context menu links

* Rename PanelLinksEditor to DrilldownLinksEditor and mote it to grafana/ui

* Expose DrilldownLinksEditor as an angular directive

* Enable visualization specifix drilldown links

* Props interfaces rename

* Drilldown: Add variables suggestion and syntax highlighting for drilldown link editor (#17391)

* Add variables suggestion in drilldown link editor

* Enable prism

* Fix backspace not working

* Move slate value helpers to grafana/ui

* Add syntax higlighting for links input

* Rename drilldown link components to data links

* Add template variabe suggestions

* Bugfix

* Fix regexp not working in Firefox

* Display correct links in panel header corner

* bugfix

* bugfix

* Bugfix

* Context menu UI tweaks

* Use data link terminology instead of drilldown

* DataLinks: changed autocomplete syntax

* Use singular form for data link

* Use the same syntax higlighting for built-in and template variables in data links editor

* UI improvements to context menu

* UI review tweaks

* Tweak layout of data link editor

* Fix vertical spacing

* Remove data link header in context menu

* Remove pointer cursor from series label in context menu

* Fix variable selection on click

* DataLinks: migrations for old links

* Update docs about data links

* Use value time instead of time range when interpolating datapoint timestamp

* Remove not used util

* Update docs

* Moved icon a bit more down

* Interpolate value ts only when using __value_time variable

* Bring href property back to LinkModel

* Add any type annotations

* Fix TS error on slate's Value type

* minor changes
This commit is contained in:
Torkel Ödegaard
2019-06-25 11:38:51 +02:00
committed by GitHub
parent cbe057fce8
commit 335cec07a5
48 changed files with 1585 additions and 199 deletions
@@ -1,4 +1,4 @@
import deprecationWarning from '../../utils/deprecationWarning';
import { deprecationWarning } from '../../utils/deprecationWarning';
import { ColorPickerProps } from './ColorPickerPopover';
export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => {
@@ -0,0 +1,261 @@
import React, { useContext, useRef } from 'react';
import { css, cx } from 'emotion';
import useClickAway from 'react-use/lib/useClickAway';
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
import { Portal, List } from '../index';
export interface ContextMenuItem {
label: string;
target?: string;
icon?: string;
url?: string;
onClick?: (event?: React.SyntheticEvent<HTMLElement>) => void;
group?: string;
}
export interface ContextMenuGroup {
label?: string;
items: ContextMenuItem[];
}
export interface ContextMenuProps {
x: number;
y: number;
onClose: () => void;
items?: ContextMenuGroup[];
renderHeader?: () => JSX.Element;
}
const getContextMenuStyles = (theme: GrafanaTheme) => {
const linkColor = selectThemeVariant(
{
light: theme.colors.dark2,
dark: theme.colors.text,
},
theme.type
);
const linkColorHover = selectThemeVariant(
{
light: theme.colors.link,
dark: theme.colors.white,
},
theme.type
);
const wrapperBg = selectThemeVariant(
{
light: theme.colors.gray7,
dark: theme.colors.dark2,
},
theme.type
);
const wrapperShadow = selectThemeVariant(
{
light: theme.colors.gray3,
dark: theme.colors.black,
},
theme.type
);
const itemColor = selectThemeVariant(
{
light: theme.colors.black,
dark: theme.colors.white,
},
theme.type
);
const groupLabelColor = selectThemeVariant(
{
light: theme.colors.gray1,
dark: theme.colors.textWeak,
},
theme.type
);
const itemBgHover = selectThemeVariant(
{
light: theme.colors.gray5,
dark: theme.colors.dark7,
},
theme.type
);
const headerBg = selectThemeVariant(
{
light: theme.colors.white,
dark: theme.colors.dark1,
},
theme.type
);
const headerSeparator = selectThemeVariant(
{
light: theme.colors.white,
dark: theme.colors.dark7,
},
theme.type
);
return {
header: css`
padding: 4px;
border-bottom: 1px solid ${headerSeparator};
background: ${headerBg};
margin-bottom: ${theme.spacing.xs};
border-radius: ${theme.border.radius.sm} ${theme.border.radius.sm} 0 0;
`,
wrapper: css`
background: ${wrapperBg};
z-index: 1;
box-shadow: 0 2px 5px 0 ${wrapperShadow};
min-width: 200px;
border-radius: ${theme.border.radius.sm};
`,
link: css`
color: ${linkColor};
display: flex;
cursor: pointer;
&:hover {
color: ${linkColorHover};
text-decoration: none;
}
`,
item: css`
background: none;
padding: 4px 8px;
color: ${itemColor};
border-left: 2px solid transparent;
cursor: pointer;
&:hover {
background: ${itemBgHover};
border-image: linear-gradient(rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%);
border-image-slice: 1;
}
`,
groupLabel: css`
color: ${groupLabelColor};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.lg};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
`,
icon: css`
opacity: 0.7;
width: 12px;
height: 12px;
display: inline-block;
margin-right: 10px;
color: ${theme.colors.linkDisabled};
position: relative;
top: 4px;
`,
};
};
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => {
const theme = useContext(ThemeContext);
const menuRef = useRef(null);
useClickAway(menuRef, () => {
if (onClose) {
onClose();
}
});
const styles = getContextMenuStyles(theme);
return (
<Portal>
<div
ref={menuRef}
style={{
position: 'fixed',
left: x - 5,
top: y + 5,
}}
className={styles.wrapper}
>
{renderHeader && <div className={styles.header}>{renderHeader()}</div>}
<List
items={items || []}
renderItem={(item, index) => {
return (
<>
<ContextMenuGroup group={item} onItemClick={onClose} />
</>
);
}}
/>
</div>
</Portal>
);
});
interface ContextMenuItemProps {
label: string;
icon?: string;
url?: string;
target?: string;
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
className?: string;
}
const ContextMenuItem: React.FC<ContextMenuItemProps> = React.memo(
({ url, icon, label, target, onClick, className }) => {
const theme = useContext(ThemeContext);
const styles = getContextMenuStyles(theme);
return (
<div className={styles.item}>
<a
href={url}
target={target || '_self'}
className={cx(className, styles.link)}
onClick={e => {
if (onClick) {
onClick(e);
}
}}
>
{icon && <i className={cx(`${icon}`, styles.icon)} />} {label}
</a>
</div>
);
}
);
interface ContextMenuGroupProps {
group: ContextMenuGroup;
onItemClick?: () => void;
}
const ContextMenuGroup: React.FC<ContextMenuGroupProps> = ({ group, onItemClick }) => {
const theme = useContext(ThemeContext);
const styles = getContextMenuStyles(theme);
if (group.items.length === 0) {
return null;
}
return (
<div>
{group.label && <div className={styles.groupLabel}>{group.label}</div>}
<List
items={group.items || []}
renderItem={item => {
return (
<ContextMenuItem
url={item.url}
label={item.label}
target={item.target}
icon={item.icon}
onClick={(e: React.MouseEvent<HTMLElement>) => {
if (item.onClick) {
item.onClick(e);
}
if (onItemClick) {
onItemClick();
}
}}
/>
);
}}
/>
</div>
);
};
ContextMenu.displayName = 'ContextMenu';
@@ -0,0 +1,85 @@
import React, { useState, ChangeEvent, useContext } from 'react';
import { DataLink } from '../../index';
import { FormField, Switch } from '../index';
import { VariableSuggestion } from './DataLinkSuggestions';
import { css, cx } from 'emotion';
import { ThemeContext } from '../../themes/index';
import { DataLinkInput } from './DataLinkInput';
interface DataLinkEditorProps {
index: number;
value: DataLink;
suggestions: VariableSuggestion[];
onChange: (index: number, link: DataLink) => void;
onRemove: (link: DataLink) => void;
}
export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
({ index, value, onChange, onRemove, suggestions }) => {
const theme = useContext(ThemeContext);
const [title, setTitle] = useState(value.title);
const onUrlChange = (url: string) => {
onChange(index, { ...value, url });
};
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
setTitle(event.target.value);
};
const onTitleBlur = () => {
onChange(index, { ...value, title: title });
};
const onRemoveClick = () => {
onRemove(value);
};
const onOpenInNewTabChanged = () => {
onChange(index, { ...value, targetBlank: !value.targetBlank });
};
return (
<div
className={cx(
'gf-form gf-form--inline',
css`
> * {
margin-right: ${theme.spacing.xs};
&:last-child {
margin-right: 0;
}
}
`
)}
>
<FormField
label="Title"
value={title}
onChange={onTitleChange}
onBlur={onTitleBlur}
inputWidth={15}
labelWidth={5}
/>
<FormField
label="URL"
labelWidth={4}
inputEl={<DataLinkInput value={value.url} onChange={onUrlChange} suggestions={suggestions} />}
className={css`
width: 100%;
`}
/>
<Switch label="Open in new tab" checked={value.targetBlank || false} onChange={onOpenInNewTabChanged} />
<div className="gf-form">
<button className="gf-form-label gf-form-label--btn" onClick={onRemoveClick}>
<i className="fa fa-times" />
</button>
</div>
</div>
);
}
);
DataLinkEditor.displayName = 'DataLinkEditor';
@@ -0,0 +1,200 @@
import React, { useState, useMemo, useCallback, useContext } from 'react';
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
import { makeValue, ThemeContext } from '../../index';
import { SelectionReference } from './SelectionReference';
import { Portal } from '../index';
// @ts-ignore
import { Editor } from 'slate-react';
// @ts-ignore
import { Value, Change, Document } from 'slate';
// @ts-ignore
import Plain from 'slate-plain-serializer';
import { Popper as ReactPopper } from 'react-popper';
import useDebounce from 'react-use/lib/useDebounce';
import { css, cx } from 'emotion';
// @ts-ignore
import PluginPrism from 'slate-prism';
interface DataLinkInputProps {
value: string;
onChange: (url: string) => void;
suggestions: VariableSuggestion[];
}
const plugins = [
PluginPrism({
onlyIn: (node: any) => node.type === 'code_block',
getSyntax: () => 'links',
}),
];
export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, suggestions }) => {
const theme = useContext(ThemeContext);
const [showingSuggestions, setShowingSuggestions] = useState(false);
const [suggestionsIndex, setSuggestionsIndex] = useState(0);
const [usedSuggestions, setUsedSuggestions] = useState(
suggestions.filter(suggestion => {
return value.indexOf(suggestion.value) > -1;
})
);
// Using any here as TS has problem pickung up `change` method existance on Value
// According to code and documentation `change` is an instance method on Value in slate 0.33.8 that we use
// https://github.com/ianstormtaylor/slate/blob/slate%400.33.8/docs/reference/slate/value.md#change
const [linkUrl, setLinkUrl] = useState<any>(makeValue(value));
const getStyles = useCallback(() => {
return {
editor: css`
.token.builtInVariable {
color: ${theme.colors.queryGreen};
}
.token.variable {
color: ${theme.colors.queryKeyword};
}
`,
};
}, [theme]);
const currentSuggestions = useMemo(
() =>
suggestions.filter(suggestion => {
return usedSuggestions.map(s => s.value).indexOf(suggestion.value) === -1;
}),
[usedSuggestions, suggestions]
);
// SelectionReference is used to position the variables suggestion relatively to current DOM selection
const selectionRef = useMemo(() => new SelectionReference(), [setShowingSuggestions]);
// Keep track of variables that has been used already
const updateUsedSuggestions = () => {
const currentLink = Plain.serialize(linkUrl);
const next = usedSuggestions.filter(suggestion => {
return currentLink.indexOf(suggestion.value) > -1;
});
if (next.length !== usedSuggestions.length) {
setUsedSuggestions(next);
}
};
useDebounce(updateUsedSuggestions, 500, [linkUrl]);
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Backspace') {
setShowingSuggestions(false);
setSuggestionsIndex(0);
}
if (event.key === 'Enter') {
if (showingSuggestions) {
onVariableSelect(currentSuggestions[suggestionsIndex]);
}
}
if (showingSuggestions) {
if (event.key === 'ArrowDown') {
event.preventDefault();
setSuggestionsIndex(index => {
return (index + 1) % currentSuggestions.length;
});
}
if (event.key === 'ArrowUp') {
event.preventDefault();
setSuggestionsIndex(index => {
const nextIndex = index - 1 < 0 ? currentSuggestions.length - 1 : (index - 1) % currentSuggestions.length;
return nextIndex;
});
}
}
if (event.key === '?' || event.key === '&' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
setShowingSuggestions(true);
}
if (event.key === 'Backspace') {
// @ts-ignore
return;
} else {
return true;
}
};
const onUrlChange = ({ value }: Change) => {
setLinkUrl(value);
};
const onUrlBlur = () => {
onChange(Plain.serialize(linkUrl));
};
const onVariableSelect = (item: VariableSuggestion) => {
const includeDollarSign = Plain.serialize(linkUrl).slice(-1) !== '$';
const change = linkUrl.change();
if (item.origin === VariableOrigin.BuiltIn) {
change.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
} else {
change.insertText(`var-${item.value}=$\{${item.value}}`);
}
setLinkUrl(change.value);
setShowingSuggestions(false);
setUsedSuggestions((previous: VariableSuggestion[]) => {
return [...previous, item];
});
setSuggestionsIndex(0);
onChange(Plain.serialize(change.value));
};
return (
<div
className={cx(
'gf-form-input',
css`
position: relative;
height: auto;
`
)}
>
<div className="slate-query-field">
{showingSuggestions && (
<Portal>
<ReactPopper
referenceElement={selectionRef}
placement="auto-end"
modifiers={{
preventOverflow: { enabled: true, boundariesElement: 'window' },
arrow: { enabled: false },
offset: { offset: 200 }, // width of the suggestions menu
}}
>
{({ ref, style, placement }) => {
return (
<div ref={ref} style={style} data-placement={placement}>
<DataLinkSuggestions
suggestions={currentSuggestions}
onSuggestionSelect={onVariableSelect}
onClose={() => setShowingSuggestions(false)}
activeIndex={suggestionsIndex}
/>
</div>
);
}}
</ReactPopper>
</Portal>
)}
<Editor
placeholder="http://your-grafana.com/d/000000010/annotations"
value={linkUrl}
onChange={onUrlChange}
onBlur={onUrlBlur}
onKeyDown={onKeyDown}
plugins={plugins}
className={getStyles().editor}
/>
</div>
</div>
);
};
DataLinkInput.displayName = 'DataLinkInput';
@@ -0,0 +1,190 @@
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
import { css, cx } from 'emotion';
import React, { useRef, useContext, useMemo } from 'react';
import useClickAway from 'react-use/lib/useClickAway';
import { List } from '../index';
export enum VariableOrigin {
BuiltIn = 'builtin',
Template = 'template',
}
export interface VariableSuggestion {
value: string;
documentation?: string;
origin: VariableOrigin;
}
interface DataLinkSuggestionsProps {
suggestions: VariableSuggestion[];
activeIndex: number;
onSuggestionSelect: (suggestion: VariableSuggestion) => void;
onClose?: () => void;
}
const getStyles = (theme: GrafanaTheme) => {
const wrapperBg = selectThemeVariant(
{
light: theme.colors.white,
dark: theme.colors.dark2,
},
theme.type
);
const wrapperShadow = selectThemeVariant(
{
light: theme.colors.gray5,
dark: theme.colors.black,
},
theme.type
);
const itemColor = selectThemeVariant(
{
light: theme.colors.black,
dark: theme.colors.white,
},
theme.type
);
const itemDocsColor = selectThemeVariant(
{
light: theme.colors.dark3,
dark: theme.colors.gray2,
},
theme.type
);
const itemBgHover = selectThemeVariant(
{
light: theme.colors.gray5,
dark: theme.colors.dark7,
},
theme.type
);
const itemBgActive = selectThemeVariant(
{
light: theme.colors.gray6,
dark: theme.colors.dark9,
},
theme.type
);
return {
wrapper: css`
background: ${wrapperBg};
z-index: 1;
width: 200px;
box-shadow: 0 5px 10px 0 ${wrapperShadow};
`,
item: css`
background: none;
padding: 4px 8px;
color: ${itemColor};
cursor: pointer;
&:hover {
background: ${itemBgHover};
}
`,
label: css`
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.lg};
padding: ${theme.spacing.sm};
`,
activeItem: css`
background: ${itemBgActive};
&:hover {
background: ${itemBgActive};
}
`,
itemValue: css`
font-family: ${theme.typography.fontFamily.monospace};
`,
itemDocs: css`
margin-top: ${theme.spacing.xs};
color: ${itemDocsColor};
font-size: ${theme.typography.size.sm};
`,
};
};
export const DataLinkSuggestions: React.FC<DataLinkSuggestionsProps> = ({ suggestions, ...otherProps }) => {
const ref = useRef(null);
const theme = useContext(ThemeContext);
useClickAway(ref, () => {
if (otherProps.onClose) {
otherProps.onClose();
}
});
const templateSuggestions = useMemo(() => {
return suggestions.filter(suggestion => suggestion.origin === VariableOrigin.Template);
}, [suggestions]);
const builtInSuggestions = useMemo(() => {
return suggestions.filter(suggestion => suggestion.origin === VariableOrigin.BuiltIn);
}, [suggestions]);
const styles = getStyles(theme);
return (
<div ref={ref} className={styles.wrapper}>
{templateSuggestions.length > 0 && (
<DataLinkSuggestionsList
{...otherProps}
suggestions={templateSuggestions}
label="Template variables"
activeIndex={otherProps.activeIndex}
activeIndexOffset={0}
/>
)}
{builtInSuggestions.length > 0 && (
<DataLinkSuggestionsList
{...otherProps}
suggestions={builtInSuggestions}
label="Built-in variables"
activeIndexOffset={templateSuggestions.length}
/>
)}
</div>
);
};
DataLinkSuggestions.displayName = 'DataLinkSuggestions';
interface DataLinkSuggestionsListProps extends DataLinkSuggestionsProps {
label: string;
activeIndexOffset: number;
}
const DataLinkSuggestionsList: React.FC<DataLinkSuggestionsListProps> = React.memo(
({ activeIndex, activeIndexOffset, label, onClose, onSuggestionSelect, suggestions }) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
return (
<>
<div className={styles.label}>{label}</div>
<List
items={suggestions}
renderItem={(item, index) => {
return (
<div
className={cx(styles.item, index + activeIndexOffset === activeIndex && styles.activeItem)}
onClick={() => {
onSuggestionSelect(item);
}}
>
<div className={styles.itemValue}>{item.value}</div>
{item.documentation && <div className={styles.itemDocs}>{item.documentation}</div>}
</div>
);
}}
/>
</>
);
}
);
DataLinkSuggestionsList.displayName = 'DataLinkSuggestionsList';
@@ -0,0 +1,77 @@
// Libraries
import React, { FC, useContext } from 'react';
// @ts-ignore
import Prism from 'prismjs';
// Components
import { css } from 'emotion';
import { DataLink, ThemeContext } from '../../index';
import { Button } from '../index';
import { DataLinkEditor } from './DataLinkEditor';
import { VariableSuggestion } from './DataLinkSuggestions';
interface DataLinksEditorProps {
value: DataLink[];
onChange: (links: DataLink[]) => void;
suggestions: VariableSuggestion[];
maxLinks?: number;
}
Prism.languages['links'] = {
builtInVariable: {
pattern: /(\${\w+})/,
},
};
export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, onChange, suggestions, maxLinks }) => {
const theme = useContext(ThemeContext);
const onAdd = () => {
onChange(value ? [...value, { url: '', title: '' }] : [{ url: '', title: '' }]);
};
const onLinkChanged = (linkIndex: number, newLink: DataLink) => {
onChange(
value.map((item, listIndex) => {
if (linkIndex === listIndex) {
return newLink;
}
return item;
})
);
};
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}
value={link}
onChange={onLinkChanged}
onRemove={onRemove}
suggestions={suggestions}
/>
))}
</div>
)}
{(!value || (value && value.length < (maxLinks || 1))) && (
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
Create link
</Button>
)}
</>
);
});
DataLinksEditor.displayName = 'DataLinksEditor';
@@ -0,0 +1,28 @@
export class SelectionReference {
getBoundingClientRect() {
const selection = window.getSelection();
const node = selection && selection.anchorNode;
if (node && node.parentElement) {
const rect = node.parentElement.getBoundingClientRect();
return rect;
}
return {
top: 0,
left: 0,
bottom: 0,
right: 0,
width: 0,
height: 0,
};
}
get clientWidth() {
return this.getBoundingClientRect().width;
}
get clientHeight() {
return this.getBoundingClientRect().height;
}
}
@@ -1,7 +1,7 @@
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormLabel } from '../FormLabel/FormLabel';
import { PopperContent } from '../Tooltip/PopperController';
import { cx } from 'emotion';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
tooltip?: PopperContent<any>;
@@ -25,10 +25,11 @@ export const FormField: FunctionComponent<Props> = ({
labelWidth,
inputWidth,
inputEl,
className,
...inputProps
}) => {
return (
<div className="form-field">
<div className={cx('form-field', className)}>
<FormLabel width={labelWidth} tooltip={tooltip}>
{label}
</FormLabel>
@@ -79,6 +79,7 @@ export const FieldPropertiesEditor: React.FC<Props> = ({ value, onChange, showMi
{'$' + VAR_CELL_PREFIX + '{N}'} / {'$' + VAR_CALC}
</div>
);
return (
<>
<FormField
@@ -22,7 +22,6 @@ export class Switch extends PureComponent<Props, State> {
internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
event.stopPropagation();
this.props.onChange(event);
};
@@ -7,6 +7,12 @@ $popper-margin-from-ref: 5px;
.popper__arrow {
border-color: $backgroundColor;
}
code {
border: none;
background: darken($backgroundColor, 15%);
color: lighten($textColor, 20%);
}
}
.popper {
@@ -14,7 +20,6 @@ $popper-margin-from-ref: 5px;
z-index: $zindex-tooltip;
color: $tooltipColor;
max-width: 400px;
text-align: center;
}
.popper__background {
@@ -6,6 +6,7 @@ export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
export * from './Button/Button';
export { ButtonVariant } from './Button/AbstractButton';
// Select
export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
@@ -65,3 +66,7 @@ export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
export * from './SingleStatShared/index';
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
export { ContextMenu, ContextMenuItem, ContextMenuGroup, ContextMenuProps } from './ContextMenu/ContextMenu';
export { VariableSuggestion, VariableOrigin } from './DataLinks/DataLinkSuggestions';
export { DataLinksEditor } from './DataLinks/DataLinksEditor';
export { SeriesIcon } from './Legend/SeriesIcon';
@@ -287,7 +287,7 @@ $popover-header-bg: $dark-9;
$popover-shadow: 0 0 20px black;
$popover-help-bg: $btn-secondary-bg;
$popover-help-color: $text-color;
$popover-help-color: $gray-6;
$popover-error-bg: $btn-danger-bg;
+6
View File
@@ -147,6 +147,12 @@ export interface RangeMap extends BaseMap {
to: string;
}
export interface DataLink {
url: string;
title: string;
targetBlank?: boolean;
}
export enum VizOrientation {
Auto = 'auto',
Vertical = 'vertical',
@@ -1,6 +1,4 @@
const deprecationWarning = (file: string, oldName: string, newName: string) => {
export const deprecationWarning = (file: string, oldName: string, newName: string) => {
const message = `[Deprecation warning] ${file}: ${oldName} is deprecated. Use ${newName} instead`;
console.warn(message);
};
export default deprecationWarning;
+1
View File
@@ -17,6 +17,7 @@ export { getFlotPairs } from './flotPairs';
export * from './object';
export * from './fieldCache';
export * from './moment_wrapper';
export * from './slate';
// Names are too general to export
// rangeutils, datemath
+42
View File
@@ -0,0 +1,42 @@
// @ts-ignore
import { Block, Document, Text, Value } from 'slate';
const SCHEMA = {
blocks: {
paragraph: 'paragraph',
codeblock: 'code_block',
codeline: 'code_line',
},
inlines: {},
marks: {},
};
export const makeFragment = (text: string, syntax?: string) => {
const lines = text.split('\n').map(line =>
Block.create({
type: 'code_line',
nodes: [Text.create(line)],
} as any)
);
const block = Block.create({
data: {
syntax,
},
type: 'code_block',
nodes: lines,
} as any);
return Document.create({
nodes: [block],
});
};
export const makeValue = (text: string, syntax?: string) => {
const fragment = makeFragment(text, syntax);
return Value.create({
document: fragment,
SCHEMA,
} as any);
};