mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[ES|QL] Recommended queries (#194418)](https://github.com/elastic/kibana/pull/194418) <!--- Backport version: 9.4.3 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Stratoula Kalafateli","email":"efstratia.kalafateli@elastic.co"},"sourceCommit":{"committedDate":"2024-10-08T14:52:21Z","message":"[ES|QL] Recommended queries (#194418)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/187325\r\n\r\n<img width=\"927\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/46220c26-f54c-4bd7-9a8b-d1d29591dc68\">\r\n\r\n<img width=\"539\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/f4d938af-f2b6-400d-918f-3dcf1d22618f\">\r\n\r\n\r\nThis is the first iteration of this feature. We want to help the users\r\nfamiliarize themselves with popular operations. This PR:\r\n- adds the recommended queries list in the help menu of unified search\r\n- adds the list after the users select a datasource with the from\r\ncommand\r\n- adds the list in the editor's empty state\r\n\r\n### Checklist\r\n\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"149e801109d7c9edf2d6eef41ebb0e281314a19f","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","release_note:feature","backport:prev-minor","Feature:ES|QL","Team:ESQL","v8.16.0"],"title":"[ES|QL] Recommended queries","number":194418,"url":"https://github.com/elastic/kibana/pull/194418","mergeCommit":{"message":"[ES|QL] Recommended queries (#194418)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/187325\r\n\r\n<img width=\"927\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/46220c26-f54c-4bd7-9a8b-d1d29591dc68\">\r\n\r\n<img width=\"539\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/f4d938af-f2b6-400d-918f-3dcf1d22618f\">\r\n\r\n\r\nThis is the first iteration of this feature. We want to help the users\r\nfamiliarize themselves with popular operations. This PR:\r\n- adds the recommended queries list in the help menu of unified search\r\n- adds the list after the users select a datasource with the from\r\ncommand\r\n- adds the list in the editor's empty state\r\n\r\n### Checklist\r\n\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"149e801109d7c9edf2d6eef41ebb0e281314a19f"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/194418","number":194418,"mergeCommit":{"message":"[ES|QL] Recommended queries (#194418)\n\n## Summary\r\n\r\nCloses https://github.com/elastic/kibana/issues/187325\r\n\r\n<img width=\"927\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/46220c26-f54c-4bd7-9a8b-d1d29591dc68\">\r\n\r\n<img width=\"539\" alt=\"image\"\r\nsrc=\"https://github.com/user-attachments/assets/f4d938af-f2b6-400d-918f-3dcf1d22618f\">\r\n\r\n\r\nThis is the first iteration of this feature. We want to help the users\r\nfamiliarize themselves with popular operations. This PR:\r\n- adds the recommended queries list in the help menu of unified search\r\n- adds the list after the users select a datasource with the from\r\ncommand\r\n- adds the list in the editor's empty state\r\n\r\n### Checklist\r\n\r\n- [ ]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas added for features that require explanation or tutorials\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios","sha":"149e801109d7c9edf2d6eef41ebb0e281314a19f"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Stratoula Kalafateli <efstratia.kalafateli@elastic.co>
This commit is contained in:
parent
9bd5789f88
commit
ba2ecd5821
16 changed files with 424 additions and 122 deletions
|
@ -76,3 +76,5 @@ export {
|
|||
} from './src/shared/resources_helpers';
|
||||
|
||||
export { wrapAsEditorMessage } from './src/code_actions/utils';
|
||||
|
||||
export { getRecommendedQueries } from './src/autocomplete/recommended_queries/templates';
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { METADATA_FIELDS } from '../../shared/constants';
|
||||
import { setup, indexes, integrations } from './helpers';
|
||||
import { getRecommendedQueries } from '../recommended_queries/templates';
|
||||
|
||||
const visibleIndices = indexes
|
||||
.filter(({ hidden }) => !hidden)
|
||||
|
@ -72,8 +73,17 @@ describe('autocomplete.suggest', () => {
|
|||
const metadataFieldsAndIndex = metadataFields.filter((field) => field !== '_index');
|
||||
|
||||
test('on <kbd>SPACE</kbd> without comma ",", suggests adding metadata', async () => {
|
||||
const recommendedQueries = getRecommendedQueries({
|
||||
fromCommand: '',
|
||||
timeField: 'dateField',
|
||||
});
|
||||
const { assertSuggestions } = await setup();
|
||||
const expected = ['METADATA $0', ',', '| '].sort();
|
||||
const expected = [
|
||||
'METADATA $0',
|
||||
',',
|
||||
'| ',
|
||||
...recommendedQueries.map((query) => query.queryString),
|
||||
].sort();
|
||||
|
||||
await assertSuggestions('from a, b /', expected);
|
||||
});
|
||||
|
|
|
@ -36,12 +36,9 @@ const setup = async (caret = '?') => {
|
|||
};
|
||||
|
||||
describe('autocomplete.suggest', () => {
|
||||
test('does not load fields when suggesting within a single FROM, SHOW, ROW command', async () => {
|
||||
test('does not load fields when suggesting within a single SHOW, ROW command', async () => {
|
||||
const { suggest, callbacks } = await setup();
|
||||
|
||||
await suggest('FROM kib, ? |');
|
||||
await suggest('FROM ?');
|
||||
await suggest('FROM ? |');
|
||||
await suggest('sHoW ?');
|
||||
await suggest('row ? |');
|
||||
|
||||
|
|
|
@ -35,8 +35,16 @@ import {
|
|||
import { METADATA_FIELDS } from '../shared/constants';
|
||||
import { ESQL_COMMON_NUMERIC_TYPES, ESQL_STRING_TYPES } from '../shared/esql_types';
|
||||
import { log10ParameterTypes, powParameterTypes } from './__tests__/constants';
|
||||
import { getRecommendedQueries } from './recommended_queries/templates';
|
||||
|
||||
const commandDefinitions = unmodifiedCommandDefinitions.filter(({ hidden }) => !hidden);
|
||||
|
||||
const getRecommendedQueriesSuggestions = (fromCommand: string, timeField?: string) =>
|
||||
getRecommendedQueries({
|
||||
fromCommand,
|
||||
timeField,
|
||||
});
|
||||
|
||||
describe('autocomplete', () => {
|
||||
type TestArgs = [
|
||||
string,
|
||||
|
@ -82,10 +90,11 @@ describe('autocomplete', () => {
|
|||
const sourceCommands = ['row', 'from', 'show'];
|
||||
|
||||
describe('New command', () => {
|
||||
testSuggestions(
|
||||
'/',
|
||||
sourceCommands.map((name) => name.toUpperCase() + ' $0')
|
||||
);
|
||||
const recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField');
|
||||
testSuggestions('/', [
|
||||
...sourceCommands.map((name) => name.toUpperCase() + ' $0'),
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
]);
|
||||
testSuggestions(
|
||||
'from a | /',
|
||||
commandDefinitions
|
||||
|
@ -523,10 +532,11 @@ describe('autocomplete', () => {
|
|||
*/
|
||||
describe('Invoke trigger kind (all commands)', () => {
|
||||
// source command
|
||||
testSuggestions(
|
||||
'f/',
|
||||
sourceCommands.map((cmd) => `${cmd.toUpperCase()} $0`)
|
||||
);
|
||||
let recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField');
|
||||
testSuggestions('f/', [
|
||||
...sourceCommands.map((cmd) => `${cmd.toUpperCase()} $0`),
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
]);
|
||||
|
||||
// pipe command
|
||||
testSuggestions(
|
||||
|
@ -575,7 +585,13 @@ describe('autocomplete', () => {
|
|||
]);
|
||||
|
||||
// FROM source METADATA
|
||||
testSuggestions('FROM index1 M/', [',', 'METADATA $0', '| ']);
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField');
|
||||
testSuggestions('FROM index1 M/', [
|
||||
',',
|
||||
'METADATA $0',
|
||||
'| ',
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
]);
|
||||
|
||||
// FROM source METADATA field
|
||||
testSuggestions('FROM index1 METADATA _/', METADATA_FIELDS);
|
||||
|
@ -710,12 +726,12 @@ describe('autocomplete', () => {
|
|||
...s,
|
||||
asSnippet: true,
|
||||
});
|
||||
|
||||
let recommendedQuerySuggestions = getRecommendedQueriesSuggestions('FROM logs*', 'dateField');
|
||||
// Source command
|
||||
testSuggestions(
|
||||
'F/',
|
||||
['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet)
|
||||
);
|
||||
testSuggestions('F/', [
|
||||
...['FROM $0', 'ROW $0', 'SHOW $0'].map(attachTriggerCommand).map(attachAsSnippet),
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
]);
|
||||
|
||||
// Pipe command
|
||||
testSuggestions(
|
||||
|
@ -787,11 +803,14 @@ describe('autocomplete', () => {
|
|||
);
|
||||
});
|
||||
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField');
|
||||
|
||||
// PIPE (|)
|
||||
testSuggestions('FROM a /', [
|
||||
attachTriggerCommand('| '),
|
||||
',',
|
||||
attachAsSnippet(attachTriggerCommand('METADATA $0')),
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
]);
|
||||
|
||||
// Assignment
|
||||
|
@ -833,6 +852,7 @@ describe('autocomplete', () => {
|
|||
],
|
||||
]
|
||||
);
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('index1', 'dateField');
|
||||
|
||||
testSuggestions(
|
||||
'FROM index1/',
|
||||
|
@ -840,6 +860,7 @@ describe('autocomplete', () => {
|
|||
{ text: 'index1 | ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'index1, ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'index1 METADATA ', filterText: 'index1', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
],
|
||||
undefined,
|
||||
[
|
||||
|
@ -851,12 +872,14 @@ describe('autocomplete', () => {
|
|||
]
|
||||
);
|
||||
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('index2', 'dateField');
|
||||
testSuggestions(
|
||||
'FROM index1, index2/',
|
||||
[
|
||||
{ text: 'index2 | ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'index2, ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'index2 METADATA ', filterText: 'index2', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
],
|
||||
undefined,
|
||||
[
|
||||
|
@ -872,6 +895,7 @@ describe('autocomplete', () => {
|
|||
// meaning that Monaco by default will only set the replacement
|
||||
// range to cover "bar" and not "foo$bar". We have to make sure
|
||||
// we're setting it ourselves.
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('foo$bar', 'dateField');
|
||||
testSuggestions(
|
||||
'FROM foo$bar/',
|
||||
[
|
||||
|
@ -894,18 +918,21 @@ describe('autocomplete', () => {
|
|||
command: TRIGGER_SUGGESTION_COMMAND,
|
||||
rangeToReplace: { start: 6, end: 13 },
|
||||
},
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
],
|
||||
undefined,
|
||||
[, [{ name: 'foo$bar', hidden: false }]]
|
||||
);
|
||||
|
||||
// This is an identifier that matches multiple sources
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('i*', 'dateField');
|
||||
testSuggestions(
|
||||
'FROM i*/',
|
||||
[
|
||||
{ text: 'i* | ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'i*, ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
{ text: 'i* METADATA ', filterText: 'i*', command: TRIGGER_SUGGESTION_COMMAND },
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
],
|
||||
undefined,
|
||||
[
|
||||
|
@ -918,11 +945,13 @@ describe('autocomplete', () => {
|
|||
);
|
||||
});
|
||||
|
||||
recommendedQuerySuggestions = getRecommendedQueriesSuggestions('', 'dateField');
|
||||
// FROM source METADATA
|
||||
testSuggestions('FROM index1 M/', [
|
||||
',',
|
||||
attachAsSnippet(attachTriggerCommand('METADATA $0')),
|
||||
'| ',
|
||||
...recommendedQuerySuggestions.map((q) => q.queryString),
|
||||
]);
|
||||
|
||||
describe('ENRICH', () => {
|
||||
|
|
|
@ -19,7 +19,7 @@ import type {
|
|||
} from '@kbn/esql-ast';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ESQL_NUMBER_TYPES, isNumericType } from '../shared/esql_types';
|
||||
import type { EditorContext, ItemKind, SuggestionRawDefinition } from './types';
|
||||
import type { EditorContext, ItemKind, SuggestionRawDefinition, GetFieldsByTypeFn } from './types';
|
||||
import {
|
||||
getColumnForASTNode,
|
||||
getCommandDefinition,
|
||||
|
@ -113,12 +113,8 @@ import {
|
|||
import { metadataOption } from '../definitions/options';
|
||||
import { comparisonFunctions } from '../definitions/builtin';
|
||||
import { countBracketsUnclosed } from '../shared/helpers';
|
||||
import { getRecommendedQueriesSuggestions } from './recommended_queries/suggestions';
|
||||
|
||||
type GetFieldsByTypeFn = (
|
||||
type: string | string[],
|
||||
ignored?: string[],
|
||||
options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean }
|
||||
) => Promise<SuggestionRawDefinition[]>;
|
||||
type GetFieldsMapFn = () => Promise<Map<string, ESQLRealField>>;
|
||||
type GetPoliciesFn = () => Promise<SuggestionRawDefinition[]>;
|
||||
type GetPolicyMetadataFn = (name: string) => Promise<ESQLPolicy | undefined>;
|
||||
|
@ -176,7 +172,7 @@ export async function suggest(
|
|||
);
|
||||
|
||||
const { getFieldsByType, getFieldsMap } = getFieldsByTypeRetriever(
|
||||
queryForFields,
|
||||
queryForFields.replace(EDITOR_MARKER, ''),
|
||||
resourceRetriever
|
||||
);
|
||||
const getSources = getSourcesHelper(resourceRetriever);
|
||||
|
@ -187,7 +183,26 @@ export async function suggest(
|
|||
// filter source commands if already defined
|
||||
const suggestions = commandAutocompleteDefinitions;
|
||||
if (!ast.length) {
|
||||
return suggestions.filter(isSourceCommand);
|
||||
// Display the recommended queries if there are no commands (empty state)
|
||||
const recommendedQueriesSuggestions: SuggestionRawDefinition[] = [];
|
||||
if (getSources) {
|
||||
let fromCommand = '';
|
||||
const sources = await getSources();
|
||||
const visibleSources = sources.filter((source) => !source.hidden);
|
||||
if (visibleSources.find((source) => source.name.startsWith('logs'))) {
|
||||
fromCommand = 'FROM logs*';
|
||||
} else fromCommand = `FROM ${visibleSources[0].name}`;
|
||||
|
||||
const { getFieldsByType: getFieldsByTypeEmptyState } = getFieldsByTypeRetriever(
|
||||
fromCommand,
|
||||
resourceRetriever
|
||||
);
|
||||
recommendedQueriesSuggestions.push(
|
||||
...(await getRecommendedQueriesSuggestions(getFieldsByTypeEmptyState, fromCommand))
|
||||
);
|
||||
}
|
||||
const sourceCommandsSuggestions = suggestions.filter(isSourceCommand);
|
||||
return [...sourceCommandsSuggestions, ...recommendedQueriesSuggestions];
|
||||
}
|
||||
|
||||
return suggestions.filter((def) => !isSourceCommand(def));
|
||||
|
@ -519,6 +534,7 @@ async function getExpressionSuggestionsByType(
|
|||
const optArg = optionsAlreadyDeclared.find(({ name: optionName }) => optionName === name);
|
||||
return (!optArg && !optionsAlreadyDeclared.length) || (optArg && index > optArg.index);
|
||||
});
|
||||
const hasRecommendedQueries = Boolean(commandDef?.hasRecommendedQueries);
|
||||
// get the next definition for the given command
|
||||
let argDef = commandDef.signature.params[argIndex];
|
||||
// tune it for the variadic case
|
||||
|
@ -910,6 +926,11 @@ async function getExpressionSuggestionsByType(
|
|||
|
||||
if (lastIndex && lastIndex.text && lastIndex.text !== EDITOR_MARKER) {
|
||||
const sources = await getSources();
|
||||
|
||||
const recommendedQueriesSuggestions = hasRecommendedQueries
|
||||
? await getRecommendedQueriesSuggestions(getFieldsByType)
|
||||
: [];
|
||||
|
||||
const suggestionsToAdd = await handleFragment(
|
||||
innerText,
|
||||
(fragment) =>
|
||||
|
@ -952,8 +973,13 @@ async function getExpressionSuggestionsByType(
|
|||
asSnippet: false, // turn this off because $ could be contained within the source name
|
||||
rangeToReplace,
|
||||
},
|
||||
...recommendedQueriesSuggestions.map((suggestion) => ({
|
||||
...suggestion,
|
||||
rangeToReplace,
|
||||
filterText: fragment,
|
||||
text: fragment + suggestion.text,
|
||||
})),
|
||||
];
|
||||
|
||||
return _suggestions;
|
||||
}
|
||||
}
|
||||
|
@ -1005,6 +1031,11 @@ async function getExpressionSuggestionsByType(
|
|||
}));
|
||||
suggestions.push(...finalSuggestions);
|
||||
}
|
||||
|
||||
// handle recommended queries for from
|
||||
if (hasRecommendedQueries) {
|
||||
suggestions.push(...(await getRecommendedQueriesSuggestions(getFieldsByType)));
|
||||
}
|
||||
}
|
||||
// Due to some logic overlapping functions can be repeated
|
||||
// so dedupe here based on text string (it can differ from name)
|
||||
|
|
|
@ -86,9 +86,7 @@ export function strictlyGetParamAtPosition(
|
|||
export function getQueryForFields(queryString: string, commands: ESQLCommand[]) {
|
||||
// If there is only one source command and it does not require fields, do not
|
||||
// fetch fields, hence return an empty string.
|
||||
return commands.length === 1 && ['from', 'row', 'show'].includes(commands[0].name)
|
||||
? ''
|
||||
: queryString;
|
||||
return commands.length === 1 && ['row', 'show'].includes(commands[0].name) ? '' : queryString;
|
||||
}
|
||||
|
||||
export function getSourcesFromCommands(commands: ESQLCommand[], sourceType: 'index' | 'policy') {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 type { SuggestionRawDefinition, GetFieldsByTypeFn } from '../types';
|
||||
import { getRecommendedQueries } from './templates';
|
||||
|
||||
export const getRecommendedQueriesSuggestions = async (
|
||||
getFieldsByType: GetFieldsByTypeFn,
|
||||
fromCommand: string = ''
|
||||
): Promise<SuggestionRawDefinition[]> => {
|
||||
const fieldSuggestions = await getFieldsByType('date', [], {
|
||||
openSuggestions: true,
|
||||
});
|
||||
let timeField = '';
|
||||
if (fieldSuggestions.length) {
|
||||
timeField =
|
||||
fieldSuggestions?.find((field) => field.label === '@timestamp')?.label ||
|
||||
fieldSuggestions[0].label;
|
||||
}
|
||||
|
||||
const recommendedQueries = getRecommendedQueries({ fromCommand, timeField });
|
||||
|
||||
const suggestions: SuggestionRawDefinition[] = recommendedQueries.map((query) => {
|
||||
return {
|
||||
label: query.label,
|
||||
text: query.queryString,
|
||||
kind: 'Issue',
|
||||
detail: query.description,
|
||||
sortText: 'D',
|
||||
};
|
||||
});
|
||||
|
||||
return suggestions;
|
||||
};
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
// Order starts with the simple ones and goes to more complex ones
|
||||
|
||||
export const getRecommendedQueries = ({
|
||||
fromCommand,
|
||||
timeField,
|
||||
}: {
|
||||
fromCommand: string;
|
||||
timeField?: string;
|
||||
}) => {
|
||||
const queries = [
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.aggregateExample.label',
|
||||
{
|
||||
defaultMessage: 'Aggregate with STATS',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.aggregateExample.description',
|
||||
{
|
||||
defaultMessage: 'Count aggregation',
|
||||
}
|
||||
),
|
||||
queryString: `${fromCommand}\n | STATS count = COUNT(*) // you can group by a field using the BY operator`,
|
||||
},
|
||||
...(timeField
|
||||
? [
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.sortByTime.label',
|
||||
{
|
||||
defaultMessage: 'Sort by time',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.sortByTime.description',
|
||||
{
|
||||
defaultMessage: 'Sort by time',
|
||||
}
|
||||
),
|
||||
queryString: `${fromCommand}\n | SORT ${timeField} // Data is not sorted by default`,
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.dateIntervals.label',
|
||||
{
|
||||
defaultMessage: 'Create 5 minute time buckets with EVAL',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.dateIntervals.description',
|
||||
{
|
||||
defaultMessage: 'Count aggregation over time',
|
||||
}
|
||||
),
|
||||
queryString: `${fromCommand}\n | EVAL buckets = DATE_TRUNC(5 minute, ${timeField}) | STATS count = COUNT(*) BY buckets // try out different intervals`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.caseExample.label',
|
||||
{
|
||||
defaultMessage: 'Create a conditional with CASE',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.caseExample.description',
|
||||
{
|
||||
defaultMessage: 'Conditional',
|
||||
}
|
||||
),
|
||||
queryString: `${fromCommand}\n | STATS count = COUNT(*)\n | EVAL newField = CASE(count < 100, "groupA", count > 100 and count < 500, "groupB", "Other")\n | KEEP newField`,
|
||||
},
|
||||
...(timeField
|
||||
? [
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.dateHistogram.label',
|
||||
{
|
||||
defaultMessage: 'Create a date histogram',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.dateHistogram.description',
|
||||
{
|
||||
defaultMessage: 'Count aggregation over time',
|
||||
}
|
||||
),
|
||||
queryString: `${fromCommand}\n | WHERE ${timeField} <=?_tend and ${timeField} >?_tstart\n | STATS count = COUNT(*) BY \`Over time\` = BUCKET(${timeField}, 50, ?_tstart, ?_tend) // ?_tstart and ?_tend take the values of the time picker`,
|
||||
},
|
||||
{
|
||||
label: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.lastHour.label',
|
||||
{
|
||||
defaultMessage: 'Total count vs count last hour',
|
||||
}
|
||||
),
|
||||
description: i18n.translate(
|
||||
'kbn-esql-validation-autocomplete.recommendedQueries.lastHour.description',
|
||||
{
|
||||
defaultMessage: 'A more complicated example',
|
||||
}
|
||||
),
|
||||
queryString: `${fromCommand}
|
||||
| SORT ${timeField}
|
||||
| EVAL now = NOW()
|
||||
| EVAL key = CASE(${timeField} < (now - 1 hour) AND ${timeField} > (now - 2 hour), "Last hour", "Other")
|
||||
| STATS count = COUNT(*) BY key
|
||||
| EVAL count_last_hour = CASE(key == "Last hour", count), count_rest = CASE(key == "Other", count)
|
||||
| EVAL total_visits = TO_DOUBLE(COALESCE(count_last_hour, 0::LONG) + COALESCE(count_rest, 0::LONG))
|
||||
| STATS count_last_hour = SUM(count_last_hour), total_visits = SUM(total_visits)`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
return queries;
|
||||
};
|
|
@ -80,3 +80,9 @@ export interface EditorContext {
|
|||
*/
|
||||
triggerKind: number;
|
||||
}
|
||||
|
||||
export type GetFieldsByTypeFn = (
|
||||
type: string | string[],
|
||||
ignored?: string[],
|
||||
options?: { advanceCursor?: boolean; openSuggestions?: boolean; addComma?: boolean }
|
||||
) => Promise<SuggestionRawDefinition[]>;
|
||||
|
|
|
@ -173,6 +173,7 @@ export const commandDefinitions: CommandDefinition[] = [
|
|||
examples: ['from logs', 'from logs-*', 'from logs_*, events-*'],
|
||||
options: [metadataOption],
|
||||
modes: [],
|
||||
hasRecommendedQueries: true,
|
||||
signature: {
|
||||
multipleParams: true,
|
||||
params: [{ name: 'index', type: 'source', wildcards: true }],
|
||||
|
|
|
@ -204,6 +204,7 @@ export interface CommandDefinition extends CommandBaseDefinition {
|
|||
examples: string[];
|
||||
validate?: (option: ESQLCommand) => ESQLMessage[];
|
||||
modes: CommandModeDefinition[];
|
||||
hasRecommendedQueries?: boolean;
|
||||
}
|
||||
|
||||
export interface Literals {
|
||||
|
|
|
@ -11,18 +11,20 @@ import React from 'react';
|
|||
import { screen, render } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { stubIndexPattern } from '@kbn/data-plugin/public/stubs';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { ESQLMenuPopover } from './esql_menu_popover';
|
||||
|
||||
describe('ESQLMenuPopover', () => {
|
||||
const renderESQLPopover = () => {
|
||||
const renderESQLPopover = (adHocDataview?: DataView) => {
|
||||
const startMock = coreMock.createStart();
|
||||
const services = {
|
||||
docLinks: startMock.docLinks,
|
||||
};
|
||||
return render(
|
||||
<KibanaContextProvider services={services}>
|
||||
<ESQLMenuPopover />{' '}
|
||||
<ESQLMenuPopover adHocDataview={adHocDataview} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -37,8 +39,14 @@ describe('ESQLMenuPopover', () => {
|
|||
expect(screen.getByTestId('esql-menu-button')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(screen.getByTestId('esql-quick-reference')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('esql-examples')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('esql-recommended-queries')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('esql-about')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('esql-feedback')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have recommended queries if a dataview is passed', async () => {
|
||||
renderESQLPopover(stubIndexPattern);
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
expect(screen.queryByTestId('esql-recommended-queries')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,23 +11,28 @@ import React, { useMemo, useState, useCallback } from 'react';
|
|||
import {
|
||||
EuiPopover,
|
||||
EuiButton,
|
||||
EuiContextMenuPanel,
|
||||
type EuiContextMenuPanelProps,
|
||||
type EuiContextMenuPanelDescriptor,
|
||||
EuiContextMenuItem,
|
||||
EuiHorizontalRule,
|
||||
EuiContextMenu,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { FEEDBACK_LINK } from '@kbn/esql-utils';
|
||||
import { getRecommendedQueries } from '@kbn/esql-validation-autocomplete';
|
||||
import { LanguageDocumentationFlyout } from '@kbn/language-documentation';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
|
||||
export interface ESQLMenuPopoverProps {
|
||||
onESQLDocsFlyoutVisibilityChanged?: (isOpen: boolean) => void;
|
||||
adHocDataview?: DataView | string;
|
||||
onESQLQuerySubmit?: (query: string) => void;
|
||||
}
|
||||
|
||||
export const ESQLMenuPopover: React.FC<ESQLMenuPopoverProps> = ({
|
||||
onESQLDocsFlyoutVisibilityChanged,
|
||||
adHocDataview,
|
||||
onESQLQuerySubmit,
|
||||
}) => {
|
||||
const kibana = useKibana<IUnifiedSearchPluginServices>();
|
||||
|
||||
|
@ -48,63 +53,115 @@ export const ESQLMenuPopover: React.FC<ESQLMenuPopoverProps> = ({
|
|||
[setIsLanguageComponentOpen, onESQLDocsFlyoutVisibilityChanged]
|
||||
);
|
||||
|
||||
const esqlPanelItems = useMemo(() => {
|
||||
const panelItems: EuiContextMenuPanelProps['items'] = [];
|
||||
panelItems.push(
|
||||
<EuiContextMenuItem
|
||||
key="quickReference"
|
||||
icon="documentation"
|
||||
data-test-subj="esql-quick-reference"
|
||||
onClick={() => toggleLanguageComponent()}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', {
|
||||
defaultMessage: 'Quick Reference',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="about"
|
||||
icon="iInCircle"
|
||||
data-test-subj="esql-about"
|
||||
target="_blank"
|
||||
href={docLinks.links.query.queryESQL}
|
||||
onClick={() => setIsESQLMenuPopoverOpen(false)}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
|
||||
defaultMessage: 'Documentation',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem
|
||||
key="examples"
|
||||
icon="nested"
|
||||
data-test-subj="esql-examples"
|
||||
target="_blank"
|
||||
href={docLinks.links.query.queryESQLExamples}
|
||||
onClick={() => setIsESQLMenuPopoverOpen(false)}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
|
||||
defaultMessage: 'Example queries',
|
||||
})}
|
||||
</EuiContextMenuItem>,
|
||||
<EuiHorizontalRule margin="xs" key="dataviewActions-divider" />,
|
||||
<EuiContextMenuItem
|
||||
key="feedback"
|
||||
icon="editorComment"
|
||||
data-test-subj="esql-feedback"
|
||||
target="_blank"
|
||||
href={FEEDBACK_LINK}
|
||||
onClick={() => setIsESQLMenuPopoverOpen(false)}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
|
||||
defaultMessage: 'Submit feedback',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
);
|
||||
return panelItems;
|
||||
}, [
|
||||
docLinks.links.query.queryESQL,
|
||||
docLinks.links.query.queryESQLExamples,
|
||||
toggleLanguageComponent,
|
||||
]);
|
||||
const esqlContextMenuPanels = useMemo(() => {
|
||||
const recommendedQueries = [];
|
||||
if (adHocDataview && typeof adHocDataview !== 'string') {
|
||||
const queryString = `from ${adHocDataview.name}`;
|
||||
const timeFieldName =
|
||||
adHocDataview.timeFieldName ?? adHocDataview.fields?.getByType('date')?.[0]?.name;
|
||||
|
||||
recommendedQueries.push(
|
||||
...getRecommendedQueries({
|
||||
fromCommand: queryString,
|
||||
timeField: timeFieldName,
|
||||
})
|
||||
);
|
||||
}
|
||||
const panels = [
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', {
|
||||
defaultMessage: 'Quick Reference',
|
||||
}),
|
||||
icon: 'nedocumentationsted',
|
||||
renderItem: () => (
|
||||
<EuiContextMenuItem
|
||||
key="quickReference"
|
||||
icon="documentation"
|
||||
data-test-subj="esql-quick-reference"
|
||||
onClick={() => toggleLanguageComponent()}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.quickReference', {
|
||||
defaultMessage: 'Quick Reference',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
|
||||
defaultMessage: 'Documentation',
|
||||
}),
|
||||
icon: 'iInCircle',
|
||||
renderItem: () => (
|
||||
<EuiContextMenuItem
|
||||
key="about"
|
||||
icon="iInCircle"
|
||||
data-test-subj="esql-about"
|
||||
target="_blank"
|
||||
href={docLinks.links.query.queryESQL}
|
||||
onClick={() => setIsESQLMenuPopoverOpen(false)}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.documentation', {
|
||||
defaultMessage: 'Documentation',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
},
|
||||
...(Boolean(recommendedQueries.length)
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.exampleQueries', {
|
||||
defaultMessage: 'Recommended queries',
|
||||
}),
|
||||
icon: 'nested',
|
||||
panel: 1,
|
||||
'data-test-subj': 'esql-recommended-queries',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.feedback', {
|
||||
defaultMessage: 'Submit feedback',
|
||||
}),
|
||||
icon: 'editorComment',
|
||||
renderItem: () => (
|
||||
<EuiContextMenuItem
|
||||
key="feedback"
|
||||
icon="editorComment"
|
||||
data-test-subj="esql-feedback"
|
||||
target="_blank"
|
||||
href={FEEDBACK_LINK}
|
||||
onClick={() => setIsESQLMenuPopoverOpen(false)}
|
||||
>
|
||||
{i18n.translate('unifiedSearch.query.queryBar.esqlMenu.feedback', {
|
||||
defaultMessage: 'Submit feedback',
|
||||
})}
|
||||
</EuiContextMenuItem>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
initialFocusedItemIndex: 1,
|
||||
title: i18n.translate('unifiedSearch.query.queryBar.esqlMenu.exampleQueries', {
|
||||
defaultMessage: 'Recommended queries',
|
||||
}),
|
||||
items: recommendedQueries.map((query) => {
|
||||
return {
|
||||
name: query.label,
|
||||
onClick: () => {
|
||||
onESQLQuerySubmit?.(query.queryString);
|
||||
setIsESQLMenuPopoverOpen(false);
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
];
|
||||
return panels as EuiContextMenuPanelDescriptor[];
|
||||
}, [adHocDataview, docLinks.links.query.queryESQL, onESQLQuerySubmit, toggleLanguageComponent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -130,7 +187,7 @@ export const ESQLMenuPopover: React.FC<ESQLMenuPopoverProps> = ({
|
|||
panelPaddingSize="s"
|
||||
display="block"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={esqlPanelItems} />
|
||||
<EuiContextMenu initialPanelId={0} panels={esqlContextMenuPanels} />
|
||||
</EuiPopover>
|
||||
<LanguageDocumentationFlyout
|
||||
searchInDescription
|
||||
|
|
|
@ -778,6 +778,13 @@ export const QueryBarTopRow = React.memo(
|
|||
{Boolean(isQueryLangSelected) && (
|
||||
<ESQLMenuPopover
|
||||
onESQLDocsFlyoutVisibilityChanged={props.onESQLDocsFlyoutVisibilityChanged}
|
||||
onESQLQuerySubmit={(queryString: string) => {
|
||||
onSubmit({
|
||||
query: { esql: queryString } as QT,
|
||||
dateRange: dateRangeRef.current,
|
||||
});
|
||||
}}
|
||||
adHocDataview={props.indexPatterns?.[0]}
|
||||
/>
|
||||
)}
|
||||
<EuiFlexItem
|
||||
|
|
|
@ -13,12 +13,12 @@ import SearchBar from './search_bar';
|
|||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { stubIndexPattern } from '@kbn/data-plugin/public/stubs';
|
||||
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
const startMock = coreMock.createStart();
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { EuiSuperDatePicker, EuiSuperUpdateButton, EuiThemeProvider } from '@elastic/eui';
|
||||
import { FilterItems } from '../filter_bar';
|
||||
import { DataViewPicker } from '..';
|
||||
|
@ -54,21 +54,6 @@ const createMockStorage = () => ({
|
|||
clear: jest.fn(),
|
||||
});
|
||||
|
||||
const mockIndexPattern = {
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields: [
|
||||
{
|
||||
name: 'response',
|
||||
type: 'number',
|
||||
esTypes: ['integer'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
} as DataView;
|
||||
|
||||
const kqlQuery = {
|
||||
query: 'response:200',
|
||||
language: 'kuery',
|
||||
|
@ -155,7 +140,7 @@ describe('SearchBar', () => {
|
|||
it('Should render query bar when no options provided (in reality - timepicker)', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -167,7 +152,7 @@ describe('SearchBar', () => {
|
|||
it('Should render filter bar, when required fields are provided', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
showDatePicker: false,
|
||||
showQueryInput: true,
|
||||
showFilterBar: true,
|
||||
|
@ -184,7 +169,7 @@ describe('SearchBar', () => {
|
|||
it('Should NOT render filter bar, if disabled', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
showFilterBar: false,
|
||||
filters: [],
|
||||
onFiltersUpdated: noop,
|
||||
|
@ -200,7 +185,7 @@ describe('SearchBar', () => {
|
|||
it('Should render query bar, when required fields are provided', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: noop,
|
||||
query: kqlQuery,
|
||||
|
@ -215,7 +200,7 @@ describe('SearchBar', () => {
|
|||
it('Should NOT render the input query input, if disabled', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: noop,
|
||||
query: kqlQuery,
|
||||
|
@ -231,7 +216,7 @@ describe('SearchBar', () => {
|
|||
it('Should NOT render the query menu button, if disabled', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: noop,
|
||||
query: kqlQuery,
|
||||
|
@ -245,7 +230,7 @@ describe('SearchBar', () => {
|
|||
it('Should render query bar and filter bar', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
showQueryInput: true,
|
||||
onQuerySubmit: noop,
|
||||
|
@ -264,7 +249,7 @@ describe('SearchBar', () => {
|
|||
it('Should NOT render the input query input, for es|ql query', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: noop,
|
||||
query: esqlQuery,
|
||||
|
@ -277,7 +262,7 @@ describe('SearchBar', () => {
|
|||
it('Should render in isDisabled state', () => {
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: noop,
|
||||
isDisabled: true,
|
||||
|
@ -316,7 +301,7 @@ describe('SearchBar', () => {
|
|||
const mockedOnQuerySubmit = jest.fn();
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: mockedOnQuerySubmit,
|
||||
query: kqlQuery,
|
||||
|
@ -344,7 +329,7 @@ describe('SearchBar', () => {
|
|||
const mockedOnQuerySubmit = jest.fn();
|
||||
const component = mount(
|
||||
wrapSearchBarInContext({
|
||||
indexPatterns: [mockIndexPattern],
|
||||
indexPatterns: [stubIndexPattern],
|
||||
screenTitle: 'test screen',
|
||||
onQuerySubmit: mockedOnQuerySubmit,
|
||||
query: kqlQuery,
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/data-view-utils",
|
||||
"@kbn/esql-utils",
|
||||
"@kbn/esql-validation-autocomplete",
|
||||
"@kbn/react-kibana-mount",
|
||||
"@kbn/field-utils",
|
||||
"@kbn/language-documentation"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue