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:
Nathan Reese 2017-11-14 10:56:16 -07:00 committed by GitHub
parent a3014d7a63
commit 94b2b324aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 452 additions and 120 deletions

View file

@ -28,6 +28,7 @@ export default function (kibana) {
};
},
uses: [
'fieldFormats',
'savedObjectTypes'
]
},

View file

@ -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"

View file

@ -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'
});
});
});
});
});

View file

@ -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();
});
}

View file

@ -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) {

View file

@ -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;
},
};
}

View file

@ -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>

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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',

View file

@ -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;

View file

@ -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(', '));
}

View file

@ -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',

View file

@ -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',