@grafana/ui
Version:
Grafana Components Library
213 lines (210 loc) • 7.85 kB
JavaScript
import { jsx, jsxs } from 'react/jsx-runtime';
import { css, cx } from '@emotion/css';
import { offset, useFloating, autoUpdate } from '@floating-ui/react';
import Prism from 'prismjs';
import * as React from 'react';
import { memo, useRef, useState, useEffect } from 'react';
import { usePrevious } from 'react-use';
import Plain from 'slate-plain-serializer';
import { Editor } from 'slate-react';
import { VariableOrigin, DataLinkBuiltInVars } from '@grafana/data';
import { SlatePrism } from '../../slate-plugins/slate-prism/index.mjs';
import { useStyles2 } from '../../themes/ThemeContext.mjs';
import { getPositioningMiddleware } from '../../utils/floating.mjs';
import { makeValue, SCHEMA } from '../../utils/slate.mjs';
import { getInputStyles } from '../Input/Input.mjs';
import { Portal } from '../Portal/Portal.mjs';
import { ScrollContainer } from '../ScrollContainer/ScrollContainer.mjs';
import { DataLinkSuggestions } from './DataLinkSuggestions.mjs';
import { SelectionReference } from './SelectionReference.mjs';
;
const modulo = (a, n) => a - n * Math.floor(a / n);
const datalinksSyntax = {
builtInVariable: {
pattern: /(\${\S+?})/
}
};
const plugins = [
SlatePrism(
{
onlyIn: (node) => "type" in node && node.type === "code_block",
getSyntax: () => "links"
},
{ ...Prism.languages, links: datalinksSyntax }
)
];
const getStyles = (theme) => ({
input: getInputStyles({ theme, invalid: false }).input,
editor: css({
".token.builtInVariable": {
color: theme.colors.success.text
},
".token.variable": {
color: theme.colors.primary.text
}
}),
suggestionsWrapper: css({
boxShadow: theme.shadows.z2
}),
// Wrapper with child selector needed.
// When classnames are applied to the same element as the wrapper, it causes the suggestions to stop working
wrapperOverrides: css({
width: "100%",
"> .slate-query-field__wrapper": {
padding: 0,
backgroundColor: "transparent",
border: "none"
}
})
});
const DataLinkInput = memo(
({
value,
onChange,
suggestions,
placeholder = "http://your-grafana.com/d/000000010/annotations"
}) => {
const editorRef = useRef(null);
const styles = useStyles2(getStyles);
const [showingSuggestions, setShowingSuggestions] = useState(false);
const [suggestionsIndex, setSuggestionsIndex] = useState(0);
const [linkUrl, setLinkUrl] = useState(makeValue(value));
const prevLinkUrl = usePrevious(linkUrl);
const [scrollTop, setScrollTop] = useState(0);
const scrollRef = useRef(null);
useEffect(() => {
var _a;
(_a = scrollRef.current) == null ? void 0 : _a.scrollTo(0, scrollTop);
}, [scrollTop]);
const middleware = [
offset(({ rects }) => ({
alignmentAxis: rects.reference.width
})),
...getPositioningMiddleware()
];
const { refs, floatingStyles } = useFloating({
open: showingSuggestions,
placement: "bottom-start",
onOpenChange: setShowingSuggestions,
middleware,
whileElementsMounted: autoUpdate,
strategy: "fixed"
});
const stateRef = useRef({ showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange });
stateRef.current = { showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange };
const activeRef = useRef(null);
useEffect(() => {
setScrollTop(getElementPosition(activeRef.current, suggestionsIndex));
}, [suggestionsIndex]);
const onKeyDown = React.useCallback((event, next) => {
if (!stateRef.current.showingSuggestions) {
if (event.key === "=" || event.key === "$" || event.keyCode === 32 && event.ctrlKey) {
const selectionRef = new SelectionReference();
refs.setReference(selectionRef);
return setShowingSuggestions(true);
}
return next();
}
switch (event.key) {
case "Backspace":
if (stateRef.current.linkUrl.focusText.getText().length === 1) {
next();
}
case "Escape":
setShowingSuggestions(false);
return setSuggestionsIndex(0);
case "Enter":
event.preventDefault();
return onVariableSelect(stateRef.current.suggestions[stateRef.current.suggestionsIndex]);
case "ArrowDown":
case "ArrowUp":
event.preventDefault();
const direction = event.key === "ArrowDown" ? 1 : -1;
return setSuggestionsIndex((index) => modulo(index + direction, stateRef.current.suggestions.length));
default:
return next();
}
}, []);
useEffect(() => {
if (prevLinkUrl && prevLinkUrl.selection.isFocused && !linkUrl.selection.isFocused) {
stateRef.current.onChange(Plain.serialize(linkUrl));
}
}, [linkUrl, prevLinkUrl]);
const onUrlChange = React.useCallback(({ value: value2 }) => {
setLinkUrl(value2);
}, []);
const onVariableSelect = (item, editor = editorRef.current) => {
const precedingChar = getCharactersAroundCaret();
const precedingDollar = precedingChar === "$";
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
editor.insertText(`${precedingDollar ? "" : "$"}{${item.value}}`);
} else {
editor.insertText(`${precedingDollar ? "" : "$"}{${item.value}:queryparam}`);
}
setLinkUrl(editor.value);
setShowingSuggestions(false);
setSuggestionsIndex(0);
stateRef.current.onChange(Plain.serialize(editor.value));
};
const getCharactersAroundCaret = () => {
const input = document.getElementById("data-link-input");
let precedingChar = "", sel, range;
if (window.getSelection) {
sel = window.getSelection();
if (sel && sel.rangeCount > 0) {
range = sel.getRangeAt(0).cloneRange();
range.collapse(true);
range.setStart(input, 0);
precedingChar = range.toString().slice(-1);
}
}
return precedingChar;
};
return /* @__PURE__ */ jsx("div", { className: styles.wrapperOverrides, children: /* @__PURE__ */ jsx("div", { className: "slate-query-field__wrapper", children: /* @__PURE__ */ jsxs("div", { id: "data-link-input", className: "slate-query-field", children: [
showingSuggestions && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx("div", { ref: refs.setFloating, style: floatingStyles, children: /* @__PURE__ */ jsx(
ScrollContainer,
{
maxHeight: "300px",
ref: scrollRef,
onScroll: (event) => setScrollTop(event.currentTarget.scrollTop),
children: /* @__PURE__ */ jsx(
DataLinkSuggestions,
{
activeRef,
suggestions: stateRef.current.suggestions,
onSuggestionSelect: onVariableSelect,
onClose: () => setShowingSuggestions(false),
activeIndex: suggestionsIndex
}
)
}
) }) }),
/* @__PURE__ */ jsx(
Editor,
{
schema: SCHEMA,
ref: editorRef,
placeholder,
value: stateRef.current.linkUrl,
onChange: onUrlChange,
onKeyDown: (event, _editor, next) => onKeyDown(event, next),
plugins,
className: cx(
styles.editor,
styles.input,
css({
padding: "3px 8px"
})
)
}
)
] }) }) });
}
);
DataLinkInput.displayName = "DataLinkInput";
function getElementPosition(suggestionElement, activeIndex) {
var _a;
return ((_a = suggestionElement == null ? void 0 : suggestionElement.clientHeight) != null ? _a : 0) * activeIndex;
}
export { DataLinkInput };
//# sourceMappingURL=DataLinkInput.mjs.map