From 21cbaf9c5187c3835af77e779823b80d7ad16fbc Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Mon, 10 Jun 2024 20:38:35 +0000 Subject: [PATCH 1/7] [Discover-next] add query editor and toggle Signed-off-by: Kawika Avilla Restore language switcher Signed-off-by: Kawika Avilla delete unused styles Signed-off-by: Kawika Avilla clean up unneeded references Signed-off-by: Kawika Avilla clean up unused code paths and add test Signed-off-by: Kawika Avilla fix tests Signed-off-by: Kawika Avilla Unify settings with enhancements Signed-off-by: Kawika Avilla More clean up related to combining settings Signed-off-by: Kawika Avilla update snapshot Signed-off-by: Kawika Avilla Set default ui setting to false Signed-off-by: Kawika Avilla --- .../dashboard_listing.test.tsx.snap | 8263 ++++++++++++++++- .../dashboard_top_nav.test.tsx.snap | 72 +- src/plugins/data/common/constants.ts | 8 +- src/plugins/data/config.ts | 3 + src/plugins/data/public/ui/_index.scss | 1 + .../ui/filter_bar/_global_filter_group.scss | 12 + src/plugins/data/public/ui/index.ts | 9 +- src/plugins/data/public/ui/mocks.ts | 11 +- .../data/public/ui/query_editor/_index.scss | 2 + .../ui/query_editor/_language_selector.scss | 14 + .../public/ui/query_editor/_query_editor.scss | 44 + .../ui/query_editor/fetch_index_patterns.ts | 42 + .../data/public/ui/query_editor/index.tsx | 26 + .../query_editor/language_selector.test.tsx | 83 + .../ui/query_editor/language_selector.tsx | 102 + .../ui/query_editor/no_data_popover.test.tsx | 81 + .../ui/query_editor/no_data_popover.tsx | 85 + .../query_editor/query_editor.test.mocks.ts | 34 + .../public/ui/query_editor/query_editor.tsx | 306 + .../ui/query_editor/query_editor_top_row.tsx | 409 + .../public/ui/query_string_input/_index.scss | 1 - .../_language_switcher.scss | 8 - .../language_switcher.test.tsx | 49 +- .../query_string_input/language_switcher.tsx | 184 +- .../legacy_language_switcher.test.tsx | 55 - .../legacy_language_switcher.tsx | 122 - .../query_bar_top_row.test.tsx | 2 - .../query_string_input/query_bar_top_row.tsx | 87 +- .../query_string_input.test.tsx | 18 +- .../query_string_input/query_string_input.tsx | 72 +- .../ui/search_bar/create_search_bar.tsx | 19 +- .../data/public/ui/search_bar/search_bar.tsx | 84 +- .../data/public/ui/settings/settings.ts | 58 +- src/plugins/data/public/ui/types.ts | 5 +- src/plugins/data/public/ui/ui_service.ts | 21 +- src/plugins/data/server/ui_settings.ts | 43 +- .../public/components/sidebar/index.tsx | 86 +- .../public/code_editor/editor_theme.ts | 131 +- 38 files changed, 10045 insertions(+), 607 deletions(-) create mode 100644 src/plugins/data/public/ui/query_editor/_index.scss create mode 100644 src/plugins/data/public/ui/query_editor/_language_selector.scss create mode 100644 src/plugins/data/public/ui/query_editor/_query_editor.scss create mode 100644 src/plugins/data/public/ui/query_editor/fetch_index_patterns.ts create mode 100644 src/plugins/data/public/ui/query_editor/index.tsx create mode 100644 src/plugins/data/public/ui/query_editor/language_selector.test.tsx create mode 100644 src/plugins/data/public/ui/query_editor/language_selector.tsx create mode 100644 src/plugins/data/public/ui/query_editor/no_data_popover.test.tsx create mode 100644 src/plugins/data/public/ui/query_editor/no_data_popover.tsx create mode 100644 src/plugins/data/public/ui/query_editor/query_editor.test.mocks.ts create mode 100644 src/plugins/data/public/ui/query_editor/query_editor.tsx create mode 100644 src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx delete mode 100644 src/plugins/data/public/ui/query_string_input/_language_switcher.scss delete mode 100644 src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx delete mode 100644 src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx diff --git a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap index 4be28c0c4d0..34d345db4f6 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -501,8 +501,16 @@ exports[`dashboard listing hideWriteControls 1`] = ` "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -1157,11 +1165,1481 @@ exports[`dashboard listing hideWriteControls 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + +
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -1674,8 +3152,16 @@ exports[`dashboard listing render table listing with initial filters from URL 1` "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -2391,11 +3877,2151 @@ exports[`dashboard listing render table listing with initial filters from URL 1` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "dashboard", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -2908,8 +6534,16 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -3625,11 +7259,274 @@ exports[`dashboard listing renders call to action when no dashboards exist 1`] = data-test-subj="dashboardLandingPage" >
+ > + + +
+ + + + } + body={ + +

+ +

+

+ + + , + } + } + /> +

+
+ } + iconType="dashboardApp" + title={ +

+ +

+ } + > +
+ + + + +
+ + +

+ + Create your first dashboard + +

+
+ + + +
+ + +
+

+ + You can combine data views from any OpenSearch Dashboards app into one dashboard and see everything in one place. + +

+

+ + + , + } + } + > + New to OpenSearch Dashboards? + + + + to take a test drive. + +

+
+
+ + + +
+ + + + + + +
+ +
+ + +
@@ -4142,8 +8039,16 @@ exports[`dashboard listing renders table rows 1`] = ` "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -4859,11 +8764,2111 @@ exports[`dashboard listing renders table rows 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
@@ -5376,8 +11381,16 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -6093,11 +12106,2231 @@ exports[`dashboard listing renders warning when listingLimit is exceeded 1`] = ` data-test-subj="dashboardLandingPage" >
+ > + + +
+
+ +
+ +
+ +

+ Dashboards +

+
+
+
+ + +
+ + + + + +
+
+
+
+
+ +
+ + + } + > +
+
+ + + + Listing limit exceeded + + +
+ +
+ +
+

+ + + , + "entityNamePlural": "dashboards", + "listingLimitText": + listingLimit + , + "listingLimitValue": 1, + "totalItems": 2, + } + } + > + You have 2 dashboards, but your + + listingLimit + + setting prevents the table below from displaying more than 1. You can change this setting under + + + + Advanced Settings + + + + . + +

+
+
+
+
+
+
+ +
+ + + } + pagination={ + Object { + "initialPageIndex": 0, + "initialPageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + } + } + responsive={true} + search={ + Object { + "box": Object { + "incremental": true, + }, + "defaultQuery": "", + "onChange": [Function], + "toolsLeft": undefined, + } + } + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={true} + tableLayout="fixed" + > +
+ + +
+ +
+ + + +
+
+ + + + +
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + } + onChange={[Function]} + pagination={ + Object { + "hidePerPageOptions": undefined, + "pageIndex": 0, + "pageSize": 10, + "pageSizeOptions": Array [ + 10, + 20, + 50, + ], + "totalItemCount": 2, + } + } + responsive={true} + selection={ + Object { + "onSelectionChange": [Function], + } + } + sorting={ + Object { + "allowNeutralSort": true, + "sort": undefined, + } + } + tableLayout="fixed" + > +
+
+ +
+ +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + +
+ +
+
+ + +
+
+ + + + + + + + + + + + + + Actions + + + + + +
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard0 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+ + +
+ +
+
+ + +
+
+
+ Title +
+
+ + + +
+
+
+ Type +
+
+ + dashboardSavedObjects + +
+
+
+ Description +
+
+ + dashboard1 desc + +
+
+
+ Last updated +
+
+
+
+ + + + + + + + + + Edit + + + + + + +
+
+
+
+ +
+ +
+ + + +
+ +
+ + + : + 10 + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > +
+
+ + + +
+
+
+
+
+ +
+ + + +
+
+
+
+
+
+ +
+ +
+ +
+
+ + +
diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 5f71c7d5d21..16df84a9ab9 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -393,8 +393,16 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -1392,8 +1400,16 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -2391,8 +2407,16 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -3390,8 +3414,16 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -4389,8 +4421,16 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { @@ -5388,8 +5428,16 @@ exports[`Dashboard top nav render with all components 1`] = ` "IndexPatternSelect": [MockFunction], "SearchBar": [MockFunction], "Settings": undefined, - "isEnhancementsEnabled": false, - "queryEnhancements": Map {}, + "container$": BehaviorSubject { + "_isScalar": false, + "_value": null, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "containerRef":
, }, }, "docLinks": Object { diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 27cfc64cf2f..feef2097e61 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -35,7 +35,6 @@ export const UI_SETTINGS = { DOC_HIGHLIGHT: 'doc_table:highlight', QUERY_STRING_OPTIONS: 'query:queryString:options', QUERY_ALLOW_LEADING_WILDCARDS: 'query:allowLeadingWildcards', - QUERY_DATA_SOURCE_READONLY: 'query:dataSourceReadOnly', SEARCH_QUERY_LANGUAGE: 'search:queryLanguage', SORT_OPTIONS: 'sort:options', COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: 'courier:ignoreFilterIfFieldNotInIndex', @@ -61,5 +60,10 @@ export const UI_SETTINGS = { INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder', FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault', FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', - DATAFRAME_HYDRATION_STRATEGY: 'dataframe:hydrationStrategy', + QUERY_ENHANCEMENTS_ENABLED: 'query:enhancements:enabled', + QUERY_DATAFRAME_HYDRATION_STRATEGY: 'query:dataframe:hydrationStrategy', + QUERY_POLLING_INTERVAL: 'query:async:pollingInterval', + QUERY_DATA_SOURCE_READONLY: 'query:dataSource:readOnly', + QUERY_SEARCH_BAR_EXTENSIONS_ENABLED: 'query:searchBarExtensions:enabled', + SEARCH_QUERY_LANGUAGE_BLOCKLIST: 'search:queryLanguageBlocklist', } as const; diff --git a/src/plugins/data/config.ts b/src/plugins/data/config.ts index f8dcad85fb4..53252fb74b4 100644 --- a/src/plugins/data/config.ts +++ b/src/plugins/data/config.ts @@ -33,6 +33,9 @@ import { schema, TypeOf } from '@osd/config-schema'; export const configSchema = schema.object({ enhancements: schema.object({ enabled: schema.boolean({ defaultValue: false }), + supportedAppNames: schema.arrayOf(schema.string(), { + defaultValue: ['discover'], + }), }), autocomplete: schema.object({ querySuggestions: schema.object({ diff --git a/src/plugins/data/public/ui/_index.scss b/src/plugins/data/public/ui/_index.scss index f8998bb1481..f7c738b8d09 100644 --- a/src/plugins/data/public/ui/_index.scss +++ b/src/plugins/data/public/ui/_index.scss @@ -2,4 +2,5 @@ @import "./typeahead/index"; @import "./saved_query_management/index"; @import "./query_string_input/index"; +@import "./query_editor/index"; @import "./shard_failure_modal/shard_failure_modal"; diff --git a/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss index 98a7ac648ad..f1af6e7f9b7 100644 --- a/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss +++ b/src/plugins/data/public/ui/filter_bar/_global_filter_group.scss @@ -11,6 +11,18 @@ padding-bottom: $euiSizeS; } +.globalQueryEditor { + padding: 0 $euiSizeXS $euiSizeXS $euiSizeXS; +} + +.globalQueryEditor:first-child { + padding-top: $euiSizeXS; +} + +.globalQueryEditor:not(:empty) { + padding-bottom: $euiSizeXS; +} + .globalFilterGroup__filterBar { margin-top: $euiSizeXS; } diff --git a/src/plugins/data/public/ui/index.ts b/src/plugins/data/public/ui/index.ts index 98e6d393ce6..582470cc997 100644 --- a/src/plugins/data/public/ui/index.ts +++ b/src/plugins/data/public/ui/index.ts @@ -28,7 +28,14 @@ * under the License. */ -export { UiEnhancements, IUiStart, createSettings, Settings, DataSettings } from './types'; +export { + UiEnhancements, + IUiStart, + IUiSetup, + createSettings, + Settings, + DataSettings, +} from './types'; export { IndexPatternSelectProps } from './index_pattern_select'; export { FilterLabel } from './filter_bar'; export { QueryStringInput, QueryStringInputProps } from './query_string_input'; diff --git a/src/plugins/data/public/ui/mocks.ts b/src/plugins/data/public/ui/mocks.ts index 47d3f059f50..0a8ea807f21 100644 --- a/src/plugins/data/public/ui/mocks.ts +++ b/src/plugins/data/public/ui/mocks.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { BehaviorSubject } from 'rxjs'; import { SettingsMock } from './settings/mocks'; import { IUiSetup, IUiStart } from './types'; @@ -32,11 +33,15 @@ function createSetupContract(): jest.Mocked { function createStartContract(isEnhancementsEnabled: boolean = false): jest.Mocked { const queryEnhancements = new Map(); return { - isEnhancementsEnabled, - queryEnhancements, IndexPatternSelect: jest.fn(), SearchBar: jest.fn(), - Settings: new SettingsMock(createMockStorage(), queryEnhancements), + Settings: new SettingsMock( + { enabled: isEnhancementsEnabled, supportedAppNames: ['discover'] }, + createMockStorage(), + queryEnhancements + ), + containerRef: document.createElement('div'), + container$: new BehaviorSubject(null), }; } diff --git a/src/plugins/data/public/ui/query_editor/_index.scss b/src/plugins/data/public/ui/query_editor/_index.scss new file mode 100644 index 00000000000..64fb0056cb7 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/_index.scss @@ -0,0 +1,2 @@ +@import "./language_selector"; +@import "./query_editor"; diff --git a/src/plugins/data/public/ui/query_editor/_language_selector.scss b/src/plugins/data/public/ui/query_editor/_language_selector.scss new file mode 100644 index 00000000000..a6747eb1b8d --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/_language_selector.scss @@ -0,0 +1,14 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +.languageSelector { + max-width: 140px; + height: 100%; + + &:first-child { + div:first-child { + height: 100%; + } + } +} diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss new file mode 100644 index 00000000000..1103762e059 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.osdQueryEditor__wrap { + height: 200px; + width: 500px; +} + +.osdQueryEditorHeader { + max-height: 400px; + + // TODO fix styling: with "overflow: auto" the scroll bar appears although the content is below max-height + // overflow: auto; +} + +@include euiBreakpoint("xs", "s") { + .osdQueryEditor--withDatePicker { + > :first-child { + // Change the order of the query bar and date picker so that the date picker is top + // and the query bar still aligns with filters + order: 1; + + // EUI Flexbox adds too much margin between responded items, this just moves it up + margin-top: $euiSizeS * -1; + } + } +} + +// IE specific fix for the datepicker to not collapse +@include euiBreakpoint("m", "l", "xl") { + .osdQueryEditor__datePickerWrapper { + max-width: 40vw; + flex-grow: 0 !important; + flex-basis: auto !important; + + &.osdQueryEditor__datePickerWrapper-isHidden { + width: 0; + overflow: hidden; + max-width: 0; + } + } +} diff --git a/src/plugins/data/public/ui/query_editor/fetch_index_patterns.ts b/src/plugins/data/public/ui/query_editor/fetch_index_patterns.ts new file mode 100644 index 00000000000..ef0134f4278 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/fetch_index_patterns.ts @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isEmpty } from 'lodash'; +import { IUiSettingsClient, SavedObjectsClientContract } from 'src/core/public'; +import { indexPatterns, IndexPatternAttributes } from '../..'; + +export async function fetchIndexPatterns( + savedObjectsClient: SavedObjectsClientContract, + indexPatternStrings: string[], + uiSettings: IUiSettingsClient +) { + if (!indexPatternStrings || isEmpty(indexPatternStrings)) { + return []; + } + + const searchString = indexPatternStrings.map((string) => `"${string}"`).join(' | '); + const indexPatternsFromSavedObjects = await savedObjectsClient.find({ + type: 'index-pattern', + fields: ['title', 'fields'], + search: searchString, + searchFields: ['title'], + }); + + const exactMatches = indexPatternsFromSavedObjects.savedObjects.filter((savedObject) => { + return indexPatternStrings.includes(savedObject.attributes.title); + }); + + const defaultIndex = uiSettings.get('defaultIndex'); + + const allMatches = + exactMatches.length === indexPatternStrings.length + ? exactMatches + : [ + ...exactMatches, + await savedObjectsClient.get('index-pattern', defaultIndex), + ]; + + return allMatches.map(indexPatterns.getFromSavedObject); +} diff --git a/src/plugins/data/public/ui/query_editor/index.tsx b/src/plugins/data/public/ui/query_editor/index.tsx new file mode 100644 index 00000000000..20ec9ca4e03 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/index.tsx @@ -0,0 +1,26 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { withOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import type { QueryEditorTopRowProps } from './query_editor_top_row'; +import type { QueryEditorProps } from './query_editor'; + +const Fallback = () =>
; + +const LazyQueryEditorTopRow = React.lazy(() => import('./query_editor_top_row')); +export const QueryEditorTopRow = (props: QueryEditorTopRowProps) => ( + }> + + +); + +const LazyQueryEditorUI = withOpenSearchDashboards(React.lazy(() => import('./query_editor'))); +export const QueryEditor = (props: QueryEditorProps) => ( + }> + + +); +export type { QueryEditorProps }; diff --git a/src/plugins/data/public/ui/query_editor/language_selector.test.tsx b/src/plugins/data/public/ui/query_editor/language_selector.test.tsx new file mode 100644 index 00000000000..3d7f9752817 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/language_selector.test.tsx @@ -0,0 +1,83 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { QueryLanguageSelector } from './language_selector'; +import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; +import { coreMock } from '../../../../../core/public/mocks'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { EuiComboBox } from '@elastic/eui'; +import { QueryEnhancement } from '../types'; + +const startMock = coreMock.createStart(); + +jest.mock('../../services', () => ({ + getUiService: () => ({ + Settings: { + getAllQueryEnhancements: () => new Map(), + setUiOverridesByUserQueryLanguage: jest.fn(), + }, + }), + getSearchService: () => ({ + __enhance: jest.fn(), + df: { + clear: jest.fn(), + }, + getDefaultSearchInterceptor: jest.fn(), + }), +})); + +describe('LanguageSelector', () => { + function wrapInContext(testProps: any) { + const services = { + uiSettings: startMock.uiSettings, + docLinks: startMock.docLinks, + }; + + return ( + + + + ); + } + + it('should select lucene if language is lucene', () => { + const component = mountWithIntl( + wrapInContext({ + language: 'lucene', + onSelectLanguage: () => { + return; + }, + }) + ); + const euiComboBox = component.find(EuiComboBox); + expect(euiComboBox.prop('selectedOptions')).toEqual( + expect.arrayContaining([ + { + label: 'Lucene', + }, + ]) + ); + }); + + it('should select DQL if language is kuery', () => { + const component = mountWithIntl( + wrapInContext({ + language: 'kuery', + onSelectLanguage: () => { + return; + }, + }) + ); + const euiComboBox = component.find(EuiComboBox); + expect(euiComboBox.prop('selectedOptions')).toEqual( + expect.arrayContaining([ + { + label: 'DQL', + }, + ]) + ); + }); +}); diff --git a/src/plugins/data/public/ui/query_editor/language_selector.tsx b/src/plugins/data/public/ui/query_editor/language_selector.tsx new file mode 100644 index 00000000000..3aa1357e351 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/language_selector.tsx @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiComboBox, EuiComboBoxOptionOption, PopoverAnchorPosition } from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import React from 'react'; +import { getSearchService, getUiService } from '../../services'; + +interface Props { + language: string; + onSelectLanguage: (newLanguage: string) => void; + anchorPosition?: PopoverAnchorPosition; + appName?: string; +} + +const mapExternalLanguageToOptions = (language: string) => { + return { + label: language, + value: language, + }; +}; + +export const QueryLanguageSelector = (props: Props) => { + const dqlLabel = i18n.translate('data.query.queryEditor.dqlLanguageName', { + defaultMessage: 'DQL', + }); + const luceneLabel = i18n.translate('data.query.queryEditor.luceneLanguageName', { + defaultMessage: 'Lucene', + }); + + const languageOptions: EuiComboBoxOptionOption[] = [ + { + label: dqlLabel, + value: 'kuery', + }, + { + label: luceneLabel, + value: 'lucene', + }, + ]; + + const uiService = getUiService(); + const searchService = getSearchService(); + + const queryEnhancements = uiService.Settings.getAllQueryEnhancements(); + queryEnhancements.forEach((enhancement) => { + if ( + (enhancement.supportedAppNames && + props.appName && + !enhancement.supportedAppNames.includes(props.appName)) || + uiService.Settings.getUserQueryLanguageBlocklist().includes( + enhancement.language.toLowerCase() + ) + ) + return; + languageOptions.unshift(mapExternalLanguageToOptions(enhancement.language)); + }); + + const selectedLanguage = { + label: + (languageOptions.find( + (option) => (option.value as string).toLowerCase() === props.language.toLowerCase() + )?.label as string) ?? languageOptions[0].label, + }; + + const setSearchEnhance = (queryLanguage: string) => { + const queryEnhancement = queryEnhancements.get(queryLanguage); + searchService.__enhance({ + searchInterceptor: queryEnhancement + ? queryEnhancement.search + : searchService.getDefaultSearchInterceptor(), + }); + + if (!queryEnhancement) { + searchService.df.clear(); + } + uiService.Settings.setUiOverridesByUserQueryLanguage(queryLanguage); + }; + + const handleLanguageChange = (newLanguage: EuiComboBoxOptionOption[]) => { + const queryLanguage = newLanguage[0].value as string; + props.onSelectLanguage(queryLanguage); + setSearchEnhance(queryLanguage); + }; + + setSearchEnhance(props.language); + + return ( + + ); +}; diff --git a/src/plugins/data/public/ui/query_editor/no_data_popover.test.tsx b/src/plugins/data/public/ui/query_editor/no_data_popover.test.tsx new file mode 100644 index 00000000000..1a24f1a7310 --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/no_data_popover.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { NoDataPopover } from './no_data_popover'; +import { EuiTourStep } from '@elastic/eui'; +import { act } from 'react-dom/test-utils'; + +describe('NoDataPopover', () => { + const createMockStorage = () => ({ + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }); + + it('should hide popover if showNoDataPopover is set to false', () => { + const Child = () => ; + const instance = mount( + + + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + expect(instance.find(EuiTourStep).find(Child)).toHaveLength(1); + }); + + it('should hide popover if showNoDataPopover is set to true, but local storage flag is set', () => { + const child = ; + const storage = createMockStorage(); + storage.get.mockReturnValue(true); + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should render popover if showNoDataPopover is set to true and local storage flag is not set', () => { + const child = ; + const instance = mount( + + {child} + + ); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(true); + }); + + it('should hide popover if it is closed', async () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('closePopover')!(); + }); + instance.setProps({ ...props }); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); + + it('should set local storage flag and hide on closing with button', () => { + const props = { + children: , + showNoDataPopover: true, + storage: createMockStorage(), + }; + const instance = mount(); + act(() => { + instance.find(EuiTourStep).prop('footerAction')!.props.onClick(); + }); + instance.setProps({ ...props }); + expect(props.storage.set).toHaveBeenCalledWith(expect.any(String), true); + expect(instance.find(EuiTourStep).prop('isStepOpen')).toBe(false); + }); +}); diff --git a/src/plugins/data/public/ui/query_editor/no_data_popover.tsx b/src/plugins/data/public/ui/query_editor/no_data_popover.tsx new file mode 100644 index 00000000000..49490e258bf --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/no_data_popover.tsx @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ReactElement, useEffect, useState } from 'react'; +import React from 'react'; +import { EuiButtonEmpty, EuiText, EuiTourStep } from '@elastic/eui'; +import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; +import { i18n } from '@osd/i18n'; + +const NO_DATA_POPOVER_STORAGE_KEY = 'data.noDataPopover'; + +export function NoDataPopover({ + showNoDataPopover, + storage, + children, +}: { + showNoDataPopover?: boolean; + storage: IStorageWrapper; + children: ReactElement; +}) { + const [noDataPopoverDismissed, setNoDataPopoverDismissed] = useState(() => + Boolean(storage.get(NO_DATA_POPOVER_STORAGE_KEY)) + ); + const [noDataPopoverVisible, setNoDataPopoverVisible] = useState(false); + + useEffect(() => { + if (showNoDataPopover && !noDataPopoverDismissed) { + setNoDataPopoverVisible(true); + } + }, [noDataPopoverDismissed, showNoDataPopover]); + + return ( + {}} + closePopover={() => { + setNoDataPopoverVisible(false); + }} + content={ + +

+ {i18n.translate('data.noDataPopover.content', { + defaultMessage: + "This time range doesn't contain any data. Increase or adjust the time range to see more fields and create charts.", + })} +

+
+ } + minWidth={300} + anchorPosition="downCenter" + anchorClassName="eui-displayBlock" + step={1} + stepsTotal={1} + isStepOpen={noDataPopoverVisible} + subtitle={i18n.translate('data.noDataPopover.subtitle', { defaultMessage: 'Tip' })} + title={i18n.translate('data.noDataPopover.title', { defaultMessage: 'Empty dataset' })} + footerAction={ + { + storage.set(NO_DATA_POPOVER_STORAGE_KEY, true); + setNoDataPopoverDismissed(true); + setNoDataPopoverVisible(false); + }} + > + {i18n.translate('data.noDataPopover.dismissAction', { + defaultMessage: "Don't show again", + })} + + } + > +
{ + setNoDataPopoverVisible(false); + }} + > + {children} +
+
+ ); +} diff --git a/src/plugins/data/public/ui/query_editor/query_editor.test.mocks.ts b/src/plugins/data/public/ui/query_editor/query_editor.test.mocks.ts new file mode 100644 index 00000000000..92408fd7b4b --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor.test.mocks.ts @@ -0,0 +1,34 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { stubIndexPatternWithFields } from '../../stubs'; + +export const mockPersistedLog = { + add: jest.fn(), + get: jest.fn(() => ['response:200']), +}; + +export const mockPersistedLogFactory = jest.fn, any>(() => { + return mockPersistedLog; +}); + +export const mockFetchIndexPatterns = jest + .fn() + .mockReturnValue(Promise.resolve([stubIndexPatternWithFields])); + +jest.mock('../../query/persisted_log', () => ({ + PersistedLog: mockPersistedLogFactory, +})); + +jest.mock('./fetch_index_patterns', () => ({ + fetchIndexPatterns: mockFetchIndexPatterns, +})); + +import _ from 'lodash'; +// Using doMock to avoid hoisting so that I can override only the debounce method in lodash +jest.doMock('lodash', () => ({ + ..._, + debounce: (func: () => any) => func, +})); diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx new file mode 100644 index 00000000000..98ba64466ac --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -0,0 +1,306 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiFlexGroup, EuiFlexItem, htmlIdGenerator, PopoverAnchorPosition } from '@elastic/eui'; +import classNames from 'classnames'; +import { isEqual } from 'lodash'; +import React, { Component, createRef, RefObject } from 'react'; +import { Settings } from '..'; +import { IDataPluginServices, IIndexPattern, Query, TimeRange } from '../..'; +import { + CodeEditor, + OpenSearchDashboardsReactContextValue, +} from '../../../../opensearch_dashboards_react/public'; +import { QuerySuggestion } from '../../autocomplete'; +import { fromUser, getQueryLog, PersistedLog, toUser } from '../../query'; +import { SuggestionsListSize } from '../typeahead/suggestions_component'; +import { DataSettings, QueryEnhancement } from '../types'; +import { fetchIndexPatterns } from './fetch_index_patterns'; +import { QueryLanguageSelector } from './language_selector'; + +export interface QueryEditorProps { + indexPatterns: Array; + query: Query; + containerRef?: React.RefCallback; + settings: Settings; + disableAutoFocus?: boolean; + screenTitle?: string; + prepend?: any; + persistedLog?: PersistedLog; + bubbleSubmitEvent?: boolean; + placeholder?: string; + languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; + onBlur?: () => void; + onChange?: (query: Query, dateRange?: TimeRange) => void; + onChangeQueryEditorFocus?: (isFocused: boolean) => void; + onSubmit?: (query: Query, dateRange?: TimeRange) => void; + getQueryStringInitialValue?: (language: string) => string; + // TODO: MQL datasources: we should consider this + // getQueryStringDataSource?: (language: string) => string; + dataTestSubj?: string; + size?: SuggestionsListSize; + className?: string; + isInvalid?: boolean; + queryEditorHeaderRef: React.RefObject; + queryEditorHeaderClassName?: string; +} + +interface Props extends QueryEditorProps { + opensearchDashboards: OpenSearchDashboardsReactContextValue; +} + +interface State { + queryEnhancements: Map; + isSuggestionsVisible: boolean; + index: number | null; + suggestions: QuerySuggestion[]; + indexPatterns: IIndexPattern[]; + queryEditorRect: DOMRect | undefined; +} + +const KEY_CODES = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + ENTER: 13, + ESC: 27, + TAB: 9, + HOME: 36, + END: 35, +}; + +// Needed for React.lazy +// TODO: MQL export this and let people extended this +// eslint-disable-next-line import/no-default-export +export default class QueryEditorUI extends Component { + public state: State = { + queryEnhancements: new Map(), + isSuggestionsVisible: false, + index: null, + suggestions: [], + indexPatterns: [], + queryEditorRect: undefined, + }; + + public inputRef: HTMLTextAreaElement | null = null; + + private persistedLog: PersistedLog | undefined; + private abortController?: AbortController; + private services = this.props.opensearchDashboards.services; + private componentIsUnmounting = false; + private queryEditorDivRefInstance: RefObject = createRef(); + + private getQueryString = () => { + if (!this.props.query.query) { + return this.props.getQueryStringInitialValue?.(this.props.query.language) ?? ''; + } + return toUser(this.props.query.query); + }; + + // TODO: MQL don't do this here? || Fetch data sources + private fetchIndexPatterns = async () => { + const stringPatterns = this.props.indexPatterns.filter( + (indexPattern) => typeof indexPattern === 'string' + ) as string[]; + const objectPatterns = this.props.indexPatterns.filter( + (indexPattern) => typeof indexPattern !== 'string' + ) as IIndexPattern[]; + + const objectPatternsFromStrings = (await fetchIndexPatterns( + this.services.savedObjects!.client, + stringPatterns, + this.services.uiSettings! + )) as IIndexPattern[]; + + this.setState({ + indexPatterns: [...objectPatterns, ...objectPatternsFromStrings], + }); + }; + + private onSubmit = (query: Query, dateRange?: TimeRange) => { + if (this.props.onSubmit) { + if (this.persistedLog) { + this.persistedLog.add(query.query); + } + + this.props.onSubmit({ query: fromUser(query.query), language: query.language }); + } + }; + + private onChange = (query: Query, dateRange?: TimeRange) => { + if (this.props.onChange) { + this.props.onChange({ query: fromUser(query.query), language: query.language }, dateRange); + } + }; + + private onQueryStringChange = (value: string) => { + this.setState({ + isSuggestionsVisible: true, + index: null, + }); + + this.onChange({ query: value, language: this.props.query.language }); + }; + + private onInputChange = (value: string) => { + this.onQueryStringChange(value); + }; + + private onClickInput = (event: React.MouseEvent) => { + if (event.target instanceof HTMLTextAreaElement) { + this.onQueryStringChange(event.target.value); + } + }; + + // TODO: MQL consider moving language select language of setting search source here + private onSelectLanguage = (language: string) => { + // Send telemetry info every time the user opts in or out of kuery + // As a result it is important this function only ever gets called in the + // UI component's change handler. + this.services.http.post('/api/opensearch-dashboards/dql_opt_in_stats', { + body: JSON.stringify({ opt_in: language === 'kuery' }), + }); + + const newQuery = { + query: this.props.getQueryStringInitialValue?.(language) ?? '', + language, + }; + + const fields = this.props.settings.getQueryEnhancements(newQuery.language)?.fields; + const newSettings: DataSettings = { + userQueryLanguage: newQuery.language, + userQueryString: newQuery.query, + ...(fields && { uiOverrides: { fields } }), + }; + this.props.settings?.updateSettings(newSettings); + + const dateRangeEnhancement = this.props.settings.getQueryEnhancements(language)?.searchBar + ?.dateRange; + const dateRange = dateRangeEnhancement + ? { + from: dateRangeEnhancement.initialFrom!, + to: dateRangeEnhancement.initialTo!, + } + : undefined; + this.onChange(newQuery, dateRange); + this.onSubmit(newQuery, dateRange); + }; + + private initPersistedLog = () => { + const { uiSettings, storage, appName } = this.services; + this.persistedLog = this.props.persistedLog + ? this.props.persistedLog + : getQueryLog(uiSettings, storage, appName, this.props.query.language); + }; + + public onMouseEnterSuggestion = (index: number) => { + this.setState({ index }); + }; + + textareaId = htmlIdGenerator()(); + + public componentDidMount() { + const parsedQuery = fromUser(toUser(this.props.query.query)); + if (!isEqual(this.props.query.query, parsedQuery)) { + this.onChange({ ...this.props.query, query: parsedQuery }); + } + + this.initPersistedLog(); + // this.fetchIndexPatterns().then(this.updateSuggestions); + this.handleListUpdate(); + + window.addEventListener('scroll', this.handleListUpdate, { + passive: true, // for better performance as we won't call preventDefault + capture: true, // scroll events don't bubble, they must be captured instead + }); + } + + public componentDidUpdate(prevProps: Props) { + const parsedQuery = fromUser(toUser(this.props.query.query)); + if (!isEqual(this.props.query.query, parsedQuery)) { + this.onChange({ ...this.props.query, query: parsedQuery }); + } + + this.initPersistedLog(); + } + + public componentWillUnmount() { + if (this.abortController) this.abortController.abort(); + this.componentIsUnmounting = true; + window.removeEventListener('scroll', this.handleListUpdate, { capture: true }); + } + + handleListUpdate = () => { + if (this.componentIsUnmounting) return; + + return this.setState({ + queryEditorRect: this.queryEditorDivRefInstance.current?.getBoundingClientRect(), + }); + }; + + handleOnFocus = () => { + if (this.props.onChangeQueryEditorFocus) { + this.props.onChangeQueryEditorFocus(true); + } + }; + + public render() { + const className = classNames(this.props.className); + + const queryEditorHeaderClassName = classNames( + 'osdQueryEditorHeader', + this.props.queryEditorHeaderClassName + ); + + return ( +
+ + + + {this.props.prepend} + + + +
+ + + + + + + + + +
+ + + +
+ ); + } +} diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx new file mode 100644 index 00000000000..c6bb1f893ea --- /dev/null +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -0,0 +1,409 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import dateMath from '@elastic/datemath'; +import classNames from 'classnames'; +import React, { useRef, useState } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiSuperDatePicker, + EuiFieldText, + prettyDuration, +} from '@elastic/eui'; +// @ts-ignore +import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; +import { isEqual, compact } from 'lodash'; +import { + IDataPluginServices, + IIndexPattern, + TimeRange, + TimeHistoryContract, + Query, + DataSource, +} from '../..'; +import { + useOpenSearchDashboards, + withOpenSearchDashboards, +} from '../../../../opensearch_dashboards_react/public'; +import QueryEditorUI from './query_editor'; +import { UI_SETTINGS } from '../../../common'; +import { PersistedLog, fromUser, getQueryLog } from '../../query'; +import { NoDataPopover } from './no_data_popover'; +import { Settings } from '../types'; + +const QueryEditor = withOpenSearchDashboards(QueryEditorUI); + +// @internal +export interface QueryEditorTopRowProps { + query?: Query; + containerRef?: React.RefCallback; + settings?: Settings; + onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; + onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; + onRefresh?: (payload: { dateRange: TimeRange }) => void; + dataTestSubj?: string; + disableAutoFocus?: boolean; + screenTitle?: string; + indexPatterns?: Array; + dataSource?: DataSource; + isLoading?: boolean; + prepend?: React.ComponentProps['prepend']; + showQueryEditor?: boolean; + showDatePicker?: boolean; + dateRangeFrom?: string; + dateRangeTo?: string; + isRefreshPaused?: boolean; + refreshInterval?: number; + showAutoRefreshOnly?: boolean; + onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; + customSubmitButton?: any; + filterBar?: any; + isDirty: boolean; + timeHistory?: TimeHistoryContract; + indicateNoData?: boolean; +} + +// Needed for React.lazy +// eslint-disable-next-line import/no-default-export +export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { + const [isDateRangeInvalid, setIsDateRangeInvalid] = useState(false); + const [isQueryEditorFocused, setIsQueryEditorFocused] = useState(false); + const queryEditorHeaderRef = useRef(null); + + const opensearchDashboards = useOpenSearchDashboards(); + const { uiSettings, storage, appName } = opensearchDashboards.services; + + const isDataSourceReadOnly = uiSettings.get(UI_SETTINGS.QUERY_DATA_SOURCE_READONLY); + + const queryLanguage = props.query && props.query.language; + const queryUiEnhancement = + (queryLanguage && + props.settings && + props.settings.getQueryEnhancements(queryLanguage)?.searchBar) || + null; + const parsedQuery = + !queryUiEnhancement || isValidQuery(props.query) + ? props.query! + : { query: getQueryStringInitialValue(queryLanguage!), language: queryLanguage! }; + if (!isEqual(parsedQuery?.query, props.query?.query)) { + onQueryChange(parsedQuery); + onSubmit({ query: parsedQuery, dateRange: getDateRange() }); + } + const persistedLog: PersistedLog | undefined = React.useMemo( + () => + queryLanguage && uiSettings && storage && appName + ? getQueryLog(uiSettings!, storage, appName, queryLanguage) + : undefined, + [appName, queryLanguage, uiSettings, storage] + ); + + function onClickSubmitButton(event: React.MouseEvent) { + if (persistedLog && props.query) { + persistedLog.add(props.query.query); + } + event.preventDefault(); + onSubmit({ query: props.query, dateRange: getDateRange() }); + } + + function getDateRange() { + const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); + return { + from: + props.dateRangeFrom || + queryUiEnhancement?.dateRange?.initialFrom || + defaultTimeSetting.from, + to: props.dateRangeTo || queryUiEnhancement?.dateRange?.initialTo || defaultTimeSetting.to, + }; + } + + function onQueryChange(query: Query, dateRange?: TimeRange) { + if (queryUiEnhancement && !isValidQuery(query)) return; + props.onChange({ + query, + dateRange: dateRange ?? getDateRange(), + }); + } + + function onChangeQueryEditorFocus(isFocused: boolean) { + setIsQueryEditorFocused(isFocused); + } + + function onTimeChange({ + start, + end, + isInvalid, + isQuickSelection, + }: { + start: string; + end: string; + isInvalid: boolean; + isQuickSelection: boolean; + }) { + setIsDateRangeInvalid(isInvalid); + const retVal = { + query: props.query, + dateRange: { + from: start, + to: end, + }, + }; + + if (isQuickSelection) { + props.onSubmit(retVal); + } else { + props.onChange(retVal); + } + } + + function onRefresh({ start, end }: OnRefreshProps) { + const retVal = { + dateRange: { + from: start, + to: end, + }, + }; + if (props.onRefresh) { + props.onRefresh(retVal); + } + } + + function onSubmit({ query, dateRange }: { query?: Query; dateRange: TimeRange }) { + if (props.timeHistory) { + props.timeHistory.add(dateRange); + } + + props.onSubmit({ query, dateRange }); + } + + function onInputSubmit(query: Query, dateRange?: TimeRange) { + onSubmit({ + query, + dateRange: dateRange ?? getDateRange(), + }); + } + + function toAbsoluteString(value: string, roundUp = false) { + const valueAsMoment = dateMath.parse(value, { roundUp }); + if (!valueAsMoment) { + return value; + } + return valueAsMoment.toISOString(); + } + + function isValidQuery(query: Query | undefined) { + if (!query || !query.query) return false; + return ( + !Array.isArray(props.indexPatterns!) || + compact(props.indexPatterns!).length === 0 || + !isDataSourceReadOnly || + fromUser(query!.query).includes( + typeof props.indexPatterns[0] === 'string' + ? props.indexPatterns[0] + : props.indexPatterns[0].title + ) + ); + } + + // TODO: MQL datasources: we should consider this + // function getQueryStringDataSource(language: string) { + // const { indexPatterns, queryEnhancements } = props; + // const input = queryEnhancements?.get(language)?.searchBar?.queryStringInput?.initialValue; + + // if ( + // !indexPatterns || + // (!Array.isArray(indexPatterns) && compact(indexPatterns).length > 0) || + // !input + // ) + // return ''; + + // const defaultDataSource = indexPatterns[0]; + // return typeof defaultDataSource === 'string' ? defaultDataSource : defaultDataSource.title; + // } + + function getQueryStringInitialValue(language: string) { + const { indexPatterns, settings } = props; + const input = settings?.getQueryEnhancements(language)?.searchBar?.queryStringInput + ?.initialValue; + + if ( + !indexPatterns || + (!Array.isArray(indexPatterns) && compact(indexPatterns).length > 0) || + !input + ) + return ''; + + const defaultDataSource = indexPatterns[0]; + const dataSource = + typeof defaultDataSource === 'string' ? defaultDataSource : defaultDataSource.title; + + return input.replace('', dataSource); + } + + function renderQueryEditor() { + if (!shouldRenderQueryEditor()) return; + return ( + + + + ); + } + + function renderSharingMetaFields() { + const { from, to } = getDateRange(); + const dateRangePretty = prettyDuration( + toAbsoluteString(from), + toAbsoluteString(to), + [], + uiSettings.get('dateFormat') + ); + return ( +
+ ); + } + + function shouldRenderDatePicker(): boolean { + return Boolean( + (props.showDatePicker && (queryUiEnhancement?.showDatePicker ?? true)) ?? + (props.showAutoRefreshOnly && (queryUiEnhancement?.showAutoRefreshOnly ?? true)) + ); + } + + function shouldRenderQueryEditor(): boolean { + // TODO: MQL probably can modify to not care about index patterns + // TODO: call queryUiEnhancement?.showQueryEditor + return Boolean( + props.showQueryEditor && props.settings && props.indexPatterns && props.query && storage + ); + } + + function renderUpdateButton() { + const button = props.customSubmitButton ? ( + React.cloneElement(props.customSubmitButton, { onClick: onClickSubmitButton }) + ) : ( + + ); + + if (!shouldRenderDatePicker()) { + return button; + } + + return ( + + + {renderDatePicker()} + {button} + + + ); + } + + function renderDatePicker() { + if (!shouldRenderDatePicker()) { + return null; + } + + let recentlyUsedRanges; + if (props.timeHistory) { + recentlyUsedRanges = props.timeHistory + .get() + .map(({ from, to }: { from: string; to: string }) => { + return { + start: from, + end: to, + }; + }); + } + + const commonlyUsedRanges = uiSettings! + .get(UI_SETTINGS.TIMEPICKER_QUICK_RANGES) + .map(({ from, to, display }: { from: string; to: string; display: string }) => { + return { + start: from, + end: to, + label: display, + }; + }); + + const wrapperClasses = classNames('osdQueryEditor__datePickerWrapper', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'osdQueryEditor__datePickerWrapper-isHidden': isQueryEditorFocused, + }); + + return ( + + + + ); + } + + const classes = classNames('osdQueryEditor', { + 'osdQueryEditor--withDatePicker': props.showDatePicker, + }); + + return ( + + {renderQueryEditor()} + + + {props.filterBar} + {renderSharingMetaFields()} + {renderUpdateButton()} + + + + ); +} + +QueryEditorTopRow.defaultProps = { + showQueryEditor: true, + showDatePicker: true, + showAutoRefreshOnly: false, +}; diff --git a/src/plugins/data/public/ui/query_string_input/_index.scss b/src/plugins/data/public/ui/query_string_input/_index.scss index f21b9cbb432..8686490016c 100644 --- a/src/plugins/data/public/ui/query_string_input/_index.scss +++ b/src/plugins/data/public/ui/query_string_input/_index.scss @@ -1,2 +1 @@ @import "./query_bar"; -@import "./language_switcher" diff --git a/src/plugins/data/public/ui/query_string_input/_language_switcher.scss b/src/plugins/data/public/ui/query_string_input/_language_switcher.scss deleted file mode 100644 index 176d072c102..00000000000 --- a/src/plugins/data/public/ui/query_string_input/_language_switcher.scss +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -.languageSelect { - max-width: 150px; - transform: translateY(-1px) translateX(-0.5px); -} diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx index dd1a3b4674c..6bfca747842 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.test.tsx @@ -33,29 +33,10 @@ import { QueryLanguageSwitcher } from './language_switcher'; import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; import { coreMock } from '../../../../../core/public/mocks'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { EuiComboBox } from '@elastic/eui'; -import { QueryEnhancement } from '../types'; - +import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; const startMock = coreMock.createStart(); -jest.mock('../../services', () => ({ - getUiService: () => ({ - isEnhancementsEnabled: true, - queryEnhancements: new Map(), - Settings: { - setUiOverridesByUserQueryLanguage: jest.fn(), - }, - }), - getSearchService: () => ({ - __enhance: jest.fn(), - df: { - clear: jest.fn(), - }, - getDefaultSearchInterceptor: jest.fn(), - }), -})); - -describe('LanguageSwitcher', () => { +describe('QueryLanguageSwitcher', () => { function wrapInContext(testProps: any) { const services = { uiSettings: startMock.uiSettings, @@ -69,7 +50,7 @@ describe('LanguageSwitcher', () => { ); } - it('should select lucene if language is lucene', () => { + it('should toggle off if language is lucene', () => { const component = mountWithIntl( wrapInContext({ language: 'lucene', @@ -78,17 +59,12 @@ describe('LanguageSwitcher', () => { }, }) ); - const euiComboBox = component.find(EuiComboBox); - expect(euiComboBox.prop('selectedOptions')).toEqual( - expect.arrayContaining([ - { - label: 'Lucene', - }, - ]) - ); + component.find(EuiButtonEmpty).simulate('click'); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); }); - it('should select DQL if language is kuery', () => { + it('should toggle on if language is kuery', () => { const component = mountWithIntl( wrapInContext({ language: 'kuery', @@ -97,13 +73,8 @@ describe('LanguageSwitcher', () => { }, }) ); - const euiComboBox = component.find(EuiComboBox); - expect(euiComboBox.prop('selectedOptions')).toEqual( - expect.arrayContaining([ - { - label: 'DQL', - }, - ]) - ); + component.find(EuiButtonEmpty).simulate('click'); + expect(component.find(EuiPopover).prop('isOpen')).toBe(true); + expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); }); }); diff --git a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx index a2d1f9ca41c..2e7770f0d03 100644 --- a/src/plugins/data/public/ui/query_string_input/language_switcher.tsx +++ b/src/plugins/data/public/ui/query_string_input/language_switcher.tsx @@ -28,100 +28,120 @@ * under the License. */ -import { EuiComboBox, EuiComboBoxOptionOption, PopoverAnchorPosition } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import React from 'react'; -import { getSearchService, getUiService } from '../../services'; +import { + EuiButtonEmpty, + EuiForm, + EuiFormRow, + EuiLink, + EuiPopover, + EuiPopoverTitle, + EuiSpacer, + EuiSwitch, + EuiText, + PopoverAnchorPosition, +} from '@elastic/eui'; +import { FormattedMessage } from '@osd/i18n/react'; +import React, { useState } from 'react'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; interface Props { language: string; onSelectLanguage: (newLanguage: string) => void; anchorPosition?: PopoverAnchorPosition; - appName?: string; -} - -function mapExternalLanguageToOptions(language: string) { - return { - label: language, - value: language, - }; } export function QueryLanguageSwitcher(props: Props) { - const dqlLabel = i18n.translate('data.query.queryBar.dqlLanguageName', { - defaultMessage: 'DQL', - }); - const luceneLabel = i18n.translate('data.query.queryBar.luceneLanguageName', { - defaultMessage: 'Lucene', - }); - - const languageOptions: EuiComboBoxOptionOption[] = [ - { - label: dqlLabel, - value: 'kuery', - }, - { - label: luceneLabel, - value: 'lucene', - }, - ]; - - const uiService = getUiService(); - const searchService = getSearchService(); - - const queryEnhancements = uiService.queryEnhancements; - if (uiService.isEnhancementsEnabled) { - queryEnhancements.forEach((enhancement) => { - if ( - enhancement.supportedAppNames && - props.appName && - !enhancement.supportedAppNames.includes(props.appName) - ) - return; - languageOptions.push(mapExternalLanguageToOptions(enhancement.language)); - }); - } - - const selectedLanguage = { - label: - (languageOptions.find( - (option) => (option.value as string).toLowerCase() === props.language.toLowerCase() - )?.label as string) ?? languageOptions[0].label, - }; - - const setSearchEnhance = (queryLanguage: string) => { - if (!uiService.isEnhancementsEnabled) return; - const queryEnhancement = queryEnhancements.get(queryLanguage); - searchService.__enhance({ - searchInterceptor: queryEnhancement - ? queryEnhancement.search - : searchService.getDefaultSearchInterceptor(), - }); + const osdDQLDocs = useOpenSearchDashboards().services.docLinks?.links.opensearchDashboards.dql + .base; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const luceneLabel = ( + + ); + const dqlLabel = ( + + ); + const dqlFullName = ( + + ); - if (!queryEnhancement) { - searchService.df.clear(); - } - uiService.Settings.setUiOverridesByUserQueryLanguage(queryLanguage); - }; + const button = ( + setIsPopoverOpen(!isPopoverOpen)} + className="euiFormControlLayout__append dqlQueryBar__languageSwitcherButton" + data-test-subj={'switchQueryLanguageButton'} + aria-label={i18n.translate('data.query.queryBar.switchQueryLanguageButtonLabel', { + defaultMessage: 'Change query language', + })} + > + {props.language === 'lucene' ? luceneLabel : dqlLabel} + + ); - const handleLanguageChange = (newLanguage: EuiComboBoxOptionOption[]) => { - const queryLanguage = newLanguage[0].value as string; - props.onSelectLanguage(queryLanguage); - setSearchEnhance(queryLanguage); - }; + return ( + setIsPopoverOpen(false)} + repositionOnScroll + > + + + +
+ +

+ + {dqlFullName} + + ), + }} + /> +

+
- setSearchEnhance(props.language); + - return ( - + + + + ) : ( + + ) + } + checked={props.language === 'kuery'} + onChange={() => { + const newLanguage = props.language === 'lucene' ? 'kuery' : 'lucene'; + props.onSelectLanguage(newLanguage); + }} + data-test-subj="languageToggle" + /> + + +
+
); } diff --git a/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx b/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx deleted file mode 100644 index 7e7e43190bb..00000000000 --- a/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { LegacyQueryLanguageSwitcher } from './legacy_language_switcher'; -import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; -import { coreMock } from '../../../../../core/public/mocks'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; -const startMock = coreMock.createStart(); - -describe('LegacyLanguageSwitcher', () => { - function wrapInContext(testProps: any) { - const services = { - uiSettings: startMock.uiSettings, - docLinks: startMock.docLinks, - }; - - return ( - - - - ); - } - - it('should toggle off if language is lucene', () => { - const component = mountWithIntl( - wrapInContext({ - language: 'lucene', - onSelectLanguage: () => { - return; - }, - }) - ); - component.find(EuiButtonEmpty).simulate('click'); - expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeFalsy(); - }); - - it('should toggle on if language is kuery', () => { - const component = mountWithIntl( - wrapInContext({ - language: 'kuery', - onSelectLanguage: () => { - return; - }, - }) - ); - component.find(EuiButtonEmpty).simulate('click'); - expect(component.find(EuiPopover).prop('isOpen')).toBe(true); - expect(component.find('[data-test-subj="languageToggle"]').get(0).props.checked).toBeTruthy(); - }); -}); diff --git a/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx b/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx deleted file mode 100644 index 3a786cfd07f..00000000000 --- a/src/plugins/data/public/ui/query_string_input/legacy_language_switcher.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { i18n } from '@osd/i18n'; -import { - EuiButtonEmpty, - EuiForm, - EuiFormRow, - EuiLink, - EuiPopover, - EuiPopoverTitle, - EuiSpacer, - EuiSwitch, - EuiText, - PopoverAnchorPosition, -} from '@elastic/eui'; -import { FormattedMessage } from '@osd/i18n/react'; -import React, { useState } from 'react'; -import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; - -interface Props { - language: string; - onSelectLanguage: (newLanguage: string) => void; - anchorPosition?: PopoverAnchorPosition; -} - -export function LegacyQueryLanguageSwitcher(props: Props) { - const osdDQLDocs = useOpenSearchDashboards().services.docLinks?.links.opensearchDashboards.dql - .base; - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const luceneLabel = ( - - ); - const dqlLabel = ( - - ); - const dqlFullName = ( - - ); - - const button = ( - setIsPopoverOpen(!isPopoverOpen)} - className="euiFormControlLayout__append dqlQueryBar__languageSwitcherButton" - data-test-subj={'switchQueryLanguageButton'} - aria-label={i18n.translate('data.query.queryBar.switchQueryLanguageButtonLabel', { - defaultMessage: 'Change query language', - })} - > - {props.language === 'lucene' ? luceneLabel : dqlLabel} - - ); - - return ( - setIsPopoverOpen(false)} - repositionOnScroll - > - - - -
- -

- - {dqlFullName} - - ), - }} - /> -

-
- - - - - - - ) : ( - - ) - } - checked={props.language === 'kuery'} - onChange={() => { - const newLanguage = props.language === 'lucene' ? 'kuery' : 'lucene'; - props.onSelectLanguage(newLanguage); - }} - data-test-subj="languageToggle" - /> - - -
-
- ); -} diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx index fa194054930..3b8c41eb1a3 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.test.tsx @@ -70,8 +70,6 @@ startMock.uiSettings.get.mockImplementation((key: string) => { from: 'now-15m', to: 'now', }; - case UI_SETTINGS.QUERY_DATA_SOURCE_READONLY: - return false; default: throw new Error(`Unexpected config key: ${key}`); } diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 1af8c0d5d1e..8c509d573e1 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -28,10 +28,10 @@ * under the License. */ -import { i18n } from '@osd/i18n'; import dateMath from '@elastic/datemath'; import classNames from 'classnames'; import React, { useState } from 'react'; +import { i18n } from '@osd/i18n'; import { EuiButton, @@ -46,7 +46,6 @@ import { import { EuiSuperUpdateButton, OnRefreshProps } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; import { Toast } from 'src/core/public'; -import { isEqual, compact } from 'lodash'; import { IDataPluginServices, IIndexPattern, TimeRange, TimeHistoryContract, Query } from '../..'; import { useOpenSearchDashboards, @@ -55,18 +54,14 @@ import { } from '../../../../opensearch_dashboards_react/public'; import QueryStringInputUI from './query_string_input'; import { doesKueryExpressionHaveLuceneSyntaxError, UI_SETTINGS } from '../../../common'; -import { PersistedLog, fromUser, getQueryLog } from '../../query'; +import { PersistedLog, getQueryLog } from '../../query'; import { NoDataPopover } from './no_data_popover'; -import { QueryEnhancement, Settings } from '../types'; const QueryStringInput = withOpenSearchDashboards(QueryStringInputUI); // @internal export interface QueryBarTopRowProps { query?: Query; - isEnhancementsEnabled?: boolean; - queryEnhancements?: Map; - settings?: Settings; onSubmit: (payload: { dateRange: TimeRange; query?: Query }) => void; onChange: (payload: { dateRange: TimeRange; query?: Query }) => void; onRefresh?: (payload: { dateRange: TimeRange }) => void; @@ -100,22 +95,8 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { const { uiSettings, notifications, storage, appName, docLinks } = opensearchDashboards.services; const osdDQLDocs: string = docLinks!.links.opensearchDashboards.dql.base; - const isDataSourceReadOnly = uiSettings.get('query:dataSourceReadOnly'); const queryLanguage = props.query && props.query.language; - const queryUiEnhancement = - (queryLanguage && - props.queryEnhancements && - props.queryEnhancements.get(queryLanguage)?.searchBar) || - null; - const parsedQuery = - !queryUiEnhancement || isValidQuery(props.query) - ? props.query! - : { query: getQueryStringInitialValue(queryLanguage!), language: queryLanguage! }; - if (!isEqual(parsedQuery?.query, props.query?.query)) { - onQueryChange(parsedQuery); - onSubmit({ query: parsedQuery, dateRange: getDateRange() }); - } const persistedLog: PersistedLog | undefined = React.useMemo( () => queryLanguage && uiSettings && storage && appName @@ -135,19 +116,15 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { function getDateRange() { const defaultTimeSetting = uiSettings!.get(UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS); return { - from: - props.dateRangeFrom || - queryUiEnhancement?.dateRange?.initialFrom || - defaultTimeSetting.from, - to: props.dateRangeTo || queryUiEnhancement?.dateRange?.initialTo || defaultTimeSetting.to, + from: props.dateRangeFrom || defaultTimeSetting.from, + to: props.dateRangeTo || defaultTimeSetting.to, }; } - function onQueryChange(query: Query, dateRange?: TimeRange) { - if (queryUiEnhancement && !isValidQuery(query)) return; + function onQueryChange(query: Query) { props.onChange({ query, - dateRange: dateRange ?? getDateRange(), + dateRange: getDateRange(), }); } @@ -204,10 +181,10 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { props.onSubmit({ query, dateRange }); } - function onInputSubmit(query: Query, dateRange?: TimeRange) { + function onInputSubmit(query: Query) { onSubmit({ query, - dateRange: dateRange ?? getDateRange(), + dateRange: getDateRange(), }); } @@ -219,38 +196,6 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { return valueAsMoment.toISOString(); } - function isValidQuery(query: Query | undefined) { - if (!query || !query.query) return false; - return ( - !Array.isArray(props.indexPatterns!) || - compact(props.indexPatterns!).length === 0 || - !isDataSourceReadOnly || - fromUser(query!.query).includes( - typeof props.indexPatterns[0] === 'string' - ? props.indexPatterns[0] - : props.indexPatterns[0].title - ) - ); - } - - function getQueryStringInitialValue(language: string) { - const { indexPatterns, queryEnhancements } = props; - const input = queryEnhancements?.get(language)?.searchBar?.queryStringInput?.initialValue; - - if ( - !indexPatterns || - (!Array.isArray(indexPatterns) && compact(indexPatterns).length > 0) || - !input - ) - return ''; - - const defaultDataSource = indexPatterns[0]; - const dataSource = - typeof defaultDataSource === 'string' ? defaultDataSource : defaultDataSource.title; - - return input.replace('', dataSource); - } - function renderQueryInput() { if (!shouldRenderQueryInput()) return; return ( @@ -259,15 +204,11 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { disableAutoFocus={props.disableAutoFocus} indexPatterns={props.indexPatterns!} prepend={props.prepend} - query={parsedQuery} - isEnhancementsEnabled={props.isEnhancementsEnabled} - queryEnhancements={props.queryEnhancements} - settings={props.settings} + query={props.query!} screenTitle={props.screenTitle} onChange={onQueryChange} onChangeQueryInputFocus={onChangeQueryInputFocus} onSubmit={onInputSubmit} - getQueryStringInitialValue={getQueryStringInitialValue} persistedLog={persistedLog} dataTestSubj={props.dataTestSubj} /> @@ -292,15 +233,10 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { } function shouldRenderDatePicker(): boolean { - return Boolean( - (props.showDatePicker && (queryUiEnhancement?.showDatePicker ?? true)) ?? - (props.showAutoRefreshOnly && (queryUiEnhancement?.showAutoRefreshOnly ?? true)) - ); + return Boolean(props.showDatePicker || props.showAutoRefreshOnly); } function shouldRenderQueryInput(): boolean { - // TODO: MQL probably can modify to not care about index patterns - // TODO: call queryUiEnhancement?.showQueryInput return Boolean(props.showQueryInput && props.indexPatterns && props.query && storage); } @@ -314,9 +250,6 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { isLoading={props.isLoading} onClick={onClickSubmitButton} data-test-subj="querySubmitButton" - aria-label={i18n.translate('data.query.queryBar.querySubmitButtonLabel', { - defaultMessage: 'Submit query', - })} /> ); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx index da5cc0e017b..dfa5d57411d 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx @@ -42,7 +42,7 @@ import { render } from '@testing-library/react'; import { EuiTextArea } from '@elastic/eui'; -import { LegacyQueryLanguageSwitcher } from './legacy_language_switcher'; +import { QueryLanguageSwitcher } from './language_switcher'; import { QueryStringInput } from './'; import type QueryStringInputUI from './query_string_input'; @@ -50,7 +50,6 @@ import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../mocks'; import { stubIndexPatternWithFields } from '../../stubs'; import { OpenSearchDashboardsContextProvider } from 'src/plugins/opensearch_dashboards_react/public'; -import { SettingsMock } from '../settings/mocks'; const startMock = coreMock.createStart(); @@ -85,8 +84,6 @@ const createMockStorage = () => ({ clear: jest.fn(), }); -const settingsMock = new SettingsMock(createMockStorage(), new Map()); - function wrapQueryStringInputInContext(testProps: any, storage?: any) { const defaultOptions = { screenTitle: 'Another Screen', @@ -135,7 +132,7 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], }) ); - expect(component.find(LegacyQueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); + expect(component.find(QueryLanguageSwitcher).prop('language')).toBe(luceneQuery.language); }); it('Should disable autoFocus on EuiTextArea when disableAutoFocus prop is true', () => { @@ -176,17 +173,16 @@ describe('QueryStringInput', () => { indexPatterns: [stubIndexPatternWithFields], disableAutoFocus: true, appName: 'discover', - settings: settingsMock, }, mockStorage ) ); - component.find(LegacyQueryLanguageSwitcher).props().onSelectLanguage('lucene'); - expect(settingsMock.updateSettings).toHaveBeenCalledWith({ - userQueryLanguage: 'lucene', - userQueryString: '', - }); + component.find(QueryLanguageSwitcher).props().onSelectLanguage('lucene'); + expect(mockStorage.set).toHaveBeenCalledWith( + 'opensearchDashboards.userQueryLanguage', + 'lucene' + ); expect(mockCallback).toHaveBeenCalledWith({ query: '', language: 'lucene' }); }); diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index db0d732d1db..5d071748700 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -47,7 +47,7 @@ import { import { FormattedMessage } from '@osd/i18n/react'; import { debounce, compact, isEqual, isFunction } from 'lodash'; import { Toast } from 'src/core/public'; -import { IDataPluginServices, IIndexPattern, Query, TimeRange } from '../..'; +import { IDataPluginServices, IIndexPattern, Query } from '../..'; import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete'; import { @@ -56,18 +56,13 @@ import { } from '../../../../opensearch_dashboards_react/public'; import { fetchIndexPatterns } from './fetch_index_patterns'; import { QueryLanguageSwitcher } from './language_switcher'; -import { LegacyQueryLanguageSwitcher } from './legacy_language_switcher'; import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query'; import { SuggestionsListSize } from '../typeahead/suggestions_component'; -import { Settings, SuggestionsComponent } from '..'; -import { DataSettings, QueryEnhancement } from '../types'; +import { SuggestionsComponent } from '..'; export interface QueryStringInputProps { indexPatterns: Array; query: Query; - isEnhancementsEnabled?: boolean; - queryEnhancements?: Map; - settings?: Settings; disableAutoFocus?: boolean; screenTitle?: string; prepend?: any; @@ -76,10 +71,9 @@ export interface QueryStringInputProps { placeholder?: string; languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition; onBlur?: () => void; - onChange?: (query: Query, dateRange?: TimeRange) => void; + onChange?: (query: Query) => void; onChangeQueryInputFocus?: (isFocused: boolean) => void; - onSubmit?: (query: Query, dateRange?: TimeRange) => void; - getQueryStringInitialValue?: (language: string) => string; + onSubmit?: (query: Query) => void; dataTestSubj?: string; size?: SuggestionsListSize; className?: string; @@ -114,7 +108,6 @@ const KEY_CODES = { }; // Needed for React.lazy -// TODO: MQL export this and let people extended this // eslint-disable-next-line import/no-default-export export default class QueryStringInputUI extends Component { public state: State = { @@ -137,13 +130,9 @@ export default class QueryStringInputUI extends Component { private queryBarInputDivRefInstance: RefObject = createRef(); private getQueryString = () => { - if (!this.props.query.query) { - return this.props.getQueryStringInitialValue?.(this.props.query.language) ?? ''; - } return toUser(this.props.query.query); }; - // TODO: MQL don't do this here? || Fetch data sources private fetchIndexPatterns = async () => { const stringPatterns = this.props.indexPatterns.filter( (indexPattern) => typeof indexPattern === 'string' @@ -235,7 +224,7 @@ export default class QueryStringInputUI extends Component { } }, 100); - private onSubmit = (query: Query, dateRange?: TimeRange) => { + private onSubmit = (query: Query) => { if (this.props.onSubmit) { if (this.persistedLog) { this.persistedLog.add(query.query); @@ -245,11 +234,11 @@ export default class QueryStringInputUI extends Component { } }; - private onChange = (query: Query, dateRange?: TimeRange) => { + private onChange = (query: Query) => { this.updateSuggestions(); if (this.props.onChange) { - this.props.onChange({ query: fromUser(query.query), language: query.language }, dateRange); + this.props.onChange({ query: fromUser(query.query), language: query.language }); } }; @@ -468,7 +457,6 @@ export default class QueryStringInputUI extends Component { } }; - // TODO: MQL consider moving language select language of setting search source here private onSelectLanguage = (language: string) => { // Send telemetry info every time the user opts in or out of kuery // As a result it is important this function only ever gets called in the @@ -477,28 +465,11 @@ export default class QueryStringInputUI extends Component { body: JSON.stringify({ opt_in: language === 'kuery' }), }); - const newQuery = { - query: this.props.getQueryStringInitialValue?.(language) ?? '', - language, - }; + this.services.storage.set('opensearchDashboards.userQueryLanguage', language); - const fields = this.props.queryEnhancements?.get(newQuery.language)?.fields; - const newSettings: DataSettings = { - userQueryLanguage: newQuery.language, - userQueryString: newQuery.query, - ...(fields && { uiOverrides: { fields } }), - }; - this.props.settings?.updateSettings(newSettings); - - const dateRangeEnhancement = this.props.queryEnhancements?.get(language)?.searchBar?.dateRange; - const dateRange = dateRangeEnhancement - ? { - from: dateRangeEnhancement.initialFrom!, - to: dateRangeEnhancement.initialTo!, - } - : undefined; - this.onChange(newQuery, dateRange); - this.onSubmit(newQuery, dateRange); + const newQuery = { query: '', language }; + this.onChange(newQuery); + this.onSubmit(newQuery); }; private onOutsideClick = () => { @@ -648,14 +619,6 @@ export default class QueryStringInputUI extends Component { return (
{this.props.prepend} - {!!this.props.isEnhancementsEnabled && ( - - )}
{
- {!!!this.props.isEnhancementsEnabled && ( - - )} + +
); } diff --git a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx index b2fdea2b49c..37b7d3d1610 100644 --- a/src/plugins/data/public/ui/search_bar/create_search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/create_search_bar.tsx @@ -29,7 +29,7 @@ */ import _ from 'lodash'; -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { CoreStart } from 'src/core/public'; import { IStorageWrapper } from 'src/plugins/opensearch_dashboards_utils/public'; import { OpenSearchDashboardsContextProvider } from '../../../../opensearch_dashboards_react/public'; @@ -41,15 +41,14 @@ import { useSavedQuery } from './lib/use_saved_query'; import { DataPublicPluginStart } from '../../types'; import { Filter, Query, TimeRange } from '../../../common'; import { useQueryStringManager } from './lib/use_query_string_manager'; -import { QueryEnhancement, Settings } from '../types'; +import { Settings } from '../types'; interface StatefulSearchBarDeps { core: CoreStart; data: Omit; storage: IStorageWrapper; - isEnhancementsEnabled: boolean; - queryEnhancements: Map; settings: Settings; + setContainerRef: (ref: HTMLDivElement | null) => void; } export type StatefulSearchBarProps = SearchBarOwnProps & { @@ -138,9 +137,8 @@ export function createSearchBar({ core, storage, data, - isEnhancementsEnabled, - queryEnhancements, settings, + setContainerRef, }: 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. @@ -176,6 +174,12 @@ export function createSearchBar({ notifications: core.notifications, }); + const containerRef = useCallback((node) => { + if (node) { + setContainerRef(node); + } + }, []); + // Fire onQuerySubmit on query or timerange change useEffect(() => { if (!useDefaultBehaviors || !onQuerySubmitRef.current) return; @@ -214,9 +218,8 @@ export function createSearchBar({ isRefreshPaused={refreshInterval.pause} filters={filters} query={query} - isEnhancementsEnabled={isEnhancementsEnabled} - queryEnhancements={queryEnhancements} settings={settings} + containerRef={containerRef} onFiltersUpdated={defaultFiltersUpdated(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)} savedQuery={savedQuery} diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 937da74914a..98b7b37f1c4 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -43,11 +43,12 @@ import { import QueryBarTopRow from '../query_string_input/query_bar_top_row'; import { SavedQueryAttributes, TimeHistoryContract, SavedQuery } from '../../query'; import { IDataPluginServices } from '../../types'; -import { TimeRange, Query, Filter, IIndexPattern } from '../../../common'; +import { TimeRange, Query, Filter, IIndexPattern, UI_SETTINGS } from '../../../common'; import { FilterBar } from '../filter_bar/filter_bar'; import { SavedQueryMeta, SaveQueryForm } from '../saved_query_form'; import { SavedQueryManagementComponent } from '../saved_query_management'; -import { QueryEnhancement, Settings } from '../types'; +import { Settings } from '../types'; +import { QueryEditorTopRow } from '../query_editor'; interface SearchBarInjectedDeps { opensearchDashboards: OpenSearchDashboardsReactContextValue; @@ -79,9 +80,8 @@ export interface SearchBarOwnProps { dateRangeTo?: string; // Query bar - should be in SearchBarInjectedDeps query?: Query; - isEnhancementsEnabled?: boolean; - queryEnhancements?: Map; settings?: Settings; + containerRef?: React.RefCallback; // Show when user has privileges to save showSaveQuery?: boolean; savedQuery?: SavedQuery; @@ -206,8 +206,23 @@ class SearchBarUI extends Component { ); }; - private shouldRenderQueryBar() { + private supportsEnhancements() { + return this.props.settings?.supportsEnhancementsEnabled(this.services.appName); + } + + private shouldRenderQueryEditor(isEnhancementsEnabledOverride: boolean) { + // TODO: MQL handle no index patterns? + if (!isEnhancementsEnabledOverride) return false; + const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly; + // TODO: MQL showQueryEditor should be a prop of it's own but using showQueryInput for now + const showQueryEditor = + this.props.showQueryInput && this.props.indexPatterns && this.state.query; + return this.props.showQueryBar && (showDatePicker || showQueryEditor); + } + + private shouldRenderQueryBar(isEnhancementsEnabledOverride: boolean) { // TODO: MQL handle no index patterns? + if (isEnhancementsEnabledOverride) return false; const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly; const showQueryInput = this.props.showQueryInput && this.props.indexPatterns && this.state.query; @@ -221,7 +236,8 @@ class SearchBarUI extends Component { this.props.filters && this.props.indexPatterns && compact(this.props.indexPatterns).length > 0 && - (this.props.queryEnhancements?.get(this.state.query?.language!)?.searchBar?.showFilterBar ?? + (this.props.settings?.getQueryEnhancements(this.state.query?.language!)?.searchBar + ?.showFilterBar ?? true) ); } @@ -388,6 +404,15 @@ class SearchBarUI extends Component { } public render() { + const isEnhancementsEnabledOverride = + this.supportsEnhancements() && + this.services.uiSettings.get(UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED); + + this.props.settings?.setUserQueryEnhancementsEnabled(isEnhancementsEnabledOverride); + this.props.settings?.setUserQueryLanguageBlocklist( + this.services.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE_BLOCKLIST) + ); + const savedQueryManagement = this.state.query && this.props.onClearSavedQuery && ( { ); let queryBar; - if (this.shouldRenderQueryBar()) { + if (this.shouldRenderQueryBar(isEnhancementsEnabledOverride)) { // TODO: MQL make this default query bar top row but this.props.queryEnhancements.get(language) can pass a component queryBar = ( { ); } + let queryEditor; + if (this.shouldRenderQueryEditor(isEnhancementsEnabledOverride)) { + // TODO: MQL make this default query bar top row but this.props.queryEnhancements.get(language) can pass a component + queryEditor = ( + + ); + } + + const className = isEnhancementsEnabledOverride ? 'globalQueryEditor' : 'globalQueryBar'; + return ( -
+
{queryBar} - {filterBar} + {queryEditor} + {!isEnhancementsEnabledOverride && filterBar} {this.state.showSaveQueryModal ? ( (false); + private enhancedAppNames: string[] = []; + constructor( + private readonly config: ConfigSchema['enhancements'], private readonly storage: IStorageWrapper, private readonly queryEnhancements: Map - ) {} + ) { + this.enabledQueryEnhancementsUpdated$.next(this.config.enabled); + this.enhancedAppNames = this.config.enabled ? this.config.supportedAppNames : []; + } + + supportsEnhancementsEnabled(appName: string) { + return this.enhancedAppNames.includes(appName); + } + + getEnabledQueryEnhancementsUpdated$ = () => { + return this.enabledQueryEnhancementsUpdated$.asObservable(); + }; + + getUserQueryEnhancementsEnabled() { + return ( + this.storage.get('opensearchDashboards.userQueryEnhancementsEnabled') || this.config.enabled + ); + } + + setUserQueryEnhancementsEnabled(enabled: boolean) { + if (!this.config.enabled) return; + this.storage.set('opensearchDashboards.userQueryEnhancementsEnabled', enabled); + this.enabledQueryEnhancementsUpdated$.next(enabled); + return true; + } + + getAllQueryEnhancements() { + return this.queryEnhancements; + } + + getQueryEnhancements(language: string) { + return this.queryEnhancements.get(language); + } + + getUserQueryLanguageBlocklist() { + return this.storage.get('opensearchDashboards.userQueryLanguageBlocklist') || []; + } + + setUserQueryLanguageBlocklist(languages: string[]) { + if (!this.config.enabled) return; + this.storage.set( + 'opensearchDashboards.userQueryLanguageBlocklist', + languages.map((language) => language.toLowerCase()) + ); + return true; + } getUserQueryLanguage() { return this.storage.get('opensearchDashboards.userQueryLanguage') || 'kuery'; @@ -84,10 +135,11 @@ export class Settings { } interface Deps { + config: ConfigSchema['enhancements']; storage: IStorageWrapper; queryEnhancements: Map; } -export function createSettings({ storage, queryEnhancements }: Deps) { - return new Settings(storage, queryEnhancements); +export function createSettings({ config, storage, queryEnhancements }: Deps) { + return new Settings(config, storage, queryEnhancements); } diff --git a/src/plugins/data/public/ui/types.ts b/src/plugins/data/public/ui/types.ts index 464e6a97afd..d570a31f2ce 100644 --- a/src/plugins/data/public/ui/types.ts +++ b/src/plugins/data/public/ui/types.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { BehaviorSubject } from 'rxjs'; import { SearchInterceptor } from '../search'; import { IndexPatternSelectProps } from './index_pattern_select'; import { StatefulSearchBarProps } from './search_bar'; @@ -57,9 +58,9 @@ export interface IUiSetup { * Data plugin prewired UI components */ export interface IUiStart { - isEnhancementsEnabled: boolean; - queryEnhancements: Map; IndexPatternSelect: React.ComponentType; SearchBar: React.ComponentType; Settings: Settings; + containerRef: HTMLDivElement | null; + container$: BehaviorSubject; } diff --git a/src/plugins/data/public/ui/ui_service.ts b/src/plugins/data/public/ui/ui_service.ts index 1ef834b5456..f0d398400e1 100644 --- a/src/plugins/data/public/ui/ui_service.ts +++ b/src/plugins/data/public/ui/ui_service.ts @@ -4,6 +4,7 @@ */ import { Plugin, CoreSetup, CoreStart, PluginInitializerContext } from 'src/core/public'; +import { BehaviorSubject } from 'rxjs'; import { IUiStart, IUiSetup, QueryEnhancement, UiEnhancements } from './types'; import { ConfigSchema } from '../../config'; @@ -26,6 +27,8 @@ export interface UiServiceStartDependencies { export class UiService implements Plugin { enhancementsConfig: ConfigSchema['enhancements']; private queryEnhancements: Map = new Map(); + private containerRef: HTMLDivElement | null = null; + private container$ = new BehaviorSubject(null); constructor(initializerContext: PluginInitializerContext) { const { enhancements } = initializerContext.config.get(); @@ -46,23 +49,31 @@ export class UiService implements Plugin { } public start(core: CoreStart, { dataServices, storage }: UiServiceStartDependencies): IUiStart { - const Settings = createSettings({ storage, queryEnhancements: this.queryEnhancements }); + const Settings = createSettings({ + config: this.enhancementsConfig, + storage, + queryEnhancements: this.queryEnhancements, + }); + + const setContainerRef = (ref: HTMLDivElement | null) => { + this.containerRef = ref; + this.container$.next(ref); + }; const SearchBar = createSearchBar({ core, data: dataServices, storage, - isEnhancementsEnabled: this.enhancementsConfig?.enabled, - queryEnhancements: this.queryEnhancements, settings: Settings, + setContainerRef, }); return { - isEnhancementsEnabled: this.enhancementsConfig?.enabled, - queryEnhancements: this.queryEnhancements, IndexPatternSelect: createIndexPatternSelect(core.savedObjects.client), SearchBar, Settings, + containerRef: this.containerRef, + container$: this.container$, }; } diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index e12113b87b2..ca24151e681 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -705,8 +705,22 @@ export function getUiSettings(): Record> { }), schema: schema.boolean(), }, - [UI_SETTINGS.DATAFRAME_HYDRATION_STRATEGY]: { - name: i18n.translate('data.advancedSettings.dataFrameHydrationStrategyTitle', { + [UI_SETTINGS.QUERY_ENHANCEMENTS_ENABLED]: { + name: i18n.translate('data.advancedSettings.query.enhancements.enableTitle', { + defaultMessage: 'Enable query enhancements', + }), + value: false, + description: i18n.translate('data.advancedSettings.query.enhancements.enableText', { + defaultMessage: ` + Experimental: + Allows users to query data using enhancements where available. If disabled, + only querying and querying languages that are considered production-ready are available to the user.`, + }), + category: ['search'], + schema: schema.boolean(), + }, + [UI_SETTINGS.QUERY_DATAFRAME_HYDRATION_STRATEGY]: { + name: i18n.translate('data.advancedSettings.query.dataFrameHydrationStrategyTitle', { defaultMessage: 'Data frame hydration strategy', }), value: 'perSource', @@ -724,7 +738,8 @@ export function getUiSettings(): Record> { Not Implemented.
  • {advanced}: hydrates the schema in intervals. If the schema hasn't changed the interval increases. If the schema has changed the interval resets. Not Implemented.
  • - `, + + Experimental: Requires query enhancements enabled.`, values: { perSource: dataFrameHydrationStrategyOptionLabels.perSource, perQuery: dataFrameHydrationStrategyOptionLabels.perQuery, @@ -736,16 +751,28 @@ export function getUiSettings(): Record> { schema: schema.string(), }, [UI_SETTINGS.QUERY_DATA_SOURCE_READONLY]: { - name: i18n.translate('data.advancedSettings.query.dataSourceReadOnlyTitle', { - defaultMessage: 'Read-only data source in query bar', + name: i18n.translate('data.advancedSettings.query.dataSource.readOnlyTitle', { + defaultMessage: 'Read-only data source in query editor', }), value: true, - description: i18n.translate('data.advancedSettings.query.dataSourceReadOnlyText', { + description: i18n.translate('data.advancedSettings.query.dataSource.readOnlyText', { defaultMessage: - 'When enabled, the global search bar prevents modifying the data source in the query input. ' + - '
    Experimental: Setting to false enables modifying the data source.', + 'When enabled, the search bar prevents modifying the data source in the query input. ' + + 'Experimental: Requires query enhancements enabled.', }), + category: ['search'], schema: schema.boolean(), }, + [UI_SETTINGS.SEARCH_QUERY_LANGUAGE_BLOCKLIST]: { + name: i18n.translate('data.advancedSettings.searchQueryLanguageBlocklistTitle', { + defaultMessage: 'Additional query languages blocklist', + }), + value: ['none'], + description: i18n.translate('data.advancedSettings.searchQueryLanguageBlocklistText', { + defaultMessage: `Additional languages that are blocked from being used in the query editor. + Note: DQL and Lucene will not be blocked even if set.`, + }), + schema: schema.arrayOf(schema.string()), + }, }; } diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index b65bd43950c..eec4e09a469 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { FC, useCallback, useEffect, useState } from 'react'; -import { EuiPageSideBar, EuiSplitPanel } from '@elastic/eui'; +import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; +import { EuiPageSideBar, EuiPortal, EuiSplitPanel } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DataSource, DataSourceGroup, DataSourceSelectable } from '../../../../data/public'; import { DataSourceOption } from '../../../../data/public/'; @@ -19,15 +19,46 @@ export const Sidebar: FC = ({ children }) => { const [selectedSources, setSelectedSources] = useState([]); const [dataSourceOptionList, setDataSourceOptionList] = useState([]); const [activeDataSources, setActiveDataSources] = useState([]); + const [isEnhancementsEnabled, setIsEnhancementsEnabled] = useState(false); + const containerRef = useRef(null); const { services: { - data: { indexPatterns, dataSources }, + data: { indexPatterns, dataSources, ui }, notifications: { toasts }, application, }, } = useOpenSearchDashboards(); + useEffect(() => { + const subscriptions = ui.Settings.getEnabledQueryEnhancementsUpdated$().subscribe( + (enabledQueryEnhancements) => { + setIsEnhancementsEnabled(enabledQueryEnhancements); + } + ); + + return () => { + subscriptions.unsubscribe(); + }; + }, [ui.Settings]); + + const setContainerRef = useCallback((uiContainerRef) => { + uiContainerRef.appendChild(containerRef.current); + }, []); + + useEffect(() => { + const subscriptions = ui.container$.subscribe((container) => { + if (container === null) return; + if (containerRef.current) { + setContainerRef(container); + } + }); + + return () => { + subscriptions.unsubscribe(); + }; + }, [ui.container$, containerRef, setContainerRef, ui.containerRef]); + useEffect(() => { let isMounted = true; const subscription = dataSources.dataSourceService @@ -102,6 +133,19 @@ export const Sidebar: FC = ({ children }) => { dataSources.dataSourceService.reload(); }, [dataSources.dataSourceService]); + const dataSourceSelector = ( + + ); + return ( { borderRadius="none" color="transparent" > - - - + {isEnhancementsEnabled && ( + { + containerRef.current = node; + }} + > + {dataSourceSelector} + + )} + {!isEnhancementsEnabled && ( + + {dataSourceSelector} + + )} {children} diff --git a/src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts b/src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts index c5b76c8bf20..56821d45d21 100644 --- a/src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts +++ b/src/plugins/opensearch_dashboards_react/public/code_editor/editor_theme.ts @@ -28,6 +28,8 @@ * under the License. */ +import Color from 'color'; + import { monaco } from '@osd/monaco'; import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; @@ -35,6 +37,7 @@ import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; // NOTE: For talk around where this theme information will ultimately live, // please see this discuss issue: https://github.com/elastic/kibana/issues/43814 +const standardizeColor = (color: string) => new Color(color).hex().toLowerCase(); export function createTheme( euiTheme: typeof darkTheme | typeof lightTheme, @@ -46,78 +49,78 @@ export function createTheme( rules: [ { token: '', - foreground: euiTheme.euiColorDarkestShade, - background: euiTheme.euiColorEmptyShade, + foreground: standardizeColor(euiTheme.euiColorDarkestShade), + background: standardizeColor(euiTheme.euiColorEmptyShade), }, - { token: 'invalid', foreground: euiTheme.euiColorAccent }, + { token: 'invalid', foreground: standardizeColor(euiTheme.euiColorAccent) }, { token: 'emphasis', fontStyle: 'italic' }, { token: 'strong', fontStyle: 'bold' }, - { token: 'variable', foreground: euiTheme.euiColorPrimary }, - { token: 'variable.predefined', foreground: euiTheme.euiColorSecondary }, - { token: 'constant', foreground: euiTheme.euiColorAccent }, - { token: 'comment', foreground: euiTheme.euiColorMediumShade }, - { token: 'number', foreground: euiTheme.euiColorAccent }, - { token: 'number.hex', foreground: euiTheme.euiColorAccent }, - { token: 'regexp', foreground: euiTheme.euiColorDanger }, - { token: 'annotation', foreground: euiTheme.euiColorMediumShade }, - { token: 'type', foreground: euiTheme.euiColorVis0 }, - - { token: 'delimiter', foreground: euiTheme.euiColorDarkestShade }, - { token: 'delimiter.html', foreground: euiTheme.euiColorDarkShade }, - { token: 'delimiter.xml', foreground: euiTheme.euiColorPrimary }, - - { token: 'tag', foreground: euiTheme.euiColorDanger }, - { token: 'tag.id.jade', foreground: euiTheme.euiColorPrimary }, - { token: 'tag.class.jade', foreground: euiTheme.euiColorPrimary }, - { token: 'meta.scss', foreground: euiTheme.euiColorAccent }, - { token: 'metatag', foreground: euiTheme.euiColorSecondary }, - { token: 'metatag.content.html', foreground: euiTheme.euiColorDanger }, - { token: 'metatag.html', foreground: euiTheme.euiColorMediumShade }, - { token: 'metatag.xml', foreground: euiTheme.euiColorMediumShade }, + { token: 'variable', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'variable.predefined', foreground: standardizeColor(euiTheme.euiColorSecondary) }, + { token: 'constant', foreground: standardizeColor(euiTheme.euiColorAccent) }, + { token: 'comment', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, + { token: 'number', foreground: standardizeColor(euiTheme.euiColorAccent) }, + { token: 'number.hex', foreground: standardizeColor(euiTheme.euiColorAccent) }, + { token: 'regexp', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'annotation', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, + { token: 'type', foreground: standardizeColor(euiTheme.euiColorVis0) }, + + { token: 'delimiter', foreground: standardizeColor(euiTheme.euiColorDarkestShade) }, + { token: 'delimiter.html', foreground: standardizeColor(euiTheme.euiColorDarkShade) }, + { token: 'delimiter.xml', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + + { token: 'tag', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'tag.id.jade', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'tag.class.jade', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'meta.scss', foreground: standardizeColor(euiTheme.euiColorAccent) }, + { token: 'metatag', foreground: standardizeColor(euiTheme.euiColorSecondary) }, + { token: 'metatag.content.html', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'metatag.html', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, + { token: 'metatag.xml', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, { token: 'metatag.php', fontStyle: 'bold' }, - { token: 'key', foreground: euiTheme.euiColorWarning }, - { token: 'string.key.json', foreground: euiTheme.euiColorDanger }, - { token: 'string.value.json', foreground: euiTheme.euiColorPrimary }, - - { token: 'attribute.name', foreground: euiTheme.euiColorDanger }, - { token: 'attribute.name.css', foreground: euiTheme.euiColorSecondary }, - { token: 'attribute.value', foreground: euiTheme.euiColorPrimary }, - { token: 'attribute.value.number', foreground: euiTheme.euiColorWarning }, - { token: 'attribute.value.unit', foreground: euiTheme.euiColorWarning }, - { token: 'attribute.value.html', foreground: euiTheme.euiColorPrimary }, - { token: 'attribute.value.xml', foreground: euiTheme.euiColorPrimary }, - - { token: 'string', foreground: euiTheme.euiColorDanger }, - { token: 'string.html', foreground: euiTheme.euiColorPrimary }, - { token: 'string.sql', foreground: euiTheme.euiColorDanger }, - { token: 'string.yaml', foreground: euiTheme.euiColorPrimary }, - - { token: 'keyword', foreground: euiTheme.euiColorPrimary }, - { token: 'keyword.json', foreground: euiTheme.euiColorPrimary }, - { token: 'keyword.flow', foreground: euiTheme.euiColorWarning }, - { token: 'keyword.flow.scss', foreground: euiTheme.euiColorPrimary }, - - { token: 'operator.scss', foreground: euiTheme.euiColorDarkShade }, - { token: 'operator.sql', foreground: euiTheme.euiColorMediumShade }, - { token: 'operator.swift', foreground: euiTheme.euiColorMediumShade }, - { token: 'predefined.sql', foreground: euiTheme.euiColorMediumShade }, + { token: 'key', foreground: standardizeColor(euiTheme.euiColorWarning) }, + { token: 'string.key.json', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'string.value.json', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + + { token: 'attribute.name', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'attribute.name.css', foreground: standardizeColor(euiTheme.euiColorSecondary) }, + { token: 'attribute.value', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'attribute.value.number', foreground: standardizeColor(euiTheme.euiColorWarning) }, + { token: 'attribute.value.unit', foreground: standardizeColor(euiTheme.euiColorWarning) }, + { token: 'attribute.value.html', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'attribute.value.xml', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + + { token: 'string', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'string.html', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'string.sql', foreground: standardizeColor(euiTheme.euiColorDanger) }, + { token: 'string.yaml', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + + { token: 'keyword', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'keyword.json', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + { token: 'keyword.flow', foreground: standardizeColor(euiTheme.euiColorWarning) }, + { token: 'keyword.flow.scss', foreground: standardizeColor(euiTheme.euiColorPrimary) }, + + { token: 'operator.scss', foreground: standardizeColor(euiTheme.euiColorDarkShade) }, + { token: 'operator.sql', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, + { token: 'operator.swift', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, + { token: 'predefined.sql', foreground: standardizeColor(euiTheme.euiColorMediumShade) }, ], colors: { - 'editor.foreground': euiTheme.euiColorDarkestShade, - 'editor.background': euiTheme.euiColorEmptyShade, - 'editorLineNumber.foreground': euiTheme.euiColorDarkShade, - 'editorLineNumber.activeForeground': euiTheme.euiColorDarkShade, - 'editorIndentGuide.background': euiTheme.euiColorLightShade, - 'editor.selectionBackground': selectionBackgroundColor, - 'editorWidget.border': euiTheme.euiColorLightShade, - 'editorWidget.background': euiTheme.euiColorLightestShade, - 'editorCursor.foreground': euiTheme.euiColorDarkestShade, - 'editorSuggestWidget.selectedBackground': euiTheme.euiColorLightShade, - 'list.hoverBackground': euiTheme.euiColorLightShade, - 'list.highlightForeground': euiTheme.euiColorPrimary, - 'editor.lineHighlightBorder': euiTheme.euiColorLightestShade, + 'editor.foreground': standardizeColor(euiTheme.euiColorDarkestShade), + 'editor.background': standardizeColor(euiTheme.euiColorEmptyShade), + 'editorLineNumber.foreground': standardizeColor(euiTheme.euiColorDarkShade), + 'editorLineNumber.activeForeground': standardizeColor(euiTheme.euiColorDarkShade), + 'editorIndentGuide.background': standardizeColor(euiTheme.euiColorLightShade), + 'editor.selectionBackground': standardizeColor(selectionBackgroundColor), + 'editorWidget.border': standardizeColor(euiTheme.euiColorLightShade), + 'editorWidget.background': standardizeColor(euiTheme.euiColorLightestShade), + 'editorCursor.foreground': standardizeColor(euiTheme.euiColorDarkestShade), + 'editorSuggestWidget.selectedBackground': standardizeColor(euiTheme.euiColorLightShade), + 'list.hoverBackground': standardizeColor(euiTheme.euiColorLightShade), + 'list.highlightForeground': standardizeColor(euiTheme.euiColorPrimary), + 'editor.lineHighlightBorder': standardizeColor(euiTheme.euiColorLightestShade), }, }; } From 3c5dfd86194d09289fc8e8a566e0f664bd9a89f9 Mon Sep 17 00:00:00 2001 From: "opensearch-changeset-bot[bot]" <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 00:23:58 +0000 Subject: [PATCH 2/7] Changeset file for PR #7001 created/updated --- changelogs/fragments/7001.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/7001.yml diff --git a/changelogs/fragments/7001.yml b/changelogs/fragments/7001.yml new file mode 100644 index 00000000000..0270e2fbced --- /dev/null +++ b/changelogs/fragments/7001.yml @@ -0,0 +1,2 @@ +feat: +- Query editor and UI settings toggle ([#7001](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7001)) \ No newline at end of file From 023736289d1f9dd86d2051d7b77e105732e1ad74 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Tue, 11 Jun 2024 00:29:14 +0000 Subject: [PATCH 3/7] remove unused setting Signed-off-by: Kawika Avilla Fix test for DE Signed-off-by: Kawika Avilla clean up filter to match editor Signed-off-by: Kawika Avilla --- src/plugins/data/common/constants.ts | 2 - .../query_string_input/query_bar_top_row.tsx | 24 ++++--- .../data/public/ui/search_bar/search_bar.tsx | 64 +++++++++---------- .../public/components/sidebar/index.test.tsx | 8 +++ .../public/components/sidebar/index.tsx | 3 +- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index feef2097e61..a00adb0b629 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -62,8 +62,6 @@ export const UI_SETTINGS = { FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues', QUERY_ENHANCEMENTS_ENABLED: 'query:enhancements:enabled', QUERY_DATAFRAME_HYDRATION_STRATEGY: 'query:dataframe:hydrationStrategy', - QUERY_POLLING_INTERVAL: 'query:async:pollingInterval', QUERY_DATA_SOURCE_READONLY: 'query:dataSource:readOnly', - QUERY_SEARCH_BAR_EXTENSIONS_ENABLED: 'query:searchBarExtensions:enabled', SEARCH_QUERY_LANGUAGE_BLOCKLIST: 'search:queryLanguageBlocklist', } as const; diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 8c509d573e1..9ad12ed5b4b 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -80,6 +80,7 @@ export interface QueryBarTopRowProps { showAutoRefreshOnly?: boolean; onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; customSubmitButton?: any; + filterBar?: any; isDirty: boolean; timeHistory?: TimeHistoryContract; indicateNoData?: boolean; @@ -379,16 +380,19 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { }); return ( - - {renderQueryInput()} - {renderSharingMetaFields()} - {renderUpdateButton()} - + <> + + {renderQueryInput()} + {renderSharingMetaFields()} + {renderUpdateButton()} + + {props.filterBar} + ); } diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 98b7b37f1c4..0e5ab8063a7 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -425,9 +425,38 @@ class SearchBarUI extends Component { /> ); + let filterBar; + if (this.shouldRenderFilterBar()) { + const filterGroupClasses = classNames('globalFilterGroup__wrapper', { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, + }); + filterBar = ( +
    { + this.filterBarWrapperRef = node; + }} + className={filterGroupClasses} + > +
    { + this.filterBarRef = node; + }} + > + +
    +
    + ); + } + let queryBar; if (this.shouldRenderQueryBar(isEnhancementsEnabledOverride)) { - // TODO: MQL make this default query bar top row but this.props.queryEnhancements.get(language) can pass a component queryBar = ( { customSubmitButton={ this.props.customSubmitButton ? this.props.customSubmitButton : undefined } + filterBar={filterBar} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} /> ); } - let filterBar; - if (this.shouldRenderFilterBar()) { - const filterGroupClasses = classNames('globalFilterGroup__wrapper', { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible, - }); - filterBar = ( -
    { - this.filterBarWrapperRef = node; - }} - className={filterGroupClasses} - > -
    { - this.filterBarRef = node; - }} - > - -
    -
    - ); - } - let queryEditor; if (this.shouldRenderQueryEditor(isEnhancementsEnabledOverride)) { - // TODO: MQL make this default query bar top row but this.props.queryEnhancements.get(language) can pass a component queryEditor = ( {
    {queryBar} {queryEditor} - {!isEnhancementsEnabledOverride && filterBar} {this.state.showSaveQueryModal ? ( { ), }, }, + ui: { + Settings: { + getEnabledQueryEnhancementsUpdated$: jest + .fn() + .mockImplementation(() => createObservable(false)), + }, + container$: jest.fn().mockImplementation(() => createObservable(null)), + }, }, notifications: { toasts: { diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index eec4e09a469..0773d9c59c1 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -47,6 +47,7 @@ export const Sidebar: FC = ({ children }) => { }, []); useEffect(() => { + if (!isEnhancementsEnabled) return; const subscriptions = ui.container$.subscribe((container) => { if (container === null) return; if (containerRef.current) { @@ -57,7 +58,7 @@ export const Sidebar: FC = ({ children }) => { return () => { subscriptions.unsubscribe(); }; - }, [ui.container$, containerRef, setContainerRef, ui.containerRef]); + }, [ui.container$, containerRef, setContainerRef, ui.containerRef, isEnhancementsEnabled]); useEffect(() => { let isMounted = true; From 4908cbda4e5b00e1a24a8662fba8d493dfd25511 Mon Sep 17 00:00:00 2001 From: Kawika Avilla Date: Tue, 11 Jun 2024 04:08:47 +0000 Subject: [PATCH 4/7] use opensearchql for query editor Signed-off-by: Kawika Avilla --- src/plugins/data/public/ui/query_editor/query_editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index 98ba64466ac..808e891c822 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -282,7 +282,7 @@ export default class QueryEditorUI extends Component {
    Date: Tue, 11 Jun 2024 16:49:09 +0000 Subject: [PATCH 5/7] Remove filter bar from top row query bar because embed mode Signed-off-by: Kawika Avilla --- .../data/public/ui/query_string_input/query_bar_top_row.tsx | 2 -- src/plugins/data/public/ui/search_bar/search_bar.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx index 9ad12ed5b4b..5b04515f2c9 100644 --- a/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx @@ -80,7 +80,6 @@ export interface QueryBarTopRowProps { showAutoRefreshOnly?: boolean; onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void; customSubmitButton?: any; - filterBar?: any; isDirty: boolean; timeHistory?: TimeHistoryContract; indicateNoData?: boolean; @@ -391,7 +390,6 @@ export default function QueryBarTopRow(props: QueryBarTopRowProps) { {renderSharingMetaFields()} {renderUpdateButton()} - {props.filterBar} ); } diff --git a/src/plugins/data/public/ui/search_bar/search_bar.tsx b/src/plugins/data/public/ui/search_bar/search_bar.tsx index 0e5ab8063a7..efd9d3467af 100644 --- a/src/plugins/data/public/ui/search_bar/search_bar.tsx +++ b/src/plugins/data/public/ui/search_bar/search_bar.tsx @@ -480,7 +480,6 @@ class SearchBarUI extends Component { customSubmitButton={ this.props.customSubmitButton ? this.props.customSubmitButton : undefined } - filterBar={filterBar} dataTestSubj={this.props.dataTestSubj} indicateNoData={this.props.indicateNoData} /> @@ -527,6 +526,7 @@ class SearchBarUI extends Component {
    {queryBar} {queryEditor} + {!isEnhancementsEnabledOverride && filterBar} {this.state.showSaveQueryModal ? ( Date: Tue, 11 Jun 2024 23:25:11 +0000 Subject: [PATCH 6/7] omit ui from data services Signed-off-by: Kawika Avilla --- src/plugins/data/public/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index f22a61423d1..ea0470d0e1a 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -234,7 +234,7 @@ export class DataPublicPlugin }, ]); - const dataServices = { + const dataServices: Omit = { actions: { createFiltersFromValueClickAction, createFiltersFromRangeSelectAction, From e04f8576490f544ca2df84ed7cde9f7879a8ed80 Mon Sep 17 00:00:00 2001 From: Sean Li Date: Tue, 11 Jun 2024 17:23:38 -0700 Subject: [PATCH 7/7] fixing language switching bug Signed-off-by: Sean Li --- .../data/public/ui/query_editor/query_editor_top_row.tsx | 6 +++++- src/plugins/data/public/ui/settings/settings.ts | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx index c6bb1f893ea..0dcaae266ce 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor_top_row.tsx @@ -102,7 +102,11 @@ export default function QueryEditorTopRow(props: QueryEditorTopRowProps) { ); function onClickSubmitButton(event: React.MouseEvent) { - if (persistedLog && props.query) { + if ( + persistedLog && + props.query && + (props.query.language === 'kuery' || props.query.language.toLowerCase() === 'lucene') + ) { persistedLog.add(props.query.query); } event.preventDefault(); diff --git a/src/plugins/data/public/ui/settings/settings.ts b/src/plugins/data/public/ui/settings/settings.ts index 463865b148b..ee7fe523c47 100644 --- a/src/plugins/data/public/ui/settings/settings.ts +++ b/src/plugins/data/public/ui/settings/settings.ts @@ -51,6 +51,10 @@ export class Settings { setUserQueryEnhancementsEnabled(enabled: boolean) { if (!this.config.enabled) return; this.storage.set('opensearchDashboards.userQueryEnhancementsEnabled', enabled); + if (!enabled) { + this.storage.remove('opensearchDashboards.userQueryLanguage'); + this.storage.remove('opensearchDashboards.userQueryString'); + } this.enabledQueryEnhancementsUpdated$.next(enabled); return true; }