[timelion] provide argument suggestions when argument name not provided (#15081)

* get wiring done for timelion application functional tests

* add tests for expression type ahead functions, arguments, and argument values

* provide argument suggestions when argument name not provided

* updates from cjcenizal review
This commit is contained in:
Nathan Reese 2017-11-28 08:51:11 -07:00 committed by GitHub
parent 50a53729ef
commit 074d7cef8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 535 additions and 14 deletions

View file

@ -54,11 +54,21 @@ argument
currentArgs.push(arg);
return arg;
}
/ name:argument_name space? '=' {
/ space? '=' space? value:arg_type? {
var exception = {
type: 'incompleteArgument',
currentArgs: currentArgs,
currentFunction: currentFunction,
location: simpleLocation(location()),
text: text()
}
error(JSON.stringify(exception));
}
/ name:argument_name space? '=' {
var exception = {
type: 'incompleteArgumentValue',
currentArgs: currentArgs,
currentFunction: currentFunction,
name: name,
location: simpleLocation(location()),
text: text()

View file

@ -75,7 +75,79 @@ describe('Timelion expression suggestions', () => {
});
});
describe('incompleteArgument', () => {
describe('no argument name provided', () => {
it('should return no argument suggestions when none provided by help', async () => {
const expression = '.otherFunc(=)';
const cursorPosition = 0;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [],
'location': {
'min': 11,
'max': 12
},
'type': 'arguments'
});
});
it('should return argument suggestions when provided by help', async () => {
const expression = '.myFunc2(=)';
const cursorPosition = 0;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': myFunc2.args,
'location': {
'min': 9,
'max': 10
},
'type': 'arguments'
});
});
it('should return argument suggestions when argument value provided', async () => {
const expression = '.myFunc2(=whatArgumentAmI)';
const cursorPosition = 0;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': myFunc2.args,
'location': {
'min': 9,
'max': 25
},
'type': 'arguments'
});
});
it('should not show first argument for chainable functions', async () => {
const expression = '.func1(=)';
const cursorPosition = 0;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }],
'location': {
'min': 7,
'max': 8
},
'type': 'arguments'
});
});
it('should not provide argument suggestions for argument that is all ready set in function def', async () => {
const expression = '.myFunc2(argAB=provided,=)';
const cursorPosition = 0;
const suggestions = await suggest(expression, functionList, Parser, cursorPosition, argValueSuggestions);
expect(suggestions).to.eql({
'list': [{ name: 'argA' }, { name: 'argABC' }],
'location': {
'min': 24,
'max': 25
},
'type': 'arguments'
});
});
});
describe('no argument value provided', () => {
it('should return no argument value suggestions when not provided by help', async () => {
const expression = '.func1(argA=)';
const cursorPosition = 11;

View file

@ -9,7 +9,6 @@
For some reasons it doesn't work without it (even though the default role of
the element is textbox anyway). -->
<textarea
id="timelionExpressionTextArea"
data-expression-input
role="textbox"
rows="{{ rows }}"
@ -28,6 +27,7 @@
aria-autocomplete="list"
aria-controls="timelionSuggestionList"
aria-activedescendant="{{ getActiveSuggestionId() }}"
data-test-subj="timelionExpressionTextArea"
></textarea>
<timelion-expression-suggestions

View file

@ -73,6 +73,23 @@ function inLocation(cursorPosition, location) {
return cursorPosition >= location.min && cursorPosition <= location.max;
}
function getArgumentsHelp(functionHelp, functionArgs = []) {
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) => {
return arg.name;
});
return argsHelp.filter(arg => {
return !functionArgNames.includes(arg.name);
});
}
async function extractSuggestionsFromParsedResult(result, cursorPosition, functionList, argValueSuggestions) {
const activeFunc = result.functions.find((func) => {
return cursorPosition >= func.location.min && cursorPosition < func.location.max;
@ -123,17 +140,8 @@ async function extractSuggestionsFromParsedResult(result, cursorPosition, functi
}
// 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)) {
return false;
}
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) {
@ -177,6 +185,18 @@ export async function suggest(expression, functionList, Parser, cursorPosition,
return { list, location: message.location, 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),
location: message.location,
type: SUGGESTION_TYPE.ARGUMENTS
};
}
case 'incompleteArgumentValue': {
const {
name: argName,
currentFunction: functionName,

View file

@ -0,0 +1,96 @@
import expect from 'expect.js';
export default function ({ getPageObjects }) {
const PageObjects = getPageObjects(['common', 'timelion', 'header', 'settings']);
describe('expression typeahead', () => {
before(async () => {
const fromTime = '2015-09-19 06:31:44.000';
const toTime = '2015-09-23 18:31:44.000';
await PageObjects.timelion.initTests();
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.header.waitUntilLoadingHasFinished();
});
it('should display function suggestions filtered by function name', async () => {
await PageObjects.timelion.setExpression('.e');
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(suggestions.length).to.eql(2);
expect(suggestions[0].includes('.elasticsearch()')).to.eql(true);
expect(suggestions[1].includes('.es()')).to.eql(true);
});
it('should show argument suggestions when function suggestion is selected', async () => {
await PageObjects.timelion.setExpression('.es');
await PageObjects.timelion.clickSuggestion();
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(suggestions.length).to.eql(9);
expect(suggestions[0].includes('fit=')).to.eql(true);
});
it('should show argument value suggestions when argument is selected', async () => {
await PageObjects.timelion.setExpression('.legend');
await PageObjects.timelion.clickSuggestion();
const argumentSuggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(argumentSuggestions.length).to.eql(4);
expect(argumentSuggestions[1].includes('position=')).to.eql(true);
await PageObjects.timelion.clickSuggestion(1);
const valueSuggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(valueSuggestions.length).to.eql(5);
expect(valueSuggestions[0].includes('disable legend')).to.eql(true);
expect(valueSuggestions[1].includes('place legend in north east corner')).to.eql(true);
});
describe('dynamic suggestions for argument values', () => {
describe('.es()', () => {
before(async () => {
await PageObjects.timelion.setExpression('.es');
await PageObjects.timelion.clickSuggestion();
});
it('should show index pattern suggestions for index argument', async () => {
await PageObjects.timelion.updateExpression('index');
await PageObjects.timelion.clickSuggestion();
await PageObjects.common.sleep(500);
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(suggestions.length).to.eql(1);
expect(suggestions[0].includes('logstash-*')).to.eql(true);
await PageObjects.timelion.clickSuggestion();
});
it('should show field suggestions for timefield argument when index pattern set', async () => {
await PageObjects.timelion.updateExpression(',timefield');
await PageObjects.timelion.clickSuggestion();
await PageObjects.common.sleep(500);
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(suggestions.length).to.eql(4);
expect(suggestions[0].includes('@timestamp')).to.eql(true);
await PageObjects.timelion.clickSuggestion();
});
it('should show field suggestions for split argument when index pattern set', async () => {
await PageObjects.timelion.updateExpression(',split');
await PageObjects.timelion.clickSuggestion();
await PageObjects.common.sleep(500);
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(suggestions.length).to.eql(52);
expect(suggestions[0].includes('@message.raw')).to.eql(true);
await PageObjects.timelion.clickSuggestion(10);
});
it('should show field suggestions for metric argument when index pattern set', async () => {
await PageObjects.timelion.updateExpression(',metric');
await PageObjects.timelion.clickSuggestion();
await PageObjects.common.sleep(500);
await PageObjects.timelion.updateExpression('avg:');
await PageObjects.timelion.clickSuggestion();
const suggestions = await PageObjects.timelion.getSuggestionItemsText();
expect(suggestions.length).to.eql(2);
expect(suggestions[0].includes('avg:bytes')).to.eql(true);
});
});
});
});
}

View file

@ -0,0 +1,17 @@
export default function ({ getService, loadTestFile }) {
const remote = getService('remote');
const log = getService('log');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
describe('timelion app', function () {
before(async function () {
log.debug('Starting timelion before method');
remote.setWindowSize(1280, 800);
await esArchiver.loadIfNeeded('logstash_functional');
await kibanaServer.uiSettings.replace({ 'dateFormat:tz': 'UTC', 'defaultIndex': 'logstash-*' });
});
loadTestFile(require.resolve('./_expression_typeahead'));
});
}

View file

@ -11,6 +11,7 @@ import {
MonitoringPageProvider,
PointSeriesPageProvider,
VisualBuilderPageProvider,
TimelionPageProvider,
} from './page_objects';
import {
@ -36,6 +37,7 @@ export default async function ({ readConfigFile }) {
require.resolve('./apps/discover'),
require.resolve('./apps/management'),
require.resolve('./apps/status_page'),
require.resolve('./apps/timelion'),
require.resolve('./apps/visualize'),
require.resolve('./apps/xpack'),
],
@ -52,6 +54,7 @@ export default async function ({ readConfigFile }) {
monitoring: MonitoringPageProvider,
pointSeries: PointSeriesPageProvider,
visualBuilder: VisualBuilderPageProvider,
timelion: TimelionPageProvider
},
services: {
es: commonConfig.get('services.es'),
@ -93,6 +96,9 @@ export default async function ({ readConfigFile }) {
pathname: '/app/kibana',
hash: '/management',
},
timelion: {
pathname: '/app/timelion',
},
console: {
pathname: '/app/kibana',
hash: '/dev_tools/console',

View file

@ -0,0 +1,246 @@
{
"type": "index",
"value": {
"index": ".kibana",
"settings": {
"index": {
"number_of_shards": "1",
"number_of_replicas": "1"
}
},
"mappings": {
"doc": {
"properties": {
"type": {
"type": "keyword"
},
"index-pattern": {
"dynamic": "strict",
"properties": {
"fieldFormatMap": {
"type": "text"
},
"fields": {
"type": "text"
},
"intervalName": {
"type": "keyword"
},
"notExpandable": {
"type": "boolean"
},
"sourceFilters": {
"type": "text"
},
"timeFieldName": {
"type": "keyword"
},
"title": {
"type": "text"
}
}
},
"search": {
"dynamic": "strict",
"properties": {
"columns": {
"type": "keyword"
},
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"sort": {
"type": "keyword"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"timelion-sheet": {
"dynamic": "strict",
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"timelion_chart_height": {
"type": "integer"
},
"timelion_columns": {
"type": "integer"
},
"timelion_interval": {
"type": "keyword"
},
"timelion_other_interval": {
"type": "keyword"
},
"timelion_rows": {
"type": "integer"
},
"timelion_sheet": {
"type": "text"
},
"title": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"server": {
"dynamic": "strict",
"properties": {
"uuid": {
"type": "keyword"
}
}
},
"config": {
"dynamic": "true",
"properties": {
"buildNum": {
"type": "keyword"
}
}
},
"dashboard": {
"dynamic": "strict",
"properties": {
"description": {
"type": "text"
},
"hits": {
"type": "integer"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"optionsJSON": {
"type": "text"
},
"panelsJSON": {
"type": "text"
},
"refreshInterval": {
"properties": {
"display": {
"type": "keyword"
},
"pause": {
"type": "boolean"
},
"section": {
"type": "integer"
},
"value": {
"type": "integer"
}
}
},
"timeFrom": {
"type": "keyword"
},
"timeRestore": {
"type": "boolean"
},
"timeTo": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
}
}
},
"visualization": {
"dynamic": "strict",
"properties": {
"description": {
"type": "text"
},
"kibanaSavedObjectMeta": {
"properties": {
"searchSourceJSON": {
"type": "text"
}
}
},
"savedSearchId": {
"type": "keyword"
},
"title": {
"type": "text"
},
"uiStateJSON": {
"type": "text"
},
"version": {
"type": "integer"
},
"visState": {
"type": "text"
}
}
},
"url": {
"dynamic": "strict",
"properties": {
"accessCount": {
"type": "long"
},
"accessDate": {
"type": "date"
},
"createDate": {
"type": "date"
},
"url": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 2048
}
}
}
}
}
}
}
}
}
}

View file

@ -10,3 +10,4 @@ export { SettingsPageProvider } from './settings_page';
export { MonitoringPageProvider } from './monitoring_page';
export { PointSeriesPageProvider } from './point_series_page';
export { VisualBuilderPageProvider } from './visual_builder_page';
export { TimelionPageProvider } from './timelion_page';

View file

@ -0,0 +1,53 @@
export function TimelionPageProvider({ getService, getPageObjects }) {
const testSubjects = getService('testSubjects');
const find = getService('find');
const log = getService('log');
const PageObjects = getPageObjects(['common', 'header']);
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
class TimelionPage {
async initTests() {
await kibanaServer.uiSettings.replace({
'dateFormat:tz': 'UTC',
'defaultIndex': 'logstash-*'
});
log.debug('load kibana index');
await esArchiver.load('timelion');
await PageObjects.common.navigateToApp('timelion');
}
async setExpression(expression) {
const input = await testSubjects.find('timelionExpressionTextArea');
await input.clearValue();
await input.type(expression);
}
async updateExpression(updates) {
const input = await testSubjects.find('timelionExpressionTextArea');
await input.type(updates);
}
async getExpression() {
const input = await testSubjects.find('timelionExpressionTextArea');
return input.getVisibleText();
}
async getSuggestionItemsText() {
const elements = await find.allByCssSelector('.suggestions .suggestion');
return await Promise.all(elements.map(async element => await element.getVisibleText()));
}
async clickSuggestion(suggestionIndex = 0) {
const elements = await find.allByCssSelector('.suggestions .suggestion');
if (suggestionIndex > elements.length) {
throw new Error(`Unable to select suggestion ${suggestionIndex}, only ${elements.length} suggestions available.`);
}
await elements[suggestionIndex].click();
}
}
return new TimelionPage();
}