diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 5ca8f75a0fba0..c0fdb537aed73 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -319,6 +319,9 @@ The default sort direction for time-based data views.
[[doctable-hidetimecolumn]]`doc_table:hideTimeColumn`::
Hides the "Time" column in *Discover* and in all saved searches on dashboards.
+[[discover:enableSql]]`discover:enableSql`::
+When enabled, allows SQL queries for search.
+
[[doctable-highlight]]`doc_table:highlight`::
Highlights results in *Discover* and saved searches on dashboards. Highlighting
slows requests when working on big documents.
diff --git a/packages/kbn-es-query/src/es_query/build_es_query.ts b/packages/kbn-es-query/src/es_query/build_es_query.ts
index 62172fa5aa4d8..1a0ac9cef15ee 100644
--- a/packages/kbn-es-query/src/es_query/build_es_query.ts
+++ b/packages/kbn-es-query/src/es_query/build_es_query.ts
@@ -11,11 +11,13 @@ import { SerializableRecord } from '@kbn/utility-types';
import { buildQueryFromKuery } from './from_kuery';
import { buildQueryFromFilters } from './from_filters';
import { buildQueryFromLucene } from './from_lucene';
-import { Filter, Query } from '../filters';
+import { Filter, Query, AggregateQuery } from '../filters';
+import { isOfQueryType } from './es_query_sql';
import { BoolQuery, DataViewBase } from './types';
import type { KueryQueryOptions } from '../kuery';
import type { EsQueryFiltersConfig } from './from_filters';
+type AnyQuery = Query | AggregateQuery;
/**
* Configurations to be used while constructing an ES query.
* @public
@@ -44,7 +46,7 @@ function removeMatchAll(filters: T[]) {
*/
export function buildEsQuery(
indexPattern: DataViewBase | undefined,
- queries: Query | Query[],
+ queries: AnyQuery | AnyQuery[],
filters: Filter | Filter[],
config: EsQueryConfig = {
allowLeadingWildcards: false,
@@ -55,7 +57,7 @@ export function buildEsQuery(
queries = Array.isArray(queries) ? queries : [queries];
filters = Array.isArray(filters) ? filters : [filters];
- const validQueries = queries.filter((query) => has(query, 'query'));
+ const validQueries = queries.filter(isOfQueryType).filter((query) => has(query, 'query'));
const queriesByLanguage = groupBy(validQueries, 'language');
const kueryQuery = buildQueryFromKuery(
indexPattern,
diff --git a/packages/kbn-es-query/src/es_query/es_query_sql.test.ts b/packages/kbn-es-query/src/es_query/es_query_sql.test.ts
new file mode 100644
index 0000000000000..da909c6e5f9b4
--- /dev/null
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.test.ts
@@ -0,0 +1,84 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import {
+ isOfQueryType,
+ isOfAggregateQueryType,
+ getAggregateQueryMode,
+ getIndexPatternFromSQLQuery,
+} from './es_query_sql';
+
+describe('sql query helpers', () => {
+ describe('isOfQueryType', () => {
+ it('should return true for a Query type query', () => {
+ const flag = isOfQueryType({ query: 'foo', language: 'test' });
+ expect(flag).toBe(true);
+ });
+
+ it('should return false for an Aggregate type query', () => {
+ const flag = isOfQueryType({ sql: 'SELECT * FROM foo' });
+ expect(flag).toBe(false);
+ });
+ });
+
+ describe('isOfAggregateQueryType', () => {
+ it('should return false for a Query type query', () => {
+ const flag = isOfAggregateQueryType({ query: 'foo', language: 'test' });
+ expect(flag).toBe(false);
+ });
+
+ it('should return true for an Aggregate type query', () => {
+ const flag = isOfAggregateQueryType({ sql: 'SELECT * FROM foo' });
+ expect(flag).toBe(true);
+ });
+ });
+
+ describe('getAggregateQueryMode', () => {
+ it('should return sql for an SQL AggregateQuery type', () => {
+ const mode = getAggregateQueryMode({ sql: 'SELECT * FROM foo' });
+ expect(mode).toBe('sql');
+ });
+
+ it('should return esql for an ESQL AggregateQuery type', () => {
+ const mode = getAggregateQueryMode({ esql: 'foo | where field > 100' });
+ expect(mode).toBe('esql');
+ });
+ });
+
+ describe('getIndexPatternFromSQLQuery', () => {
+ it('should return the index pattern string from sql queries', () => {
+ const idxPattern1 = getIndexPatternFromSQLQuery('SELECT * FROM foo');
+ expect(idxPattern1).toBe('foo');
+
+ const idxPattern2 = getIndexPatternFromSQLQuery('SELECT woof, meow FROM "foo"');
+ expect(idxPattern2).toBe('foo');
+
+ const idxPattern3 = getIndexPatternFromSQLQuery('SELECT woof, meow FROM "the_index_pattern"');
+ expect(idxPattern3).toBe('the_index_pattern');
+
+ const idxPattern4 = getIndexPatternFromSQLQuery('SELECT woof, meow FROM "the-index-pattern"');
+ expect(idxPattern4).toBe('the-index-pattern');
+
+ const idxPattern5 = getIndexPatternFromSQLQuery('SELECT woof, meow from "the-index-pattern"');
+ expect(idxPattern5).toBe('the-index-pattern');
+
+ const idxPattern6 = getIndexPatternFromSQLQuery('SELECT woof, meow from "logstash-*"');
+ expect(idxPattern6).toBe('logstash-*');
+
+ const idxPattern7 = getIndexPatternFromSQLQuery(
+ 'SELECT woof, meow from logstash-1234! WHERE field > 100'
+ );
+ expect(idxPattern7).toBe('logstash-1234!');
+
+ const idxPattern8 = getIndexPatternFromSQLQuery(
+ 'SELECT * FROM (SELECT woof, miaou FROM "logstash-1234!" GROUP BY woof)'
+ );
+ expect(idxPattern8).toBe('logstash-1234!');
+ });
+ });
+});
diff --git a/packages/kbn-es-query/src/es_query/es_query_sql.ts b/packages/kbn-es-query/src/es_query/es_query_sql.ts
new file mode 100644
index 0000000000000..46de33dc04e86
--- /dev/null
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+import type { Query, AggregateQuery } from '../filters';
+
+type Language = keyof AggregateQuery;
+
+// Checks if the query is of type Query
+export function isOfQueryType(arg?: Query | AggregateQuery): arg is Query {
+ return Boolean(arg && 'query' in arg);
+}
+
+// Checks if the query is of type AggregateQuery
+// currently only supports the sql query type
+// should be enhanced to support other query types
+export function isOfAggregateQueryType(
+ query: AggregateQuery | Query | { [key: string]: any }
+): query is AggregateQuery {
+ return Boolean(query && ('sql' in query || 'esql' in query));
+}
+
+// returns the language of the aggregate Query, sql, esql etc
+export function getAggregateQueryMode(query: AggregateQuery): Language {
+ return Object.keys(query)[0] as Language;
+}
+
+// retrieves the index pattern from the aggregate query
+export function getIndexPatternFromSQLQuery(sqlQuery?: string): string {
+ let sql = sqlQuery?.replaceAll('"', '').replaceAll("'", '');
+ const splitFroms = sql?.split(new RegExp(/FROM\s/, 'ig'));
+ const fromsLength = splitFroms?.length ?? 0;
+ if (splitFroms && splitFroms?.length > 2) {
+ sql = `${splitFroms[fromsLength - 2]} FROM ${splitFroms[fromsLength - 1]}`;
+ }
+ // case insensitive match for the index pattern
+ const regex = new RegExp(/FROM\s+([\w*-.!@$^()~;]+)/, 'i');
+ const matches = sql?.match(regex);
+ if (matches) {
+ return matches[1];
+ }
+ return '';
+}
diff --git a/packages/kbn-es-query/src/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts
index d4e45b35728f6..5f14b1f03769e 100644
--- a/packages/kbn-es-query/src/es_query/index.ts
+++ b/packages/kbn-es-query/src/es_query/index.ts
@@ -13,6 +13,12 @@ export { buildEsQuery } from './build_es_query';
export { buildQueryFromFilters } from './from_filters';
export { luceneStringToDsl } from './lucene_string_to_dsl';
export { decorateQuery } from './decorate_query';
+export {
+ isOfQueryType,
+ isOfAggregateQueryType,
+ getAggregateQueryMode,
+ getIndexPatternFromSQLQuery,
+} from './es_query_sql';
export type {
IFieldSubType,
BoolQuery,
diff --git a/packages/kbn-es-query/src/filters/build_filters/types.ts b/packages/kbn-es-query/src/filters/build_filters/types.ts
index 30d44fd0b1cba..5e920d11bcab5 100644
--- a/packages/kbn-es-query/src/filters/build_filters/types.ts
+++ b/packages/kbn-es-query/src/filters/build_filters/types.ts
@@ -82,6 +82,8 @@ export type Query = {
language: string;
};
+export type AggregateQuery = { sql: string } | { esql: string };
+
/**
* An interface for a latitude-longitude pair
* @public
diff --git a/packages/kbn-es-query/src/filters/index.ts b/packages/kbn-es-query/src/filters/index.ts
index 765a4a68d2aea..820559d5f9069 100644
--- a/packages/kbn-es-query/src/filters/index.ts
+++ b/packages/kbn-es-query/src/filters/index.ts
@@ -59,6 +59,7 @@ export {
export type {
Query,
+ AggregateQuery,
Filter,
LatLon,
FieldFilter,
diff --git a/packages/kbn-es-query/src/index.ts b/packages/kbn-es-query/src/index.ts
index aadec300b5610..d29141ab39ac9 100644
--- a/packages/kbn-es-query/src/index.ts
+++ b/packages/kbn-es-query/src/index.ts
@@ -29,6 +29,7 @@ export type {
PhraseFilter,
PhrasesFilter,
Query,
+ AggregateQuery,
QueryStringFilter,
RangeFilter,
RangeFilterMeta,
@@ -52,6 +53,10 @@ export {
decorateQuery,
luceneStringToDsl,
migrateFilter,
+ isOfQueryType,
+ isOfAggregateQueryType,
+ getAggregateQueryMode,
+ getIndexPatternFromSQLQuery,
} from './es_query';
export {
diff --git a/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts b/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts
index 60116c69dca9f..0b7f9ccb0e84f 100644
--- a/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts
+++ b/src/plugins/dashboard/public/application/lib/dashboard_session_restoration.ts
@@ -8,6 +8,7 @@
import { History } from 'history';
import { createQueryParamObservable } from '@kbn/kibana-utils-plugin/public';
+import type { Query } from '@kbn/es-query';
import { DashboardAppLocatorParams, DashboardConstants } from '../..';
import { DashboardState } from '../../types';
import { getDashboardTitle } from '../../dashboard_strings';
@@ -113,7 +114,7 @@ function getLocatorParams({
timeRange: shouldRestoreSearchSession ? timefilter.getAbsoluteTime() : timefilter.getTime(),
searchSessionId: shouldRestoreSearchSession ? data.search.session.getSessionId() : undefined,
panels: getDashboardId() ? undefined : appState.panels,
- query: queryString.formatQuery(appState.query),
+ query: queryString.formatQuery(appState.query) as Query,
filters: filterManager.getFilters(),
savedQuery: appState.savedQuery,
dashboardId: getDashboardId(),
diff --git a/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts b/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts
index 94c9d996499c3..a23ae278c6978 100644
--- a/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts
+++ b/src/plugins/dashboard/public/application/lib/sync_dashboard_filter_state.ts
@@ -9,7 +9,6 @@
import _ from 'lodash';
import { merge } from 'rxjs';
import { debounceTime, finalize, map, switchMap, tap } from 'rxjs/operators';
-
import { setQuery } from '../state';
import { DashboardBuildContext, DashboardState } from '../../types';
import { DashboardSavedObject } from '../../saved_dashboards';
@@ -100,7 +99,7 @@ export const syncDashboardFilterState = ({
// apply filters when the filter manager changes
const filterManagerSubscription = merge(filterManager.getUpdates$(), queryString.getUpdates$())
.pipe(debounceTime(100))
- .subscribe(() => applyFilters(queryString.getQuery(), filterManager.getFilters()));
+ .subscribe(() => applyFilters(queryString.getQuery() as Query, filterManager.getFilters()));
const timeRefreshSubscription = merge(
timefilterService.getRefreshIntervalUpdate$(),
diff --git a/src/plugins/data/common/query/query_state.ts b/src/plugins/data/common/query/query_state.ts
index fbc5626f9a28c..a9ca73d7f0def 100644
--- a/src/plugins/data/common/query/query_state.ts
+++ b/src/plugins/data/common/query/query_state.ts
@@ -8,7 +8,7 @@
import type { Filter } from '@kbn/es-query';
import type { TimeRange, RefreshInterval } from './timefilter/types';
-import type { Query } from './types';
+import type { Query, AggregateQuery } from './types';
/**
* All query state service state
@@ -22,5 +22,5 @@ export type QueryState = {
time?: TimeRange;
refreshInterval?: RefreshInterval;
filters?: Filter[];
- query?: Query;
+ query?: Query | AggregateQuery;
};
diff --git a/src/plugins/data/common/query/to_expression_ast.test.ts b/src/plugins/data/common/query/to_expression_ast.test.ts
index 9865b70bd491c..d7c1424869aa8 100644
--- a/src/plugins/data/common/query/to_expression_ast.test.ts
+++ b/src/plugins/data/common/query/to_expression_ast.test.ts
@@ -5,106 +5,74 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import { DataViewsContract } from '@kbn/data-views-plugin/common';
import { queryStateToExpressionAst } from './to_expression_ast';
describe('queryStateToExpressionAst', () => {
- it('returns an object with the correct structure', () => {
- const actual = queryStateToExpressionAst({
+ it('returns an object with the correct structure', async () => {
+ const dataViewsService = {} as unknown as DataViewsContract;
+ const actual = await queryStateToExpressionAst({
filters: [],
query: { language: 'lucene', query: '' },
time: {
from: 'now',
to: 'now+7d',
},
+ dataViewsService,
});
- expect(actual).toMatchInlineSnapshot(`
- Object {
- "findFunction": [Function],
- "functions": Array [
- Object {
- "addArgument": [Function],
- "arguments": Object {},
- "getArgument": [Function],
- "name": "kibana",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
- },
- Object {
- "addArgument": [Function],
- "arguments": Object {
- "filters": Array [],
- "q": Array [
- Object {
- "findFunction": [Function],
- "functions": Array [
- Object {
- "addArgument": [Function],
- "arguments": Object {
- "q": Array [
- "\\"\\"",
- ],
- },
- "getArgument": [Function],
- "name": "lucene",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
- },
- ],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_builder",
- },
- ],
- "timeRange": Array [
- Object {
- "findFunction": [Function],
- "functions": Array [
- Object {
- "addArgument": [Function],
- "arguments": Object {
- "from": Array [
- "now",
- ],
- "to": Array [
- "now+7d",
- ],
- },
- "getArgument": [Function],
- "name": "timerange",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
- },
- ],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_builder",
- },
- ],
- },
- "getArgument": [Function],
- "name": "kibana_context",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
+ expect(actual).toHaveProperty(
+ 'chain.1.arguments.timeRange.0.chain.0.arguments',
+ expect.objectContaining({
+ from: ['now'],
+ to: ['now+7d'],
+ })
+ );
+
+ expect(actual).toHaveProperty('chain.1.arguments.filters', expect.arrayContaining([]));
+ });
+
+ it('returns an object with the correct structure for an SQL query', async () => {
+ const dataViewsService = {
+ getIdsWithTitle: jest.fn(() => {
+ return [
+ {
+ title: 'foo',
+ id: 'bar',
},
- ],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_builder",
- }
- `);
+ ];
+ }),
+ get: jest.fn(() => {
+ return {
+ title: 'foo',
+ id: 'bar',
+ timeFieldName: 'baz',
+ };
+ }),
+ } as unknown as DataViewsContract;
+ const actual = await queryStateToExpressionAst({
+ filters: [],
+ query: { sql: 'SELECT * FROM foo' },
+ time: {
+ from: 'now',
+ to: 'now+7d',
+ },
+ dataViewsService,
+ });
+
+ expect(actual).toHaveProperty(
+ 'chain.1.arguments.timeRange.0.chain.0.arguments',
+ expect.objectContaining({
+ from: ['now'],
+ to: ['now+7d'],
+ })
+ );
+
+ expect(actual).toHaveProperty(
+ 'chain.2.arguments',
+ expect.objectContaining({
+ query: ['SELECT * FROM foo'],
+ })
+ );
});
});
diff --git a/src/plugins/data/common/query/to_expression_ast.ts b/src/plugins/data/common/query/to_expression_ast.ts
index 929a7cc928cdd..e522ea367a47e 100644
--- a/src/plugins/data/common/query/to_expression_ast.ts
+++ b/src/plugins/data/common/query/to_expression_ast.ts
@@ -5,31 +5,73 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import {
+ isOfAggregateQueryType,
+ getAggregateQueryMode,
+ getIndexPatternFromSQLQuery,
+ Query,
+} from '@kbn/es-query';
import { buildExpression, buildExpressionFunction } from '@kbn/expressions-plugin/common';
+import type { DataViewsContract } from '@kbn/data-views-plugin/common';
import {
ExpressionFunctionKibana,
ExpressionFunctionKibanaContext,
- filtersToAst,
QueryState,
+ aggregateQueryToAst,
queryToAst,
+ filtersToAst,
timerangeToAst,
} from '..';
+interface Args extends QueryState {
+ dataViewsService: DataViewsContract;
+ inputQuery?: Query;
+}
+
/**
* Converts QueryState to expression AST
* @param filters array of kibana filters
- * @param query kibana query
+ * @param query kibana query or aggregate query
* @param time kibana time range
*/
-export function queryStateToExpressionAst({ filters, query, time }: QueryState) {
+export async function queryStateToExpressionAst({
+ filters,
+ query,
+ inputQuery,
+ time,
+ dataViewsService,
+}: Args) {
const kibana = buildExpressionFunction('kibana', {});
+ let q;
+ if (inputQuery) {
+ q = inputQuery;
+ }
const kibanaContext = buildExpressionFunction('kibana_context', {
- q: query && queryToAst(query),
- filters: filters && filtersToAst(filters),
+ q: q && queryToAst(q),
timeRange: time && timerangeToAst(time),
+ filters: filters && filtersToAst(filters),
});
+ const ast = buildExpression([kibana, kibanaContext]).toAst();
+
+ if (query && isOfAggregateQueryType(query)) {
+ const mode = getAggregateQueryMode(query);
+ // sql query
+ if (mode === 'sql' && 'sql' in query) {
+ const idxPattern = getIndexPatternFromSQLQuery(query.sql);
+ const idsTitles = await dataViewsService.getIdsWithTitle();
+ const dataViewIdTitle = idsTitles.find(({ title }) => title === idxPattern);
+ if (dataViewIdTitle) {
+ const dataView = await dataViewsService.get(dataViewIdTitle.id);
+ const timeFieldName = dataView.timeFieldName;
+ const essql = aggregateQueryToAst(query, timeFieldName);
- const ast = buildExpression([kibana, kibanaContext]);
+ if (essql) {
+ ast.chain.push(essql);
+ }
+ } else {
+ throw new Error(`No data view found for index pattern ${idxPattern}`);
+ }
+ }
+ }
return ast;
}
diff --git a/src/plugins/data/common/query/types.ts b/src/plugins/data/common/query/types.ts
index fea59ea558a35..e10afaf746455 100644
--- a/src/plugins/data/common/query/types.ts
+++ b/src/plugins/data/common/query/types.ts
@@ -10,7 +10,7 @@ import type { Query, Filter } from '@kbn/es-query';
import type { RefreshInterval, TimeRange } from './timefilter/types';
export type { RefreshInterval, TimeRange, TimeRangeBounds } from './timefilter/types';
-export type { Query } from '@kbn/es-query';
+export type { Query, AggregateQuery } from '@kbn/es-query';
export type SavedQueryTimeFilter = TimeRange & {
refreshInterval: RefreshInterval;
diff --git a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts
new file mode 100644
index 0000000000000..f292954feea82
--- /dev/null
+++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { aggregateQueryToAst } from './aggregate_query_to_ast';
+
+describe('aggregateQueryToAst', () => {
+ it('should return a function', () => {
+ expect(aggregateQueryToAst({ sql: 'SELECT * from foo' })).toHaveProperty('type', 'function');
+ });
+
+ it('should forward arguments', () => {
+ expect(aggregateQueryToAst({ sql: 'SELECT * from foo' }, 'baz')).toHaveProperty(
+ 'arguments',
+ expect.objectContaining({
+ query: ['SELECT * from foo'],
+ timeField: ['baz'],
+ })
+ );
+ });
+});
diff --git a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
new file mode 100644
index 0000000000000..84e1e4e5f2262
--- /dev/null
+++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { buildExpressionFunction, ExpressionAstFunction } from '@kbn/expressions-plugin/common';
+import { AggregateQuery } from '../../query';
+import { EssqlExpressionFunctionDefinition } from './essql';
+
+export const aggregateQueryToAst = (
+ query: AggregateQuery,
+ timeField?: string
+): undefined | ExpressionAstFunction => {
+ if ('sql' in query) {
+ return buildExpressionFunction('essql', {
+ query: query.sql,
+ timeField,
+ }).toAst();
+ }
+};
diff --git a/src/plugins/data/common/search/expressions/essql.ts b/src/plugins/data/common/search/expressions/essql.ts
index 1038bf422fee8..398b92de490d8 100644
--- a/src/plugins/data/common/search/expressions/essql.ts
+++ b/src/plugins/data/common/search/expressions/essql.ts
@@ -40,9 +40,9 @@ type Output = Observable;
interface Arguments {
query: string;
- parameter: Array;
- count: number;
- timezone: string;
+ parameter?: Array;
+ count?: number;
+ timezone?: string;
timeField?: string;
}
diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts
index 23d3b865b4c05..8c37836e30dea 100644
--- a/src/plugins/data/common/search/expressions/index.ts
+++ b/src/plugins/data/common/search/expressions/index.ts
@@ -27,6 +27,7 @@ export * from './numerical_range_to_ast';
export * from './query_filter';
export * from './query_filter_to_ast';
export * from './query_to_ast';
+export * from './aggregate_query_to_ast';
export * from './timerange_to_ast';
export * from './kibana_context_type';
export * from './esaggs';
diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts
index 6183484a57b46..5c29ed23aad76 100644
--- a/src/plugins/data/common/search/expressions/kibana_context.ts
+++ b/src/plugins/data/common/search/expressions/kibana_context.ts
@@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, ExecutionContext } from '@kbn/expressions-plugin/common';
import { Adapters } from '@kbn/inspector-plugin/common';
import { Filter } from '@kbn/es-query';
-import { Query, uniqFilters } from '@kbn/es-query';
+import { Query, uniqFilters, AggregateQuery } from '@kbn/es-query';
import { unboxExpressionValue } from '@kbn/expressions-plugin/common';
import { SavedObjectReference } from '@kbn/core/types';
import { SavedObjectsClientCommon } from '@kbn/data-views-plugin/common';
@@ -41,8 +41,11 @@ export type ExpressionFunctionKibanaContext = ExpressionFunctionDefinition<
const getParsedValue = (data: any, defaultValue: any) =>
typeof data === 'string' && data.length ? JSON.parse(data) || defaultValue : defaultValue;
-const mergeQueries = (first: Query | Query[] = [], second: Query | Query[]) =>
- uniqBy(
+const mergeQueries = (
+ first: Query | AggregateQuery | Array = [],
+ second: Query | AggregateQuery | Array
+) =>
+ uniqBy(
[...(Array.isArray(first) ? first : [first]), ...(Array.isArray(second) ? second : [second])],
(n: any) => JSON.stringify(n.query)
);
diff --git a/src/plugins/data/common/search/expressions/kibana_context_type.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts
index 53a443166a57f..3c65f4e20a070 100644
--- a/src/plugins/data/common/search/expressions/kibana_context_type.ts
+++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts
@@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import { Filter } from '@kbn/es-query';
+import { Filter, AggregateQuery } from '@kbn/es-query';
import { ExpressionValueBoxed, ExpressionValueFilter } from '@kbn/expressions-plugin/common';
import { Query, TimeRange } from '../../query';
import { adaptToExpressionValueFilter, DataViewField } from '../..';
@@ -13,7 +13,7 @@ import { adaptToExpressionValueFilter, DataViewField } from '../..';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ExecutionContextSearch = {
filters?: Filter[];
- query?: Query | Query[];
+ query?: Query | AggregateQuery | Array;
timeRange?: TimeRange;
};
diff --git a/src/plugins/data/common/search/search_source/migrate_legacy_query.ts b/src/plugins/data/common/search/search_source/migrate_legacy_query.ts
index 70961d705f7b8..1395173562953 100644
--- a/src/plugins/data/common/search/search_source/migrate_legacy_query.ts
+++ b/src/plugins/data/common/search/search_source/migrate_legacy_query.ts
@@ -5,9 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import { Query, AggregateQuery, isOfAggregateQueryType } from '@kbn/es-query';
import { has } from 'lodash';
-import { Query } from '../../query/types';
/**
* Creates a standardized query object from old queries that were either strings or pure ES query DSL
@@ -16,9 +15,15 @@ import { Query } from '../../query/types';
* @return Object
*/
-export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query {
+export function migrateLegacyQuery(
+ query: Query | { [key: string]: any } | string | AggregateQuery
+): Query | AggregateQuery {
// Lucene was the only option before, so language-less queries are all lucene
+ // If the query is already a AggregateQuery, just return it
if (!has(query, 'language')) {
+ if (typeof query === 'object' && isOfAggregateQueryType(query)) {
+ return query;
+ }
return { query, language: 'lucene' };
}
diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts
index a3f127573644d..3c0ea55799641 100644
--- a/src/plugins/data/common/search/search_source/search_source.ts
+++ b/src/plugins/data/common/search/search_source/search_source.ts
@@ -72,7 +72,7 @@ import {
} from 'rxjs/operators';
import { defer, EMPTY, from, lastValueFrom, Observable } from 'rxjs';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
-import { buildEsQuery, Filter } from '@kbn/es-query';
+import { buildEsQuery, Filter, isOfQueryType } from '@kbn/es-query';
import { fieldWildcardFilter } from '@kbn/kibana-utils-plugin/common';
import { getHighlightRequest } from '@kbn/field-formats-plugin/common';
import type { DataView } from '@kbn/data-views-plugin/common';
@@ -261,7 +261,11 @@ export class SearchSource {
filters = this.getFilters(originalFilters);
}
- const queryString = Array.isArray(query) ? query.map((q) => q.query) : query?.query;
+ const queryString = Array.isArray(query)
+ ? query.map((q) => q.query)
+ : isOfQueryType(query)
+ ? query?.query
+ : undefined;
const indexPatternFromQuery =
typeof queryString === 'string'
diff --git a/src/plugins/data/common/search/search_source/types.ts b/src/plugins/data/common/search/search_source/types.ts
index 04cee3603bedb..3cee6355ccbfa 100644
--- a/src/plugins/data/common/search/search_source/types.ts
+++ b/src/plugins/data/common/search/search_source/types.ts
@@ -7,13 +7,13 @@
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { Query, AggregateQuery } from '@kbn/es-query';
import { SerializableRecord } from '@kbn/utility-types';
import { PersistableStateService } from '@kbn/kibana-utils-plugin/common';
import type { Filter } from '@kbn/es-query';
import type { DataView, DataViewSpec } from '@kbn/data-views-plugin/common';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { AggConfigSerialized, IAggConfigs } from '../../../public';
-import { Query } from '../..';
import type { SearchSource } from './search_source';
/**
@@ -78,7 +78,7 @@ export interface SearchSourceFields {
/**
* {@link Query}
*/
- query?: Query;
+ query?: Query | AggregateQuery;
/**
* {@link Filter}
*/
@@ -125,7 +125,7 @@ export type SerializedSearchSourceFields = {
/**
* {@link Query}
*/
- query?: Query;
+ query?: Query | AggregateQuery;
/**
* {@link Filter}
*/
diff --git a/src/plugins/data/public/query/query_string/query_string_manager.test.ts b/src/plugins/data/public/query/query_string/query_string_manager.test.ts
index 7ba28a7e04cd9..2da2ee68c86e6 100644
--- a/src/plugins/data/public/query/query_string/query_string_manager.test.ts
+++ b/src/plugins/data/public/query/query_string/query_string_manager.test.ts
@@ -10,7 +10,7 @@ import { QueryStringManager } from './query_string_manager';
import { Storage } from '@kbn/kibana-utils-plugin/public/storage';
import { StubBrowserStorage } from '@kbn/test-jest-helpers';
import { coreMock } from '@kbn/core/public/mocks';
-import { Query } from '../../../common/query';
+import { Query, AggregateQuery } from '../../../common/query';
describe('QueryStringManager', () => {
let service: QueryStringManager;
@@ -24,7 +24,7 @@ describe('QueryStringManager', () => {
test('getUpdates$ is a cold emits only after query changes', () => {
const obs$ = service.getUpdates$();
- const emittedValues: Query[] = [];
+ const emittedValues: Array = [];
obs$.subscribe((v) => {
emittedValues.push(v);
});
diff --git a/src/plugins/data/public/query/query_string/query_string_manager.ts b/src/plugins/data/public/query/query_string/query_string_manager.ts
index f8d9025ad0db4..b259b29130055 100644
--- a/src/plugins/data/public/query/query_string/query_string_manager.ts
+++ b/src/plugins/data/public/query/query_string/query_string_manager.ts
@@ -10,18 +10,19 @@ import { BehaviorSubject } from 'rxjs';
import { skip } from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
import { CoreStart } from '@kbn/core/public';
-import type { Query } from '@kbn/es-query';
+import type { Query, AggregateQuery } from '@kbn/es-query';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
+import { isEqual } from 'lodash';
import { KIBANA_USER_QUERY_LANGUAGE_KEY, UI_SETTINGS } from '../../../common';
export class QueryStringManager {
- private query$: BehaviorSubject;
+ private query$: BehaviorSubject;
constructor(
private readonly storage: IStorageWrapper,
private readonly uiSettings: CoreStart['uiSettings']
) {
- this.query$ = new BehaviorSubject(this.getDefaultQuery());
+ this.query$ = new BehaviorSubject(this.getDefaultQuery());
}
private getDefaultLanguage() {
@@ -38,7 +39,7 @@ export class QueryStringManager {
};
}
- public formatQuery(query: Query | string | undefined): Query {
+ public formatQuery(query: Query | AggregateQuery | string | undefined): Query | AggregateQuery {
if (!query) {
return this.getDefaultQuery();
} else if (typeof query === 'string') {
@@ -55,17 +56,17 @@ export class QueryStringManager {
return this.query$.asObservable().pipe(skip(1));
};
- public getQuery = (): Query => {
+ public getQuery = (): Query | AggregateQuery => {
return this.query$.getValue();
};
/**
* Updates the query.
- * @param {Query} query
+ * @param {Query | AggregateQuery} query
*/
- public setQuery = (query: Query) => {
+ public setQuery = (query: Query | AggregateQuery) => {
const curQuery = this.query$.getValue();
- if (query?.language !== curQuery.language || query?.query !== curQuery.query) {
+ if (!isEqual(query, curQuery)) {
this.query$.next(query);
}
};
diff --git a/src/plugins/data/server/query/route_handler_context.test.ts b/src/plugins/data/server/query/route_handler_context.test.ts
index 33d4597ecff0b..1c274fcfc3953 100644
--- a/src/plugins/data/server/query/route_handler_context.test.ts
+++ b/src/plugins/data/server/query/route_handler_context.test.ts
@@ -7,7 +7,7 @@
*/
import { coreMock } from '@kbn/core/server/mocks';
-import { FilterStateStore } from '@kbn/es-query';
+import { FilterStateStore, Query } from '@kbn/es-query';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../common';
import type { SavedObject, SavedQueryAttributes } from '../../common';
import { registerSavedQueryRouteHandlerContext } from './route_handler_context';
@@ -438,7 +438,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
- expect(response.attributes.query.query).toEqual({ x: 'y' });
+ const query = response.attributes.query as Query;
+ expect(query.query).toEqual({ x: 'y' });
});
it('should handle null string', async () => {
@@ -460,7 +461,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
- expect(response.attributes.query.query).toEqual('null');
+ const query = response.attributes.query as Query;
+ expect(query.query).toEqual('null');
});
it('should handle null quoted string', async () => {
@@ -482,7 +484,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
- expect(response.attributes.query.query).toEqual('"null"');
+ const query = response.attributes.query as Query;
+ expect(query.query).toEqual('"null"');
});
it('should not lose quotes', async () => {
@@ -504,7 +507,8 @@ describe('saved query route handler context', () => {
});
const response = await context.get('food');
- expect(response.attributes.query.query).toEqual('"Bob"');
+ const query = response.attributes.query as Query;
+ expect(query.query).toEqual('"Bob"');
});
it('should inject references', async () => {
diff --git a/src/plugins/data/server/query/route_handler_context.ts b/src/plugins/data/server/query/route_handler_context.ts
index a79063014abc0..6179a909eee57 100644
--- a/src/plugins/data/server/query/route_handler_context.ts
+++ b/src/plugins/data/server/query/route_handler_context.ts
@@ -7,7 +7,7 @@
*/
import { CustomRequestHandlerContext, RequestHandlerContext, SavedObject } from '@kbn/core/server';
-import { isFilters } from '@kbn/es-query';
+import { isFilters, isOfQueryType } from '@kbn/es-query';
import { isQuery, SavedQueryAttributes } from '../../common';
import { extract, inject } from '../../common/query/filters/persistable_state';
@@ -17,7 +17,7 @@ function injectReferences({
references,
}: Pick, 'id' | 'attributes' | 'references'>) {
const { query } = attributes;
- if (typeof query.query === 'string') {
+ if (isOfQueryType(query) && typeof query.query === 'string') {
try {
const parsed = JSON.parse(query.query);
query.query = parsed instanceof Object ? parsed : query.query;
@@ -37,13 +37,22 @@ function extractReferences({
timefilter,
}: SavedQueryAttributes) {
const { state: extractedFilters, references } = extract(filters);
+ const isOfQueryTypeQuery = isOfQueryType(query);
+ let queryString = '';
+ if (isOfQueryTypeQuery) {
+ if (typeof query.query === 'string') {
+ queryString = query.query;
+ } else {
+ queryString = JSON.stringify(query.query);
+ }
+ }
const attributes: SavedQueryAttributes = {
title: title.trim(),
description: description.trim(),
query: {
...query,
- query: typeof query.query === 'string' ? query.query : JSON.stringify(query.query),
+ ...(queryString && { query: queryString }),
},
filters: extractedFilters,
...(timefilter && { timefilter }),
diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts
index df0b64e5f102c..c9ce8777f386e 100644
--- a/src/plugins/discover/common/index.ts
+++ b/src/plugins/discover/common/index.ts
@@ -28,3 +28,4 @@ export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight';
export const ROW_HEIGHT_OPTION = 'discover:rowHeightOption';
export const SEARCH_EMBEDDABLE_TYPE = 'search';
export const HIDE_ANNOUNCEMENTS = 'hideAnnouncements';
+export const ENABLE_SQL = 'discover:enableSql';
diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json
index cb40433b73fa1..7c8376795863a 100644
--- a/src/plugins/discover/kibana.json
+++ b/src/plugins/discover/kibana.json
@@ -14,7 +14,8 @@
"uiActions",
"savedObjects",
"dataViewFieldEditor",
- "dataViewEditor"
+ "dataViewEditor",
+ "expressions"
],
"optionalPlugins": ["home", "share", "usageCollection", "spaces", "triggersActionsUi"],
"requiredBundles": ["kibanaUtils", "kibanaReact", "dataViews", "unifiedSearch"],
diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_index_pattern_mock.tsx b/src/plugins/discover/public/__mocks__/__storybook_mocks__/get_index_pattern_mock.tsx
similarity index 100%
rename from src/plugins/discover/public/application/main/components/layout/__stories__/get_index_pattern_mock.tsx
rename to src/plugins/discover/public/__mocks__/__storybook_mocks__/get_index_pattern_mock.tsx
diff --git a/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx b/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx
new file mode 100644
index 0000000000000..bd1bb210935b9
--- /dev/null
+++ b/src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx
@@ -0,0 +1,139 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { FunctionComponent } from 'react';
+import { action } from '@storybook/addon-actions';
+import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
+import { LIGHT_THEME } from '@elastic/charts';
+import { FieldFormat } from '@kbn/field-formats-plugin/common';
+import { identity } from 'lodash';
+import { CoreStart, IUiSettingsClient, PluginInitializerContext } from '@kbn/core/public';
+import {
+ DEFAULT_COLUMNS_SETTING,
+ DOC_TABLE_LEGACY,
+ MAX_DOC_FIELDS_DISPLAYED,
+ ROW_HEIGHT_OPTION,
+ SAMPLE_SIZE_SETTING,
+ SEARCH_FIELDS_FROM_SOURCE,
+ SHOW_MULTIFIELDS,
+} from '../../../common';
+import { SIDEBAR_CLOSED_KEY } from '../../application/main/components/layout/discover_layout';
+import { LocalStorageMock } from '../local_storage_mock';
+import { DiscoverServices } from '../../build_services';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { Plugin as NavigationPublicPlugin } from '@kbn/navigation-plugin/public';
+import { SearchBar, UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
+import { SavedQuery } from '@kbn/data-plugin/public';
+
+const NavigationPlugin = new NavigationPublicPlugin({} as PluginInitializerContext);
+
+export const uiSettingsMock = {
+ get: (key: string) => {
+ if (key === MAX_DOC_FIELDS_DISPLAYED) {
+ return 3;
+ } else if (key === SAMPLE_SIZE_SETTING) {
+ return 10;
+ } else if (key === DEFAULT_COLUMNS_SETTING) {
+ return ['default_column'];
+ } else if (key === DOC_TABLE_LEGACY) {
+ return false;
+ } else if (key === SEARCH_FIELDS_FROM_SOURCE) {
+ return false;
+ } else if (key === SHOW_MULTIFIELDS) {
+ return false;
+ } else if (key === ROW_HEIGHT_OPTION) {
+ return 3;
+ } else if (key === 'dateFormat:tz') {
+ return true;
+ }
+ },
+ isDefault: () => {
+ return true;
+ },
+} as unknown as IUiSettingsClient;
+
+const services = {
+ core: { http: { basePath: { prepend: () => void 0 } } },
+ storage: new LocalStorageMock({
+ [SIDEBAR_CLOSED_KEY]: false,
+ }) as unknown as Storage,
+ data: {
+ query: {
+ timefilter: {
+ timefilter: {
+ setTime: action('Set timefilter time'),
+ getAbsoluteTime: () => {
+ return { from: '2020-05-14T11:05:13.590', to: '2020-05-14T11:20:13.590' };
+ },
+ },
+ },
+ savedQueries: { findSavedQueries: () => Promise.resolve({ queries: [] as SavedQuery[] }) },
+ },
+ dataViews: {
+ getIdsWithTitle: () => Promise.resolve([]),
+ },
+ },
+ uiSettings: uiSettingsMock,
+ dataViewFieldEditor: {
+ openEditor: () => void 0,
+ userPermissions: {
+ editIndexPattern: () => void 0,
+ },
+ },
+ navigation: NavigationPlugin.start({} as CoreStart, {
+ unifiedSearch: { ui: { SearchBar } } as unknown as UnifiedSearchPublicPluginStart,
+ }),
+ theme: {
+ useChartsTheme: () => ({
+ ...EUI_CHARTS_THEME_LIGHT.theme,
+ chartPaddings: {
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0,
+ },
+ heatmap: { xAxisLabel: { rotation: {} } },
+ }),
+ useChartsBaseTheme: () => LIGHT_THEME,
+ },
+ capabilities: {
+ visualize: {
+ show: true,
+ },
+ discover: {
+ save: false,
+ },
+ advancedSettings: {
+ save: true,
+ },
+ },
+ docLinks: { links: { discover: {} } },
+ addBasePath: (path: string) => path,
+ filterManager: {
+ getGlobalFilters: () => [],
+ getAppFilters: () => [],
+ },
+ history: () => ({}),
+ fieldFormats: {
+ deserialize: () => {
+ const DefaultFieldFormat = FieldFormat.from(identity);
+ return new DefaultFieldFormat();
+ },
+ },
+ toastNotifications: {
+ addInfo: action('add toast'),
+ },
+} as unknown as DiscoverServices;
+
+export const withDiscoverServices = (Component: FunctionComponent) => {
+ return (props: object) => (
+
+
+
+ );
+};
diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts
index 3636c4a84f83f..ae361d961a6e8 100644
--- a/src/plugins/discover/public/__mocks__/services.ts
+++ b/src/plugins/discover/public/__mocks__/services.ts
@@ -8,6 +8,7 @@
import { EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
import { DiscoverServices } from '../build_services';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
+import { expressionsPluginMock } from '@kbn/expressions-plugin/public/mocks';
import { chromeServiceMock, coreMock, docLinksServiceMock } from '@kbn/core/public/mocks';
import {
CONTEXT_STEP_SETTING,
@@ -25,6 +26,7 @@ import { FORMATS_UI_SETTINGS } from '@kbn/field-formats-plugin/common';
import { LocalStorageMock } from './local_storage_mock';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
const dataPlugin = dataPluginMock.createStartContract();
+const expressionsPlugin = expressionsPluginMock.createStartContract();
export const discoverServiceMock = {
core: coreMock.createStart(),
@@ -95,7 +97,7 @@ export const discoverServiceMock = {
},
},
navigation: {
- ui: { TopNavMenu },
+ ui: { TopNavMenu, AggregateQueryTopNavMenu: TopNavMenu },
},
metadata: {
branch: 'test',
@@ -110,4 +112,5 @@ export const discoverServiceMock = {
addInfo: jest.fn(),
addWarning: jest.fn(),
},
+ expressions: expressionsPlugin,
} as unknown as DiscoverServices;
diff --git a/src/plugins/discover/public/application/context/context_app.test.tsx b/src/plugins/discover/public/application/context/context_app.test.tsx
index 032e815690d70..b213f98eb530f 100644
--- a/src/plugins/discover/public/application/context/context_app.test.tsx
+++ b/src/plugins/discover/public/application/context/context_app.test.tsx
@@ -24,7 +24,9 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
const mockFilterManager = createFilterManagerMock();
-const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } };
+const mockNavigationPlugin = {
+ ui: { TopNavMenu: mockTopNavMenu, AggregateQueryTopNavMenu: mockTopNavMenu },
+};
describe('ContextApp test', () => {
const services = {
diff --git a/src/plugins/discover/public/application/context/context_app.tsx b/src/plugins/discover/public/application/context/context_app.tsx
index 74f5910a4dec3..9d6adad8396b2 100644
--- a/src/plugins/discover/public/application/context/context_app.tsx
+++ b/src/plugins/discover/public/application/context/context_app.tsx
@@ -135,7 +135,7 @@ export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => {
[filterManager, indexPatterns, indexPattern, capabilities]
);
- const TopNavMenu = navigation.ui.TopNavMenu;
+ const TopNavMenu = navigation.ui.AggregateQueryTopNavMenu;
const getNavBarProps = () => {
return {
appName: 'context',
diff --git a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx
index 44dcb0901dd7c..3b46dbe1b8bca 100644
--- a/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx
+++ b/src/plugins/discover/public/application/main/components/document_explorer_callout/document_explorer_update_callout.test.tsx
@@ -62,7 +62,7 @@ describe('Document Explorer Update callout', () => {
it('should start a tour when the button is clicked', () => {
const result = mountWithIntl(
-
+
diff --git a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx
index be6e2dd54e9d9..9306c972dadd9 100644
--- a/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx
+++ b/src/plugins/discover/public/application/main/components/field_stats_table/field_stats_table.tsx
@@ -7,7 +7,7 @@
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
-import type { Filter, Query } from '@kbn/es-query';
+import type { Filter, Query, AggregateQuery } from '@kbn/es-query';
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
import type { DataViewField, DataView } from '@kbn/data-views-plugin/public';
import {
@@ -26,7 +26,7 @@ import { AvailableFields$, DataRefetch$ } from '../../hooks/use_saved_search';
export interface DataVisualizerGridEmbeddableInput extends EmbeddableInput {
dataView: DataView;
savedSearch?: SavedSearch;
- query?: Query;
+ query?: Query | AggregateQuery;
visibleFieldNames?: string[];
filters?: Filter[];
showPreviewByDefault?: boolean;
@@ -65,7 +65,7 @@ export interface FieldStatisticsTableProps {
/**
* Optional query to update the table content
*/
- query?: Query;
+ query?: Query | AggregateQuery;
/**
* Filters query to update the table content
*/
diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx b/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx
index 72f44ed63e4c2..8cbe66111e5d2 100644
--- a/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/__stories__/discover_layout.stories.tsx
@@ -6,30 +6,64 @@
* Side Public License, v 1.
*/
+import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
-import React from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
-import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
-import { getIndexPatternMock } from './get_index_pattern_mock';
-import { getServices } from './get_services';
-import { getLayoutProps } from './get_layout_props';
+import { getIndexPatternMock } from '../../../../../__mocks__/__storybook_mocks__/get_index_pattern_mock';
+import { withDiscoverServices } from '../../../../../__mocks__/__storybook_mocks__/with_discover_services';
+import { getDocumentsLayoutProps, getPlainRecordLayoutProps } from './get_layout_props';
import { DiscoverLayout } from '../discover_layout';
import { setHeaderActionMenuMounter } from '../../../../../kibana_services';
+import { AppState } from '../../../services/discover_state';
+import { DiscoverLayoutProps } from '../types';
setHeaderActionMenuMounter(() => void 0);
-storiesOf('components/layout/DiscoverLayout', module).add('Data view with timestamp', () => (
-
-
-
-
-
-));
-
-storiesOf('components/layout/DiscoverLayout', module).add('Data view without timestamp', () => (
-
-
-
-
-
-));
+const DiscoverLayoutStory = (layoutProps: DiscoverLayoutProps) => {
+ const [state, setState] = useState(layoutProps.state);
+
+ const setAppState = (newState: Partial) => {
+ setState((prevState) => ({ ...prevState, ...newState }));
+ };
+
+ const getState = () => state;
+
+ return (
+
+ );
+};
+
+storiesOf('components/layout/DiscoverLayout', module).add(
+ 'Data view with timestamp',
+ withDiscoverServices(() => (
+
+
+
+ ))
+);
+
+storiesOf('components/layout/DiscoverLayout', module).add(
+ 'Data view without timestamp',
+ withDiscoverServices(() => (
+
+
+
+ ))
+);
+
+storiesOf('components/layout/DiscoverLayout', module).add(
+ 'SQL view',
+ withDiscoverServices(() => (
+
+
+
+ ))
+);
diff --git a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts
index 7aea5b191a36e..6b717d20ff6b2 100644
--- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts
+++ b/src/plugins/discover/public/application/main/components/layout/__stories__/get_layout_props.ts
@@ -18,81 +18,76 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
+ RecordRawType,
} from '../../../hooks/use_saved_search';
import { buildDataTableRecordList } from '../../../../../utils/build_data_record';
import { esHits } from '../../../../../__mocks__/es_hits';
import { Chart } from '../../chart/point_series';
import { SavedSearch } from '../../../../..';
-import { GetStateReturn } from '../../../services/discover_state';
import { DiscoverLayoutProps } from '../types';
+import { GetStateReturn } from '../../../services/discover_state';
-export function getLayoutProps(indexPattern: DataView) {
- const searchSourceMock = {} as unknown as SearchSource;
-
- const indexPatternList = [indexPattern].map((ip) => {
- return { ...ip, ...{ attributes: { title: ip.title } } };
- }) as unknown as Array>;
+const chartData = {
+ xAxisOrderedValues: [
+ 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000,
+ 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000,
+ 1624917600000, 1625004000000, 1625090400000,
+ ],
+ xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
+ xAxisLabel: 'order_date per day',
+ yAxisFormat: { id: 'number' },
+ ordered: {
+ date: true,
+ interval: {
+ asMilliseconds: () => 1000,
+ },
+ intervalESUnit: 'd',
+ intervalESValue: 1,
+ min: '2021-03-18T08:28:56.411Z',
+ max: '2021-07-01T07:28:56.411Z',
+ },
+ yAxisLabel: 'Count',
+ values: [
+ { x: 1623880800000, y: 134 },
+ { x: 1623967200000, y: 152 },
+ { x: 1624053600000, y: 141 },
+ { x: 1624140000000, y: 138 },
+ { x: 1624226400000, y: 142 },
+ { x: 1624312800000, y: 157 },
+ { x: 1624399200000, y: 149 },
+ { x: 1624485600000, y: 146 },
+ { x: 1624572000000, y: 170 },
+ { x: 1624658400000, y: 137 },
+ { x: 1624744800000, y: 150 },
+ { x: 1624831200000, y: 144 },
+ { x: 1624917600000, y: 147 },
+ { x: 1625004000000, y: 137 },
+ { x: 1625090400000, y: 66 },
+ ],
+} as unknown as Chart;
- const main$ = new BehaviorSubject({
+const documentObservables = {
+ main$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
foundDocuments: true,
- }) as DataMain$;
+ }) as DataMain$,
- const documents$ = new BehaviorSubject({
+ documents$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: buildDataTableRecordList(esHits),
- }) as DataDocuments$;
+ }) as DataDocuments$,
- const availableFields$ = new BehaviorSubject({
+ availableFields$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
fields: [] as string[],
- }) as AvailableFields$;
+ }) as AvailableFields$,
- const totalHits$ = new BehaviorSubject({
+ totalHits$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
result: Number(esHits.length),
- }) as DataTotalHits$;
-
- const chartData = {
- xAxisOrderedValues: [
- 1623880800000, 1623967200000, 1624053600000, 1624140000000, 1624226400000, 1624312800000,
- 1624399200000, 1624485600000, 1624572000000, 1624658400000, 1624744800000, 1624831200000,
- 1624917600000, 1625004000000, 1625090400000,
- ],
- xAxisFormat: { id: 'date', params: { pattern: 'YYYY-MM-DD' } },
- xAxisLabel: 'order_date per day',
- yAxisFormat: { id: 'number' },
- ordered: {
- date: true,
- interval: {
- asMilliseconds: () => 1000,
- },
- intervalESUnit: 'd',
- intervalESValue: 1,
- min: '2021-03-18T08:28:56.411Z',
- max: '2021-07-01T07:28:56.411Z',
- },
- yAxisLabel: 'Count',
- values: [
- { x: 1623880800000, y: 134 },
- { x: 1623967200000, y: 152 },
- { x: 1624053600000, y: 141 },
- { x: 1624140000000, y: 138 },
- { x: 1624226400000, y: 142 },
- { x: 1624312800000, y: 157 },
- { x: 1624399200000, y: 149 },
- { x: 1624485600000, y: 146 },
- { x: 1624572000000, y: 170 },
- { x: 1624658400000, y: 137 },
- { x: 1624744800000, y: 150 },
- { x: 1624831200000, y: 144 },
- { x: 1624917600000, y: 147 },
- { x: 1625004000000, y: 137 },
- { x: 1625090400000, y: 66 },
- ],
- } as unknown as Chart;
-
- const charts$ = new BehaviorSubject({
+ }) as DataTotalHits$,
+
+ charts$: new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
chartData,
bucketInterval: {
@@ -100,29 +95,59 @@ export function getLayoutProps(indexPattern: DataView) {
description: 'test',
scale: 2,
},
- }) as DataCharts$;
-
- const savedSearchData$ = {
- main$,
- documents$,
- totalHits$,
- charts$,
- availableFields$,
- };
+ }) as DataCharts$,
+};
+
+const plainRecordObservables = {
+ main$: new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ foundDocuments: true,
+ recordRawType: RecordRawType.PLAIN,
+ }) as DataMain$,
+
+ documents$: new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ result: buildDataTableRecordList(esHits),
+ recordRawType: RecordRawType.PLAIN,
+ }) as DataDocuments$,
+
+ availableFields$: new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ fields: [] as string[],
+ recordRawType: RecordRawType.PLAIN,
+ }) as AvailableFields$,
+
+ totalHits$: new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ recordRawType: RecordRawType.PLAIN,
+ }) as DataTotalHits$,
+
+ charts$: new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ recordRawType: RecordRawType.PLAIN,
+ }) as DataCharts$,
+};
+
+const getCommonProps = (dataView: DataView) => {
+ const searchSourceMock = {} as unknown as SearchSource;
+
+ const dataViewList = [dataView].map((ip) => {
+ return { ...ip, ...{ attributes: { title: ip.title } } };
+ }) as unknown as Array>;
+
const savedSearchMock = {} as unknown as SavedSearch;
return {
- indexPattern,
- indexPatternList,
+ indexPattern: dataView,
+ indexPatternList: dataViewList,
inspectorAdapters: { requests: new RequestAdapter() },
navigateTo: action('navigate to somewhere nice'),
onChangeIndexPattern: action('change the data view'),
onUpdateQuery: action('update the query'),
resetSavedSearch: action('reset the saved search the query'),
savedSearch: savedSearchMock,
- savedSearchData$,
savedSearchRefetch$: new Subject(),
searchSource: searchSourceMock,
- state: { columns: ['name', 'message', 'bytes'], sort: [['date', 'desc']] },
+
stateContainer: {
setAppState: action('Set app state'),
appStateContainer: {
@@ -133,5 +158,34 @@ export function getLayoutProps(indexPattern: DataView) {
},
} as unknown as GetStateReturn,
setExpandedDoc: action('opening an expanded doc'),
+ };
+};
+
+export function getDocumentsLayoutProps(dataView: DataView) {
+ return {
+ ...getCommonProps(dataView),
+ savedSearchData$: documentObservables,
+ state: {
+ columns: ['name', 'message', 'bytes'],
+ sort: [['date', 'desc']],
+ query: {
+ language: 'kuery',
+ query: '',
+ },
+ },
} as unknown as DiscoverLayoutProps;
}
+
+export const getPlainRecordLayoutProps = (dataView: DataView) => {
+ return {
+ ...getCommonProps(dataView),
+ savedSearchData$: plainRecordObservables,
+ state: {
+ columns: ['name', 'message', 'bytes'],
+ sort: [['date', 'desc']],
+ query: {
+ sql: 'SELECT * FROM "kibana_sample_data_ecommerce"',
+ },
+ },
+ } as unknown as DiscoverLayoutProps;
+};
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
index 5bddb53a45f1f..e20c4dd83f9f0 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx
@@ -5,13 +5,13 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import React, { useMemo, useCallback, memo } from 'react';
+import React, { memo, useCallback, useMemo } from 'react';
import {
EuiFlexItem,
- EuiSpacer,
- EuiText,
EuiLoadingSpinner,
EuiScreenReaderOnly,
+ EuiSpacer,
+ EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataView } from '@kbn/data-views-plugin/public';
@@ -28,7 +28,7 @@ import {
} from '../../../../../common';
import { useColumns } from '../../../../hooks/use_data_grid_columns';
import { SavedSearch } from '../../../../services/saved_searches';
-import { DataDocumentsMsg, DataDocuments$ } from '../../hooks/use_saved_search';
+import { DataDocuments$, DataDocumentsMsg, RecordRawType } from '../../hooks/use_saved_search';
import { AppState, GetStateReturn } from '../../services/discover_state';
import { useDataState } from '../../hooks/use_data_state';
import { DocTableInfinite } from '../../../../components/doc_table/doc_table_infinite';
@@ -37,6 +37,7 @@ import { DocumentExplorerCallout } from '../document_explorer_callout';
import { DocumentExplorerUpdateCallout } from '../document_explorer_callout/document_explorer_update_callout';
import { DiscoverTourProvider } from '../../../../components/discover_tour';
import { DataTableRecord } from '../../../../types';
+import { getRawRecordType } from '../../utils/get_raw_record_type';
const DocTableInfiniteMemoized = React.memo(DocTableInfinite);
const DataGridMemoized = React.memo(DiscoverGrid);
@@ -56,12 +57,12 @@ function DiscoverDocumentsComponent({
expandedDoc?: DataTableRecord;
indexPattern: DataView;
navigateTo: (url: string) => void;
- onAddFilter: DocViewFilterFn;
+ onAddFilter?: DocViewFilterFn;
savedSearch: SavedSearch;
setExpandedDoc: (doc?: DataTableRecord) => void;
state: AppState;
stateContainer: GetStateReturn;
- onFieldEdited: () => void;
+ onFieldEdited?: () => void;
}) {
const { capabilities, indexPatterns, uiSettings } = useDiscoverServices();
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
@@ -71,7 +72,10 @@ function DiscoverDocumentsComponent({
const documentState: DataDocumentsMsg = useDataState(documents$);
const isLoading = documentState.fetchStatus === FetchStatus.LOADING;
-
+ const isPlainRecord = useMemo(
+ () => getRawRecordType(state.query) === RecordRawType.PLAIN,
+ [state.query]
+ );
const rows = useMemo(() => documentState.result || [], [documentState.result]);
const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useColumns({
@@ -119,8 +123,11 @@ function DiscoverDocumentsComponent({
);
const showTimeCol = useMemo(
- () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName,
- [uiSettings, indexPattern.timeFieldName]
+ () =>
+ !isPlainRecord &&
+ !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) &&
+ !!indexPattern.timeFieldName,
+ [isPlainRecord, uiSettings, indexPattern.timeFieldName]
);
if (
@@ -160,7 +167,7 @@ function DiscoverDocumentsComponent({
onFilter={onAddFilter as DocViewFilterFn}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
- onSort={onSort}
+ onSort={!isPlainRecord ? onSort : undefined}
useNewFieldsApi={useNewFieldsApi}
dataTestSubj="discoverDocTable"
/>
@@ -168,8 +175,8 @@ function DiscoverDocumentsComponent({
)}
{!isLegacy && (
<>
- {!hideAnnouncements && (
-
+ {!hideAnnouncements && !isPlainRecord && (
+
)}
@@ -185,18 +192,20 @@ function DiscoverDocumentsComponent({
sampleSize={sampleSize}
searchDescription={savedSearch.description}
searchTitle={savedSearch.title}
- setExpandedDoc={setExpandedDoc}
+ setExpandedDoc={!isPlainRecord ? setExpandedDoc : undefined}
showTimeCol={showTimeCol}
settings={state.grid}
onAddColumn={onAddColumn}
onFilter={onAddFilter as DocViewFilterFn}
onRemoveColumn={onRemoveColumn}
onSetColumns={onSetColumns}
- onSort={onSort}
+ onSort={!isPlainRecord ? onSort : undefined}
onResize={onResize}
useNewFieldsApi={useNewFieldsApi}
rowHeightState={state.rowHeight}
onUpdateRowHeight={onUpdateRowHeight}
+ isSortEnabled={!isPlainRecord}
+ isPlainRecord={isPlainRecord}
rowsPerPageState={state.rowsPerPage}
onUpdateRowsPerPage={onUpdateRowsPerPage}
onFieldEdited={onFieldEdited}
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx
index e0fc90a83b296..da8f249daffec 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.test.tsx
@@ -9,6 +9,7 @@
import React from 'react';
import { Subject, BehaviorSubject } from 'rxjs';
import { mountWithIntl } from '@kbn/test-jest-helpers';
+import type { Query, AggregateQuery } from '@kbn/es-query';
import { setHeaderActionMenuMounter } from '../../../../kibana_services';
import { DiscoverLayout, SIDEBAR_CLOSED_KEY } from './discover_layout';
import { esHits } from '../../../../__mocks__/es_hits';
@@ -26,6 +27,7 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
+ RecordRawType,
} from '../../hooks/use_saved_search';
import { discoverServiceMock } from '../../../../__mocks__/services';
import { FetchStatus } from '../../../types';
@@ -42,7 +44,9 @@ setHeaderActionMenuMounter(jest.fn());
function mountComponent(
indexPattern: DataView,
prevSidebarClosed?: boolean,
- mountOptions: { attachTo?: HTMLElement } = {}
+ mountOptions: { attachTo?: HTMLElement } = {},
+ query?: Query | AggregateQuery,
+ isPlainRecord?: boolean
) {
const searchSourceMock = createSearchSourceMock({});
const services = {
@@ -61,6 +65,7 @@ function mountComponent(
const main$ = new BehaviorSubject({
fetchStatus: FetchStatus.COMPLETE,
+ recordRawType: isPlainRecord ? RecordRawType.PLAIN : RecordRawType.DOCUMENT,
foundDocuments: true,
}) as DataMain$;
@@ -148,7 +153,7 @@ function mountComponent(
savedSearchData$,
savedSearchRefetch$: new Subject(),
searchSource: searchSourceMock,
- state: { columns: [] },
+ state: { columns: [], query },
stateContainer: {
setAppState: () => {},
appStateContainer: {
@@ -179,6 +184,17 @@ describe('Discover component', () => {
expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeTruthy();
});
+ test('sql query displays no chart toggle', () => {
+ const component = mountComponent(
+ indexPatternWithTimefieldMock,
+ false,
+ {},
+ { sql: 'SELECT * FROM test' },
+ true
+ );
+ expect(component.find('[data-test-subj="discoverChartOptionsToggle"]').exists()).toBeFalsy();
+ });
+
test('the saved search title h1 gains focus on navigate', () => {
const container = document.createElement('div');
document.body.appendChild(container);
diff --git a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
index 26b02996a4078..6b8a386aea68c 100644
--- a/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
+++ b/src/plugins/discover/public/application/main/components/layout/discover_layout.tsx
@@ -20,6 +20,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { METRIC_TYPE } from '@kbn/analytics';
+import { isOfQueryType } from '@kbn/es-query';
import classNames from 'classnames';
import { generateFilters } from '@kbn/data-plugin/public';
import { DataView, DataViewField, DataViewType } from '@kbn/data-views-plugin/public';
@@ -36,7 +37,7 @@ import { DocViewFilterFn } from '../../../../services/doc_views/doc_views_types'
import { DiscoverChart } from '../chart';
import { getResultState } from '../../utils/get_result_state';
import { DiscoverUninitialized } from '../uninitialized/uninitialized';
-import { DataMainMsg } from '../../hooks/use_saved_search';
+import { DataMainMsg, RecordRawType } from '../../hooks/use_saved_search';
import { useColumns } from '../../../../hooks/use_data_grid_columns';
import { DiscoverDocuments } from './discover_documents';
import { FetchStatus } from '../../../types';
@@ -46,6 +47,7 @@ import { FieldStatisticsTable } from '../field_stats_table';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { DOCUMENTS_VIEW_CLICK, FIELD_STATISTICS_VIEW_CLICK } from '../field_stats_table/constants';
import { hasActiveFilter } from './utils';
+import { getRawRecordType } from '../../utils/get_raw_record_type';
/**
* Local storage key for sidebar persistence state
@@ -88,6 +90,7 @@ export function DiscoverLayout({
} = useDiscoverServices();
const { main$, charts$, totalHits$ } = savedSearchData$;
const [inspectorSession, setInspectorSession] = useState(undefined);
+ const dataState: DataMainMsg = useDataState(main$);
const viewMode = useMemo(() => {
if (uiSettings.get(SHOW_FIELD_STATISTICS) !== true) return VIEW_MODE.DOCUMENT_LEVEL;
@@ -110,7 +113,6 @@ export function DiscoverLayout({
);
const fetchCounter = useRef(0);
- const dataState: DataMainMsg = useDataState(main$);
useEffect(() => {
if (dataState.fetchStatus === FetchStatus.LOADING) {
@@ -130,9 +132,13 @@ export function DiscoverLayout({
const [isSidebarClosed, setIsSidebarClosed] = useState(initialSidebarClosed);
const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]);
+ const isPlainRecord = useMemo(
+ () => getRawRecordType(state.query) === RecordRawType.PLAIN,
+ [state.query]
+ );
const resultState = useMemo(
- () => getResultState(dataState.fetchStatus, dataState.foundDocuments!),
- [dataState.fetchStatus, dataState.foundDocuments]
+ () => getResultState(dataState.fetchStatus, dataState.foundDocuments!, isPlainRecord),
+ [dataState.fetchStatus, dataState.foundDocuments, isPlainRecord]
);
const onOpenInspector = useCallback(() => {
@@ -207,6 +213,12 @@ export function DiscoverLayout({
savedSearchTitle.current?.focus();
}, []);
+ const textBasedLanguageModeErrors = useMemo(() => {
+ if (isPlainRecord) {
+ return dataState.error;
+ }
+ }, [dataState.error, isPlainRecord]);
+
return (
@@ -254,7 +268,7 @@ export function DiscoverLayout({
documents$={savedSearchData$.documents$}
indexPatternList={indexPatternList}
onAddField={onAddColumn}
- onAddFilter={onAddFilter}
+ onAddFilter={!isPlainRecord ? onAddFilter : undefined}
onRemoveField={onRemoveColumn}
onChangeIndexPattern={onChangeIndexPattern}
selectedIndexPattern={indexPattern}
@@ -303,7 +317,7 @@ export function DiscoverLayout({
isTimeBased={isTimeBased}
data={data}
error={dataState.error}
- hasQuery={!!state.query?.query}
+ hasQuery={isOfQueryType(state.query) && !!state.query?.query}
hasFilters={hasActiveFilter(state.filters)}
onDisableFilters={onDisableFilters}
/>
@@ -320,33 +334,37 @@ export function DiscoverLayout({
gutterSize="none"
responsive={false}
>
-
-
-
-
+ {!isPlainRecord && (
+ <>
+
+
+
+
+ >
+ )}
{viewMode === VIEW_MODE.DOCUMENT_LEVEL ? (
) : (
diff --git a/src/plugins/discover/public/application/main/components/layout/types.ts b/src/plugins/discover/public/application/main/components/layout/types.ts
index 26bb24c866c78..49608c3ba923c 100644
--- a/src/plugins/discover/public/application/main/components/layout/types.ts
+++ b/src/plugins/discover/public/application/main/components/layout/types.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import type { Query, TimeRange } from '@kbn/es-query';
+import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import type { SavedObject } from '@kbn/data-plugin/public';
import type { DataView, DataViewAttributes } from '@kbn/data-views-plugin/public';
import { ISearchSource } from '@kbn/data-plugin/public';
@@ -22,7 +22,10 @@ export interface DiscoverLayoutProps {
inspectorAdapters: { requests: RequestAdapter };
navigateTo: (url: string) => void;
onChangeIndexPattern: (id: string) => void;
- onUpdateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
+ onUpdateQuery: (
+ payload: { dateRange: TimeRange; query?: Query | AggregateQuery },
+ isUpdate?: boolean
+ ) => void;
resetSavedSearch: () => void;
expandedDoc?: DataTableRecord;
setExpandedDoc: (doc?: DataTableRecord) => void;
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx
index 4b2ccf1bff0bb..4b2aa7f9ded84 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.test.tsx
@@ -27,10 +27,12 @@ function getComponent({
selected = false,
showDetails = false,
field,
+ onAddFilterExists = true,
}: {
selected?: boolean;
showDetails?: boolean;
field?: DataViewField;
+ onAddFilterExists?: boolean;
}) {
const finalField =
field ??
@@ -49,7 +51,7 @@ function getComponent({
indexPattern: stubDataView,
field: finalField,
getDetails: jest.fn(() => ({ buckets: [], error: '', exists: 1, total: 2, columns: [] })),
- onAddFilter: jest.fn(),
+ ...(onAddFilterExists && { onAddFilter: jest.fn() }),
onAddField: jest.fn(),
onRemoveField: jest.fn(),
showDetails,
@@ -139,4 +141,21 @@ describe('discover sidebar field', function () {
findTestSubject(comp, 'field-bytes-showDetails').simulate('click');
expect(props.getDetails.mock.calls.length).toEqual(1);
});
+ it('should not return the popover if onAddFilter is not provided', function () {
+ const field = new DataViewField({
+ name: '_source',
+ type: '_source',
+ esTypes: ['_source'],
+ searchable: true,
+ aggregatable: true,
+ readFromDocValues: true,
+ });
+ const { comp } = getComponent({
+ selected: true,
+ field,
+ onAddFilterExists: false,
+ });
+ const popover = findTestSubject(comp, 'discoverFieldListPanelPopover');
+ expect(popover.length).toBe(0);
+ });
});
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
index 33fc01abb5150..0b9c331a3060b 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field.tsx
@@ -167,10 +167,11 @@ interface MultiFieldsProps {
multiFields: NonNullable;
toggleDisplay: (field: DataViewField) => void;
alwaysShowActionButton: boolean;
+ isDocumentRecord: boolean;
}
const MultiFields: React.FC = memo(
- ({ multiFields, toggleDisplay, alwaysShowActionButton }) => (
+ ({ multiFields, toggleDisplay, alwaysShowActionButton, isDocumentRecord }) => (
@@ -186,7 +187,7 @@ const MultiFields: React.FC = memo(
className="dscSidebarItem dscSidebarItem--multi"
isActive={false}
dataTestSubj={`field-${entry.field.name}-showDetails`}
- fieldIcon={}
+ fieldIcon={isDocumentRecord && }
fieldAction={
void;
+ onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
/**
* Callback to remove/deselect a the field
* @param fieldName
@@ -280,6 +281,7 @@ function DiscoverFieldComponent({
showFieldStats,
}: DiscoverFieldProps) {
const [infoIsOpen, setOpen] = useState(false);
+ const isDocumentRecord = !!onAddFilter;
const toggleDisplay = useCallback(
(f: DataViewField) => {
@@ -304,7 +306,7 @@ function DiscoverFieldComponent({
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={}
+ fieldIcon={isDocumentRecord && }
fieldAction={
);
+ const button = (
+ }
+ fieldAction={
+
+ }
+ fieldName={}
+ fieldInfoIcon={field.type === 'conflict' && }
+ />
+ );
+ if (!isDocumentRecord) {
+ return button;
+ }
+
const renderPopover = () => {
const details = getDetails(field);
return (
@@ -398,6 +424,7 @@ function DiscoverFieldComponent({
multiFields={multiFields}
alwaysShowActionButton={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
+ isDocumentRecord={isDocumentRecord}
/>
>
)}
@@ -415,28 +442,10 @@ function DiscoverFieldComponent({
return (
}
- fieldAction={
-
- }
- fieldName={}
- fieldInfoIcon={field.type === 'conflict' && }
- />
- }
+ button={button}
isOpen={infoIsOpen}
closePopover={() => setOpen(false)}
+ data-test-subj="discoverFieldListPanelPopover"
anchorPosition="rightUp"
panelClassName="dscSidebarItem__fieldPopoverPanel"
>
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx
index ac0dd3e7f8186..47808d14e1cc3 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_bucket.tsx
@@ -17,7 +17,7 @@ import './discover_field_bucket.scss';
interface Props {
bucket: Bucket;
field: DataViewField;
- onAddFilter: (field: DataViewField | string, value: string, type: '+' | '-') => void;
+ onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
@@ -66,7 +66,7 @@ export function DiscoverFieldBucket({ field, bucket, onAddFilter }: Props) {
count={bucket.count}
/>
- {field.filterable && (
+ {onAddFilter && field.filterable && (
void;
+ onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
}
export function DiscoverFieldDetails({
@@ -43,7 +43,7 @@ export function DiscoverFieldDetails({
- {!indexPattern.metaFields.includes(field.name) && !field.scripted ? (
+ {onAddFilter && !indexPattern.metaFields.includes(field.name) && !field.scripted ? (
onAddFilter('_exists_', field.name, '+')}
data-test-subj="onAddFilterButton"
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx
index 1601032fc5af2..8f54936f4963b 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.test.tsx
@@ -21,6 +21,7 @@ describe('DiscoverFieldSearch', () => {
value: 'test',
types: ['any', 'string', '_source'],
presentFieldTypes: ['string', 'date', 'boolean', 'number'],
+ isPlainRecord: false,
};
function mountComponent(props?: Props) {
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx
index 0c0e88c8ca424..59ba2833d94f5 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_field_search.tsx
@@ -64,6 +64,10 @@ export interface Props {
* the input value of the user
*/
value?: string;
+ /**
+ * is text base lang mode
+ */
+ isPlainRecord: boolean;
}
interface FieldTypeTableItem {
@@ -76,7 +80,13 @@ interface FieldTypeTableItem {
* Component is Discover's side bar to search of available fields
* Additionally there's a button displayed that allows the user to show/hide more filter fields
*/
-export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes }: Props) {
+export function DiscoverFieldSearch({
+ onChange,
+ value,
+ types,
+ presentFieldTypes,
+ isPlainRecord,
+}: Props) {
const searchPlaceholder = i18n.translate('discover.fieldChooser.searchPlaceHolder', {
defaultMessage: 'Search field names',
});
@@ -353,81 +363,83 @@ export function DiscoverFieldSearch({ onChange, value, types, presentFieldTypes
-
-
- {
- setPopoverOpen(false);
- }}
- button={buttonContent}
- >
-
- {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', {
- defaultMessage: 'Filter by type',
- })}
-
- {selectionPanel}
- {footer()}
-
-
-
- {i18n.translate('discover.fieldChooser.popoverTitle', {
- defaultMessage: 'Field types',
- })}
-
-
+
+ {
+ setPopoverOpen(false);
+ }}
+ button={buttonContent}
>
-
+ {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', {
+ defaultMessage: 'Filter by type',
})}
- items={items}
- compressed={true}
- rowHeader="firstName"
- columns={columnsSidebar}
- responsive={false}
- />
-
-
-
-
- {i18n.translate('discover.fieldTypesPopover.learnMoreText', {
- defaultMessage: 'Learn more about',
+
+ {selectionPanel}
+ {footer()}
+
+
+
+ {i18n.translate('discover.fieldChooser.popoverTitle', {
+ defaultMessage: 'Field types',
+ })}
+
+
+
-
-
-
-
-
-
-
-
+ items={items}
+ compressed={true}
+ rowHeader="firstName"
+ columns={columnsSidebar}
+ responsive={false}
+ />
+
+
+
+
+ {i18n.translate('discover.fieldTypesPopover.learnMoreText', {
+ defaultMessage: 'Learn more about',
+ })}
+
+
+
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
index 4c016eadf69a3..f5a0448bc4415 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar.tsx
@@ -125,6 +125,7 @@ export function DiscoverSidebarComponent({
const [fieldsToRender, setFieldsToRender] = useState(FIELDS_PER_PAGE);
const [fieldsPerPage, setFieldsPerPage] = useState(FIELDS_PER_PAGE);
const availableFieldsContainer = useRef(null);
+ const isPlainRecord = !onAddFilter;
useEffect(() => {
if (documents) {
@@ -160,16 +161,24 @@ export function DiscoverSidebarComponent({
[fields, columns, popularLimit, fieldCounts, fieldFilter, useNewFieldsApi]
);
+ /**
+ * Popular fields are not displayed in text based lang mode
+ */
+ const restFields = useMemo(
+ () => (isPlainRecord ? [...popularFields, ...unpopularFields] : unpopularFields),
+ [isPlainRecord, popularFields, unpopularFields]
+ );
+
const paginate = useCallback(() => {
const newFieldsToRender = fieldsToRender + Math.round(fieldsPerPage * 0.5);
- setFieldsToRender(Math.max(fieldsPerPage, Math.min(newFieldsToRender, unpopularFields.length)));
- }, [setFieldsToRender, fieldsToRender, unpopularFields, fieldsPerPage]);
+ setFieldsToRender(Math.max(fieldsPerPage, Math.min(newFieldsToRender, restFields.length)));
+ }, [setFieldsToRender, fieldsToRender, restFields, fieldsPerPage]);
useEffect(() => {
- if (scrollContainer && unpopularFields.length && availableFieldsContainer.current) {
+ if (scrollContainer && restFields.length && availableFieldsContainer.current) {
const { clientHeight, scrollHeight } = scrollContainer;
const isScrollable = scrollHeight > clientHeight; // there is no scrolling currently
- const allFieldsRendered = fieldsToRender >= unpopularFields.length;
+ const allFieldsRendered = fieldsToRender >= restFields.length;
if (!isScrollable && !allFieldsRendered) {
// Not all available fields were rendered with the given fieldsPerPage number
@@ -187,7 +196,7 @@ export function DiscoverSidebarComponent({
}, [
fieldsPerPage,
scrollContainer,
- unpopularFields,
+ restFields,
fieldsToRender,
setFieldsPerPage,
setFieldsToRender,
@@ -198,11 +207,11 @@ export function DiscoverSidebarComponent({
if (scrollContainer) {
const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
const nearBottom = scrollTop + clientHeight > scrollHeight * 0.9;
- if (nearBottom && unpopularFields) {
+ if (nearBottom && restFields) {
paginate();
}
}
- }, [paginate, scrollContainer, unpopularFields]);
+ }, [paginate, scrollContainer, restFields]);
const { fieldTypes, presentFieldTypes } = useMemo(() => {
const result = ['any'];
@@ -342,6 +351,7 @@ export function DiscoverSidebarComponent({
value={fieldFilter.name}
types={fieldTypes}
presentFieldTypes={presentFieldTypes}
+ isPlainRecord={isPlainRecord}
/>
@@ -428,12 +438,12 @@ export function DiscoverSidebarComponent({
}
extraAction={
- {popularFields.length + unpopularFields.length}
+ {restFields.length}
}
>
- {popularFields.length > 0 && (
+ {!isPlainRecord && popularFields.length > 0 && (
<>
@@ -477,7 +487,7 @@ export function DiscoverSidebarComponent({
data-test-subj={`fieldList-unpopular`}
ref={availableFieldsContainer}
>
- {getPaginated(unpopularFields).map((field: DataViewField) => {
+ {getPaginated(restFields).map((field: DataViewField) => {
return (
{
+ const initialProps = getCompProps();
+ const propsWithTextBasedMode = {
+ ...initialProps,
+ onAddFilter: undefined,
+ documents$: new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ recordRawType: RecordRawType.PLAIN,
+ result: getDataTableRecords(stubLogstashIndexPattern),
+ }) as DataDocuments$,
+ state: {
+ ...initialProps.state,
+ query: { sql: 'SELECT * FROM `index`' },
+ },
+ };
+ const compInViewerMode = mountWithIntl(
+
+
+
+ );
+ expect(findTestSubject(compInViewerMode, 'indexPattern-add-field_btn').length).toBe(0);
+ });
+
it('should not show "Add a field" button in viewer mode', () => {
const mockedServicesInViewerMode = {
...mockServices,
diff --git a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx
index 8cf1b73a72ec0..3076c4ab9a80e 100644
--- a/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx
+++ b/src/plugins/discover/public/application/main/components/sidebar/discover_sidebar_responsive.tsx
@@ -6,33 +6,34 @@
* Side Public License, v 1.
*/
-import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { UiCounterMetricType } from '@kbn/analytics';
import {
- EuiTitle,
- EuiHideFor,
- EuiShowFor,
- EuiButton,
EuiBadge,
- EuiFlyoutHeader,
+ EuiButton,
EuiFlyout,
+ EuiFlyoutHeader,
+ EuiHideFor,
EuiIcon,
EuiLink,
EuiPortal,
+ EuiShowFor,
+ EuiTitle,
} from '@elastic/eui';
-import type { DataViewField, DataView, DataViewAttributes } from '@kbn/data-views-plugin/public';
+import type { DataView, DataViewAttributes, DataViewField } from '@kbn/data-views-plugin/public';
import { SavedObject } from '@kbn/core/types';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { getDefaultFieldFilter } from './lib/field_filter';
import { DiscoverSidebar } from './discover_sidebar';
import { AppState } from '../../services/discover_state';
-import { AvailableFields$, DataDocuments$ } from '../../hooks/use_saved_search';
+import { AvailableFields$, DataDocuments$, RecordRawType } from '../../hooks/use_saved_search';
import { calcFieldCounts } from '../../utils/calc_field_counts';
import { VIEW_MODE } from '../../../../components/view_mode_toggle';
import { FetchStatus } from '../../../types';
import { DISCOVER_TOUR_STEP_ANCHOR_IDS } from '../../../../components/discover_tour';
+import { getRawRecordType } from '../../utils/get_raw_record_type';
export interface DiscoverSidebarResponsiveProps {
/**
@@ -62,7 +63,7 @@ export interface DiscoverSidebarResponsiveProps {
/**
* Callback function when adding a filter from sidebar
*/
- onAddFilter: (field: DataViewField | string, value: string, type: '+' | '-') => void;
+ onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
/**
* Callback function when changing an index pattern
*/
@@ -115,6 +116,10 @@ export interface DiscoverSidebarResponsiveProps {
*/
export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) {
const services = useDiscoverServices();
+ const isPlainRecord = useMemo(
+ () => getRawRecordType(props.state.query) === RecordRawType.PLAIN,
+ [props.state.query]
+ );
const { selectedIndexPattern, onFieldEdited, onDataViewCreated } = props;
const [fieldFilter, setFieldFilter] = useState(getDefaultFieldFilter());
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
@@ -210,7 +215,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
const editField = useMemo(
() =>
- canEditDataView && selectedIndexPattern
+ !isPlainRecord && canEditDataView && selectedIndexPattern
? (fieldName?: string) => {
const ref = dataViewFieldEditor.openEditor({
ctx: {
@@ -230,11 +235,12 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps)
}
: undefined,
[
+ isPlainRecord,
canEditDataView,
- closeFlyout,
- dataViewFieldEditor,
selectedIndexPattern,
+ dataViewFieldEditor,
setFieldEditorRef,
+ closeFlyout,
onFieldEdited,
]
);
diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx
index eff01ee958190..3342c0a3a3537 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.test.tsx
@@ -43,6 +43,7 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
resetSavedSearch: () => {},
onFieldEdited: jest.fn(),
onChangeIndexPattern: jest.fn(),
+ isPlainRecord: false,
};
}
diff --git a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx
index bd99462295f78..0c71991737de0 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/discover_topnav.tsx
@@ -5,27 +5,35 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-import React, { useCallback, useMemo, useRef, useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useHistory } from 'react-router-dom';
-import type { Query, TimeRange } from '@kbn/es-query';
+import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import { DataViewType } from '@kbn/data-views-plugin/public';
+import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
+import { ENABLE_SQL } from '../../../../../common';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
import { DiscoverLayoutProps } from '../layout/types';
import { getTopNavLinks } from './get_top_nav_links';
import { getHeaderActionMenuMounter } from '../../../../kibana_services';
import { GetStateReturn } from '../../services/discover_state';
+import { onSaveSearch } from './on_save_search';
export type DiscoverTopNavProps = Pick<
DiscoverLayoutProps,
'indexPattern' | 'navigateTo' | 'savedSearch' | 'searchSource'
> & {
onOpenInspector: () => void;
- query?: Query;
+ query?: Query | AggregateQuery;
savedQuery?: string;
- updateQuery: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
+ updateQuery: (
+ payload: { dateRange: TimeRange; query?: Query | AggregateQuery },
+ isUpdate?: boolean
+ ) => void;
stateContainer: GetStateReturn;
resetSavedSearch: () => void;
onChangeIndexPattern: (indexPattern: string) => void;
+ isPlainRecord: boolean;
+ textBasedLanguageModeErrors?: Error;
onFieldEdited: () => void;
};
@@ -41,22 +49,25 @@ export const DiscoverTopNav = ({
savedSearch,
resetSavedSearch,
onChangeIndexPattern,
+ isPlainRecord,
+ textBasedLanguageModeErrors,
onFieldEdited,
}: DiscoverTopNavProps) => {
const history = useHistory();
+
const showDatePicker = useMemo(
() => indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP,
[indexPattern]
);
const services = useDiscoverServices();
- const { dataViewEditor, navigation, dataViewFieldEditor, data } = services;
+ const { dataViewEditor, navigation, dataViewFieldEditor, data, uiSettings } = services;
const canEditDataView = Boolean(dataViewEditor?.userPermissions.editDataView());
const closeFieldEditor = useRef<() => void | undefined>();
const closeDataViewEditor = useRef<() => void | undefined>();
- const { TopNavMenu } = navigation.ui;
+ const { AggregateQueryTopNavMenu } = navigation.ui;
const onOpenSavedSearch = useCallback(
(newSavedSearchId: string) => {
@@ -134,6 +145,7 @@ export const DiscoverTopNav = ({
onOpenInspector,
searchSource,
onOpenSavedSearch,
+ isPlainRecord,
}),
[
indexPattern,
@@ -144,6 +156,7 @@ export const DiscoverTopNav = ({
onOpenInspector,
searchSource,
onOpenSavedSearch,
+ isPlainRecord,
]
);
@@ -163,7 +176,11 @@ export const DiscoverTopNav = ({
const setMenuMountPoint = useMemo(() => {
return getHeaderActionMenuMounter();
}, []);
-
+ const isSQLModeEnabled = uiSettings.get(ENABLE_SQL);
+ const supportedTextBasedLanguages = [];
+ if (isSQLModeEnabled) {
+ supportedTextBasedLanguages.push('SQL');
+ }
const dataViewPickerProps = {
trigger: {
label: indexPattern?.getName() || '',
@@ -173,11 +190,27 @@ export const DiscoverTopNav = ({
currentDataViewId: indexPattern?.id,
onAddField: addField,
onDataViewCreated: createNewDataView,
- onChangeDataView: (newIndexPatternId: string) => onChangeIndexPattern(newIndexPatternId),
+ onChangeDataView: onChangeIndexPattern,
+ textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
};
+ const onTextBasedSavedAndExit = useCallback(
+ async ({ onSave, onCancel }) => {
+ await onSaveSearch({
+ savedSearch,
+ services,
+ indexPattern,
+ navigateTo,
+ state: stateContainer,
+ onClose: onCancel,
+ onSaveCb: onSave,
+ });
+ },
+ [indexPattern, navigateTo, savedSearch, services, stateContainer]
+ );
+
return (
-
);
};
diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts
index 40ef4669db093..01d966bc6699f 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts
+++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.test.ts
@@ -36,6 +36,7 @@ test('getTopNavLinks result', () => {
state,
searchSource: {} as ISearchSource,
onOpenSavedSearch: () => {},
+ isPlainRecord: false,
});
expect(topNavLinks).toMatchInlineSnapshot(`
Array [
@@ -86,3 +87,58 @@ test('getTopNavLinks result', () => {
]
`);
});
+
+test('getTopNavLinks result for sql mode', () => {
+ const topNavLinks = getTopNavLinks({
+ indexPattern: indexPatternMock,
+ navigateTo: jest.fn(),
+ onOpenInspector: jest.fn(),
+ savedSearch: savedSearchMock,
+ services,
+ state,
+ searchSource: {} as ISearchSource,
+ onOpenSavedSearch: () => {},
+ isPlainRecord: true,
+ });
+ expect(topNavLinks).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "description": "Options",
+ "id": "options",
+ "label": "Options",
+ "run": [Function],
+ "testId": "discoverOptionsButton",
+ },
+ Object {
+ "description": "New Search",
+ "id": "new",
+ "label": "New",
+ "run": [Function],
+ "testId": "discoverNewButton",
+ },
+ Object {
+ "description": "Open Saved Search",
+ "id": "open",
+ "label": "Open",
+ "run": [Function],
+ "testId": "discoverOpenButton",
+ },
+ Object {
+ "description": "Open Inspector for search",
+ "id": "inspect",
+ "label": "Inspect",
+ "run": [Function],
+ "testId": "openInspectorButton",
+ },
+ Object {
+ "description": "Save Search",
+ "emphasize": true,
+ "iconType": "save",
+ "id": "save",
+ "label": "Save",
+ "run": [Function],
+ "testId": "discoverSaveButton",
+ },
+ ]
+ `);
+});
diff --git a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx
index 747e4ec11e93e..28376217e064d 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/get_top_nav_links.tsx
@@ -32,6 +32,7 @@ export const getTopNavLinks = ({
onOpenInspector,
searchSource,
onOpenSavedSearch,
+ isPlainRecord,
}: {
indexPattern: DataView;
navigateTo: (url: string) => void;
@@ -41,6 +42,7 @@ export const getTopNavLinks = ({
onOpenInspector: () => void;
searchSource: ISearchSource;
onOpenSavedSearch: (id: string) => void;
+ isPlainRecord: boolean;
}): TopNavMenuData[] => {
const options = {
id: 'options',
@@ -196,11 +198,12 @@ export const getTopNavLinks = ({
...(services.capabilities.advancedSettings.save ? [options] : []),
newSearch,
openSearch,
+ ...(!isPlainRecord ? [shareSearch] : []),
...(services.triggersActionsUi &&
- services.capabilities.management?.insightsAndAlerting?.triggersActions
+ services.capabilities.management?.insightsAndAlerting?.triggersActions &&
+ !isPlainRecord
? [alerts]
: []),
- shareSearch,
inspectSearch,
...(services.capabilities.discover.save ? [saveSearch] : []),
];
diff --git a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
index 11512fc542117..8c050985d2934 100644
--- a/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
+++ b/src/plugins/discover/public/application/main/components/top_nav/on_save_search.tsx
@@ -24,6 +24,7 @@ async function saveDataSource({
saveOptions,
services,
state,
+ navigateOrReloadSavedSearch,
}: {
indexPattern: DataView;
navigateTo: (url: string) => void;
@@ -31,6 +32,7 @@ async function saveDataSource({
saveOptions: SaveSavedSearchOptions;
services: DiscoverServices;
state: GetStateReturn;
+ navigateOrReloadSavedSearch: boolean;
}) {
const prevSavedSearchId = savedSearch.id;
function onSuccess(id: string) {
@@ -44,20 +46,22 @@ async function saveDataSource({
}),
'data-test-subj': 'saveSearchSuccess',
});
- if (id !== prevSavedSearchId) {
- navigateTo(`/view/${encodeURIComponent(id)}`);
- } else {
- // Update defaults so that "reload saved query" functions correctly
- state.resetAppState();
- services.chrome.docTitle.change(savedSearch.title!);
+ if (navigateOrReloadSavedSearch) {
+ if (id !== prevSavedSearchId) {
+ navigateTo(`/view/${encodeURIComponent(id)}`);
+ } else {
+ // Update defaults so that "reload saved query" functions correctly
+ state.resetAppState();
+ services.chrome.docTitle.change(savedSearch.title!);
- setBreadcrumbsTitle(
- {
- ...savedSearch,
- id: prevSavedSearchId ?? id,
- },
- services.chrome
- );
+ setBreadcrumbsTitle(
+ {
+ ...savedSearch,
+ id: prevSavedSearchId ?? id,
+ },
+ services.chrome
+ );
+ }
}
}
}
@@ -90,6 +94,7 @@ export async function onSaveSearch({
services,
state,
onClose,
+ onSaveCb,
}: {
indexPattern: DataView;
navigateTo: (path: string) => void;
@@ -97,6 +102,7 @@ export async function onSaveSearch({
services: DiscoverServices;
state: GetStateReturn;
onClose?: () => void;
+ onSaveCb?: () => void;
}) {
const { uiSettings } = services;
const onSave = async ({
@@ -124,6 +130,7 @@ export async function onSaveSearch({
copyOnSave: newCopyOnSave,
isTitleDuplicateConfirmed,
};
+ const navigateOrReloadSavedSearch = !Boolean(onSaveCb);
const response = await saveDataSource({
indexPattern,
saveOptions,
@@ -131,6 +138,7 @@ export async function onSaveSearch({
navigateTo,
savedSearch,
state,
+ navigateOrReloadSavedSearch,
});
// If the save wasn't successful, put the original values back.
if (!response.id || response.error) {
@@ -139,6 +147,7 @@ export async function onSaveSearch({
} else {
state.resetInitialAppState();
}
+ onSaveCb?.();
return response;
};
diff --git a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts
index 2d82e12824f04..f4a89413b0afe 100644
--- a/src/plugins/discover/public/application/main/hooks/use_discover_state.ts
+++ b/src/plugins/discover/public/application/main/hooks/use_discover_state.ts
@@ -6,14 +6,21 @@
* Side Public License, v 1.
*/
import { useMemo, useEffect, useState, useCallback } from 'react';
+import usePrevious from 'react-use/lib/usePrevious';
import { isEqual } from 'lodash';
import { History } from 'history';
+import {
+ isOfAggregateQueryType,
+ getIndexPatternFromSQLQuery,
+ AggregateQuery,
+ Query,
+} from '@kbn/es-query';
import { getState } from '../services/discover_state';
import { getStateDefaults } from '../utils/get_state_defaults';
import { DiscoverServices } from '../../../build_services';
import { SavedSearch, getSavedSearch } from '../../../services/saved_searches';
import { loadIndexPattern } from '../utils/resolve_index_pattern';
-import { useSavedSearch as useSavedSearchData } from './use_saved_search';
+import { useSavedSearch as useSavedSearchData, DataDocumentsMsg } from './use_saved_search';
import {
MODIFY_COLUMNS_ON_SWITCH,
SEARCH_FIELDS_FROM_SOURCE,
@@ -21,11 +28,14 @@ import {
SORT_DEFAULT_ORDER_SETTING,
} from '../../../../common';
import { useSearchSession } from './use_search_session';
+import { useDataState } from './use_data_state';
import { FetchStatus } from '../../types';
import { getSwitchIndexPatternAppState } from '../utils/get_switch_index_pattern_app_state';
import { SortPairArr } from '../../../components/doc_table/utils/get_sort';
import { DataTableRecord } from '../../../types';
+const MAX_NUM_OF_COLUMNS = 50;
+
export function useDiscoverState({
services,
history,
@@ -69,6 +79,9 @@ export function useDiscoverState({
const { appStateContainer } = stateContainer;
const [state, setState] = useState(appStateContainer.getState());
+ const [documentStateCols, setDocumentStateCols] = useState([]);
+ const [sqlQuery] = useState(state.query);
+ const prevQuery = usePrevious(state.query);
/**
* Search session logic
@@ -99,6 +112,8 @@ export function useDiscoverState({
useNewFieldsApi,
});
+ const documentState: DataDocumentsMsg = useDataState(data$.documents$);
+
/**
* Reset to display loading spinner when savedSearch is changing
*/
@@ -196,13 +211,23 @@ export function useDiscoverState({
state.columns || [],
(state.sort || []) as SortPairArr[],
config.get(MODIFY_COLUMNS_ON_SWITCH),
- config.get(SORT_DEFAULT_ORDER_SETTING)
+ config.get(SORT_DEFAULT_ORDER_SETTING),
+ state.query
);
stateContainer.setAppState(nextAppState);
}
setExpandedDoc(undefined);
},
- [config, indexPattern, indexPatterns, setExpandedDoc, state.columns, state.sort, stateContainer]
+ [
+ config,
+ indexPattern,
+ indexPatterns,
+ setExpandedDoc,
+ state.columns,
+ state.query,
+ state.sort,
+ stateContainer,
+ ]
);
/**
* Function triggered when the user changes the query in the search bar
@@ -220,12 +245,53 @@ export function useDiscoverState({
/**
* Trigger data fetching on indexPattern or savedSearch changes
*/
+ useEffect(() => {
+ if (!isEqual(state.query, prevQuery)) {
+ setDocumentStateCols([]);
+ }
+ }, [state.query, prevQuery]);
+
useEffect(() => {
if (indexPattern) {
refetch$.next(undefined);
}
}, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]);
+ const getResultColumns = useCallback(() => {
+ if (documentState.result?.length && documentState.fetchStatus === FetchStatus.COMPLETE) {
+ const firstRow = documentState.result[0];
+ const columns = Object.keys(firstRow.raw).slice(0, MAX_NUM_OF_COLUMNS);
+ if (!isEqual(columns, documentStateCols) && !isEqual(state.query, sqlQuery)) {
+ return columns;
+ }
+ return [];
+ }
+ return [];
+ }, [documentState, documentStateCols, sqlQuery, state.query]);
+
+ useEffect(() => {
+ async function fetchDataview() {
+ if (state.query && isOfAggregateQueryType(state.query) && 'sql' in state.query) {
+ const indexPatternFromQuery = getIndexPatternFromSQLQuery(state.query.sql);
+ const idsTitles = await indexPatterns.getIdsWithTitle();
+ const dataViewObj = idsTitles.find(({ title }) => title === indexPatternFromQuery);
+ if (dataViewObj) {
+ const columns = getResultColumns();
+ if (columns.length) {
+ setDocumentStateCols(columns);
+ }
+ const nextState = {
+ index: dataViewObj.id,
+ ...(columns.length && { columns }),
+ };
+ stateContainer.replaceUrlAppState(nextState);
+ }
+ }
+ }
+ fetchDataview();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [config, documentState, indexPatterns]);
+
return {
data$,
indexPattern,
diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts
index 597d8b76be8aa..ddf9a95ab37bd 100644
--- a/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts
+++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.test.ts
@@ -10,8 +10,8 @@ import { renderHook } from '@testing-library/react-hooks';
import { createSearchSessionMock } from '../../../__mocks__/search_session';
import { discoverServiceMock } from '../../../__mocks__/services';
import { savedSearchMock } from '../../../__mocks__/saved_search';
-import { useSavedSearch } from './use_saved_search';
-import { getState } from '../services/discover_state';
+import { RecordRawType, useSavedSearch } from './use_saved_search';
+import { getState, AppState } from '../services/discover_state';
import { uiSettingsMock } from '../../../__mocks__/ui_settings';
import { useDiscoverState } from './use_discover_state';
import { FetchStatus } from '../../types';
@@ -128,4 +128,31 @@ describe('test useSavedSearch', () => {
result.current.reset();
expect(result.current.data$.main$.value.fetchStatus).toBe(FetchStatus.LOADING);
});
+
+ test('useSavedSearch returns plain record raw type', async () => {
+ const { history, searchSessionManager } = createSearchSessionMock();
+ const stateContainer = getState({
+ getStateDefaults: () =>
+ ({
+ index: 'the-index-pattern-id',
+ query: { sql: 'SELECT * FROM test' },
+ } as unknown as AppState),
+ history,
+ uiSettings: uiSettingsMock,
+ });
+
+ const { result } = renderHook(() => {
+ return useSavedSearch({
+ initialFetchStatus: FetchStatus.LOADING,
+ savedSearch: savedSearchMock,
+ searchSessionManager,
+ searchSource: savedSearchMock.searchSource.createCopy(),
+ services: discoverServiceMock,
+ stateContainer,
+ useNewFieldsApi: true,
+ });
+ });
+
+ expect(result.current.data$.main$.getValue().recordRawType).toBe(RecordRawType.PLAIN);
+ });
});
diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts
index 0cfa1b2e97579..def74bd94e7b1 100644
--- a/src/plugins/discover/public/application/main/hooks/use_saved_search.ts
+++ b/src/plugins/discover/public/application/main/hooks/use_saved_search.ts
@@ -10,6 +10,7 @@ import { BehaviorSubject, Subject } from 'rxjs';
import type { AutoRefreshDoneFn } from '@kbn/data-plugin/public';
import { ISearchSource } from '@kbn/data-plugin/public';
import { RequestAdapter } from '@kbn/inspector-plugin/public';
+import { getRawRecordType } from '../utils/get_raw_record_type';
import { DiscoverServices } from '../../../build_services';
import { DiscoverSearchSessionManager } from '../services/discover_search_session';
import { GetStateReturn } from '../services/discover_state';
@@ -53,11 +54,23 @@ export interface UseSavedSearch {
inspectorAdapters: { requests: RequestAdapter };
}
+export enum RecordRawType {
+ /**
+ * Documents returned Elasticsearch, nested structure
+ */
+ DOCUMENT = 'document',
+ /**
+ * Data returned e.g. SQL queries, flat structure
+ * */
+ PLAIN = 'plain',
+}
+
export type DataRefetchMsg = 'reset' | undefined;
export interface DataMsg {
fetchStatus: FetchStatus;
error?: Error;
+ recordRawType?: RecordRawType;
}
export interface DataMainMsg extends DataMsg {
@@ -106,6 +119,9 @@ export const useSavedSearch = ({
}) => {
const { data, filterManager } = services;
const timefilter = data.query.timefilter.timefilter;
+ const { query } = stateContainer.appStateContainer.getState();
+
+ const recordRawType = useMemo(() => getRawRecordType(query), [query]);
const inspectorAdapters = useMemo(() => ({ requests: new RequestAdapter() }), []);
@@ -113,17 +129,12 @@ export const useSavedSearch = ({
* The observables the UI (aka React component) subscribes to get notified about
* the changes in the data fetching process (high level: fetching started, data was received)
*/
- const main$: DataMain$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
-
- const documents$: DataDocuments$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
-
- const totalHits$: DataTotalHits$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
-
- const charts$: DataCharts$ = useBehaviorSubject({ fetchStatus: initialFetchStatus });
-
- const availableFields$: AvailableFields$ = useBehaviorSubject({
- fetchStatus: initialFetchStatus,
- });
+ const initialState = { fetchStatus: initialFetchStatus, recordRawType };
+ const main$: DataMain$ = useBehaviorSubject(initialState) as DataMain$;
+ const documents$: DataDocuments$ = useBehaviorSubject(initialState) as DataDocuments$;
+ const totalHits$: DataTotalHits$ = useBehaviorSubject(initialState) as DataTotalHits$;
+ const charts$: DataCharts$ = useBehaviorSubject(initialState) as DataCharts$;
+ const availableFields$: AvailableFields$ = useBehaviorSubject(initialState) as AvailableFields$;
const dataSubjects = useMemo(() => {
return {
diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts
index 0d74061ac46a3..1159aee1c5d13 100644
--- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts
+++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.test.ts
@@ -14,7 +14,7 @@ import {
} from './use_saved_search_messages';
import { FetchStatus } from '../../types';
import { BehaviorSubject } from 'rxjs';
-import { DataMainMsg } from './use_saved_search';
+import { DataMainMsg, RecordRawType } from './use_saved_search';
import { filter } from 'rxjs/operators';
describe('test useSavedSearch message generators', () => {
@@ -52,14 +52,17 @@ describe('test useSavedSearch message generators', () => {
sendPartialMsg(main$);
});
test('sendLoadingMsg', (done) => {
- const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.COMPLETE });
+ const main$ = new BehaviorSubject({
+ fetchStatus: FetchStatus.COMPLETE,
+ });
main$.subscribe((value) => {
if (value.fetchStatus !== FetchStatus.COMPLETE) {
expect(value.fetchStatus).toBe(FetchStatus.LOADING);
+ expect(value.recordRawType).toBe(RecordRawType.DOCUMENT);
done();
}
});
- sendLoadingMsg(main$);
+ sendLoadingMsg(main$, RecordRawType.DOCUMENT);
});
test('sendErrorMsg', (done) => {
const main$ = new BehaviorSubject({ fetchStatus: FetchStatus.PARTIAL });
diff --git a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts
index a2d42147a9e8f..f4aab13592500 100644
--- a/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts
+++ b/src/plugins/discover/public/application/main/hooks/use_saved_search_messages.ts
@@ -12,6 +12,7 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
+ RecordRawType,
SavedSearchData,
} from './use_saved_search';
@@ -33,10 +34,12 @@ export function sendCompleteMsg(main$: DataMain$, foundDocuments = true) {
if (main$.getValue().fetchStatus === FetchStatus.COMPLETE) {
return;
}
+ const recordRawType = main$.getValue().recordRawType;
main$.next({
fetchStatus: FetchStatus.COMPLETE,
foundDocuments,
error: undefined,
+ recordRawType,
});
}
@@ -45,8 +48,10 @@ export function sendCompleteMsg(main$: DataMain$, foundDocuments = true) {
*/
export function sendPartialMsg(main$: DataMain$) {
if (main$.getValue().fetchStatus === FetchStatus.LOADING) {
+ const recordRawType = main$.getValue().recordRawType;
main$.next({
fetchStatus: FetchStatus.PARTIAL,
+ recordRawType,
});
}
}
@@ -54,10 +59,14 @@ export function sendPartialMsg(main$: DataMain$) {
/**
* Send LOADING message via main observable
*/
-export function sendLoadingMsg(data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$) {
+export function sendLoadingMsg(
+ data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$,
+ recordRawType: RecordRawType
+) {
if (data$.getValue().fetchStatus !== FetchStatus.LOADING) {
data$.next({
fetchStatus: FetchStatus.LOADING,
+ recordRawType,
});
}
}
@@ -69,9 +78,11 @@ export function sendErrorMsg(
data$: DataMain$ | DataDocuments$ | DataTotalHits$ | DataCharts$,
error: Error
) {
+ const recordRawType = data$.getValue().recordRawType;
data$.next({
fetchStatus: FetchStatus.ERROR,
error,
+ recordRawType,
});
}
@@ -80,21 +91,26 @@ export function sendErrorMsg(
* Needed when index pattern is switched or a new runtime field is added
*/
export function sendResetMsg(data: SavedSearchData, initialFetchStatus: FetchStatus) {
+ const recordRawType = data.main$.getValue().recordRawType;
data.main$.next({
fetchStatus: initialFetchStatus,
foundDocuments: undefined,
+ recordRawType,
});
data.documents$.next({
fetchStatus: initialFetchStatus,
result: [],
+ recordRawType,
});
data.charts$.next({
fetchStatus: initialFetchStatus,
chartData: undefined,
bucketInterval: undefined,
+ recordRawType,
});
data.totalHits$.next({
fetchStatus: initialFetchStatus,
result: undefined,
+ recordRawType,
});
}
diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts
index 706068006464c..1555eb82d5cc3 100644
--- a/src/plugins/discover/public/application/main/services/discover_state.ts
+++ b/src/plugins/discover/public/application/main/services/discover_state.ts
@@ -16,6 +16,7 @@ import {
compareFilters,
COMPARE_ALL_OPTIONS,
Query,
+ AggregateQuery,
} from '@kbn/es-query';
import {
createKbnUrlStateStorage,
@@ -69,7 +70,7 @@ export interface AppState {
/**
* Lucence or KQL query
*/
- query?: Query;
+ query?: Query | AggregateQuery;
/**
* Array of the used sorting [[field,direction],...]
*/
diff --git a/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts b/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts
index 9abe8bbce4202..11f1a6d3c8ae3 100644
--- a/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts
+++ b/src/plugins/discover/public/application/main/utils/cleanup_url_state.ts
@@ -5,6 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
+import { isOfAggregateQueryType } from '@kbn/es-query';
import { migrateLegacyQuery } from '../../../utils/migrate_legacy_query';
import { AppState, AppStateUrl } from '../services/discover_state';
@@ -13,7 +14,12 @@ import { AppState, AppStateUrl } from '../services/discover_state';
* @param appStateFromUrl
*/
export function cleanupUrlState(appStateFromUrl: AppStateUrl): AppState {
- if (appStateFromUrl && appStateFromUrl.query && !appStateFromUrl.query.language) {
+ if (
+ appStateFromUrl &&
+ appStateFromUrl.query &&
+ !isOfAggregateQueryType(appStateFromUrl.query) &&
+ !appStateFromUrl.query.language
+ ) {
appStateFromUrl.query = migrateLegacyQuery(appStateFromUrl.query);
}
diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts
index 581fe265ea3e8..0f695535062e9 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_all.test.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_all.test.ts
@@ -25,6 +25,7 @@ import {
} from '../hooks/use_saved_search';
import { fetchDocuments } from './fetch_documents';
+import { fetchSql } from './fetch_sql';
import { fetchChart } from './fetch_chart';
import { fetchTotalHits } from './fetch_total_hits';
import { indexPatternMock } from '../../../__mocks__/index_pattern';
@@ -34,6 +35,10 @@ jest.mock('./fetch_documents', () => ({
fetchDocuments: jest.fn().mockResolvedValue([]),
}));
+jest.mock('./fetch_sql', () => ({
+ fetchSql: jest.fn().mockResolvedValue([]),
+}));
+
jest.mock('./fetch_chart', () => ({
fetchChart: jest.fn(),
}));
@@ -45,6 +50,7 @@ jest.mock('./fetch_total_hits', () => ({
const mockFetchDocuments = fetchDocuments as unknown as jest.MockedFunction;
const mockFetchTotalHits = fetchTotalHits as unknown as jest.MockedFunction;
const mockFetchChart = fetchChart as unknown as jest.MockedFunction;
+const mockFetchSQL = fetchSql as unknown as jest.MockedFunction;
function subjectCollector(subject: Subject): () => Promise {
const promise = firstValueFrom(
@@ -89,6 +95,7 @@ describe('test fetchAll', () => {
searchSource = savedSearchMock.searchSource.createChild();
mockFetchDocuments.mockReset().mockResolvedValue([]);
+ mockFetchSQL.mockReset().mockResolvedValue([]);
mockFetchTotalHits.mockReset().mockResolvedValue(42);
mockFetchChart
.mockReset()
@@ -116,14 +123,16 @@ describe('test fetchAll', () => {
{ _id: '1', _index: 'logs' },
{ _id: '2', _index: 'logs' },
];
- mockFetchDocuments.mockResolvedValue(hits);
+ const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
+ mockFetchDocuments.mockResolvedValue(documents);
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
{
fetchStatus: FetchStatus.COMPLETE,
- result: hits.map((hit) => buildDataTableRecord(hit, indexPatternMock)),
+ recordRawType: 'document',
+ result: documents,
},
]);
});
@@ -135,14 +144,16 @@ describe('test fetchAll', () => {
{ _id: '2', _index: 'logs' },
];
searchSource.getField('index')!.isTimeBased = () => false;
- mockFetchDocuments.mockResolvedValue(hits);
+ const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
+ mockFetchDocuments.mockResolvedValue(documents);
+
mockFetchTotalHits.mockResolvedValue(42);
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
- { fetchStatus: FetchStatus.PARTIAL, result: 2 },
- { fetchStatus: FetchStatus.COMPLETE, result: 42 },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
+ { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 2 },
+ { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', result: 42 },
]);
});
@@ -152,8 +163,13 @@ describe('test fetchAll', () => {
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
- { fetchStatus: FetchStatus.COMPLETE, bucketInterval: {}, chartData: {} },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
+ {
+ fetchStatus: FetchStatus.COMPLETE,
+ recordRawType: 'document',
+ bucketInterval: {},
+ chartData: {},
+ },
]);
});
@@ -165,9 +181,9 @@ describe('test fetchAll', () => {
await fetchAll(subjects, searchSource, false, deps);
expect(await collect()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
- { fetchStatus: FetchStatus.PARTIAL, result: 0 }, // From documents query
- { fetchStatus: FetchStatus.COMPLETE, result: 32 },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
+ { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 0 }, // From documents query
+ { fetchStatus: FetchStatus.COMPLETE, recordRawType: 'document', result: 32 },
]);
expect(mockFetchTotalHits).not.toHaveBeenCalled();
});
@@ -177,19 +193,26 @@ describe('test fetchAll', () => {
const collectMain = subjectCollector(subjects.main$);
searchSource.getField('index')!.isTimeBased = () => false;
mockFetchTotalHits.mockRejectedValue({ msg: 'Oh noes!' });
- mockFetchDocuments.mockResolvedValue([{ _id: '1', _index: 'logs' }]);
+ const hits = [{ _id: '1', _index: 'logs' }];
+ const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
+ mockFetchDocuments.mockResolvedValue(documents);
await fetchAll(subjects, searchSource, false, deps);
expect(await collectTotalHits()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
- { fetchStatus: FetchStatus.PARTIAL, result: 1 },
- { fetchStatus: FetchStatus.ERROR, error: { msg: 'Oh noes!' } },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
+ { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document', result: 1 },
+ { fetchStatus: FetchStatus.ERROR, recordRawType: 'document', error: { msg: 'Oh noes!' } },
]);
expect(await collectMain()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
- { fetchStatus: FetchStatus.PARTIAL },
- { fetchStatus: FetchStatus.COMPLETE, foundDocuments: true },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
+ { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' },
+ {
+ fetchStatus: FetchStatus.COMPLETE,
+ foundDocuments: true,
+ error: undefined,
+ recordRawType: 'document',
+ },
]);
});
@@ -200,10 +223,49 @@ describe('test fetchAll', () => {
await fetchAll(subjects, searchSource, false, deps);
expect(await collectMain()).toEqual([
{ fetchStatus: FetchStatus.UNINITIALIZED },
- { fetchStatus: FetchStatus.LOADING },
- { fetchStatus: FetchStatus.PARTIAL }, // From totalHits query
- { fetchStatus: FetchStatus.ERROR, error: { msg: 'This query failed' } },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'document' },
+ { fetchStatus: FetchStatus.PARTIAL, recordRawType: 'document' }, // From totalHits query
+ {
+ fetchStatus: FetchStatus.ERROR,
+ error: { msg: 'This query failed' },
+ recordRawType: 'document',
+ },
// Here should be no COMPLETE coming anymore
]);
});
+
+ test('emits loading and documents on documents$ correctly for SQL query', async () => {
+ const collect = subjectCollector(subjects.documents$);
+ const hits = [
+ { _id: '1', _index: 'logs' },
+ { _id: '2', _index: 'logs' },
+ ];
+ const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
+ mockFetchSQL.mockResolvedValue(documents);
+ deps = {
+ appStateContainer: {
+ getState: () => {
+ return { interval: 'auto', query: { sql: 'SELECT * from foo' } };
+ },
+ } as unknown as ReduxLikeStateContainer,
+ abortController: new AbortController(),
+ data: discoverServiceMock.data,
+ inspectorAdapters: { requests: new RequestAdapter() },
+ searchSessionId: '123',
+ initialFetchStatus: FetchStatus.UNINITIALIZED,
+ useNewFieldsApi: true,
+ savedSearch: savedSearchMock,
+ services: discoverServiceMock,
+ };
+ await fetchAll(subjects, searchSource, false, deps);
+ expect(await collect()).toEqual([
+ { fetchStatus: FetchStatus.UNINITIALIZED },
+ { fetchStatus: FetchStatus.LOADING, recordRawType: 'plain' },
+ {
+ fetchStatus: FetchStatus.COMPLETE,
+ recordRawType: 'plain',
+ result: documents,
+ },
+ ]);
+ });
});
diff --git a/src/plugins/discover/public/application/main/utils/fetch_all.ts b/src/plugins/discover/public/application/main/utils/fetch_all.ts
index bf623c3ad07df..405e79e0e7d55 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_all.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_all.ts
@@ -9,7 +9,7 @@ import { DataPublicPluginStart, ISearchSource } from '@kbn/data-plugin/public';
import { Adapters } from '@kbn/inspector-plugin/common';
import { ReduxLikeStateContainer } from '@kbn/kibana-utils-plugin/common';
import { DataViewType } from '@kbn/data-views-plugin/public';
-import { buildDataTableRecord } from '../../../utils/build_data_record';
+import { getRawRecordType } from './get_raw_record_type';
import {
sendCompleteMsg,
sendErrorMsg,
@@ -30,9 +30,11 @@ import {
DataDocuments$,
DataMain$,
DataTotalHits$,
+ RecordRawType,
SavedSearchData,
} from '../hooks/use_saved_search';
import { DiscoverServices } from '../../../build_services';
+import { fetchSql } from './fetch_sql';
export interface FetchDeps {
abortController: AbortController;
@@ -81,38 +83,42 @@ export function fetchAll(
};
try {
- const indexPattern = searchSource.getField('index')!;
-
+ const dataView = searchSource.getField('index')!;
if (reset) {
sendResetMsg(dataSubjects, initialFetchStatus);
}
-
- const { hideChart, sort } = appStateContainer.getState();
-
- // Update the base searchSource, base for all child fetches
- updateSearchSource(searchSource, false, {
- indexPattern,
- services,
- sort: sort as SortOrder[],
- useNewFieldsApi,
- });
+ const { hideChart, sort, query } = appStateContainer.getState();
+ const recordRawType = getRawRecordType(query);
+ const useSql = recordRawType === RecordRawType.PLAIN;
+
+ if (recordRawType === RecordRawType.DOCUMENT) {
+ // Update the base searchSource, base for all child fetches
+ updateSearchSource(searchSource, false, {
+ indexPattern: dataView,
+ services,
+ sort: sort as SortOrder[],
+ useNewFieldsApi,
+ });
+ }
// Mark all subjects as loading
- sendLoadingMsg(dataSubjects.main$);
- sendLoadingMsg(dataSubjects.documents$);
- sendLoadingMsg(dataSubjects.totalHits$);
- sendLoadingMsg(dataSubjects.charts$);
+ sendLoadingMsg(dataSubjects.main$, recordRawType);
+ sendLoadingMsg(dataSubjects.documents$, recordRawType);
+ sendLoadingMsg(dataSubjects.totalHits$, recordRawType);
+ sendLoadingMsg(dataSubjects.charts$, recordRawType);
const isChartVisible =
- !hideChart && indexPattern.isTimeBased() && indexPattern.type !== DataViewType.ROLLUP;
+ !hideChart && dataView.isTimeBased() && dataView.type !== DataViewType.ROLLUP;
// Start fetching all required requests
- const documents = fetchDocuments(searchSource.createCopy(), fetchDeps);
- const charts = isChartVisible ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined;
- const totalHits = !isChartVisible
- ? fetchTotalHits(searchSource.createCopy(), fetchDeps)
- : undefined;
-
+ const documents =
+ useSql && query
+ ? fetchSql(query, services.indexPatterns, data, services.expressions)
+ : fetchDocuments(searchSource.createCopy(), fetchDeps);
+ const charts =
+ isChartVisible && !useSql ? fetchChart(searchSource.createCopy(), fetchDeps) : undefined;
+ const totalHits =
+ !isChartVisible && !useSql ? fetchTotalHits(searchSource.createCopy(), fetchDeps) : undefined;
/**
* This method checks the passed in hit count and will send a PARTIAL message to main$
* if there are results, indicating that we have finished some of the requests that have been
@@ -138,15 +144,14 @@ export function fetchAll(
dataSubjects.totalHits$.next({
fetchStatus: FetchStatus.PARTIAL,
result: docs.length,
+ recordRawType,
});
}
- const dataView = searchSource.getField('index')!;
-
- const resultDocs = docs.map((doc) => buildDataTableRecord(doc, dataView));
dataSubjects.documents$.next({
fetchStatus: FetchStatus.COMPLETE,
- result: resultDocs,
+ result: docs,
+ recordRawType,
});
checkHitCount(docs.length);
@@ -161,12 +166,14 @@ export function fetchAll(
dataSubjects.totalHits$.next({
fetchStatus: FetchStatus.COMPLETE,
result: chart.totalHits,
+ recordRawType,
});
dataSubjects.charts$.next({
fetchStatus: FetchStatus.COMPLETE,
chartData: chart.chartData,
bucketInterval: chart.bucketInterval,
+ recordRawType,
});
checkHitCount(chart.totalHits);
@@ -175,7 +182,11 @@ export function fetchAll(
totalHits
?.then((hitCount) => {
- dataSubjects.totalHits$.next({ fetchStatus: FetchStatus.COMPLETE, result: hitCount });
+ dataSubjects.totalHits$.next({
+ fetchStatus: FetchStatus.COMPLETE,
+ result: hitCount,
+ recordRawType,
+ });
checkHitCount(hitCount);
})
.catch(sendErrorTo(dataSubjects.totalHits$));
diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts
index 2dc34d1a53a23..a2f7f039d9d5a 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_documents.test.ts
@@ -14,6 +14,9 @@ import { IKibanaSearchResponse } from '@kbn/data-plugin/public';
import { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { FetchDeps } from './fetch_all';
import { fetchTotalHits } from './fetch_total_hits';
+import type { EsHitRecord } from '../../../types';
+import { buildDataTableRecord } from '../../../utils/build_data_record';
+import { indexPatternMock } from '../../../__mocks__/index_pattern';
const getDeps = () =>
({
@@ -30,10 +33,11 @@ describe('test fetchDocuments', () => {
const hits = [
{ _id: '1', foo: 'bar' },
{ _id: '2', foo: 'baz' },
- ];
+ ] as unknown as EsHitRecord[];
+ const documents = hits.map((hit) => buildDataTableRecord(hit, indexPatternMock));
savedSearchMock.searchSource.fetch$ = () =>
of({ rawResponse: { hits: { hits } } } as unknown as IKibanaSearchResponse);
- expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual(hits);
+ expect(fetchDocuments(savedSearchMock.searchSource, getDeps())).resolves.toEqual(documents);
});
test('rejects on query failure', () => {
diff --git a/src/plugins/discover/public/application/main/utils/fetch_documents.ts b/src/plugins/discover/public/application/main/utils/fetch_documents.ts
index e09875d11deb6..7653d7096b49b 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_documents.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_documents.ts
@@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n';
import { filter, map } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public';
+import { buildDataTableRecordList } from '../../../utils/build_data_record';
import { SAMPLE_SIZE_SETTING } from '../../../../common';
import { FetchDeps } from './fetch_all';
@@ -31,6 +32,7 @@ export const fetchDocuments = (
// not a rollup index pattern.
searchSource.setOverwriteDataViewType(undefined);
}
+ const dataView = searchSource.getField('index')!;
const executionContext = {
description: 'fetch documents',
@@ -53,7 +55,9 @@ export const fetchDocuments = (
})
.pipe(
filter((res) => isCompleteResponse(res)),
- map((res) => res.rawResponse.hits.hits)
+ map((res) => {
+ return buildDataTableRecordList(res.rawResponse.hits.hits, dataView);
+ })
);
return lastValueFrom(fetch$);
diff --git a/src/plugins/discover/public/application/main/utils/fetch_sql.ts b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
new file mode 100644
index 0000000000000..2faa821bc66ec
--- /dev/null
+++ b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+import { pluck } from 'rxjs/operators';
+import { lastValueFrom } from 'rxjs';
+import { Query, AggregateQuery, Filter } from '@kbn/es-query';
+import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
+import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
+import type { Datatable } from '@kbn/expressions-plugin/public';
+import type { DataViewsContract } from '@kbn/data-views-plugin/common';
+import { queryStateToExpressionAst } from '@kbn/data-plugin/common';
+import { DataTableRecord } from '../../../types';
+
+interface SQLErrorResponse {
+ error: {
+ message: string;
+ };
+ type: 'error';
+}
+
+export function fetchSql(
+ query: Query | AggregateQuery,
+ dataViewsService: DataViewsContract,
+ data: DataPublicPluginStart,
+ expressions: ExpressionsStart,
+ filters?: Filter[],
+ inputQuery?: Query
+) {
+ const timeRange = data.query.timefilter.timefilter.getTime();
+ return queryStateToExpressionAst({
+ filters,
+ query,
+ time: timeRange,
+ dataViewsService,
+ inputQuery,
+ })
+ .then((ast) => {
+ if (ast) {
+ const execution = expressions.run(ast, null);
+ let finalData: DataTableRecord[] = [];
+ let error: string | undefined;
+ execution.pipe(pluck('result')).subscribe((resp) => {
+ const response = resp as Datatable | SQLErrorResponse;
+ if (response.type === 'error') {
+ error = response.error.message;
+ } else {
+ const table = response as Datatable;
+ const rows = table?.rows ?? [];
+ finalData = rows.map(
+ (row: Record, idx: number) =>
+ ({
+ id: String(idx),
+ raw: row,
+ flattened: row,
+ } as unknown as DataTableRecord)
+ );
+ }
+ });
+ return lastValueFrom(execution).then(() => {
+ if (error) {
+ throw new Error(error);
+ } else {
+ return finalData || [];
+ }
+ });
+ }
+ return [];
+ })
+ .catch((err) => {
+ throw new Error(err.message);
+ });
+}
diff --git a/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts b/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts
new file mode 100644
index 0000000000000..879ece28a527f
--- /dev/null
+++ b/src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts
@@ -0,0 +1,23 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { RecordRawType } from '../hooks/use_saved_search';
+import { getRawRecordType } from './get_raw_record_type';
+
+describe('getRawRecordType', () => {
+ it('returns empty string for Query type query', () => {
+ const mode = getRawRecordType({ query: '', language: 'lucene' });
+ expect(mode).toEqual(RecordRawType.DOCUMENT);
+ });
+
+ it('returns sql for Query type query', () => {
+ const mode = getRawRecordType({ sql: 'SELECT * from foo' });
+
+ expect(mode).toEqual(RecordRawType.PLAIN);
+ });
+});
diff --git a/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts b/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts
new file mode 100644
index 0000000000000..8a2fc81f06a82
--- /dev/null
+++ b/src/plugins/discover/public/application/main/utils/get_raw_record_type.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import {
+ AggregateQuery,
+ Query,
+ isOfAggregateQueryType,
+ getAggregateQueryMode,
+} from '@kbn/es-query';
+import { RecordRawType } from '../hooks/use_saved_search';
+
+export function getRawRecordType(query?: Query | AggregateQuery) {
+ if (query && isOfAggregateQueryType(query) && getAggregateQueryMode(query) === 'sql') {
+ return RecordRawType.PLAIN;
+ }
+
+ return RecordRawType.DOCUMENT;
+}
+
+export function isPlainRecord(query?: Query | AggregateQuery): query is AggregateQuery {
+ return getRawRecordType(query) === RecordRawType.PLAIN;
+}
diff --git a/src/plugins/discover/public/application/main/utils/get_result_state.ts b/src/plugins/discover/public/application/main/utils/get_result_state.ts
index ceb6de0cc7798..ff31e114754ad 100644
--- a/src/plugins/discover/public/application/main/utils/get_result_state.ts
+++ b/src/plugins/discover/public/application/main/utils/get_result_state.ts
@@ -18,10 +18,15 @@ export const resultStatuses = {
* Returns the current state of the result, depends on fetchStatus and the given fetched rows
* Determines what is displayed in Discover main view (loading view, data view, empty data view, ...)
*/
-export function getResultState(fetchStatus: FetchStatus, foundDocuments: boolean = false) {
+export function getResultState(
+ fetchStatus: FetchStatus,
+ foundDocuments: boolean = false,
+ isPlainRecord?: boolean
+) {
if (fetchStatus === FetchStatus.UNINITIALIZED) {
return resultStatuses.UNINITIALIZED;
}
+ if (isPlainRecord && fetchStatus === FetchStatus.ERROR) return resultStatuses.NO_RESULTS;
if (!foundDocuments && fetchStatus === FetchStatus.LOADING) return resultStatuses.LOADING;
else if (foundDocuments) return resultStatuses.READY;
diff --git a/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts
index 59dbd79f157bf..bab5207a18e4e 100644
--- a/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts
+++ b/src/plugins/discover/public/application/main/utils/get_switch_index_pattern_app_state.ts
@@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import { isOfAggregateQueryType, Query, AggregateQuery } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public';
import { getSortArray, SortPairArr } from '../../../components/doc_table/utils/get_sort';
@@ -19,7 +19,8 @@ export function getSwitchIndexPatternAppState(
currentColumns: string[],
currentSort: SortPairArr[],
modifyColumns: boolean = true,
- sortDirection: string = 'desc'
+ sortDirection: string = 'desc',
+ query?: Query | AggregateQuery
) {
const nextColumns = modifyColumns
? currentColumns.filter(
@@ -27,7 +28,11 @@ export function getSwitchIndexPatternAppState(
nextIndexPattern.fields.getByName(column) || !currentIndexPattern.fields.getByName(column)
)
: currentColumns;
- const columns = nextColumns.length ? nextColumns : [];
+
+ let columns = nextColumns.length ? nextColumns : [];
+ if (query && isOfAggregateQueryType(query)) {
+ columns = [];
+ }
// when switching from an index pattern with timeField to an index pattern without timeField
// filter out sorting by timeField in case it is set. index patterns without timeField don't
diff --git a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts
index 30430abd941a8..78e4dbb39383b 100644
--- a/src/plugins/discover/public/application/main/utils/persist_saved_search.ts
+++ b/src/plugins/discover/public/application/main/utils/persist_saved_search.ts
@@ -5,7 +5,7 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import { isOfAggregateQueryType } from '@kbn/es-query';
import { DataView } from '@kbn/data-views-plugin/public';
import { SavedObjectSaveOpts } from '@kbn/saved-objects-plugin/public';
import { updateSearchSource } from './update_search_source';
@@ -14,7 +14,6 @@ import { AppState } from '../services/discover_state';
import type { SortOrder } from '../../../services/saved_searches';
import { DiscoverServices } from '../../../build_services';
import { saveSavedSearch } from '../../../services/saved_searches';
-
/**
* Helper function to update and persist the given savedSearch
*/
@@ -63,6 +62,13 @@ export async function persistSavedSearch(
savedSearch.hideAggregatedPreview = state.hideAggregatedPreview;
}
+ // add a flag here to identify text based language queries
+ // these should be filtered out from the visualize editor
+ const isTextBasedQuery = state.query && isOfAggregateQueryType(state.query);
+ if (savedSearch.isTextBasedQuery || isTextBasedQuery) {
+ savedSearch.isTextBasedQuery = isTextBasedQuery;
+ }
+
try {
const id = await saveSavedSearch(savedSearch, saveOptions, services.core.savedObjects.client);
if (id) {
diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts
index d77bc5dde2660..23fe49149b4ea 100644
--- a/src/plugins/discover/public/build_services.ts
+++ b/src/plugins/discover/public/build_services.ts
@@ -27,6 +27,7 @@ import {
DataViewsContract,
DataPublicPluginStart,
} from '@kbn/data-plugin/public';
+import type { ExpressionsStart } from '@kbn/expressions-plugin/public';
import { Start as InspectorPublicPluginStart } from '@kbn/inspector-plugin/public';
import { SharePluginStart } from '@kbn/share-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
@@ -81,6 +82,7 @@ export interface DiscoverServices {
spaces?: SpacesApi;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
locator: DiscoverAppLocator;
+ expressions: ExpressionsStart;
}
export const buildServices = memoize(function (
@@ -125,5 +127,6 @@ export const buildServices = memoize(function (
dataViewEditor: plugins.dataViewEditor,
triggersActionsUi: plugins.triggersActionsUi,
locator,
+ expressions: plugins.expressions,
};
});
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/components/discover_grid/discover_grid.scss
index 71335e968db4c..3ac2055650128 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid.scss
@@ -3,6 +3,7 @@
max-width: 100%;
height: 100%;
overflow: hidden;
+ border-radius: $euiBorderRadius;
.euiDataGrid__controls {
border: none;
@@ -14,12 +15,16 @@
padding: 0;
}
+ .dscDiscoverGrid__textLanguageMode .euiDataGridRowCell.euiDataGridRowCell--firstColumn {
+ padding: $euiSizeXS;
+ }
+
.euiDataGridRowCell.euiDataGridRowCell--lastColumn {
border-right: none;
}
- .euiDataGridRowCell:first-of-type,
- .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type {
+ .dscDiscoverGrid__documentsMode .euiDataGridRowCell:first-of-type,
+ .dscDiscoverGrid__documentsMode .euiDataGrid--headerShade.euiDataGrid--bordersAll .euiDataGridHeaderCell:first-of-type {
border-left: none;
border-right: none;
}
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
index 803ad6de49a22..c45a9a6f2dc22 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
@@ -117,7 +117,7 @@ export interface DiscoverGridProps {
/**
* Function to set the expanded document, which is displayed in a flyout
*/
- setExpandedDoc: (doc?: DataTableRecord) => void;
+ setExpandedDoc?: (doc?: DataTableRecord) => void;
/**
* Grid display settings persisted in Elasticsearch (e.g. column width)
*/
@@ -162,6 +162,10 @@ export interface DiscoverGridProps {
* Update row height state
*/
onUpdateRowHeight?: (rowHeight: number) => void;
+ /**
+ * Is text base lang mode enabled
+ */
+ isPlainRecord?: boolean;
/**
* Current state value for rowsPerPage
*/
@@ -207,6 +211,7 @@ export const DiscoverGrid = ({
className,
rowHeightState,
onUpdateRowHeight,
+ isPlainRecord = false,
rowsPerPageState,
onUpdateRowsPerPage,
onFieldEdited,
@@ -391,9 +396,11 @@ export const DiscoverGrid = ({
isSortEnabled,
services,
valueToStringConverter,
+ onFilter,
editField,
}),
[
+ onFilter,
displayedColumns,
displayedRows,
indexPattern,
@@ -428,8 +435,8 @@ export const DiscoverGrid = ({
return { columns: sortingColumns, onSort: () => {} };
}, [sortingColumns, onTableSort, isSortEnabled]);
const lead = useMemo(
- () => getLeadControlColumns().filter(({ id }) => controlColumnIds.includes(id)),
- [controlColumnIds]
+ () => getLeadControlColumns(setExpandedDoc).filter(({ id }) => controlColumnIds.includes(id)),
+ [controlColumnIds, setExpandedDoc]
);
const additionalControls = useMemo(
@@ -539,7 +546,11 @@ export const DiscoverGrid = ({
data-title={searchTitle}
data-description={searchDescription}
data-document-number={displayedRows.length}
- className={classnames(className, 'dscDiscoverGrid__table')}
+ className={classnames(
+ className,
+ 'dscDiscoverGrid__table',
+ isPlainRecord ? 'dscDiscoverGrid__textLanguageMode' : 'dscDiscoverGrid__documentsMode'
+ )}
>
)}
- {expandedDoc && (
+ {setExpandedDoc && expandedDoc && (
{},
});
expect(actual).toMatchInlineSnapshot(`
Array [
@@ -134,6 +135,7 @@ describe('Discover grid columns', function () {
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
rowsCount: 100,
services: discoverServiceMock,
+ onFilter: () => {},
});
expect(actual).toMatchInlineSnapshot(`
Array [
@@ -238,6 +240,7 @@ describe('Discover grid columns', function () {
valueToStringConverter: discoverGridContextMock.valueToStringConverter,
rowsCount: 100,
services: discoverServiceMock,
+ onFilter: () => {},
});
expect(actual).toMatchInlineSnapshot(`
Array [
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx
index 561906f8d1863..1f3bd67b20fb5 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_columns.tsx
@@ -10,6 +10,7 @@ import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiDataGridColumn, EuiIcon, EuiScreenReaderOnly, EuiToolTip } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public';
+import { DocViewFilterFn } from '../../services/doc_views/doc_views_types';
import { ExpandButton } from './discover_grid_expand_button';
import { DiscoverGridSettings } from './types';
import type { ValueToStringConverter } from '../../types';
@@ -19,39 +20,44 @@ import { SelectButton } from './discover_grid_document_selection';
import { defaultTimeColumnWidth } from './constants';
import { buildCopyColumnNameButton, buildCopyColumnValuesButton } from './build_copy_column_button';
import { DiscoverServices } from '../../build_services';
+import { DataTableRecord } from '../../types';
import { buildEditFieldButton } from './build_edit_field_button';
-export function getLeadControlColumns() {
- return [
- {
- id: 'openDetails',
- width: 24,
- headerCellRender: () => (
-
-
- {i18n.translate('discover.controlColumnHeader', {
- defaultMessage: 'Control column',
- })}
-
-
- ),
- rowCellRender: ExpandButton,
- },
- {
- id: 'select',
- width: 24,
- rowCellRender: SelectButton,
- headerCellRender: () => (
-
-
- {i18n.translate('discover.selectColumnHeader', {
- defaultMessage: 'Select column',
- })}
-
-
- ),
- },
- ];
+const openDetails = {
+ id: 'openDetails',
+ width: 24,
+ headerCellRender: () => (
+
+
+ {i18n.translate('discover.controlColumnHeader', {
+ defaultMessage: 'Control column',
+ })}
+
+
+ ),
+ rowCellRender: ExpandButton,
+};
+
+const select = {
+ id: 'select',
+ width: 24,
+ rowCellRender: SelectButton,
+ headerCellRender: () => (
+
+
+ {i18n.translate('discover.selectColumnHeader', {
+ defaultMessage: 'Select column',
+ })}
+
+
+ ),
+};
+
+export function getLeadControlColumns(setExpandedDoc?: (doc?: DataTableRecord) => void) {
+ if (!setExpandedDoc) {
+ return [select];
+ }
+ return [openDetails, select];
}
function buildEuiGridColumn({
@@ -63,6 +69,7 @@ function buildEuiGridColumn({
services,
valueToStringConverter,
rowsCount,
+ onFilter,
editField,
}: {
columnName: string;
@@ -73,6 +80,7 @@ function buildEuiGridColumn({
services: DiscoverServices;
valueToStringConverter: ValueToStringConverter;
rowsCount: number;
+ onFilter?: DocViewFilterFn;
editField?: (fieldName: string) => void;
}) {
const indexPatternField = indexPattern.getFieldByName(columnName);
@@ -115,7 +123,7 @@ function buildEuiGridColumn({
...(editFieldButton ? [editFieldButton] : []),
],
},
- cellActions: indexPatternField ? buildCellActions(indexPatternField) : [],
+ cellActions: indexPatternField ? buildCellActions(indexPatternField, onFilter) : [],
};
if (column.id === indexPattern.timeFieldName) {
@@ -161,6 +169,7 @@ export function getEuiGridColumns({
isSortEnabled,
services,
valueToStringConverter,
+ onFilter,
editField,
}: {
columns: string[];
@@ -172,6 +181,7 @@ export function getEuiGridColumns({
isSortEnabled: boolean;
services: DiscoverServices;
valueToStringConverter: ValueToStringConverter;
+ onFilter: DocViewFilterFn;
editField?: (fieldName: string) => void;
}) {
const timeFieldName = indexPattern.timeFieldName;
@@ -192,6 +202,7 @@ export function getEuiGridColumns({
services,
valueToStringConverter,
rowsCount,
+ onFilter,
editField,
})
);
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx
index 0761e4c40376e..24b2275f11763 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_context.tsx
@@ -13,9 +13,9 @@ import type { DataTableRecord, ValueToStringConverter } from '../../types';
export interface GridContext {
expanded?: DataTableRecord | undefined;
- setExpanded: (hit?: DataTableRecord) => void;
+ setExpanded?: (hit?: DataTableRecord) => void;
rows: DataTableRecord[];
- onFilter: DocViewFilterFn;
+ onFilter?: DocViewFilterFn;
indexPattern: DataView;
isDarkMode: boolean;
selectedDocs: string[];
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx
index e00e722f9c2e9..dfe44dbb2edbb 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_expand_button.tsx
@@ -43,6 +43,9 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
const testSubj = current.isAnchor
? 'docTableExpandToggleColumnAnchor'
: 'docTableExpandToggleColumn';
+ if (!setExpanded) {
+ return null;
+ }
return (
@@ -52,7 +55,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle
iconSize="s"
aria-label={buttonLabel}
data-test-subj={testSubj}
- onClick={() => setExpanded(isCurrentRowExpanded ? undefined : current)}
+ onClick={() => setExpanded?.(isCurrentRowExpanded ? undefined : current)}
color={isCurrentRowExpanded ? 'primary' : 'text'}
iconType={isCurrentRowExpanded ? 'minimize' : 'expand'}
isSelected={isCurrentRowExpanded}
diff --git a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx
index c2165fc27ee2a..af16fba24556e 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid_flyout.tsx
@@ -38,7 +38,7 @@ export interface DiscoverGridFlyoutProps {
indexPattern: DataView;
onAddColumn: (column: string) => void;
onClose: () => void;
- onFilter: DocViewFilterFn;
+ onFilter?: DocViewFilterFn;
onRemoveColumn: (column: string) => void;
setExpandedDoc: (doc: DataTableRecord) => void;
}
@@ -213,14 +213,18 @@ export function DiscoverGridFlyout({
hit={actualHit}
columns={columns}
indexPattern={indexPattern}
- filter={(mapping, value, mode) => {
- onFilter(mapping, value, mode);
- services.toastNotifications.addSuccess(
- i18n.translate('discover.grid.flyout.toastFilterAdded', {
- defaultMessage: `Filter was added`,
- })
- );
- }}
+ filter={
+ onFilter
+ ? (mapping, value, mode) => {
+ onFilter(mapping, value, mode);
+ services.toastNotifications.addSuccess(
+ i18n.translate('discover.grid.flyout.toastFilterAdded', {
+ defaultMessage: `Filter was added`,
+ })
+ );
+ }
+ : undefined
+ }
onRemoveColumn={(columnName: string) => {
onRemoveColumn(columnName);
services.toastNotifications.addSuccess(
diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx
index cc25819443d22..7a2e31d500570 100644
--- a/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx
+++ b/src/plugins/discover/public/components/discover_tour/discover_tour.test.tsx
@@ -16,10 +16,10 @@ import { useDiscoverTourContext } from './discover_tour_context';
import { DISCOVER_TOUR_STEP_ANCHORS } from './discover_tour_anchors';
describe('Discover tour', () => {
- const mountComponent = (innerContent?: JSX.Element) => {
+ const mountComponent = (innerContent: JSX.Element) => {
return mountWithIntl(
- {innerContent}
+ {innerContent}
);
};
diff --git a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx
index 8b50261981f3d..01aabdba43fc5 100644
--- a/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx
+++ b/src/plugins/discover/public/components/discover_tour/discover_tour_provider.tsx
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import React, { useCallback, useMemo } from 'react';
+import React, { ReactElement, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@@ -40,43 +40,64 @@ interface TourStepDefinition {
imageAltText: string;
}
+const ADD_FIELDS_STEP = {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields,
+ title: i18n.translate('discover.dscTour.stepAddFields.title', {
+ defaultMessage: 'Add fields to the table',
+ }),
+ content: (
+ ,
+ }}
+ />
+ ),
+ imageName: 'add_fields.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepAddFields.imageAltText', {
+ defaultMessage:
+ 'In the Available fields list, click the plus icon to toggle a field into the document table.',
+ }),
+};
+
+const ORDER_TABLE_COLUMNS_STEP = {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns,
+ title: i18n.translate('discover.dscTour.stepReorderColumns.title', {
+ defaultMessage: 'Order the table columns',
+ }),
+ content: (
+
+ ),
+ imageName: 'reorder_columns.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepReorderColumns.imageAltText', {
+ defaultMessage: 'Use the Columns popover to drag the columns to the order you prefer.',
+ }),
+};
+
+const CHANGE_ROW_HEIGHT_STEP = {
+ anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight,
+ title: i18n.translate('discover.dscTour.stepChangeRowHeight.title', {
+ defaultMessage: 'Change the row height',
+ }),
+ content: (
+
+ ),
+ imageName: 'rows_per_line.gif',
+ imageAltText: i18n.translate('discover.dscTour.stepChangeRowHeight.imageAltText', {
+ defaultMessage: 'Click the display options icon to adjust the row height to fit the contents.',
+ }),
+};
+
const tourStepDefinitions: TourStepDefinition[] = [
- {
- anchor: DISCOVER_TOUR_STEP_ANCHORS.addFields,
- title: i18n.translate('discover.dscTour.stepAddFields.title', {
- defaultMessage: 'Add fields to the table',
- }),
- content: (
- ,
- }}
- />
- ),
- imageName: 'add_fields.gif',
- imageAltText: i18n.translate('discover.dscTour.stepAddFields.imageAltText', {
- defaultMessage:
- 'In the Available fields list, click the plus icon to toggle a field into the document table.',
- }),
- },
- {
- anchor: DISCOVER_TOUR_STEP_ANCHORS.reorderColumns,
- title: i18n.translate('discover.dscTour.stepReorderColumns.title', {
- defaultMessage: 'Order the table columns',
- }),
- content: (
-
- ),
- imageName: 'reorder_columns.gif',
- imageAltText: i18n.translate('discover.dscTour.stepReorderColumns.imageAltText', {
- defaultMessage: 'Use the Columns popover to drag the columns to the order you prefer.',
- }),
- },
+ ADD_FIELDS_STEP,
+ ORDER_TABLE_COLUMNS_STEP,
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.sort,
title: i18n.translate('discover.dscTour.stepSort.title', {
@@ -94,23 +115,7 @@ const tourStepDefinitions: TourStepDefinition[] = [
'Click a column header and select the desired sort order. Adjust a multi-field sort using the fields sorted popover.',
}),
},
- {
- anchor: DISCOVER_TOUR_STEP_ANCHORS.changeRowHeight,
- title: i18n.translate('discover.dscTour.stepChangeRowHeight.title', {
- defaultMessage: 'Change the row height',
- }),
- content: (
-
- ),
- imageName: 'rows_per_line.gif',
- imageAltText: i18n.translate('discover.dscTour.stepChangeRowHeight.imageAltText', {
- defaultMessage:
- 'Click the display options icon to adjust the row height to fit the contents.',
- }),
- },
+ CHANGE_ROW_HEIGHT_STEP,
{
anchor: DISCOVER_TOUR_STEP_ANCHORS.expandDocument,
title: i18n.translate('discover.dscTour.stepExpand.title', {
@@ -193,7 +198,13 @@ const tourConfig: EuiTourState = {
tourSubtitle: '',
};
-export const DiscoverTourProvider: React.FC = ({ children }) => {
+export const DiscoverTourProvider = ({
+ children,
+ isPlainRecord,
+}: {
+ children: ReactElement;
+ isPlainRecord: boolean;
+}) => {
const services = useDiscoverServices();
const prependToBasePath = services.core.http.basePath.prepend;
const getAssetPath = useCallback(
@@ -203,8 +214,14 @@ export const DiscoverTourProvider: React.FC = ({ children }) => {
[prependToBasePath]
);
const tourSteps = useMemo(
- () => prepareTourSteps(tourStepDefinitions, getAssetPath),
- [getAssetPath]
+ () =>
+ isPlainRecord
+ ? prepareTourSteps(
+ [ADD_FIELDS_STEP, ORDER_TABLE_COLUMNS_STEP, CHANGE_ROW_HEIGHT_STEP],
+ getAssetPath
+ )
+ : prepareTourSteps(tourStepDefinitions, getAssetPath),
+ [getAssetPath, isPlainRecord]
);
const [steps, actions, reducerState] = useEuiTour(tourSteps, tourConfig);
const currentTourStep = reducerState.currentTourStep;
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 999dfbd383570..c693331f66922 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -27,6 +27,7 @@ import { ISearchSource } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
+import { RecordRawType } from '../application/main/hooks/use_saved_search';
import { buildDataTableRecord } from '../utils/build_data_record';
import { DataTableRecord } from '../types';
import { ISearchEmbeddable, SearchInput, SearchOutput } from './types';
@@ -52,6 +53,8 @@ import { SortOrder } from '../components/doc_table/components/table_header/helpe
import { VIEW_MODE } from '../components/view_mode_toggle';
import { updateSearchSource } from './utils/update_search_source';
import { FieldStatisticsTable } from '../application/main/components/field_stats_table';
+import { getRawRecordType } from '../application/main/utils/get_raw_record_type';
+import { fetchSql } from '../application/main/utils/fetch_sql';
export type SearchProps = Partial &
Partial & {
@@ -204,8 +207,36 @@ export class SavedSearchEmbeddable
}
: child;
+ const query = this.savedSearch.searchSource.getField('query');
+ const recordRawType = getRawRecordType(query);
+ const useSql = recordRawType === RecordRawType.PLAIN;
+
try {
- // Make the request
+ // Request SQL data
+ if (useSql && query) {
+ const result = await fetchSql(
+ this.savedSearch.searchSource.getField('query')!,
+ this.services.indexPatterns,
+ this.services.data,
+ this.services.expressions,
+ this.input.filters,
+ this.input.query
+ );
+ this.updateOutput({
+ ...this.getOutput(),
+ loading: false,
+ });
+
+ this.searchProps!.rows = result;
+ this.searchProps!.totalHitCount = result.length;
+ this.searchProps!.isLoading = false;
+ this.searchProps!.isPlainRecord = true;
+ this.searchProps!.showTimeCol = false;
+ this.searchProps!.isSortEnabled = false;
+ return;
+ }
+
+ // Request document data
const { rawResponse: resp } = await lastValueFrom(
searchSource.fetch$({
abortSignal: this.abortController.signal,
diff --git a/src/plugins/discover/public/embeddable/saved_search_grid.tsx b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
index dbfb64775ed2e..0184f1af75b3d 100644
--- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
@@ -35,7 +35,11 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
)}
-
+
);
diff --git a/src/plugins/discover/public/locator.ts b/src/plugins/discover/public/locator.ts
index 459c124d46453..b8db1023e28e1 100644
--- a/src/plugins/discover/public/locator.ts
+++ b/src/plugins/discover/public/locator.ts
@@ -7,7 +7,7 @@
*/
import type { SerializableRecord } from '@kbn/utility-types';
-import type { Filter, TimeRange, Query } from '@kbn/es-query';
+import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query';
import type { GlobalQueryStateFromUrl, RefreshInterval } from '@kbn/data-plugin/public';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/public';
@@ -44,7 +44,7 @@ export interface DiscoverAppLocatorParams extends SerializableRecord {
/**
* Optionally set a query.
*/
- query?: Query;
+ query?: Query | AggregateQuery;
/**
* If not given, will use the uiSettings configuration for `storeInSessionStorage`. useHash determines
@@ -116,7 +116,7 @@ export class DiscoverAppLocatorDefinition implements LocatorDefinition {
"hideAggregatedPreview": undefined,
"hideChart": false,
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
+ "isTextBasedQuery": undefined,
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": Object {
@@ -149,4 +150,97 @@ describe('getSavedSearch', () => {
}
`);
});
+
+ test('should find saved search with sql mode', async () => {
+ savedObjectsClient.resolve = jest.fn().mockReturnValue({
+ saved_object: {
+ attributes: {
+ kibanaSavedObjectMeta: {
+ searchSourceJSON:
+ '{"query":{"sql":"SELECT * FROM foo"},"filter":[],"indexRefName":"kibanaSavedObjectMeta.searchSourceJSON.index"}',
+ },
+ title: 'test2',
+ sort: [['order_date', 'desc']],
+ columns: ['_source'],
+ description: 'description',
+ grid: {},
+ hideChart: true,
+ isTextBasedQuery: true,
+ },
+ id: 'ccf1af80-2297-11ec-86e0-1155ffb9c7a7',
+ type: 'search',
+ references: [
+ {
+ name: 'kibanaSavedObjectMeta.searchSourceJSON.index',
+ id: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f',
+ type: 'index-pattern',
+ },
+ ],
+ namespaces: ['default'],
+ },
+ outcome: 'exactMatch',
+ });
+
+ const savedSearch = await getSavedSearch('ccf1af80-2297-11ec-86e0-1155ffb9c7a7', {
+ savedObjectsClient,
+ search,
+ });
+
+ expect(savedObjectsClient.resolve).toHaveBeenCalled();
+ expect(savedSearch).toMatchInlineSnapshot(`
+ Object {
+ "columns": Array [
+ "_source",
+ ],
+ "description": "description",
+ "grid": Object {},
+ "hideAggregatedPreview": undefined,
+ "hideChart": true,
+ "id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
+ "isTextBasedQuery": true,
+ "rowHeight": undefined,
+ "rowsPerPage": undefined,
+ "searchSource": Object {
+ "create": [MockFunction],
+ "createChild": [MockFunction],
+ "createCopy": [MockFunction],
+ "destroy": [MockFunction],
+ "fetch": [MockFunction],
+ "fetch$": [MockFunction],
+ "getActiveIndexFilter": [MockFunction],
+ "getField": [MockFunction],
+ "getFields": [MockFunction],
+ "getId": [MockFunction],
+ "getOwnField": [MockFunction],
+ "getParent": [MockFunction],
+ "getSearchRequestBody": [MockFunction],
+ "getSerializedFields": [MockFunction],
+ "history": Array [],
+ "onRequestStart": [MockFunction],
+ "parseActiveIndexPatternFromQueryString": [MockFunction],
+ "removeField": [MockFunction],
+ "serialize": [MockFunction],
+ "setField": [MockFunction],
+ "setFields": [MockFunction],
+ "setOverwriteDataViewType": [MockFunction],
+ "setParent": [MockFunction],
+ "toExpressionAst": [MockFunction],
+ },
+ "sharingSavedObjectProps": Object {
+ "aliasPurpose": undefined,
+ "aliasTargetId": undefined,
+ "errorJSON": undefined,
+ "outcome": "exactMatch",
+ },
+ "sort": Array [
+ Array [
+ "order_date",
+ "desc",
+ ],
+ ],
+ "title": "test2",
+ "viewMode": undefined,
+ }
+ `);
+ });
});
diff --git a/src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts b/src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts
index 23c1107fe23c1..0c88b2bcea94a 100644
--- a/src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts
+++ b/src/plugins/discover/public/services/saved_searches/save_saved_searches.test.ts
@@ -87,6 +87,7 @@ describe('saveSavedSearch', () => {
columns: [],
description: '',
grid: {},
+ isTextBasedQuery: false,
hideChart: false,
kibanaSavedObjectMeta: { searchSourceJSON: '{}' },
sort: [],
@@ -106,6 +107,7 @@ describe('saveSavedSearch', () => {
columns: [],
description: '',
grid: {},
+ isTextBasedQuery: false,
hideChart: false,
kibanaSavedObjectMeta: { searchSourceJSON: '{}' },
sort: [],
diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts
index 74be73b609a3b..84a6c35de0c02 100644
--- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts
+++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.test.ts
@@ -27,6 +27,7 @@ describe('saved_searches_utils', () => {
description: 'foo',
grid: {},
hideChart: true,
+ isTextBasedQuery: false,
};
expect(fromSavedSearchAttributes('id', attributes, createSearchSourceMock(), {}))
@@ -41,6 +42,7 @@ describe('saved_searches_utils', () => {
"hideAggregatedPreview": undefined,
"hideChart": true,
"id": "id",
+ "isTextBasedQuery": false,
"rowHeight": undefined,
"rowsPerPage": undefined,
"searchSource": SearchSource {
@@ -104,6 +106,7 @@ describe('saved_searches_utils', () => {
description: 'description',
grid: {},
hideChart: true,
+ isTextBasedQuery: true,
};
expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(`
@@ -116,6 +119,7 @@ describe('saved_searches_utils', () => {
"grid": Object {},
"hideAggregatedPreview": undefined,
"hideChart": true,
+ "isTextBasedQuery": true,
"kibanaSavedObjectMeta": Object {
"searchSourceJSON": "{}",
},
diff --git a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts
index d07e418c3aa10..5c13c9d78cb9e 100644
--- a/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts
+++ b/src/plugins/discover/public/services/saved_searches/saved_searches_utils.ts
@@ -45,6 +45,7 @@ export const fromSavedSearchAttributes = (
viewMode: attributes.viewMode,
hideAggregatedPreview: attributes.hideAggregatedPreview,
rowHeight: attributes.rowHeight,
+ isTextBasedQuery: attributes.isTextBasedQuery,
rowsPerPage: attributes.rowsPerPage,
});
@@ -62,5 +63,6 @@ export const toSavedSearchAttributes = (
viewMode: savedSearch.viewMode,
hideAggregatedPreview: savedSearch.hideAggregatedPreview,
rowHeight: savedSearch.rowHeight,
+ isTextBasedQuery: savedSearch.isTextBasedQuery ?? false,
rowsPerPage: savedSearch.rowsPerPage,
});
diff --git a/src/plugins/discover/public/services/saved_searches/types.ts b/src/plugins/discover/public/services/saved_searches/types.ts
index 1e81c9e88c8a3..e04d4c8054aa0 100644
--- a/src/plugins/discover/public/services/saved_searches/types.ts
+++ b/src/plugins/discover/public/services/saved_searches/types.ts
@@ -21,6 +21,7 @@ export interface SavedSearchAttributes {
columns?: Record;
};
hideChart: boolean;
+ isTextBasedQuery: boolean;
kibanaSavedObjectMeta: {
searchSourceJSON: string;
};
@@ -54,5 +55,6 @@ export interface SavedSearch {
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
+ isTextBasedQuery?: boolean;
rowsPerPage?: number;
}
diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts
index c7eba9dfc0b14..85f6542f6b07d 100644
--- a/src/plugins/discover/server/saved_objects/search.ts
+++ b/src/plugins/discover/server/saved_objects/search.ts
@@ -38,6 +38,7 @@ export function getSavedSearchObjectType(
description: { type: 'text' },
viewMode: { type: 'keyword', index: false, doc_values: false },
hideChart: { type: 'boolean', index: false, doc_values: false },
+ isTextBasedQuery: { type: 'boolean', index: false, doc_values: false },
hideAggregatedPreview: { type: 'boolean', index: false, doc_values: false },
hits: { type: 'integer', index: false, doc_values: false },
kibanaSavedObjectMeta: {
diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts
index 4f9f8f05b7803..0419594d31d47 100644
--- a/src/plugins/discover/server/ui_settings.ts
+++ b/src/plugins/discover/server/ui_settings.ts
@@ -30,9 +30,14 @@ import {
TRUNCATE_MAX_HEIGHT,
SHOW_FIELD_STATISTICS,
ROW_HEIGHT_OPTION,
+ ENABLE_SQL,
} from '../common';
import { DEFAULT_ROWS_PER_PAGE, ROWS_PER_PAGE_OPTIONS } from '../common/constants';
+const technicalPreviewLabel = i18n.translate('discover.advancedSettings.technicalPreviewLabel', {
+ defaultMessage: 'technical preview',
+});
+
export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record = (
docLinks: DocLinksServiceSetup
) => ({
@@ -303,4 +308,26 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record` +
+ i18n.translate('discover.advancedSettings.enableSQL.discussLinkText', {
+ defaultMessage: 'discuss.elastic.co/c/elastic-stack/kibana',
+ }) +
+ '',
+ technicalPreviewLabel: `[${technicalPreviewLabel}]`,
+ },
+ }),
+ requiresPageReload: true,
+ category: ['discover'],
+ schema: schema.boolean(),
+ },
});
diff --git a/src/plugins/discover/tsconfig.json b/src/plugins/discover/tsconfig.json
index 9915680ada26e..5cdc21844c8a1 100644
--- a/src/plugins/discover/tsconfig.json
+++ b/src/plugins/discover/tsconfig.json
@@ -11,6 +11,7 @@
{ "path": "../../core/tsconfig.json" },
{ "path": "../charts/tsconfig.json" },
{ "path": "../data/tsconfig.json" },
+ { "path": "../expressions/tsconfig.json" },
{ "path": "../embeddable/tsconfig.json" },
{ "path": "../inspector/tsconfig.json" },
{ "path": "../url_forwarding/tsconfig.json" },
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
index 107be88cffbd4..f1b230b72c337 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -486,6 +486,10 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
+ 'discover:enableSql': {
+ type: 'boolean',
+ _meta: { description: 'Non-default value of setting.' },
+ },
'discover:rowHeightOption': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
index 8fb092fd286e5..1a946e99f29bd 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -34,6 +34,7 @@ export interface UsageStats {
'discover:searchFieldsFromSource': boolean;
'discover:showFieldStatistics': boolean;
'discover:showMultiFields': boolean;
+ 'discover:enableSql': boolean;
'discover:maxDocFieldsDisplayed': number;
'securitySolution:rulesTableRefresh': string;
'observability:enableInspectEsQueries': boolean;
diff --git a/src/plugins/navigation/public/mocks.ts b/src/plugins/navigation/public/mocks.ts
index ef41036c34b30..64e535b6e114a 100644
--- a/src/plugins/navigation/public/mocks.ts
+++ b/src/plugins/navigation/public/mocks.ts
@@ -23,6 +23,7 @@ const createStartContract = (): jest.Mocked => {
const startContract = {
ui: {
TopNavMenu: jest.fn(),
+ AggregateQueryTopNavMenu: jest.fn(),
},
};
return startContract;
diff --git a/src/plugins/navigation/public/plugin.ts b/src/plugins/navigation/public/plugin.ts
index 9d04acdeacba6..e3fe470d9c338 100644
--- a/src/plugins/navigation/public/plugin.ts
+++ b/src/plugins/navigation/public/plugin.ts
@@ -39,6 +39,7 @@ export class NavigationPublicPlugin
return {
ui: {
TopNavMenu: createTopNav(unifiedSearch, extensions),
+ AggregateQueryTopNavMenu: createTopNav(unifiedSearch, extensions),
},
};
}
diff --git a/src/plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx
index 46e2ed5336f7a..0ff485b7db400 100644
--- a/src/plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/create_top_nav_menu.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
+import { AggregateQuery, Query } from '@kbn/es-query';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { TopNavMenuProps, TopNavMenu } from './top_nav_menu';
import { RegisteredTopNavMenuData } from './top_nav_menu_data';
@@ -16,7 +17,7 @@ export function createTopNav(
unifiedSearch: UnifiedSearchPublicPluginStart,
extraConfig: RegisteredTopNavMenuData[]
) {
- return (props: TopNavMenuProps) => {
+ return (props: TopNavMenuProps) => {
const relevantConfig = extraConfig.filter(
(dataItem) => dataItem.appName === undefined || dataItem.appName === props.appName
);
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
index aee35c1f331c7..127bd6e8482b1 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
@@ -18,13 +18,14 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
const unifiedSearch = {
ui: {
SearchBar: () => ,
+ AggregateQuerySearchBar: () => ,
},
} as unknown as UnifiedSearchPublicPluginStart;
describe('TopNavMenu', () => {
const WRAPPER_SELECTOR = '.kbnTopNavMenu__wrapper';
const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem';
- const SEARCH_BAR_SELECTOR = 'SearchBar';
+ const SEARCH_BAR_SELECTOR = 'AggregateQuerySearchBar';
const menuItems: TopNavMenuData[] = [
{
id: 'test',
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
index 86c83a6b48be5..2f07824d884a0 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
@@ -14,41 +14,43 @@ import { MountPoint } from '@kbn/core/public';
import { MountPointPortal } from '@kbn/kibana-react-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { StatefulSearchBarProps, SearchBarProps } from '@kbn/unified-search-plugin/public';
+import { AggregateQuery, Query } from '@kbn/es-query';
import { TopNavMenuData } from './top_nav_menu_data';
import { TopNavMenuItem } from './top_nav_menu_item';
-export type TopNavMenuProps = StatefulSearchBarProps &
- Omit & {
- config?: TopNavMenuData[];
- badges?: Array;
- showSearchBar?: boolean;
- showQueryBar?: boolean;
- showQueryInput?: boolean;
- showDatePicker?: boolean;
- showFilterBar?: boolean;
- unifiedSearch?: UnifiedSearchPublicPluginStart;
- className?: string;
- visible?: boolean;
- /**
- * If provided, the menu part of the component will be rendered as a portal inside the given mount point.
- *
- * This is meant to be used with the `setHeaderActionMenu` core API.
- *
- * @example
- * ```ts
- * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
- * const topNavConfig = ...; // TopNavMenuProps
- * return (
- *
- *
- *
- *
- * )
- * }
- * ```
- */
- setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
- };
+export type TopNavMenuProps =
+ StatefulSearchBarProps &
+ Omit, 'kibana' | 'intl' | 'timeHistory'> & {
+ config?: TopNavMenuData[];
+ badges?: Array;
+ showSearchBar?: boolean;
+ showQueryBar?: boolean;
+ showQueryInput?: boolean;
+ showDatePicker?: boolean;
+ showFilterBar?: boolean;
+ unifiedSearch?: UnifiedSearchPublicPluginStart;
+ className?: string;
+ visible?: boolean;
+ /**
+ * If provided, the menu part of the component will be rendered as a portal inside the given mount point.
+ *
+ * This is meant to be used with the `setHeaderActionMenu` core API.
+ *
+ * @example
+ * ```ts
+ * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => {
+ * const topNavConfig = ...; // TopNavMenuProps
+ * return (
+ *
+ *
+ *
+ *
+ * )
+ * }
+ * ```
+ */
+ setMenuMountPoint?: (menuMount: MountPoint | undefined) => void;
+ };
/*
* Top Nav Menu is a convenience wrapper component for:
@@ -59,7 +61,9 @@ export type TopNavMenuProps = StatefulSearchBarProps &
*
**/
-export function TopNavMenu(props: TopNavMenuProps): ReactElement | null {
+export function TopNavMenu(
+ props: TopNavMenuProps
+): ReactElement | null {
const { config, badges, showSearchBar, ...searchBarProps } = props;
if ((!config || config.length === 0) && (!showSearchBar || !props.unifiedSearch)) {
@@ -101,8 +105,8 @@ export function TopNavMenu(props: TopNavMenuProps): ReactElement | null {
function renderSearchBar(): ReactElement | null {
// Validate presense of all required fields
if (!showSearchBar || !props.unifiedSearch) return null;
- const { SearchBar } = props.unifiedSearch.ui;
- return ;
+ const { AggregateQuerySearchBar } = props.unifiedSearch.ui;
+ return {...searchBarProps} />;
}
function renderLayout() {
diff --git a/src/plugins/navigation/public/types.ts b/src/plugins/navigation/public/types.ts
index 40f83697cc9d6..a4ca72cf9cb3a 100644
--- a/src/plugins/navigation/public/types.ts
+++ b/src/plugins/navigation/public/types.ts
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { AggregateQuery, Query } from '@kbn/es-query';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup } from './top_nav_menu';
@@ -15,7 +16,8 @@ export interface NavigationPublicPluginSetup {
export interface NavigationPublicPluginStart {
ui: {
- TopNavMenu: React.ComponentType;
+ TopNavMenu: (props: TopNavMenuProps) => React.ReactElement;
+ AggregateQueryTopNavMenu: (props: TopNavMenuProps) => React.ReactElement;
};
}
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index fde583cf907e4..4927723395e13 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -8497,6 +8497,12 @@
"description": "Non-default value of setting."
}
},
+ "discover:enableSql": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
+ },
"discover:rowHeightOption": {
"type": "integer",
"_meta": {
diff --git a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx
index a885e4fe481de..551915a34b11f 100644
--- a/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx
+++ b/src/plugins/unified_search/public/__stories__/search_bar.stories.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
+import type { Query } from '@kbn/es-query';
import { storiesOf } from '@storybook/react';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
@@ -167,7 +168,7 @@ setIndexPatterns({
get: () => Promise.resolve(mockIndexPatterns[0]),
} as unknown as DataViewsContract);
-function wrapSearchBarInContext(testProps: SearchBarProps) {
+function wrapSearchBarInContext(testProps: SearchBarProps) {
const defaultOptions = {
appName: 'test',
timeHistory: mockTimeHistory,
@@ -185,12 +186,12 @@ function wrapSearchBarInContext(testProps: SearchBarProps) {
filters: [],
onClearSavedQuery: action('onClearSavedQuery'),
onFiltersUpdated: action('onFiltersUpdated'),
- } as unknown as SearchBarProps;
+ } as unknown as SearchBarProps;
return (
-
+ {...defaultOptions} {...testProps} />
);
@@ -459,4 +460,78 @@ storiesOf('SearchBar', module)
},
],
} as unknown as SearchBarProps)
+ )
+ .add('with dataviewPicker with SQL', () =>
+ wrapSearchBarInContext({
+ dataViewPickerComponentProps: {
+ currentDataViewId: '1234',
+ trigger: {
+ 'data-test-subj': 'dataView-switch-link',
+ label: 'logstash-*',
+ title: 'logstash-*',
+ },
+ onChangeDataView: action('onChangeDataView'),
+ onAddField: action('onAddField'),
+ onDataViewCreated: action('onDataViewCreated'),
+ textBasedLanguages: ['SQL'],
+ },
+ } as SearchBarProps)
+ )
+ .add('with dataviewPicker with SQL and sql query', () =>
+ wrapSearchBarInContext({
+ dataViewPickerComponentProps: {
+ currentDataViewId: '1234',
+ trigger: {
+ 'data-test-subj': 'dataView-switch-link',
+ label: 'SQL',
+ title: 'SQL',
+ },
+ onChangeDataView: action('onChangeDataView'),
+ onAddField: action('onAddField'),
+ onDataViewCreated: action('onDataViewCreated'),
+ textBasedLanguages: ['SQL'],
+ },
+ query: { sql: 'SELECT field1, field2 FROM DATAVIEW' },
+ } as unknown as SearchBarProps)
+ )
+ .add('with dataviewPicker with SQL and large sql query', () =>
+ wrapSearchBarInContext({
+ dataViewPickerComponentProps: {
+ currentDataViewId: '1234',
+ trigger: {
+ 'data-test-subj': 'dataView-switch-link',
+ label: 'SQL',
+ title: 'SQL',
+ },
+ onChangeDataView: action('onChangeDataView'),
+ onAddField: action('onAddField'),
+ onDataViewCreated: action('onDataViewCreated'),
+ textBasedLanguages: ['SQL'],
+ },
+ query: {
+ sql: 'SELECT field1, field2, field 3, field 4, field 5 FROM DATAVIEW WHERE field5 IS NOT NULL AND field4 IS NULL',
+ },
+ } as unknown as SearchBarProps)
+ )
+ .add('with dataviewPicker with SQL and errors in sql query', () =>
+ wrapSearchBarInContext({
+ dataViewPickerComponentProps: {
+ currentDataViewId: '1234',
+ trigger: {
+ 'data-test-subj': 'dataView-switch-link',
+ label: 'SQL',
+ title: 'SQL',
+ },
+ onChangeDataView: action('onChangeDataView'),
+ onAddField: action('onAddField'),
+ onDataViewCreated: action('onDataViewCreated'),
+ textBasedLanguages: ['SQL'],
+ },
+ textBasedLanguageModeErrors: [
+ new Error(
+ '[essql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem line 1:16: Unknown column [field10]'
+ ),
+ ],
+ query: { sql: 'SELECT field1, field10 FROM DATAVIEW' },
+ } as unknown as SearchBarProps)
);
diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx
index 2fcd2d8e6a5d5..3513900c68dd1 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.test.tsx
@@ -15,7 +15,7 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { ChangeDataView } from './change_dataview';
import { EuiTourStep } from '@elastic/eui';
-import type { DataViewPickerProps } from '.';
+import { DataViewPickerPropsExtended, TextBasedLanguages } from '.';
describe('DataView component', () => {
const createMockWebStorage = () => ({
@@ -41,7 +41,7 @@ describe('DataView component', () => {
};
function wrapDataViewComponentInContext(
- testProps: DataViewPickerProps,
+ testProps: DataViewPickerPropsExtended,
storageValue: boolean,
uiSettingValue: boolean = false
) {
@@ -69,7 +69,7 @@ describe('DataView component', () => {
);
}
- let props: DataViewPickerProps;
+ let props: DataViewPickerPropsExtended;
beforeEach(() => {
props = {
currentDataViewId: 'dataview-1',
@@ -80,6 +80,7 @@ describe('DataView component', () => {
'data-test-subj': 'dataview-trigger',
},
onChangeDataView: jest.fn(),
+ onTextLangQuerySubmit: jest.fn(),
};
});
it('should not render the tour component by default', async () => {
@@ -149,4 +150,21 @@ describe('DataView component', () => {
component.find('[data-test-subj="dataview-create-new"]').first().simulate('click');
expect(addDataViewSpy).toHaveBeenCalled();
});
+
+ it('should render the text based languages panels if languages are given', async () => {
+ const component = mount(
+ wrapDataViewComponentInContext(
+ {
+ ...props,
+ showNewMenuTour: true,
+ textBasedLanguages: [TextBasedLanguages.ESQL, TextBasedLanguages.SQL],
+ textBasedLanguage: TextBasedLanguages.SQL,
+ },
+ false
+ )
+ );
+ findTestSubject(component, 'dataview-trigger').simulate('click');
+ const text = component.find('[data-test-subj="select-text-based-language-panel"]');
+ expect(text.length).not.toBe(0);
+ });
});
diff --git a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
index c7e19429c66e3..d2762f0d27b2f 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -7,7 +7,7 @@
*/
import { i18n } from '@kbn/i18n';
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { css } from '@emotion/react';
import {
EuiPopover,
@@ -22,16 +22,25 @@ import {
EuiText,
EuiTourStep,
EuiContextMenuPanelProps,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiToolTip,
} from '@elastic/eui';
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
import { IDataPluginServices } from '@kbn/data-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
-import type { DataViewPickerProps } from '.';
+import type { DataViewPickerPropsExtended } from '.';
import { DataViewsList } from './dataview_list';
+import type { TextBasedLanguagesListProps } from './text_languages_list';
+import type { TextBasedLanguagesTransitionModalProps } from './text_languages_transition_modal';
import { changeDataViewStyles } from './change_dataview.styles';
const hideAnnouncementsUISetting = 'hideAnnouncements';
+// local storage key for the tour component
const NEW_DATA_VIEW_MENU_STORAGE_KEY = 'data.newDataViewMenu';
+// local storage key for the text based languages transition modal
+const TEXT_LANG_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal';
const newMenuTourTitle = i18n.translate('unifiedSearch.query.dataViewMenu.newMenuTour.title', {
defaultMessage: 'A better data view menu',
@@ -52,6 +61,26 @@ const newMenuTourDismissLabel = i18n.translate(
}
);
+const Fallback = () => ;
+
+const LazyTextBasedLanguagesTransitionModal = React.lazy(
+ () => import('./text_languages_transition_modal')
+);
+export const TextBasedLanguagesTransitionModal = (
+ props: TextBasedLanguagesTransitionModalProps
+) => (
+ }>
+
+
+);
+
+const LazyTextBasedLanguagesList = React.lazy(() => import('./text_languages_list'));
+export const TextBasedLanguagesList = (props: TextBasedLanguagesListProps) => (
+ }>
+
+
+);
+
export function ChangeDataView({
isMissingCurrent,
currentDataViewId,
@@ -61,14 +90,27 @@ export function ChangeDataView({
trigger,
selectableProps,
showNewMenuTour = false,
-}: DataViewPickerProps) {
+ textBasedLanguages,
+ onSaveTextLanguageQuery,
+ onTextLangQuerySubmit,
+ textBasedLanguage,
+}: DataViewPickerPropsExtended) {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setPopoverIsOpen] = useState(false);
const [dataViewsList, setDataViewsList] = useState([]);
const [triggerLabel, setTriggerLabel] = useState('');
+ const [isTextBasedLangSelected, setIsTextBasedLangSelected] = useState(
+ Boolean(textBasedLanguage)
+ );
+ const [isTextLangTransitionModalVisible, setIsTextLangTransitionModalVisible] = useState(false);
+ const [selectedDataViewId, setSelectedDataViewId] = useState(currentDataViewId);
+
const kibana = useKibana();
const { application, data, storage, uiSettings } = kibana.services;
const styles = changeDataViewStyles({ fullWidth: trigger.fullWidth });
+ const [isTextLangTransitionModalDismissed, setIsTextLangTransitionModalDismissed] = useState(() =>
+ Boolean(storage.get(TEXT_LANG_TRANSITION_MODAL_KEY))
+ );
const isHideAnnouncementSettingsOn = Boolean(uiSettings.get(hideAnnouncementsUISetting));
const [isTourDismissed, setIsTourDismissed] = useState(() =>
@@ -77,10 +119,21 @@ export function ChangeDataView({
const [isTourOpen, setIsTourOpen] = useState(false);
useEffect(() => {
- if (showNewMenuTour && !isTourDismissed && !isHideAnnouncementSettingsOn) {
+ if (
+ showNewMenuTour &&
+ !isTourDismissed &&
+ !isHideAnnouncementSettingsOn &&
+ !isTextBasedLangSelected
+ ) {
setIsTourOpen(true);
}
- }, [isHideAnnouncementSettingsOn, isTourDismissed, setIsTourOpen, showNewMenuTour]);
+ }, [
+ isHideAnnouncementSettingsOn,
+ isTextBasedLangSelected,
+ isTourDismissed,
+ setIsTourOpen,
+ showNewMenuTour,
+ ]);
const onTourDismiss = () => {
storage.set(NEW_DATA_VIEW_MENU_STORAGE_KEY, true);
@@ -101,9 +154,19 @@ export function ChangeDataView({
useEffect(() => {
if (trigger.label) {
- setTriggerLabel(trigger.label);
+ if (textBasedLanguage) {
+ setTriggerLabel(textBasedLanguage.toUpperCase());
+ } else {
+ setTriggerLabel(trigger.label);
+ }
}
- }, [trigger.label]);
+ }, [textBasedLanguage, trigger.label]);
+
+ useEffect(() => {
+ if (Boolean(textBasedLanguage) !== isTextBasedLangSelected) {
+ setIsTextBasedLangSelected(Boolean(textBasedLanguage));
+ }
+ }, [isTextBasedLangSelected, textBasedLanguage]);
const createTrigger = function () {
const { label, title, 'data-test-subj': dataTestSubj, fullWidth, ...rest } = trigger;
@@ -130,7 +193,7 @@ export function ChangeDataView({
const getPanelItems = () => {
const panelItems: EuiContextMenuPanelProps['items'] = [];
- if (onAddField) {
+ if (onAddField && !isTextBasedLangSelected) {
panelItems.push(
{
- const dataView = await data.dataViews.get(newId);
- await data.dataViews.refreshFields(dataView);
- onChangeDataView(newId);
- setPopoverIsOpen(false);
- }}
- currentDataViewId={currentDataViewId}
- selectableProps={selectableProps}
- searchListInputId={searchListInputId}
- />
+ <>
+ {onDataViewCreated && (
+
+
+
+
+ {Boolean(isTextBasedLangSelected) ? (
+
+
+
+ ) : null}
+
+
+
+
+ {i18n.translate('unifiedSearch.query.queryBar.indexPattern.dataViewsLabel', {
+ defaultMessage: 'Data views',
+ })}
+
+
+
+
+
+
+ {
+ setPopoverIsOpen(false);
+ onDataViewCreated();
+ }}
+ size="xs"
+ iconType="plusInCircleFilled"
+ iconSide="left"
+ data-test-subj="dataview-create-new"
+ >
+ {i18n.translate('unifiedSearch.query.queryBar.indexPattern.addNewDataView', {
+ defaultMessage: 'Create a data view',
+ })}
+
+
+
+ )}
+
+ {
+ const dataView = await data.dataViews.get(newId);
+ await data.dataViews.refreshFields(dataView);
+ setSelectedDataViewId(newId);
+ setPopoverIsOpen(false);
+ if (isTextBasedLangSelected && !isTextLangTransitionModalDismissed) {
+ setIsTextLangTransitionModalVisible(true);
+ } else if (isTextBasedLangSelected && isTextLangTransitionModalDismissed) {
+ setIsTextBasedLangSelected(false);
+ // clean up the Text based language query
+ onTextLangQuerySubmit?.({
+ language: 'kql',
+ query: '',
+ });
+ onChangeDataView(newId);
+ setTriggerLabel(trigger.label);
+ } else {
+ onChangeDataView(newId);
+ }
+ }}
+ currentDataViewId={currentDataViewId}
+ selectableProps={selectableProps}
+ searchListInputId={searchListInputId}
+ isTextBasedLangSelected={isTextBasedLangSelected}
+ />
+ >
);
- if (onDataViewCreated) {
+ if (textBasedLanguages?.length) {
panelItems.push(
,
- {
+ >
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesLabel',
+ {
+ defaultMessage: 'Text-based query languages',
+ }
+ )}
+
+
+
+ ,
+ {
+ setTriggerLabel(lang);
setPopoverIsOpen(false);
- onDataViewCreated();
+ setIsTextBasedLangSelected(true);
+ // also update the query with the sql query
+ onTextLangQuerySubmit?.({ sql: `SELECT * FROM "${trigger.title}"` });
}}
- >
- {i18n.translate('unifiedSearch.query.queryBar.indexPattern.addNewDataView', {
- defaultMessage: 'Create a data view',
- })}
-
+ />
);
}
return panelItems;
};
- return (
-
- {newMenuTourTitle}
- >
+ let modal;
+
+ const onTransitionModalDismiss = useCallback(() => {
+ storage.set(TEXT_LANG_TRANSITION_MODAL_KEY, true);
+ setIsTextLangTransitionModalDismissed(true);
+ }, [storage]);
+
+ const cleanup = useCallback(
+ (shouldDismissModal: boolean) => {
+ setIsTextLangTransitionModalVisible(false);
+ setIsTextBasedLangSelected(false);
+ // clean up the Text based language query
+ onTextLangQuerySubmit?.({
+ language: 'kql',
+ query: '',
+ });
+ if (selectedDataViewId) {
+ onChangeDataView(selectedDataViewId);
}
- content={
-
- {newMenuTourDescription}
-
+ setTriggerLabel(trigger.label);
+ if (shouldDismissModal) {
+ onTransitionModalDismiss();
}
- isStepOpen={isTourOpen}
- onFinish={onTourDismiss}
- step={1}
- stepsTotal={1}
- footerAction={
-
- {newMenuTourDismissLabel}
-
+ },
+ [
+ onChangeDataView,
+ onTextLangQuerySubmit,
+ onTransitionModalDismiss,
+ selectedDataViewId,
+ trigger.label,
+ ]
+ );
+
+ const onModalClose = useCallback(
+ (shouldDismissModal: boolean, needsSave?: boolean) => {
+ if (Boolean(needsSave)) {
+ setIsTextLangTransitionModalVisible(false);
+ onSaveTextLanguageQuery?.({
+ onSave: () => {
+ cleanup(shouldDismissModal);
+ },
+ onCancel: () => {
+ setIsTextLangTransitionModalVisible(false);
+ },
+ });
+ } else {
+ cleanup(shouldDismissModal);
}
- repositionOnScroll
- display="block"
- >
- setPopoverIsOpen(false)}
- panelPaddingSize="none"
- initialFocus={`#${searchListInputId}`}
+ },
+ [cleanup, onSaveTextLanguageQuery]
+ );
+
+ if (isTextLangTransitionModalVisible && !isTextLangTransitionModalDismissed) {
+ modal = (
+
+ );
+ }
+
+ return (
+ <>
+
+ {newMenuTourTitle}
+ >
+ }
+ content={
+
+ {newMenuTourDescription}
+
+ }
+ isStepOpen={isTourOpen}
+ onFinish={onTourDismiss}
+ step={1}
+ stepsTotal={1}
+ footerAction={
+
+ {newMenuTourDismissLabel}
+
+ }
+ repositionOnScroll
display="block"
- buffer={8}
>
-
-
-
-
-
+ setPopoverIsOpen(false)}
+ panelPaddingSize="none"
+ initialFocus={!isTextBasedLangSelected ? `#${searchListInputId}` : false}
+ display="block"
+ buffer={8}
+ >
+
+
+
+
+
+ {modal}
+ >
);
}
diff --git a/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx b/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx
index 6bbbe6d8df05b..fe6601b90d79c 100644
--- a/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.test.tsx
@@ -51,6 +51,7 @@ describe('DataView list component', () => {
currentDataViewId: 'dataview-1',
onChangeDataView: changeDataViewSpy,
dataViewsList: list,
+ isTextBasedLangSelected: false,
};
});
it('should trigger the onChangeDataView if a new dataview is selected', async () => {
@@ -69,4 +70,10 @@ describe('DataView list component', () => {
'dataview-2',
]);
});
+
+ it('should render a warning icon if a text based language is selected', () => {
+ const component = shallow();
+
+ expect(getDataViewPickerOptions(component)!.map((option: any) => option.append)).not.toBeNull();
+ });
});
diff --git a/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx b/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx
index da0919bd0ce8e..1a4a1ddd4355e 100644
--- a/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx
@@ -15,6 +15,7 @@ import { DataViewListItem } from '@kbn/data-views-plugin/public';
export interface DataViewsListProps {
dataViewsList: DataViewListItem[];
onChangeDataView: (newId: string) => void;
+ isTextBasedLangSelected?: boolean;
currentDataViewId?: string;
selectableProps?: EuiSelectableProps;
searchListInputId?: string;
@@ -23,6 +24,7 @@ export interface DataViewsListProps {
export function DataViewsList({
dataViewsList,
onChangeDataView,
+ isTextBasedLangSelected,
currentDataViewId,
selectableProps,
searchListInputId,
@@ -42,7 +44,7 @@ export function DataViewsList({
key: id,
label: name ? name : title,
value: id,
- checked: id === currentDataViewId ? 'on' : undefined,
+ checked: id === currentDataViewId && !Boolean(isTextBasedLangSelected) ? 'on' : undefined,
}))}
onChange={(choices) => {
const choice = choices.find(({ checked }) => checked) as unknown as {
@@ -61,16 +63,18 @@ export function DataViewsList({
}}
>
{(list, search) => (
-
- {search}
+ <>
+
+ {search}
+
{list}
-
+ >
)}
);
diff --git a/src/plugins/unified_search/public/dataview_picker/index.tsx b/src/plugins/unified_search/public/dataview_picker/index.tsx
index bd24aef0498ef..40ced3b6f4d47 100644
--- a/src/plugins/unified_search/public/dataview_picker/index.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/index.tsx
@@ -8,6 +8,7 @@
import React from 'react';
import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui';
+import type { AggregateQuery, Query } from '@kbn/es-query';
import { ChangeDataView } from './change_dataview';
export type ChangeDataViewTriggerProps = EuiButtonProps & {
@@ -15,16 +16,73 @@ export type ChangeDataViewTriggerProps = EuiButtonProps & {
title?: string;
};
+export enum TextBasedLanguages {
+ SQL = 'SQL',
+ ESQL = 'ESQL',
+}
+
+export interface OnSaveTextLanguageQueryProps {
+ onSave: () => void;
+ onCancel: () => void;
+}
+
/** @public */
export interface DataViewPickerProps {
+ /**
+ * The properties of the button that triggers the dataview picker.
+ */
trigger: ChangeDataViewTriggerProps;
+ /**
+ * Flag that should be enabled when the current dataview is missing.
+ */
isMissingCurrent?: boolean;
+ /**
+ * Callback that is called when the user changes the currently selected dataview.
+ */
onChangeDataView: (newId: string) => void;
+ /**
+ * The id of the selected dataview.
+ */
currentDataViewId?: string;
+ /**
+ * EuiSelectable properties.
+ */
selectableProps?: EuiSelectableProps;
+ /**
+ * Callback that is called when the user clicks the add runtime field option.
+ * Also works as a flag to show the add runtime field button.
+ */
onAddField?: () => void;
+ /**
+ * Callback that is called when the user clicks the create dataview option.
+ * Also works as a flag to show the create dataview button.
+ */
onDataViewCreated?: () => void;
+ /**
+ * Flag to show the tour component for the first time.
+ */
showNewMenuTour?: boolean;
+ /**
+ * List of the supported text based languages (SQL, ESQL) etc.
+ * Defined per application, if not provided, no text based languages
+ * will be available.
+ */
+ textBasedLanguages?: TextBasedLanguages[];
+ /**
+ * Callback that is called when the user clicks the Save and switch transition modal button
+ */
+ onSaveTextLanguageQuery?: ({ onSave, onCancel }: OnSaveTextLanguageQueryProps) => void;
+}
+
+export interface DataViewPickerPropsExtended extends DataViewPickerProps {
+ /**
+ * Callback that is called when the user clicks the submit button
+ */
+ onTextLangQuerySubmit?: (query?: Query | AggregateQuery) => void;
+ /**
+ * Text based language that is currently selected; depends on the query
+ */
+ textBasedLanguage?: string;
}
export const DataViewPicker = ({
@@ -36,7 +94,11 @@ export const DataViewPicker = ({
trigger,
selectableProps,
showNewMenuTour,
-}: DataViewPickerProps) => {
+ textBasedLanguages,
+ onSaveTextLanguageQuery,
+ onTextLangQuerySubmit,
+ textBasedLanguage,
+}: DataViewPickerPropsExtended) => {
return (
);
};
diff --git a/src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx b/src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx
new file mode 100644
index 0000000000000..21b77534f90a4
--- /dev/null
+++ b/src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx
@@ -0,0 +1,63 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { MouseEvent } from 'react';
+import { EuiSelectable } from '@elastic/eui';
+import { act } from 'react-dom/test-utils';
+import { ShallowWrapper } from 'enzyme';
+import { shallowWithIntl as shallow } from '@kbn/test-jest-helpers';
+import TextBasedLanguagesList, { TextBasedLanguagesListProps } from './text_languages_list';
+import { TextBasedLanguages } from '.';
+
+function getTextLanguagesPickerList(instance: ShallowWrapper) {
+ return instance.find(EuiSelectable).first();
+}
+
+function getTextLanguagesPickerOptions(instance: ShallowWrapper) {
+ return getTextLanguagesPickerList(instance).prop('options');
+}
+
+function selectTextLanguagePickerOption(instance: ShallowWrapper, selectedLabel: string) {
+ const event = {} as MouseEvent;
+ const options: Array<{ label: string; checked?: 'on' | 'off' }> = getTextLanguagesPickerOptions(
+ instance
+ ).map((option: { label: string }) =>
+ option.label === selectedLabel
+ ? { ...option, checked: 'on' }
+ : { ...option, checked: undefined }
+ );
+ return getTextLanguagesPickerList(instance).prop('onChange')!(options, event);
+}
+
+describe('Text based languages list component', () => {
+ const changeLanguageSpy = jest.fn();
+ let props: TextBasedLanguagesListProps;
+ beforeEach(() => {
+ props = {
+ selectedOption: 'ESQL',
+ onChange: changeLanguageSpy,
+ textBasedLanguages: [TextBasedLanguages.ESQL, TextBasedLanguages.SQL],
+ };
+ });
+ it('should trigger the onChange if a new language is selected', async () => {
+ const component = shallow();
+ await act(async () => {
+ selectTextLanguagePickerOption(component, 'SQL');
+ });
+ expect(changeLanguageSpy).toHaveBeenCalled();
+ });
+
+ it('should list all languages', () => {
+ const component = shallow();
+
+ expect(getTextLanguagesPickerOptions(component)!.map((option: any) => option.label)).toEqual([
+ 'ESQL',
+ 'SQL',
+ ]);
+ });
+});
diff --git a/src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx b/src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx
new file mode 100644
index 0000000000000..7b6ca242237c7
--- /dev/null
+++ b/src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiSelectable, EuiPanel, EuiBetaBadge } from '@elastic/eui';
+import { css } from '@emotion/react';
+import { TextBasedLanguages } from '.';
+
+export interface TextBasedLanguagesListProps {
+ textBasedLanguages: TextBasedLanguages[];
+ onChange: (lang: string) => void;
+ selectedOption: string;
+}
+
+// Needed for React.lazy
+// eslint-disable-next-line import/no-default-export
+export default function TextBasedLanguagesList({
+ textBasedLanguages,
+ onChange,
+ selectedOption,
+}: TextBasedLanguagesListProps) {
+ return (
+
+ data-test-subj="text-based-languages-switcher"
+ singleSelection="always"
+ options={textBasedLanguages.map((lang) => ({
+ key: lang,
+ label: lang,
+ value: lang,
+ checked: lang === selectedOption ? 'on' : undefined,
+ append: (
+
+ ),
+ }))}
+ onChange={(choices) => {
+ const choice = choices.find(({ checked }) => checked) as unknown as {
+ value: string;
+ };
+ onChange(choice.value);
+ }}
+ >
+ {(list) => (
+
+ {list}
+
+ )}
+
+ );
+}
diff --git a/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx b/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx
new file mode 100644
index 0000000000000..12f5414b92b9e
--- /dev/null
+++ b/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx
@@ -0,0 +1,119 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useState, useCallback } from 'react';
+import { i18n } from '@kbn/i18n';
+
+import {
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiButton,
+ EuiText,
+ EuiCheckbox,
+ EuiFlexItem,
+ EuiFlexGroup,
+} from '@elastic/eui';
+
+export interface TextBasedLanguagesTransitionModalProps {
+ closeModal: (dismissFlag: boolean, needsSave?: boolean) => void;
+ setIsTextLangTransitionModalVisible: (flag: boolean) => void;
+}
+// Needed for React.lazy
+// eslint-disable-next-line import/no-default-export
+export default function TextBasedLanguagesTransitionModal({
+ closeModal,
+ setIsTextLangTransitionModalVisible,
+}: TextBasedLanguagesTransitionModalProps) {
+ const [dismissModalChecked, setDismissModalChecked] = useState(false);
+ const onTransitionModalDismiss = useCallback((e) => {
+ setDismissModalChecked(e.target.checked);
+ }, []);
+
+ return (
+ setIsTextLangTransitionModalVisible(false)} style={{ width: 700 }}>
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle',
+ {
+ defaultMessage: 'Your query will be removed',
+ }
+ )}
+
+
+
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody',
+ {
+ defaultMessage:
+ "Switching data views removes the current SQL query. Save this search to ensure you don't lose work.",
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ closeModal(dismissModalChecked)}
+ color="warning"
+ iconType="merge"
+ >
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton',
+ {
+ defaultMessage: 'Switch without saving',
+ }
+ )}
+
+
+
+ closeModal(dismissModalChecked, true)}
+ fill
+ color="success"
+ iconType="save"
+ >
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalSaveButton',
+ {
+ defaultMessage: 'Save and switch',
+ }
+ )}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/plugins/unified_search/public/index.ts b/src/plugins/unified_search/public/index.ts
index e20b08f9e0e39..69b7ae95bf2fb 100755
--- a/src/plugins/unified_search/public/index.ts
+++ b/src/plugins/unified_search/public/index.ts
@@ -18,6 +18,8 @@ export { FilterLabel, FilterItem } from './filter_bar';
export { DataViewsList } from './dataview_picker/dataview_list';
export { DataViewPicker } from './dataview_picker';
+export type { DataViewPickerProps } from './dataview_picker';
+
export type { ApplyGlobalFilterActionContext } from './actions';
export { ACTION_GLOBAL_APPLY_FILTER, UPDATE_FILTER_REFERENCES_ACTION } from './actions';
export { UPDATE_FILTER_REFERENCES_TRIGGER } from './triggers';
diff --git a/src/plugins/unified_search/public/mocks/mocks.ts b/src/plugins/unified_search/public/mocks/mocks.ts
index 029a215ff7180..8507de9798480 100644
--- a/src/plugins/unified_search/public/mocks/mocks.ts
+++ b/src/plugins/unified_search/public/mocks/mocks.ts
@@ -35,6 +35,7 @@ const createStartContract = (): Start => {
ui: {
IndexPatternSelect: jest.fn(),
SearchBar: jest.fn().mockReturnValue(null),
+ AggregateQuerySearchBar: jest.fn().mockReturnValue(null),
},
};
};
diff --git a/src/plugins/unified_search/public/plugin.ts b/src/plugins/unified_search/public/plugin.ts
index 5161076a03b70..05e22b035614d 100755
--- a/src/plugins/unified_search/public/plugin.ts
+++ b/src/plugins/unified_search/public/plugin.ts
@@ -94,6 +94,7 @@ export class UnifiedSearchPublicPlugin
ui: {
IndexPatternSelect: createIndexPatternSelect(dataViews),
SearchBar,
+ AggregateQuerySearchBar: SearchBar,
},
autocomplete: autocompleteStart,
};
diff --git a/src/plugins/unified_search/public/query_string_input/index.tsx b/src/plugins/unified_search/public/query_string_input/index.tsx
index 74b3c2060b4e7..536df031edaa7 100644
--- a/src/plugins/unified_search/public/query_string_input/index.tsx
+++ b/src/plugins/unified_search/public/query_string_input/index.tsx
@@ -7,6 +7,7 @@
*/
import React from 'react';
+import { AggregateQuery, Query } from '@kbn/es-query';
import { withKibana } from '@kbn/kibana-react-plugin/public';
import type { QueryBarTopRowProps } from './query_bar_top_row';
import type { QueryStringInputProps } from './query_string_input';
@@ -14,9 +15,12 @@ import type { QueryStringInputProps } from './query_string_input';
const Fallback = () => ;
const LazyQueryBarTopRow = React.lazy(() => import('./query_bar_top_row'));
-export const QueryBarTopRow = (props: QueryBarTopRowProps) => (
+
+export const QueryBarTopRow = (
+ props: QueryBarTopRowProps
+) => (
}>
-
+ )} />
);
diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx
index 189f12765ad15..fd1ea2e9bce78 100644
--- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx
+++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.test.tsx
@@ -9,7 +9,7 @@
import { mockPersistedLogFactory } from './query_string_input.test.mocks';
import React from 'react';
-import { mount } from 'enzyme';
+import { mount, shallow } from 'enzyme';
import { render } from '@testing-library/react';
import { EMPTY } from 'rxjs';
@@ -65,6 +65,10 @@ const kqlQuery = {
language: 'kuery',
};
+const sqlQuery = {
+ sql: 'SELECT * FROM test',
+};
+
const createMockWebStorage = () => ({
clear: jest.fn(),
getItem: jest.fn(),
@@ -257,4 +261,21 @@ describe('QueryBarTopRowTopRow', () => {
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(0);
});
+
+ it('Should NOT render query input bar if on text based languages mode', () => {
+ const component = shallow(
+ wrapQueryBarTopRowInContext({
+ query: sqlQuery,
+ isDirty: false,
+ screenTitle: 'SQL Screen',
+ timeHistory: mockTimeHistory,
+ indexPatterns: [stubIndexPattern],
+ showDatePicker: false,
+ dateRangeFrom: 'now-7d',
+ dateRangeTo: 'now',
+ })
+ );
+
+ expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
+ });
});
diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx
index 52eb26bf737a3..22c6765368c42 100644
--- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx
+++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx
@@ -11,7 +11,8 @@ import classNames from 'classnames';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import useObservable from 'react-use/lib/useObservable';
-import type { Filter, TimeRange, Query } from '@kbn/es-query';
+import type { Filter, TimeRange, Query, AggregateQuery } from '@kbn/es-query';
+import { getAggregateQueryMode, isOfQueryType, isOfAggregateQueryType } from '@kbn/es-query';
import { EMPTY } from 'rxjs';
import { map } from 'rxjs/operators';
import {
@@ -35,9 +36,14 @@ import QueryStringInputUI from './query_string_input';
import { NoDataPopover } from './no_data_popover';
import { shallowEqual } from '../utils/shallow_equal';
import { AddFilterPopover } from './add_filter_popover';
-import { DataViewPicker, DataViewPickerProps } from '../dataview_picker';
+import {
+ DataViewPicker,
+ DataViewPickerProps,
+ OnSaveTextLanguageQueryProps,
+} from '../dataview_picker';
import { FilterButtonGroup } from '../filter_bar/filter_button_group/filter_button_group';
import type { SuggestionsListSize } from '../typeahead/suggestions_component';
+import { TextBasedLanguagesEditor } from './text_based_languages_editor';
import './query_bar.scss';
const SuperDatePicker = React.memo(
@@ -47,7 +53,7 @@ const SuperDatePicker = React.memo(
const QueryStringInput = withKibana(QueryStringInputUI);
// @internal
-export interface QueryBarTopRowProps {
+export interface QueryBarTopRowProps {
customSubmitButton?: any;
dataTestSubj?: string;
dateRangeFrom?: string;
@@ -62,13 +68,13 @@ export interface QueryBarTopRowProps {
isLoading?: boolean;
isRefreshPaused?: boolean;
nonKqlMode?: 'lucene' | 'text';
- onChange: (payload: { dateRange: TimeRange; query?: Query }) => void;
+ onChange: (payload: { dateRange: TimeRange; query?: Query | QT }) => void;
onRefresh?: (payload: { dateRange: TimeRange }) => void;
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
- onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void;
+ onSubmit: (payload: { dateRange: TimeRange; query?: Query | QT }) => void;
placeholder?: string;
prepend?: React.ComponentProps['prepend'];
- query?: Query;
+ query?: Query | QT;
refreshInterval?: number;
screenTitle?: string;
showQueryInput?: boolean;
@@ -80,11 +86,15 @@ export interface QueryBarTopRowProps {
filters: Filter[];
onFiltersUpdated?: (filters: Filter[]) => void;
dataViewPickerComponentProps?: DataViewPickerProps;
+ textBasedLanguageModeErrors?: Error[];
+ onTextBasedSavedAndExit?: ({ onSave }: OnSaveTextLanguageQueryProps) => void;
filterBar?: React.ReactNode;
showDatePickerAsBadge?: boolean;
showSubmitButton?: boolean;
suggestionsSize?: SuggestionsListSize;
isScreenshotMode?: boolean;
+ onTextLangQuerySubmit: (query?: Query | AggregateQuery) => void;
+ onTextLangQueryChange: (query: AggregateQuery) => void;
}
const SharingMetaFields = React.memo(function SharingMetaFields({
@@ -119,10 +129,17 @@ const SharingMetaFields = React.memo(function SharingMetaFields({
);
});
+type GenericQueryBarTopRow = (
+ props: QueryBarTopRowProps
+) => React.ReactElement;
+
export const QueryBarTopRow = React.memo(
- function QueryBarTopRow(props: QueryBarTopRowProps) {
+ function QueryBarTopRow(
+ props: QueryBarTopRowProps
+ ) {
const isMobile = useIsWithinBreakpoints(['xs', 's']);
const [isXXLarge, setIsXXLarge] = useState(false);
+ const [codeEditorIsExpanded, setCodeEditorIsExpanded] = useState(false);
useEffect(() => {
function handleResize() {
@@ -148,9 +165,10 @@ export const QueryBarTopRow = React.memo(
const kibana = useKibana();
const { uiSettings, storage, appName } = kibana.services;
+ const isQueryLangSelected = props.query && !isOfQueryType(props.query);
- const queryLanguage = props.query && props.query.language;
- const queryRef = useRef(props.query);
+ const queryLanguage = props.query && isOfQueryType(props.query) && props.query.language;
+ const queryRef = useRef(props.query);
queryRef.current = props.query;
const persistedLog: PersistedLog | undefined = React.useMemo(
@@ -207,7 +225,7 @@ export const QueryBarTopRow = React.memo(
});
const onSubmit = useCallback(
- ({ query, dateRange }: { query?: Query; dateRange: TimeRange }) => {
+ ({ query, dateRange }: { query?: Query | QT; dateRange: TimeRange }) => {
if (timeHistory) {
timeHistory.add(dateRange);
}
@@ -219,7 +237,7 @@ export const QueryBarTopRow = React.memo(
const onClickSubmitButton = useCallback(
(event: React.MouseEvent) => {
- if (persistedLog && queryRef.current) {
+ if (persistedLog && queryRef.current && isOfQueryType(queryRef.current)) {
persistedLog.add(queryRef.current.query);
}
event.preventDefault();
@@ -371,12 +389,19 @@ export const QueryBarTopRow = React.memo(
}
);
+ const buttonLabelRun = i18n.translate('unifiedSearch.queryBarTopRow.submitButton.run', {
+ defaultMessage: 'Run query',
+ });
+
+ const iconDirty = Boolean(isQueryLangSelected) ? 'play' : 'kqlFunction';
+ const tooltipDirty = Boolean(isQueryLangSelected) ? buttonLabelRun : buttonLabelUpdate;
+
const button = props.customSubmitButton ? (
React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton })
) : (
);
@@ -465,7 +497,7 @@ export const QueryBarTopRow = React.memo(
setCodeEditorIsExpanded(status)}
+ isCodeEditorExpanded={codeEditorIsExpanded}
+ errors={props.textBasedLanguageModeErrors}
+ onTextLangQuerySubmit={() =>
+ onSubmit({
+ query: queryRef.current,
+ dateRange: dateRangeRef.current,
+ })
+ }
+ />
+ )
+ );
+ }
+
const isScreenshotMode = props.isScreenshotMode === true;
return (
@@ -509,14 +563,19 @@ export const QueryBarTopRow = React.memo(
{renderDataViewsPicker()}
- {renderQueryInput()}
+ {!isQueryLangSelected
+ ? renderQueryInput()
+ : !codeEditorIsExpanded
+ ? renderTextLangEditor()
+ : null}
{shouldShowDatePickerAsBadge() && props.filterBar}
{renderUpdateButton()}
{!shouldShowDatePickerAsBadge() && props.filterBar}
+ {codeEditorIsExpanded && renderTextLangEditor()}
>
)}
>
@@ -532,7 +591,7 @@ export const QueryBarTopRow = React.memo(
return isQueryEqual && shallowEqual(prevProps, nextProps);
}
-);
+) as GenericQueryBarTopRow;
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/README.md b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/README.md
new file mode 100644
index 0000000000000..cd733033e0c6a
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/README.md
@@ -0,0 +1,46 @@
+# Unified search text based languages editor
+
+A **monaco** based editor that is part of the unified search experience. It is rendered for all the applications that support text-based languages.
+In order to enable text based languages on your unified search bar add `textBasedLanguages: ['SQL', 'ESQL', '...']` to the dataViewPicker properties.
+
+
+## Languages supported
+- SQL: based on the Elasticsearch sql api
+
+
+## Features
+- The editor operates in 3 modes:
+ - The inline mode: This is the one liner compact mode. If the query is large or consists of >1 lines then the user can't see the entire query.
+ - The inline focused mode. The editor is transferred to this mode automatically when the user clicks on the above mode. On this mode the user can work with multiple lines, see the entire context, see the errors, the editor line numbers and interact with the editor on a compact way. The editor returns automatically to the inline mode when the user clicks outside the editor.
+ - The expanded mode: The user has to click the maximize button to use this mode. Here the user has more space and can also minimize/maximize the editor height with a drag and drop experience.
+- The editor has a built in way to depict the errors but the user has to submit the query first. The error should be on the inline focuses mode or the expanded mode to view the errors details.
+- The editor is responsive regardless of the mode selected.
+- The editor has a built in documentation that dynamically changes based on the language of the query.
+- The user can quickly submit the query by pressing CMD/CTRL + Enter.
+
+## Preview
+Run `node scripts/storybook unified_search` for a preview of the unified search bar with the editor.
+
+## Component properties
+The editor is imported to the query_bar_top_row.tsx file. Accepts the following properties:
+- query: This is the **AggregateQuery** query. i.e. (`{sql: SELECT * FROM 'DATAVIEW1'}`)
+- onTextLangQueryChange: callback that is called every time the query is updated
+- expandCodeEditor: flag that opens the editor on the expanded mode
+- errors: array of `Error`.
+- onTextLangQuerySubmit: callback that is called when the user submits the query
+
+```
+ setCodeEditorIsExpanded(status)}
+ isCodeEditorExpanded={codeEditorIsExpanded}
+ errors={props.textBasedLanguageModeErrors}
+ onTextLangQuerySubmit={() =>
+ onSubmit({
+ query: queryRef.current,
+ dateRange: dateRangeRef.current,
+ })
+ }
+ />
+```
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.scss b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.scss
new file mode 100644
index 0000000000000..4cf5fbd08cdcd
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.scss
@@ -0,0 +1,151 @@
+.documentation {
+ display: flex;
+ flex-direction: column;
+
+ & > * {
+ flex: 1;
+ min-height: 0;
+ }
+
+ & > * + * {
+ border-top: $euiBorderThin;
+ }
+}
+
+.documentation__editor {
+
+ & > * + * {
+ border-top: $euiBorderThin;
+ }
+}
+
+.documentation__editorHeader,
+.documentation__editorFooter {
+ padding: $euiSizeS $euiSize;
+}
+
+.documentation__editorFooter {
+ // make sure docs are rendered in front of monaco
+ z-index: 1;
+ background-color: $euiColorLightestShade;
+}
+
+.documentation__editorHeaderGroup,
+.documentation__editorFooterGroup {
+ display: block; // Overrides EUI's styling of `display: flex` on `EuiFlexItem` components
+}
+
+.documentation__editorContent {
+ min-height: 0;
+ position: relative;
+}
+
+.documentation__editorPlaceholder {
+ position: absolute;
+ top: 0;
+ left: $euiSize;
+ right: 0;
+ color: $euiTextSubduedColor;
+ // Matches monaco editor
+ font-family: Menlo, Monaco, 'Courier New', monospace;
+ pointer-events: none;
+}
+
+.documentation__warningText + .documentation__warningText {
+ margin-top: $euiSizeS;
+ border-top: $euiBorderThin;
+ padding-top: $euiSizeS;
+}
+
+.documentation__editorHelp--inline {
+ align-items: center;
+ display: flex;
+ padding: $euiSizeXS;
+
+ & > * + * {
+ margin-left: $euiSizeXS;
+ }
+}
+
+.documentation__editorError {
+ white-space: nowrap;
+}
+
+.documentation__docs {
+ background: $euiColorEmptyShade;
+}
+
+.documentation__docs--inline {
+ display: flex;
+ flex-direction: column;
+ // make sure docs are rendered in front of monaco
+ z-index: 1;
+}
+
+.documentation__docsContent {
+ .documentation__docs--overlay & {
+ height: 40vh;
+ width: #{'min(75vh, 90vw)'};
+ }
+
+ .documentation__docs--inline & {
+ flex: 1;
+ min-height: 0;
+ }
+
+ & > * + * {
+ border-left: $euiBorderThin;
+ }
+}
+
+.documentation__docsSidebar {
+ background: $euiColorLightestShade;
+}
+
+.documentation__docsSidebarInner {
+ min-height: 0;
+
+ & > * + * {
+ border-top: $euiBorderThin;
+ }
+}
+
+.documentation__docsSearch {
+ padding: $euiSize;
+}
+
+.documentation__docsNav {
+ @include euiYScroll;
+}
+
+.documentation__docsNavGroup {
+ padding: $euiSize;
+
+ & + & {
+ border-top: $euiBorderThin;
+ }
+}
+
+.documentation__docsNavGroupLink {
+ font-weight: inherit;
+}
+
+.documentation__docsText {
+ @include euiYScroll;
+ padding: $euiSize;
+}
+
+.documentation__docsTextGroup,
+.documentation__docsTextItem {
+ margin-top: $euiSizeXXL;
+}
+
+.documentation__docsTextGroup {
+ border-top: $euiBorderThin;
+ padding-top: $euiSizeXXL;
+}
+
+.documentationOverflow {
+ // Needs to be higher than the modal and all flyouts
+ z-index: $euiZLevel9 + 1;
+}
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.tsx
new file mode 100644
index 0000000000000..6efd7ffe1577b
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.tsx
@@ -0,0 +1,203 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+import React, { useEffect, useRef, useState } from 'react';
+import { i18n } from '@kbn/i18n';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiPopoverTitle,
+ EuiText,
+ EuiListGroupItem,
+ EuiListGroup,
+ EuiTitle,
+ EuiFieldSearch,
+ EuiHighlight,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import './documentation.scss';
+
+export interface DocumentationSections {
+ groups: Array<{
+ label: string;
+ description?: string;
+ items: Array<{ label: string; description?: JSX.Element }>;
+ }>;
+ initialSection: JSX.Element;
+}
+
+interface DocumentationProps {
+ language: string;
+ sections?: DocumentationSections;
+}
+
+function Documentation({ language, sections }: DocumentationProps) {
+ const [selectedSection, setSelectedSection] = useState();
+ const scrollTargets = useRef>({});
+
+ useEffect(() => {
+ if (selectedSection && scrollTargets.current[selectedSection]) {
+ scrollTargets.current[selectedSection].scrollIntoView();
+ }
+ }, [selectedSection]);
+
+ const [searchText, setSearchText] = useState('');
+
+ const normalizedSearchText = searchText.trim().toLocaleLowerCase();
+
+ const filteredGroups = sections?.groups
+ .map((group) => {
+ const items = group.items.filter((helpItem) => {
+ return (
+ !normalizedSearchText || helpItem.label.toLocaleLowerCase().includes(normalizedSearchText)
+ );
+ });
+ return { ...group, items };
+ })
+ .filter((group) => {
+ if (group.items.length > 0 || !normalizedSearchText) {
+ return true;
+ }
+ return group.label.toLocaleLowerCase().includes(normalizedSearchText);
+ });
+
+ return (
+ <>
+
+ {i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.documentation.header', {
+ defaultMessage: '{language} reference',
+ values: { language: language.toUpperCase() },
+ })}
+
+
+
+
+
+ {
+ setSearchText(e.target.value);
+ }}
+ placeholder={i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.searchPlaceholder',
+ {
+ defaultMessage: 'Search',
+ }
+ )}
+ />
+
+
+ {filteredGroups?.map((helpGroup, index) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {
+ if (el && sections?.groups?.length) {
+ scrollTargets.current[sections.groups[0].label] = el;
+ }
+ }}
+ >
+ {sections?.initialSection}
+
+ {sections?.groups.slice(1).map((helpGroup, index) => {
+ return (
+ {
+ if (el) {
+ scrollTargets.current[helpGroup.label] = el;
+ }
+ }}
+ >
+ {helpGroup.label}
+
+ {helpGroup.description}
+
+ {sections?.groups[index + 1].items.map((helpItem) => {
+ return (
+ {
+ if (el) {
+ scrollTargets.current[helpItem.label] = el;
+ }
+ }}
+ >
+ {helpItem.description}
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+ >
+ );
+}
+
+export const MemoizedDocumentation = React.memo(Documentation);
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/editor_footer.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/editor_footer.tsx
new file mode 100644
index 0000000000000..c9baf17eeec8d
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/editor_footer.tsx
@@ -0,0 +1,171 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { memo, useState } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import {
+ EuiCode,
+ EuiText,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiIcon,
+ EuiPopover,
+ EuiPopoverTitle,
+ EuiDescriptionList,
+ EuiDescriptionListDescription,
+} from '@elastic/eui';
+import { Interpolation, Theme, css } from '@emotion/react';
+
+const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0;
+const COMMAND_KEY = isMac ? '⌘' : '^';
+
+interface EditorFooterProps {
+ lines: number;
+ containerCSS: Interpolation;
+ errors?: Array<{ startLineNumber: number; message: string }>;
+}
+
+export const EditorFooter = memo(function EditorFooter({
+ lines,
+ containerCSS,
+ errors,
+}: EditorFooterProps) {
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ return (
+
+
+
+
+
+
+ {i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.lineCount', {
+ defaultMessage: '{count} {count, plural, one {line} other {lines}}',
+ values: { count: lines },
+ })}
+
+
+
+ {errors && errors.length > 0 && (
+
+
+
+
+
+
+ setIsPopoverOpen(!isPopoverOpen)}
+ >
+
+ {i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.errorCount',
+ {
+ defaultMessage: '{count} {count, plural, one {error} other {errors}}',
+ values: { count: errors.length },
+ }
+ )}
+
+
+ }
+ ownFocus={false}
+ isOpen={isPopoverOpen}
+ closePopover={() => setIsPopoverOpen(false)}
+ >
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.errorsTitle',
+ {
+ defaultMessage: 'Errors',
+ }
+ )}
+
+
+ {errors.map((error, index) => {
+ return (
+
+
+
+
+
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.lineNumber',
+ {
+ defaultMessage: 'Line {lineNumber}',
+ values: { lineNumber: error.startLineNumber },
+ }
+ )}
+
+
+
+
+ {error.message}
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ {i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.runQuery', {
+ defaultMessage: 'Run query',
+ })}
+
+
+
+
+ {`${COMMAND_KEY} + Enter`}
+
+
+
+
+ );
+});
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.test.ts b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.test.ts
new file mode 100644
index 0000000000000..bf34735abd4b6
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.test.ts
@@ -0,0 +1,105 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { parseErrors } from './helpers';
+
+describe('helpers', function () {
+ describe('parseErrors', function () {
+ it('should return the correct error object from SQL ES response for an one liner query', function () {
+ const error = new Error(
+ '[essql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem\nline 1:8: Unknown column [miaou]'
+ );
+ const errors = [error];
+ expect(parseErrors(errors, 'SELECT miaou from test')).toEqual([
+ {
+ endColumn: 13,
+ endLineNumber: 1,
+ message: ' Unknown column [miaou]',
+ severity: 8,
+ startColumn: 8,
+ startLineNumber: 1,
+ },
+ ]);
+ });
+
+ it('should return the correct error object from SQL ES response for an multi liner query', function () {
+ const error = new Error(
+ '[essql] > Unexpected error from Elasticsearch: verification_exception - Found 1 problem line 3:7: Condition expression needs to be boolean, found [TEXT]'
+ );
+ const errors = [error];
+ expect(
+ parseErrors(
+ errors,
+ `SELECT *
+ FROM "kibana_sample_data_ecommerce"
+ WHERE category`
+ )
+ ).toEqual([
+ {
+ endColumn: 11,
+ endLineNumber: 3,
+ message: ' Condition expression needs to be boolean, found [TEXT]',
+ severity: 8,
+ startColumn: 7,
+ startLineNumber: 3,
+ },
+ ]);
+ });
+
+ it('should return the correct error object if dataview not found for an one liner query', function () {
+ const error = new Error('No data view found for index pattern kibana_sample_data_ecommerce1');
+ const errors = [error];
+ expect(parseErrors(errors, `SELECT * FROM "kibana_sample_data_ecommerce1"`)).toEqual([
+ {
+ endColumn: 46,
+ endLineNumber: 1,
+ message: 'No data view found for index pattern kibana_sample_data_ecommerce1',
+ severity: 8,
+ startColumn: 10,
+ startLineNumber: 1,
+ },
+ ]);
+ });
+
+ it('should return the correct error object if dataview not found for a multiline query', function () {
+ const error = new Error('No data view found for index pattern kibana_sample_data_ecommerce1');
+ const errors = [error];
+ expect(
+ parseErrors(
+ errors,
+ `SELECT *
+ from "kibana_sample_data_ecommerce1"`
+ )
+ ).toEqual([
+ {
+ endColumn: 41,
+ endLineNumber: 2,
+ message: 'No data view found for index pattern kibana_sample_data_ecommerce1',
+ severity: 8,
+ startColumn: 5,
+ startLineNumber: 2,
+ },
+ ]);
+ });
+
+ it('should return the generic error object for an error of unknown format', function () {
+ const error = new Error('I am an unknown error');
+ const errors = [error];
+ expect(parseErrors(errors, `SELECT * FROM "kibana_sample_data_ecommerce"`)).toEqual([
+ {
+ endColumn: 10,
+ endLineNumber: 1,
+ message: 'I am an unknown error',
+ severity: 8,
+ startColumn: 1,
+ startLineNumber: 1,
+ },
+ ]);
+ });
+ });
+});
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.ts b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.ts
new file mode 100644
index 0000000000000..112d1a364e3ee
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.ts
@@ -0,0 +1,134 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { useRef } from 'react';
+import useDebounce from 'react-use/lib/useDebounce';
+import { monaco } from '@kbn/monaco';
+import { getIndexPatternFromSQLQuery } from '@kbn/es-query';
+import { i18n } from '@kbn/i18n';
+
+export const useDebounceWithOptions = (
+ fn: Function,
+ { skipFirstRender }: { skipFirstRender: boolean } = { skipFirstRender: false },
+ ms?: number | undefined,
+ deps?: React.DependencyList | undefined
+) => {
+ const isFirstRender = useRef(true);
+ const newDeps = [...(deps || []), isFirstRender];
+
+ return useDebounce(
+ () => {
+ if (skipFirstRender && isFirstRender.current) {
+ isFirstRender.current = false;
+ return;
+ }
+ return fn();
+ },
+ ms,
+ newDeps
+ );
+};
+
+export const parseErrors = (errors: Error[], code: string) => {
+ return errors.map((error) => {
+ if (error.message.includes('line')) {
+ const text = error.message.split('line')[1];
+ const [lineNumber, startPosition, errorMessage] = text.split(':');
+ // initialize the length to 10 in case no error word found
+ let errorLength = 10;
+ const [_, wordWithError] = errorMessage.split('[');
+ if (wordWithError) {
+ errorLength = wordWithError.length - 1;
+ }
+ return {
+ message: errorMessage,
+ startColumn: Number(startPosition),
+ startLineNumber: Number(lineNumber),
+ endColumn: Number(startPosition) + errorLength,
+ endLineNumber: Number(lineNumber),
+ severity: monaco.MarkerSeverity.Error,
+ };
+ } else if (error.message.includes('No data view found')) {
+ const dataviewString = getIndexPatternFromSQLQuery(code);
+ const temp = code.split(dataviewString);
+ const lastChar = temp[0]?.charAt(temp[0]?.length - 1);
+ const additionnalLength = lastChar === '"' || "'" ? 2 : 0;
+ // 5 is the length of FROM + space
+ const errorLength = 5 + dataviewString.length + additionnalLength;
+ // no dataview found error message
+ const hasLines = /\r|\n/.exec(code);
+ if (hasLines) {
+ const linesText = code.split(/\r|\n/);
+ let indexWithError = 1;
+ let lineWithError = '';
+ linesText.forEach((line, index) => {
+ if (line.includes('FROM') || line.includes('from')) {
+ indexWithError = index + 1;
+ lineWithError = line;
+ }
+ });
+ const lineWithErrorUpperCase = lineWithError.toUpperCase();
+ return {
+ message: error.message,
+ startColumn: lineWithErrorUpperCase.indexOf('FROM') + 1,
+ startLineNumber: indexWithError,
+ endColumn: lineWithErrorUpperCase.indexOf('FROM') + 1 + errorLength,
+ endLineNumber: indexWithError,
+ severity: monaco.MarkerSeverity.Error,
+ };
+ } else {
+ return {
+ message: error.message,
+ startColumn: code.toUpperCase().indexOf('FROM') + 1,
+ startLineNumber: 1,
+ endColumn: code.toUpperCase().indexOf('FROM') + 1 + errorLength,
+ endLineNumber: 1,
+ severity: monaco.MarkerSeverity.Error,
+ };
+ }
+ } else {
+ // unknown error message
+ return {
+ message: error.message,
+ startColumn: 1,
+ startLineNumber: 1,
+ endColumn: 10,
+ endLineNumber: 1,
+ severity: monaco.MarkerSeverity.Error,
+ };
+ }
+ });
+};
+
+export const getDocumentationSections = async (language: string) => {
+ const groups: Array<{
+ label: string;
+ description?: string;
+ items: Array<{ label: string; description?: JSX.Element }>;
+ }> = [];
+ if (language === 'sql') {
+ const {
+ comparisonOperators,
+ logicalOperators,
+ mathOperators,
+ initialSection,
+ aggregateFunctions,
+ } = await import('./sql_documentation_sections');
+ groups.push({
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.howItWorks', {
+ defaultMessage: 'How it works',
+ }),
+ items: [],
+ });
+ groups.push(comparisonOperators, logicalOperators, mathOperators, aggregateFunctions);
+ return {
+ groups,
+ initialSection,
+ };
+ }
+};
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx
new file mode 100644
index 0000000000000..222d890572bc1
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx
@@ -0,0 +1,611 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React, { useRef, memo, useEffect, useState, useCallback } from 'react';
+import classNames from 'classnames';
+import { EsqlLang, monaco } from '@kbn/monaco';
+import type { AggregateQuery } from '@kbn/es-query';
+import { getAggregateQueryMode } from '@kbn/es-query';
+
+import { i18n } from '@kbn/i18n';
+import {
+ EuiBadge,
+ useEuiTheme,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonIcon,
+ EuiPopover,
+ EuiResizeObserver,
+ EuiOutsideClickDetector,
+ EuiToolTip,
+} from '@elastic/eui';
+import { CodeEditor } from '@kbn/kibana-react-plugin/public';
+import type { CodeEditorProps } from '@kbn/kibana-react-plugin/public';
+
+import {
+ textBasedLanguagedEditorStyles,
+ EDITOR_INITIAL_HEIGHT,
+ EDITOR_INITIAL_HEIGHT_EXPANDED,
+ EDITOR_MAX_HEIGHT,
+ EDITOR_MIN_HEIGHT,
+} from './text_based_languages_editor.styles';
+import { MemoizedDocumentation, DocumentationSections } from './documentation';
+import { useDebounceWithOptions, parseErrors, getDocumentationSections } from './helpers';
+import { EditorFooter } from './editor_footer';
+
+import './overwrite.scss';
+
+export interface TextBasedLanguagesEditorProps {
+ query: AggregateQuery;
+ onTextLangQueryChange: (query: AggregateQuery) => void;
+ onTextLangQuerySubmit: () => void;
+ expandCodeEditor: (status: boolean) => void;
+ isCodeEditorExpanded: boolean;
+ errors?: Error[];
+}
+
+const MAX_COMPACT_VIEW_LENGTH = 250;
+const FONT_WIDTH = 8;
+const EDITOR_ONE_LINER_UNUSED_SPACE = 180;
+const EDITOR_ONE_LINER_UNUSED_SPACE_WITH_ERRORS = 220;
+
+const languageId = (language: string) => {
+ switch (language) {
+ case 'sql':
+ default: {
+ return EsqlLang.ID;
+ }
+ }
+};
+
+let clickedOutside = false;
+let initialRender = true;
+let updateLinesFromModel = false;
+
+export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
+ query,
+ onTextLangQueryChange,
+ onTextLangQuerySubmit,
+ expandCodeEditor,
+ isCodeEditorExpanded,
+ errors,
+}: TextBasedLanguagesEditorProps) {
+ const { euiTheme } = useEuiTheme();
+ const language = getAggregateQueryMode(query);
+ const queryString: string = query[language] ?? '';
+ const [lines, setLines] = useState(1);
+ const [code, setCode] = useState(queryString ?? '');
+ const [codeOneLiner, setCodeOneLiner] = useState('');
+ const [editorHeight, setEditorHeight] = useState(
+ isCodeEditorExpanded ? EDITOR_INITIAL_HEIGHT_EXPANDED : EDITOR_INITIAL_HEIGHT
+ );
+ const [showLineNumbers, setShowLineNumbers] = useState(isCodeEditorExpanded);
+ const [isCompactFocused, setIsCompactFocused] = useState(isCodeEditorExpanded);
+ const [isCodeEditorExpandedFocused, setIsCodeEditorExpandedFocused] = useState(false);
+ const [isWordWrapped, setIsWordWrapped] = useState(true);
+ const [userDrags, setUserDrags] = useState(false);
+ const [isHelpOpen, setIsHelpOpen] = useState(false);
+ const [editorErrors, setEditorErrors] = useState<
+ Array<{ startLineNumber: number; message: string }>
+ >([]);
+ const [documentationSections, setDocumentationSections] = useState();
+
+ const styles = textBasedLanguagedEditorStyles(
+ euiTheme,
+ isCompactFocused,
+ editorHeight,
+ isCodeEditorExpanded,
+ Boolean(errors?.length),
+ isCodeEditorExpandedFocused
+ );
+ const editorModel = useRef();
+ const editor1 = useRef();
+ const containerRef = useRef(null);
+
+ const editorClassName = classNames('unifiedTextLangEditor', {
+ 'unifiedTextLangEditor--expanded': isCodeEditorExpanded,
+ 'unifiedTextLangEditor--compact': isCompactFocused,
+ 'unifiedTextLangEditor--initial': !isCompactFocused,
+ });
+
+ // When the editor is on full size mode, the user can resize the height of the editor.
+ const onMouseDownResizeHandler = useCallback(
+ (mouseDownEvent: React.MouseEvent) => {
+ const startSize = editorHeight;
+ const startPosition = mouseDownEvent.pageY;
+
+ function onMouseMove(mouseMoveEvent: MouseEvent) {
+ const height = startSize - startPosition + mouseMoveEvent.pageY;
+ const validatedHeight = Math.min(Math.max(height, EDITOR_MIN_HEIGHT), EDITOR_MAX_HEIGHT);
+ setEditorHeight(validatedHeight);
+ setUserDrags(true);
+ }
+ function onMouseUp() {
+ document.body.removeEventListener('mousemove', onMouseMove);
+ setUserDrags(false);
+ }
+
+ document.body.addEventListener('mousemove', onMouseMove);
+ document.body.addEventListener('mouseup', onMouseUp, { once: true });
+ },
+ [editorHeight]
+ );
+
+ const updateHeight = () => {
+ if (editor1.current) {
+ const linesCount = editorModel.current?.getLineCount() || 1;
+ if (linesCount === 1 || clickedOutside || initialRender) return;
+ const editorElement = editor1.current.getDomNode();
+ const contentHeight = Math.min(MAX_COMPACT_VIEW_LENGTH, editor1.current.getContentHeight());
+
+ if (editorElement) {
+ editorElement.style.height = `${contentHeight}px`;
+ }
+ const contentWidth = Number(editorElement?.style.width.replace('px', ''));
+ editor1.current.layout({ width: contentWidth, height: contentHeight });
+ setEditorHeight(contentHeight);
+ }
+ };
+
+ const restoreInitialMode = () => {
+ setIsCodeEditorExpandedFocused(false);
+ if (isCodeEditorExpanded) return;
+ setEditorHeight(EDITOR_INITIAL_HEIGHT);
+ setIsCompactFocused(false);
+ setShowLineNumbers(false);
+ updateLinesFromModel = false;
+ clickedOutside = true;
+ if (editor1.current) {
+ const editorElement = editor1.current.getDomNode();
+ if (editorElement) {
+ editorElement.style.height = `${EDITOR_INITIAL_HEIGHT}px`;
+ const contentWidth = Number(editorElement?.style.width.replace('px', ''));
+ calculateVisibleCode(contentWidth, true);
+ editor1.current.layout({ width: contentWidth, height: EDITOR_INITIAL_HEIGHT });
+ }
+ }
+ };
+
+ useDebounceWithOptions(
+ () => {
+ if (!editorModel.current) return;
+ editor1.current?.onDidChangeModelContent((e) => {
+ if (updateLinesFromModel) {
+ setLines(editorModel.current?.getLineCount() || 1);
+ }
+ });
+ editor1.current?.onDidFocusEditorText(() => {
+ setIsCompactFocused(true);
+ setIsCodeEditorExpandedFocused(true);
+ setShowLineNumbers(true);
+ setCodeOneLiner('');
+ clickedOutside = false;
+ initialRender = false;
+ updateLinesFromModel = true;
+ });
+ // on CMD/CTRL + Enter submit the query
+ // eslint-disable-next-line no-bitwise
+ editor1.current?.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function () {
+ onTextLangQuerySubmit();
+ });
+ if (!isCodeEditorExpanded) {
+ editor1.current?.onDidContentSizeChange(updateHeight);
+ }
+ if (errors && errors.length) {
+ const parsedErrors = parseErrors(errors, code);
+ setEditorErrors(parsedErrors);
+ monaco.editor.setModelMarkers(editorModel.current, 'Unified search', parsedErrors);
+ } else {
+ monaco.editor.setModelMarkers(editorModel.current, 'Unified search', []);
+ setEditorErrors([]);
+ }
+ },
+ { skipFirstRender: false },
+ 256,
+ [errors]
+ );
+
+ // Clean up the monaco editor and DOM on unmount
+ useEffect(() => {
+ const model = editorModel;
+ const editor1ref = editor1;
+ return () => {
+ model.current?.dispose();
+ editor1ref.current?.dispose();
+ };
+ }, []);
+
+ const calculateVisibleCode = useCallback(
+ (width: number, force?: boolean) => {
+ const containerWidth = containerRef.current?.offsetWidth;
+ if (containerWidth && (!isCompactFocused || force)) {
+ const hasLines = /\r|\n/.exec(queryString);
+ if (hasLines && !updateLinesFromModel) {
+ setLines(queryString.split(/\r|\n/).length);
+ }
+ const trimmedText = queryString.replace(/\r?\n|\r/g, '');
+ const text = hasLines ? trimmedText : queryString;
+ const queryLength = text.length;
+ const unusedSpace =
+ errors && errors.length
+ ? EDITOR_ONE_LINER_UNUSED_SPACE_WITH_ERRORS
+ : EDITOR_ONE_LINER_UNUSED_SPACE;
+ const charactersAlowed = Math.floor((width - unusedSpace) / FONT_WIDTH);
+ if (queryLength > charactersAlowed) {
+ const shortedCode = text.substring(0, charactersAlowed) + '...';
+ setCodeOneLiner(shortedCode);
+ } else {
+ const shortedCode = text;
+ setCodeOneLiner(shortedCode);
+ }
+ }
+ },
+ [queryString, errors, isCompactFocused]
+ );
+
+ useEffect(() => {
+ if (editor1.current && !isCompactFocused) {
+ const editorElement = editor1.current.getDomNode();
+ if (editorElement) {
+ const contentWidth = Number(editorElement?.style.width.replace('px', ''));
+ if (code !== queryString) {
+ setCode(queryString);
+ calculateVisibleCode(contentWidth);
+ }
+ }
+ }
+ }, [calculateVisibleCode, code, isCompactFocused, queryString]);
+
+ const onResize = ({ width }: { width: number }) => {
+ calculateVisibleCode(width);
+ if (editor1.current) {
+ editor1.current.layout({ width, height: editorHeight });
+ }
+ };
+
+ const onQueryUpdate = useCallback(
+ (value: string) => {
+ setCode(value);
+ onTextLangQueryChange({ [language]: value } as AggregateQuery);
+ },
+ [language, onTextLangQueryChange]
+ );
+
+ const toggleDocumentationPopover = useCallback(() => {
+ setIsHelpOpen(!isHelpOpen);
+ }, [isHelpOpen]);
+
+ useEffect(() => {
+ async function getDocumentation() {
+ const sections = await getDocumentationSections(language);
+ setDocumentationSections(sections);
+ }
+
+ getDocumentation();
+ }, [language]);
+
+ const codeEditorOptions: CodeEditorProps['options'] = {
+ automaticLayout: false,
+ accessibilitySupport: 'off',
+ folding: false,
+ fontSize: 14,
+ padding: {
+ top: 8,
+ bottom: 8,
+ },
+ scrollBeyondLastLine: false,
+ quickSuggestions: true,
+ minimap: { enabled: false },
+ wordWrap: isWordWrapped ? 'on' : 'off',
+ lineNumbers: showLineNumbers ? 'on' : 'off',
+ theme: 'vs',
+ lineDecorationsWidth: 12,
+ autoIndent: 'none',
+ wrappingIndent: 'none',
+ lineNumbersMinChars: 3,
+ overviewRulerLanes: 0,
+ hideCursorInOverviewRuler: true,
+ scrollbar: {
+ vertical: 'hidden',
+ horizontal: 'hidden',
+ },
+ overviewRulerBorder: false,
+ };
+
+ if (isCompactFocused) {
+ codeEditorOptions.overviewRulerLanes = 4;
+ codeEditorOptions.hideCursorInOverviewRuler = false;
+ codeEditorOptions.overviewRulerBorder = true;
+ }
+
+ const editorPanel = (
+ <>
+ {isCodeEditorExpanded && (
+
+
+
+ {
+ editor1.current?.updateOptions({
+ wordWrap: isWordWrapped ? 'off' : 'on',
+ });
+ setIsWordWrapped(!isWordWrapped);
+ }}
+ />
+
+
+
+
+
+
+ {
+ expandCodeEditor(false);
+ updateLinesFromModel = false;
+ }}
+ />
+
+
+
+ setIsHelpOpen(false)}
+ ownFocus={false}
+ button={
+
+
+
+ }
+ >
+
+
+
+
+
+
+ )}
+
+
+ {(resizeRef) => (
+ {
+ restoreInitialMode();
+ }}
+ >
+
+
+
+ {!isCompactFocused && (
+
+ {i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.lineCount', {
+ defaultMessage: '{count} {count, plural, one {line} other {lines}}',
+ values: { count: lines },
+ })}
+
+ )}
+ {!isCompactFocused && errors && errors.length > 0 && (
+
+ {errors.length}
+
+ )}
+ {
+ editor1.current = editor;
+ const model = editor.getModel();
+ if (model) {
+ editorModel.current = model;
+ }
+ if (isCodeEditorExpanded) {
+ setLines(model?.getLineCount() || 1);
+ }
+ }}
+ />
+ {isCompactFocused && !isCodeEditorExpanded && (
+
+ )}
+
+
+
+
+ )}
+
+ {!isCodeEditorExpanded && (
+
+
+
+
+ expandCodeEditor(true)}
+ data-test-subj="unifiedTextLangEditor-expand"
+ css={{
+ borderRadius: 0,
+ backgroundColor: '#e9edf3',
+ border: '1px solid rgb(17 43 134 / 10%) !important',
+ }}
+ />
+
+
+
+ setIsHelpOpen(false)}
+ ownFocus={false}
+ button={
+
+
+
+ }
+ >
+
+
+
+
+
+ )}
+
+ {isCodeEditorExpanded && (
+
+ )}
+ {isCodeEditorExpanded && (
+
+ {!userDrags && (
+
+ )}
+
+ )}
+ >
+ );
+
+ return editorPanel;
+});
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/overwrite.scss b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/overwrite.scss
new file mode 100644
index 0000000000000..cad8d05191d9b
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/overwrite.scss
@@ -0,0 +1,45 @@
+.unifiedTextLangEditor .monaco-editor {
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+}
+
+.unifiedTextLangEditor .monaco-editor .monaco-hover {
+ display: none !important;
+}
+
+.unifiedTextLangEditor--expanded .monaco-editor .monaco-hover {
+ display: block !important;
+}
+
+.unifiedTextLangEditor .monaco-editor .margin {
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+}
+
+.unifiedTextLangEditor--compact .monaco-editor {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.unifiedTextLangEditor--compact .monaco-editor .margin {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ background-color: $euiColorLightestShade;
+ color: $euiColorDisabledText;
+}
+
+.unifiedTextLangEditor .monaco-editor .margin-view-overlays .line-numbers {
+ color: $euiColorDisabledText;
+}
+
+.unifiedTextLangEditor .monaco-editor .current-line ~ .line-numbers {
+ color: $euiTextSubduedColor;
+}
+
+.unifiedTextLangEditor--compact .monaco-editor .monaco-scrollable-element {
+ margin-left: 4px;
+}
+
+.unifiedTextLangEditor_errorMessage {
+ @include euiTextBreakWord;
+}
\ No newline at end of file
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/sql_documentation_sections.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/sql_documentation_sections.tsx
new file mode 100644
index 0000000000000..d58728da61d2c
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/sql_documentation_sections.tsx
@@ -0,0 +1,1127 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { Markdown } from '@kbn/kibana-react-plugin/public';
+
+export const initialSection = (
+
+);
+
+export const comparisonOperators = {
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.comparisonOperators', {
+ defaultMessage: 'Comparison operators',
+ }),
+ description: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.comparisonOperatorsDocumentationDescription',
+ {
+ defaultMessage: `Boolean operator for comparing against one or multiple expressions.`,
+ }
+ ),
+ items: [
+ {
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.documentation.equality', {
+ defaultMessage: 'Equality',
+ }),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.nullSafeEquality',
+ {
+ defaultMessage: 'Null safe equality (<=>)',
+ }
+ ),
+ description: (
+ null AS "equals"
+
+ equals
+---------------
+false
+\`\`\`
+\`\`\`
+SELECT null <=> null AS "equals"
+
+ equals
+---------------
+true
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.inequality',
+ {
+ defaultMessage: 'Inequality',
+ }
+ ),
+ description: (
+ or !=)
+\`\`\`
+SELECT last_name l FROM "test_emp"
+WHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.comparison',
+ {
+ defaultMessage: 'Comparison',
+ }
+ ),
+ description: (
+ , >=)
+\`\`\`
+SELECT last_name l FROM "test_emp"
+WHERE emp_no < 10003 ORDER BY emp_no LIMIT 5
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ {
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.documentation.between', {
+ defaultMessage: 'Between',
+ }),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.nullNotNull',
+ {
+ defaultMessage: 'IS NULL and IS NOT NULL',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.inOperator',
+ {
+ defaultMessage: 'IN',
+ }
+ ),
+ description: (
+ , , ...)
+\`\`\`
+SELECT last_name l FROM "test_emp"
+WHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ ],
+};
+
+export const logicalOperators = {
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.logicalOperators', {
+ defaultMessage: 'Logical operators',
+ }),
+ description: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.logicalOperatorsDocumentationDescription',
+ {
+ defaultMessage: `Boolean operator for evaluating one or two expressions.`,
+ }
+ ),
+ items: [
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.AndOperator',
+ {
+ defaultMessage: 'AND',
+ }
+ ),
+ description: (
+ 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.OrOperator',
+ {
+ defaultMessage: 'OR',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.NotOperator',
+ {
+ defaultMessage: 'NOT',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ ],
+};
+
+export const mathOperators = {
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.mathOperators', {
+ defaultMessage: 'Math operators',
+ }),
+ description: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.mathOperatorsDocumentationDescription',
+ {
+ defaultMessage: `Perform mathematical operations affecting one or two values. The result is a value of numeric type..`,
+ }
+ ),
+ items: [
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.AddOperator',
+ {
+ defaultMessage: 'Add',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.SubtractOperator',
+ {
+ defaultMessage: 'Subtract',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.negateOperator',
+ {
+ defaultMessage: 'Negate',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.multiplyOperator',
+ {
+ defaultMessage: 'Multiply',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.divideOperator',
+ {
+ defaultMessage: 'Divide',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.moduloOperator',
+ {
+ defaultMessage: 'Modulo or remainder',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ ],
+};
+
+export const aggregateFunctions = {
+ label: i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.aggregateFunctions', {
+ defaultMessage: 'Aggregate functions',
+ }),
+ description: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.aggregateFunctionsDocumentationDescription',
+ {
+ defaultMessage: `Functions for computing a single result from a set of input values. Elasticsearch SQL supports aggregate functions only alongside grouping (implicit or explicit).`,
+ }
+ ),
+ items: [
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.averageFunction',
+ {
+ defaultMessage: 'Average',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.countFunction',
+ {
+ defaultMessage: 'Count',
+ }
+ ),
+ description: (
+ ), all values are considered, including null or missing ones. For COUNT(), null values are not considered.
+\`\`\`
+SELECT COUNT(*) AS count FROM emp
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.countAllFunction',
+ {
+ defaultMessage: 'Count (All)',
+ }
+ ),
+ description: (
+ ) and COUNT(ALL ) are equivalent.
+
+\`\`\`
+COUNT(ALL field_name)
+\`\`\`
+- a field name. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
+\`\`\`
+SELECT COUNT(ALL last_name) AS count_all, COUNT(DISTINCT last_name) count_distinct FROM emp
+\`\`\`
+ `,
+ description:
+ 'Text is in markdown. Do not translate function names, special characters, or field names like sum(bytes)',
+ }
+ )}
+ />
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.countDistinctFunction',
+ {
+ defaultMessage: 'Count (Distinct)',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.firstFunction',
+ {
+ defaultMessage: 'First / First_value',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.lastFunction',
+ {
+ defaultMessage: 'Last / Last_value',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.maxFunction',
+ {
+ defaultMessage: 'Max',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.minFunction',
+ {
+ defaultMessage: 'Min',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.sumFunction',
+ {
+ defaultMessage: 'Sum',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.kurtosisFunction',
+ {
+ defaultMessage: 'Kurtosis',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.madFunction',
+ {
+ defaultMessage: 'Mad',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.percentileFunction',
+ {
+ defaultMessage: 'Percentile',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.percentileRankFunction',
+ {
+ defaultMessage: 'Percentile rank',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.skewnessFunction',
+ {
+ defaultMessage: 'Skewness',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.stsdevpopFunction',
+ {
+ defaultMessage: 'STDDEV_POP',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.stsdevsampFunction',
+ {
+ defaultMessage: 'STDDEV_SAMP',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.sumofsquaresFunction',
+ {
+ defaultMessage: 'Sum of squares',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.varpopFunction',
+ {
+ defaultMessage: 'VAR_POP',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ {
+ label: i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.documentation.varsampFunction',
+ {
+ defaultMessage: 'VAR_SAMP',
+ }
+ ),
+ description: (
+
+ ),
+ },
+ ],
+};
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.styles.ts b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.styles.ts
new file mode 100644
index 0000000000000..94fe389b062ec
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.styles.ts
@@ -0,0 +1,109 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+import type { EuiThemeComputed } from '@elastic/eui';
+
+export const EDITOR_INITIAL_HEIGHT = 38;
+export const EDITOR_INITIAL_HEIGHT_EXPANDED = 140;
+export const EDITOR_MIN_HEIGHT = 40;
+export const EDITOR_MAX_HEIGHT = 400;
+
+export const textBasedLanguagedEditorStyles = (
+ euiTheme: EuiThemeComputed,
+ isCompactFocused: boolean,
+ editorHeight: number,
+ isCodeEditorExpanded: boolean,
+ hasErrors: boolean,
+ isCodeEditorExpandedFocused: boolean
+) => {
+ let position = isCompactFocused ? ('absolute' as 'absolute') : ('relative' as 'relative'); // cast string to type 'relative' | 'absolute'
+ if (isCodeEditorExpanded) {
+ position = 'relative' as 'relative';
+ }
+ const bottomContainerBorderColor = hasErrors ? euiTheme.colors.danger : euiTheme.colors.primary;
+ return {
+ editorContainer: {
+ position,
+ zIndex: isCompactFocused ? 4 : 0,
+ height: `${editorHeight}px`,
+ border: isCompactFocused ? euiTheme.border.thin : 'none',
+ borderTopLeftRadius: isCodeEditorExpanded ? 0 : '6px',
+ borderBottom: isCodeEditorExpanded
+ ? 'none'
+ : isCompactFocused
+ ? euiTheme.border.thin
+ : 'none',
+ },
+ resizableContainer: {
+ display: 'flex',
+ width: isCodeEditorExpanded ? '100%' : 'calc(100% - 80px)',
+ alignItems: isCompactFocused ? 'flex-start' : 'center',
+ border: !isCompactFocused ? euiTheme.border.thin : 'none',
+ borderTopLeftRadius: '6px',
+ borderBottomLeftRadius: '6px',
+ borderBottomWidth: hasErrors ? '2px' : '1px',
+ borderBottomColor: hasErrors ? euiTheme.colors.danger : euiTheme.colors.lightShade,
+ },
+ linesBadge: {
+ position: 'absolute' as 'absolute', // cast string to type 'absolute',
+ zIndex: 1,
+ right: hasErrors ? '60px' : '12px',
+ top: '50%',
+ transform: 'translate(0, -50%)',
+ },
+ errorsBadge: {
+ position: 'absolute' as 'absolute', // cast string to type 'absolute',
+ zIndex: 1,
+ right: '12px',
+ top: '50%',
+ transform: 'translate(0, -50%)',
+ },
+ bottomContainer: {
+ border: euiTheme.border.thin,
+ borderTop:
+ isCodeEditorExpanded && !isCodeEditorExpandedFocused
+ ? hasErrors
+ ? `2px solid ${euiTheme.colors.danger}`
+ : euiTheme.border.thin
+ : `2px solid ${bottomContainerBorderColor}`,
+ backgroundColor: euiTheme.colors.lightestShade,
+ paddingLeft: euiTheme.size.base,
+ paddingRight: euiTheme.size.base,
+ paddingTop: euiTheme.size.xs,
+ paddingBottom: euiTheme.size.xs,
+ width: 'calc(100% + 2px)',
+ position: 'relative' as 'relative', // cast string to type 'relative',
+ marginTop: 0,
+ marginLeft: 0,
+ borderBottomLeftRadius: '6px',
+ borderBottomRightRadius: '6px',
+ },
+ topContainer: {
+ border: euiTheme.border.thin,
+ borderTopLeftRadius: '6px',
+ borderTopRightRadius: '6px',
+ backgroundColor: euiTheme.colors.lightestShade,
+ paddingLeft: euiTheme.size.base,
+ paddingRight: euiTheme.size.base,
+ paddingTop: euiTheme.size.xs,
+ paddingBottom: euiTheme.size.xs,
+ width: 'calc(100% + 2px)',
+ position: 'relative' as 'relative', // cast string to type 'relative',
+ marginLeft: 0,
+ marginTop: euiTheme.size.s,
+ },
+ dragResizeContainer: {
+ width: '100%',
+ cursor: 'row-resize',
+ textAlign: 'center' as 'center',
+ height: euiTheme.size.base,
+ },
+ dragResizeButton: {
+ cursor: 'row-resize',
+ },
+ };
+};
diff --git a/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.test.tsx b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.test.tsx
new file mode 100644
index 0000000000000..8c0b4b9a4b8a1
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.test.tsx
@@ -0,0 +1,153 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { IUiSettingsClient } from '@kbn/core/public';
+import { mountWithIntl as mount } from '@kbn/test-jest-helpers';
+import { findTestSubject } from '@elastic/eui/lib/test';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import { TextBasedLanguagesEditor, TextBasedLanguagesEditorProps } from '.';
+
+describe('TextBasedLanguagesEditor', () => {
+ const uiConfig: Record = {};
+ const uiSettings = {
+ get: (key: string) => uiConfig[key],
+ } as IUiSettingsClient;
+
+ const services = {
+ uiSettings,
+ };
+
+ function renderTextBasedLanguagesEditorComponent(testProps: TextBasedLanguagesEditorProps) {
+ return (
+
+
+
+ );
+ }
+ let props: TextBasedLanguagesEditorProps;
+ beforeEach(() => {
+ props = {
+ query: { sql: 'SELECT * FROM test' },
+ isCodeEditorExpanded: false,
+ onTextLangQueryChange: jest.fn(),
+ onTextLangQuerySubmit: jest.fn(),
+ expandCodeEditor: jest.fn(),
+ };
+ });
+ it('should render the editor component', async () => {
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
+ expect(component.find('[data-test-subj="unifiedTextLangEditor"]').length).not.toBe(0);
+ });
+ });
+
+ it('should render the lines badge for the inline mode by default', async () => {
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
+ expect(
+ component.find('[data-test-subj="unifiedTextLangEditor-inline-lines-badge"]').length
+ ).not.toBe(0);
+ });
+ });
+
+ it('should render the errors badge for the inline mode by default if errors are provides', async () => {
+ const newProps = {
+ ...props,
+ errors: [new Error('error1')],
+ };
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
+ expect(
+ component.find('[data-test-subj="unifiedTextLangEditor-inline-errors-badge"]').length
+ ).not.toBe(0);
+ });
+ });
+
+ it('should render the correct buttons for the inline code editor mode', async () => {
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...props }));
+ expect(component.find('[data-test-subj="unifiedTextLangEditor-expand"]').length).not.toBe(0);
+ expect(
+ component.find('[data-test-subj="unifiedTextLangEditor-inline-documentation"]').length
+ ).not.toBe(0);
+ });
+ });
+
+ it('should call the expand editor function when expand button is clicked', async () => {
+ const expandCodeEditorSpy = jest.fn();
+ const newProps = {
+ ...props,
+ expandCodeEditor: expandCodeEditorSpy,
+ };
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
+ findTestSubject(component, 'unifiedTextLangEditor-expand').simulate('click');
+ expect(expandCodeEditorSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('should render the correct buttons for the expanded code editor mode', async () => {
+ const newProps = {
+ ...props,
+ isCodeEditorExpanded: true,
+ };
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
+ expect(
+ component.find('[data-test-subj="unifiedTextLangEditor-toggleWordWrap"]').length
+ ).not.toBe(0);
+ expect(component.find('[data-test-subj="unifiedTextLangEditor-minimize"]').length).not.toBe(
+ 0
+ );
+ expect(
+ component.find('[data-test-subj="unifiedTextLangEditor-documentation"]').length
+ ).not.toBe(0);
+ });
+ });
+
+ it('should call the expand editor function when minimize button is clicked', async () => {
+ const expandCodeEditorSpy = jest.fn();
+ const newProps = {
+ ...props,
+ isCodeEditorExpanded: true,
+ expandCodeEditor: expandCodeEditorSpy,
+ };
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
+ findTestSubject(component, 'unifiedTextLangEditor-minimize').simulate('click');
+ expect(expandCodeEditorSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('should render the resize for the expanded code editor mode', async () => {
+ const newProps = {
+ ...props,
+ isCodeEditorExpanded: true,
+ };
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
+ expect(component.find('[data-test-subj="unifiedTextLangEditor-resize"]').length).not.toBe(0);
+ });
+ });
+
+ it('should render the footer for the expanded code editor mode', async () => {
+ const newProps = {
+ ...props,
+ isCodeEditorExpanded: true,
+ };
+ await act(async () => {
+ const component = mount(renderTextBasedLanguagesEditorComponent({ ...newProps }));
+ expect(component.find('[data-test-subj="unifiedTextLangEditor-footer"]').length).not.toBe(0);
+ expect(
+ component.find('[data-test-subj="unifiedTextLangEditor-footer-lines"]').at(0).text()
+ ).toBe('1 line');
+ });
+ });
+});
diff --git a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx
index 7fbb9df37b741..bb6af5f1b0d4a 100644
--- a/src/plugins/unified_search/public/search_bar/create_search_bar.tsx
+++ b/src/plugins/unified_search/public/search_bar/create_search_bar.tsx
@@ -12,7 +12,7 @@ import { CoreStart } from '@kbn/core/public';
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { QueryStart, SavedQuery, DataPublicPluginStart } from '@kbn/data-plugin/public';
-import type { Query } from '@kbn/es-query';
+import type { Query, AggregateQuery } from '@kbn/es-query';
import type { Filter, TimeRange } from '@kbn/es-query';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { SearchBar } from '.';
@@ -30,12 +30,13 @@ interface StatefulSearchBarDeps {
isScreenshotMode?: boolean;
}
-export type StatefulSearchBarProps = SearchBarOwnProps & {
- appName: string;
- useDefaultBehaviors?: boolean;
- savedQueryId?: string;
- onSavedQueryIdChange?: (savedQueryId?: string) => void;
-};
+export type StatefulSearchBarProps =
+ SearchBarOwnProps & {
+ appName: string;
+ useDefaultBehaviors?: boolean;
+ savedQueryId?: string;
+ onSavedQueryIdChange?: (savedQueryId?: string) => void;
+ };
// Respond to user changing the filters
const defaultFiltersUpdated = (queryService: QueryStart) => {
@@ -56,16 +57,16 @@ const defaultOnRefreshChange = (queryService: QueryStart) => {
};
// Respond to user changing the query string or time settings
-const defaultOnQuerySubmit = (
- props: StatefulSearchBarProps,
+const defaultOnQuerySubmit = (
+ props: StatefulSearchBarProps,
queryService: QueryStart,
- currentQuery: Query
+ currentQuery: QT | Query
) => {
if (!props.useDefaultBehaviors) return props.onQuerySubmit;
const { timefilter } = queryService.timefilter;
- return (payload: { dateRange: TimeRange; query?: Query }) => {
+ return (payload: { dateRange: TimeRange; query?: QT | Query }) => {
const isUpdate =
!_.isEqual(timefilter.getTime(), payload.dateRange) ||
!_.isEqual(payload.query, currentQuery);
@@ -91,7 +92,10 @@ const defaultOnQuerySubmit = (
};
// Respond to user clearing a saved query
-const defaultOnClearSavedQuery = (props: StatefulSearchBarProps, clearSavedQuery: Function) => {
+const defaultOnClearSavedQuery = (
+ props: StatefulSearchBarProps,
+ clearSavedQuery: Function
+) => {
if (!props.useDefaultBehaviors) return props.onClearSavedQuery;
return () => {
clearSavedQuery();
@@ -100,7 +104,10 @@ const defaultOnClearSavedQuery = (props: StatefulSearchBarProps, clearSavedQuery
};
// Respond to user saving or updating a saved query
-const defaultOnSavedQueryUpdated = (props: StatefulSearchBarProps, setSavedQuery: Function) => {
+const defaultOnSavedQueryUpdated = (
+ props: StatefulSearchBarProps,
+ setSavedQuery: Function
+) => {
if (!props.useDefaultBehaviors) return props.onSavedQueryUpdated;
return (savedQuery: SavedQuery) => {
setSavedQuery(savedQuery);
@@ -108,7 +115,9 @@ const defaultOnSavedQueryUpdated = (props: StatefulSearchBarProps, setSavedQuery
};
};
-const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => {
+const overrideDefaultBehaviors = (
+ props: StatefulSearchBarProps
+) => {
return props.useDefaultBehaviors ? {} : props;
};
@@ -121,7 +130,7 @@ export function createSearchBar({
}: StatefulSearchBarDeps) {
// App name should come from the core application service.
// Until it's available, we'll ask the user to provide it for the pre-wired component.
- return (props: StatefulSearchBarProps) => {
+ return (props: StatefulSearchBarProps) => {
const { useDefaultBehaviors } = props;
// Handle queries
const onQuerySubmitRef = useRef(props.onQuerySubmit);
@@ -135,7 +144,7 @@ export function createSearchBar({
const { query } = useQueryStringManager({
query: props.query,
queryStringManager: data.query.queryString,
- });
+ }) as { query: QT };
const { timeRange, refreshInterval } = useTimefilter({
dateRangeFrom: props.dateRangeFrom,
dateRangeTo: props.dateRangeTo,
@@ -204,6 +213,8 @@ export function createSearchBar({
placeholder={props.placeholder}
{...overrideDefaultBehaviors(props)}
dataViewPickerComponentProps={props.dataViewPickerComponentProps}
+ textBasedLanguageModeErrors={props.textBasedLanguageModeErrors}
+ onTextBasedSavedAndExit={props.onTextBasedSavedAndExit}
displayStyle={props.displayStyle}
isScreenshotMode={isScreenshotMode}
/>
diff --git a/src/plugins/unified_search/public/search_bar/index.tsx b/src/plugins/unified_search/public/search_bar/index.tsx
index f8c9de7ec7d87..3a32ccb982147 100644
--- a/src/plugins/unified_search/public/search_bar/index.tsx
+++ b/src/plugins/unified_search/public/search_bar/index.tsx
@@ -7,20 +7,21 @@
*/
import React from 'react';
-import { injectI18n } from '@kbn/i18n-react';
-import { withKibana } from '@kbn/kibana-react-plugin/public';
+import { AggregateQuery, Query } from '@kbn/es-query';
import type { SearchBarProps } from './search_bar';
const Fallback = () => ;
const LazySearchBar = React.lazy(() => import('./search_bar'));
-const WrappedSearchBar = (props: SearchBarProps) => (
+const WrappedSearchBar = (
+ props: Omit, 'intl' | 'kibana'>
+) => (
}>
-
+ )} />
);
-export const SearchBar = injectI18n(withKibana(WrappedSearchBar));
+export const SearchBar = WrappedSearchBar;
export type { StatefulSearchBarProps } from './create_search_bar';
export type { SearchBarProps, SearchBarOwnProps } from './search_bar';
export { createSearchBar } from './create_search_bar';
diff --git a/src/plugins/unified_search/public/search_bar/lib/use_query_string_manager.ts b/src/plugins/unified_search/public/search_bar/lib/use_query_string_manager.ts
index e3e6ceb350812..6bd27727b28db 100644
--- a/src/plugins/unified_search/public/search_bar/lib/use_query_string_manager.ts
+++ b/src/plugins/unified_search/public/search_bar/lib/use_query_string_manager.ts
@@ -8,17 +8,23 @@
import { useState, useEffect, useMemo } from 'react';
import { Subscription } from 'rxjs';
-import type { Query } from '@kbn/es-query';
+import { Query, AggregateQuery } from '@kbn/es-query';
import type { QueryStringContract } from '@kbn/data-plugin/public';
+function isOfQueryType(arg: Query | AggregateQuery): arg is Query {
+ return Boolean(arg && 'query' in arg);
+}
+
interface UseQueryStringProps {
- query?: Query;
+ query?: Query | AggregateQuery;
queryStringManager: QueryStringContract;
}
export const useQueryStringManager = (props: UseQueryStringProps) => {
// Filters should be either what's passed in the initial state or the current state of the filter manager
- const [query, setQuery] = useState(props.query || props.queryStringManager.getQuery());
+ const [query, setQuery] = useState(
+ props.query || props.queryStringManager.getQuery()
+ );
useEffect(() => {
const subscriptions = new Subscription();
@@ -36,13 +42,15 @@ export const useQueryStringManager = (props: UseQueryStringProps) => {
};
}, [props.queryStringManager]);
- const stableQuery = useMemo(
- () => ({
- language: query.language,
- query: query.query,
- }),
- [query.language, query.query]
- );
-
+ const isQueryType = isOfQueryType(query);
+ const stableQuery = useMemo(() => {
+ if (isQueryType) {
+ return {
+ language: query.language,
+ query: query.query,
+ };
+ }
+ return query;
+ }, [isQueryType, query]);
return { query: stableQuery };
};
diff --git a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx
index fe5e03ab7fb37..7e1ad2bba21c8 100644
--- a/src/plugins/unified_search/public/search_bar/search_bar.test.tsx
+++ b/src/plugins/unified_search/public/search_bar/search_bar.test.tsx
@@ -70,6 +70,10 @@ const kqlQuery = {
language: 'kuery',
};
+const sqlQuery = {
+ sql: 'SELECT * from test',
+};
+
function wrapSearchBarInContext(testProps: any) {
const defaultOptions = {
appName: 'test',
@@ -124,6 +128,7 @@ describe('SearchBar', () => {
const FILTER_BAR = '[data-test-subj="unifiedFilterBar"]';
const QUERY_BAR = '.kbnQueryBar';
const QUERY_INPUT = '[data-test-subj="unifiedQueryInput"]';
+ const EDITOR = '[data-test-subj="unifiedTextLangEditor"]';
beforeEach(() => {
jest.clearAllMocks();
@@ -224,4 +229,17 @@ describe('SearchBar', () => {
expect(component.find(QUERY_BAR).length).toBeTruthy();
expect(component.find(QUERY_INPUT).length).toBeTruthy();
});
+
+ it('Should NOT render the input query input, for sql query', () => {
+ const component = mount(
+ wrapSearchBarInContext({
+ indexPatterns: [mockIndexPattern],
+ screenTitle: 'test screen',
+ onQuerySubmit: noop,
+ query: sqlQuery,
+ })
+ );
+ expect(component.find(QUERY_INPUT).length).toBeFalsy();
+ expect(component.find(EDITOR).length).toBeTruthy();
+ });
});
diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx
index aada686db3dc8..6b58acfd29fe0 100644
--- a/src/plugins/unified_search/public/search_bar/search_bar.tsx
+++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx
@@ -15,7 +15,7 @@ import { get, isEqual } from 'lodash';
import memoizeOne from 'memoize-one';
import { METRIC_TYPE } from '@kbn/analytics';
-import { Query, Filter, TimeRange } from '@kbn/es-query';
+import { Query, Filter, TimeRange, AggregateQuery, isOfQueryType } from '@kbn/es-query';
import { withKibana, KibanaReactContextValue } from '@kbn/kibana-react-plugin/public';
import type { TimeHistoryContract, SavedQuery } from '@kbn/data-plugin/public';
import type { SavedQueryAttributes } from '@kbn/data-plugin/common';
@@ -25,7 +25,7 @@ import { DataView } from '@kbn/data-views-plugin/public';
import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form';
import { SavedQueryManagementList } from '../saved_query_management';
import { QueryBarMenu, QueryBarMenuProps } from '../query_string_input/query_bar_menu';
-import type { DataViewPickerProps } from '../dataview_picker';
+import type { DataViewPickerProps, OnSaveTextLanguageQueryProps } from '../dataview_picker';
import QueryBarTopRow from '../query_string_input/query_bar_top_row';
import { FilterBar, FilterItems } from '../filter_bar';
import type { SuggestionsListSize } from '../typeahead/suggestions_component';
@@ -41,7 +41,7 @@ export interface SearchBarInjectedDeps {
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
}
-export interface SearchBarOwnProps {
+export interface SearchBarOwnProps {
indexPatterns?: DataView[];
isLoading?: boolean;
customSubmitButton?: React.ReactNode;
@@ -61,12 +61,15 @@ export interface SearchBarOwnProps {
dateRangeFrom?: string;
dateRangeTo?: string;
// Query bar - should be in SearchBarInjectedDeps
- query?: Query;
+ query?: QT | Query;
// Show when user has privileges to save
showSaveQuery?: boolean;
savedQuery?: SavedQuery;
- onQueryChange?: (payload: { dateRange: TimeRange; query?: Query }) => void;
- onQuerySubmit?: (payload: { dateRange: TimeRange; query?: Query }, isUpdate?: boolean) => void;
+ onQueryChange?: (payload: { dateRange: TimeRange; query?: QT | Query }) => void;
+ onQuerySubmit?: (
+ payload: { dateRange: TimeRange; query?: QT | Query },
+ isUpdate?: boolean
+ ) => void;
// User has saved the current state as a saved query
onSaved?: (savedQuery: SavedQuery) => void;
// User has modified the saved query, your app should persist the update
@@ -87,25 +90,31 @@ export interface SearchBarOwnProps {
// super update button background fill control
fillSubmitButton?: boolean;
dataViewPickerComponentProps?: DataViewPickerProps;
+ textBasedLanguageModeErrors?: Error[];
+ onTextBasedSavedAndExit?: ({ onSave }: OnSaveTextLanguageQueryProps) => void;
showSubmitButton?: boolean;
// defines size of suggestions query popover
suggestionsSize?: SuggestionsListSize;
isScreenshotMode?: boolean;
}
-export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
+export type SearchBarProps = SearchBarOwnProps &
+ SearchBarInjectedDeps;
-interface State {
+interface State {
isFiltersVisible: boolean;
openQueryBarMenu: boolean;
showSavedQueryPopover: boolean;
currentProps?: SearchBarProps;
- query?: Query;
+ query?: QT | Query;
dateRangeFrom: string;
dateRangeTo: string;
}
-class SearchBarUI extends Component {
+class SearchBarUI extends Component<
+ SearchBarProps & WithEuiThemeProps,
+ State
+> {
public static defaultProps = {
showQueryBar: true,
showFilterBar: true,
@@ -117,13 +126,20 @@ class SearchBarUI extends Component {
private services = this.props.kibana.services;
private savedQueryService = this.services.data.query.savedQueries;
- public static getDerivedStateFromProps(nextProps: SearchBarProps, prevState: State) {
+ public static getDerivedStateFromProps(
+ nextProps: SearchBarProps,
+ prevState: State
+ ) {
if (isEqual(prevState.currentProps, nextProps)) {
return null;
}
let nextQuery = null;
- if (nextProps.query && nextProps.query.query !== get(prevState, 'currentProps.query.query')) {
+ if (
+ nextProps.query &&
+ isOfQueryType(nextProps.query) &&
+ nextProps.query.query !== get(prevState, 'currentProps.query.query')
+ ) {
nextQuery = {
query: nextProps.query.query,
language: nextProps.query.language,
@@ -131,12 +147,16 @@ class SearchBarUI extends Component {
} else if (
nextProps.query &&
prevState.query &&
+ isOfQueryType(nextProps.query) &&
+ isOfQueryType(prevState.query) &&
nextProps.query.language !== prevState.query.language
) {
nextQuery = {
query: '',
language: nextProps.query.language,
};
+ } else if (nextProps.query && !isOfQueryType(nextProps.query)) {
+ nextQuery = nextProps.query;
}
let nextDateRange = null;
@@ -183,15 +203,15 @@ class SearchBarUI extends Component {
query: this.props.query ? { ...this.props.query } : undefined,
dateRangeFrom: get(this.props, 'dateRangeFrom', 'now-15m'),
dateRangeTo: get(this.props, 'dateRangeTo', 'now'),
- };
+ } as State;
public isDirty = () => {
if (!this.props.showDatePicker && this.state.query && this.props.query) {
- return this.state.query.query !== this.props.query.query;
+ return !isEqual(this.state.query, this.props.query);
}
return (
- (this.state.query && this.props.query && this.state.query.query !== this.props.query.query) ||
+ (this.state.query && this.props.query && !isEqual(this.state.query, this.props.query)) ||
this.state.dateRangeFrom !== this.props.dateRangeFrom ||
this.state.dateRangeTo !== this.props.dateRangeTo
);
@@ -235,7 +255,7 @@ class SearchBarUI extends Component {
const savedQueryAttributes: SavedQueryAttributes = {
title: savedQueryMeta.title,
description: savedQueryMeta.description,
- query: this.state.query,
+ query: this.state.query as Query,
};
if (savedQueryMeta.shouldIncludeFilters) {
@@ -285,7 +305,7 @@ class SearchBarUI extends Component {
}
};
- public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: Query }) => {
+ public onQueryBarChange = (queryAndDateRange: { dateRange: TimeRange; query?: QT | Query }) => {
this.setState({
query: queryAndDateRange.query,
dateRangeFrom: queryAndDateRange.dateRange.from,
@@ -296,13 +316,49 @@ class SearchBarUI extends Component {
}
};
+ public onTextLangQueryChange = (query?: any) => {
+ this.setState({
+ query,
+ });
+ if (this.props.onQueryChange) {
+ this.props.onQueryChange({
+ query,
+ dateRange: {
+ from: this.state.dateRangeFrom,
+ to: this.state.dateRangeTo,
+ },
+ });
+ }
+ };
+
public toggleFilterBarMenuPopover = (value: boolean) => {
this.setState({
openQueryBarMenu: value,
});
};
- public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: Query }) => {
+ public onTextLangQuerySubmit = (query?: Query | AggregateQuery) => {
+ // clean up all filters
+ this.props.onFiltersUpdated?.([]);
+ this.setState(
+ {
+ query: query as QT,
+ },
+ () => {
+ if (this.props.onQuerySubmit) {
+ this.props.onQuerySubmit({
+ query: this.state.query,
+ dateRange: {
+ from: this.state.dateRangeFrom,
+ to: this.state.dateRangeTo,
+ },
+ });
+ }
+ }
+ );
+ };
+
+ public onQueryBarSubmit = (queryAndDateRange: { dateRange?: TimeRange; query?: QT | Query }) => {
this.setState(
{
query: queryAndDateRange.query,
@@ -391,7 +447,11 @@ class SearchBarUI extends Component {
const queryBarMenu = (
{
onFiltersUpdated={this.props.onFiltersUpdated}
filters={this.props.filters}
hiddenPanelOptions={this.props.hiddenFilterPanelOptions}
- query={this.state.query}
+ query={this.state.query as Query}
savedQuery={this.props.savedQuery}
onClearSavedQuery={this.props.onClearSavedQuery}
showQueryInput={this.props.showQueryInput}
@@ -450,7 +510,7 @@ class SearchBarUI extends Component {
return (
-
timeHistory={this.props.timeHistory}
query={this.state.query}
screenTitle={this.props.screenTitle}
@@ -485,10 +545,14 @@ class SearchBarUI extends Component {
filters={this.props.filters!}
onFiltersUpdated={this.props.onFiltersUpdated}
dataViewPickerComponentProps={this.props.dataViewPickerComponentProps}
+ textBasedLanguageModeErrors={this.props.textBasedLanguageModeErrors}
+ onTextBasedSavedAndExit={this.props.onTextBasedSavedAndExit}
showDatePickerAsBadge={this.shouldShowDatePickerAsBadge()}
filterBar={filterBar}
suggestionsSize={this.props.suggestionsSize}
isScreenshotMode={this.props.isScreenshotMode}
+ onTextLangQuerySubmit={this.onTextLangQuerySubmit}
+ onTextLangQueryChange={this.onTextLangQueryChange}
/>
);
@@ -496,7 +560,9 @@ class SearchBarUI extends Component {
private hasFiltersOrQuery() {
const hasFilters = Boolean(this.props.filters && this.props.filters.length > 0);
- const hasQuery = Boolean(this.state.query && this.state.query.query);
+ const hasQuery = Boolean(
+ this.state.query && isOfQueryType(this.state.query) && this.state.query.query
+ );
return hasFilters || hasQuery;
}
@@ -525,4 +591,12 @@ class SearchBarUI extends Component {
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
-export default injectI18n(withEuiTheme(withKibana(SearchBarUI)));
+export default injectI18n(
+ withEuiTheme(
+ withKibana(
+ SearchBarUI as React.ComponentType<
+ SearchBarOwnProps & SearchBarInjectedDeps & WithEuiThemeProps<{}>
+ >
+ )
+ )
+);
diff --git a/src/plugins/unified_search/public/types.ts b/src/plugins/unified_search/public/types.ts
index f218106284ac2..3189e7cf32d08 100755
--- a/src/plugins/unified_search/public/types.ts
+++ b/src/plugins/unified_search/public/types.ts
@@ -12,6 +12,7 @@ import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/publ
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { UiActionsSetup, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
+import { Query, AggregateQuery } from '@kbn/es-query';
import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
import type { IndexPatternSelectProps, StatefulSearchBarProps } from '.';
@@ -38,7 +39,10 @@ export interface UnifiedSearchStartDependencies {
*/
export interface UnifiedSearchPublicPluginStartUi {
IndexPatternSelect: React.ComponentType;
- SearchBar: React.ComponentType;
+ SearchBar: (props: StatefulSearchBarProps) => React.ReactElement;
+ AggregateQuerySearchBar: (
+ props: StatefulSearchBarProps
+ ) => React.ReactElement;
}
/**
diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.ts
index a8a6fa3eea2a4..4f7245ed436d7 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_editor_updates.ts
@@ -9,7 +9,7 @@
import { useEffect, useState } from 'react';
import { isEqual } from 'lodash';
import { EventEmitter } from 'events';
-
+import { Query } from '@kbn/es-query';
import {
VisualizeServices,
VisualizeAppState,
@@ -51,7 +51,7 @@ export const useEditorUpdates = (
uiState: vis.uiState,
timeRange: timefilter.getTime(),
filters: filterManager.getFilters(),
- query: queryString.getQuery(),
+ query: queryString.getQuery() as Query,
linked: !!vis.data.savedSearchId,
savedSearch,
});
@@ -59,7 +59,7 @@ export const useEditorUpdates = (
embeddableHandler.updateInput({
timeRange: timefilter.getTime(),
filters: filterManager.getFilters(),
- query: queryString.getQuery(),
+ query: queryString.getQuery() as Query,
searchSessionId: services.data.search.session.getSessionId(),
});
}
diff --git a/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts b/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts
index 476edc41b802a..8d7f2a8ef61f4 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/use/use_linked_search_updates.ts
@@ -10,7 +10,7 @@ import { useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EventEmitter } from 'events';
-import { Filter } from '@kbn/es-query';
+import { Filter, Query } from '@kbn/es-query';
import {
VisualizeServices,
VisualizeAppStateContainer,
@@ -40,7 +40,7 @@ export const useLinkedSearchUpdates = (
searchSource.setParent(searchSourceGrandparent);
appState.transitions.unlinkSavedSearch({
- query: searchSourceParent?.getField('query'),
+ query: searchSourceParent?.getField('query') as Query,
parentFilters: (searchSourceParent?.getOwnField('filter') as Filter[]) || [],
});
diff --git a/src/plugins/visualizations/public/visualize_app/utils/utils.ts b/src/plugins/visualizations/public/visualize_app/utils/utils.ts
index f122b301af1f8..a2c8189d68f36 100644
--- a/src/plugins/visualizations/public/visualize_app/utils/utils.ts
+++ b/src/plugins/visualizations/public/visualize_app/utils/utils.ts
@@ -9,7 +9,7 @@
import { i18n } from '@kbn/i18n';
import type { History } from 'history';
import type { ChromeStart, DocLinksStart } from '@kbn/core/public';
-import type { Filter } from '@kbn/es-query';
+import type { Filter, Query } from '@kbn/es-query';
import { redirectWhenMissing } from '@kbn/kibana-utils-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { VisualizeConstants } from '../../../common/constants';
@@ -56,7 +56,7 @@ export const visStateToEditorState = (
return {
uiState:
savedVis && savedVis.uiStateJSON ? JSON.parse(savedVis.uiStateJSON) : vis.uiState.toJSON(),
- query: vis.data.searchSource?.getOwnField('query') || getDefaultQuery(services),
+ query: (vis.data.searchSource?.getOwnField('query') || getDefaultQuery(services)) as Query,
filters: (vis.data.searchSource?.getOwnField('filter') as Filter[]) || [],
vis: { ...savedVisState.visState, title: vis.title },
linked: savedVis && savedVis.id ? !!savedVis.savedSearchId : !!savedVisState.savedSearchId,
diff --git a/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx b/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx
index e6179593de8d6..51a5c5b47156a 100644
--- a/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx
+++ b/src/plugins/visualizations/public/wizard/search_selection/search_selection.tsx
@@ -9,6 +9,7 @@
import { EuiModalBody, EuiModalHeader, EuiModalHeaderTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
+import type { SimpleSavedObject, SavedObjectAttributes } from '@kbn/core/public';
import React from 'react';
import { IUiSettingsClient, SavedObjectsStart } from '@kbn/core/public';
@@ -23,6 +24,9 @@ interface SearchSelectionProps {
savedObjects: SavedObjectsStart;
goBack: () => void;
}
+interface SavedSearchesAttributes extends SavedObjectAttributes {
+ isTextBasedQuery: boolean;
+}
export class SearchSelection extends React.Component {
private fixedPageSize: number = 8;
@@ -66,6 +70,12 @@ export class SearchSelection extends React.Component {
defaultMessage: 'Saved search',
}
),
+ // ignore the saved searches that have text-based languages queries
+ includeFields: ['isTextBasedQuery'],
+ showSavedObject: (savedObject) => {
+ const so = savedObject as unknown as SimpleSavedObject;
+ return !so.attributes.isTextBasedQuery;
+ },
},
{
type: 'index-pattern',
diff --git a/test/functional/apps/discover/_sql_view.ts b/test/functional/apps/discover/_sql_view.ts
new file mode 100644
index 0000000000000..2643fc163d488
--- /dev/null
+++ b/test/functional/apps/discover/_sql_view.ts
@@ -0,0 +1,93 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const log = getService('log');
+ const dataGrid = getService('dataGrid');
+ const testSubjects = getService('testSubjects');
+ const monacoEditor = getService('monacoEditor');
+ const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
+
+ const defaultSettings = {
+ defaultIndex: 'logstash-*',
+ 'discover:enableSql': true,
+ };
+
+ describe('discover sql view', async function () {
+ before(async () => {
+ log.debug('load kibana index with default index pattern');
+ await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover');
+ // and load a set of makelogs data
+ await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional');
+ await kibanaServer.uiSettings.replace(defaultSettings);
+ await PageObjects.common.navigateToApp('discover');
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ });
+
+ describe('test', () => {
+ it('should render sql view correctly', async function () {
+ expect(await testSubjects.exists('showQueryBarMenu')).to.be(true);
+ expect(await testSubjects.exists('superDatePickerToggleQuickMenuButton')).to.be(true);
+ expect(await testSubjects.exists('addFilter')).to.be(true);
+ expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(true);
+ expect(await testSubjects.exists('discoverChart')).to.be(true);
+ expect(await testSubjects.exists('discoverQueryHits')).to.be(true);
+ expect(await testSubjects.exists('discoverAlertsButton')).to.be(true);
+ expect(await testSubjects.exists('shareTopNavButton')).to.be(true);
+ expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(true);
+ expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(true);
+ expect(await testSubjects.exists('fieldFilterSearchInput')).to.be(true);
+ expect(await testSubjects.exists('toggleFieldFilterButton')).to.be(true);
+ expect(await testSubjects.exists('fieldTypesHelpButton')).to.be(true);
+ await testSubjects.click('field-@message-showDetails');
+ expect(await testSubjects.exists('discoverFieldListPanelEditItem')).to.be(true);
+
+ await PageObjects.discover.selectTextBaseLang('SQL');
+
+ expect(await testSubjects.exists('fieldFilterSearchInput')).to.be(true);
+ expect(await testSubjects.exists('unifiedTextLangEditor')).to.be(true);
+ expect(await testSubjects.exists('superDatePickerToggleQuickMenuButton')).to.be(true);
+
+ expect(await testSubjects.exists('showQueryBarMenu')).to.be(false);
+ expect(await testSubjects.exists('addFilter')).to.be(false);
+ expect(await testSubjects.exists('dscViewModeDocumentButton')).to.be(false);
+ expect(await testSubjects.exists('discoverChart')).to.be(false);
+ expect(await testSubjects.exists('discoverQueryHits')).to.be(false);
+ expect(await testSubjects.exists('discoverAlertsButton')).to.be(false);
+ expect(await testSubjects.exists('shareTopNavButton')).to.be(false);
+ expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(false);
+ expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(false);
+ expect(await testSubjects.exists('toggleFieldFilterButton')).to.be(false);
+ expect(await testSubjects.exists('fieldTypesHelpButton')).to.be(false);
+ await testSubjects.click('field-@message-showDetails');
+ expect(await testSubjects.exists('discoverFieldListPanelEditItem')).to.be(false);
+ });
+
+ it('should perform test query correctly', async function () {
+ await PageObjects.discover.selectTextBaseLang('SQL');
+ const testQuery = `SELECT "@tags", geo.dest, count(*) occurred FROM "logstash-*"
+ GROUP BY "@tags", geo.dest
+ HAVING occurred > 20
+ ORDER BY occurred DESC`;
+
+ await monacoEditor.setCodeEditorValue(testQuery);
+ await testSubjects.click('querySubmitButton');
+ await PageObjects.header.waitUntilLoadingHasFinished();
+
+ const cell = await dataGrid.getCellElement(0, 3);
+ expect(await cell.getVisibleText()).to.be('2269');
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/discover/index.ts b/test/functional/apps/discover/index.ts
index e6a40d79d41b0..2c08d122f9247 100644
--- a/test/functional/apps/discover/index.ts
+++ b/test/functional/apps/discover/index.ts
@@ -58,6 +58,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_data_grid_doc_table'));
loadTestFile(require.resolve('./_data_grid_copy_to_clipboard'));
loadTestFile(require.resolve('./_data_grid_pagination'));
+ loadTestFile(require.resolve('./_sql_view'));
loadTestFile(require.resolve('./_indexpattern_with_unmapped_fields'));
loadTestFile(require.resolve('./_runtime_fields_editor'));
loadTestFile(require.resolve('./_huge_fields'));
diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts
index f16f0396cf091..3f81384310eea 100644
--- a/test/functional/page_objects/discover_page.ts
+++ b/test/functional/page_objects/discover_page.ts
@@ -507,6 +507,14 @@ export class DiscoverPageObject extends FtrService {
await this.header.waitUntilLoadingHasFinished();
}
+ public async selectTextBaseLang(lang: 'SQL') {
+ await this.testSubjects.click('discover-dataView-switch-link');
+ await this.find.clickByCssSelector(
+ `[data-test-subj="text-based-languages-switcher"] [title="${lang}"]`
+ );
+ await this.header.waitUntilLoadingHasFinished();
+ }
+
public async removeHeaderColumn(name: string) {
const isLegacyDefault = await this.useLegacyTable();
if (isLegacyDefault) {
diff --git a/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx
index 045a36840a394..919029e1c95ed 100644
--- a/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx
+++ b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx
@@ -25,12 +25,7 @@ export const renderApp = (
testId: 'demoNewButton',
},
];
- render(
-
- Hey
- ,
- element
- );
+ render(, element);
return () => unmountComponentAtNode(element);
};
diff --git a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts
index 269e715bd2c06..6c4e1a7509f2d 100644
--- a/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts
+++ b/x-pack/plugins/cloud_security_posture/public/pages/findings/utils.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { buildEsQuery } from '@kbn/es-query';
+import { buildEsQuery, type Query } from '@kbn/es-query';
import { EuiBasicTableProps, Pagination } from '@elastic/eui';
import { useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
@@ -50,7 +50,7 @@ export const usePersistedQuery = (getter: ({ filters, query }: FindingsBaseUR
() =>
getter({
filters: filterManager.getAppFilters(),
- query: queryString.getQuery(),
+ query: queryString.getQuery() as Query,
}),
[getter, filterManager, queryString]
);
diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
index ba5811dfd6ba4..54e1c5eda5d58 100644
--- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx
+++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx
@@ -186,8 +186,9 @@ describe('Lens App', () => {
],
},
});
-
- const extraEntry = instance.find(services.navigation.ui.TopNavMenu).prop('config')[0];
+ const navigationComponent = services.navigation.ui
+ .TopNavMenu as unknown as React.ReactElement;
+ const extraEntry = instance.find(navigationComponent).prop('config')[0];
expect(extraEntry.label).toEqual('My entry');
expect(extraEntry.run).toBe(runFn);
});
diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
index 227b889ea904a..b0610a5b26e4d 100644
--- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx
+++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx
@@ -192,7 +192,7 @@ function getViewUnderlyingDataArgs({
}
const { filters: newFilters, query: newQuery } = combineQueryAndFilters(
- query,
+ query as Query,
filters,
meta,
dataViews,
diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts
index 7ea8053caa588..2a71cd9aaab48 100644
--- a/x-pack/plugins/lens/public/state_management/lens_slice.ts
+++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts
@@ -8,6 +8,7 @@
import { createAction, createReducer, current, PayloadAction } from '@reduxjs/toolkit';
import { VisualizeFieldContext } from '@kbn/ui-actions-plugin/public';
import { mapValues } from 'lodash';
+import { Query } from '@kbn/es-query';
import { History } from 'history';
import { LensEmbeddableInput } from '..';
import { getDatasourceLayers } from '../editor_frame_service/editor_frame';
@@ -63,7 +64,7 @@ export const getPreloadedState = ({
// only if Lens was opened with the intention to visualize a field (e.g. coming from Discover)
query: !initialContext
? data.query.queryString.getDefaultQuery()
- : data.query.queryString.getQuery(),
+ : (data.query.queryString.getQuery() as Query),
filters: !initialContext
? data.query.filterManager.getGlobalFilters()
: data.query.filterManager.getFilters(),
diff --git a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts
index 79bfc0ce0d8fd..1309eb9376587 100644
--- a/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts
+++ b/x-pack/plugins/maps/public/legacy_visualizations/region_map/region_map_fn.ts
@@ -45,6 +45,7 @@ export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({
},
},
async fn(input, args) {
+ const query = input.query as Query;
return {
type: 'render',
as: REGION_MAP_RENDER,
@@ -52,7 +53,7 @@ export const createRegionMapFn = (): RegionMapExpressionFunctionDefinition => ({
visType: REGION_MAP_VIS_TYPE,
visConfig: JSON.parse(args.visConfig),
filters: input.filters,
- query: Array.isArray(input.query) ? input.query[0] : input.query,
+ query: Array.isArray(query) ? query[0] : query,
timeRange: input.timeRange,
},
};
diff --git a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts
index 116b3375eba76..98ade49d6f869 100644
--- a/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts
+++ b/x-pack/plugins/maps/public/legacy_visualizations/tile_map/tile_map_fn.ts
@@ -45,6 +45,7 @@ export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({
},
},
async fn(input, args) {
+ const query = input.query as Query;
return {
type: 'render',
as: TILE_MAP_RENDER,
@@ -52,7 +53,7 @@ export const createTileMapFn = (): TileMapExpressionFunctionDefinition => ({
visType: TILE_MAP_VIS_TYPE,
visConfig: JSON.parse(args.visConfig),
filters: input.filters,
- query: Array.isArray(input.query) ? input.query[0] : input.query,
+ query: Array.isArray(query) ? query[0] : query,
timeRange: input.timeRange,
},
};
diff --git a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts
index cfd140434e022..c3e1f5681fe61 100644
--- a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts
+++ b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts
@@ -7,6 +7,7 @@
import uuid from 'uuid/v4';
import { i18n } from '@kbn/i18n';
+import type { Query } from '@kbn/es-query';
import type { SerializableRecord } from '@kbn/utility-types';
import {
createAction,
@@ -85,7 +86,7 @@ const getMapsLink = async (context: VisualizeFieldContext) => {
const locator = getShareService().url.locators.get(MAPS_APP_LOCATOR) as MapsAppLocator;
const location = await locator.getLocation({
filters: getData().query.filterManager.getFilters(),
- query: getData().query.queryString.getQuery(),
+ query: getData().query.queryString.getQuery() as Query,
initialLayers: initialLayers as unknown as LayerDescriptor[] & SerializableRecord,
timeRange: getData().query.timefilter.timefilter.getTime(),
});
diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
index 7685b3d0924b7..c7f3c9c1f578e 100644
--- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
+++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx
@@ -19,7 +19,6 @@ import { CSV_REPORTING_ACTION } from '../../common/constants';
import { checkLicense } from '../lib/license_check';
import { ReportingAPIClient } from '../lib/reporting_api_client';
import type { ReportingPublicPluginStartDendencies } from '../plugin';
-
function isSavedSearchEmbeddable(
embeddable: IEmbeddable | ISearchEmbeddable
): embeddable is ISearchEmbeddable {
@@ -98,7 +97,20 @@ export class ReportingCsvPanelAction implements ActionDefinition
}
const { embeddable } = context;
- return embeddable.getInput().viewMode !== ViewMode.EDIT && embeddable.type === 'search';
+
+ if (embeddable.type !== 'search') {
+ return false;
+ }
+
+ const savedSearch = embeddable.getSavedSearch();
+ const query = savedSearch.searchSource.getField('query');
+
+ // using isOfAggregateQueryType(query) added increased the bundle size over the configured limit of 55.7KB
+ if (query && Boolean(query && 'sql' in query)) {
+ // hide exporting CSV for SQL
+ return false;
+ }
+ return embeddable.getInput().viewMode !== ViewMode.EDIT;
};
public execute = async (context: ActionContext) => {
diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx
index 1a6ad28eea9fe..87e475e5f2c83 100644
--- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx
+++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression/search_source_expression_form.tsx
@@ -87,7 +87,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
},
{
index: searchSource.getField('index')!,
- query: searchSource.getField('query')!,
+ query: searchSource.getField('query')! as Query,
filter: mapAndFlattenFilters(searchSource.getField('filter') as Filter[]),
threshold: ruleParams.threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: ruleParams.thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,