[Vis: Default editor] Reactify the timelion editor (#52990) (#54329)

* Reactify timelion editor

* Change translation ids

* Add @types/pegjs into renovate.json5

* Add validation, add hover suggestions

* Style fixes

* Change plugin setup, use kibana context

* Change plugin start

* Mock services

* Fix other comments

* Build renovate config

* Fix some classnames and SASS file structure

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Caroline Horn <549577+cchaos@users.noreply.github.com>

# Conflicts:
#	renovate.json5
This commit is contained in:
Daniil Suleiman 2020-01-09 13:00:19 +03:00 committed by GitHub
parent 7479e2b487
commit 34dbe7139b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 875 additions and 93 deletions

View file

@ -336,6 +336,7 @@
"@types/mustache": "^0.8.31",
"@types/node": "^10.12.27",
"@types/opn": "^5.1.0",
"@types/pegjs": "^0.10.1",
"@types/pngjs": "^3.3.2",
"@types/podium": "^1.0.0",
"@types/prop-types": "^15.5.3",

View file

@ -500,6 +500,14 @@
'@types/opn',
],
},
{
groupSlug: 'pegjs',
groupName: 'pegjs related packages',
packageNames: [
'pegjs',
'@types/pegjs',
],
},
{
groupSlug: 'pngjs',
groupName: 'pngjs related packages',

View file

@ -0,0 +1,46 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null';
interface TimelionFunctionArgsSuggestion {
name: string;
help: string;
}
export interface TimelionFunctionArgs {
name: string;
help?: string;
multi?: boolean;
types: TimelionFunctionArgsTypes[];
suggestions?: TimelionFunctionArgsSuggestion[];
}
export interface ITimelionFunction {
aliases: string[];
args: TimelionFunctionArgs[];
name: string;
help: string;
chainable: boolean;
extended: boolean;
isAlias: boolean;
argsByName: {
[key: string]: TimelionFunctionArgs[];
};
}

View file

@ -0,0 +1 @@
@import './timelion_expression_input';

View file

@ -0,0 +1,18 @@
.timExpressionInput {
flex: 1 1 auto;
display: flex;
flex-direction: column;
margin-top: $euiSize;
}
.timExpressionInput__editor {
height: 100%;
padding-top: $euiSizeS;
}
@include euiBreakpoint('xs', 's', 'm') {
.timExpressionInput__editor {
height: $euiSize * 15;
max-height: $euiSize * 15;
}
}

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './timelion_expression_input';
export * from './timelion_interval';

View file

@ -0,0 +1,146 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useCallback, useRef, useMemo } from 'react';
import { EuiFormLabel } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public';
import { suggest, getSuggestion } from './timelion_expression_input_helpers';
import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
import { getArgValueSuggestions } from '../services/arg_value_suggestions';
const LANGUAGE_ID = 'timelion_expression';
monacoEditor.languages.register({ id: LANGUAGE_ID });
interface TimelionExpressionInputProps {
value: string;
setValue(value: string): void;
}
function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputProps) {
const functionList = useRef([]);
const kibana = useKibana();
const argValueSuggestions = useMemo(getArgValueSuggestions, []);
const provideCompletionItems = useCallback(
async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
const text = model.getValue();
const wordUntil = model.getWordUntilPosition(position);
const wordRange = new monacoEditor.Range(
position.lineNumber,
wordUntil.startColumn,
position.lineNumber,
wordUntil.endColumn
);
const suggestions = await suggest(
text,
functionList.current,
// it's important to offset the cursor position on 1 point left
// because of PEG parser starts the line with 0, but monaco with 1
position.column - 1,
argValueSuggestions
);
return {
suggestions: suggestions
? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) =>
getSuggestion(s, suggestions.type, wordRange)
)
: [],
};
},
[argValueSuggestions]
);
const provideHover = useCallback(
async (model: monacoEditor.editor.ITextModel, position: monacoEditor.Position) => {
const suggestions = await suggest(
model.getValue(),
functionList.current,
// it's important to offset the cursor position on 1 point left
// because of PEG parser starts the line with 0, but monaco with 1
position.column - 1,
argValueSuggestions
);
return {
contents: suggestions
? suggestions.list.map((s: ITimelionFunction | TimelionFunctionArgs) => ({
value: s.help,
}))
: [],
};
},
[argValueSuggestions]
);
useEffect(() => {
if (kibana.services.http) {
kibana.services.http.get('../api/timelion/functions').then(data => {
functionList.current = data;
});
}
}, [kibana.services.http]);
return (
<div className="timExpressionInput">
<EuiFormLabel>
<FormattedMessage id="timelion.vis.expressionLabel" defaultMessage="Timelion expression" />
</EuiFormLabel>
<div className="timExpressionInput__editor">
<CodeEditor
languageId={LANGUAGE_ID}
value={value}
onChange={setValue}
suggestionProvider={{
triggerCharacters: ['.', ',', '(', '=', ':'],
provideCompletionItems,
}}
hoverProvider={{ provideHover }}
options={{
fixedOverflowWidgets: true,
fontSize: 14,
folding: false,
lineNumbers: 'off',
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
wordBasedSuggestions: false,
wordWrap: 'on',
wrappingIndent: 'indent',
}}
languageConfiguration={{
autoClosingPairs: [
{
open: '(',
close: ')',
},
],
}}
/>
</div>
</div>
);
}
export { TimelionExpressionInput };

View file

@ -0,0 +1,287 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { get, startsWith } from 'lodash';
import PEG from 'pegjs';
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
// @ts-ignore
import grammar from 'raw-loader!../chain.peg';
import { i18n } from '@kbn/i18n';
import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
import { ArgValueSuggestions, FunctionArg, Location } from '../services/arg_value_suggestions';
const Parser = PEG.generate(grammar);
export enum SUGGESTION_TYPE {
ARGUMENTS = 'arguments',
ARGUMENT_VALUE = 'argument_value',
FUNCTIONS = 'functions',
}
function inLocation(cursorPosition: number, location: Location) {
return cursorPosition >= location.min && cursorPosition <= location.max;
}
function getArgumentsHelp(
functionHelp: ITimelionFunction | undefined,
functionArgs: FunctionArg[] = []
) {
if (!functionHelp) {
return [];
}
// Do not provide 'inputSeries' as argument suggestion for chainable functions
const argsHelp = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0);
// ignore arguments that are already provided in function declaration
const functionArgNames = functionArgs.map(arg => arg.name);
return argsHelp.filter(arg => !functionArgNames.includes(arg.name));
}
async function extractSuggestionsFromParsedResult(
result: ReturnType<typeof Parser.parse>,
cursorPosition: number,
functionList: ITimelionFunction[],
argValueSuggestions: ArgValueSuggestions
) {
const activeFunc = result.functions.find(({ location }: { location: Location }) =>
inLocation(cursorPosition, location)
);
if (!activeFunc) {
return;
}
const functionHelp = functionList.find(({ name }) => name === activeFunc.function);
if (!functionHelp) {
return;
}
// return function suggestion when cursor is outside of parentheses
// location range includes '.', function name, and '('.
const openParen = activeFunc.location.min + activeFunc.function.length + 2;
if (cursorPosition < openParen) {
return { list: [functionHelp], type: SUGGESTION_TYPE.FUNCTIONS };
}
// return argument value suggestions when cursor is inside argument value
const activeArg = activeFunc.arguments.find((argument: FunctionArg) => {
return inLocation(cursorPosition, argument.location);
});
if (
activeArg &&
activeArg.type === 'namedArg' &&
inLocation(cursorPosition, activeArg.value.location)
) {
const { function: functionName, arguments: functionArgs } = activeFunc;
const {
name: argName,
value: { text: partialInput },
} = activeArg;
let valueSuggestions;
if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
functionName,
argName,
functionArgs,
partialInput
);
} else {
const { suggestions: staticSuggestions } =
functionHelp.args.find(arg => arg.name === activeArg.name) || {};
valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput(
partialInput,
staticSuggestions
);
}
return {
list: valueSuggestions,
type: SUGGESTION_TYPE.ARGUMENT_VALUE,
};
}
// return argument suggestions
const argsHelp = getArgumentsHelp(functionHelp, activeFunc.arguments);
const argumentSuggestions = argsHelp.filter(arg => {
if (get(activeArg, 'type') === 'namedArg') {
return startsWith(arg.name, activeArg.name);
} else if (activeArg) {
return startsWith(arg.name, activeArg.text);
}
return true;
});
return { list: argumentSuggestions, type: SUGGESTION_TYPE.ARGUMENTS };
}
export async function suggest(
expression: string,
functionList: ITimelionFunction[],
cursorPosition: number,
argValueSuggestions: ArgValueSuggestions
) {
try {
const result = await Parser.parse(expression);
return await extractSuggestionsFromParsedResult(
result,
cursorPosition,
functionList,
argValueSuggestions
);
} catch (err) {
let message: any;
try {
// The grammar will throw an error containing a message if the expression is formatted
// correctly and is prepared to accept suggestions. If the expression is not formatted
// correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
// attempt will throw an error.
message = JSON.parse(err.message);
} catch (e) {
// The expression isn't correctly formatted, so JSON.parse threw an error.
return;
}
switch (message.type) {
case 'incompleteFunction': {
let list;
if (message.function) {
// The user has start typing a function name, so we'll filter the list down to only
// possible matches.
list = functionList.filter(func => startsWith(func.name, message.function));
} else {
// The user hasn't typed anything yet, so we'll just return the entire list.
list = functionList;
}
return { list, type: SUGGESTION_TYPE.FUNCTIONS };
}
case 'incompleteArgument': {
const { currentFunction: functionName, currentArgs: functionArgs } = message;
const functionHelp = functionList.find(func => func.name === functionName);
return {
list: getArgumentsHelp(functionHelp, functionArgs),
type: SUGGESTION_TYPE.ARGUMENTS,
};
}
case 'incompleteArgumentValue': {
const { name: argName, currentFunction: functionName, currentArgs: functionArgs } = message;
let valueSuggestions = [];
if (argValueSuggestions.hasDynamicSuggestionsForArgument(functionName, argName)) {
valueSuggestions = await argValueSuggestions.getDynamicSuggestionsForArgument(
functionName,
argName,
functionArgs
);
} else {
const functionHelp = functionList.find(func => func.name === functionName);
if (functionHelp) {
const argHelp = functionHelp.args.find(arg => arg.name === argName);
if (argHelp && argHelp.suggestions) {
valueSuggestions = argHelp.suggestions;
}
}
}
return {
list: valueSuggestions,
type: SUGGESTION_TYPE.ARGUMENT_VALUE,
};
}
}
}
}
export function getSuggestion(
suggestion: ITimelionFunction | TimelionFunctionArgs,
type: SUGGESTION_TYPE,
range: monacoEditor.Range
): monacoEditor.languages.CompletionItem {
let kind: monacoEditor.languages.CompletionItemKind =
monacoEditor.languages.CompletionItemKind.Method;
let insertText: string = suggestion.name;
let insertTextRules: monacoEditor.languages.CompletionItem['insertTextRules'];
let detail: string = '';
let command: monacoEditor.languages.CompletionItem['command'];
switch (type) {
case SUGGESTION_TYPE.ARGUMENTS:
command = {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
};
kind = monacoEditor.languages.CompletionItemKind.Property;
insertText = `${insertText}=`;
detail = `${i18n.translate(
'timelion.expressionSuggestions.argument.description.acceptsText',
{
defaultMessage: 'Accepts',
}
)}: ${(suggestion as TimelionFunctionArgs).types}`;
break;
case SUGGESTION_TYPE.FUNCTIONS:
command = {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
};
kind = monacoEditor.languages.CompletionItemKind.Function;
insertText = `${insertText}($0)`;
insertTextRules = monacoEditor.languages.CompletionItemInsertTextRule.InsertAsSnippet;
detail = `(${
(suggestion as ITimelionFunction).chainable
? i18n.translate('timelion.expressionSuggestions.func.description.chainableHelpText', {
defaultMessage: 'Chainable',
})
: i18n.translate('timelion.expressionSuggestions.func.description.dataSourceHelpText', {
defaultMessage: 'Data source',
})
})`;
break;
case SUGGESTION_TYPE.ARGUMENT_VALUE:
const param = suggestion.name.split(':');
if (param.length === 1 || param[1]) {
insertText = `${param.length === 1 ? insertText : param[1]},`;
}
command = {
title: 'Trigger Suggestion Dialog',
id: 'editor.action.triggerSuggest',
};
kind = monacoEditor.languages.CompletionItemKind.Property;
detail = suggestion.help || '';
break;
}
return {
detail,
insertText,
insertTextRules,
kind,
label: suggestion.name,
documentation: suggestion.help,
command,
range,
};
}

View file

@ -0,0 +1,144 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useMemo, useCallback } from 'react';
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useValidation } from 'ui/vis/editors/default/controls/agg_utils';
import { isValidEsInterval } from '../../../../core_plugins/data/common';
const intervalOptions = [
{
label: i18n.translate('timelion.vis.interval.auto', {
defaultMessage: 'Auto',
}),
value: 'auto',
},
{
label: i18n.translate('timelion.vis.interval.second', {
defaultMessage: '1 second',
}),
value: '1s',
},
{
label: i18n.translate('timelion.vis.interval.minute', {
defaultMessage: '1 minute',
}),
value: '1m',
},
{
label: i18n.translate('timelion.vis.interval.hour', {
defaultMessage: '1 hour',
}),
value: '1h',
},
{
label: i18n.translate('timelion.vis.interval.day', {
defaultMessage: '1 day',
}),
value: '1d',
},
{
label: i18n.translate('timelion.vis.interval.week', {
defaultMessage: '1 week',
}),
value: '1w',
},
{
label: i18n.translate('timelion.vis.interval.month', {
defaultMessage: '1 month',
}),
value: '1M',
},
{
label: i18n.translate('timelion.vis.interval.year', {
defaultMessage: '1 year',
}),
value: '1y',
},
];
interface TimelionIntervalProps {
value: string;
setValue(value: string): void;
setValidity(valid: boolean): void;
}
function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProps) {
const onCustomInterval = useCallback(
(customValue: string) => {
setValue(customValue.trim());
},
[setValue]
);
const onChange = useCallback(
(opts: Array<EuiComboBoxOptionProps<string>>) => {
setValue((opts[0] && opts[0].value) || '');
},
[setValue]
);
const selectedOptions = useMemo(
() => [intervalOptions.find(op => op.value === value) || { label: value, value }],
[value]
);
const isValid = intervalOptions.some(int => int.value === value) || isValidEsInterval(value);
useValidation(setValidity, isValid);
return (
<EuiFormRow
compressed
fullWidth
helpText={i18n.translate('timelion.vis.selectIntervalHelpText', {
defaultMessage:
'Select an option or create a custom value. Examples: 30s, 20m, 24h, 2d, 1w, 1M',
})}
isInvalid={!isValid}
error={
!isValid &&
i18n.translate('timelion.vis.invalidIntervalErrorMessage', {
defaultMessage: 'Invalid interval format.',
})
}
label={i18n.translate('timelion.vis.intervalLabel', {
defaultMessage: 'Interval',
})}
>
<EuiComboBox
compressed
fullWidth
isInvalid={!isValid}
onChange={onChange}
onCreateOption={onCustomInterval}
options={intervalOptions}
selectedOptions={selectedOptions}
singleSelection={{ asPlainText: true }}
placeholder={i18n.translate('timelion.vis.selectIntervalPlaceholder', {
defaultMessage: 'Select an interval',
})}
/>
</EuiFormRow>
);
}
export { TimelionInterval };

View file

@ -21,9 +21,15 @@ import expect from '@kbn/expect';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../chain.peg';
import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers';
import { ArgValueSuggestionsProvider } from '../timelion_expression_suggestions/arg_value_suggestions';
import { getArgValueSuggestions } from '../../services/arg_value_suggestions';
import { setIndexPatterns, setSavedObjectsClient } from '../../services/plugin_services';
describe('Timelion expression suggestions', () => {
setIndexPatterns({});
setSavedObjectsClient({});
const argValueSuggestions = getArgValueSuggestions();
describe('getSuggestions', () => {
const func1 = {
name: 'func1',
@ -44,11 +50,6 @@ describe('Timelion expression suggestions', () => {
};
const functionList = [func1, myFunc2];
let Parser;
const privateStub = () => {
return {};
};
const indexPatternsStub = {};
const argValueSuggestions = ArgValueSuggestionsProvider(privateStub, indexPatternsStub); // eslint-disable-line new-cap
beforeEach(function() {
Parser = PEG.generate(grammar);
});

View file

@ -52,11 +52,11 @@ import {
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from '@elastic/eui';
import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions';
import { getArgValueSuggestions } from '../services/arg_value_suggestions';
const Parser = PEG.generate(grammar);
export function TimelionExpInput($http, $timeout, Private) {
export function TimelionExpInput($http, $timeout) {
return {
restrict: 'E',
scope: {
@ -68,7 +68,7 @@ export function TimelionExpInput($http, $timeout, Private) {
replace: true,
template: timelionExpressionInputTemplate,
link: function(scope, elem) {
const argValueSuggestions = Private(ArgValueSuggestionsProvider);
const argValueSuggestions = getArgValueSuggestions();
const expressionInput = elem.find('[data-expression-input]');
const functionReference = {};
let suggestibleFunctionLocation = {};

View file

@ -11,5 +11,6 @@
// timChart__legend-isLoading
@import './app';
@import './components/index';
@import './directives/index';
@import './vis/index';

View file

@ -37,4 +37,4 @@ const setupPlugins: Readonly<TimelionPluginSetupDependencies> = {
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
export const start = pluginInstance.start(npStart.core);
export const start = pluginInstance.start(npStart.core, npStart.plugins);

View file

@ -35,6 +35,7 @@ const DEBOUNCE_DELAY = 50;
export function timechartFn(dependencies: TimelionVisualizationDependencies) {
const { $rootScope, $compile, uiSettings } = dependencies;
return function() {
return {
help: 'Draw a timeseries chart',

View file

@ -26,12 +26,14 @@ import {
} from 'kibana/public';
import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public';
import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public';
import { PluginsStart } from 'ui/new_platform/new_platform';
import { VisualizationsSetup } from '../../visualizations/public/np_ready/public';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisualization } from './vis';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim';
import { setIndexPatterns, setSavedObjectsClient } from './services/plugin_services';
/** @internal */
export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup {
@ -85,12 +87,15 @@ export class TimelionPlugin implements Plugin<Promise<void>, void> {
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
public start(core: CoreStart) {
public start(core: CoreStart, plugins: PluginsStart) {
const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled');
if (timelionUiEnabled === false) {
core.chrome.navLinks.update('timelion', { hidden: true });
}
setIndexPatterns(plugins.data.indexPatterns);
setSavedObjectsClient(core.savedObjects.client);
}
public stop(): void {}

View file

@ -17,33 +17,51 @@
* under the License.
*/
import _ from 'lodash';
import { npStart } from 'ui/new_platform';
import { get } from 'lodash';
import { TimelionFunctionArgs } from '../../common/types';
import { getIndexPatterns, getSavedObjectsClient } from './plugin_services';
export function ArgValueSuggestionsProvider() {
const { indexPatterns } = npStart.plugins.data;
const { client: savedObjectsClient } = npStart.core.savedObjects;
export interface Location {
min: number;
max: number;
}
async function getIndexPattern(functionArgs) {
const indexPatternArg = functionArgs.find(argument => {
return argument.name === 'index';
});
export interface FunctionArg {
function: string;
location: Location;
name: string;
text: string;
type: string;
value: {
location: Location;
text: string;
type: string;
value: string;
};
}
export function getArgValueSuggestions() {
const indexPatterns = getIndexPatterns();
const savedObjectsClient = getSavedObjectsClient();
async function getIndexPattern(functionArgs: FunctionArg[]) {
const indexPatternArg = functionArgs.find(({ name }) => name === 'index');
if (!indexPatternArg) {
// index argument not provided
return;
}
const indexPatternTitle = _.get(indexPatternArg, 'value.text');
const indexPatternTitle = get(indexPatternArg, 'value.text');
const resp = await savedObjectsClient.find({
const { savedObjects } = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title'],
search: `"${indexPatternTitle}"`,
search_fields: ['title'],
searchFields: ['title'],
perPage: 10,
});
const indexPatternSavedObject = resp.savedObjects.find(savedObject => {
return savedObject.attributes.title === indexPatternTitle;
});
const indexPatternSavedObject = savedObjects.find(
({ attributes }) => attributes.title === indexPatternTitle
);
if (!indexPatternSavedObject) {
// index argument does not match an index pattern
return;
@ -52,7 +70,7 @@ export function ArgValueSuggestionsProvider() {
return await indexPatterns.get(indexPatternSavedObject.id);
}
function containsFieldName(partial, field) {
function containsFieldName(partial: string, field: { name: string }) {
if (!partial) {
return true;
}
@ -63,13 +81,13 @@ export function ArgValueSuggestionsProvider() {
// Could not put with function definition since functions are defined on server
const customHandlers = {
es: {
index: async function(partial) {
async index(partial: string) {
const search = partial ? `${partial}*` : '*';
const resp = await savedObjectsClient.find({
type: 'index-pattern',
fields: ['title', 'type'],
search: `${search}`,
search_fields: ['title'],
searchFields: ['title'],
perPage: 25,
});
return resp.savedObjects
@ -78,7 +96,7 @@ export function ArgValueSuggestionsProvider() {
return { name: savedObject.attributes.title };
});
},
metric: async function(partial, functionArgs) {
async metric(partial: string, functionArgs: FunctionArg[]) {
if (!partial || !partial.includes(':')) {
return [
{ name: 'avg:' },
@ -109,7 +127,7 @@ export function ArgValueSuggestionsProvider() {
return { name: `${valueSplit[0]}:${field.name}`, help: field.type };
});
},
split: async function(partial, functionArgs) {
async split(partial: string, functionArgs: FunctionArg[]) {
const indexPattern = await getIndexPattern(functionArgs);
if (!indexPattern) {
return [];
@ -127,7 +145,7 @@ export function ArgValueSuggestionsProvider() {
return { name: field.name, help: field.type };
});
},
timefield: async function(partial, functionArgs) {
async timefield(partial: string, functionArgs: FunctionArg[]) {
const indexPattern = await getIndexPattern(functionArgs);
if (!indexPattern) {
return [];
@ -150,7 +168,10 @@ export function ArgValueSuggestionsProvider() {
* @param {string} argName - user provided argument name
* @return {boolean} true when dynamic suggestion handler provided for function argument
*/
hasDynamicSuggestionsForArgument: (functionName, argName) => {
hasDynamicSuggestionsForArgument: <T extends keyof typeof customHandlers>(
functionName: T,
argName: keyof typeof customHandlers[T]
) => {
return customHandlers[functionName] && customHandlers[functionName][argName];
},
@ -161,12 +182,13 @@ export function ArgValueSuggestionsProvider() {
* @param {string} partial - user provided argument value
* @return {array} array of dynamic suggestions matching partial
*/
getDynamicSuggestionsForArgument: async (
functionName,
argName,
functionArgs,
getDynamicSuggestionsForArgument: async <T extends keyof typeof customHandlers>(
functionName: T,
argName: keyof typeof customHandlers[T],
functionArgs: FunctionArg[],
partialInput = ''
) => {
// @ts-ignore
return await customHandlers[functionName][argName](partialInput, functionArgs);
},
@ -175,7 +197,10 @@ export function ArgValueSuggestionsProvider() {
* @param {array} staticSuggestions - argument value suggestions
* @return {array} array of static suggestions matching partial
*/
getStaticSuggestionsForInput: (partialInput = '', staticSuggestions = []) => {
getStaticSuggestionsForInput: (
partialInput = '',
staticSuggestions: TimelionFunctionArgs['suggestions'] = []
) => {
if (partialInput) {
return staticSuggestions.filter(suggestion => {
return suggestion.name.includes(partialInput);
@ -186,3 +211,5 @@ export function ArgValueSuggestionsProvider() {
},
};
}
export type ArgValueSuggestions = ReturnType<typeof getArgValueSuggestions>;

View file

@ -0,0 +1,30 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { IndexPatternsContract } from 'src/plugins/data/public';
import { SavedObjectsClientContract } from 'kibana/public';
import { createGetterSetter } from '../../../../../plugins/kibana_utils/public';
export const [getIndexPatterns, setIndexPatterns] = createGetterSetter<IndexPatternsContract>(
'IndexPatterns'
);
export const [getSavedObjectsClient, setSavedObjectsClient] = createGetterSetter<
SavedObjectsClientContract
>('SavedObjectsClient');

View file

@ -28,7 +28,7 @@ const name = 'timelion_vis';
interface Arguments {
expression: string;
interval: any;
interval: string;
}
interface RenderValue {
@ -38,7 +38,7 @@ interface RenderValue {
}
type Context = KibanaContext | null;
type VisParams = Arguments;
export type VisParams = Arguments;
type Return = Promise<Render<RenderValue>>;
export const getTimelionVisualizationConfig = (
@ -60,7 +60,7 @@ export const getTimelionVisualizationConfig = (
help: '',
},
interval: {
types: ['string', 'null'],
types: ['string'],
default: 'auto',
help: '',
},

View file

@ -1 +1,2 @@
@import './timelion_vis';
@import './timelion_editor';

View file

@ -0,0 +1,15 @@
.visEditor--timelion {
vis-options-react-wrapper,
.visEditorSidebar__options,
.visEditorSidebar__timelionOptions {
flex: 1 1 auto;
display: flex;
flex-direction: column;
}
.visEditor__sidebar {
@include euiBreakpoint('xs', 's', 'm') {
width: 100%;
}
}
}

View file

@ -17,19 +17,24 @@
* under the License.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { DefaultEditorSize } from 'ui/vis/editor_size';
import { VisOptionsProps } from 'ui/vis/editors/default';
import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public';
import { getTimelionRequestHandler } from './timelion_request_handler';
import visConfigTemplate from './timelion_vis.html';
import editorConfigTemplate from './timelion_vis_params.html';
import { TimelionVisualizationDependencies } from '../plugin';
// @ts-ignore
import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type';
import { TimelionOptions } from './timelion_options';
import { VisParams } from '../timelion_vis_fn';
export const TIMELION_VIS_NAME = 'timelion';
export function getTimelionVisualization(dependencies: TimelionVisualizationDependencies) {
const { http, uiSettings } = dependencies;
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
// return the visType object, which kibana will use to display and configure new
@ -50,7 +55,11 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe
template: visConfigTemplate,
},
editorConfig: {
optionsTemplate: editorConfigTemplate,
optionsTemplate: (props: VisOptionsProps<VisParams>) => (
<KibanaContextProvider services={{ uiSettings, http }}>
<TimelionOptions {...props} />
</KibanaContextProvider>
),
defaultSize: DefaultEditorSize.MEDIUM,
},
requestHandler: timelionRequestHandler,

View file

@ -0,0 +1,48 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback } from 'react';
import { EuiPanel } from '@elastic/eui';
import { VisOptionsProps } from 'ui/vis/editors/default';
import { VisParams } from '../timelion_vis_fn';
import { TimelionInterval, TimelionExpressionInput } from '../components';
function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps<VisParams>) {
const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [
setValue,
]);
const setExpressionInput = useCallback(
(value: VisParams['expression']) => setValue('expression', value),
[setValue]
);
return (
<EuiPanel className="visEditorSidebar__timelionOptions" paddingSize="s">
<TimelionInterval
value={stateParams.interval}
setValue={setInterval}
setValidity={setValidity}
/>
<TimelionExpressionInput value={stateParams.expression} setValue={setExpressionInput} />
</EuiPanel>
);
}
export { TimelionOptions };

View file

@ -1,27 +0,0 @@
<div class="visEditorSidebar__section">
<div class="form-group">
<label
for="timelionInterval"
i18n-id="timelion.vis.intervalLabel"
i18n-default-message="Interval"
></label>
<div class="form-group">
<timelion-interval model="editorState.params.interval"></timelion-interval>
</div>
</div>
<div class="form-group">
<div>
<label
i18n-id="timelion.vis.expressionLabel"
i18n-default-message="Timelion Expression"
></label>
</div>
<timelion-expression-input
sheet="editorState.params.expression"
rows="9"
></timelion-expression-input>
</div>
</div>

View file

@ -17,6 +17,8 @@
* under the License.
*/
import { TimelionFunctionArgs } from '../../../common/types';
export interface TimelionFunctionInterface extends TimelionFunctionConfig {
chainable: boolean;
originalFn: Function;
@ -32,21 +34,6 @@ export interface TimelionFunctionConfig {
args: TimelionFunctionArgs[];
}
export interface TimelionFunctionArgs {
name: string;
help?: string;
multi?: boolean;
types: TimelionFunctionArgsTypes[];
suggestions?: TimelionFunctionArgsSuggestion[];
}
export type TimelionFunctionArgsTypes = 'seriesList' | 'number' | 'string' | 'boolean' | 'null';
export interface TimelionFunctionArgsSuggestion {
name: string;
help: string;
}
// eslint-disable-next-line import/no-default-export
export default class TimelionFunction {
constructor(name: string, config: TimelionFunctionConfig);

View file

@ -17,12 +17,5 @@
* under the License.
*/
export {
TimelionFunctionInterface,
TimelionFunctionConfig,
TimelionFunctionArgs,
TimelionFunctionArgsSuggestion,
TimelionFunctionArgsTypes,
} from './lib/classes/timelion_function';
export { TimelionFunctionInterface, TimelionFunctionConfig } from './lib/classes/timelion_function';
export { TimelionRequestQuery } from './routes/run';

View file

@ -78,6 +78,13 @@ export interface Props {
*/
hoverProvider?: monacoEditor.languages.HoverProvider;
/**
* Language config provider for bracket
* Documentation for the provider can be found here:
* https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.languageconfiguration.html
*/
languageConfiguration?: monacoEditor.languages.LanguageConfiguration;
/**
* Function called before the editor is mounted in the view
*/
@ -130,6 +137,13 @@ export class CodeEditor extends React.Component<Props, {}> {
if (this.props.hoverProvider) {
monaco.languages.registerHoverProvider(this.props.languageId, this.props.hoverProvider);
}
if (this.props.languageConfiguration) {
monaco.languages.setLanguageConfiguration(
this.props.languageId,
this.props.languageConfiguration
);
}
});
// Register the theme

View file

@ -4434,6 +4434,11 @@
resolved "https://registry.yarnpkg.com/@types/parse-link-header/-/parse-link-header-1.0.0.tgz#69f059e40a0fa93dc2e095d4142395ae6adc5d7a"
integrity sha512-fCA3btjE7QFeRLfcD0Sjg+6/CnmC66HpMBoRfRzd2raTaWMJV21CCZ0LO8MOqf8onl5n0EPfjq4zDhbyX8SVwA==
"@types/pegjs@^0.10.1":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@types/pegjs/-/pegjs-0.10.1.tgz#9a2f3961dc62430fdb21061eb0ddbd890f9e3b94"
integrity sha512-ra8IchO9odGQmYKbm+94K58UyKCEKdZh9y0vxhG4pIpOJOBlC1C+ZtBVr6jLs+/oJ4pl+1p/4t3JtBA8J10Vvw==
"@types/pngjs@^3.3.2":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/pngjs/-/pngjs-3.3.2.tgz#8ed3bd655ab3a92ea32ada7a21f618e63b93b1d4"