From 95da4fc0ab4d4620af32f69ff1d7895fa1817d10 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 20 Jul 2022 08:58:30 +0300
Subject: [PATCH 01/30] [Discover] Supports SQL query language (#134429)
* Move the add dataview action above the dataview selection panel
* Implements a new selectable on the dataview picker for the text based languages
* Implementation of the transition modal when on SQL mode and select a dataview
* Fix es lint
* Change switch modal button modal icon
* Lazy load components
* Small changes on the styling of the switch without saving button
* Initialization of mocaco editor
* Change to the type
* Fixes types checks
* New submit button for query mode
* Implememtation of the expanded mode of the editor
* Implement documentation
* Implementation of the oneliner mode with ellipsis
* Some fixes on the resizer
* Implementation of the errors layout, WIP
* Fetch SQL data in Discover
* Fix expression test
* Fix editor zIndex
* Fix types error
* Fix type check in Discover
* Fix more types
* some CI fixes
* Fixes
* Cleanup after merge
* Remove from state
* Connect search errors with the unified search editor
* Add error mrkers in unified search editor
* Save and open saved searches
* Filter out saved searches from text based languages
* Some fixes
* Fix unit tests
* Fix checks
* On save and exit modal implementation
* Add shortcut on the editor for submit query
* Fix wrong condition
* Initial types change
* Use regex to find the index pattern string
* Fix some types and cleanup
* Fix types
* Fix some types
* Further fixes
* More fixes
* More fixes
* Fix visualize types
* more
* More fixes
* Fixes more types
* Fix dashboard types
* Fix dashboard types
* Controls plugin types
* Fix Lens types
* Fix data plugin types
* Fix types in Lens 2
* buildEsConfig type fixes
* Fix observability types
* Fix maps types
* data visualizer types
* Fix ml types
* xpack rest types
* Fix jest test
* Fix
* Move helper functions to es config
* fix bug on breadcrumb click
* Fix time field bug
* Add enableSql advanced setting to discover for enabling the sql mode
* Make the documentation component more dynamic
* Add some comments, improvements
* Enhance storybook with the textbased languages
* Update storybook with the error state of the editor
* Adds a readme for the editor and fixes the modal mobile version
* [Discover] improve test and storybook for new data type
* [Discover] add functional tests
* Add aggregate functions to the documentation
* [Discover] fix tests
* Add some unit tests
* [Discover] fix linting
* [Discover] update linting
* More unti tests
* Dataview picker unit tests
* Fix a bug on the dataview picker
* Add unit tests for the editor
* Fix jest test
* [Discover] apply suggestions
* [Discover] adjust styles
* Fix some bugs and select columns in the sql mode
* [Discover] fix eslint and tests
* [Discover] update unit tests
* Fix bug on transitioning from sql mode to dataview mode
* [Discover] fix tests
* Design fixes on the errors messages
* [Discover] fix ci
* Update the columns only if the query changes
* [Discover] change isPlainRecord retrieval method
* Fix bug on cleanup
* Fix bug on opening a saved search
* [Discover] fix comments
* [Discover] fix bug with browser refresh
* [Discover] fix functional
* [Discover] fix another functional
* Fix ordering lost when the user refreshes the browser
* [Discover] revert use_discover_state
* [Discover] revert functional impl
* Fix security solution types
* Casting dashboard plugin
* Revert change
* type param
* [CI] Auto-commit changed files from 'node scripts/precommit_hook.js --ref HEAD~1..HEAD --fix'
* Revert types changes
* More reverts
* Types fixes
* Fix Discover jest test
* Fix context app jest test
* Final types changes
* Fixes unit test
Co-authored-by: Dzmitry Tamashevich
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Joe Reuter
---
docs/management/advanced-options.asciidoc | 3 +
.../src/es_query/build_es_query.ts | 8 +-
.../src/es_query/es_query_sql.test.ts | 79 ++
.../kbn-es-query/src/es_query/es_query_sql.ts | 37 +
packages/kbn-es-query/src/es_query/index.ts | 6 +
.../src/filters/build_filters/types.ts | 6 +
packages/kbn-es-query/src/filters/index.ts | 1 +
packages/kbn-es-query/src/index.ts | 5 +
.../lib/dashboard_session_restoration.ts | 3 +-
.../lib/sync_dashboard_filter_state.ts | 3 +-
src/plugins/data/common/query/query_state.ts | 4 +-
.../common/query/to_expression_ast.test.ts | 139 +-
.../data/common/query/to_expression_ast.ts | 42 +-
src/plugins/data/common/query/types.ts | 2 +-
.../aggregate_query_to_ast.test.ts | 25 +
.../expressions/aggregate_query_to_ast.ts | 20 +
.../data/common/search/expressions/essql.ts | 6 +-
.../data/common/search/expressions/index.ts | 1 +
.../search/expressions/kibana_context.ts | 9 +-
.../search/expressions/kibana_context_type.ts | 4 +-
.../search_source/migrate_legacy_query.ts | 11 +-
.../data/common/search/search_source/types.ts | 6 +-
.../query_string/query_string_manager.test.ts | 4 +-
.../query_string/query_string_manager.ts | 17 +-
.../query/route_handler_context.test.ts | 14 +-
.../server/query/route_handler_context.ts | 15 +-
src/plugins/discover/common/index.ts | 1 +
src/plugins/discover/kibana.json | 3 +-
.../get_index_pattern_mock.tsx | 0
.../with_discover_services.tsx | 139 ++
.../discover/public/__mocks__/services.ts | 5 +-
.../application/context/context_app.test.tsx | 4 +-
.../application/context/context_app.tsx | 2 +-
.../document_explorer_update_callout.test.tsx | 2 +-
.../field_stats_table/field_stats_table.tsx | 6 +-
.../__stories__/discover_layout.stories.tsx | 74 +-
.../layout/__stories__/get_layout_props.ts | 192 ++-
.../layout/__stories__/get_services.tsx | 126 --
.../components/layout/discover_documents.tsx | 33 +-
.../layout/discover_layout.test.tsx | 20 +-
.../components/layout/discover_layout.tsx | 64 +-
.../main/components/layout/types.ts | 7 +-
.../sidebar/discover_field.test.tsx | 21 +-
.../components/sidebar/discover_field.tsx | 57 +-
.../sidebar/discover_field_bucket.tsx | 4 +-
.../sidebar/discover_field_details.tsx | 4 +-
.../sidebar/discover_field_search.test.tsx | 1 +
.../sidebar/discover_field_search.tsx | 158 +--
.../components/sidebar/discover_sidebar.tsx | 30 +-
.../discover_sidebar_responsive.test.tsx | 25 +-
.../sidebar/discover_sidebar_responsive.tsx | 30 +-
.../top_nav/discover_topnav.test.tsx | 1 +
.../components/top_nav/discover_topnav.tsx | 57 +-
.../top_nav/get_top_nav_links.test.ts | 56 +
.../components/top_nav/get_top_nav_links.tsx | 8 +-
.../components/top_nav/on_save_search.tsx | 35 +-
.../main/hooks/use_discover_state.ts | 64 +-
.../main/hooks/use_saved_search.test.ts | 31 +-
.../main/hooks/use_saved_search.ts | 33 +-
.../hooks/use_saved_search_messages.test.ts | 9 +-
.../main/hooks/use_saved_search_messages.ts | 18 +-
.../main/services/discover_state.ts | 3 +-
.../main/utils/cleanup_url_state.ts | 8 +-
.../application/main/utils/fetch_all.test.ts | 106 +-
.../application/main/utils/fetch_all.ts | 69 +-
.../main/utils/fetch_documents.test.ts | 8 +-
.../application/main/utils/fetch_documents.ts | 6 +-
.../application/main/utils/fetch_sql.ts | 72 ++
.../main/utils/get_raw_record_type.test.ts | 23 +
.../main/utils/get_raw_record_type.ts | 27 +
.../main/utils/get_result_state.ts | 7 +-
.../get_switch_index_pattern_app_state.ts | 11 +-
.../main/utils/persist_saved_search.ts | 9 +-
src/plugins/discover/public/build_services.ts | 3 +
.../discover_grid/discover_grid.scss | 9 +-
.../discover_grid/discover_grid.tsx | 21 +-
.../discover_grid_cell_actions.tsx | 7 +-
.../discover_grid_columns.test.tsx | 3 +
.../discover_grid/discover_grid_columns.tsx | 75 +-
.../discover_grid/discover_grid_context.tsx | 4 +-
.../discover_grid_expand_button.tsx | 5 +-
.../discover_grid/discover_grid_flyout.tsx | 22 +-
.../discover_tour/discover_tour.test.tsx | 4 +-
.../discover_tour/discover_tour_provider.tsx | 131 +-
src/plugins/discover/public/locator.ts | 6 +-
src/plugins/discover/public/plugin.tsx | 3 +
.../saved_searches/get_saved_searches.test.ts | 91 ++
.../save_saved_searches.test.ts | 2 +
.../saved_searches_utils.test.ts | 4 +
.../saved_searches/saved_searches_utils.ts | 2 +
.../public/services/saved_searches/types.ts | 2 +
.../discover/server/saved_objects/search.ts | 1 +
src/plugins/discover/server/ui_settings.ts | 18 +
src/plugins/discover/tsconfig.json | 1 +
.../server/collectors/management/schema.ts | 4 +
.../server/collectors/management/types.ts | 1 +
src/plugins/navigation/public/mocks.ts | 1 +
src/plugins/navigation/public/plugin.ts | 1 +
.../top_nav_menu/create_top_nav_menu.tsx | 3 +-
.../public/top_nav_menu/top_nav_menu.test.tsx | 3 +-
.../public/top_nav_menu/top_nav_menu.tsx | 74 +-
src/plugins/navigation/public/types.ts | 4 +-
src/plugins/telemetry/schema/oss_plugins.json | 6 +
.../public/__stories__/search_bar.stories.tsx | 81 +-
.../dataview_picker/change_dataview.test.tsx | 24 +-
.../dataview_picker/change_dataview.tsx | 316 ++++-
.../dataview_picker/dataview_list.test.tsx | 7 +
.../public/dataview_picker/dataview_list.tsx | 40 +-
.../public/dataview_picker/index.tsx | 67 +-
.../text_languages_list.test.tsx | 63 +
.../dataview_picker/text_languages_list.tsx | 67 +
.../text_languages_transition_modal.tsx | 129 ++
src/plugins/unified_search/public/index.ts | 2 +
src/plugins/unified_search/public/mocks.ts | 1 +
src/plugins/unified_search/public/plugin.ts | 1 +
.../public/query_string_input/index.tsx | 8 +-
.../query_bar_top_row.test.tsx | 23 +-
.../query_string_input/query_bar_top_row.tsx | 95 +-
.../text_based_languages_editor/README.md | 46 +
.../documentation.scss | 151 +++
.../documentation.tsx | 203 +++
.../editor_footer.tsx | 153 +++
.../helpers.test.ts | 105 ++
.../text_based_languages_editor/helpers.ts | 131 ++
.../text_based_languages_editor/index.tsx | 513 ++++++++
.../sql_documentation_sections.tsx | 1127 +++++++++++++++++
.../text_based_languages_editor.styles.ts | 84 ++
.../text_based_languages_editor.test.tsx | 153 +++
.../public/search_bar/create_search_bar.tsx | 43 +-
.../public/search_bar/index.tsx | 11 +-
.../lib/use_query_string_manager.ts | 30 +-
.../public/search_bar/search_bar.test.tsx | 18 +
.../public/search_bar/search_bar.tsx | 120 +-
src/plugins/unified_search/public/types.ts | 6 +-
.../utils/use/use_editor_updates.ts | 6 +-
.../utils/use/use_linked_search_updates.ts | 4 +-
.../public/visualize_app/utils/utils.ts | 4 +-
.../search_selection/search_selection.tsx | 10 +
test/functional/apps/discover/_sql_view.ts | 93 ++
test/functional/apps/discover/index.ts | 1 +
test/functional/page_objects/discover_page.ts | 8 +
.../kbn_top_nav/public/application.tsx | 7 +-
.../public/pages/findings/utils.ts | 11 +-
.../lens/public/app_plugin/app.test.tsx | 5 +-
.../lens/public/embeddable/embeddable.tsx | 2 +-
.../public/state_management/lens_slice.ts | 3 +-
.../region_map/region_map_fn.ts | 3 +-
.../tile_map/tile_map_fn.ts | 3 +-
.../visualize_geo_field_action.ts | 3 +-
.../search_source_expression_form.tsx | 2 +-
150 files changed, 5697 insertions(+), 936 deletions(-)
create mode 100644 packages/kbn-es-query/src/es_query/es_query_sql.test.ts
create mode 100644 packages/kbn-es-query/src/es_query/es_query_sql.ts
create mode 100644 src/plugins/data/common/search/expressions/aggregate_query_to_ast.test.ts
create mode 100644 src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
rename src/plugins/discover/public/{application/main/components/layout/__stories__ => __mocks__/__storybook_mocks__}/get_index_pattern_mock.tsx (100%)
create mode 100644 src/plugins/discover/public/__mocks__/__storybook_mocks__/with_discover_services.tsx
delete mode 100644 src/plugins/discover/public/application/main/components/layout/__stories__/get_services.tsx
create mode 100644 src/plugins/discover/public/application/main/utils/fetch_sql.ts
create mode 100644 src/plugins/discover/public/application/main/utils/get_raw_record_type.test.ts
create mode 100644 src/plugins/discover/public/application/main/utils/get_raw_record_type.ts
create mode 100644 src/plugins/unified_search/public/dataview_picker/text_languages_list.test.tsx
create mode 100644 src/plugins/unified_search/public/dataview_picker/text_languages_list.tsx
create mode 100644 src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/README.md
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.scss
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/documentation.tsx
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/editor_footer.tsx
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.test.ts
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.ts
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/sql_documentation_sections.tsx
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.styles.ts
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.test.tsx
create mode 100644 test/functional/apps/discover/_sql_view.ts
diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc
index 5ed2d734a81c2..2b64d02d0fb69 100644
--- a/docs/management/advanced-options.asciidoc
+++ b/docs/management/advanced-options.asciidoc
@@ -316,6 +316,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..4428541413e78 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,7 +11,8 @@ 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';
@@ -44,7 +45,7 @@ function removeMatchAll(filters: T[]) {
*/
export function buildEsQuery(
indexPattern: DataViewBase | undefined,
- queries: Query | Query[],
+ queries: Query | AggregateQuery | Array,
filters: Filter | Filter[],
config: EsQueryConfig = {
allowLeadingWildcards: false,
@@ -55,7 +56,8 @@ export function buildEsQuery(
queries = Array.isArray(queries) ? queries : [queries];
filters = Array.isArray(filters) ? filters : [filters];
- const validQueries = queries.filter((query) => has(query, 'query'));
+ const isOfQueryTypeQueries = queries.filter(isOfQueryType);
+ const validQueries = isOfQueryTypeQueries.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..2747411769c98
--- /dev/null
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.test.ts
@@ -0,0 +1,79 @@
+/*
+ * 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!');
+ });
+ });
+});
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..2b604d62fd269
--- /dev/null
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.ts
@@ -0,0 +1,37 @@
+/*
+ * 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';
+
+// 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): query is AggregateQuery {
+ return Boolean(query && 'sql' in query);
+}
+
+// returns the language of the aggregate Query, sql, esql etc
+export function getAggregateQueryMode(query: AggregateQuery): string {
+ return Object.keys(query)[0];
+}
+
+// retrieves the index pattern from the aggregate query
+export function getIndexPatternFromSQLQuery(sqlQuery?: string): string {
+ const sql = sqlQuery?.replaceAll('"', '').replaceAll("'", '');
+ // 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 3f6d586feed98..7e91ff251a5fe 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,12 @@ export type Query = {
language: string;
};
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+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..7a72359535a2a 100644
--- a/src/plugins/data/common/query/to_expression_ast.test.ts
+++ b/src/plugins/data/common/query/to_expression_ast.test.ts
@@ -5,70 +5,103 @@
* 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 [
+ "chain": Array [
Object {
- "addArgument": [Function],
"arguments": Object {},
- "getArgument": [Function],
- "name": "kibana",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
+ "function": "kibana",
+ "type": "function",
},
Object {
- "addArgument": [Function],
"arguments": Object {
- "filters": Array [],
- "q": Array [
+ "timeRange": Array [
Object {
- "findFunction": [Function],
- "functions": Array [
+ "chain": Array [
Object {
- "addArgument": [Function],
"arguments": Object {
- "q": Array [
- "\\"\\"",
+ "from": Array [
+ "now",
+ ],
+ "to": Array [
+ "now+7d",
],
},
- "getArgument": [Function],
- "name": "lucene",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
+ "function": "timerange",
+ "type": "function",
},
],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_builder",
+ "type": "expression",
},
],
+ },
+ "function": "kibana_context",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ }
+ `);
+ });
+
+ it('returns an object with the correct structure for an SQL query', async () => {
+ const dataViewsService = {
+ getIdsWithTitle: jest.fn(() => {
+ return [
+ {
+ title: 'foo',
+ id: 'bar',
+ },
+ ];
+ }),
+ 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).toMatchInlineSnapshot(`
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {},
+ "function": "kibana",
+ "type": "function",
+ },
+ Object {
+ "arguments": Object {
"timeRange": Array [
Object {
- "findFunction": [Function],
- "functions": Array [
+ "chain": Array [
Object {
- "addArgument": [Function],
"arguments": Object {
"from": Array [
"now",
@@ -77,33 +110,31 @@ describe('queryStateToExpressionAst', () => {
"now+7d",
],
},
- "getArgument": [Function],
- "name": "timerange",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
+ "function": "timerange",
+ "type": "function",
},
],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_builder",
+ "type": "expression",
},
],
},
- "getArgument": [Function],
- "name": "kibana_context",
- "removeArgument": [Function],
- "replaceArgument": [Function],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_function_builder",
+ "function": "kibana_context",
+ "type": "function",
+ },
+ Object {
+ "arguments": Object {
+ "query": Array [
+ "SELECT * FROM foo",
+ ],
+ "timeField": Array [
+ "baz",
+ ],
+ },
+ "function": "essql",
+ "type": "function",
},
],
- "toAst": [Function],
- "toString": [Function],
- "type": "expression_builder",
+ "type": "expression",
}
`);
});
diff --git a/src/plugins/data/common/query/to_expression_ast.ts b/src/plugins/data/common/query/to_expression_ast.ts
index 929a7cc928cdd..23b23aec03aa2 100644
--- a/src/plugins/data/common/query/to_expression_ast.ts
+++ b/src/plugins/data/common/query/to_expression_ast.ts
@@ -5,31 +5,57 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
-
+import {
+ isOfAggregateQueryType,
+ getAggregateQueryMode,
+ getIndexPatternFromSQLQuery,
+} 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,
- queryToAst,
+ aggregateQueryToAst,
timerangeToAst,
} from '..';
+interface Args extends QueryState {
+ dataViewsService: DataViewsContract;
+}
+
/**
* 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, time, dataViewsService }: Args) {
const kibana = buildExpressionFunction('kibana', {});
const kibanaContext = buildExpressionFunction('kibana_context', {
- q: query && queryToAst(query),
- filters: filters && filtersToAst(filters),
timeRange: time && timerangeToAst(time),
});
+ const ast = buildExpression([kibana, kibanaContext]).toAst();
+
+ if (query && isOfAggregateQueryType(query)) {
+ const mode = getAggregateQueryMode(query);
+ // sql query
+ if (mode === 'sql') {
+ 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..8c62021c16e57
--- /dev/null
+++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
@@ -0,0 +1,20 @@
+/*
+ * 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 } from '@kbn/expressions-plugin/common';
+import { AggregateQuery } from '../../query';
+import { EssqlExpressionFunctionDefinition } from './essql';
+
+export const aggregateQueryToAst = (query: AggregateQuery, timeField?: string) => {
+ if (query.sql) {
+ 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/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 b5f2238430d02..40f2a520a30d7 100644
--- a/src/plugins/discover/common/index.ts
+++ b/src/plugins/discover/common/index.ts
@@ -27,3 +27,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 d6b15da2e3275..bb0535ab866e8 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,
@@ -24,6 +25,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(),
@@ -92,7 +94,7 @@ export const discoverServiceMock = {
},
},
navigation: {
- ui: { TopNavMenu },
+ ui: { TopNavMenu, AggregateQueryTopNavMenu: TopNavMenu },
},
metadata: {
branch: 'test',
@@ -107,4 +109,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 b0e12b3bcd7d2..a96840635769c 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/__stories__/get_services.tsx b/src/plugins/discover/public/application/main/components/layout/__stories__/get_services.tsx
deleted file mode 100644
index 9caffb1f5921b..0000000000000
--- a/src/plugins/discover/public/application/main/components/layout/__stories__/get_services.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * 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 { action } from '@storybook/addon-actions';
-import { TopNavMenu } from '@kbn/navigation-plugin/public';
-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 { IUiSettingsClient } 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 '../discover_layout';
-import { LocalStorageMock } from '../../../../../__mocks__/local_storage_mock';
-import { DiscoverServices } from '../../../../../build_services';
-
-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;
-
-export function getServices() {
- return {
- 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' };
- },
- },
- },
- },
- dataViews: {
- getIdsWithTitle: () => Promise.resolve([]),
- },
- },
- uiSettings: uiSettingsMock,
- dataViewFieldEditor: {
- openEditor: () => void 0,
- userPermissions: {
- editIndexPattern: () => void 0,
- },
- },
- navigation: {
- ui: { TopNavMenu },
- },
- 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;
-}
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 e7d94b9856fab..6343fd2424eb8 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,7 +57,7 @@ function DiscoverDocumentsComponent({
expandedDoc?: DataTableRecord;
indexPattern: DataView;
navigateTo: (url: string) => void;
- onAddFilter: DocViewFilterFn;
+ onAddFilter?: DocViewFilterFn;
savedSearch: SavedSearch;
setExpandedDoc: (doc?: DataTableRecord) => void;
state: AppState;
@@ -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({
@@ -112,8 +116,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 (
@@ -153,7 +160,7 @@ function DiscoverDocumentsComponent({
onFilter={onAddFilter as DocViewFilterFn}
onMoveColumn={onMoveColumn}
onRemoveColumn={onRemoveColumn}
- onSort={onSort}
+ onSort={!isPlainRecord ? onSort : undefined}
useNewFieldsApi={useNewFieldsApi}
dataTestSubj="discoverDocTable"
/>
@@ -162,7 +169,7 @@ function DiscoverDocumentsComponent({
{!isLegacy && (
<>
{!hideAnnouncements && (
-
+
)}
@@ -178,18 +185,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}
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 4aff6e2a78070..252041a0b7e78 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..6d321778460e3 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,28 +334,32 @@ 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 f381f87c7389d..46ccab3b93a7b 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..40aedaac23e79 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;
+ isPlainRecord: boolean;
}
const MultiFields: React.FC = memo(
- ({ multiFields, toggleDisplay, alwaysShowActionButton }) => (
+ ({ multiFields, toggleDisplay, alwaysShowActionButton, isPlainRecord }) => (
@@ -186,7 +187,7 @@ const MultiFields: React.FC = memo(
className="dscSidebarItem dscSidebarItem--multi"
isActive={false}
dataTestSubj={`field-${entry.field.name}-showDetails`}
- fieldIcon={ }
+ fieldIcon={isPlainRecord && }
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 isPlainRecord = !!onAddFilter;
const toggleDisplay = useCallback(
(f: DataViewField) => {
@@ -304,7 +306,7 @@ function DiscoverFieldComponent({
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={ }
+ fieldIcon={isPlainRecord && }
fieldAction={
);
+ const button = (
+ }
+ fieldAction={
+
+ }
+ fieldName={ }
+ fieldInfoIcon={field.type === 'conflict' && }
+ />
+ );
+ if (!isPlainRecord) {
+ return button;
+ }
+
const renderPopover = () => {
const details = getDetails(field);
return (
@@ -398,6 +424,7 @@ function DiscoverFieldComponent({
multiFields={multiFields}
alwaysShowActionButton={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
+ isPlainRecord={isPlainRecord}
/>
>
)}
@@ -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..b3c6436bfdfc5 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 SQLModeIsEnabled = uiSettings.get(ENABLE_SQL);
+ const supportedTextBasedLanguages = [];
+ if (SQLModeIsEnabled) {
+ 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 }) => {
+ await onSaveSearch({
+ savedSearch,
+ services,
+ indexPattern,
+ navigateTo,
+ state: stateContainer,
+ onClose: onSave,
+ 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..42741d18b3950 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,13 @@ export const getTopNavLinks = ({
...(services.capabilities.advancedSettings.save ? [options] : []),
newSearch,
openSearch,
+ ...(!isPlainRecord ? [shareSearch] : []),
...(services.triggersActionsUi &&
- services.capabilities.management?.insightsAndAlerting?.triggersActions
+ !isPlainRecord &&
+ 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 6263df7814e2b..f8a16a166aa0c 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
@@ -23,6 +23,7 @@ async function saveDataSource({
saveOptions,
services,
state,
+ navigateOrReloadSavedSearch,
}: {
indexPattern: DataView;
navigateTo: (url: string) => void;
@@ -30,6 +31,7 @@ async function saveDataSource({
saveOptions: SaveSavedSearchOptions;
services: DiscoverServices;
state: GetStateReturn;
+ navigateOrReloadSavedSearch: boolean;
}) {
const prevSavedSearchId = savedSearch.id;
function onSuccess(id: string) {
@@ -43,20 +45,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
+ );
+ }
}
}
}
@@ -89,6 +93,7 @@ export async function onSaveSearch({
services,
state,
onClose,
+ onSaveCb,
}: {
indexPattern: DataView;
navigateTo: (path: string) => void;
@@ -96,6 +101,7 @@ export async function onSaveSearch({
services: DiscoverServices;
state: GetStateReturn;
onClose?: () => void;
+ onSaveCb?: () => void;
}) {
const onSave = async ({
newTitle,
@@ -118,6 +124,7 @@ export async function onSaveSearch({
copyOnSave: newCopyOnSave,
isTitleDuplicateConfirmed,
};
+ const navigateOrReloadSavedSearch = !Boolean(onSaveCb);
const response = await saveDataSource({
indexPattern,
saveOptions,
@@ -125,6 +132,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) {
@@ -132,6 +140,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..4d570a608d6bf 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
@@ -8,12 +8,18 @@
import { useMemo, useEffect, useState, useCallback } from 'react';
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 +27,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 +78,8 @@ export function useDiscoverState({
const { appStateContainer } = stateContainer;
const [state, setState] = useState(appStateContainer.getState());
+ const [documentStateCols, setDocumentStateCols] = useState([]);
+ const [sqlQuery] = useState(state.query);
/**
* Search session logic
@@ -99,6 +110,8 @@ export function useDiscoverState({
useNewFieldsApi,
});
+ const documentState: DataDocumentsMsg = useDataState(data$.documents$);
+
/**
* Reset to display loading spinner when savedSearch is changing
*/
@@ -196,13 +209,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
@@ -226,6 +249,41 @@ export function useDiscoverState({
}
}, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]);
+ const fetchResults = useCallback(() => {
+ if (documentState.result?.length) {
+ 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.result, documentStateCols, sqlQuery, state.query]);
+
+ useEffect(() => {
+ async function fetchDataview() {
+ if (state.query && isOfAggregateQueryType(state.query)) {
+ const indexPatternFROMQuery = getIndexPatternFromSQLQuery(state.query.sql);
+ const idsTitles = await indexPatterns.getIdsWithTitle();
+ const dataViewObj = idsTitles.find(({ title }) => title === indexPatternFROMQuery);
+ if (dataViewObj) {
+ const columns = fetchResults();
+ if (columns.length) {
+ setDocumentStateCols(columns);
+ }
+ const nextState = {
+ index: dataViewObj.id,
+ ...(columns.length && { columns }),
+ };
+ stateContainer.setAppState(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 86d279ec09c20..b51a7e918a7c9 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 0148931f73c58..b302cd555fbcb 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 e33d931c571da..904f0756bc55b 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 655027dddbf1e..c1e985bcdfec1 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';
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 5796c488dd8fb..2a7cabb8f30e2 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..bada02de40442
--- /dev/null
+++ b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
@@ -0,0 +1,72 @@
+/*
+ * 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 } 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
+) {
+ const timeRange = data.query.timefilter.timefilter.getTime();
+ return queryStateToExpressionAst({
+ query,
+ time: timeRange,
+ dataViewsService,
+ })
+ .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..d9f04038d8183 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,12 @@ 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
+ if (state.query && isOfAggregateQueryType(state.query)) {
+ savedSearch.isTextBasedQuery = true;
+ }
+
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..011a180177972 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: 4px;
+ }
+
.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 857910e11314e..da019553fa088 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid.tsx
@@ -121,7 +121,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)
*/
@@ -166,6 +166,10 @@ export interface DiscoverGridProps {
* Update row height state
*/
onUpdateRowHeight?: (rowHeight: number) => void;
+ /**
+ * Is text base lang mode enabled
+ */
+ isPlainRecord?: boolean;
/**
* Callback to execute on edit runtime field
*/
@@ -203,6 +207,7 @@ export const DiscoverGrid = ({
className,
rowHeightState,
onUpdateRowHeight,
+ isPlainRecord = false,
onFieldEdited,
}: DiscoverGridProps) => {
const dataGridRef = useRef(null);
@@ -364,9 +369,11 @@ export const DiscoverGrid = ({
isSortEnabled,
services,
valueToStringConverter,
+ onFilter,
editField,
}),
[
+ onFilter,
displayedColumns,
displayedRows,
indexPattern,
@@ -401,8 +408,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(
@@ -512,7 +519,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/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,
"searchSource": Object {
"create": [MockFunction],
@@ -146,4 +147,94 @@ 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,
+ "searchSource": Object {
+ "create": [MockFunction],
+ "createChild": [MockFunction],
+ "createCopy": [MockFunction],
+ "destroy": [MockFunction],
+ "fetch": [MockFunction],
+ "fetch$": [MockFunction],
+ "getField": [MockFunction],
+ "getFields": [MockFunction],
+ "getId": [MockFunction],
+ "getOwnField": [MockFunction],
+ "getParent": [MockFunction],
+ "getSearchRequestBody": [MockFunction],
+ "getSerializedFields": [MockFunction],
+ "history": Array [],
+ "onRequestStart": [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 f0958737d3b79..39fd5442512d1 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,
"searchSource": SearchSource {
"dependencies": Object {
@@ -103,6 +105,7 @@ describe('saved_searches_utils', () => {
description: 'description',
grid: {},
hideChart: true,
+ isTextBasedQuery: true,
};
expect(toSavedSearchAttributes(savedSearch, '{}')).toMatchInlineSnapshot(`
@@ -115,6 +118,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 26b3c0b7cf9b5..a2fbc1f575e4b 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,
});
export const toSavedSearchAttributes = (
@@ -61,4 +62,5 @@ export const toSavedSearchAttributes = (
viewMode: savedSearch.viewMode,
hideAggregatedPreview: savedSearch.hideAggregatedPreview,
rowHeight: savedSearch.rowHeight,
+ isTextBasedQuery: savedSearch.isTextBasedQuery ?? false,
});
diff --git a/src/plugins/discover/public/services/saved_searches/types.ts b/src/plugins/discover/public/services/saved_searches/types.ts
index aba95f85afd11..800c48993f39f 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;
};
@@ -53,4 +54,5 @@ export interface SavedSearch {
viewMode?: VIEW_MODE;
hideAggregatedPreview?: boolean;
rowHeight?: number;
+ isTextBasedQuery?: boolean;
}
diff --git a/src/plugins/discover/server/saved_objects/search.ts b/src/plugins/discover/server/saved_objects/search.ts
index f30fa932aa133..833e6fd0961ef 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 46e2d4d9969c6..c0339af400db0 100644
--- a/src/plugins/discover/server/ui_settings.ts
+++ b/src/plugins/discover/server/ui_settings.ts
@@ -29,8 +29,13 @@ import {
TRUNCATE_MAX_HEIGHT,
SHOW_FIELD_STATISTICS,
ROW_HEIGHT_OPTION,
+ ENABLE_SQL,
} from '../common';
+const technicalPreviewLabel = i18n.translate('discover.advancedSettings.technicalPreviewLabel', {
+ defaultMessage: 'technical preview',
+});
+
export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record = (
docLinks: DocLinksServiceSetup
) => ({
@@ -288,4 +293,17 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record[${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 653319c91b5f9..5fdb45d9a4238 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts
@@ -482,4 +482,8 @@ export const stackManagementSchema: MakeSchemaFrom = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
+ 'discover:enableSql': {
+ type: 'boolean',
+ _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 de22ac0cecb8a..1332ac15b2246 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts
@@ -33,6 +33,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 0932c785b9d5d..f9ad56134e2cf 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -8490,6 +8490,12 @@
"_meta": {
"description": "Non-default value of setting."
}
+ },
+ "discover:enableSql": {
+ "type": "boolean",
+ "_meta": {
+ "description": "Non-default value of setting."
+ }
}
}
},
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 c217b8c52b06f..bf52e8900d4c4 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,24 @@ import {
EuiText,
EuiTourStep,
EuiContextMenuPanelProps,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
} 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 +60,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 +89,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 +118,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 +153,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 +192,7 @@ export function ChangeDataView({
const getPanelItems = () => {
const panelItems: EuiContextMenuPanelProps['items'] = [];
- if (onAddField) {
+ if (onAddField && !isTextBasedLangSelected) {
panelItems.push(
{
- onChangeDataView(newId);
- setPopoverIsOpen(false);
- }}
- currentDataViewId={currentDataViewId}
- selectableProps={selectableProps}
- searchListInputId={searchListInputId}
- />
+ <>
+ {onDataViewCreated && (
+
+
+
+
+ {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',
+ })}
+
+
+
+ )}
+
+ {
+ setSelectedDataViewId(newId);
+ setPopoverIsOpen(false);
+ if (isTextBasedLangSelected && !isTextLangTransitionModalDismissed) {
+ setIsTextLangTransitionModalVisible(true);
+ } 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)) {
+ onSaveTextLanguageQuery?.({
+ onSave: () => {
+ cleanup(shouldDismissModal);
+ },
+ });
+ } 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..7453c865f8814 100644
--- a/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/dataview_list.tsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { EuiSelectable, EuiSelectableProps, EuiPanel } from '@elastic/eui';
+import { EuiSelectable, EuiSelectableProps, EuiPanel, EuiIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import { DataViewListItem } from '@kbn/data-views-plugin/public';
@@ -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,21 @@ export function DataViewsList({
key: id,
label: name ? name : title,
value: id,
- checked: id === currentDataViewId ? 'on' : undefined,
+ checked: id === currentDataViewId && !Boolean(isTextBasedLangSelected) ? 'on' : undefined,
+ append: Boolean(isTextBasedLangSelected) ? (
+
+
+
+ ) : null,
}))}
onChange={(choices) => {
const choice = choices.find(({ checked }) => checked) as unknown as {
@@ -61,16 +77,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..c5ea9eb2db68e 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,72 @@ export type ChangeDataViewTriggerProps = EuiButtonProps & {
title?: string;
};
+export enum TextBasedLanguages {
+ SQL = 'SQL',
+ ESQL = 'ESQL',
+}
+
+export interface OnSaveTextLanguageQueryProps {
+ onSave: () => 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 }: 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 +93,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..ea5ba98ba5704
--- /dev/null
+++ b/src/plugins/unified_search/public/dataview_picker/text_languages_transition_modal.tsx
@@ -0,0 +1,129 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 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 { css } from '@emotion/react';
+
+import {
+ EuiModal,
+ EuiModalBody,
+ EuiModalFooter,
+ EuiModalHeader,
+ EuiModalHeaderTitle,
+ EuiButton,
+ EuiText,
+ EuiCheckbox,
+ EuiFlexItem,
+ EuiFlexGroup,
+ useEuiTheme,
+} from '@elastic/eui';
+
+export interface TextBasedLanguagesTransitionModalProps {
+ closeModal: (dismissFlag: boolean, needsSave?: boolean) => void;
+}
+// Needed for React.lazy
+// eslint-disable-next-line import/no-default-export
+export default function TextBasedLanguagesTransitionModal({
+ closeModal,
+}: TextBasedLanguagesTransitionModalProps) {
+ const [dismissModalChecked, setDismissModalChecked] = useState(false);
+ const onTransitionModalDismiss = useCallback((e) => {
+ setDismissModalChecked(e.target.checked);
+ }, []);
+
+ const { euiTheme } = useEuiTheme();
+ return (
+ closeModal(dismissModalChecked)} style={{ width: 700 }}>
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle',
+ {
+ defaultMessage: 'Current text-based query will be cleared',
+ }
+ )}
+
+
+
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody',
+ {
+ defaultMessage:
+ 'The current text-based language query will be cleared when switching to a specific data view. To ensure that no work is inadvertently lost in the transition, it is recommended that you save this search before switching.',
+ }
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ closeModal(dismissModalChecked)}
+ color="warning"
+ iconType="merge"
+ css={css`
+ color: ${euiTheme.colors.warning};
+ border: 1px solid ${euiTheme.colors.warning};
+ background-color: ${euiTheme.colors.emptyShade};
+ `}
+ >
+ {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 bc7974b42efb3..3226004f953a1 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 } from './actions';
diff --git a/src/plugins/unified_search/public/mocks.ts b/src/plugins/unified_search/public/mocks.ts
index e119c58e89f3f..c674647eb7287 100644
--- a/src/plugins/unified_search/public/mocks.ts
+++ b/src/plugins/unified_search/public/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 59c327449d354..47cc5f6aae0c8 100755
--- a/src/plugins/unified_search/public/plugin.ts
+++ b/src/plugins/unified_search/public/plugin.ts
@@ -82,6 +82,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 (
+
+
+
+ {
+ setSelectedSection(helpGroup.label);
+ }}
+ >
+ {helpGroup.label}
+
+
+
+
+ {helpGroup.items.length ? (
+ <>
+
+
+
+ {helpGroup.items.map((helpItem) => {
+ return (
+ {helpItem.label}
+ }
+ size="s"
+ onClick={() => {
+ setSelectedSection(helpItem.label);
+ }}
+ />
+ );
+ })}
+
+ >
+ ) : null}
+
+ );
+ })}
+
+
+
+
+
+ {
+ 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..4d90f68680bad
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/editor_footer.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, { memo, useState } from 'react';
+
+import { i18n } from '@kbn/i18n';
+import {
+ EuiBadge,
+ 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(true)}
+ >
+
+ {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..372af2898f8de
--- /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: 44,
+ 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: 39,
+ 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..f83b8a799f994
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/helpers.ts
@@ -0,0 +1,131 @@
+/*
+ * 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);
+ // 5 is the length of FROM + space
+ const errorLength = 5 + dataviewString.length;
+ // 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..732554f29983a
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/index.tsx
@@ -0,0 +1,513 @@
+/*
+ * 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 { 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,
+} 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';
+
+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) as 'sql' | 'esql';
+ const queryString = 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 [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)
+ );
+ const editorModel = useRef();
+ const editor1 = useRef();
+ const containerRef = useRef(null);
+
+ // 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 = () => {
+ 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);
+ 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 text = hasLines ? queryString.split(/\r|\n/)[0] : 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 = hasLines ? `${text}...` : text;
+ setCodeOneLiner(shortedCode);
+ }
+ }
+ },
+ [queryString, errors, isCompactFocused]
+ );
+
+ useEffect(() => {
+ if (editor1.current) {
+ const editorElement = editor1.current.getDomNode();
+ if (editorElement) {
+ const contentWidth = Number(editorElement?.style.width.replace('px', ''));
+ if (code !== queryString) {
+ setCode(queryString);
+ calculateVisibleCode(contentWidth);
+ }
+ }
+ }
+ }, [calculateVisibleCode, code, 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 });
+ },
+ [language, onTextLangQueryChange]
+ );
+
+ useEffect(() => {
+ async function getDocumentation() {
+ const sections = await getDocumentationSections(language);
+ setDocumentationSections(sections);
+ }
+
+ getDocumentation();
+ }, [language]);
+
+ const codeEditorOptions: CodeEditorProps['options'] = {
+ automaticLayout: false,
+ folding: false,
+ fontSize: 14,
+ scrollBeyondLastLine: false,
+ quickSuggestions: true,
+ minimap: { enabled: false },
+ wordWrap: isWordWrapped ? 'on' : 'off',
+ lineNumbers: showLineNumbers ? 'on' : 'off',
+ lineDecorationsWidth: 16,
+ accessibilitySupport: 'off',
+ autoIndent: 'none',
+ wrappingIndent: 'none',
+ overviewRulerLanes: 0,
+ hideCursorInOverviewRuler: true,
+ scrollbar: {
+ vertical: 'auto',
+ 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={
+ setIsHelpOpen(true)}
+ />
+ }
+ >
+
+
+
+
+
+
+ )}
+
+
+ {(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 }}
+ />
+
+
+ setIsHelpOpen(false)}
+ ownFocus={false}
+ button={
+ setIsHelpOpen(true)}
+ css={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
+ />
+ }
+ >
+
+
+
+
+
+ )}
+
+ {isCodeEditorExpanded && (
+
+ )}
+ {isCodeEditorExpanded && (
+
+ {!userDrags && (
+
+ )}
+ {userDrags &&
}
+
+ )}
+ >
+ );
+
+ return editorPanel;
+});
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..ead231afe55bb
--- /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..65569a4eff904
--- /dev/null
+++ b/src/plugins/unified_search/public/query_string_input/text_based_languages_editor/text_based_languages_editor.styles.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 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
+) => {
+ 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',
+ },
+ resizableContainer: {
+ display: 'flex',
+ width: isCodeEditorExpanded ? '100%' : 'calc(100% - 80px)',
+ alignItems: isCompactFocused ? 'flex-start' : 'center',
+ border: !isCompactFocused ? euiTheme.border.thin : 'none',
+ borderBottomColor: hasErrors ? euiTheme.colors.danger : euiTheme.colors.lightShade,
+ },
+ linesBadge: {
+ position: 'absolute' as 'absolute', // cast string to type 'absolute',
+ zIndex: 1,
+ right: hasErrors ? '64px' : '16px',
+ top: '50%',
+ transform: 'translate(0, -50%)',
+ },
+ errorsBadge: {
+ position: 'absolute' as 'absolute', // cast string to type 'absolute',
+ zIndex: 1,
+ right: '16px',
+ top: '50%',
+ transform: 'translate(0, -50%)',
+ },
+ bottomContainer: {
+ border: euiTheme.border.thin,
+ borderTop: isCodeEditorExpanded ? 'none' : `1px solid ${bottomContainerBorderColor}`,
+ backgroundColor: euiTheme.colors.lightestShade,
+ paddingLeft: euiTheme.size.s,
+ paddingRight: euiTheme.size.s,
+ position: 'relative' as 'relative', // cast string to type 'relative',
+ marginTop: 0,
+ marginLeft: 0,
+ },
+ topContainer: {
+ border: euiTheme.border.thin,
+ backgroundColor: euiTheme.colors.lightestShade,
+ paddingLeft: euiTheme.size.s,
+ paddingRight: euiTheme.size.s,
+ 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 803ca1fec57cd..f25b35bd2608c 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 55d33ec15b83b..8b965953826d3 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,14 @@
* 2.0.
*/
-import { buildEsQuery, type Filter, buildFilter, FILTERS, FilterStateStore } from '@kbn/es-query';
+import {
+ buildEsQuery,
+ type Filter,
+ buildFilter,
+ FILTERS,
+ FilterStateStore,
+ type Query,
+} from '@kbn/es-query';
import { EuiBasicTableProps, Pagination } from '@elastic/eui';
import { useCallback, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
@@ -51,7 +58,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 29022a25a5673..b249b9a0e2ba6 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/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,
From 5af2a49b9d1230e499888f979e300d3c1f0d8db9 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 20 Jul 2022 09:27:46 +0300
Subject: [PATCH 02/30] Fix types
---
.../search/search_source/search_source.ts | 8 +++-
.../dataview_picker/change_dataview.tsx | 39 +++++++++++++++----
.../public/dataview_picker/dataview_list.tsx | 16 +-------
3 files changed, 39 insertions(+), 24 deletions(-)
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/unified_search/public/dataview_picker/change_dataview.tsx b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
index 50fd8b67bbea5..905f0f28aeb69 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -25,6 +25,7 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
+ EuiToolTip,
} from '@elastic/eui';
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
import { IDataPluginServices } from '@kbn/data-plugin/public';
@@ -238,13 +239,37 @@ export function ChangeDataView({
`}
>
-
-
- {i18n.translate('unifiedSearch.query.queryBar.indexPattern.dataViewsLabel', {
- defaultMessage: 'Data views',
- })}
-
-
+
+
+ {Boolean(isTextBasedLangSelected) ? (
+
+
+
+ ) : null}
+
+
+
+
+ {i18n.translate('unifiedSearch.query.queryBar.indexPattern.dataViewsLabel', {
+ defaultMessage: 'Data views',
+ })}
+
+
+
+
-
-
- ) : null,
}))}
onChange={(choices) => {
const choice = choices.find(({ checked }) => checked) as unknown as {
From c1b5ca26b9a429e1174df805d46ce8fdc53c983f Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 20 Jul 2022 10:19:30 +0300
Subject: [PATCH 03/30] Fix jest test
---
.../components/layout/discover_documents.tsx | 2 +-
.../saved_searches/get_saved_searches.test.ts | 3 +
.../dataview_picker/change_dataview.tsx | 8 +-
.../text_languages_transition_modal.tsx | 15 +-
.../text_based_languages_editor/index.tsx | 172 ++++++++++++------
5 files changed, 135 insertions(+), 65 deletions(-)
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 43dcd5dbd66a8..f0cda46ea5e1a 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
@@ -175,7 +175,7 @@ function DiscoverDocumentsComponent({
)}
{!isLegacy && (
<>
- {!hideAnnouncements && (
+ {!hideAnnouncements && !isPlainRecord && (
diff --git a/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts
index e4aa1e703a33d..8ff4f6c1e9428 100644
--- a/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts
+++ b/src/plugins/discover/public/services/saved_searches/get_saved_searches.test.ts
@@ -199,6 +199,7 @@ describe('getSavedSearch', () => {
"id": "ccf1af80-2297-11ec-86e0-1155ffb9c7a7",
"isTextBasedQuery": true,
"rowHeight": undefined,
+ "rowsPerPage": undefined,
"searchSource": Object {
"create": [MockFunction],
"createChild": [MockFunction],
@@ -206,6 +207,7 @@ describe('getSavedSearch', () => {
"destroy": [MockFunction],
"fetch": [MockFunction],
"fetch$": [MockFunction],
+ "getActiveIndexFilter": [MockFunction],
"getField": [MockFunction],
"getFields": [MockFunction],
"getId": [MockFunction],
@@ -215,6 +217,7 @@ describe('getSavedSearch', () => {
"getSerializedFields": [MockFunction],
"history": Array [],
"onRequestStart": [MockFunction],
+ "parseActiveIndexPatternFromQueryString": [MockFunction],
"removeField": [MockFunction],
"serialize": [MockFunction],
"setField": [MockFunction],
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 905f0f28aeb69..148b903ba9a5f 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -390,6 +390,7 @@ export function ChangeDataView({
const onModalClose = useCallback(
(shouldDismissModal: boolean, needsSave?: boolean) => {
if (Boolean(needsSave)) {
+ setIsTextLangTransitionModalVisible(false);
onSaveTextLanguageQuery?.({
onSave: () => {
cleanup(shouldDismissModal);
@@ -403,7 +404,12 @@ export function ChangeDataView({
);
if (isTextLangTransitionModalVisible && !isTextLangTransitionModalDismissed) {
- modal = ;
+ modal = (
+
+ );
}
return (
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
index ea5ba98ba5704..8aa9e4e29a468 100644
--- 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
@@ -21,25 +21,25 @@ import {
EuiCheckbox,
EuiFlexItem,
EuiFlexGroup,
- useEuiTheme,
} 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);
}, []);
- const { euiTheme } = useEuiTheme();
return (
- closeModal(dismissModalChecked)} style={{ width: 700 }}>
+ setIsTextLangTransitionModalVisible(false)} style={{ width: 700 }}>
@@ -70,7 +70,7 @@ export default function TextBasedLanguagesTransitionModal({
justify-content: space-between;
`}
>
-
+
-
+
closeModal(dismissModalChecked)}
color="warning"
iconType="merge"
- css={css`
- color: ${euiTheme.colors.warning};
- border: 1px solid ${euiTheme.colors.warning};
- background-color: ${euiTheme.colors.emptyShade};
- `}
>
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalCloseButton',
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
index 732554f29983a..06d77195a76ff 100644
--- 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
@@ -21,6 +21,7 @@ import {
EuiPopover,
EuiResizeObserver,
EuiOutsideClickDetector,
+ EuiToolTip,
} from '@elastic/eui';
import { CodeEditor } from '@kbn/kibana-react-plugin/public';
import type { CodeEditorProps } from '@kbn/kibana-react-plugin/public';
@@ -308,12 +309,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
responsive={false}
>
- {
- editor1.current?.updateOptions({
- wordWrap: isWordWrapped ? 'off' : 'on',
- });
- setIsWordWrapped(!isWordWrapped);
- }}
- />
+ >
+ {
+ editor1.current?.updateOptions({
+ wordWrap: isWordWrapped ? 'off' : 'on',
+ });
+ setIsWordWrapped(!isWordWrapped);
+ }}
+ />
+
- {
- expandCodeEditor(false);
- updateLinesFromModel = false;
- }}
- />
+ >
+ {
+ expandCodeEditor(false);
+ updateLinesFromModel = false;
+ }}
+ />
+
setIsHelpOpen(false)}
ownFocus={false}
button={
- setIsHelpOpen(true)}
- />
+ >
+ setIsHelpOpen(true)}
+ />
+
}
>
@@ -453,15 +496,25 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
- expandCodeEditor(true)}
- data-test-subj="unifiedTextLangEditor-expand"
- css={{ borderRadius: 0 }}
- />
+
+ expandCodeEditor(true)}
+ data-test-subj="unifiedTextLangEditor-expand"
+ css={{ borderRadius: 0 }}
+ />
+
setIsHelpOpen(false)}
ownFocus={false}
button={
- setIsHelpOpen(true)}
- css={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
- />
+
+ setIsHelpOpen(true)}
+ css={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
+ />
+
}
>
From 902dc58ef3e86fbd7c16a7bcfd546b529ecf2e0a Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 20 Jul 2022 13:41:55 +0300
Subject: [PATCH 04/30] More design fixes
---
.../editor_footer.tsx | 22 +++++++-----
.../text_based_languages_editor/index.tsx | 36 +++++++++++++------
.../text_based_languages_editor.styles.ts | 23 ++++++++----
3 files changed, 55 insertions(+), 26 deletions(-)
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
index 4d90f68680bad..c3ee6f7eafbec 100644
--- 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
@@ -10,7 +10,7 @@ import React, { memo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
- EuiBadge,
+ EuiCode,
EuiText,
EuiFlexGroup,
EuiFlexItem,
@@ -46,9 +46,9 @@ export const EditorFooter = memo(function EditorFooter({
responsive={false}
>
-
+
-
+
{i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.lineCount', {
defaultMessage: '{count} {count, plural, one {line} other {lines}}',
@@ -67,7 +67,7 @@ export const EditorFooter = memo(function EditorFooter({
setIsPopoverOpen(true)}
+ onClick={() => setIsPopoverOpen(!isPopoverOpen)}
>
{i18n.translate(
@@ -109,7 +109,7 @@ export const EditorFooter = memo(function EditorFooter({
-
+
{i18n.translate(
'unifiedSearch.query.textBasedLanguagesEditor.lineNumber',
{
@@ -133,9 +133,9 @@ export const EditorFooter = memo(function EditorFooter({
-
+
-
+
{i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.runQuery', {
defaultMessage: 'Run query',
@@ -144,7 +144,11 @@ export const EditorFooter = memo(function EditorFooter({
- {`${COMMAND_KEY} + Enter`}
+ {`${COMMAND_KEY} + Enter`}
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
index 06d77195a76ff..c0ae2c8d52834 100644
--- 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
@@ -215,7 +215,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
if (hasLines && !updateLinesFromModel) {
setLines(queryString.split(/\r|\n/).length);
}
- const text = hasLines ? queryString.split(/\r|\n/)[0] : queryString;
+ const trimmedText = queryString.replace(/\r?\n|\r/g, ' ');
+ const text = hasLines ? trimmedText : queryString;
const queryLength = text.length;
const unusedSpace =
errors && errors.length
@@ -226,7 +227,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const shortedCode = text.substring(0, charactersAlowed) + '...';
setCodeOneLiner(shortedCode);
} else {
- const shortedCode = hasLines ? `${text}...` : text;
+ const shortedCode = text;
setCodeOneLiner(shortedCode);
}
}
@@ -262,6 +263,10 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
[language, onTextLangQueryChange]
);
+ const toggleDocumentationPopover = useCallback(() => {
+ setIsHelpOpen(!isHelpOpen);
+ }, [isHelpOpen]);
+
useEffect(() => {
async function getDocumentation() {
const sections = await getDocumentationSections(language);
@@ -280,14 +285,14 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
minimap: { enabled: false },
wordWrap: isWordWrapped ? 'on' : 'off',
lineNumbers: showLineNumbers ? 'on' : 'off',
- lineDecorationsWidth: 16,
+ lineDecorationsWidth: 12,
accessibilitySupport: 'off',
autoIndent: 'none',
wrappingIndent: 'none',
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
scrollbar: {
- vertical: 'auto',
+ vertical: 'hidden',
horizontal: 'hidden',
},
overviewRulerBorder: false,
@@ -329,7 +334,6 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
>
setIsHelpOpen(true)}
+ onClick={toggleDocumentationPopover}
/>
}
@@ -506,13 +510,17 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
)}
>
expandCodeEditor(true)}
data-test-subj="unifiedTextLangEditor-expand"
- css={{ borderRadius: 0 }}
+ css={{
+ borderRadius: 0,
+ backgroundColor: '#e9edf3',
+ border: '1px solid rgb(17 43 134 / 10%) !important',
+ }}
/>
@@ -537,13 +545,19 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
)}
>
setIsHelpOpen(true)}
- css={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
+ onClick={toggleDocumentationPopover}
+ css={{
+ borderTopLeftRadius: 0,
+ borderBottomLeftRadius: 0,
+ backgroundColor: '#e9edf3',
+ border: '1px solid rgb(17 43 134 / 10%) !important',
+ borderLeft: 'transparent !important',
+ }}
/>
}
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
index 65569a4eff904..188b0f982e40d 100644
--- 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
@@ -30,6 +30,11 @@ export const textBasedLanguagedEditorStyles = (
zIndex: isCompactFocused ? 4 : 0,
height: `${editorHeight}px`,
border: isCompactFocused ? euiTheme.border.thin : 'none',
+ borderBottom: isCodeEditorExpanded
+ ? 'none'
+ : isCompactFocused
+ ? euiTheme.border.thin
+ : 'none',
},
resizableContainer: {
display: 'flex',
@@ -41,25 +46,30 @@ export const textBasedLanguagedEditorStyles = (
linesBadge: {
position: 'absolute' as 'absolute', // cast string to type 'absolute',
zIndex: 1,
- right: hasErrors ? '64px' : '16px',
+ right: hasErrors ? '60px' : '12px',
top: '50%',
transform: 'translate(0, -50%)',
},
errorsBadge: {
position: 'absolute' as 'absolute', // cast string to type 'absolute',
zIndex: 1,
- right: '16px',
+ right: '12px',
top: '50%',
transform: 'translate(0, -50%)',
},
bottomContainer: {
border: euiTheme.border.thin,
- borderTop: isCodeEditorExpanded ? 'none' : `1px solid ${bottomContainerBorderColor}`,
+ borderTop: isCodeEditorExpanded
+ ? euiTheme.border.thin
+ : `2px solid ${bottomContainerBorderColor}`,
backgroundColor: euiTheme.colors.lightestShade,
- paddingLeft: euiTheme.size.s,
- paddingRight: euiTheme.size.s,
+ 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,
+ marginTop: '1px',
marginLeft: 0,
},
topContainer: {
@@ -67,6 +77,7 @@ export const textBasedLanguagedEditorStyles = (
backgroundColor: euiTheme.colors.lightestShade,
paddingLeft: euiTheme.size.s,
paddingRight: euiTheme.size.s,
+ width: 'calc(100% + 2px)',
position: 'relative' as 'relative', // cast string to type 'relative',
marginLeft: 0,
marginTop: euiTheme.size.s,
From 0a2504bc83593e0ebee510e74f4b50c5aab2d4c5 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 20 Jul 2022 14:37:20 +0300
Subject: [PATCH 05/30] Update advanced setting description
---
src/plugins/discover/server/ui_settings.ts | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts
index 6fd629b3a0977..0419594d31d47 100644
--- a/src/plugins/discover/server/ui_settings.ts
+++ b/src/plugins/discover/server/ui_settings.ts
@@ -314,8 +314,17 @@ export const getUiSettings: (docLinks: DocLinksServiceSetup) => Record[${technicalPreviewLabel}]` },
+ defaultMessage:
+ '{technicalPreviewLabel} This tech preview feature is highly experimental--do not rely on this for production saved searches or dashboards. This setting enables SQL as a text-based query language in Discover. If you have feedback on this experience please reach out to us on {link}',
+ values: {
+ link:
+ `` +
+ i18n.translate('discover.advancedSettings.enableSQL.discussLinkText', {
+ defaultMessage: 'discuss.elastic.co/c/elastic-stack/kibana',
+ }) +
+ ' ',
+ technicalPreviewLabel: `[${technicalPreviewLabel}] `,
+ },
}),
requiresPageReload: true,
category: ['discover'],
From ab15187e9534afac8eba4329bf356ae092f853eb Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Wed, 20 Jul 2022 16:20:04 +0300
Subject: [PATCH 06/30] Further design changes
---
.../editor_footer.tsx | 30 ++++++++++++-------
.../text_based_languages_editor/index.tsx | 15 ++++++++--
.../overwrite.scss | 4 +++
.../text_based_languages_editor.styles.ts | 10 ++++---
4 files changed, 42 insertions(+), 17 deletions(-)
create mode 100644 src/plugins/unified_search/public/query_string_input/text_based_languages_editor/overwrite.scss
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
index c3ee6f7eafbec..b8ebe9718710f 100644
--- 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
@@ -105,18 +105,26 @@ export const EditorFooter = memo(function EditorFooter({
{errors.map((error, index) => {
return (
-
+
-
-
-
- {i18n.translate(
- 'unifiedSearch.query.textBasedLanguagesEditor.lineNumber',
- {
- defaultMessage: 'Line {lineNumber}',
- values: { lineNumber: error.startLineNumber },
- }
- )}
+
+
+
+
+
+ {i18n.translate(
+ 'unifiedSearch.query.textBasedLanguagesEditor.lineNumber',
+ {
+ defaultMessage: 'Line {lineNumber}',
+ values: { lineNumber: error.startLineNumber },
+ }
+ )}
+
+
{error.message}
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
index c0ae2c8d52834..e24060c6b726a 100644
--- 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
@@ -7,6 +7,7 @@
*/
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';
@@ -37,6 +38,8 @@ 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;
@@ -83,6 +86,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
);
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);
@@ -96,12 +100,17 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
isCompactFocused,
editorHeight,
isCodeEditorExpanded,
- Boolean(errors?.length)
+ Boolean(errors?.length),
+ isCodeEditorExpandedFocused
);
const editorModel = useRef();
const editor1 = useRef();
const containerRef = useRef(null);
+ const editorClassName = classNames('unifiedTextLangEditor', {
+ 'unifiedTextLangEditor--expanded': isCompactFocused,
+ });
+
// When the editor is on full size mode, the user can resize the height of the editor.
const onMouseDownResizeHandler = useCallback(
(mouseDownEvent: React.MouseEvent) => {
@@ -142,6 +151,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
};
const restoreInitialMode = () => {
+ setIsCodeEditorExpandedFocused(false);
if (isCodeEditorExpanded) return;
setEditorHeight(EDITOR_INITIAL_HEIGHT);
setIsCompactFocused(false);
@@ -169,6 +179,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
});
editor1.current?.onDidFocusEditorText(() => {
setIsCompactFocused(true);
+ setIsCodeEditorExpandedFocused(true);
setShowLineNumbers(true);
setCodeOneLiner('');
clickedOutside = false;
@@ -441,7 +452,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
}}
>
-
+
{!isCompactFocused && (
{
let position = isCompactFocused ? ('absolute' as 'absolute') : ('relative' as 'relative'); // cast string to type 'relative' | 'absolute'
if (isCodeEditorExpanded) {
@@ -59,9 +60,10 @@ export const textBasedLanguagedEditorStyles = (
},
bottomContainer: {
border: euiTheme.border.thin,
- borderTop: isCodeEditorExpanded
- ? euiTheme.border.thin
- : `2px solid ${bottomContainerBorderColor}`,
+ borderTop:
+ isCodeEditorExpanded && !isCodeEditorExpandedFocused
+ ? euiTheme.border.thin
+ : `2px solid ${bottomContainerBorderColor}`,
backgroundColor: euiTheme.colors.lightestShade,
paddingLeft: euiTheme.size.base,
paddingRight: euiTheme.size.base,
From 074a6943f391ff3ef3fcb1d9532b1c21f6d3fbd7 Mon Sep 17 00:00:00 2001
From: Matthias Wilhelm
Date: Thu, 21 Jul 2022 09:07:24 +0200
Subject: [PATCH 07/30] [Discover] Remove document explorer header column edit
data view field functionality (#136743)
* remove Edit data view field for SQL
* Fix the fix
---
.../application/main/components/layout/discover_documents.tsx | 2 +-
.../application/main/components/layout/discover_layout.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
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 f0cda46ea5e1a..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
@@ -62,7 +62,7 @@ function DiscoverDocumentsComponent({
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]);
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 6d321778460e3..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
@@ -364,7 +364,7 @@ export function DiscoverLayout({
setExpandedDoc={setExpandedDoc}
state={state}
stateContainer={stateContainer}
- onFieldEdited={onFieldEdited}
+ onFieldEdited={!isPlainRecord ? onFieldEdited : undefined}
/>
) : (
Date: Thu, 21 Jul 2022 10:08:40 +0200
Subject: [PATCH 08/30] [Discover] Implement SQL data fetching for embeddable
(#136793)
* remove Edit data view field for SQL
* Fix the fix
* Implement SQL for embeddable
* Fix non-saved-search embeddables
* Fix reporting bundle size
---
.../embeddable/saved_search_embeddable.tsx | 31 ++++++++++++++++++-
.../public/embeddable/saved_search_grid.tsx | 6 +++-
.../panel_actions/get_csv_panel_action.tsx | 16 ++++++++--
3 files changed, 49 insertions(+), 4 deletions(-)
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 999dfbd383570..60eb95e5b7cd2 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,34 @@ 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.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 7c567dc75dfa3..6fb0e45982b5f 100644
--- a/src/plugins/discover/public/embeddable/saved_search_grid.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_grid.tsx
@@ -34,7 +34,11 @@ export function DiscoverGridEmbeddable(props: DiscoverGridEmbeddableProps) {
)}
-
+
);
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) => {
From 0a91517386a77bb7d2dfeeb6bbc1d044634d5e14 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 11:10:48 +0300
Subject: [PATCH 09/30] Allow filters on dashboard level for sql searches
---
src/plugins/data/common/query/to_expression_ast.test.ts | 2 ++
src/plugins/data/common/query/to_expression_ast.ts | 2 ++
.../discover/public/application/main/utils/fetch_sql.ts | 6 ++++--
.../discover/public/embeddable/saved_search_embeddable.tsx | 3 ++-
4 files changed, 10 insertions(+), 3 deletions(-)
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 7a72359535a2a..0f14c70f89d67 100644
--- a/src/plugins/data/common/query/to_expression_ast.test.ts
+++ b/src/plugins/data/common/query/to_expression_ast.test.ts
@@ -31,6 +31,7 @@ describe('queryStateToExpressionAst', () => {
},
Object {
"arguments": Object {
+ "filters": Array [],
"timeRange": Array [
Object {
"chain": Array [
@@ -98,6 +99,7 @@ describe('queryStateToExpressionAst', () => {
},
Object {
"arguments": Object {
+ "filters": Array [],
"timeRange": Array [
Object {
"chain": Array [
diff --git a/src/plugins/data/common/query/to_expression_ast.ts b/src/plugins/data/common/query/to_expression_ast.ts
index 23b23aec03aa2..5bf77dff2b431 100644
--- a/src/plugins/data/common/query/to_expression_ast.ts
+++ b/src/plugins/data/common/query/to_expression_ast.ts
@@ -17,6 +17,7 @@ import {
ExpressionFunctionKibanaContext,
QueryState,
aggregateQueryToAst,
+ filtersToAst,
timerangeToAst,
} from '..';
@@ -34,6 +35,7 @@ export async function queryStateToExpressionAst({ filters, query, time, dataView
const kibana = buildExpressionFunction('kibana', {});
const kibanaContext = buildExpressionFunction('kibana_context', {
timeRange: time && timerangeToAst(time),
+ filters: filters && filtersToAst(filters),
});
const ast = buildExpression([kibana, kibanaContext]).toAst();
diff --git a/src/plugins/discover/public/application/main/utils/fetch_sql.ts b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
index bada02de40442..299d3e07c8f9d 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_sql.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
@@ -7,7 +7,7 @@
*/
import { pluck } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
-import { Query, AggregateQuery } from '@kbn/es-query';
+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';
@@ -26,10 +26,12 @@ export function fetchSql(
query: Query | AggregateQuery,
dataViewsService: DataViewsContract,
data: DataPublicPluginStart,
- expressions: ExpressionsStart
+ expressions: ExpressionsStart,
+ filters?: Filter[]
) {
const timeRange = data.query.timefilter.timefilter.getTime();
return queryStateToExpressionAst({
+ filters,
query,
time: timeRange,
dataViewsService,
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index 60eb95e5b7cd2..e61e523445a0c 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -218,7 +218,8 @@ export class SavedSearchEmbeddable
this.savedSearch.searchSource.getField('query')!,
this.services.indexPatterns,
this.services.data,
- this.services.expressions
+ this.services.expressions,
+ this.input.filters
);
this.updateOutput({
...this.getOutput(),
From 2927cc5aa091294449190a9c47f5857fabdc0b89 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 15:25:13 +0300
Subject: [PATCH 10/30] Fix the radius on the editor
---
.../overwrite.scss | 20 +++++++++++++++++++
.../text_based_languages_editor.styles.ts | 9 ++++++++-
2 files changed, 28 insertions(+), 1 deletion(-)
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
index 8585d7ba4486d..989085d0a30f1 100644
--- 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
@@ -1,3 +1,23 @@
+.unifiedTextLangEditor .monaco-editor {
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+}
+
+.unifiedTextLangEditor .monaco-editor .margin {
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+}
+
+.unifiedTextLangEditor--expanded .monaco-editor {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.unifiedTextLangEditor--expanded .monaco-editor .margin {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
.unifiedTextLangEditor--expanded .monaco-editor .margin-view-overlays {
background-color: rgb(233, 237, 243);
padding-right: 6px;
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
index 412cae3965432..34ab5b1ec5592 100644
--- 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
@@ -31,6 +31,7 @@ export const textBasedLanguagedEditorStyles = (
zIndex: isCompactFocused ? 4 : 0,
height: `${editorHeight}px`,
border: isCompactFocused ? euiTheme.border.thin : 'none',
+ borderTopLeftRadius: isCodeEditorExpanded ? 0 : '6px',
borderBottom: isCodeEditorExpanded
? 'none'
: isCompactFocused
@@ -42,6 +43,8 @@ export const textBasedLanguagedEditorStyles = (
width: isCodeEditorExpanded ? '100%' : 'calc(100% - 80px)',
alignItems: isCompactFocused ? 'flex-start' : 'center',
border: !isCompactFocused ? euiTheme.border.thin : 'none',
+ borderTopLeftRadius: '6px',
+ borderBottomLeftRadius: '6px',
borderBottomColor: hasErrors ? euiTheme.colors.danger : euiTheme.colors.lightShade,
},
linesBadge: {
@@ -72,10 +75,14 @@ export const textBasedLanguagedEditorStyles = (
width: 'calc(100% + 2px)',
position: 'relative' as 'relative', // cast string to type 'relative',
marginTop: '1px',
- marginLeft: 0,
+ marginLeft: '-1px',
+ borderBottomLeftRadius: '6px',
+ borderBottomRightRadius: '6px',
},
topContainer: {
border: euiTheme.border.thin,
+ borderTopLeftRadius: '6px',
+ borderTopRightRadius: '6px',
backgroundColor: euiTheme.colors.lightestShade,
paddingLeft: euiTheme.size.s,
paddingRight: euiTheme.size.s,
From 56fea1240e703d7ad3c79ccea233086bc64a04a0 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 15:54:25 +0300
Subject: [PATCH 11/30] Add vertical padding on the editor
---
.../text_based_languages_editor/index.tsx | 6 +++++-
.../text_based_languages_editor/overwrite.scss | 11 ++++++++---
2 files changed, 13 insertions(+), 4 deletions(-)
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
index e24060c6b726a..7198726c62688 100644
--- 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
@@ -139,7 +139,10 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
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());
+ const contentHeight = Math.min(
+ MAX_COMPACT_VIEW_LENGTH,
+ editor1.current.getContentHeight() + 16
+ );
if (editorElement) {
editorElement.style.height = `${contentHeight}px`;
@@ -300,6 +303,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
accessibilitySupport: 'off',
autoIndent: 'none',
wrappingIndent: 'none',
+ lineNumbersMinChars: 3,
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
scrollbar: {
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
index 989085d0a30f1..a5b1f7c12443e 100644
--- 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
@@ -16,9 +16,14 @@
.unifiedTextLangEditor--expanded .monaco-editor .margin {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
+ background-color: rgb(233, 237, 243);
+ padding-top: 8px;
}
-.unifiedTextLangEditor--expanded .monaco-editor .margin-view-overlays {
- background-color: rgb(233, 237, 243);
- padding-right: 6px;
+.unifiedTextLangEditor .monaco-editor .monaco-scrollable-element {
+ margin-top: 8px;
+}
+
+.unifiedTextLangEditor--expanded .monaco-editor .monaco-scrollable-element {
+ margin-left: 4px;
}
\ No newline at end of file
From 6a14a4cf6983e3f16c961cb516daad6e01be78aa Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 16:47:01 +0300
Subject: [PATCH 12/30] Change the theme
---
.../query_string_input/text_based_languages_editor/index.tsx | 1 +
1 file changed, 1 insertion(+)
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
index 7198726c62688..7c4bf65904f0e 100644
--- 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
@@ -299,6 +299,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
minimap: { enabled: false },
wordWrap: isWordWrapped ? 'on' : 'off',
lineNumbers: showLineNumbers ? 'on' : 'off',
+ theme: 'vs',
lineDecorationsWidth: 12,
accessibilitySupport: 'off',
autoIndent: 'none',
From 687a8c5a919b5beab4871f9c5b6fabb746ea272b Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 19:08:52 +0300
Subject: [PATCH 13/30] Address PR comments
---
.../src/es_query/build_es_query.ts | 6 +-
.../src/filters/build_filters/types.ts | 6 +-
.../common/query/to_expression_ast.test.ts | 111 ++++--------------
.../expressions/aggregate_query_to_ast.ts | 7 +-
.../text_based_languages_editor/index.tsx | 2 +-
5 files changed, 33 insertions(+), 99 deletions(-)
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 4428541413e78..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
@@ -17,6 +17,7 @@ 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
@@ -45,7 +46,7 @@ function removeMatchAll(filters: T[]) {
*/
export function buildEsQuery(
indexPattern: DataViewBase | undefined,
- queries: Query | AggregateQuery | Array,
+ queries: AnyQuery | AnyQuery[],
filters: Filter | Filter[],
config: EsQueryConfig = {
allowLeadingWildcards: false,
@@ -56,8 +57,7 @@ export function buildEsQuery(
queries = Array.isArray(queries) ? queries : [queries];
filters = Array.isArray(filters) ? filters : [filters];
- const isOfQueryTypeQueries = queries.filter(isOfQueryType);
- const validQueries = isOfQueryTypeQueries.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/filters/build_filters/types.ts b/packages/kbn-es-query/src/filters/build_filters/types.ts
index 435965dee0997..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,11 +82,7 @@ export type Query = {
language: string;
};
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-export type AggregateQuery = {
- sql?: string;
- esql?: string;
-};
+export type AggregateQuery = { sql: string } | { esql: string };
/**
* An interface for a latitude-longitude pair
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 0f14c70f89d67..d7c1424869aa8 100644
--- a/src/plugins/data/common/query/to_expression_ast.test.ts
+++ b/src/plugins/data/common/query/to_expression_ast.test.ts
@@ -21,44 +21,15 @@ describe('queryStateToExpressionAst', () => {
dataViewsService,
});
- expect(actual).toMatchInlineSnapshot(`
- Object {
- "chain": Array [
- Object {
- "arguments": Object {},
- "function": "kibana",
- "type": "function",
- },
- Object {
- "arguments": Object {
- "filters": Array [],
- "timeRange": Array [
- Object {
- "chain": Array [
- Object {
- "arguments": Object {
- "from": Array [
- "now",
- ],
- "to": Array [
- "now+7d",
- ],
- },
- "function": "timerange",
- "type": "function",
- },
- ],
- "type": "expression",
- },
- ],
- },
- "function": "kibana_context",
- "type": "function",
- },
- ],
- "type": "expression",
- }
- `);
+ 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 () => {
@@ -89,55 +60,19 @@ describe('queryStateToExpressionAst', () => {
dataViewsService,
});
- expect(actual).toMatchInlineSnapshot(`
- Object {
- "chain": Array [
- Object {
- "arguments": Object {},
- "function": "kibana",
- "type": "function",
- },
- Object {
- "arguments": Object {
- "filters": Array [],
- "timeRange": Array [
- Object {
- "chain": Array [
- Object {
- "arguments": Object {
- "from": Array [
- "now",
- ],
- "to": Array [
- "now+7d",
- ],
- },
- "function": "timerange",
- "type": "function",
- },
- ],
- "type": "expression",
- },
- ],
- },
- "function": "kibana_context",
- "type": "function",
- },
- Object {
- "arguments": Object {
- "query": Array [
- "SELECT * FROM foo",
- ],
- "timeField": Array [
- "baz",
- ],
- },
- "function": "essql",
- "type": "function",
- },
- ],
- "type": "expression",
- }
- `);
+ 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/search/expressions/aggregate_query_to_ast.ts b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
index 8c62021c16e57..6313b4fdfbf3a 100644
--- a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
+++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
@@ -6,11 +6,14 @@
* Side Public License, v 1.
*/
-import { buildExpressionFunction } from '@kbn/expressions-plugin/common';
+import { buildExpressionFunction, ExpressionAstFunction } from '@kbn/expressions-plugin/common';
import { AggregateQuery } from '../../query';
import { EssqlExpressionFunctionDefinition } from './essql';
-export const aggregateQueryToAst = (query: AggregateQuery, timeField?: string) => {
+export const aggregateQueryToAst = (
+ query: AggregateQuery,
+ timeField?: string
+): undefined | ExpressionAstFunction => {
if (query.sql) {
return buildExpressionFunction('essql', {
query: query.sql,
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
index 7c4bf65904f0e..173b0c1a1f55d 100644
--- 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
@@ -301,7 +301,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
lineNumbers: showLineNumbers ? 'on' : 'off',
theme: 'vs',
lineDecorationsWidth: 12,
- accessibilitySupport: 'off',
+ accessibilitySupport: 'on',
autoIndent: 'none',
wrappingIndent: 'none',
lineNumbersMinChars: 3,
From 39c82303189d5aa57c2374c5c84ba310c355bef2 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 20:01:13 +0300
Subject: [PATCH 14/30] Fix types
---
packages/kbn-es-query/src/filters/build_filters/types.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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 5e920d11bcab5..435965dee0997 100644
--- a/packages/kbn-es-query/src/filters/build_filters/types.ts
+++ b/packages/kbn-es-query/src/filters/build_filters/types.ts
@@ -82,7 +82,11 @@ export type Query = {
language: string;
};
-export type AggregateQuery = { sql: string } | { esql: string };
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type AggregateQuery = {
+ sql?: string;
+ esql?: string;
+};
/**
* An interface for a latitude-longitude pair
From eebc483ebd41650fe34d16c34381c68c61ea22f3 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 20:49:51 +0300
Subject: [PATCH 15/30] Address some of the comments
---
packages/kbn-es-query/src/es_query/es_query_sql.ts | 6 ++++--
.../kbn-es-query/src/filters/build_filters/types.ts | 6 +-----
src/plugins/data/common/query/to_expression_ast.ts | 2 +-
.../search/expressions/aggregate_query_to_ast.ts | 2 +-
.../main/components/top_nav/discover_topnav.tsx | 4 ++--
.../main/components/top_nav/get_top_nav_links.tsx | 1 -
.../application/main/hooks/use_discover_state.ts | 10 +++++-----
7 files changed, 14 insertions(+), 17 deletions(-)
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
index 2b604d62fd269..748da33cad687 100644
--- a/packages/kbn-es-query/src/es_query/es_query_sql.ts
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.ts
@@ -15,8 +15,10 @@ export function isOfQueryType(arg?: Query | AggregateQuery): arg is Query {
// 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): query is AggregateQuery {
- return Boolean(query && 'sql' in query);
+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
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 435965dee0997..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,11 +82,7 @@ export type Query = {
language: string;
};
-// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
-export type AggregateQuery = {
- sql?: string;
- esql?: string;
-};
+export type AggregateQuery = { sql: string } | { esql: string };
/**
* An interface for a latitude-longitude pair
diff --git a/src/plugins/data/common/query/to_expression_ast.ts b/src/plugins/data/common/query/to_expression_ast.ts
index 5bf77dff2b431..659a00311d8d6 100644
--- a/src/plugins/data/common/query/to_expression_ast.ts
+++ b/src/plugins/data/common/query/to_expression_ast.ts
@@ -42,7 +42,7 @@ export async function queryStateToExpressionAst({ filters, query, time, dataView
if (query && isOfAggregateQueryType(query)) {
const mode = getAggregateQueryMode(query);
// sql query
- if (mode === 'sql') {
+ if (mode === 'sql' && 'sql' in query) {
const idxPattern = getIndexPatternFromSQLQuery(query.sql);
const idsTitles = await dataViewsService.getIdsWithTitle();
const dataViewIdTitle = idsTitles.find(({ title }) => title === idxPattern);
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
index 6313b4fdfbf3a..84e1e4e5f2262 100644
--- a/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
+++ b/src/plugins/data/common/search/expressions/aggregate_query_to_ast.ts
@@ -14,7 +14,7 @@ export const aggregateQueryToAst = (
query: AggregateQuery,
timeField?: string
): undefined | ExpressionAstFunction => {
- if (query.sql) {
+ if ('sql' in query) {
return buildExpressionFunction('essql', {
query: query.sql,
timeField,
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 b3c6436bfdfc5..485d2b401b136 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
@@ -176,9 +176,9 @@ export const DiscoverTopNav = ({
const setMenuMountPoint = useMemo(() => {
return getHeaderActionMenuMounter();
}, []);
- const SQLModeIsEnabled = uiSettings.get(ENABLE_SQL);
+ const isSQLModeEnabled = uiSettings.get(ENABLE_SQL);
const supportedTextBasedLanguages = [];
- if (SQLModeIsEnabled) {
+ if (isSQLModeEnabled) {
supportedTextBasedLanguages.push('SQL');
}
const dataViewPickerProps = {
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 42741d18b3950..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
@@ -200,7 +200,6 @@ export const getTopNavLinks = ({
openSearch,
...(!isPlainRecord ? [shareSearch] : []),
...(services.triggersActionsUi &&
- !isPlainRecord &&
services.capabilities.management?.insightsAndAlerting?.triggersActions &&
!isPlainRecord
? [alerts]
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 4d570a608d6bf..d47ef7cbb2844 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
@@ -249,7 +249,7 @@ export function useDiscoverState({
}
}, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]);
- const fetchResults = useCallback(() => {
+ const getResultColumns = useCallback(() => {
if (documentState.result?.length) {
const firstRow = documentState.result[0];
const columns = Object.keys(firstRow.raw).slice(0, MAX_NUM_OF_COLUMNS);
@@ -263,12 +263,12 @@ export function useDiscoverState({
useEffect(() => {
async function fetchDataview() {
- if (state.query && isOfAggregateQueryType(state.query)) {
- const indexPatternFROMQuery = getIndexPatternFromSQLQuery(state.query.sql);
+ 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);
+ const dataViewObj = idsTitles.find(({ title }) => title === indexPatternFromQuery);
if (dataViewObj) {
- const columns = fetchResults();
+ const columns = getResultColumns();
if (columns.length) {
setDocumentStateCols(columns);
}
From 6b8c02d062a9619886e4025020bcec9b9ce8f103 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 20:58:47 +0300
Subject: [PATCH 16/30] Fix bug on transitioning from SQL to dataview mode with
the modal dismissed
---
.../public/dataview_picker/change_dataview.tsx | 9 +++++++++
1 file changed, 9 insertions(+)
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 148b903ba9a5f..f697809cb3b29 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -299,6 +299,15 @@ export function ChangeDataView({
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);
}
From b29e637ba4af3dc3dcffd4ac4a48544e31984369 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Thu, 21 Jul 2022 21:41:12 +0300
Subject: [PATCH 17/30] More types fixes
---
packages/kbn-es-query/src/es_query/es_query_sql.ts | 6 ++++--
.../text_based_languages_editor/index.tsx | 10 +++++-----
2 files changed, 9 insertions(+), 7 deletions(-)
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
index 748da33cad687..8cd2f087f103c 100644
--- a/packages/kbn-es-query/src/es_query/es_query_sql.ts
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.ts
@@ -7,6 +7,8 @@
*/
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);
@@ -22,8 +24,8 @@ export function isOfAggregateQueryType(
}
// returns the language of the aggregate Query, sql, esql etc
-export function getAggregateQueryMode(query: AggregateQuery): string {
- return Object.keys(query)[0];
+export function getAggregateQueryMode(query: AggregateQuery): Language {
+ return Object.keys(query)[0] as Language;
}
// retrieves the index pattern from the aggregate query
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
index 173b0c1a1f55d..87ccf301ef4cb 100644
--- 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
@@ -76,8 +76,8 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
errors,
}: TextBasedLanguagesEditorProps) {
const { euiTheme } = useEuiTheme();
- const language = getAggregateQueryMode(query) as 'sql' | 'esql';
- const queryString = query[language] ?? '';
+ const language = getAggregateQueryMode(query);
+ const queryString: string = query[language] ?? '';
const [lines, setLines] = useState(1);
const [code, setCode] = useState(queryString ?? '');
const [codeOneLiner, setCodeOneLiner] = useState('');
@@ -272,7 +272,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const onQueryUpdate = useCallback(
(value: string) => {
setCode(value);
- onTextLangQueryChange({ [language]: value });
+ onTextLangQueryChange({ [language]: value } as AggregateQuery);
},
[language, onTextLangQueryChange]
);
@@ -421,7 +421,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
{
defaultMessage: '{lang} reference',
values: {
- lang: language.toUpperCase(),
+ lang: String(language).toUpperCase(),
},
}
)}
@@ -555,7 +555,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
{
defaultMessage: '{lang} reference',
values: {
- lang: language.toUpperCase(),
+ lang: String(language).toUpperCase(),
},
}
)}
From 68de2337d98d805d30f7b8e08110db4b92872f3d Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 10:04:44 +0300
Subject: [PATCH 18/30] Design review comments
---
.../main/components/top_nav/discover_topnav.tsx | 4 ++--
.../public/dataview_picker/change_dataview.tsx | 3 +++
.../unified_search/public/dataview_picker/index.tsx | 3 ++-
.../text_based_languages_editor/editor_footer.tsx | 5 +++--
.../text_based_languages_editor/index.tsx | 5 ++---
.../text_based_languages_editor/overwrite.scss | 13 +++++++++++--
.../text_based_languages_editor.styles.ts | 13 +++++++++----
7 files changed, 32 insertions(+), 14 deletions(-)
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 485d2b401b136..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
@@ -195,14 +195,14 @@ export const DiscoverTopNav = ({
};
const onTextBasedSavedAndExit = useCallback(
- async ({ onSave }) => {
+ async ({ onSave, onCancel }) => {
await onSaveSearch({
savedSearch,
services,
indexPattern,
navigateTo,
state: stateContainer,
- onClose: onSave,
+ onClose: onCancel,
onSaveCb: onSave,
});
},
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 f697809cb3b29..42c16ca939c7d 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -404,6 +404,9 @@ export function ChangeDataView({
onSave: () => {
cleanup(shouldDismissModal);
},
+ onCancel: () => {
+ setIsTextLangTransitionModalVisible(false);
+ },
});
} else {
cleanup(shouldDismissModal);
diff --git a/src/plugins/unified_search/public/dataview_picker/index.tsx b/src/plugins/unified_search/public/dataview_picker/index.tsx
index c5ea9eb2db68e..40ced3b6f4d47 100644
--- a/src/plugins/unified_search/public/dataview_picker/index.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/index.tsx
@@ -23,6 +23,7 @@ export enum TextBasedLanguages {
export interface OnSaveTextLanguageQueryProps {
onSave: () => void;
+ onCancel: () => void;
}
/** @public */
@@ -70,7 +71,7 @@ export interface DataViewPickerProps {
/**
* Callback that is called when the user clicks the Save and switch transition modal button
*/
- onSaveTextLanguageQuery?: ({ onSave }: OnSaveTextLanguageQueryProps) => void;
+ onSaveTextLanguageQuery?: ({ onSave, onCancel }: OnSaveTextLanguageQueryProps) => void;
}
export interface DataViewPickerPropsExtended extends DataViewPickerProps {
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
index b8ebe9718710f..606d8dd0b2e89 100644
--- 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
@@ -47,7 +47,7 @@ export const EditorFooter = memo(function EditorFooter({
>
-
+
{i18n.translate('unifiedSearch.query.textBasedLanguagesEditor.lineCount', {
@@ -105,7 +105,7 @@ export const EditorFooter = memo(function EditorFooter({
{errors.map((error, index) => {
return (
-
+
@@ -153,6 +153,7 @@ export const EditorFooter = memo(function EditorFooter({
-
+
Date: Fri, 22 Jul 2022 13:03:36 +0300
Subject: [PATCH 19/30] Discovery team review comments
---
.../main/hooks/use_discover_state.ts | 4 +-
.../main/utils/persist_saved_search.ts | 5 +-
.../dataview_picker/change_dataview.tsx | 3 +-
.../text_based_languages_editor/helpers.ts | 5 +-
.../text_based_languages_editor/index.tsx | 10 +--
.../overwrite.scss | 17 ++--
.../sql_documentation_sections.tsx | 78 +++++++++----------
7 files changed, 67 insertions(+), 55 deletions(-)
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 d47ef7cbb2844..72fb2f5a75dc3 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
@@ -250,7 +250,7 @@ export function useDiscoverState({
}, [initialFetchStatus, refetch$, indexPattern, savedSearch.id]);
const getResultColumns = useCallback(() => {
- if (documentState.result?.length) {
+ 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)) {
@@ -259,7 +259,7 @@ export function useDiscoverState({
return [];
}
return [];
- }, [documentState.result, documentStateCols, sqlQuery, state.query]);
+ }, [documentState, documentStateCols, sqlQuery, state.query]);
useEffect(() => {
async function fetchDataview() {
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 d9f04038d8183..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
@@ -64,8 +64,9 @@ export async function persistSavedSearch(
// add a flag here to identify text based language queries
// these should be filtered out from the visualize editor
- if (state.query && isOfAggregateQueryType(state.query)) {
- savedSearch.isTextBasedQuery = true;
+ const isTextBasedQuery = state.query && isOfAggregateQueryType(state.query);
+ if (savedSearch.isTextBasedQuery || isTextBasedQuery) {
+ savedSearch.isTextBasedQuery = isTextBasedQuery;
}
try {
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 42c16ca939c7d..04bf948298813 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -233,13 +233,14 @@ export function ChangeDataView({
alignItems="center"
gutterSize="none"
justifyContent="spaceBetween"
+ responsive={false}
css={css`
margin: ${euiTheme.size.s};
margin-bottom: 0;
`}
>
-
+
{Boolean(isTextBasedLangSelected) ? (
{
};
} 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;
+ const errorLength = 5 + dataviewString.length + additionnalLength;
// no dataview found error message
const hasLines = /\r|\n/.exec(code);
if (hasLines) {
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
index 635373c060882..49491654009b2 100644
--- 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
@@ -108,7 +108,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const containerRef = useRef(null);
const editorClassName = classNames('unifiedTextLangEditor', {
- 'unifiedTextLangEditor--expanded': isCompactFocused,
+ '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.
@@ -139,10 +141,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
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() + 16
- );
+ const contentHeight = Math.min(MAX_COMPACT_VIEW_LENGTH, editor1.current.getContentHeight());
if (editorElement) {
editorElement.style.height = `${contentHeight}px`;
@@ -292,6 +291,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
const codeEditorOptions: CodeEditorProps['options'] = {
automaticLayout: false,
+ accessibilitySupport: 'off',
folding: false,
fontSize: 14,
scrollBeyondLastLine: false,
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
index 82800ff56502f..1a8c0ed99f60e 100644
--- 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
@@ -3,21 +3,28 @@
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--expanded .monaco-editor {
+.unifiedTextLangEditor--compact .monaco-editor {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
-.unifiedTextLangEditor--expanded .monaco-editor .margin {
+.unifiedTextLangEditor--compact .monaco-editor .margin {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: $euiColorLightestShade;
- padding-top: 8px;
color: $euiColorDisabledText;
}
@@ -29,10 +36,10 @@
color: $euiTextSubduedColor;
}
-.unifiedTextLangEditor .monaco-editor .monaco-scrollable-element .lines-content {
+.unifiedTextLangEditor--initial .monaco-editor .monaco-scrollable-element .lines-content {
margin-top: 8px;
}
-.unifiedTextLangEditor--expanded .monaco-editor .monaco-scrollable-element {
+.unifiedTextLangEditor--compact .monaco-editor .monaco-scrollable-element {
margin-left: 4px;
}
\ 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
index ead231afe55bb..d58728da61d2c 100644
--- 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
@@ -18,7 +18,7 @@ export const initialSection = (
With Elasticsearch SQL, you can access that full text search,
blazing speed, and effortless scalability with a familiar query syntax.
-can use SQL to search and aggregate data natively inside Elasticsearch.
+You can use SQL to search and aggregate data natively inside Elasticsearch.
One can think of Elasticsearch SQL as a translator,
one that understands both SQL and Elasticsearch and makes it easy
to read and process data in real-time.
@@ -66,7 +66,7 @@ export const comparisonOperators = {
defaultMessage: `### Equality (=)
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no = 10000 LIMIT 5;
+WHERE emp_no = 10000 LIMIT 5
\`\`\`
`,
description:
@@ -90,18 +90,18 @@ WHERE emp_no = 10000 LIMIT 5;
{
defaultMessage: `### Null safe equality:
\`\`\`
-SELECT 'elastic' <=> null AS "equals";
+SELECT 'elastic' <=> null AS "equals"
equals
---------------
-false;
+false
\`\`\`
\`\`\`
-SELECT null <=> null AS "equals";
+SELECT null <=> null AS "equals"
equals
---------------
-true;
+true
\`\`\`
`,
description:
@@ -126,7 +126,7 @@ true;
defaultMessage: `### Inequality (<> or !=)
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5;
+WHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5
\`\`\`
`,
description:
@@ -151,7 +151,7 @@ WHERE emp_no <> 10000 ORDER BY emp_no LIMIT 5;
defaultMessage: `### Comparison (<, <=, >, >=)
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no < 10003 ORDER BY emp_no LIMIT 5;
+WHERE emp_no < 10003 ORDER BY emp_no LIMIT 5
\`\`\`
`,
description:
@@ -173,7 +173,7 @@ WHERE emp_no < 10003 ORDER BY emp_no LIMIT 5;
defaultMessage: `### Between
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no BETWEEN 9990 AND 10003 ORDER BY emp_no;
+WHERE emp_no BETWEEN 9990 AND 10003 ORDER BY emp_no
\`\`\`
`,
description:
@@ -198,7 +198,7 @@ WHERE emp_no BETWEEN 9990 AND 10003 ORDER BY emp_no;
defaultMessage: `### IS NULL/IS NOT NULL
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no IS NOT NULL AND gender IS NULL;
+WHERE emp_no IS NOT NULL AND gender IS NULL
\`\`\`
`,
description:
@@ -223,7 +223,7 @@ WHERE emp_no IS NOT NULL AND gender IS NULL;
defaultMessage: `### IN (, , ...)
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5;
+WHERE emp_no IN (10000, 10001, 10002, 999) ORDER BY emp_no LIMIT 5
\`\`\`
`,
description:
@@ -262,7 +262,7 @@ export const logicalOperators = {
defaultMessage: `### AND
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no > 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5;
+WHERE emp_no > 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5
\`\`\`
`,
description:
@@ -287,7 +287,7 @@ WHERE emp_no > 10000 AND emp_no < 10005 ORDER BY emp_no LIMIT 5;
defaultMessage: `### OR
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE emp_no < 10003 OR emp_no = 10005 ORDER BY emp_no LIMIT 5;
+WHERE emp_no < 10003 OR emp_no = 10005 ORDER BY emp_no LIMIT 5
\`\`\`
`,
description:
@@ -312,7 +312,7 @@ WHERE emp_no < 10003 OR emp_no = 10005 ORDER BY emp_no LIMIT 5;
defaultMessage: `### NOT
\`\`\`
SELECT last_name l FROM "test_emp"
-WHERE NOT emp_no = 10000 LIMIT 5;
+WHERE NOT emp_no = 10000 LIMIT 5
\`\`\`
`,
description:
@@ -350,7 +350,7 @@ export const mathOperators = {
{
defaultMessage: `### Add (+)
\`\`\`
-SELECT 1 + 1 AS x;
+SELECT 1 + 1 AS x
\`\`\`
`,
description:
@@ -374,7 +374,7 @@ SELECT 1 + 1 AS x;
{
defaultMessage: `### Subtract (infix -)
\`\`\`
-SELECT 1 - 1 AS x;
+SELECT 1 - 1 AS x
\`\`\`
`,
description:
@@ -398,7 +398,7 @@ SELECT 1 - 1 AS x;
{
defaultMessage: `### Negate (unary -)
\`\`\`
-SELECT - 1 AS x;
+SELECT - 1 AS x
\`\`\`
`,
description:
@@ -422,7 +422,7 @@ SELECT - 1 AS x;
{
defaultMessage: `### Multiply (*)
\`\`\`
-SELECT 2 * 3 AS x;
+SELECT 2 * 3 AS x
\`\`\`
`,
description:
@@ -446,7 +446,7 @@ SELECT 2 * 3 AS x;
{
defaultMessage: `### Divide (/)
\`\`\`
-SELECT 6 / 3 AS x;
+SELECT 6 / 3 AS x
\`\`\`
`,
description:
@@ -470,7 +470,7 @@ SELECT 6 / 3 AS x;
{
defaultMessage: `### Modulo or remainder(%)
\`\`\`
-SELECT 5 % 2 AS x;
+SELECT 5 % 2 AS x
\`\`\`
`,
description:
@@ -513,7 +513,7 @@ AVG(numeric_field)
\`\`\`
- numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT AVG(salary) AS avg FROM emp;
+SELECT AVG(salary) AS avg FROM emp
\`\`\`
`,
description:
@@ -544,7 +544,7 @@ COUNT(expression)
\`\`\`
- expression. a field name, wildcard (*) or any numeric value. For COUNT(*) or COUNT(), all values are considered, including null or missing ones. For COUNT(), null values are not considered.
\`\`\`
-SELECT COUNT(*) AS count FROM emp;
+SELECT COUNT(*) AS count FROM emp
\`\`\`
`,
description:
@@ -574,7 +574,7 @@ 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;
+SELECT COUNT(ALL last_name) AS count_all, COUNT(DISTINCT last_name) count_distinct FROM emp
\`\`\`
`,
description:
@@ -605,7 +605,7 @@ COUNT(DISTINCT field_name)
- Input: a field name.
- Output: numeric value. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT COUNT(DISTINCT hire_date) unique_hires, COUNT(hire_date) AS hires FROM emp;
+SELECT COUNT(DISTINCT hire_date) unique_hires, COUNT(hire_date) AS hires FROM emp
\`\`\`
`,
@@ -640,7 +640,7 @@ FIRST(
- ordering_field_name: optional field used for ordering.
\`\`\`
-SELECT gender, FIRST(first_name, birth_date) FROM emp GROUP BY gender ORDER BY gender;
+SELECT gender, FIRST(first_name, birth_date) FROM emp GROUP BY gender ORDER BY gender
\`\`\`
- FIRST cannot be used in a HAVING clause.
@@ -676,7 +676,7 @@ LAST(
- field name: target field for the aggregation
- ordering_field_name: optional field used for ordering.
\`\`\`
-SELECT gender, LAST(first_name) FROM emp GROUP BY gender ORDER BY gender;
+SELECT gender, LAST(first_name) FROM emp GROUP BY gender ORDER BY gender
\`\`\`
- LAST cannot be used in a HAVING clause.
- LAST cannot be used with columns of type text unless the field is also saved as a keyword.
@@ -709,7 +709,7 @@ MAX(field_name)
- a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MAX(salary) AS max FROM emp;
+SELECT MAX(salary) AS max FROM emp
\`\`\`
- MAX on a field of type text or keyword is translated into FIRST/FIRST_VALUE and therefore, it cannot be used in HAVING clause.
@@ -743,7 +743,7 @@ MIN(field_name)
- a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min FROM emp;
+SELECT MIN(salary) AS min FROM emp
\`\`\`
- MIN on a field of type text or keyword is translated into FIRST/FIRST_VALUE and therefore, it cannot be used in HAVING clause.
@@ -776,7 +776,7 @@ SUM(field_name)
- a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT SUM(salary) AS sum FROM emp;
+SELECT SUM(salary) AS sum FROM emp
\`\`\`
`,
description:
@@ -807,7 +807,7 @@ KURTOSIS(field_name)
- a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, KURTOSIS(salary) AS k FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, KURTOSIS(salary) AS k FROM emp
\`\`\`
- KURTOSIS cannot be used on top of scalar functions or operators but only directly on a field.
@@ -840,7 +840,7 @@ MAD(field_name)
- a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, AVG(salary) AS avg, MAD(salary) AS mad FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, AVG(salary) AS avg, MAD(salary) AS mad FROM emp
\`\`\`
`,
description:
@@ -883,7 +883,7 @@ SELECT
PERCENTILE(salary, 97.3, 'tdigest', 100.0) AS "97.3_TDigest",
PERCENTILE(salary, 97.3, 'hdr', 3) AS "97.3_HDR"
FROM emp
-GROUP BY languages;
+GROUP BY languages
\`\`\`
`,
description:
@@ -926,7 +926,7 @@ SELECT
ROUND(PERCENTILE_RANK(salary, 65000, 'tdigest', 100.0), 2) AS "rank_TDigest",
ROUND(PERCENTILE_RANK(salary, 65000, 'hdr', 3), 2) AS "rank_HDR"
FROM emp
-GROUP BY languages;
+GROUP BY languages
\`\`\`
`,
description:
@@ -957,7 +957,7 @@ SKEWNESS(field_name)
- field_name : a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, SKEWNESS(salary) AS s FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, SKEWNESS(salary) AS s FROM emp
\`\`\`
`,
description:
@@ -988,7 +988,7 @@ STDDEV_POP(field_name)
- field_name : a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_POP(salary) AS stddev FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_POP(salary) AS stddev FROM emp
\`\`\`
`,
description:
@@ -1019,7 +1019,7 @@ STDDEV_SAMP(field_name)
- field_name : a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_SAMP(salary) AS stddev FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, STDDEV_SAMP(salary) AS stddev FROM emp
\`\`\`
`,
description:
@@ -1051,7 +1051,7 @@ SUM_OF_SQUARES(field_name)
\`\`\`
SELECT MIN(salary) AS min, MAX(salary) AS max, SUM_OF_SQUARES(salary) AS sumsq
- FROM emp;
+ FROM emp
\`\`\`
`,
description:
@@ -1082,7 +1082,7 @@ VAR_POP(field_name)
- field_name : a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, VAR_POP(salary) AS varpop FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, VAR_POP(salary) AS varpop FROM emp
\`\`\`
`,
description:
@@ -1113,7 +1113,7 @@ VAR_SAMP(field_name)
- field_name : a numeric field. If this field contains only null values, the function returns null. Otherwise, the function ignores null values in this field.
\`\`\`
-SELECT MIN(salary) AS min, MAX(salary) AS max, VAR_SAMP(salary) AS varsamp FROM emp;
+SELECT MIN(salary) AS min, MAX(salary) AS max, VAR_SAMP(salary) AS varsamp FROM emp
\`\`\`
`,
description:
From 067740078d5a59ec202d0a1dd583167bcd547265 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 13:51:37 +0300
Subject: [PATCH 20/30] Fix jest tests
---
.../text_based_languages_editor/helpers.test.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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
index 372af2898f8de..bf34735abd4b6 100644
--- 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
@@ -56,7 +56,7 @@ describe('helpers', function () {
const errors = [error];
expect(parseErrors(errors, `SELECT * FROM "kibana_sample_data_ecommerce1"`)).toEqual([
{
- endColumn: 44,
+ endColumn: 46,
endLineNumber: 1,
message: 'No data view found for index pattern kibana_sample_data_ecommerce1',
severity: 8,
@@ -77,7 +77,7 @@ describe('helpers', function () {
)
).toEqual([
{
- endColumn: 39,
+ endColumn: 41,
endLineNumber: 2,
message: 'No data view found for index pattern kibana_sample_data_ecommerce1',
severity: 8,
From 6fba8ed0bc0ffd437356c52ffe05d99c98a3a03a Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 14:44:10 +0300
Subject: [PATCH 21/30] Fix bug on navigating from the SQL mode to the dataview
mode and back in sql mode by clicking the breadcrumb
---
.../public/application/main/hooks/use_discover_state.ts | 8 ++++++++
1 file changed, 8 insertions(+)
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 72fb2f5a75dc3..d3064bdf0c8d2 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,6 +6,7 @@
* 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 {
@@ -80,6 +81,7 @@ export function useDiscoverState({
const [state, setState] = useState(appStateContainer.getState());
const [documentStateCols, setDocumentStateCols] = useState([]);
const [sqlQuery] = useState(state.query);
+ const prevQuery = usePrevious(state.query);
/**
* Search session logic
@@ -243,6 +245,12 @@ 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);
From c469244ec331c818ea0584204c079f7f1aaee007 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 15:30:08 +0300
Subject: [PATCH 22/30] Update
src/plugins/discover/public/application/main/hooks/use_discover_state.ts
Co-authored-by: Matthias Wilhelm
---
.../public/application/main/hooks/use_discover_state.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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 d3064bdf0c8d2..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
@@ -284,7 +284,7 @@ export function useDiscoverState({
index: dataViewObj.id,
...(columns.length && { columns }),
};
- stateContainer.setAppState(nextState);
+ stateContainer.replaceUrlAppState(nextState);
}
}
}
From 3e6331822d705e1c92fb3df7e0054c3a774abe29 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 18:24:55 +0300
Subject: [PATCH 23/30] Add padding to the top of the editor without creating
any bug
---
.../query_string_input/text_based_languages_editor/index.tsx | 3 +++
.../text_based_languages_editor/overwrite.scss | 4 ----
2 files changed, 3 insertions(+), 4 deletions(-)
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
index 49491654009b2..0ab84e2285667 100644
--- 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
@@ -294,6 +294,9 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
accessibilitySupport: 'off',
folding: false,
fontSize: 14,
+ padding: {
+ top: 8,
+ },
scrollBeyondLastLine: false,
quickSuggestions: true,
minimap: { enabled: false },
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
index 1a8c0ed99f60e..18cea3057d5a8 100644
--- 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
@@ -36,10 +36,6 @@
color: $euiTextSubduedColor;
}
-.unifiedTextLangEditor--initial .monaco-editor .monaco-scrollable-element .lines-content {
- margin-top: 8px;
-}
-
.unifiedTextLangEditor--compact .monaco-editor .monaco-scrollable-element {
margin-left: 4px;
}
\ No newline at end of file
From 408363e236e149e19cfa73ff8c5d4242b4900158 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 18:27:19 +0300
Subject: [PATCH 24/30] Add some padding to the bottom without creating any bug
---
.../query_string_input/text_based_languages_editor/index.tsx | 1 +
1 file changed, 1 insertion(+)
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
index 0ab84e2285667..cb1450b38ba31 100644
--- 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
@@ -296,6 +296,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
fontSize: 14,
padding: {
top: 8,
+ bottom: 8,
},
scrollBeyondLastLine: false,
quickSuggestions: true,
From 7cbbc3f4e46e33bacbb3bc88ce2906b907cb0b33 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Fri, 22 Jul 2022 18:53:03 +0300
Subject: [PATCH 25/30] Fixes undo bug
---
.../query_string_input/text_based_languages_editor/index.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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
index cb1450b38ba31..fe193a5438212 100644
--- 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
@@ -249,7 +249,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
);
useEffect(() => {
- if (editor1.current) {
+ if (editor1.current && !isCompactFocused) {
const editorElement = editor1.current.getDomNode();
if (editorElement) {
const contentWidth = Number(editorElement?.style.width.replace('px', ''));
@@ -259,7 +259,7 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
}
}
}
- }, [calculateVisibleCode, code, queryString]);
+ }, [calculateVisibleCode, code, isCompactFocused, queryString]);
const onResize = ({ width }: { width: number }) => {
calculateVisibleCode(width);
From 3f9b97c86c0681e2ee33d9d394615897a820adbc Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 25 Jul 2022 10:14:20 +0300
Subject: [PATCH 26/30] Fix confusing naming of variable
---
.../main/components/sidebar/discover_field.tsx | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
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 40aedaac23e79..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,11 +167,11 @@ interface MultiFieldsProps {
multiFields: NonNullable;
toggleDisplay: (field: DataViewField) => void;
alwaysShowActionButton: boolean;
- isPlainRecord: boolean;
+ isDocumentRecord: boolean;
}
const MultiFields: React.FC = memo(
- ({ multiFields, toggleDisplay, alwaysShowActionButton, isPlainRecord }) => (
+ ({ multiFields, toggleDisplay, alwaysShowActionButton, isDocumentRecord }) => (
@@ -187,7 +187,7 @@ const MultiFields: React.FC = memo(
className="dscSidebarItem dscSidebarItem--multi"
isActive={false}
dataTestSubj={`field-${entry.field.name}-showDetails`}
- fieldIcon={isPlainRecord && }
+ fieldIcon={isDocumentRecord && }
fieldAction={
{
@@ -306,7 +306,7 @@ function DiscoverFieldComponent({
size="s"
className="dscSidebarItem"
dataTestSubj={`field-${field.name}-showDetails`}
- fieldIcon={isPlainRecord && }
+ fieldIcon={isDocumentRecord && }
fieldAction={
}
+ fieldIcon={isDocumentRecord && }
fieldAction={
}
/>
);
- if (!isPlainRecord) {
+ if (!isDocumentRecord) {
return button;
}
@@ -424,7 +424,7 @@ function DiscoverFieldComponent({
multiFields={multiFields}
alwaysShowActionButton={alwaysShowActionButton}
toggleDisplay={toggleDisplay}
- isPlainRecord={isPlainRecord}
+ isDocumentRecord={isDocumentRecord}
/>
>
)}
From 29afcbf48793a4472e6866e94d823840bae4ac1f Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 25 Jul 2022 14:27:22 +0300
Subject: [PATCH 27/30] Fix nested selects
---
packages/kbn-es-query/src/es_query/es_query_sql.test.ts | 5 +++++
packages/kbn-es-query/src/es_query/es_query_sql.ts | 7 ++++++-
2 files changed, 11 insertions(+), 1 deletion(-)
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
index 2747411769c98..da909c6e5f9b4 100644
--- 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
@@ -74,6 +74,11 @@ describe('sql query helpers', () => {
'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
index 8cd2f087f103c..46de33dc04e86 100644
--- a/packages/kbn-es-query/src/es_query/es_query_sql.ts
+++ b/packages/kbn-es-query/src/es_query/es_query_sql.ts
@@ -30,7 +30,12 @@ export function getAggregateQueryMode(query: AggregateQuery): Language {
// retrieves the index pattern from the aggregate query
export function getIndexPatternFromSQLQuery(sqlQuery?: string): string {
- const sql = sqlQuery?.replaceAll('"', '').replaceAll("'", '');
+ 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);
From f88e01f688cbee73778b2537855f1c9c1c3bb051 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 25 Jul 2022 17:58:46 +0300
Subject: [PATCH 28/30] Update texts for transition modal and warning
---
.../unified_search/public/dataview_picker/change_dataview.tsx | 2 +-
.../dataview_picker/text_languages_transition_modal.tsx | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
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 04bf948298813..d2762f0d27b2f 100644
--- a/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
+++ b/src/plugins/unified_search/public/dataview_picker/change_dataview.tsx
@@ -249,7 +249,7 @@ export function ChangeDataView({
'unifiedSearch.query.queryBar.indexPattern.textBasedLangSwitchWarning',
{
defaultMessage:
- 'The current text-based query will be cleared when switching to a data view. Ensure that you have saved this search to avoid losing your work.',
+ "Switching data views removes the current SQL query. Save this search to ensure you don't lose work.",
}
)}
>
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
index 8aa9e4e29a468..40d12297b49d1 100644
--- 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
@@ -46,7 +46,7 @@ export default function TextBasedLanguagesTransitionModal({
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalTitle',
{
- defaultMessage: 'Current text-based query will be cleared',
+ defaultMessage: 'Your query will be removed',
}
)}
@@ -59,7 +59,7 @@ export default function TextBasedLanguagesTransitionModal({
'unifiedSearch.query.queryBar.indexPattern.textBasedLanguagesTransitionModalBody',
{
defaultMessage:
- 'The current text-based language query will be cleared when switching to a specific data view. To ensure that no work is inadvertently lost in the transition, it is recommended that you save this search before switching.',
+ "Switching data views removes the current SQL query. Save this search to ensure you don't lose work.",
}
)}
From 785bf571688fa9b63b55d190908aec7fc98932d7 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Mon, 25 Jul 2022 19:22:54 +0300
Subject: [PATCH 29/30] Make it work with dashboard Query
---
.../data/common/query/to_expression_ast.ts | 16 +++++++++++++++-
.../public/application/main/utils/fetch_sql.ts | 4 +++-
.../embeddable/saved_search_embeddable.tsx | 3 ++-
3 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/src/plugins/data/common/query/to_expression_ast.ts b/src/plugins/data/common/query/to_expression_ast.ts
index 659a00311d8d6..e522ea367a47e 100644
--- a/src/plugins/data/common/query/to_expression_ast.ts
+++ b/src/plugins/data/common/query/to_expression_ast.ts
@@ -9,6 +9,7 @@ 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';
@@ -17,12 +18,14 @@ import {
ExpressionFunctionKibanaContext,
QueryState,
aggregateQueryToAst,
+ queryToAst,
filtersToAst,
timerangeToAst,
} from '..';
interface Args extends QueryState {
dataViewsService: DataViewsContract;
+ inputQuery?: Query;
}
/**
@@ -31,9 +34,20 @@ interface Args extends QueryState {
* @param query kibana query or aggregate query
* @param time kibana time range
*/
-export async function queryStateToExpressionAst({ filters, query, time, dataViewsService }: Args) {
+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: q && queryToAst(q),
timeRange: time && timerangeToAst(time),
filters: filters && filtersToAst(filters),
});
diff --git a/src/plugins/discover/public/application/main/utils/fetch_sql.ts b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
index 299d3e07c8f9d..2faa821bc66ec 100644
--- a/src/plugins/discover/public/application/main/utils/fetch_sql.ts
+++ b/src/plugins/discover/public/application/main/utils/fetch_sql.ts
@@ -27,7 +27,8 @@ export function fetchSql(
dataViewsService: DataViewsContract,
data: DataPublicPluginStart,
expressions: ExpressionsStart,
- filters?: Filter[]
+ filters?: Filter[],
+ inputQuery?: Query
) {
const timeRange = data.query.timefilter.timefilter.getTime();
return queryStateToExpressionAst({
@@ -35,6 +36,7 @@ export function fetchSql(
query,
time: timeRange,
dataViewsService,
+ inputQuery,
})
.then((ast) => {
if (ast) {
diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
index e61e523445a0c..c693331f66922 100644
--- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
+++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx
@@ -219,7 +219,8 @@ export class SavedSearchEmbeddable
this.services.indexPatterns,
this.services.data,
this.services.expressions,
- this.input.filters
+ this.input.filters,
+ this.input.query
);
this.updateOutput({
...this.getOutput(),
From 51ad131a334aaf881fb2aff83a0e573e75a82808 Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 26 Jul 2022 09:23:03 +0300
Subject: [PATCH 30/30] Address some of the comments
---
.../public/components/discover_grid/discover_grid.scss | 2 +-
.../dataview_picker/text_languages_transition_modal.tsx | 9 ++-------
.../text_based_languages_editor/editor_footer.tsx | 9 +++++++--
.../text_based_languages_editor/index.tsx | 1 -
.../text_based_languages_editor/overwrite.scss | 4 ++++
.../text_based_languages_editor.styles.ts | 2 +-
6 files changed, 15 insertions(+), 12 deletions(-)
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 011a180177972..3ac2055650128 100644
--- a/src/plugins/discover/public/components/discover_grid/discover_grid.scss
+++ b/src/plugins/discover/public/components/discover_grid/discover_grid.scss
@@ -16,7 +16,7 @@
}
.dscDiscoverGrid__textLanguageMode .euiDataGridRowCell.euiDataGridRowCell--firstColumn {
- padding: 4px;
+ padding: $euiSizeXS;
}
.euiDataGridRowCell.euiDataGridRowCell--lastColumn {
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
index 40d12297b49d1..12f5414b92b9e 100644
--- 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
@@ -8,7 +8,6 @@
import React, { useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
-import { css } from '@emotion/react';
import {
EuiModal,
@@ -65,11 +64,7 @@ export default function TextBasedLanguagesTransitionModal({
-
+
-
+
closeModal(dismissModalChecked)}
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
index 606d8dd0b2e89..c9baf17eeec8d 100644
--- 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
@@ -105,7 +105,7 @@ export const EditorFooter = memo(function EditorFooter({
{errors.map((error, index) => {
return (
-
+
@@ -126,7 +126,12 @@ export const EditorFooter = memo(function EditorFooter({
- {error.message}
+
+ {error.message}
+
);
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
index fe193a5438212..222d890572bc1 100644
--- 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
@@ -602,7 +602,6 @@ export const TextBasedLanguagesEditor = memo(function TextBasedLanguagesEditor({
css={styles.dragResizeButton}
/>
)}
- {userDrags &&
}
)}
>
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
index 18cea3057d5a8..cad8d05191d9b 100644
--- 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
@@ -38,4 +38,8 @@
.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/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
index 5f44088b6d6d5..94fe389b062ec 100644
--- 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
@@ -78,7 +78,7 @@ export const textBasedLanguagedEditorStyles = (
width: 'calc(100% + 2px)',
position: 'relative' as 'relative', // cast string to type 'relative',
marginTop: 0,
- marginLeft: '-1px',
+ marginLeft: 0,
borderBottomLeftRadius: '6px',
borderBottomRightRadius: '6px',
},