mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[ES|QL] Separate ENRICH
autocomplete routine (#211657)
## Summary Part of https://github.com/elastic/kibana/issues/195418 Gives `ENRICH` autocomplete logic its own home 🏡 ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### Identify risks - [ ] As with any refactor, there's a possibility this will introduce a regression in the behavior of commands. However, all automated tests are passing and I have tested the behavior manually and can detect no regression. --------- Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
d2913395af
commit
f2a91732d8
13 changed files with 549 additions and 376 deletions
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { camelCase } from 'lodash';
|
||||
import { getFieldNamesByType, getPolicyFields, policies, setup } from './helpers';
|
||||
|
||||
describe('autocomplete.suggest', () => {
|
||||
describe('ENRICH', () => {
|
||||
const modes = ['any', 'coordinator', 'remote'];
|
||||
const expectedPolicyNameSuggestions = policies
|
||||
.map(({ name, suggestedAs }) => suggestedAs || name)
|
||||
.map((name) => `${name} `);
|
||||
|
||||
let assertSuggestions: Awaited<ReturnType<typeof setup>>['assertSuggestions'];
|
||||
beforeEach(async () => {
|
||||
const setupResult = await setup();
|
||||
assertSuggestions = setupResult.assertSuggestions;
|
||||
});
|
||||
|
||||
it('suggests policy names', async () => {
|
||||
await assertSuggestions(`from a | enrich /`, expectedPolicyNameSuggestions);
|
||||
await assertSuggestions(`from a | enrich po/`, expectedPolicyNameSuggestions);
|
||||
});
|
||||
|
||||
test('modes', async () => {
|
||||
await assertSuggestions(
|
||||
`from a | enrich _/`,
|
||||
modes.map((mode) => `_${mode}:$0`),
|
||||
{ triggerCharacter: '_' }
|
||||
);
|
||||
await assertSuggestions('from a | enrich _any: /', []);
|
||||
for (const mode of modes) {
|
||||
await assertSuggestions(`from a | enrich _${mode}:/`, expectedPolicyNameSuggestions, {
|
||||
triggerCharacter: ':',
|
||||
});
|
||||
|
||||
await assertSuggestions(
|
||||
`from a | enrich _${mode.toUpperCase()}:/`,
|
||||
expectedPolicyNameSuggestions,
|
||||
{ triggerCharacter: ':' }
|
||||
);
|
||||
|
||||
await assertSuggestions(
|
||||
`from a | enrich _${camelCase(mode)}:/`,
|
||||
expectedPolicyNameSuggestions,
|
||||
{ triggerCharacter: ':' }
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('suggests ON and WITH after policy name', async () => {
|
||||
await assertSuggestions(`from a | enrich policy /`, ['ON ', 'WITH ', '| ']);
|
||||
await assertSuggestions(`from a | enrich policy O/`, ['ON ', 'WITH ', '| ']);
|
||||
});
|
||||
|
||||
it('suggests fields after ON', async () => {
|
||||
await assertSuggestions(
|
||||
`from a | enrich policy on /`,
|
||||
getFieldNamesByType('any').map((v) => `${v} `)
|
||||
);
|
||||
await assertSuggestions(
|
||||
`from a | enrich policy on fi/`,
|
||||
getFieldNamesByType('any').map((v) => `${v} `)
|
||||
);
|
||||
});
|
||||
|
||||
describe('WITH', () => {
|
||||
it('suggests WITH after ON <field>', async () => {
|
||||
await assertSuggestions(`from a | enrich policy on field /`, ['WITH ', '| ']);
|
||||
});
|
||||
|
||||
it('suggests fields for new WITH clauses', async () => {
|
||||
await assertSuggestions(`from a | enrich policy on field with /`, [
|
||||
'var0 = ',
|
||||
...getPolicyFields('policy').map((name) => ({
|
||||
text: name,
|
||||
// Makes sure the suggestion menu isn't opened when a field is accepted
|
||||
command: undefined,
|
||||
})),
|
||||
]);
|
||||
await assertSuggestions(`from a | enrich policy on field with fi/`, [
|
||||
'var0 = ',
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = otherField, /`, [
|
||||
'var1 = ',
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = otherField, fi/`, [
|
||||
'var1 = ',
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('waits to suggest fields until space', async () => {
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = otherField,/`, []);
|
||||
await assertSuggestions(`from a | enrich policy on b with/`, []);
|
||||
});
|
||||
|
||||
test('after first word', async () => {
|
||||
// not a recognized column name
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 /`, ['= $0']);
|
||||
// recognized column name
|
||||
await assertSuggestions(`from a | enrich policy on b with otherField /`, [',', '| ']);
|
||||
});
|
||||
|
||||
test('suggests enrich fields after open assignment', async () => {
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = /`, [
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = fi/`, [
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = otherField, var1 = /`, [
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('after complete clause', async () => {
|
||||
// works with escaped field names
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = \`otherField\` /`, [
|
||||
',',
|
||||
'| ',
|
||||
]);
|
||||
await assertSuggestions(`from a | enrich policy on b with var0=otherField /`, [',', '| ']);
|
||||
await assertSuggestions(`from a | enrich policy on b with otherField /`, [',', '| ']);
|
||||
});
|
||||
|
||||
test('after user-defined column name', async () => {
|
||||
await assertSuggestions(`from a | enrich policy on b with var0 = otherField, var1 /`, [
|
||||
'= $0',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -12,7 +12,6 @@ import { scalarFunctionDefinitions } from '../definitions/generated/scalar_funct
|
|||
import { timeUnitsToSuggest } from '../definitions/literals';
|
||||
import { commandDefinitions as unmodifiedCommandDefinitions } from '../definitions/commands';
|
||||
import { getSafeInsertText, TIME_SYSTEM_PARAMS, TRIGGER_SUGGESTION_COMMAND } from './factories';
|
||||
import { camelCase } from 'lodash';
|
||||
import { getAstAndSyntaxErrors } from '@kbn/esql-ast';
|
||||
import {
|
||||
policies,
|
||||
|
@ -206,84 +205,6 @@ describe('autocomplete', () => {
|
|||
});
|
||||
}
|
||||
|
||||
describe('enrich', () => {
|
||||
const modes = ['any', 'coordinator', 'remote'];
|
||||
const expectedPolicyNameSuggestions = policies
|
||||
.map(({ name, suggestedAs }) => suggestedAs || name)
|
||||
.map((name) => `${name} `);
|
||||
for (const prevCommand of [
|
||||
'',
|
||||
// '| enrich other-policy ',
|
||||
// '| enrich other-policy on b ',
|
||||
// '| enrich other-policy with c ',
|
||||
]) {
|
||||
testSuggestions(`from a ${prevCommand}| enrich /`, expectedPolicyNameSuggestions);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich _/`,
|
||||
modes.map((mode) => `_${mode}:$0`),
|
||||
'_'
|
||||
);
|
||||
for (const mode of modes) {
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich _${mode}:/`,
|
||||
expectedPolicyNameSuggestions,
|
||||
':'
|
||||
);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich _${mode.toUpperCase()}:/`,
|
||||
expectedPolicyNameSuggestions,
|
||||
':'
|
||||
);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich _${camelCase(mode)}:/`,
|
||||
expectedPolicyNameSuggestions,
|
||||
':'
|
||||
);
|
||||
}
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy /`, ['ON $0', 'WITH $0', '| ']);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy on /`,
|
||||
getFieldNamesByType('any').map((v) => `${v} `)
|
||||
);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b /`, ['WITH $0', '| ']);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy on b with /`,
|
||||
['var0 = ', ...getPolicyFields('policy')],
|
||||
' '
|
||||
);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 /`, ['= $0', ',', '| ']);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = /`, [
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = keywordField /`, [
|
||||
',',
|
||||
'| ',
|
||||
]);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy on b with var0 = keywordField, /`, [
|
||||
'var1 = ',
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy on b with var0 = keywordField, var1 /`,
|
||||
['= $0', ',', '| ']
|
||||
);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy on b with var0 = keywordField, var1 = /`,
|
||||
[...getPolicyFields('policy')]
|
||||
);
|
||||
testSuggestions(
|
||||
`from a ${prevCommand}| enrich policy with /`,
|
||||
['var0 = ', ...getPolicyFields('policy')],
|
||||
' '
|
||||
);
|
||||
testSuggestions(`from a ${prevCommand}| enrich policy with keywordField /`, [
|
||||
'= $0',
|
||||
',',
|
||||
'| ',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
// @TODO: get updated eval block from main
|
||||
describe('values suggestions', () => {
|
||||
testSuggestions('FROM "i/"', []);
|
||||
|
@ -449,7 +370,7 @@ describe('autocomplete', () => {
|
|||
);
|
||||
|
||||
// ENRICH policy ON
|
||||
testSuggestions('FROM index1 | ENRICH policy O/', ['ON $0', 'WITH $0', '| ']);
|
||||
testSuggestions('FROM index1 | ENRICH policy O/', ['ON ', 'WITH ', '| ']);
|
||||
|
||||
// ENRICH policy ON field
|
||||
testSuggestions(
|
||||
|
@ -816,10 +737,7 @@ describe('autocomplete', () => {
|
|||
.map(attachTriggerCommand)
|
||||
.map((s) => ({ ...s, rangeToReplace: { start: 17, end: 20 } }))
|
||||
);
|
||||
testSuggestions(
|
||||
'FROM a | ENRICH policy /',
|
||||
['ON $0', 'WITH $0', '| '].map(attachTriggerCommand)
|
||||
);
|
||||
testSuggestions('FROM a | ENRICH policy /', ['ON ', 'WITH ', '| '].map(attachTriggerCommand));
|
||||
|
||||
testSuggestions(
|
||||
'FROM a | ENRICH policy ON /',
|
||||
|
@ -829,12 +747,12 @@ describe('autocomplete', () => {
|
|||
);
|
||||
testSuggestions(
|
||||
'FROM a | ENRICH policy ON @timestamp /',
|
||||
['WITH $0', '| '].map(attachTriggerCommand)
|
||||
['WITH ', '| '].map(attachTriggerCommand)
|
||||
);
|
||||
// nothing fancy with this field list
|
||||
testSuggestions('FROM a | ENRICH policy ON @timestamp WITH /', [
|
||||
'var0 = ',
|
||||
...getPolicyFields('policy').map((name) => ({ text: name, command: undefined })),
|
||||
...getPolicyFields('policy'),
|
||||
]);
|
||||
describe('replacement range', () => {
|
||||
testSuggestions('FROM a | ENRICH policy ON @timestamp WITH othe/', [
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
getCommandDefinition,
|
||||
getCommandOption,
|
||||
getFunctionDefinition,
|
||||
getLastNonWhitespaceChar,
|
||||
isAssignment,
|
||||
isAssignmentComplete,
|
||||
isColumnItem,
|
||||
|
@ -34,23 +33,19 @@ import {
|
|||
isOptionItem,
|
||||
isRestartingExpression,
|
||||
isSourceCommand,
|
||||
isSettingItem,
|
||||
isSourceItem,
|
||||
isTimeIntervalItem,
|
||||
getAllFunctions,
|
||||
isSingleItem,
|
||||
nonNullable,
|
||||
getColumnExists,
|
||||
findPreviousWord,
|
||||
noCaseCompare,
|
||||
correctQuerySyntax,
|
||||
getColumnByName,
|
||||
findFinalWord,
|
||||
getAllCommands,
|
||||
getExpressionType,
|
||||
} from '../shared/helpers';
|
||||
import { collectVariables, excludeVariablesFromCurrentCommand } from '../shared/variables';
|
||||
import type { ESQLPolicy, ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types';
|
||||
import type { ESQLRealField, ESQLVariable, ReferenceMaps } from '../validation/types';
|
||||
import {
|
||||
allStarConstant,
|
||||
commaCompleteItem,
|
||||
|
@ -59,17 +54,13 @@ import {
|
|||
pipeCompleteItem,
|
||||
} from './complete_items';
|
||||
import {
|
||||
buildFieldsDefinitions,
|
||||
buildPoliciesDefinitions,
|
||||
getNewVariableSuggestion,
|
||||
buildNoPoliciesAvailableDefinition,
|
||||
getFunctionSuggestions,
|
||||
buildMatchingFieldsDefinition,
|
||||
getCompatibleLiterals,
|
||||
buildConstantsDefinitions,
|
||||
buildVariablesDefinitions,
|
||||
buildOptionDefinition,
|
||||
buildSettingDefinitions,
|
||||
buildValueDefinitions,
|
||||
getDateLiterals,
|
||||
buildFieldsDefinitionsWithMetadata,
|
||||
|
@ -99,37 +90,17 @@ import {
|
|||
getSuggestionsToRightOfOperatorExpression,
|
||||
checkFunctionInvocationComplete,
|
||||
} from './helper';
|
||||
import { FunctionParameter, isParameterType, FunctionDefinitionTypes } from '../definitions/types';
|
||||
import {
|
||||
FunctionParameter,
|
||||
isParameterType,
|
||||
FunctionDefinitionTypes,
|
||||
GetPolicyMetadataFn,
|
||||
} from '../definitions/types';
|
||||
import { comparisonFunctions } from '../definitions/all_operators';
|
||||
import { getRecommendedQueriesSuggestions } from './recommended_queries/suggestions';
|
||||
|
||||
type GetFieldsMapFn = () => Promise<Map<string, ESQLRealField>>;
|
||||
type GetPoliciesFn = () => Promise<SuggestionRawDefinition[]>;
|
||||
type GetPolicyMetadataFn = (name: string) => Promise<ESQLPolicy | undefined>;
|
||||
|
||||
function hasSameArgBothSides(assignFn: ESQLFunction) {
|
||||
if (assignFn.name === '=' && isColumnItem(assignFn.args[0]) && assignFn.args[1]) {
|
||||
const assignValue = assignFn.args[1];
|
||||
if (Array.isArray(assignValue) && isColumnItem(assignValue[0])) {
|
||||
return assignFn.args[0].name === assignValue[0].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function appendEnrichFields(
|
||||
fieldsMap: Map<string, ESQLRealField>,
|
||||
policyMetadata: ESQLPolicy | undefined
|
||||
) {
|
||||
if (!policyMetadata) {
|
||||
return fieldsMap;
|
||||
}
|
||||
// @TODO: improve this
|
||||
const newMap: Map<string, ESQLRealField> = new Map(fieldsMap);
|
||||
for (const field of policyMetadata.enrichFields) {
|
||||
newMap.set(field, { name: field, type: 'double' });
|
||||
}
|
||||
return newMap;
|
||||
}
|
||||
|
||||
function getFinalSuggestions({ comma }: { comma?: boolean } = { comma: true }) {
|
||||
const finalSuggestions = [pipeCompleteItem];
|
||||
|
@ -205,7 +176,8 @@ export async function suggest(
|
|||
astContext.type === 'expression' ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'join') ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'dissect') ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'from')
|
||||
(astContext.type === 'option' && astContext.command?.name === 'from') ||
|
||||
(astContext.type === 'option' && astContext.command?.name === 'enrich')
|
||||
) {
|
||||
return getSuggestionsWithinCommandExpression(
|
||||
innerText,
|
||||
|
@ -215,22 +187,13 @@ export async function suggest(
|
|||
getFieldsByType,
|
||||
getFieldsMap,
|
||||
getPolicies,
|
||||
getPolicyMetadata,
|
||||
getVariablesByType,
|
||||
resourceRetriever?.getPreferences,
|
||||
resourceRetriever,
|
||||
supportsControls
|
||||
);
|
||||
}
|
||||
if (astContext.type === 'setting') {
|
||||
return getSettingArgsSuggestions(
|
||||
innerText,
|
||||
ast,
|
||||
astContext,
|
||||
getFieldsByType,
|
||||
getFieldsMap,
|
||||
getPolicyMetadata
|
||||
);
|
||||
}
|
||||
if (astContext.type === 'option') {
|
||||
// need this wrap/unwrap thing to make TS happy
|
||||
const { option, ...rest } = astContext;
|
||||
|
@ -391,6 +354,7 @@ async function getSuggestionsWithinCommandExpression(
|
|||
getColumnsByType: GetColumnsByTypeFn,
|
||||
getFieldsMap: GetFieldsMapFn,
|
||||
getPolicies: GetPoliciesFn,
|
||||
getPolicyMetadata: GetPolicyMetadataFn,
|
||||
getVariablesByType?: (type: ESQLVariableType) => ESQLControlVariable[] | undefined,
|
||||
getPreferences?: () => Promise<{ histogramBarTarget: number } | undefined>,
|
||||
callbacks?: ESQLCallbacks,
|
||||
|
@ -409,8 +373,19 @@ async function getSuggestionsWithinCommandExpression(
|
|||
innerText,
|
||||
command,
|
||||
getColumnsByType,
|
||||
getAllColumnNames: () => Array.from(fieldsMap.keys()),
|
||||
columnExists: (col: string) => Boolean(getColumnByName(col, references)),
|
||||
getSuggestedVariableName: () => findNewVariable(anyVariables),
|
||||
getSuggestedVariableName: (extraFieldNames?: string[]) => {
|
||||
if (!extraFieldNames?.length) {
|
||||
return findNewVariable(anyVariables);
|
||||
}
|
||||
|
||||
const augmentedFieldsMap = new Map(fieldsMap);
|
||||
extraFieldNames.forEach((name) => {
|
||||
augmentedFieldsMap.set(name, { name, type: 'double' });
|
||||
});
|
||||
return findNewVariable(collectVariables(commands, augmentedFieldsMap, innerText));
|
||||
},
|
||||
getExpressionType: (expression: ESQLAstItem | undefined) =>
|
||||
getExpressionType(expression, references.fields, references.variables),
|
||||
getPreferences,
|
||||
|
@ -423,6 +398,8 @@ async function getSuggestionsWithinCommandExpression(
|
|||
callbacks,
|
||||
getVariablesByType,
|
||||
supportsControls,
|
||||
getPolicies,
|
||||
getPolicyMetadata,
|
||||
});
|
||||
} else {
|
||||
// The deprecated path.
|
||||
|
@ -866,29 +843,10 @@ async function getExpressionSuggestionsByType(
|
|||
}
|
||||
}
|
||||
}
|
||||
if (argDef.type === 'source') {
|
||||
if (argDef.innerTypes?.includes('policy')) {
|
||||
// ... | ENRICH <suggest>
|
||||
const policies = await getPolicies();
|
||||
const lastWord = findFinalWord(innerText);
|
||||
if (lastWord !== '') {
|
||||
policies.forEach((suggestion) => {
|
||||
suggestions.push({
|
||||
...suggestion,
|
||||
rangeToReplace: {
|
||||
start: innerText.length - lastWord.length + 1,
|
||||
end: innerText.length + 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
suggestions.push(...(policies.length ? policies : [buildNoPoliciesAvailableDefinition()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nonOptionArgs = command.args.filter(
|
||||
(arg) => !isOptionItem(arg) && !isSettingItem(arg) && !Array.isArray(arg) && !arg.incomplete
|
||||
(arg) => !isOptionItem(arg) && !Array.isArray(arg) && !arg.incomplete
|
||||
);
|
||||
// Perform some checks on mandatory arguments
|
||||
const mandatoryArgsAlreadyPresent =
|
||||
|
@ -1234,35 +1192,6 @@ async function getListArgsSuggestions(
|
|||
return suggestions;
|
||||
}
|
||||
|
||||
async function getSettingArgsSuggestions(
|
||||
innerText: string,
|
||||
commands: ESQLCommand[],
|
||||
{
|
||||
command,
|
||||
node,
|
||||
}: {
|
||||
command: ESQLCommand;
|
||||
node: ESQLSingleAstItem | undefined;
|
||||
},
|
||||
getFieldsByType: GetColumnsByTypeFn,
|
||||
getFieldsMaps: GetFieldsMapFn,
|
||||
getPolicyMetadata: GetPolicyMetadataFn
|
||||
) {
|
||||
const suggestions = [];
|
||||
|
||||
const settingDefs = getCommandDefinition(command.name).modes || [];
|
||||
|
||||
if (settingDefs.length) {
|
||||
const lastChar = getLastNonWhitespaceChar(innerText);
|
||||
const matchingSettingDefs = settingDefs.filter(({ prefix }) => lastChar === prefix);
|
||||
if (matchingSettingDefs.length) {
|
||||
// COMMAND _<here>
|
||||
suggestions.push(...matchingSettingDefs.flatMap(buildSettingDefinitions));
|
||||
}
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated — this will disappear when https://github.com/elastic/kibana/issues/195418 is complete
|
||||
* because "options" will be handled in imperative command-specific routines instead of being independent.
|
||||
|
@ -1294,105 +1223,6 @@ async function getOptionArgsSuggestions(
|
|||
const fieldsMap = await getFieldsMaps();
|
||||
const anyVariables = collectVariables(commands, fieldsMap, innerText);
|
||||
|
||||
if (command.name === 'enrich') {
|
||||
if (option.name === 'on') {
|
||||
// if it's a new expression, suggest fields to match on
|
||||
if (
|
||||
isNewExpression ||
|
||||
noCaseCompare(findPreviousWord(innerText), 'ON') ||
|
||||
(option && isAssignment(option.args[0]) && !option.args[1])
|
||||
) {
|
||||
const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined;
|
||||
if (policyName) {
|
||||
const policyMetadata = await getPolicyMetadata(policyName);
|
||||
if (policyMetadata) {
|
||||
suggestions.push(
|
||||
...buildMatchingFieldsDefinition(
|
||||
policyMetadata.matchField,
|
||||
Array.from(fieldsMap.keys())
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// propose the with option
|
||||
suggestions.push(
|
||||
buildOptionDefinition(getCommandOption('with')!),
|
||||
...getFinalSuggestions({
|
||||
comma: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
if (option.name === 'with') {
|
||||
const policyName = isSourceItem(command.args[0]) ? command.args[0].name : undefined;
|
||||
if (policyName) {
|
||||
const policyMetadata = await getPolicyMetadata(policyName);
|
||||
const anyEnhancedVariables = collectVariables(
|
||||
commands,
|
||||
appendEnrichFields(fieldsMap, policyMetadata),
|
||||
innerText
|
||||
);
|
||||
|
||||
if (isNewExpression || noCaseCompare(findPreviousWord(innerText), 'WITH')) {
|
||||
suggestions.push(getNewVariableSuggestion(findNewVariable(anyEnhancedVariables)));
|
||||
}
|
||||
|
||||
// make sure to remove the marker arg from the assign fn
|
||||
const assignFn = isAssignment(lastArg)
|
||||
? (removeMarkerArgFromArgsList(lastArg) as ESQLFunction)
|
||||
: undefined;
|
||||
|
||||
if (policyMetadata) {
|
||||
if (isNewExpression || (assignFn && !isAssignmentComplete(assignFn))) {
|
||||
// ... | ENRICH ... WITH a =
|
||||
// ... | ENRICH ... WITH b
|
||||
const fieldSuggestions = buildFieldsDefinitions(policyMetadata.enrichFields);
|
||||
// in this case, we don't want to open the suggestions menu when the field is accepted
|
||||
// because we're keeping the suggestions simple here for now. Could always revisit.
|
||||
fieldSuggestions.forEach((s) => (s.command = undefined));
|
||||
|
||||
// attach the replacement range if needed
|
||||
const lastWord = findFinalWord(innerText);
|
||||
if (lastWord) {
|
||||
// ENRICH ... WITH a <suggest>
|
||||
const rangeToReplace = {
|
||||
start: innerText.length - lastWord.length + 1,
|
||||
end: innerText.length + 1,
|
||||
};
|
||||
fieldSuggestions.forEach((s) => (s.rangeToReplace = rangeToReplace));
|
||||
}
|
||||
suggestions.push(...fieldSuggestions);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
assignFn &&
|
||||
hasSameArgBothSides(assignFn) &&
|
||||
!isNewExpression &&
|
||||
!isIncompleteItem(assignFn)
|
||||
) {
|
||||
// ... | ENRICH ... WITH a
|
||||
// effectively only assign will apper
|
||||
suggestions.push(
|
||||
...pushItUpInTheList(getOperatorSuggestions({ command: command.name }), true)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
assignFn &&
|
||||
(isAssignmentComplete(assignFn) || hasSameArgBothSides(assignFn)) &&
|
||||
!isNewExpression
|
||||
) {
|
||||
suggestions.push(
|
||||
...getFinalSuggestions({
|
||||
comma: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (command.name === 'rename') {
|
||||
if (option.args.length < 2) {
|
||||
suggestions.push(...buildVariablesDefinitions([findNewVariable(anyVariables)]));
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ESQLSource } from '@kbn/esql-ast';
|
||||
import {
|
||||
findFinalWord,
|
||||
findPreviousWord,
|
||||
isSingleItem,
|
||||
unescapeColumnName,
|
||||
} from '../../../shared/helpers';
|
||||
import { CommandSuggestParams } from '../../../definitions/types';
|
||||
import type { SuggestionRawDefinition } from '../../types';
|
||||
import {
|
||||
Position,
|
||||
buildMatchingFieldsDefinition,
|
||||
getPosition,
|
||||
modeSuggestions,
|
||||
noPoliciesAvailableSuggestion,
|
||||
onSuggestion,
|
||||
withSuggestion,
|
||||
} from './util';
|
||||
import { commaCompleteItem, pipeCompleteItem } from '../../complete_items';
|
||||
import {
|
||||
TRIGGER_SUGGESTION_COMMAND,
|
||||
buildFieldsDefinitions,
|
||||
getNewVariableSuggestion,
|
||||
getOperatorSuggestions,
|
||||
} from '../../factories';
|
||||
|
||||
export async function suggest({
|
||||
innerText,
|
||||
command,
|
||||
getPolicies,
|
||||
getPolicyMetadata,
|
||||
getAllColumnNames,
|
||||
getSuggestedVariableName,
|
||||
}: CommandSuggestParams<'enrich'>): Promise<SuggestionRawDefinition[]> {
|
||||
const pos = getPosition(innerText, command);
|
||||
|
||||
const policyName = (
|
||||
command.args.find((arg) => isSingleItem(arg) && arg.type === 'source') as ESQLSource | undefined
|
||||
)?.name;
|
||||
|
||||
const getFieldSuggestionsForWithClause = async () => {
|
||||
if (!policyName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const policyMetadata = await getPolicyMetadata(policyName);
|
||||
if (!policyMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fieldSuggestions = buildFieldsDefinitions(policyMetadata.enrichFields, false);
|
||||
|
||||
const lastWord = findFinalWord(innerText);
|
||||
if (lastWord) {
|
||||
// ENRICH ... WITH a <suggest>
|
||||
const rangeToReplace = {
|
||||
start: innerText.length - lastWord.length + 1,
|
||||
end: innerText.length + 1,
|
||||
};
|
||||
fieldSuggestions.forEach((s) => {
|
||||
s.rangeToReplace = rangeToReplace;
|
||||
});
|
||||
}
|
||||
|
||||
return fieldSuggestions;
|
||||
};
|
||||
|
||||
switch (pos) {
|
||||
case Position.MODE:
|
||||
return modeSuggestions;
|
||||
|
||||
case Position.POLICY: {
|
||||
const policies = await getPolicies();
|
||||
const lastWord = findFinalWord(innerText);
|
||||
if (lastWord !== '') {
|
||||
policies.forEach((policySuggestion) => {
|
||||
policySuggestion.rangeToReplace = {
|
||||
start: innerText.length - lastWord.length + 1,
|
||||
end: innerText.length + 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
return policies.length ? policies : [noPoliciesAvailableSuggestion];
|
||||
}
|
||||
|
||||
case Position.AFTER_POLICY:
|
||||
return [onSuggestion, withSuggestion, pipeCompleteItem];
|
||||
|
||||
case Position.MATCH_FIELD: {
|
||||
if (!policyName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const policyMetadata = await getPolicyMetadata(policyName);
|
||||
if (!policyMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buildMatchingFieldsDefinition(policyMetadata.matchField, getAllColumnNames());
|
||||
}
|
||||
|
||||
case Position.AFTER_ON_CLAUSE:
|
||||
return [withSuggestion, pipeCompleteItem];
|
||||
|
||||
case Position.WITH_NEW_CLAUSE: {
|
||||
if (!policyName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const policyMetadata = await getPolicyMetadata(policyName);
|
||||
if (!policyMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const suggestions: SuggestionRawDefinition[] = [];
|
||||
suggestions.push(
|
||||
getNewVariableSuggestion(getSuggestedVariableName(policyMetadata.enrichFields))
|
||||
);
|
||||
suggestions.push(...(await getFieldSuggestionsForWithClause()));
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
case Position.WITH_AFTER_FIRST_WORD: {
|
||||
if (!policyName) {
|
||||
return [];
|
||||
}
|
||||
const policyMetadata = await getPolicyMetadata(policyName);
|
||||
|
||||
if (!policyMetadata) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const word = findPreviousWord(innerText);
|
||||
if (policyMetadata.enrichFields.includes(unescapeColumnName(word))) {
|
||||
// complete field name
|
||||
return [pipeCompleteItem, { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND }];
|
||||
} else {
|
||||
// not recognized as a field name, assume new user-defined column name
|
||||
return getOperatorSuggestions({ command: 'enrich' });
|
||||
}
|
||||
}
|
||||
|
||||
case Position.WITH_AFTER_ASSIGNMENT: {
|
||||
const suggestions: SuggestionRawDefinition[] = [];
|
||||
suggestions.push(...(await getFieldSuggestionsForWithClause()));
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
case Position.WITH_AFTER_COMPLETE_CLAUSE: {
|
||||
return [pipeCompleteItem, { ...commaCompleteItem, command: TRIGGER_SUGGESTION_COMMAND }];
|
||||
}
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { ESQLCommand } from '@kbn/esql-ast';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isSingleItem } from '../../../..';
|
||||
import { ENRICH_MODES } from '../../../definitions/settings';
|
||||
import { SuggestionRawDefinition } from '../../types';
|
||||
import { TRIGGER_SUGGESTION_COMMAND, getSafeInsertText } from '../../factories';
|
||||
|
||||
export enum Position {
|
||||
MODE = 'mode',
|
||||
POLICY = 'policy',
|
||||
AFTER_POLICY = 'after_policy',
|
||||
MATCH_FIELD = 'match_field',
|
||||
AFTER_ON_CLAUSE = 'after_on_clause',
|
||||
WITH_NEW_CLAUSE = 'with_new_clause',
|
||||
WITH_AFTER_FIRST_WORD = 'with_after_first_word',
|
||||
WITH_AFTER_ASSIGNMENT = 'with_after_assignment',
|
||||
WITH_AFTER_COMPLETE_CLAUSE = 'with_after_complete_clause',
|
||||
}
|
||||
|
||||
export const getPosition = (
|
||||
innerText: string,
|
||||
command: ESQLCommand<'enrich'>
|
||||
): Position | undefined => {
|
||||
if (command.args.length < 2) {
|
||||
if (innerText.match(/_[^:\s]*$/)) {
|
||||
return Position.MODE;
|
||||
}
|
||||
if (innerText.match(/(:|ENRICH\s+)\S*$/i)) {
|
||||
return Position.POLICY;
|
||||
}
|
||||
if (innerText.match(/:\s+$/)) {
|
||||
return undefined;
|
||||
}
|
||||
if (innerText.match(/\s+\S*$/)) {
|
||||
return Position.AFTER_POLICY;
|
||||
}
|
||||
}
|
||||
|
||||
const lastArg = command.args[command.args.length - 1];
|
||||
if (isSingleItem(lastArg) && lastArg.name === 'on') {
|
||||
if (innerText.match(/on\s+\S*$/i)) {
|
||||
return Position.MATCH_FIELD;
|
||||
}
|
||||
if (innerText.match(/on\s+\S+\s+$/i)) {
|
||||
return Position.AFTER_ON_CLAUSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSingleItem(lastArg) && lastArg.name === 'with') {
|
||||
if (innerText.match(/[,|with]\s+\S*$/i)) {
|
||||
return Position.WITH_NEW_CLAUSE;
|
||||
}
|
||||
if (innerText.match(/[,|with]\s+\S+\s*=\s*\S+\s+$/i)) {
|
||||
return Position.WITH_AFTER_COMPLETE_CLAUSE;
|
||||
}
|
||||
if (innerText.match(/[,|with]\s+\S+\s+$/i)) {
|
||||
return Position.WITH_AFTER_FIRST_WORD;
|
||||
}
|
||||
if (innerText.match(/=\s+[^,\s]*$/i)) {
|
||||
return Position.WITH_AFTER_ASSIGNMENT;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const noPoliciesAvailableSuggestion: SuggestionRawDefinition = {
|
||||
label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabel', {
|
||||
defaultMessage: 'No available policy',
|
||||
}),
|
||||
text: '',
|
||||
kind: 'Issue',
|
||||
detail: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabelsFound',
|
||||
{
|
||||
defaultMessage: 'Click to create',
|
||||
}
|
||||
),
|
||||
sortText: 'D',
|
||||
command: {
|
||||
id: 'esql.policies.create',
|
||||
title: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.createNewPolicy', {
|
||||
defaultMessage: 'Click to create',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const modeSuggestions: SuggestionRawDefinition[] = ENRICH_MODES.values.map(
|
||||
({ name, description }) => ({
|
||||
label: `${ENRICH_MODES.prefix || ''}${name}`,
|
||||
text: `${ENRICH_MODES.prefix || ''}${name}:$0`,
|
||||
asSnippet: true,
|
||||
kind: 'Reference',
|
||||
detail: `${ENRICH_MODES.description} - ${description}`,
|
||||
sortText: 'D',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
})
|
||||
);
|
||||
|
||||
export const onSuggestion: SuggestionRawDefinition = {
|
||||
label: 'ON',
|
||||
text: 'ON ',
|
||||
kind: 'Reference',
|
||||
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.onDoc', {
|
||||
defaultMessage: 'On',
|
||||
}),
|
||||
sortText: '1',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
};
|
||||
|
||||
export const withSuggestion: SuggestionRawDefinition = {
|
||||
label: 'WITH',
|
||||
text: 'WITH ',
|
||||
kind: 'Reference',
|
||||
detail: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.withDoc', {
|
||||
defaultMessage: 'With',
|
||||
}),
|
||||
sortText: '1',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
};
|
||||
|
||||
export const buildMatchingFieldsDefinition = (
|
||||
matchingField: string,
|
||||
fields: string[]
|
||||
): SuggestionRawDefinition[] =>
|
||||
fields.map((label) => ({
|
||||
label,
|
||||
text: getSafeInsertText(label) + ' ',
|
||||
kind: 'Variable',
|
||||
detail: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.matchingFieldDefinition',
|
||||
{
|
||||
defaultMessage: `Use to match on {matchingField} on the policy`,
|
||||
values: {
|
||||
matchingField,
|
||||
},
|
||||
}
|
||||
),
|
||||
sortText: 'D',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}));
|
|
@ -102,7 +102,6 @@ export const suggest: CommandBaseDefinition<'join'>['suggest'] = async ({
|
|||
getColumnsByType,
|
||||
definition,
|
||||
callbacks,
|
||||
previousCommands,
|
||||
}: CommandSuggestParams<'join'>): Promise<SuggestionRawDefinition[]> => {
|
||||
let commandText: string = innerText;
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import { timeUnitsToSuggest } from '../definitions/literals';
|
|||
import {
|
||||
FunctionDefinition,
|
||||
CommandOptionsDefinition,
|
||||
CommandModeDefinition,
|
||||
FunctionParameterType,
|
||||
FunctionDefinitionTypes,
|
||||
} from '../definitions/types';
|
||||
|
@ -245,7 +244,10 @@ export const buildFieldsDefinitionsWithMetadata = (
|
|||
return [...suggestions];
|
||||
};
|
||||
|
||||
export const buildFieldsDefinitions = (fields: string[]): SuggestionRawDefinition[] => {
|
||||
export const buildFieldsDefinitions = (
|
||||
fields: string[],
|
||||
openSuggestions = true
|
||||
): SuggestionRawDefinition[] => {
|
||||
return fields.map((label) => ({
|
||||
label,
|
||||
text: getSafeInsertText(label),
|
||||
|
@ -254,7 +256,7 @@ export const buildFieldsDefinitions = (fields: string[]): SuggestionRawDefinitio
|
|||
defaultMessage: `Field specified by the input table`,
|
||||
}),
|
||||
sortText: 'D',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
command: openSuggestions ? TRIGGER_SUGGESTION_COMMAND : undefined,
|
||||
}));
|
||||
};
|
||||
export const buildVariablesDefinitions = (variables: string[]): SuggestionRawDefinition[] =>
|
||||
|
@ -365,27 +367,7 @@ export const buildPoliciesDefinitions = (
|
|||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}));
|
||||
|
||||
export const buildMatchingFieldsDefinition = (
|
||||
matchingField: string,
|
||||
fields: string[]
|
||||
): SuggestionRawDefinition[] =>
|
||||
fields.map((label) => ({
|
||||
label,
|
||||
text: getSafeInsertText(label) + ' ',
|
||||
kind: 'Variable',
|
||||
detail: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.matchingFieldDefinition',
|
||||
{
|
||||
defaultMessage: `Use to match on {matchingField} on the policy`,
|
||||
values: {
|
||||
matchingField,
|
||||
},
|
||||
}
|
||||
),
|
||||
sortText: 'D',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}));
|
||||
|
||||
/** @deprecated — options will be removed */
|
||||
export const buildOptionDefinition = (
|
||||
option: CommandOptionsDefinition,
|
||||
isAssignType: boolean = false
|
||||
|
@ -407,42 +389,6 @@ export const buildOptionDefinition = (
|
|||
return completeItem;
|
||||
};
|
||||
|
||||
export const buildSettingDefinitions = (
|
||||
setting: CommandModeDefinition
|
||||
): SuggestionRawDefinition[] => {
|
||||
// for now there's just a single setting with one argument
|
||||
return setting.values.map(({ name, description }) => ({
|
||||
label: `${setting.prefix || ''}${name}`,
|
||||
text: `${setting.prefix || ''}${name}:$0`,
|
||||
asSnippet: true,
|
||||
kind: 'Reference',
|
||||
detail: description ? `${setting.description} - ${description}` : setting.description,
|
||||
sortText: 'D',
|
||||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
}));
|
||||
};
|
||||
|
||||
export const buildNoPoliciesAvailableDefinition = (): SuggestionRawDefinition => ({
|
||||
label: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabel', {
|
||||
defaultMessage: 'No available policy',
|
||||
}),
|
||||
text: '',
|
||||
kind: 'Issue',
|
||||
detail: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.esql.autocomplete.noPoliciesLabelsFound',
|
||||
{
|
||||
defaultMessage: 'Click to create',
|
||||
}
|
||||
),
|
||||
sortText: 'D',
|
||||
command: {
|
||||
id: 'esql.policies.create',
|
||||
title: i18n.translate('kbn-esql-validation-autocomplete.esql.autocomplete.createNewPolicy', {
|
||||
defaultMessage: 'Click to create',
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export function getUnitDuration(unit: number = 1) {
|
||||
const filteredTimeLiteral = timeUnitsToSuggest.filter(({ name }) => {
|
||||
const result = /s$/.test(name);
|
||||
|
|
|
@ -27,7 +27,6 @@ import {
|
|||
isFunctionOperatorParam,
|
||||
isLiteralItem,
|
||||
} from '../shared/helpers';
|
||||
import { ENRICH_MODES } from './settings';
|
||||
import {
|
||||
appendSeparatorOption,
|
||||
asOption,
|
||||
|
@ -36,6 +35,8 @@ import {
|
|||
onOption,
|
||||
withOption,
|
||||
} from './options';
|
||||
import { ENRICH_MODES } from './settings';
|
||||
|
||||
import { type CommandDefinition, FunctionDefinitionTypes } from './types';
|
||||
import { suggest as suggestForSort } from '../autocomplete/commands/sort';
|
||||
import { suggest as suggestForKeep } from '../autocomplete/commands/keep';
|
||||
|
@ -48,6 +49,7 @@ import { suggest as suggestForRow } from '../autocomplete/commands/row';
|
|||
import { suggest as suggestForShow } from '../autocomplete/commands/show';
|
||||
import { suggest as suggestForGrok } from '../autocomplete/commands/grok';
|
||||
import { suggest as suggestForDissect } from '../autocomplete/commands/dissect';
|
||||
import { suggest as suggestForEnrich } from '../autocomplete/commands/enrich';
|
||||
|
||||
const statsValidator = (command: ESQLCommand) => {
|
||||
const messages: ESQLMessage[] = [];
|
||||
|
@ -521,14 +523,15 @@ export const commandDefinitions: Array<CommandDefinition<any>> = [
|
|||
multipleParams: false,
|
||||
params: [{ name: 'policyName', type: 'source', innerTypes: ['policy'] }],
|
||||
},
|
||||
suggest: suggestForEnrich,
|
||||
},
|
||||
{
|
||||
name: 'hidden_command',
|
||||
description: 'A test fixture to test hidden-ness',
|
||||
hidden: true,
|
||||
examples: [],
|
||||
modes: [],
|
||||
options: [],
|
||||
modes: [],
|
||||
signature: {
|
||||
params: [],
|
||||
multipleParams: false,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { isLiteralItem, isColumnItem, isInlineCastItem } from '../shared/helpers
|
|||
import { getMessageFromId } from '../validation/errors';
|
||||
import type { CommandOptionsDefinition } from './types';
|
||||
|
||||
/** @deprecated — options are going away */
|
||||
export const byOption: CommandOptionsDefinition = {
|
||||
name: 'by',
|
||||
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.byDoc', {
|
||||
|
@ -25,6 +26,7 @@ export const byOption: CommandOptionsDefinition = {
|
|||
optional: true,
|
||||
};
|
||||
|
||||
/** @deprecated — options are going away */
|
||||
export const metadataOption: CommandOptionsDefinition = {
|
||||
name: 'metadata',
|
||||
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.metadataDoc', {
|
||||
|
@ -60,6 +62,7 @@ export const metadataOption: CommandOptionsDefinition = {
|
|||
},
|
||||
};
|
||||
|
||||
/** @deprecated — options are going away */
|
||||
export const asOption: CommandOptionsDefinition = {
|
||||
name: 'as',
|
||||
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.asDoc', {
|
||||
|
@ -75,6 +78,7 @@ export const asOption: CommandOptionsDefinition = {
|
|||
optional: false,
|
||||
};
|
||||
|
||||
/** @deprecated — options are going away */
|
||||
export const onOption: CommandOptionsDefinition = {
|
||||
name: 'on',
|
||||
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.onDoc', {
|
||||
|
@ -87,6 +91,7 @@ export const onOption: CommandOptionsDefinition = {
|
|||
optional: true,
|
||||
};
|
||||
|
||||
/** @deprecated — options are going away */
|
||||
export const withOption: CommandOptionsDefinition = {
|
||||
name: 'with',
|
||||
description: i18n.translate('kbn-esql-validation-autocomplete.esql.definitions.withDoc', {
|
||||
|
@ -99,6 +104,7 @@ export const withOption: CommandOptionsDefinition = {
|
|||
optional: true,
|
||||
};
|
||||
|
||||
/** @deprecated — options are going away */
|
||||
export const appendSeparatorOption: CommandOptionsDefinition = {
|
||||
name: 'append_separator',
|
||||
description: i18n.translate(
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
import type { ESQLVariableType, ESQLControlVariable } from '@kbn/esql-types';
|
||||
import type {
|
||||
ESQLAstItem,
|
||||
ESQLCommand,
|
||||
|
@ -15,8 +14,10 @@ import type {
|
|||
ESQLMessage,
|
||||
ESQLSource,
|
||||
} from '@kbn/esql-ast';
|
||||
import { ESQLControlVariable, ESQLVariableType } from '@kbn/esql-types';
|
||||
import { GetColumnsByTypeFn, SuggestionRawDefinition } from '../autocomplete/types';
|
||||
import type { ESQLCallbacks, ESQLSourceResult } from '../shared/types';
|
||||
import type { ESQLPolicy } from '../validation/types';
|
||||
import { ESQLCallbacks, ESQLSourceResult } from '../shared/types';
|
||||
|
||||
/**
|
||||
* All supported field types in ES|QL. This is all the types
|
||||
|
@ -192,6 +193,8 @@ export interface FunctionDefinition {
|
|||
customParametersSnippet?: string;
|
||||
}
|
||||
|
||||
export type GetPolicyMetadataFn = (name: string) => Promise<ESQLPolicy | undefined>;
|
||||
|
||||
export interface CommandSuggestParams<CommandName extends string> {
|
||||
/**
|
||||
* The text of the query to the left of the cursor.
|
||||
|
@ -202,10 +205,14 @@ export interface CommandSuggestParams<CommandName extends string> {
|
|||
*/
|
||||
command: ESQLCommand<CommandName>;
|
||||
/**
|
||||
* Get a list of columns by type. This includes fields from any sources as well as
|
||||
* variables defined in the query.
|
||||
* Get suggestions for columns by type. This includes fields from any sources as well as
|
||||
* user-defined columns in the query.
|
||||
*/
|
||||
getColumnsByType: GetColumnsByTypeFn;
|
||||
/**
|
||||
* Gets the names of all columns
|
||||
*/
|
||||
getAllColumnNames: () => string[];
|
||||
/**
|
||||
* Check for the existence of a column by name.
|
||||
* @param column
|
||||
|
@ -214,9 +221,13 @@ export interface CommandSuggestParams<CommandName extends string> {
|
|||
columnExists: (column: string) => boolean;
|
||||
/**
|
||||
* Gets the name that should be used for the next variable.
|
||||
*
|
||||
* @param extraFieldNames — names that should be recognized as columns
|
||||
* but that won't be found in the current table from Elasticsearch. This is currently only
|
||||
* used to recognize enrichment fields from a policy in the ENRICH command.
|
||||
* @returns
|
||||
*/
|
||||
getSuggestedVariableName: () => string;
|
||||
getSuggestedVariableName: (extraFieldNames?: string[]) => string;
|
||||
/**
|
||||
* Examine the AST to determine the type of an expression.
|
||||
* @param expression
|
||||
|
@ -237,6 +248,14 @@ export interface CommandSuggestParams<CommandName extends string> {
|
|||
* @returns
|
||||
*/
|
||||
getSources: () => Promise<ESQLSourceResult[]>;
|
||||
/**
|
||||
* Fetch suggestions for all available policies
|
||||
*/
|
||||
getPolicies: () => Promise<SuggestionRawDefinition[]>;
|
||||
/**
|
||||
* Get metadata for a policy by name
|
||||
*/
|
||||
getPolicyMetadata: GetPolicyMetadataFn;
|
||||
/**
|
||||
* Inspect the AST and returns the sources that are used in the query.
|
||||
* @param type
|
||||
|
|
|
@ -18,14 +18,12 @@ import {
|
|||
Walker,
|
||||
isIdentifier,
|
||||
} from '@kbn/esql-ast';
|
||||
import { ENRICH_MODES } from '../definitions/settings';
|
||||
import { FunctionDefinitionTypes } from '../definitions/types';
|
||||
import { EDITOR_MARKER } from './constants';
|
||||
import {
|
||||
isOptionItem,
|
||||
isColumnItem,
|
||||
isSourceItem,
|
||||
isSettingItem,
|
||||
pipePrecedesCurrentWord,
|
||||
getFunctionDefinition,
|
||||
} from './helpers';
|
||||
|
@ -66,10 +64,6 @@ function findOption(nodes: ESQLAstItem[], offset: number): ESQLCommandOption | u
|
|||
return findCommandSubType(nodes, offset, isOptionItem);
|
||||
}
|
||||
|
||||
function findSetting(nodes: ESQLAstItem[], offset: number): ESQLCommandMode | undefined {
|
||||
return findCommandSubType(nodes, offset, isSettingItem);
|
||||
}
|
||||
|
||||
function findCommandSubType<T extends ESQLCommandMode | ESQLCommandOption>(
|
||||
nodes: ESQLAstItem[],
|
||||
offset: number,
|
||||
|
@ -131,7 +125,6 @@ function findAstPosition(ast: ESQLAst, offset: number) {
|
|||
command: removeMarkerArgFromArgsList(command)!,
|
||||
option: removeMarkerArgFromArgsList(findOption(command.args, offset)),
|
||||
node: removeMarkerArgFromArgsList(cleanMarkerNode(findNode(command.args, offset))),
|
||||
setting: removeMarkerArgFromArgsList(findSetting(command.args, offset)),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -171,16 +164,16 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
|
|||
};
|
||||
}
|
||||
|
||||
const { command, option, setting, node } = findAstPosition(ast, offset);
|
||||
const { command, option, node } = findAstPosition(ast, offset);
|
||||
if (node) {
|
||||
if (node.type === 'literal' && node.literalType === 'keyword') {
|
||||
// command ... "<here>"
|
||||
return { type: 'value' as const, command, node, option, setting };
|
||||
return { type: 'value' as const, command, node, option };
|
||||
}
|
||||
if (node.type === 'function') {
|
||||
if (['in', 'not_in'].includes(node.name) && Array.isArray(node.args[1])) {
|
||||
// command ... a in ( <here> )
|
||||
return { type: 'list' as const, command, node, option, setting };
|
||||
return { type: 'list' as const, command, node, option };
|
||||
}
|
||||
if (
|
||||
isNotEnrichClauseAssigment(node, command) &&
|
||||
|
@ -191,24 +184,19 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
|
|||
!(isOperator(node) && command.name !== 'stats')
|
||||
) {
|
||||
// command ... fn( <here> )
|
||||
return { type: 'function' as const, command, node, option, setting };
|
||||
return { type: 'function' as const, command, node, option };
|
||||
}
|
||||
}
|
||||
// for now it's only an enrich thing
|
||||
if (node.type === 'source' && node.text === ENRICH_MODES.prefix) {
|
||||
// command _<here>
|
||||
return { type: 'setting' as const, command, node, option, setting };
|
||||
}
|
||||
}
|
||||
if (!command || (queryString.length <= offset && pipePrecedesCurrentWord(queryString))) {
|
||||
// // ... | <here>
|
||||
return { type: 'newCommand' as const, command: undefined, node, option, setting };
|
||||
return { type: 'newCommand' as const, command: undefined, node, option };
|
||||
}
|
||||
|
||||
// TODO — remove this option branch once https://github.com/elastic/kibana/issues/195418 is complete
|
||||
if (command && isOptionItem(command.args[command.args.length - 1]) && command.name !== 'stats') {
|
||||
if (option) {
|
||||
return { type: 'option' as const, command, node, option, setting };
|
||||
return { type: 'option' as const, command, node, option };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,6 +206,5 @@ export function getAstContext(queryString: string, ast: ESQLAst, offset: number)
|
|||
command,
|
||||
option,
|
||||
node,
|
||||
setting,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
Walker,
|
||||
type ESQLAstItem,
|
||||
type ESQLColumn,
|
||||
type ESQLCommandMode,
|
||||
type ESQLCommandOption,
|
||||
type ESQLFunction,
|
||||
type ESQLLiteral,
|
||||
|
@ -20,6 +19,7 @@ import {
|
|||
type ESQLTimeInterval,
|
||||
} from '@kbn/esql-ast';
|
||||
import {
|
||||
ESQLCommandMode,
|
||||
ESQLIdentifier,
|
||||
ESQLInlineCast,
|
||||
ESQLParamLiteral,
|
||||
|
@ -66,6 +66,7 @@ export function isSingleItem(arg: ESQLAstItem): arg is ESQLSingleAstItem {
|
|||
return arg && !Array.isArray(arg);
|
||||
}
|
||||
|
||||
/** @deprecated — a "setting" is a concept we will be getting rid of soon */
|
||||
export function isSettingItem(arg: ESQLAstItem): arg is ESQLCommandMode {
|
||||
return isSingleItem(arg) && arg.type === 'mode';
|
||||
}
|
||||
|
@ -268,6 +269,18 @@ export function getColumnForASTNode(
|
|||
return getColumnByName(formatted, { fields, variables });
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a column name like "`my``column`"" and return "my`column"
|
||||
*/
|
||||
export function unescapeColumnName(columnName: string) {
|
||||
// TODO this doesn't cover all escaping scenarios... the best thing to do would be
|
||||
// to use the AST column node parts array, but in some cases the AST node isn't available.
|
||||
if (columnName.startsWith(SINGLE_BACKTICK) && columnName.endsWith(SINGLE_BACKTICK)) {
|
||||
return columnName.slice(1, -1).replace(DOUBLE_TICKS_REGEX, SINGLE_BACKTICK);
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function returns the variable or field matching a column
|
||||
*/
|
||||
|
@ -275,12 +288,8 @@ export function getColumnByName(
|
|||
columnName: string,
|
||||
{ fields, variables }: Pick<ReferenceMaps, 'fields' | 'variables'>
|
||||
): ESQLRealField | ESQLVariable | undefined {
|
||||
// TODO this doesn't cover all escaping scenarios... the best thing to do would be
|
||||
// to use the AST column node parts array, but in some cases the AST node isn't available.
|
||||
if (columnName.startsWith(SINGLE_BACKTICK) && columnName.endsWith(SINGLE_BACKTICK)) {
|
||||
columnName = columnName.slice(1, -1).replace(DOUBLE_TICKS_REGEX, SINGLE_BACKTICK);
|
||||
}
|
||||
return fields.get(columnName) || variables.get(columnName)?.[0];
|
||||
const unescaped = unescapeColumnName(columnName);
|
||||
return fields.get(unescaped) || variables.get(unescaped)?.[0];
|
||||
}
|
||||
|
||||
const ARRAY_REGEXP = /\[\]$/;
|
||||
|
|
|
@ -710,6 +710,7 @@ function validateFunction({
|
|||
return uniqBy(messages, ({ location }) => `${location.min}-${location.max}`);
|
||||
}
|
||||
|
||||
/** @deprecated — "command settings" will be removed soon */
|
||||
function validateSetting(
|
||||
setting: ESQLCommandMode,
|
||||
settingDef: CommandModeDefinition | undefined,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue