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 && ( <>