mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 18:01:40 -06:00
Monaco: add suggestions for template variables (#25921)
* now with suggestions * using suggestions API * using variable suggestions * using variable suggestions * show variables * minor cleanup * add @alpha warning * Do not produce data variables if panel does not support queries Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
df72344d3c
commit
bbd24cd93a
@ -1,36 +1,37 @@
|
||||
import React from 'react';
|
||||
import { withTheme } from '../../themes';
|
||||
import { Themeable } from '../../types';
|
||||
import { KeyCode, editor, KeyMod } from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { CodeEditorProps } from './types';
|
||||
import { registerSuggestions } from './suggestions';
|
||||
import ReactMonaco from 'react-monaco-editor';
|
||||
|
||||
export interface CodeEditorProps {
|
||||
value: string;
|
||||
language: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
|
||||
readOnly?: boolean;
|
||||
showMiniMap?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
|
||||
/**
|
||||
* Callback after the editor has mounted that gives you raw access to monaco
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
onEditorDidMount?: (editor: editor.IStandaloneCodeEditor) => void;
|
||||
|
||||
/** Handler to be performed when editor is blurred */
|
||||
onBlur?: CodeEditorChangeHandler;
|
||||
|
||||
/** Handler to be performed when Cmd/Ctrl+S is pressed */
|
||||
onSave?: CodeEditorChangeHandler;
|
||||
}
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
type Props = CodeEditorProps & Themeable;
|
||||
|
||||
class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
completionCancel?: monaco.IDisposable;
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.completionCancel) {
|
||||
console.log('dispose of the custom completion stuff');
|
||||
this.completionCancel.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
const { getSuggestions, language } = this.props;
|
||||
if (getSuggestions) {
|
||||
// Language changed
|
||||
if (language !== oldProps.language) {
|
||||
if (this.completionCancel) {
|
||||
this.completionCancel.dispose();
|
||||
}
|
||||
this.completionCancel = registerSuggestions(language, getSuggestions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is replaced with a real function when the actual editor mounts
|
||||
getEditorValue = () => '';
|
||||
|
||||
onBlur = () => {
|
||||
@ -40,13 +41,20 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
}
|
||||
};
|
||||
|
||||
editorDidMount = (editor: editor.IStandaloneCodeEditor) => {
|
||||
editorWillMount = (m: typeof monaco) => {
|
||||
const { language, getSuggestions } = this.props;
|
||||
if (getSuggestions) {
|
||||
this.completionCancel = registerSuggestions(language, getSuggestions);
|
||||
}
|
||||
};
|
||||
|
||||
editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
const { onSave, onEditorDidMount } = this.props;
|
||||
|
||||
this.getEditorValue = () => editor.getValue();
|
||||
|
||||
if (onSave) {
|
||||
editor.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, () => {
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => {
|
||||
onSave(this.getEditorValue());
|
||||
});
|
||||
}
|
||||
@ -61,7 +69,7 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
const value = this.props.value ?? '';
|
||||
const longText = value.length > 100;
|
||||
|
||||
const options: editor.IEditorConstructionOptions = {
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
wordWrap: 'off',
|
||||
codeLens: false, // not included in the bundle
|
||||
minimap: {
|
||||
@ -91,6 +99,7 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
theme={theme.isDark ? 'vs-dark' : 'vs-light'}
|
||||
value={value}
|
||||
options={options}
|
||||
editorWillMount={this.editorWillMount}
|
||||
editorDidMount={this.editorDidMount}
|
||||
/>
|
||||
</div>
|
||||
@ -98,5 +107,4 @@ class UnthemedCodeEditor extends React.PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export type CodeEditorChangeHandler = (value: string) => void;
|
||||
export default withTheme(UnthemedCodeEditor);
|
||||
|
@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { useAsyncDependency } from '../../utils/useAsyncDependency';
|
||||
import { ErrorWithStack, LoadingPlaceholder } from '..';
|
||||
import { CodeEditorProps } from './CodeEditor';
|
||||
|
||||
export type CodeEditorChangeHandler = (value: string) => void;
|
||||
import { CodeEditorProps } from './types';
|
||||
|
||||
export const CodeEditor: React.FC<CodeEditorProps> = props => {
|
||||
const { loading, error, dependency } = useAsyncDependency(
|
||||
@ -11,7 +9,7 @@ export const CodeEditor: React.FC<CodeEditorProps> = props => {
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <LoadingPlaceholder text={'Loading...'} />;
|
||||
return <LoadingPlaceholder text={''} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
104
packages/grafana-ui/src/components/Monaco/suggestions.ts
Normal file
104
packages/grafana-ui/src/components/Monaco/suggestions.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind, CodeEditorSuggestionProvider } from './types';
|
||||
|
||||
function getCompletionItems(
|
||||
prefix: string,
|
||||
suggestions: CodeEditorSuggestionItem[],
|
||||
range: monaco.IRange
|
||||
): monaco.languages.CompletionItem[] {
|
||||
const items: monaco.languages.CompletionItem[] = [];
|
||||
for (const suggestion of suggestions) {
|
||||
if (prefix && !suggestion.label.startsWith(prefix)) {
|
||||
continue; // skip non-matching suggestions
|
||||
}
|
||||
|
||||
items.push({
|
||||
...suggestion,
|
||||
kind: mapKinds(suggestion.kind),
|
||||
range,
|
||||
insertText: suggestion.insertText ?? suggestion.label,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function mapKinds(sug?: CodeEditorSuggestionItemKind): monaco.languages.CompletionItemKind {
|
||||
switch (sug) {
|
||||
case CodeEditorSuggestionItemKind.Method:
|
||||
return monaco.languages.CompletionItemKind.Method;
|
||||
case CodeEditorSuggestionItemKind.Field:
|
||||
return monaco.languages.CompletionItemKind.Field;
|
||||
case CodeEditorSuggestionItemKind.Property:
|
||||
return monaco.languages.CompletionItemKind.Property;
|
||||
case CodeEditorSuggestionItemKind.Constant:
|
||||
return monaco.languages.CompletionItemKind.Constant;
|
||||
case CodeEditorSuggestionItemKind.Text:
|
||||
return monaco.languages.CompletionItemKind.Text;
|
||||
}
|
||||
return monaco.languages.CompletionItemKind.Text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export function registerSuggestions(
|
||||
language: string,
|
||||
getSuggestions: CodeEditorSuggestionProvider
|
||||
): monaco.IDisposable | undefined {
|
||||
if (!language || !getSuggestions) {
|
||||
return undefined;
|
||||
}
|
||||
return monaco.languages.registerCompletionItemProvider(language, {
|
||||
triggerCharacters: ['$'],
|
||||
|
||||
provideCompletionItems: (model, position, context) => {
|
||||
if (context.triggerCharacter === '$') {
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: position.column - 1,
|
||||
endColumn: position.column,
|
||||
};
|
||||
return {
|
||||
suggestions: getCompletionItems('$', getSuggestions(), range),
|
||||
};
|
||||
}
|
||||
|
||||
// find out if we are completing a property in the 'dependencies' object.
|
||||
const lineText = model.getValueInRange({
|
||||
startLineNumber: position.lineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: position.lineNumber,
|
||||
endColumn: position.column,
|
||||
});
|
||||
|
||||
const idx = lineText.lastIndexOf('$');
|
||||
if (idx >= 0) {
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: idx, // the last $ we found
|
||||
endColumn: position.column,
|
||||
};
|
||||
return {
|
||||
suggestions: getCompletionItems(lineText.substr(idx), getSuggestions(), range),
|
||||
};
|
||||
}
|
||||
|
||||
// Empty line that asked for suggestion
|
||||
if (lineText.trim().length < 1) {
|
||||
return {
|
||||
suggestions: getCompletionItems('', getSuggestions(), {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: position.column,
|
||||
endColumn: position.column,
|
||||
}),
|
||||
};
|
||||
}
|
||||
// console.log('complete?', lineText, context);
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
}
|
77
packages/grafana-ui/src/components/Monaco/types.ts
Normal file
77
packages/grafana-ui/src/components/Monaco/types.ts
Normal file
@ -0,0 +1,77 @@
|
||||
export type CodeEditorChangeHandler = (value: string) => void;
|
||||
export type CodeEditorSuggestionProvider = () => CodeEditorSuggestionItem[];
|
||||
|
||||
export interface CodeEditorProps {
|
||||
value: string;
|
||||
language: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
|
||||
readOnly?: boolean;
|
||||
showMiniMap?: boolean;
|
||||
showLineNumbers?: boolean;
|
||||
|
||||
/**
|
||||
* Callback after the editor has mounted that gives you raw access to monaco
|
||||
*
|
||||
* @experimental - real type is: monaco.editor.IStandaloneCodeEditor
|
||||
*/
|
||||
onEditorDidMount?: (editor: any) => void;
|
||||
|
||||
/** Handler to be performed when editor is blurred */
|
||||
onBlur?: CodeEditorChangeHandler;
|
||||
|
||||
/** Handler to be performed when Cmd/Ctrl+S is pressed */
|
||||
onSave?: CodeEditorChangeHandler;
|
||||
|
||||
/**
|
||||
* Language agnostic suggestion completions -- typically for template variables
|
||||
*/
|
||||
getSuggestions?: CodeEditorSuggestionProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export enum CodeEditorSuggestionItemKind {
|
||||
Method = 'method',
|
||||
Field = 'field',
|
||||
Property = 'property',
|
||||
Constant = 'constant',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface CodeEditorSuggestionItem {
|
||||
/**
|
||||
* The label of this completion item. By default
|
||||
* this is also the text that is inserted when selecting
|
||||
* this completion.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The kind of this completion item. An icon is chosen
|
||||
* by the editor based on the kind.
|
||||
*/
|
||||
kind?: CodeEditorSuggestionItemKind;
|
||||
|
||||
/**
|
||||
* A human-readable string with additional information
|
||||
* about this item, like type or symbol information.
|
||||
*/
|
||||
detail?: string;
|
||||
|
||||
/**
|
||||
* A human-readable string that represents a doc-comment.
|
||||
*/
|
||||
documentation?: string; // | IMarkdownString;
|
||||
|
||||
/**
|
||||
* A string or snippet that should be inserted in a document when selecting
|
||||
* this completion. When `falsy` the `label` is used.
|
||||
*/
|
||||
insertText?: string;
|
||||
}
|
17
packages/grafana-ui/src/components/Monaco/utils.ts
Normal file
17
packages/grafana-ui/src/components/Monaco/utils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { VariableSuggestion } from '@grafana/data';
|
||||
import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from './types';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export function variableSuggestionToCodeEditorSuggestion(sug: VariableSuggestion): CodeEditorSuggestionItem {
|
||||
const label = '${' + sug.value + '}';
|
||||
const detail = sug.value === sug.label ? sug.origin : `${sug.label} / ${sug.origin}`;
|
||||
|
||||
return {
|
||||
label,
|
||||
kind: CodeEditorSuggestionItemKind.Property,
|
||||
detail,
|
||||
documentation: sug.documentation,
|
||||
};
|
||||
}
|
@ -34,7 +34,11 @@ export { FilterPill } from './FilterPill/FilterPill';
|
||||
|
||||
export { ConfirmModal } from './ConfirmModal/ConfirmModal';
|
||||
export { QueryField } from './QueryField/QueryField';
|
||||
|
||||
// Code editor
|
||||
export { CodeEditor } from './Monaco/CodeEditorLazy';
|
||||
export { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from './Monaco/types';
|
||||
export { variableSuggestionToCodeEditorSuggestion } from './Monaco/utils';
|
||||
|
||||
// TODO: namespace
|
||||
export { Modal } from './Modal/Modal';
|
||||
|
@ -5,11 +5,13 @@ import {
|
||||
PanelOptionsEditorItem,
|
||||
PanelPlugin,
|
||||
StandardEditorContext,
|
||||
VariableSuggestionsScope,
|
||||
} from '@grafana/data';
|
||||
import { get as lodashGet, set as lodashSet } from 'lodash';
|
||||
import { Field, Label } from '@grafana/ui';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { OptionsGroup } from './OptionsGroup';
|
||||
import { getPanelOptionsVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
|
||||
interface PanelOptionsEditorProps<TOptions> {
|
||||
plugin: PanelPlugin;
|
||||
@ -38,9 +40,12 @@ export const PanelOptionsEditor: React.FC<PanelOptionsEditorProps<any>> = ({
|
||||
};
|
||||
|
||||
const context: StandardEditorContext<any> = {
|
||||
data: data ?? [],
|
||||
data: data || [],
|
||||
replaceVariables,
|
||||
options,
|
||||
getSuggestions: (scope?: VariableSuggestionsScope) => {
|
||||
return getPanelOptionsVariableSuggestions(plugin, data);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
urlUtil,
|
||||
textUtil,
|
||||
DataLink,
|
||||
PanelPlugin,
|
||||
} from '@grafana/data';
|
||||
|
||||
const timeRangeVars = [
|
||||
@ -231,6 +232,18 @@ export const getCalculationValueDataLinksVariableSuggestions = (dataFrames: Data
|
||||
return [...seriesVars, ...fieldVars, ...valueVars, valueCalcVar, ...getPanelLinksVariableSuggestions()];
|
||||
};
|
||||
|
||||
export const getPanelOptionsVariableSuggestions = (plugin: PanelPlugin, data?: DataFrame[]): VariableSuggestion[] => {
|
||||
const dataVariables = plugin.meta.skipDataQuery ? [] : getDataFrameVars(data || []);
|
||||
return [
|
||||
...dataVariables, // field values
|
||||
...templateSrv.getVariables().map(variable => ({
|
||||
value: variable.name as string,
|
||||
label: variable.name,
|
||||
origin: VariableOrigin.Template,
|
||||
})),
|
||||
];
|
||||
};
|
||||
|
||||
export interface LinkService {
|
||||
getDataLinkUIModel: <T>(link: DataLink, scopedVars: ScopedVars, origin: T) => LinkModel<T>;
|
||||
getAnchorInfo: (link: any) => any;
|
||||
|
@ -1,7 +1,13 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { CodeEditor, stylesFactory, useTheme } from '@grafana/ui';
|
||||
import {
|
||||
CodeEditor,
|
||||
stylesFactory,
|
||||
useTheme,
|
||||
CodeEditorSuggestionItem,
|
||||
variableSuggestionToCodeEditorSuggestion,
|
||||
} from '@grafana/ui';
|
||||
import { GrafanaTheme, StandardEditorProps } from '@grafana/data';
|
||||
|
||||
import { TextOptions } from './types';
|
||||
@ -10,6 +16,14 @@ export const TextPanelEditor: FC<StandardEditorProps<string, any, TextOptions>>
|
||||
const language = useMemo(() => context.options?.mode ?? 'markdown', [context]);
|
||||
const theme = useTheme();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
const getSuggestions = (): CodeEditorSuggestionItem[] => {
|
||||
if (!context.getSuggestions) {
|
||||
return [];
|
||||
}
|
||||
return context.getSuggestions().map(v => variableSuggestionToCodeEditorSuggestion(v));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cx(styles.editorBox)}>
|
||||
<AutoSizer disableHeight>
|
||||
@ -17,7 +31,6 @@ export const TextPanelEditor: FC<StandardEditorProps<string, any, TextOptions>>
|
||||
if (width === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
value={value}
|
||||
@ -28,6 +41,7 @@ export const TextPanelEditor: FC<StandardEditorProps<string, any, TextOptions>>
|
||||
showMiniMap={false}
|
||||
showLineNumbers={false}
|
||||
height="200px"
|
||||
getSuggestions={getSuggestions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -80,7 +80,7 @@ module.exports = {
|
||||
'!cursorUndo',
|
||||
'!dnd',
|
||||
'!find',
|
||||
'!folding',
|
||||
'folding',
|
||||
'!fontZoom',
|
||||
'!format',
|
||||
'!gotoError',
|
||||
@ -93,14 +93,14 @@ module.exports = {
|
||||
'!linesOperations',
|
||||
'!links',
|
||||
'!multicursor',
|
||||
'!parameterHints',
|
||||
'parameterHints',
|
||||
'!quickCommand',
|
||||
'!quickOutline',
|
||||
'!referenceSearch',
|
||||
'!rename',
|
||||
'!smartSelect',
|
||||
'!snippets',
|
||||
'!suggest',
|
||||
'suggest',
|
||||
'!toggleHighContrast',
|
||||
'!toggleTabFocusMode',
|
||||
'!transpose',
|
||||
|
Loading…
Reference in New Issue
Block a user