mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Timelion typeahead for argument values (#14801)
* timelion argument value suggestions for legend function * update functions with argument value suggestions * use async/await instead of promise resolve/reject for suggestion generation * lookup value suggestions for es index and timefield arguments * custom handlers for es metric and split arguments * update suggestions unit test * remove duplicate code from insertSuggestion switch * refactor arg_value_suggestions to provide three methods instead of single getSuggetions method * template literal * update es index argument desc to include note about type ahead support
This commit is contained in:
parent
a3014d7a63
commit
94b2b324aa
15 changed files with 452 additions and 120 deletions
|
@ -28,6 +28,7 @@ export default function (kibana) {
|
|||
};
|
||||
},
|
||||
uses: [
|
||||
'fieldFormats',
|
||||
'savedObjectTypes'
|
||||
]
|
||||
},
|
||||
|
|
|
@ -18,6 +18,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
var currentFunction;
|
||||
var currentArgs = [];
|
||||
|
||||
var functions = [];
|
||||
var args = [];
|
||||
var variables = {};
|
||||
|
@ -40,18 +43,22 @@ arg_list
|
|||
}
|
||||
|
||||
argument
|
||||
= name:function_name space? '=' space? value:arg_type {
|
||||
return {
|
||||
= name:argument_name space? '=' space? value:arg_type {
|
||||
var arg = {
|
||||
type: 'namedArg',
|
||||
name: name,
|
||||
value: value,
|
||||
location: simpleLocation(location()),
|
||||
text: text()
|
||||
}
|
||||
};
|
||||
currentArgs.push(arg);
|
||||
return arg;
|
||||
}
|
||||
/ name:function_name space? '=' {
|
||||
/ name:argument_name space? '=' {
|
||||
var exception = {
|
||||
type: 'incompleteArgument',
|
||||
currentArgs: currentArgs,
|
||||
currentFunction: currentFunction,
|
||||
name: name,
|
||||
location: simpleLocation(location()),
|
||||
text: text()
|
||||
|
@ -71,7 +78,7 @@ arg_type
|
|||
}
|
||||
|
||||
variable_get
|
||||
= '$' name:function_name {
|
||||
= '$' name:argument_name {
|
||||
if (variables[name]) {
|
||||
return variables[name];
|
||||
} else {
|
||||
|
@ -80,7 +87,7 @@ variable_get
|
|||
}
|
||||
|
||||
variable_set
|
||||
= '$' name:function_name space? '=' space? value:arg_type {
|
||||
= '$' name:argument_name space? '=' space? value:arg_type {
|
||||
variables[name] = value;
|
||||
}
|
||||
|
||||
|
@ -97,6 +104,13 @@ series
|
|||
}
|
||||
|
||||
function_name
|
||||
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* {
|
||||
currentFunction = first.join('') + rest.join('');
|
||||
currentArgs = [];
|
||||
return currentFunction;
|
||||
}
|
||||
|
||||
argument_name
|
||||
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* { return first.join('') + rest.join('') }
|
||||
|
||||
function "function"
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
SUGGESTION_TYPE,
|
||||
suggest
|
||||
} from '../timelion_expression_input_helpers';
|
||||
import { ArgValueSuggestionsProvider } from '../timelion_expression_suggestions/arg_value_suggestions';
|
||||
|
||||
describe('Timelion expression suggestions', () => {
|
||||
|
||||
|
@ -15,7 +16,10 @@ describe('Timelion expression suggestions', () => {
|
|||
args: [
|
||||
{ name: 'inputSeries' },
|
||||
{ name: 'argA' },
|
||||
{ name: 'argAB' }
|
||||
{
|
||||
name: 'argAB',
|
||||
suggestions: [{ name: 'value1' }]
|
||||
}
|
||||
]
|
||||
};
|
||||
const myFunc2 = {
|
||||
|
@ -29,6 +33,13 @@ 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.buildParser(grammar);
|
||||
});
|
||||
|
@ -39,7 +50,7 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should return function suggestions', async () => {
|
||||
const expression = '.';
|
||||
const cursorPosition = 1;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [func1, myFunc2],
|
||||
'location': {
|
||||
|
@ -52,7 +63,7 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should filter function suggestions by function name', async () => {
|
||||
const expression = '.myF';
|
||||
const cursorPosition = 4;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [myFunc2],
|
||||
'location': {
|
||||
|
@ -65,15 +76,29 @@ describe('Timelion expression suggestions', () => {
|
|||
});
|
||||
|
||||
describe('incompleteArgument', () => {
|
||||
it('should return argument value suggestions', async () => {
|
||||
it('should return no argument value suggestions when not provided by help', async () => {
|
||||
const expression = '.func1(argA=)';
|
||||
const cursorPosition = 11;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [],
|
||||
'location': {
|
||||
'min': 7,
|
||||
'max': 12
|
||||
'min': 11,
|
||||
'max': 11
|
||||
},
|
||||
'type': 'argument_value'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return argument value suggestions when provided by help', async () => {
|
||||
const expression = '.func1(argAB=)';
|
||||
const cursorPosition = 11;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [{ name: 'value1' }],
|
||||
'location': {
|
||||
'min': 11,
|
||||
'max': 11
|
||||
},
|
||||
'type': 'argument_value'
|
||||
});
|
||||
|
@ -87,7 +112,7 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should return function suggestion', async () => {
|
||||
const expression = '.func1()';
|
||||
const cursorPosition = 1;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [func1],
|
||||
'location': {
|
||||
|
@ -104,7 +129,7 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should return argument suggestions', async () => {
|
||||
const expression = '.myFunc2()';
|
||||
const cursorPosition = 9;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': myFunc2.args,
|
||||
'location': {
|
||||
|
@ -117,7 +142,7 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should not provide argument suggestions for argument that is all ready set in function def', async () => {
|
||||
const expression = '.myFunc2(argAB=provided,)';
|
||||
const cursorPosition = 24;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions.type).to.equal(SUGGESTION_TYPE.ARGUMENTS);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [{ name: 'argA' }, { name: 'argABC' }],
|
||||
|
@ -131,7 +156,7 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should filter argument suggestions by argument name', async () => {
|
||||
const expression = '.myFunc2(argAB,)';
|
||||
const cursorPosition = 14;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [{ name: 'argAB' }, { name: 'argABC' }],
|
||||
'location': {
|
||||
|
@ -144,9 +169,9 @@ describe('Timelion expression suggestions', () => {
|
|||
it('should not show first argument for chainable functions', async () => {
|
||||
const expression = '.func1()';
|
||||
const cursorPosition = 7;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [{ name: 'argA' }, { name: 'argAB' }],
|
||||
'list': [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }],
|
||||
'location': {
|
||||
'min': 7,
|
||||
'max': 7
|
||||
|
@ -156,10 +181,10 @@ describe('Timelion expression suggestions', () => {
|
|||
});
|
||||
});
|
||||
describe('cursor in argument value', () => {
|
||||
it('should return argument value suggestions', async () => {
|
||||
it('should return no argument value suggestions when not provided by help', async () => {
|
||||
const expression = '.myFunc2(argA=42)';
|
||||
const cursorPosition = 14;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition);
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [],
|
||||
'location': {
|
||||
|
@ -169,6 +194,20 @@ describe('Timelion expression suggestions', () => {
|
|||
'type': 'argument_value'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return no argument value suggestions when provided by help', async () => {
|
||||
const expression = '.func1(argAB=val)';
|
||||
const cursorPosition = 16;
|
||||
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
|
||||
expect(suggestions).to.eql({
|
||||
'list': [{ name: 'value1' }],
|
||||
'location': {
|
||||
'min': 13,
|
||||
'max': 16
|
||||
},
|
||||
'type': 'argument_value'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,11 +35,12 @@ import {
|
|||
insertAtLocation,
|
||||
} from './timelion_expression_input_helpers';
|
||||
import { comboBoxKeyCodes } from 'ui_framework/services';
|
||||
import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions';
|
||||
|
||||
const Parser = PEG.buildParser(grammar);
|
||||
const app = require('ui/modules').get('apps/timelion', []);
|
||||
|
||||
app.directive('timelionExpressionInput', function ($document, $http, $interval, $timeout) {
|
||||
app.directive('timelionExpressionInput', function ($document, $http, $interval, $timeout, Private) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
|
@ -51,6 +52,7 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
|
|||
replace: true,
|
||||
template: timelionExpressionInputTemplate,
|
||||
link: function (scope, elem) {
|
||||
const argValueSuggestions = Private(ArgValueSuggestionsProvider);
|
||||
const expressionInput = elem.find('[data-expression-input]');
|
||||
const functionReference = {};
|
||||
let suggestibleFunctionLocation = {};
|
||||
|
@ -81,35 +83,36 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
|
|||
return;
|
||||
}
|
||||
|
||||
const { min, max } = suggestibleFunctionLocation;
|
||||
let insertedValue;
|
||||
let insertPositionMinOffset = 0;
|
||||
|
||||
switch (scope.suggestions.type) {
|
||||
case SUGGESTION_TYPE.FUNCTIONS: {
|
||||
const functionName = `${scope.suggestions.list[suggestionIndex].name}()`;
|
||||
const { min, max } = suggestibleFunctionLocation;
|
||||
|
||||
// Update the expression with the function.
|
||||
// min advanced one to not replace function '.'
|
||||
const updatedExpression = insertAtLocation(functionName, scope.sheet, min + 1, max);
|
||||
scope.sheet = updatedExpression;
|
||||
|
||||
// Position the caret inside of the function parentheses.
|
||||
const newCaretOffset = min + functionName.length;
|
||||
setCaretOffset(newCaretOffset);
|
||||
insertedValue = `${scope.suggestions.list[suggestionIndex].name}()`;
|
||||
|
||||
// min advanced one to not replace function '.'
|
||||
insertPositionMinOffset = 1;
|
||||
break;
|
||||
}
|
||||
case SUGGESTION_TYPE.ARGUMENTS: {
|
||||
const argumentName = `${scope.suggestions.list[suggestionIndex].name}=`;
|
||||
const { min, max } = suggestibleFunctionLocation;
|
||||
|
||||
// Update the expression with the function.
|
||||
const updatedExpression = insertAtLocation(argumentName, scope.sheet, min, max);
|
||||
scope.sheet = updatedExpression;
|
||||
|
||||
// Position the caret after the '='
|
||||
const newCaretOffset = min + argumentName.length;
|
||||
setCaretOffset(newCaretOffset);
|
||||
insertedValue = `${scope.suggestions.list[suggestionIndex].name}=`;
|
||||
break;
|
||||
}
|
||||
case SUGGESTION_TYPE.ARGUMENT_VALUE: {
|
||||
// Position the caret after the argument value
|
||||
insertedValue = `${scope.suggestions.list[suggestionIndex].name}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedExpression = insertAtLocation(insertedValue, scope.sheet, min + insertPositionMinOffset, max);
|
||||
scope.sheet = updatedExpression;
|
||||
|
||||
const newCaretOffset = min + insertedValue.length;
|
||||
setCaretOffset(newCaretOffset);
|
||||
}
|
||||
|
||||
function scrollToSuggestionAt(index) {
|
||||
|
@ -127,15 +130,18 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
|
|||
return null;
|
||||
}
|
||||
|
||||
function getSuggestions() {
|
||||
suggest(
|
||||
async function getSuggestions() {
|
||||
const suggestions = await suggest(
|
||||
scope.sheet,
|
||||
functionReference.list,
|
||||
Parser,
|
||||
getCursorPosition()
|
||||
).then(suggestions => {
|
||||
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
|
||||
scope.$apply(() => {
|
||||
getCursorPosition(),
|
||||
argValueSuggestions
|
||||
);
|
||||
|
||||
// We're using ES6 Promises, not $q, so we have to wrap this in $apply.
|
||||
scope.$apply(() => {
|
||||
if (suggestions) {
|
||||
scope.suggestions.setList(suggestions.list, suggestions.type);
|
||||
scope.suggestions.show();
|
||||
suggestibleFunctionLocation = suggestions.location;
|
||||
|
@ -143,12 +149,11 @@ app.directive('timelionExpressionInput', function ($document, $http, $interval,
|
|||
const suggestionsList = $('[data-suggestions-list]');
|
||||
suggestionsList.scrollTop(0);
|
||||
}, 0);
|
||||
});
|
||||
}, (noSuggestions = {}) => {
|
||||
scope.$apply(() => {
|
||||
suggestibleFunctionLocation = noSuggestions.location;
|
||||
scope.suggestions.reset();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
suggestibleFunctionLocation = undefined;
|
||||
scope.suggestions.reset();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ function inLocation(cursorPosition, location) {
|
|||
return cursorPosition >= location.min && cursorPosition <= location.max;
|
||||
}
|
||||
|
||||
function extractSuggestionsFromParsedResult(result, cursorPosition, functionList) {
|
||||
async function extractSuggestionsFromParsedResult(result, cursorPosition, functionList, argValueSuggestions) {
|
||||
const activeFunc = result.functions.find((func) => {
|
||||
return cursorPosition >= func.location.min && cursorPosition < func.location.max;
|
||||
});
|
||||
|
@ -82,32 +82,52 @@ function extractSuggestionsFromParsedResult(result, cursorPosition, functionList
|
|||
return;
|
||||
}
|
||||
|
||||
const funcDefinition = functionList.find((func) => {
|
||||
const functionHelp = functionList.find((func) => {
|
||||
return func.name === activeFunc.function;
|
||||
});
|
||||
const providedArguments = activeFunc.arguments.map((arg) => {
|
||||
return arg.name;
|
||||
});
|
||||
|
||||
// return function suggestion if cursor is outside of parentheses
|
||||
// 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: [funcDefinition], location: activeFunc.location, type: SUGGESTION_TYPE.FUNCTIONS };
|
||||
return { list: [functionHelp], location: activeFunc.location, type: SUGGESTION_TYPE.FUNCTIONS };
|
||||
}
|
||||
|
||||
// Do not provide 'inputSeries' as argument suggestion for chainable functions
|
||||
const args = funcDefinition.chainable ? funcDefinition.args.slice(1) : funcDefinition.args.slice(0);
|
||||
|
||||
// return argument value suggestions when cursor is inside agrument value
|
||||
const activeArg = activeFunc.arguments.find((argument) => {
|
||||
return inLocation(cursorPosition, argument.location);
|
||||
});
|
||||
// return argument_value suggestions when cursor is inside agrument value
|
||||
if (activeArg && activeArg.type === 'namedArg' && inLocation(cursorPosition, activeArg.value.location)) {
|
||||
// TODO - provide argument value suggestions once function list contains required data
|
||||
return { list: [], location: activeArg.value.location, type: SUGGESTION_TYPE.ARGUMENT_VALUE };
|
||||
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) => {
|
||||
return arg.name === activeArg.name;
|
||||
});
|
||||
valueSuggestions = argValueSuggestions.getStaticSuggestionsForInput(partialInput, staticSuggestions);
|
||||
}
|
||||
return { list: valueSuggestions, location: activeArg.value.location, type: SUGGESTION_TYPE.ARGUMENT_VALUE };
|
||||
}
|
||||
|
||||
// return argument suggestions
|
||||
const providedArguments = activeFunc.arguments.map((arg) => {
|
||||
return arg.name;
|
||||
});
|
||||
// Do not provide 'inputSeries' as argument suggestion for chainable functions
|
||||
const args = functionHelp.chainable ? functionHelp.args.slice(1) : functionHelp.args.slice(0);
|
||||
const argumentSuggestions = args.filter(arg => {
|
||||
// ignore arguments that are all ready provided in function declaration
|
||||
if (providedArguments.includes(arg.name)) {
|
||||
|
@ -125,51 +145,63 @@ function extractSuggestionsFromParsedResult(result, cursorPosition, functionList
|
|||
return { list: argumentSuggestions, location: location, type: SUGGESTION_TYPE.ARGUMENTS };
|
||||
}
|
||||
|
||||
export function suggest(expression, functionList, Parser, cursorPosition) {
|
||||
return new Promise((resolve, reject) => {
|
||||
export async function suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions) {
|
||||
try {
|
||||
const result = await Parser.parse(expression);
|
||||
return await extractSuggestionsFromParsedResult(result, cursorPosition, functionList, argValueSuggestions);
|
||||
} catch (e) {
|
||||
|
||||
let message;
|
||||
try {
|
||||
// We rely on the grammar to throw an error in order to suggest function(s).
|
||||
const result = Parser.parse(expression);
|
||||
|
||||
const suggestions = extractSuggestionsFromParsedResult(result, cursorPosition, functionList);
|
||||
if (suggestions) {
|
||||
return resolve(suggestions);
|
||||
}
|
||||
|
||||
return reject();
|
||||
// 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 formmated
|
||||
// correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
|
||||
// attempt will throw an error.
|
||||
message = JSON.parse(e.message);
|
||||
} catch (e) {
|
||||
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 formmated
|
||||
// correctly the grammar will just throw a regular PEG SyntaxError, and this JSON.parse
|
||||
// attempt will throw an error.
|
||||
const message = JSON.parse(e.message);
|
||||
const location = message.location;
|
||||
// The expression isn't correctly formatted, so JSON.parse threw an error.
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === '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 resolve({ list, location, type: SUGGESTION_TYPE.FUNCTIONS });
|
||||
} else if (message.type === 'incompleteArgument') {
|
||||
// TODO - provide argument value suggestions once function list contains required data
|
||||
return resolve({ list: [], location, type: SUGGESTION_TYPE.ARGUMENT_VALUE });
|
||||
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;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// The expression isn't correctly formatted, so JSON.parse threw an error.
|
||||
return reject();
|
||||
return { list, location: message.location, type: SUGGESTION_TYPE.FUNCTIONS };
|
||||
}
|
||||
case 'incompleteArgument': {
|
||||
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,
|
||||
location: { min: cursorPosition, max: cursorPosition },
|
||||
type: SUGGESTION_TYPE.ARGUMENT_VALUE
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function insertAtLocation(valueToInsert, destination, replacementRangeStart, replacementRangeEnd) {
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
import _ from 'lodash';
|
||||
import { SavedObjectsClientProvider } from 'ui/saved_objects';
|
||||
|
||||
export function ArgValueSuggestionsProvider(Private, indexPatterns) {
|
||||
|
||||
const savedObjectsClient = Private(SavedObjectsClientProvider);
|
||||
|
||||
async function getIndexPattern(functionArgs) {
|
||||
const indexPatternArg = functionArgs.find(argument => {
|
||||
return argument.name === 'index';
|
||||
});
|
||||
if (!indexPatternArg) {
|
||||
// index argument not provided
|
||||
return;
|
||||
}
|
||||
const indexPatternTitle = _.get(indexPatternArg, 'value.text');
|
||||
|
||||
const resp = await savedObjectsClient.find({
|
||||
type: 'index-pattern',
|
||||
fields: ['title'],
|
||||
search: `"${indexPatternTitle}"`,
|
||||
search_fields: ['title'],
|
||||
perPage: 10
|
||||
});
|
||||
const indexPatternSavedObject = resp.savedObjects.find(savedObject => {
|
||||
return savedObject.attributes.title === indexPatternTitle;
|
||||
});
|
||||
if (!indexPatternSavedObject) {
|
||||
// index argument does not match an index pattern
|
||||
return;
|
||||
}
|
||||
|
||||
return await indexPatterns.get(indexPatternSavedObject.id);
|
||||
}
|
||||
|
||||
function containsFieldName(partial, field) {
|
||||
if (!partial) {
|
||||
return true;
|
||||
}
|
||||
return field.name.includes(partial);
|
||||
}
|
||||
|
||||
// Argument value suggestion handlers requiring custom client side code
|
||||
// Could not put with function definition since functions are defined on server
|
||||
const customHandlers = {
|
||||
es: {
|
||||
index: async function (partial) {
|
||||
const search = partial ? `${partial}*` : '*';
|
||||
const resp = await savedObjectsClient.find({
|
||||
type: 'index-pattern',
|
||||
fields: ['title'],
|
||||
search: `${search}`,
|
||||
search_fields: ['title'],
|
||||
perPage: 25
|
||||
});
|
||||
return resp.savedObjects.map(savedObject => {
|
||||
return { name: savedObject.attributes.title };
|
||||
});
|
||||
},
|
||||
metric: async function (partial, functionArgs) {
|
||||
if (!partial || !partial.includes(':')) {
|
||||
return [
|
||||
{ name: 'avg:' },
|
||||
{ name: 'cardinality:' },
|
||||
{ name: 'count' },
|
||||
{ name: 'max:' },
|
||||
{ name: 'min:' },
|
||||
{ name: 'sum:' }
|
||||
];
|
||||
}
|
||||
|
||||
const indexPattern = await getIndexPattern(functionArgs);
|
||||
if (!indexPattern) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const valueSplit = partial.split(':');
|
||||
return indexPattern.fields
|
||||
.filter(field => {
|
||||
return field.aggregatable && 'number' === field.type && containsFieldName(valueSplit[1], field);
|
||||
})
|
||||
.map(field => {
|
||||
return { name: `${valueSplit[0]}:${field.name}`, help: field.type };
|
||||
});
|
||||
|
||||
},
|
||||
split: async function (partial, functionArgs) {
|
||||
const indexPattern = await getIndexPattern(functionArgs);
|
||||
if (!indexPattern) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return indexPattern.fields
|
||||
.filter(field => {
|
||||
return field.aggregatable
|
||||
&& ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type)
|
||||
&& containsFieldName(partial, field);
|
||||
})
|
||||
.map(field => {
|
||||
return { name: field.name, help: field.type };
|
||||
});
|
||||
},
|
||||
timefield: async function (partial, functionArgs) {
|
||||
const indexPattern = await getIndexPattern(functionArgs);
|
||||
if (!indexPattern) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return indexPattern.fields
|
||||
.filter(field => {
|
||||
return 'date' === field.type && containsFieldName(partial, field);
|
||||
})
|
||||
.map(field => {
|
||||
return { name: field.name };
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
/**
|
||||
* @param {string} functionName - user provided function name containing argument
|
||||
* @param {string} argName - user provided argument name
|
||||
* @return {boolean} true when dynamic suggestion handler provided for function argument
|
||||
*/
|
||||
hasDynamicSuggestionsForArgument: (functionName, argName) => {
|
||||
return (customHandlers[functionName] && customHandlers[functionName][argName]);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} functionName - user provided function name containing argument
|
||||
* @param {string} argName - user provided argument name
|
||||
* @param {object} functionArgs - user provided function arguments parsed ahead of current argument
|
||||
* @param {string} partial - user provided argument value
|
||||
* @return {array} array of dynamic suggestions matching partial
|
||||
*/
|
||||
getDynamicSuggestionsForArgument: async (functionName, argName, functionArgs, partialInput = '') => {
|
||||
return await customHandlers[functionName][argName](partialInput, functionArgs);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} partial - user provided argument value
|
||||
* @param {array} staticSuggestions - arugment value suggestions
|
||||
* @return {array} array of static suggestions matching partial
|
||||
*/
|
||||
getStaticSuggestionsForInput: (partialInput = '', staticSuggestions = []) => {
|
||||
if (partialInput) {
|
||||
return staticSuggestions.filter(suggestion => {
|
||||
return suggestion.name.includes(partialInput);
|
||||
});
|
||||
}
|
||||
|
||||
return staticSuggestions;
|
||||
},
|
||||
};
|
||||
}
|
|
@ -70,6 +70,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="argument_value">
|
||||
<h4>
|
||||
<strong>{{suggestion.name}}</strong>
|
||||
<small id="timelionSuggestionDescription{{$index}}">
|
||||
{{suggestion.help}}
|
||||
</small>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,8 +12,34 @@ export default new Chainable('condition', {
|
|||
{
|
||||
name: 'operator', // <, <=, >, >=, ==, !=
|
||||
types: ['string'],
|
||||
help: 'Operator to use for comparison, valid operators are eq (equal), ne (not equal), lt (less than), lte ' +
|
||||
'(less than equal), gt (greater than), gte (greater than equal)'
|
||||
help: 'comparision operator to use for comparison, valid operators are eq (equal), ne (not equal), lt (less than), lte ' +
|
||||
'(less than equal), gt (greater than), gte (greater than equal)',
|
||||
suggestions: [
|
||||
{
|
||||
name: 'eq',
|
||||
help: 'equal',
|
||||
},
|
||||
{
|
||||
name: 'ne',
|
||||
help: 'not equal'
|
||||
},
|
||||
{
|
||||
name: 'lt',
|
||||
help: 'less than'
|
||||
},
|
||||
{
|
||||
name: 'lte',
|
||||
help: 'less than equal'
|
||||
},
|
||||
{
|
||||
name: 'gt',
|
||||
help: 'greater than'
|
||||
},
|
||||
{
|
||||
name: 'gte',
|
||||
help: 'greater than equal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'if',
|
||||
|
|
|
@ -27,7 +27,8 @@ export default new Datasource('es', {
|
|||
{
|
||||
name: 'index',
|
||||
types: ['string', 'null'],
|
||||
help: 'Index to query, wildcards accepted. Provide Index Pattern name for scripted field support.'
|
||||
help: 'Index to query, wildcards accepted. Provide Index Pattern name for scripted fields and ' +
|
||||
'field name type ahead suggestions for metrics, split, and timefield arguments.'
|
||||
},
|
||||
{
|
||||
name: 'timefield',
|
||||
|
|
|
@ -13,7 +13,10 @@ export default new Chainable('fit', {
|
|||
{
|
||||
name: 'mode',
|
||||
types: ['string'],
|
||||
help: 'The algorithm to use for fitting the series to the target. One of: ' + _.keys(fitFunctions).join(', ')
|
||||
help: `The algorithm to use for fitting the series to the target. One of: ${_.keys(fitFunctions).join(', ')}`,
|
||||
suggestions: _.keys(fitFunctions).map(key => {
|
||||
return { name: key };
|
||||
})
|
||||
}
|
||||
],
|
||||
help: 'Fills null values using a defined fit function',
|
||||
|
|
|
@ -11,7 +11,29 @@ export default new Chainable('legend', {
|
|||
{
|
||||
name: 'position',
|
||||
types: ['string', 'boolean', 'null'],
|
||||
help: 'Corner to place the legend in: nw, ne, se, or sw. You can also pass false to disable the legend'
|
||||
help: 'Corner to place the legend in: nw, ne, se, or sw. You can also pass false to disable the legend',
|
||||
suggestions: [
|
||||
{
|
||||
name: 'false',
|
||||
help: 'disable legend',
|
||||
},
|
||||
{
|
||||
name: 'nw',
|
||||
help: 'place legend in north west corner'
|
||||
},
|
||||
{
|
||||
name: 'ne',
|
||||
help: 'place legend in north east corner'
|
||||
},
|
||||
{
|
||||
name: 'se',
|
||||
help: 'place legend in south east corner'
|
||||
},
|
||||
{
|
||||
name: 'sw',
|
||||
help: 'place legend in south west corner'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
|
|
|
@ -3,6 +3,9 @@ import _ from 'lodash';
|
|||
import Chainable from '../lib/classes/chainable';
|
||||
import toMS from '../lib/to_milliseconds.js';
|
||||
|
||||
const validPositions = ['left', 'right', 'center'];
|
||||
const defaultPosition = 'center';
|
||||
|
||||
export default new Chainable('movingaverage', {
|
||||
args: [
|
||||
{
|
||||
|
@ -19,7 +22,14 @@ export default new Chainable('movingaverage', {
|
|||
{
|
||||
name: 'position',
|
||||
types: ['string', 'null'],
|
||||
help: 'Position of the averaged points relative to the result time. Options are left, right, and center (default).'
|
||||
help: `Position of the averaged points relative to the result time. One of: ${validPositions.join(', ')}`,
|
||||
suggestions: validPositions.map(position => {
|
||||
const suggestion = { name: position };
|
||||
if (position === defaultPosition) {
|
||||
suggestion.help = 'default';
|
||||
}
|
||||
return suggestion;
|
||||
})
|
||||
}
|
||||
],
|
||||
aliases: ['mvavg'],
|
||||
|
@ -39,8 +49,7 @@ export default new Chainable('movingaverage', {
|
|||
_window = Math.round(windowMilliseconds / intervalMilliseconds) || 1;
|
||||
}
|
||||
|
||||
_position = _position || 'center';
|
||||
const validPositions = ['left', 'right', 'center'];
|
||||
_position = _position || defaultPosition;
|
||||
if (!_.contains(validPositions, _position)) throw new Error('Valid positions are: ' + validPositions.join(', '));
|
||||
|
||||
const pairs = eachSeries.data;
|
||||
|
|
|
@ -2,6 +2,9 @@ import alter from '../lib/alter.js';
|
|||
import _ from 'lodash';
|
||||
import Chainable from '../lib/classes/chainable';
|
||||
|
||||
const validSymbols = ['triangle', 'cross', 'square', 'diamond', 'circle'];
|
||||
const defaultSymbol = 'circle';
|
||||
|
||||
export default new Chainable('points', {
|
||||
args: [
|
||||
{
|
||||
|
@ -30,8 +33,15 @@ export default new Chainable('points', {
|
|||
},
|
||||
{
|
||||
name: 'symbol',
|
||||
help: 'cross, circle, triangle, square or diamond',
|
||||
types: ['string', 'null']
|
||||
help: `point symbol. One of: ${validSymbols.join(', ')}`,
|
||||
types: ['string', 'null'],
|
||||
suggestions: validSymbols.map(symbol => {
|
||||
const suggestion = { name: symbol };
|
||||
if (symbol === defaultSymbol) {
|
||||
suggestion.help = 'default';
|
||||
}
|
||||
return suggestion;
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'show',
|
||||
|
@ -57,9 +67,8 @@ export default new Chainable('points', {
|
|||
eachSeries.points.lineWidth = weight;
|
||||
}
|
||||
|
||||
symbol = symbol || 'circle';
|
||||
const validSymbols = ['triangle', 'cross', 'square', 'diamond', 'circle'];
|
||||
if (!_.contains(['triangle', 'cross', 'square', 'diamond', 'circle'], symbol)) {
|
||||
symbol = symbol || defaultSymbol;
|
||||
if (!_.contains(validSymbols, symbol)) {
|
||||
throw new Error('Valid symbols are: ' + validSymbols.join(', '));
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,10 @@ export default new Chainable('trend', {
|
|||
{
|
||||
name: 'mode',
|
||||
types: ['string'],
|
||||
help: 'The algorithm to use for generating the trend line. One of: ' + _.keys(validRegressions).join(', ')
|
||||
help: `The algorithm to use for generating the trend line. One of: ${_.keys(validRegressions).join(', ')}`,
|
||||
suggestions: _.keys(validRegressions).map(key => {
|
||||
return { name: key, help: validRegressions[key] };
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'start',
|
||||
|
|
|
@ -50,7 +50,10 @@ export default new Chainable('yaxis', {
|
|||
{
|
||||
name: 'units',
|
||||
types: ['string', 'null'],
|
||||
help: 'The function to use for formatting y-axis labels. One of: ' + _.values(tickFormatters).join(', ')
|
||||
help: `The function to use for formatting y-axis labels. One of: ${_.values(tickFormatters).join(', ')}`,
|
||||
suggestions: _.keys(tickFormatters).map(key => {
|
||||
return { name: key, help: tickFormatters[key] };
|
||||
})
|
||||
},
|
||||
{
|
||||
name: 'tickDecimals',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue