From d18c5a47778d1c15fdedf0a121efd02f14ae09e5 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 13 Jul 2023 06:52:24 +0000 Subject: [PATCH 01/16] Adds toggle between legacy and new discover Signed-off-by: Ashwin P Chandran --- .../data_explorer/opensearch_dashboards.json | 2 +- .../data_explorer/public/application.tsx | 10 +-- .../data_explorer/public/components/app.tsx | 13 +-- .../public/components/app_container.tsx | 10 ++- src/plugins/data_explorer/public/plugin.ts | 25 +++++- .../public/services/view_service/types.ts | 5 +- src/plugins/data_explorer/public/types.ts | 7 ++ src/plugins/discover/common/index.ts | 2 +- .../view_components/canvas/canvas.tsx | 27 +++++++ .../view_components/canvas/index.tsx | 14 ++-- .../view_components/canvas/top_nav.tsx | 79 +++++++++++++++++++ .../view_components/panel/index.tsx | 3 +- .../view_components/panel/panel.tsx | 10 +++ src/plugins/discover/public/index.ts | 1 + src/plugins/discover/public/plugin.ts | 7 +- src/plugins/discover/server/ui_settings.ts | 6 +- .../opensearch_dashboards.json | 14 +++- .../public/application/angular/discover.js | 17 ++++ src/plugins/discover_legacy/public/plugin.ts | 21 +++++ 19 files changed, 230 insertions(+), 43 deletions(-) create mode 100644 src/plugins/discover/public/application/view_components/canvas/canvas.tsx create mode 100644 src/plugins/discover/public/application/view_components/canvas/top_nav.tsx create mode 100644 src/plugins/discover/public/application/view_components/panel/panel.tsx diff --git a/src/plugins/data_explorer/opensearch_dashboards.json b/src/plugins/data_explorer/opensearch_dashboards.json index 7bf93a9c24d7..7b5a78c55030 100644 --- a/src/plugins/data_explorer/opensearch_dashboards.json +++ b/src/plugins/data_explorer/opensearch_dashboards.json @@ -4,7 +4,7 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["navigation"], + "requiredPlugins": ["data", "navigation"], "optionalPlugins": [], "requiredBundles": ["opensearchDashboardsReact"] } diff --git a/src/plugins/data_explorer/public/application.tsx b/src/plugins/data_explorer/public/application.tsx index 7491ae7d8675..505209358ff1 100644 --- a/src/plugins/data_explorer/public/application.tsx +++ b/src/plugins/data_explorer/public/application.tsx @@ -14,20 +14,16 @@ import { DataExplorerApp } from './components/app'; export const renderApp = ( { notifications, http }: CoreStart, services: DataExplorerServices, - { appBasePath, element, history }: AppMountParameters + params: AppMountParameters ) => { + const { history, element } = params; ReactDOM.render( - + diff --git a/src/plugins/data_explorer/public/components/app.tsx b/src/plugins/data_explorer/public/components/app.tsx index 8a36443da43d..40d23d97356b 100644 --- a/src/plugins/data_explorer/public/components/app.tsx +++ b/src/plugins/data_explorer/public/components/app.tsx @@ -4,19 +4,12 @@ */ import React from 'react'; -import { CoreStart, ScopedHistory } from '../../../../core/public'; +import { AppMountParameters } from '../../../../core/public'; import { useView } from '../utils/use'; import { AppContainer } from './app_container'; -interface DataExplorerAppDeps { - basename: string; - notifications: CoreStart['notifications']; - http: CoreStart['http']; - history: ScopedHistory; -} - -export const DataExplorerApp = (deps: DataExplorerAppDeps) => { +export const DataExplorerApp = ({ params }: { params: AppMountParameters }) => { const { view } = useView(); - return ; + return ; }; diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index 9044a6c69a28..8923d2c5b175 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -5,12 +5,12 @@ import React, { useLayoutEffect, useRef, useState } from 'react'; import { EuiPageTemplate } from '@elastic/eui'; - +import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; import { NoView } from './no_view'; import { View } from '../services/view_service/view'; -export const AppContainer = ({ view }: { view?: View }) => { +export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { const [showSpinner, setShowSpinner] = useState(false); const canvasRef = useRef(null); const panelRef = useRef(null); @@ -24,7 +24,8 @@ export const AppContainer = ({ view }: { view?: View }) => { } }; - if (!view) { + // Do nothing if the view is not defined or if the view is the same as the previous view + if (!view || (unmountRef.current && unmountRef.current.viewId === view.id)) { return; } @@ -38,6 +39,7 @@ export const AppContainer = ({ view }: { view?: View }) => { (await view.mount({ canvasElement: canvasRef.current!, panelElement: panelRef.current!, + appParams: params, })) || null; } catch (e) { // TODO: add error UI @@ -54,7 +56,7 @@ export const AppContainer = ({ view }: { view?: View }) => { mount(); return unmount; - }, [view]); + }, [params, view]); // TODO: Make this more robust. if (!view) { diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts index 3075cd27e834..d44e6713cc7a 100644 --- a/src/plugins/data_explorer/public/plugin.ts +++ b/src/plugins/data_explorer/public/plugin.ts @@ -10,15 +10,29 @@ import { Plugin, AppNavLinkStatus, } from '../../../core/public'; -import { DataExplorerPluginSetup, DataExplorerPluginStart, DataExplorerServices } from './types'; +import { + DataExplorerPluginSetup, + DataExplorerPluginSetupDependencies, + DataExplorerPluginStart, + DataExplorerPluginStartDependencies, + DataExplorerServices, +} from './types'; import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { ViewService } from './services/view_service'; export class DataExplorerPlugin - implements Plugin { + implements + Plugin< + DataExplorerPluginSetup, + DataExplorerPluginStart, + DataExplorerPluginSetupDependencies, + DataExplorerPluginStartDependencies + > { private viewService = new ViewService(); - public setup(core: CoreSetup): DataExplorerPluginSetup { + public setup( + core: CoreSetup + ): DataExplorerPluginSetup { const viewService = this.viewService; // Register an application into the side navigation menu core.application.register({ @@ -26,7 +40,10 @@ export class DataExplorerPlugin title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, async mount(params: AppMountParameters) { - const [coreStart, depsStart] = await core.getStartServices(); + const [coreStart, pluginsStart] = await core.getStartServices(); + + // make sure the index pattern list is up to date + pluginsStart.data.indexPatterns.clearCache(); const services: DataExplorerServices = { ...coreStart, diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index ee13e8a6b467..d56f8f3058b1 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -// TODO: Correctly type this file. +import { AppMountParameters } from '../../../../../core/public'; + +// TODO: State management props interface ViewListItem { id: string; @@ -13,6 +15,7 @@ interface ViewListItem { export interface ViewMountParameters { canvasElement: HTMLDivElement; panelElement: HTMLDivElement; + appParams: AppMountParameters; } export interface ViewDefinition { diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts index 00c61864fe19..8976745eb3f7 100644 --- a/src/plugins/data_explorer/public/types.ts +++ b/src/plugins/data_explorer/public/types.ts @@ -5,6 +5,7 @@ import { CoreStart } from 'opensearch-dashboards/public'; import { ViewService } from './services/view_service'; +import { DataPublicPluginStart } from '../../data/public'; export interface DataExplorerPluginSetup { registerView: ViewService['registerView']; @@ -12,6 +13,12 @@ export interface DataExplorerPluginSetup { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataExplorerPluginStart {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DataExplorerPluginSetupDependencies {} +export interface DataExplorerPluginStartDependencies { + data: DataPublicPluginStart; +} + export interface ViewRedirectParams { view: string; path?: string; diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index d41669c16959..0cac73333e25 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -4,7 +4,7 @@ */ export const PLUGIN_ID = 'discover'; -export const DISCOVER_LEGACY_TOGGLE = 'discover:legacyToggle'; +export const NEW_DISCOVER_APP = 'discover:v2'; export const DEFAULT_COLUMNS_SETTING = 'defaultColumns'; export const SAMPLE_SIZE_SETTING = 'discover:sampleSize'; export const AGGS_TERMS_SIZE_SETTING = 'discover:aggs:terms:size'; diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx new file mode 100644 index 000000000000..8f512a10837f --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { AppMountParameters } from '../../../../../../core/public'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DiscoverServices } from '../../../build_services'; +import { TopNav } from './top_nav'; + +interface CanvasProps { + opts: { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + }; +} + +export const Canvas = ({ opts }: CanvasProps) => { + const { services } = useOpenSearchDashboards(); + + return ( +
+ + Canvas +
+ ); +}; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index e7b894b18a97..2e911832d146 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -8,17 +8,21 @@ import ReactDOM from 'react-dom'; import { ViewMountParameters } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; +import { Canvas } from './canvas'; export const renderCanvas = ( - { canvasElement }: ViewMountParameters, + { canvasElement, appParams }: ViewMountParameters, services: DiscoverServices ) => { + const { setHeaderActionMenu } = appParams; + ReactDOM.render( - {/* This is dummy code, inline styles will not be added in production */} -
- {JSON.stringify(services.capabilities.navLinks, null, 2)} -
+
, canvasElement ); diff --git a/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx new file mode 100644 index 000000000000..f6d18f08d05c --- /dev/null +++ b/src/plugins/discover/public/application/view_components/canvas/top_nav.tsx @@ -0,0 +1,79 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { AppMountParameters } from '../../../../../../core/public'; +import { NEW_DISCOVER_APP, PLUGIN_ID } from '../../../../common'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DiscoverServices } from '../../../build_services'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; + +export interface TopNavProps { + opts: { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + }; +} + +export const TopNav = ({ opts }: TopNavProps) => { + const { + services: { + uiSettings, + navigation: { + ui: { TopNavMenu }, + }, + core: { + application: { navigateToApp }, + }, + data, + }, + } = useOpenSearchDashboards(); + const [indexPatterns, setIndexPatterns] = useState(undefined); + + useEffect(() => { + if (uiSettings.get(NEW_DISCOVER_APP) === false) { + const path = window.location.hash; + navigateToApp('discoverLegacy', { + replace: true, + path, + }); + } + + return () => {}; + }, [navigateToApp, uiSettings]); + + useEffect(() => { + const getDefaultIndexPattern = async () => { + await data.indexPatterns.ensureDefaultIndexPattern(); + const indexPattern = await data.indexPatterns.getDefault(); + + setIndexPatterns(indexPattern ? [indexPattern] : undefined); + }; + + getDefaultIndexPattern(); + }, [data.indexPatterns]); + + return ( + { + await uiSettings.set(NEW_DISCOVER_APP, false); + window.location.reload(); + }, + emphasize: true, + }, + ]} + showSearchBar + useDefaultBehaviors + setMenuMountPoint={opts.setHeaderActionMenu} + indexPatterns={indexPatterns} + /> + ); +}; diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index 9e4d9a040e2e..f92e5af0bfdf 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -8,11 +8,12 @@ import ReactDOM from 'react-dom'; import { ViewMountParameters } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; +import { Panel } from './panel'; export const renderPanel = ({ panelElement }: ViewMountParameters, services: DiscoverServices) => { ReactDOM.render( -
Side panel
+
, panelElement ); diff --git a/src/plugins/discover/public/application/view_components/panel/panel.tsx b/src/plugins/discover/public/application/view_components/panel/panel.tsx new file mode 100644 index 000000000000..fe3b36f6e87b --- /dev/null +++ b/src/plugins/discover/public/application/view_components/panel/panel.tsx @@ -0,0 +1,10 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; + +export const Panel = () => { + return
Side Panel
; +}; diff --git a/src/plugins/discover/public/index.ts b/src/plugins/discover/public/index.ts index 191ecf14cbbf..3bc009914940 100644 --- a/src/plugins/discover/public/index.ts +++ b/src/plugins/discover/public/index.ts @@ -40,3 +40,4 @@ export { SavedSearch, SavedSearchLoader, createSavedSearchesLoader } from './sav // TODO: Fix embeddable after removing Angular // export { ISearchEmbeddable, SEARCH_EMBEDDABLE_TYPE, SearchInput } from './application/embeddable'; export { DISCOVER_APP_URL_GENERATOR, DiscoverUrlGeneratorState } from './url_generator'; +export { NEW_DISCOVER_APP } from '../common'; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 34cc84ee26d1..b40f76dbf2ef 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -86,7 +86,7 @@ import { DiscoverUrlGenerator, } from './url_generator'; // import { SearchEmbeddableFactory } from './application/embeddable'; -import { DISCOVER_LEGACY_TOGGLE, PLUGIN_ID } from '../common'; +import { NEW_DISCOVER_APP, PLUGIN_ID } from '../common'; import { DataExplorerPluginSetup, ViewRedirectParams } from '../../data_explorer/public'; import { registerFeature } from './register_feature'; @@ -319,9 +319,10 @@ export class DiscoverPlugin }, } = await this.initializeServices(); + // This is for instances where the user navigates to the app from the application nav menu const path = window.location.hash; - const enableLegacyMode = await core.uiSettings.get(DISCOVER_LEGACY_TOGGLE); - if (enableLegacyMode) { + const v2Enabled = await core.uiSettings.get(NEW_DISCOVER_APP); + if (!v2Enabled) { navigateToApp('discoverLegacy', { replace: true, path, diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index a802897c2485..2b35384c2e5c 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -33,7 +33,7 @@ import { schema } from '@osd/config-schema'; import { UiSettingsParams } from 'opensearch-dashboards/server'; import { - DISCOVER_LEGACY_TOGGLE, + NEW_DISCOVER_APP, DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING, AGGS_TERMS_SIZE_SETTING, @@ -48,9 +48,9 @@ import { } from '../common'; export const uiSettings: Record = { - [DISCOVER_LEGACY_TOGGLE]: { + [NEW_DISCOVER_APP]: { name: i18n.translate('discover.advancedSettings.legacyToggleTitle', { - defaultMessage: 'Disable new discover app', + defaultMessage: 'Enable new discover app', }), value: true, description: i18n.translate('discover.advancedSettings.legacyToggleText', { diff --git a/src/plugins/discover_legacy/opensearch_dashboards.json b/src/plugins/discover_legacy/opensearch_dashboards.json index 6cc4a8930ee4..6a4259a41d75 100644 --- a/src/plugins/discover_legacy/opensearch_dashboards.json +++ b/src/plugins/discover_legacy/opensearch_dashboards.json @@ -14,6 +14,14 @@ "uiActions", "visualizations" ], - "optionalPlugins": ["home", "share"], - "requiredBundles": ["opensearchDashboardsUtils", "savedObjects", "opensearchDashboardsReact"] -} + "optionalPlugins": [ + "home", + "share" + ], + "requiredBundles": [ + "opensearchDashboardsUtils", + "savedObjects", + "opensearchDashboardsReact", + "discover" + ] +} \ No newline at end of file diff --git a/src/plugins/discover_legacy/public/application/angular/discover.js b/src/plugins/discover_legacy/public/application/angular/discover.js index de244e3c44b6..5c5eaecb35e3 100644 --- a/src/plugins/discover_legacy/public/application/angular/discover.js +++ b/src/plugins/discover_legacy/public/application/angular/discover.js @@ -93,6 +93,7 @@ import { DOC_HIDE_TIME_COLUMN_SETTING, MODIFY_COLUMNS_ON_SWITCH, } from '../../../common'; +import { NEW_DISCOVER_APP } from '../../../../discover/public'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -480,7 +481,23 @@ function discoverController($element, $route, $scope, $timeout, $window, Promise }, }; + const newDiscover = { + id: 'discover-new', + label: i18n.translate('discover.localMenu.newDiscoverTitle', { + defaultMessage: 'New Discover', + }), + description: i18n.translate('discover.localMenu.newDiscoverDescription', { + defaultMessage: 'New Discover Experience', + }), + testId: 'discoverNewButton', + run: async function () { + await getServices().uiSettings.set(NEW_DISCOVER_APP, true); + window.location.reload(); + }, + }; + return [ + newDiscover, newSearch, ...(uiCapabilities.discover.save ? [saveSearch] : []), openSearch, diff --git a/src/plugins/discover_legacy/public/plugin.ts b/src/plugins/discover_legacy/public/plugin.ts index 305fe62c07ef..85cd68a07791 100644 --- a/src/plugins/discover_legacy/public/plugin.ts +++ b/src/plugins/discover_legacy/public/plugin.ts @@ -56,6 +56,7 @@ import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { stringify } from 'query-string'; import rison from 'rison-node'; +import { NEW_DISCOVER_APP } from '../../discover/public'; import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; @@ -88,6 +89,7 @@ import { } from './url_generator'; import { SearchEmbeddableFactory } from './application/embeddable'; import { AppNavLinkStatus } from '../../../core/public'; +import { ViewRedirectParams } from '../../data_explorer/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -318,6 +320,25 @@ export class DiscoverPlugin if (!this.initializeInnerAngular) { throw Error('Discover plugin method initializeInnerAngular is undefined'); } + + // If a user explicitly tries to access the legacy app URL + const { + core: { + application: { navigateToApp }, + }, + } = await this.initializeServices(); + const path = window.location.hash; + + const v2Enabled = core.uiSettings.get(NEW_DISCOVER_APP); + if (v2Enabled) { + navigateToApp('data-explorer', { + replace: true, + path: `/discover`, + state: { + path, + } as ViewRedirectParams, + }); + } setScopedHistory(params.history); setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); From a1d12706051d176d0cbe48cf37a18d60c6ec65cd Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 13 Jul 2023 07:04:28 +0000 Subject: [PATCH 02/16] Fixes header offset Signed-off-by: Ashwin P Chandran --- src/plugins/data_explorer/public/index.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/plugins/data_explorer/public/index.scss b/src/plugins/data_explorer/public/index.scss index 1a807b7b60ab..8389e31b426a 100644 --- a/src/plugins/data_explorer/public/index.scss +++ b/src/plugins/data_explorer/public/index.scss @@ -3,3 +3,7 @@ $osdHeaderOffset: $euiHeaderHeightCompensation; .dePageTemplate { height: calc(100vh - #{$osdHeaderOffset}); } + +.headerIsExpanded .dePageTemplate { + height: calc(100vh - #{$osdHeaderOffset * 2}); +} From 4a72f862c2bea12d6a3bdd81d8134ad465718ae1 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Fri, 14 Jul 2023 07:48:40 +0000 Subject: [PATCH 03/16] adds basic state management Signed-off-by: Ashwin P Chandran --- .../data_explorer/opensearch_dashboards.json | 4 +- .../data_explorer/public/application.tsx | 23 ++-- .../public/components/sidebar.tsx | 40 ------- .../public/components/sidebar/index.tsx | 102 ++++++++++++++++++ src/plugins/data_explorer/public/plugin.ts | 34 +++++- .../public/services/view_service/types.ts | 2 +- .../services/view_service/view_service.ts | 10 +- src/plugins/data_explorer/public/types.ts | 21 ++-- .../data_explorer/public/utils/mocks.ts | 35 ++++++ .../public/utils/state_management/hooks.ts | 11 ++ .../public/utils/state_management/index.ts | 7 ++ .../utils/state_management/metadata_slice.ts | 52 +++++++++ .../public/utils/state_management/preload.ts | 21 ++++ .../redux_persistence.test.tsx | 45 ++++++++ .../state_management/redux_persistence.ts | 30 ++++++ .../public/utils/state_management/store.ts | 65 +++++++++++ 16 files changed, 435 insertions(+), 67 deletions(-) delete mode 100644 src/plugins/data_explorer/public/components/sidebar.tsx create mode 100644 src/plugins/data_explorer/public/components/sidebar/index.tsx create mode 100644 src/plugins/data_explorer/public/utils/mocks.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/hooks.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/index.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/preload.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx create mode 100644 src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts create mode 100644 src/plugins/data_explorer/public/utils/state_management/store.ts diff --git a/src/plugins/data_explorer/opensearch_dashboards.json b/src/plugins/data_explorer/opensearch_dashboards.json index 7b5a78c55030..8cc6f5991ce6 100644 --- a/src/plugins/data_explorer/opensearch_dashboards.json +++ b/src/plugins/data_explorer/opensearch_dashboards.json @@ -4,7 +4,7 @@ "opensearchDashboardsVersion": "opensearchDashboards", "server": true, "ui": true, - "requiredPlugins": ["data", "navigation"], + "requiredPlugins": ["data", "navigation", "embeddable", "expressions"], "optionalPlugins": [], - "requiredBundles": ["opensearchDashboardsReact"] + "requiredBundles": ["opensearchDashboardsReact","opensearchDashboardsUtils"] } diff --git a/src/plugins/data_explorer/public/application.tsx b/src/plugins/data_explorer/public/application.tsx index 505209358ff1..ae57070f7451 100644 --- a/src/plugins/data_explorer/public/application.tsx +++ b/src/plugins/data_explorer/public/application.tsx @@ -5,28 +5,33 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider as ReduxProvider } from 'react-redux'; import { Router, Route, Switch } from 'react-router-dom'; import { AppMountParameters, CoreStart } from '../../../core/public'; import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public'; import { DataExplorerServices } from './types'; import { DataExplorerApp } from './components/app'; +import { Store } from './utils/state_management'; export const renderApp = ( - { notifications, http }: CoreStart, + core: CoreStart, services: DataExplorerServices, - params: AppMountParameters + params: AppMountParameters, + store: Store ) => { const { history, element } = params; ReactDOM.render( - - - - - - - + + + + + + + + + , element diff --git a/src/plugins/data_explorer/public/components/sidebar.tsx b/src/plugins/data_explorer/public/components/sidebar.tsx deleted file mode 100644 index 3d6e5070a5ef..000000000000 --- a/src/plugins/data_explorer/public/components/sidebar.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo, FC } from 'react'; -import { EuiPanel, EuiComboBox, EuiSelect, EuiSelectOption } from '@elastic/eui'; -import { useView } from '../utils/use'; - -export const Sidebar: FC = ({ children }) => { - const { view, viewRegistry } = useView(); - const views = viewRegistry.all(); - const viewOptions: EuiSelectOption[] = useMemo( - () => - views.map(({ id, title }) => ({ - value: id, - text: title, - })), - [views] - ); - return ( - <> - - {}} - /> - - - {children} - - ); -}; diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx new file mode 100644 index 000000000000..3900d2bd9467 --- /dev/null +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -0,0 +1,102 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, FC, useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { + EuiPanel, + EuiComboBox, + EuiSelect, + EuiSelectOption, + EuiComboBoxOptionOption, +} from '@elastic/eui'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { useView } from '../../utils/use'; +import { DataExplorerServices } from '../../types'; +import { useTypedDispatch, useTypedSelector, setIndexPattern } from '../../utils/state_management'; + +export const Sidebar: FC = ({ children }) => { + const { indexPattern: indexPatternId } = useTypedSelector((state) => state.metadata); + const dispatch = useTypedDispatch(); + const [options, setOptions] = useState>>([]); + const [selectedOption, setSelectedOption] = useState>(); + const { view, viewRegistry } = useView(); + const views = viewRegistry.all(); + const viewOptions: EuiSelectOption[] = useMemo( + () => + views.map(({ id, title }) => ({ + value: id, + text: title, + })), + [views] + ); + + const { + services: { + data: { indexPatterns }, + notifications: { toasts }, + }, + } = useOpenSearchDashboards(); + + useEffect(() => { + const fetchIndexPatterns = async () => { + await indexPatterns.ensureDefaultIndexPattern(); + const cache = await indexPatterns.getCache(); + const currentOptions = (cache || []).map((indexPattern) => ({ + label: indexPattern.attributes.title, + value: indexPattern.id, + })); + setOptions(currentOptions); + }; + fetchIndexPatterns(); + }, [indexPatterns]); + + // Set option to the current index pattern + useEffect(() => { + if (indexPatternId) { + const option = options.find((o) => o.value === indexPatternId); + setSelectedOption(option); + } + }, [indexPatternId, options]); + + return ( + <> + + { + // TODO: There are many issues with this approach, but it's a start + // 1. Combo box can delete a selected index pattern. This should not be possible + // 2. Combo box is severely truncated. This should be fixed in the EUI component + // 3. The onchange can fire with a option that is not valid. discuss where to handle this. + // 4. value is optional. If the combobox needs to act as a slecet, this should be required. + const { value } = selected[0] || {}; + + if (!value) { + toasts.addWarning({ + id: 'index-pattern-not-found', + title: i18n.translate('dataExplorer.indexPatternError', { + defaultMessage: 'Index pattern not found', + }), + }); + return; + } + + dispatch( + setIndexPattern({ + state: value, + }) + ); + }} + /> + + + {children} + + ); +}; diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts index d44e6713cc7a..e1ea9a703a8e 100644 --- a/src/plugins/data_explorer/public/plugin.ts +++ b/src/plugins/data_explorer/public/plugin.ts @@ -9,6 +9,7 @@ import { CoreStart, Plugin, AppNavLinkStatus, + ScopedHistory, } from '../../../core/public'; import { DataExplorerPluginSetup, @@ -19,6 +20,11 @@ import { } from './types'; import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { ViewService } from './services/view_service'; +import { + createOsdUrlStateStorage, + withNotifyOnErrors, +} from '../../opensearch_dashboards_utils/public'; +import { getPreloadedStore } from './utils/state_management'; export class DataExplorerPlugin implements @@ -29,32 +35,52 @@ export class DataExplorerPlugin DataExplorerPluginStartDependencies > { private viewService = new ViewService(); + private currentHistory?: ScopedHistory; public setup( core: CoreSetup ): DataExplorerPluginSetup { const viewService = this.viewService; + // Register an application into the side navigation menu core.application.register({ id: PLUGIN_ID, title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, - async mount(params: AppMountParameters) { + mount: async (params: AppMountParameters) => { + // Load application bundle + const { renderApp } = await import('./application'); + const [coreStart, pluginsStart] = await core.getStartServices(); + this.currentHistory = params.history; // make sure the index pattern list is up to date pluginsStart.data.indexPatterns.clearCache(); const services: DataExplorerServices = { ...coreStart, + scopedHistory: this.currentHistory, + data: pluginsStart.data, + embeddable: pluginsStart.embeddable, + expressions: pluginsStart.expressions, + osdUrlStateStorage: createOsdUrlStateStorage({ + history: this.currentHistory, + useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), + ...withNotifyOnErrors(coreStart.notifications.toasts), + }), viewRegistry: viewService.start(), }; - // Load application bundle - const { renderApp } = await import('./application'); // Get start services as specified in opensearch_dashboards.json // Render the application - return renderApp(coreStart, services, params); + const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); + + const unmount = renderApp(coreStart, services, params, store); + + return () => { + unsubscribeStore(); + unmount(); + }; }, }); diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index d56f8f3058b1..aea8c25a0f98 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -22,7 +22,7 @@ export interface ViewDefinition { readonly id: string; readonly title: string; readonly ui?: { - defaults: T; + defaults: T | (() => T); reducer: (state: T, action: any) => T; }; readonly mount: (params: ViewMountParameters) => Promise<() => void>; diff --git a/src/plugins/data_explorer/public/services/view_service/view_service.ts b/src/plugins/data_explorer/public/services/view_service/view_service.ts index e527e1645730..895bfe3dd422 100644 --- a/src/plugins/data_explorer/public/services/view_service/view_service.ts +++ b/src/plugins/data_explorer/public/services/view_service/view_service.ts @@ -38,13 +38,13 @@ import { View } from './view'; * @internal */ export class ViewService implements CoreService { - private views: Record = {}; + private views: Record = {}; - private registerView(viewDefinition: View) { - if (this.views[viewDefinition.id]) { - throw new Error(`A view with this the id ${viewDefinition.id} already exists!`); + private registerView(view: View) { + if (this.views[view.id]) { + throw new Error(`A view with this the id ${view.id} already exists!`); } - this.views[viewDefinition.id] = viewDefinition; + this.views[view.id] = view; } public setup() { diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts index 8976745eb3f7..18c22a3d1c2c 100644 --- a/src/plugins/data_explorer/public/types.ts +++ b/src/plugins/data_explorer/public/types.ts @@ -3,19 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CoreStart } from 'opensearch-dashboards/public'; -import { ViewService } from './services/view_service'; +import { CoreStart, ScopedHistory } from 'opensearch-dashboards/public'; +import { EmbeddableStart } from '../../embeddable/public'; +import { ExpressionsStart } from '../../expressions/public'; +import { ViewServiceStart, ViewServiceSetup } from './services/view_service'; +import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; import { DataPublicPluginStart } from '../../data/public'; -export interface DataExplorerPluginSetup { - registerView: ViewService['registerView']; -} +export type DataExplorerPluginSetup = ViewServiceSetup; + // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataExplorerPluginStart {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataExplorerPluginSetupDependencies {} export interface DataExplorerPluginStartDependencies { + expressions: ExpressionsStart; + embeddable: EmbeddableStart; data: DataPublicPluginStart; } @@ -25,5 +29,10 @@ export interface ViewRedirectParams { } export interface DataExplorerServices extends CoreStart { - viewRegistry: ReturnType; + viewRegistry: ViewServiceStart; + expressions: ExpressionsStart; + embeddable: EmbeddableStart; + data: DataPublicPluginStart; + scopedHistory: ScopedHistory; + osdUrlStateStorage: IOsdUrlStateStorage; } diff --git a/src/plugins/data_explorer/public/utils/mocks.ts b/src/plugins/data_explorer/public/utils/mocks.ts new file mode 100644 index 000000000000..b0bda1a9c60f --- /dev/null +++ b/src/plugins/data_explorer/public/utils/mocks.ts @@ -0,0 +1,35 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ScopedHistory } from '../../../../core/public'; +import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { embeddablePluginMock } from '../../../embeddable/public/mocks'; +import { expressionsPluginMock } from '../../../expressions/public/mocks'; +import { createOsdUrlStateStorage } from '../../../opensearch_dashboards_utils/public'; +import { DataExplorerServices } from '../types'; + +export const createDataExplorerServicesMock = () => { + const coreStartMock = coreMock.createStart(); + const dataMock = dataPluginMock.createStartContract(); + const embeddableMock = embeddablePluginMock.createStartContract(); + const expressionMock = expressionsPluginMock.createStartContract(); + const osdUrlStateStorageMock = createOsdUrlStateStorage({ useHash: false }); + + const dataExplorerServicesMock: DataExplorerServices = { + ...coreStartMock, + expressions: expressionMock, + data: dataMock, + osdUrlStateStorage: osdUrlStateStorageMock, + embeddable: embeddableMock, + scopedHistory: (scopedHistoryMock.create() as unknown) as ScopedHistory, + viewRegistry: { + get: jest.fn(), + all: jest.fn(), + }, + }; + + return (dataExplorerServicesMock as unknown) as jest.Mocked; +}; diff --git a/src/plugins/data_explorer/public/utils/state_management/hooks.ts b/src/plugins/data_explorer/public/utils/state_management/hooks.ts new file mode 100644 index 000000000000..607fe05b1623 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/hooks.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// Use throughout the app instead of plain `useDispatch` and `useSelector` +export const useTypedDispatch = () => useDispatch(); +export const useTypedSelector: TypedUseSelectorHook = useSelector; diff --git a/src/plugins/data_explorer/public/utils/state_management/index.ts b/src/plugins/data_explorer/public/utils/state_management/index.ts new file mode 100644 index 000000000000..edb5c2a17184 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './store'; +export * from './hooks'; diff --git a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts new file mode 100644 index 000000000000..2b1ee7d43759 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts @@ -0,0 +1,52 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { DataExplorerServices } from '../../types'; + +export interface MetadataState { + indexPattern?: string; + originatingApp?: string; +} + +const initialState: MetadataState = {}; + +export const getPreloadedState = async ({ + embeddable, + scopedHistory, + data, +}: DataExplorerServices): Promise => { + const { originatingApp } = + embeddable + .getStateTransfer(scopedHistory) + .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; + const defaultIndexPattern = await data.indexPatterns.getDefault(); + const preloadedState: MetadataState = { + ...initialState, + originatingApp, + indexPattern: defaultIndexPattern?.id, + }; + + return preloadedState; +}; + +export const slice = createSlice({ + name: 'metadata', + initialState, + reducers: { + setIndexPattern: (state, action: PayloadAction<{ state?: string }>) => { + state.indexPattern = action.payload.state; + }, + setOriginatingApp: (state, action: PayloadAction<{ state?: string }>) => { + state.originatingApp = action.payload.state; + }, + setState: (_state, action: PayloadAction) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setIndexPattern, setOriginatingApp, setState } = slice.actions; diff --git a/src/plugins/data_explorer/public/utils/state_management/preload.ts b/src/plugins/data_explorer/public/utils/state_management/preload.ts new file mode 100644 index 000000000000..b6255170bed5 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/preload.ts @@ -0,0 +1,21 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PreloadedState } from '@reduxjs/toolkit'; +import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; +import { RootState } from './store'; +import { DataExplorerServices } from '../../types'; + +export const getPreloadedState = async ( + services: DataExplorerServices +): Promise> => { + const metadataState = await getPreloadedMetadataState(services); + + // TODO: preload view states + + return { + metadata: metadataState, + }; +}; diff --git a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx new file mode 100644 index 000000000000..4acf56d0fc55 --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataExplorerServices } from '../../types'; +import { createDataExplorerServicesMock } from '../mocks'; +import { loadReduxState, persistReduxState } from './redux_persistence'; +import { RootState } from './store'; + +describe('test redux state persistence', () => { + let mockServices: jest.Mocked; + let reduxStateParams: any; + + beforeEach(() => { + mockServices = createDataExplorerServicesMock(); + reduxStateParams = { + style: 'style', + visualization: 'visualization', + metadata: 'metadata', + ui: 'ui', + }; + }); + + test('test load redux state when url is empty', async () => { + const defaultStates: RootState = { + metadata: {}, + }; + + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(defaultStates); + }); + + test('test load redux state', async () => { + mockServices.osdUrlStateStorage.set('_a', reduxStateParams, { replace: true }); + const returnStates = await loadReduxState(mockServices); + expect(returnStates).toStrictEqual(reduxStateParams); + }); + + test('test persist redux state', () => { + persistReduxState(reduxStateParams, mockServices); + const urlStates = mockServices.osdUrlStateStorage.get('_a'); + expect(urlStates).toStrictEqual(reduxStateParams); + }); +}); diff --git a/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts new file mode 100644 index 000000000000..81517f3e9f4f --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/redux_persistence.ts @@ -0,0 +1,30 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DataExplorerServices } from '../../types'; +import { getPreloadedState } from './preload'; +import { RootState } from './store'; + +export const loadReduxState = async (services: DataExplorerServices) => { + try { + const serializedState = services.osdUrlStateStorage.get('_a'); + if (serializedState !== null) return serializedState; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + + return await getPreloadedState(services); +}; + +export const persistReduxState = (root: RootState, services: DataExplorerServices) => { + try { + services.osdUrlStateStorage.set('_a', root, { + replace: true, + }); + } catch (err) { + return; + } +}; diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts new file mode 100644 index 000000000000..2a4175ec2e6d --- /dev/null +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -0,0 +1,65 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { reducer as metadataReducer } from './metadata_slice'; +import { loadReduxState, persistReduxState } from './redux_persistence'; +import { DataExplorerServices } from '../../types'; + +const rootReducer = combineReducers({ + metadata: metadataReducer, +}); + +export const configurePreloadedStore = (preloadedState: PreloadedState) => { + return configureStore({ + reducer: rootReducer, + preloadedState, + }); +}; + +export const getPreloadedStore = async (services: DataExplorerServices) => { + const preloadedState = await loadReduxState(services); + const store = configurePreloadedStore(preloadedState); + + let previousState = store.getState(); + + // Listen to changes + const handleChange = () => { + const state = store.getState(); + persistReduxState(state, services); + + if (isEqual(state, previousState)) return; + + // Add Side effects here to apply after changes to the store are made. None for now. + + previousState = state; + }; + + // the store subscriber will automatically detect changes and call handleChange function + const unsubscribe = store.subscribe(handleChange); + + return { store, unsubscribe }; +}; + +// export const registerSlice = (slice: any) => { +// dynamicReducers[slice.name] = slice.reducer; +// store.replaceReducer(combineReducers(dynamicReducers)); + +// // Extend RootState to include the new slice +// declare module 'path-to-main-store' { +// interface RootState { +// [slice.name]: ReturnType; +// } +// } +// } + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +export type RenderState = Omit; // Remaining state after auxillary states are removed +export type Store = ReturnType; +export type AppDispatch = Store['dispatch']; + +export { MetadataState, setIndexPattern, setOriginatingApp } from './metadata_slice'; From 32a1299a52450cd458413111be1c2362cfbdb52b Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Fri, 14 Jul 2023 07:49:19 +0000 Subject: [PATCH 04/16] attempt 1 at dynamic state management Signed-off-by: Ashwin P Chandran --- .../public/components/app_container.tsx | 9 +- src/plugins/data_explorer/public/index.ts | 1 + src/plugins/data_explorer/public/plugin.ts | 10 +++ .../public/services/view_service/types.ts | 7 +- .../services/view_service/view_service.ts | 2 +- src/plugins/data_explorer/public/types.ts | 2 + .../public/utils/state_management/preload.ts | 25 ++++-- .../public/utils/state_management/store.ts | 46 ++++++---- .../utils/state_management/discover_slice.tsx | 87 +++++++++++++++++++ .../utils/state_management/index.ts | 4 + .../view_components/canvas/canvas.tsx | 18 ++++ .../view_components/canvas/index.tsx | 16 ++-- src/plugins/discover/public/plugin.ts | 54 ++++++------ 13 files changed, 221 insertions(+), 60 deletions(-) create mode 100644 src/plugins/discover/public/application/utils/state_management/discover_slice.tsx create mode 100644 src/plugins/discover/public/application/utils/state_management/index.ts diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index 8923d2c5b175..8ecd045a2450 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -9,9 +9,14 @@ import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; import { NoView } from './no_view'; import { View } from '../services/view_service/view'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { DataExplorerServices } from '../types'; export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { const [showSpinner, setShowSpinner] = useState(false); + const { + services: { store }, + } = useOpenSearchDashboards(); const canvasRef = useRef(null); const panelRef = useRef(null); const unmountRef = useRef(null); @@ -40,6 +45,8 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa canvasElement: canvasRef.current!, panelElement: panelRef.current!, appParams: params, + // The provider is added to the services right after the store is created. so it is safe to assume its here. + store: store!, })) || null; } catch (e) { // TODO: add error UI @@ -56,7 +63,7 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa mount(); return unmount; - }, [params, view]); + }, [params, view, store]); // TODO: Make this more robust. if (!view) { diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index ce419fbdce0f..eb0db4303753 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -14,3 +14,4 @@ export function plugin() { } export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types'; export { ViewMountParameters, ViewDefinition } from './services/view_service'; +export { RootState as DataExplorerRootState } from './utils/state_management'; diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts index e1ea9a703a8e..a78d16246943 100644 --- a/src/plugins/data_explorer/public/plugin.ts +++ b/src/plugins/data_explorer/public/plugin.ts @@ -41,6 +41,9 @@ export class DataExplorerPlugin core: CoreSetup ): DataExplorerPluginSetup { const viewService = this.viewService; + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('data_explorer: Setup'); // Register an application into the side navigation menu core.application.register({ @@ -48,6 +51,9 @@ export class DataExplorerPlugin title: PLUGIN_NAME, navLinkStatus: AppNavLinkStatus.hidden, mount: async (params: AppMountParameters) => { + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('data_explorer: Mounted'); // Load application bundle const { renderApp } = await import('./application'); @@ -74,6 +80,7 @@ export class DataExplorerPlugin // Get start services as specified in opensearch_dashboards.json // Render the application const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); + services.store = store; const unmount = renderApp(coreStart, services, params, store); @@ -90,6 +97,9 @@ export class DataExplorerPlugin } public start(core: CoreStart): DataExplorerPluginStart { + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('data_explorer: Started'); return {}; } diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index aea8c25a0f98..ed34f5d32479 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -3,7 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { Slice } from '@reduxjs/toolkit'; import { AppMountParameters } from '../../../../../core/public'; +import { Store } from '../../utils/state_management'; // TODO: State management props @@ -16,14 +18,15 @@ export interface ViewMountParameters { canvasElement: HTMLDivElement; panelElement: HTMLDivElement; appParams: AppMountParameters; + store: any; } export interface ViewDefinition { readonly id: string; readonly title: string; readonly ui?: { - defaults: T | (() => T); - reducer: (state: T, action: any) => T; + defaults: T | (() => T) | (() => Promise); + slice: Slice; }; readonly mount: (params: ViewMountParameters) => Promise<() => void>; readonly defaultPath: string; diff --git a/src/plugins/data_explorer/public/services/view_service/view_service.ts b/src/plugins/data_explorer/public/services/view_service/view_service.ts index 895bfe3dd422..02d30d838e42 100644 --- a/src/plugins/data_explorer/public/services/view_service/view_service.ts +++ b/src/plugins/data_explorer/public/services/view_service/view_service.ts @@ -53,7 +53,7 @@ export class ViewService implements CoreService { + registerView: (config: ViewDefinition): void => { const view = new View(config); this.registerView(view); }, diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts index 18c22a3d1c2c..b99ca9640b0e 100644 --- a/src/plugins/data_explorer/public/types.ts +++ b/src/plugins/data_explorer/public/types.ts @@ -9,6 +9,7 @@ import { ExpressionsStart } from '../../expressions/public'; import { ViewServiceStart, ViewServiceSetup } from './services/view_service'; import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; import { DataPublicPluginStart } from '../../data/public'; +import { Store } from './utils/state_management'; export type DataExplorerPluginSetup = ViewServiceSetup; @@ -29,6 +30,7 @@ export interface ViewRedirectParams { } export interface DataExplorerServices extends CoreStart { + store?: Store; viewRegistry: ViewServiceStart; expressions: ExpressionsStart; embeddable: EmbeddableStart; diff --git a/src/plugins/data_explorer/public/utils/state_management/preload.ts b/src/plugins/data_explorer/public/utils/state_management/preload.ts index b6255170bed5..9cb85c3f4912 100644 --- a/src/plugins/data_explorer/public/utils/state_management/preload.ts +++ b/src/plugins/data_explorer/public/utils/state_management/preload.ts @@ -11,11 +11,26 @@ import { DataExplorerServices } from '../../types'; export const getPreloadedState = async ( services: DataExplorerServices ): Promise> => { - const metadataState = await getPreloadedMetadataState(services); + const rootState: RootState = { + metadata: await getPreloadedMetadataState(services), + }; - // TODO: preload view states + // initialize the default state for each view + const views = services.viewRegistry.all(); + views.forEach(async (view) => { + if (!view.ui) { + return; + } - return { - metadata: metadataState, - }; + const { defaults } = view.ui; + + // defaults can be a function or an object + if (typeof defaults === 'function') { + rootState[view.id] = await defaults(); + } else { + rootState[view.id] = defaults; + } + }); + + return rootState; }; diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts index 2a4175ec2e6d..ccc939237dd2 100644 --- a/src/plugins/data_explorer/public/utils/state_management/store.ts +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -3,24 +3,43 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; +import React from 'react'; +import { combineReducers, configureStore, PreloadedState, Reducer, Slice } from '@reduxjs/toolkit'; import { isEqual } from 'lodash'; -import { reducer as metadataReducer } from './metadata_slice'; +import { Provider } from 'react-redux'; +import { reducer as metadataReducer, MetadataState } from './metadata_slice'; import { loadReduxState, persistReduxState } from './redux_persistence'; import { DataExplorerServices } from '../../types'; -const rootReducer = combineReducers({ +const dynamicReducers: { + metadata: Reducer; + [key: string]: Reducer; +} = { metadata: metadataReducer, -}); +}; + +const rootReducer = combineReducers(dynamicReducers); export const configurePreloadedStore = (preloadedState: PreloadedState) => { + // After registering the slices the root reducer needs to be updated + const updatedRootReducer = combineReducers(dynamicReducers); + return configureStore({ - reducer: rootReducer, + reducer: updatedRootReducer, preloadedState, }); }; export const getPreloadedStore = async (services: DataExplorerServices) => { + // For each view preload the data and register the slice + const views = services.viewRegistry.all(); + views.forEach((view) => { + if (!view.ui) return; + + const { slice } = view.ui; + registerSlice(slice); + }); + const preloadedState = await loadReduxState(services); const store = configurePreloadedStore(preloadedState); @@ -44,17 +63,12 @@ export const getPreloadedStore = async (services: DataExplorerServices) => { return { store, unsubscribe }; }; -// export const registerSlice = (slice: any) => { -// dynamicReducers[slice.name] = slice.reducer; -// store.replaceReducer(combineReducers(dynamicReducers)); - -// // Extend RootState to include the new slice -// declare module 'path-to-main-store' { -// interface RootState { -// [slice.name]: ReturnType; -// } -// } -// } +export const registerSlice = (slice: Slice) => { + if (dynamicReducers[slice.name]) { + throw new Error(`Slice ${slice.name} already registered`); + } + dynamicReducers[slice.name] = slice.reducer; +}; // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType; diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx new file mode 100644 index 000000000000..cfac288cd3bc --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -0,0 +1,87 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { TypedUseSelectorHook, createDispatchHook, createSelectorHook } from 'react-redux'; +import { createContext } from 'react'; +import { Filter, Query } from '../../../../../data/public'; +import { DiscoverServices } from '../../../build_services'; +import { DataExplorerRootState } from '../../../../../data_explorer/public'; + +export interface DiscoverState { + /** + * Columns displayed in the table + */ + columns?: string[]; + /** + * Array of applied filters + */ + filters?: Filter[]; + /** + * id of the used index pattern + */ + index?: string; + /** + * Used interval of the histogram + */ + interval?: string; + /** + * Lucence or DQL query + */ + query?: Query; + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][]; + /** + * id of the used saved query + */ + savedQuery?: string; +} + +export interface RootState extends DataExplorerRootState { + discover: DiscoverState; +} + +const initialState = {} as DiscoverState; + +export const getPreloadedState = async ({ data }: DiscoverServices): Promise => { + // console.log(data.query.timefilter.timefilter.getRefreshInterval().value.toString()); + return { + ...initialState, + interval: data.query.timefilter.timefilter.getRefreshInterval().value.toString(), + }; +}; + +export const discoverSlice = createSlice({ + name: 'discover', + initialState, + reducers: { + setState(state: T, action: PayloadAction) { + return action.payload; + }, + updateState(state: T, action: PayloadAction>) { + state = { + ...state, + ...action.payload, + }; + }, + }, +}); + +// Exposing the state functions as generics +export const setState = discoverSlice.actions.setState as (payload: T) => PayloadAction; +export const updateState = discoverSlice.actions.updateState as ( + payload: Partial +) => PayloadAction>; + +export const { reducer } = discoverSlice; +export const contextDiscover = createContext({}); + +export const useTypedSelector: TypedUseSelectorHook = createSelectorHook( + contextDiscover +); + +export const useDispatch = createDispatchHook(contextDiscover); diff --git a/src/plugins/discover/public/application/utils/state_management/index.ts b/src/plugins/discover/public/application/utils/state_management/index.ts new file mode 100644 index 000000000000..a850c1690e3b --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/index.ts @@ -0,0 +1,4 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx index 8f512a10837f..7031c1e00f48 100644 --- a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx @@ -8,6 +8,11 @@ import { AppMountParameters } from '../../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; import { TopNav } from './top_nav'; +import { + updateState, + useDispatch, + useTypedSelector, +} from '../../utils/state_management/discover_slice'; interface CanvasProps { opts: { @@ -17,11 +22,24 @@ interface CanvasProps { export const Canvas = ({ opts }: CanvasProps) => { const { services } = useOpenSearchDashboards(); + const { + discover: { interval }, + } = useTypedSelector((state) => state); + const dispatch = useDispatch(); return (
Canvas + { + dispatch(updateState({ interval: e.target.value })); + }} + />
); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 2e911832d146..66140a696ad6 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -5,24 +5,28 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; import { ViewMountParameters } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; import { Canvas } from './canvas'; +import { contextDiscover } from '../../utils/state_management/discover_slice'; export const renderCanvas = ( - { canvasElement, appParams }: ViewMountParameters, + { canvasElement, appParams, store }: ViewMountParameters, services: DiscoverServices ) => { const { setHeaderActionMenu } = appParams; ReactDOM.render( - + + + , canvasElement ); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index b40f76dbf2ef..590d95c13c67 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -1,31 +1,6 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. */ import { i18n } from '@osd/i18n'; @@ -89,6 +64,11 @@ import { import { NEW_DISCOVER_APP, PLUGIN_ID } from '../common'; import { DataExplorerPluginSetup, ViewRedirectParams } from '../../data_explorer/public'; import { registerFeature } from './register_feature'; +import { + DiscoverState, + discoverSlice, + getPreloadedState, +} from './application/utils/state_management/discover_slice'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -186,6 +166,9 @@ export class DiscoverPlugin private initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('DiscoverPlugin.setup()'); const baseUrl = core.http.basePath.prepend('/app/discover'); if (plugins.share) { @@ -306,6 +289,9 @@ export class DiscoverPlugin defaultPath: '#/', category: DEFAULT_APP_CATEGORIES.opensearchDashboards, mount: async (params: AppMountParameters) => { + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('DiscoverPlugin.mount()'); if (!this.initializeServices) { throw Error('Discover plugin method initializeServices is undefined'); } @@ -373,7 +359,7 @@ export class DiscoverPlugin registerFeature(plugins.home); } - plugins.dataExplorer.registerView({ + plugins.dataExplorer.registerView({ id: PLUGIN_ID, title: 'Discover', defaultPath: '#/', @@ -387,11 +373,18 @@ export class DiscoverPlugin }, }, ui: { - defaults: {}, - reducer: () => ({}), + defaults: async () => { + await this.initializeServices?.(); + const services = getServices(); + return await getPreloadedState(services); + }, + slice: discoverSlice, }, shouldShow: () => true, mount: async (params) => { + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('DiscoverPlugin.dataExplorer.mount()'); const { renderCanvas, renderPanel } = await import('./application/view_components'); const [coreStart, pluginsStart] = await core.getStartServices(); const services = await buildServices(coreStart, pluginsStart, this.initializerContext); @@ -416,6 +409,9 @@ export class DiscoverPlugin } start(core: CoreStart, plugins: DiscoverStartPlugins) { + // TODO: Remove this before merge to main + // eslint-disable-next-line no-console + console.log('DiscoverPlugin.start()'); setUiActions(plugins.uiActions); this.initializeServices = async () => { From 5981002b699be7dcfc7daa90a87c976dcd24515d Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 17 Jul 2023 08:04:18 +0000 Subject: [PATCH 05/16] Working multi view state management Signed-off-by: Ashwin P Chandran --- .../public/components/app_container.tsx | 68 +++---------------- src/plugins/data_explorer/public/index.ts | 4 +- .../public/services/view_service/types.ts | 12 ++-- .../public/services/view_service/view.ts | 6 +- .../public/utils/state_management/hooks.ts | 7 +- .../public/utils/state_management/preload.ts | 3 +- .../utils/state_management/discover_slice.tsx | 21 ++---- .../utils/state_management/index.ts | 13 ++++ .../view_components/canvas/canvas.tsx | 13 ++-- .../view_components/canvas/index.tsx | 37 ++++------ .../view_components/panel/index.tsx | 18 +++-- .../view_components/panel/panel.tsx | 4 +- src/plugins/discover/public/build_services.ts | 4 +- src/plugins/discover/public/plugin.ts | 30 +++----- 14 files changed, 84 insertions(+), 156 deletions(-) diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index 8ecd045a2450..91b75a12423f 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -3,80 +3,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useLayoutEffect, useRef, useState } from 'react'; +import React from 'react'; import { EuiPageTemplate } from '@elastic/eui'; +import { Suspense } from 'react'; import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; import { NoView } from './no_view'; import { View } from '../services/view_service/view'; -import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { DataExplorerServices } from '../types'; export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { - const [showSpinner, setShowSpinner] = useState(false); - const { - services: { store }, - } = useOpenSearchDashboards(); - const canvasRef = useRef(null); - const panelRef = useRef(null); - const unmountRef = useRef(null); - - useLayoutEffect(() => { - const unmount = () => { - if (unmountRef.current) { - unmountRef.current(); - unmountRef.current = null; - } - }; - - // Do nothing if the view is not defined or if the view is the same as the previous view - if (!view || (unmountRef.current && unmountRef.current.viewId === view.id)) { - return; - } - - // unmount the previous view - unmount(); - - const mount = async () => { - setShowSpinner(true); - try { - unmountRef.current = - (await view.mount({ - canvasElement: canvasRef.current!, - panelElement: panelRef.current!, - appParams: params, - // The provider is added to the services right after the store is created. so it is safe to assume its here. - store: store!, - })) || null; - } catch (e) { - // TODO: add error UI - // eslint-disable-next-line no-console - console.error(e); - } finally { - // if (canvasRef.current && panelRef.current) { - if (canvasRef.current) { - setShowSpinner(false); - } - } - }; - - mount(); - - return unmount; - }, [params, view, store]); - // TODO: Make this more robust. if (!view) { return ; } + const { Canvas, Panel } = view; + // Render the application DOM. // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. return ( -
+ Loading...
}> + + } className="dePageTemplate" @@ -85,8 +36,9 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa paddingSize="none" > {/* TODO: improve loading state */} - {showSpinner &&
Loading...
} -
+ Loading...
}> + +
); }; diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index eb0db4303753..0a0575e339c1 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -13,5 +13,5 @@ export function plugin() { return new DataExplorerPlugin(); } export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types'; -export { ViewMountParameters, ViewDefinition } from './services/view_service'; -export { RootState as DataExplorerRootState } from './utils/state_management'; +export { ViewProps, ViewDefinition } from './services/view_service'; +export { RootState, useTypedSelector, useTypedDispatch } from './utils/state_management'; diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index ed34f5d32479..2aa3915da468 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -4,8 +4,8 @@ */ import { Slice } from '@reduxjs/toolkit'; +import { LazyExoticComponent } from 'react'; import { AppMountParameters } from '../../../../../core/public'; -import { Store } from '../../utils/state_management'; // TODO: State management props @@ -14,12 +14,7 @@ interface ViewListItem { label: string; } -export interface ViewMountParameters { - canvasElement: HTMLDivElement; - panelElement: HTMLDivElement; - appParams: AppMountParameters; - store: any; -} +export type ViewProps = AppMountParameters; export interface ViewDefinition { readonly id: string; @@ -28,7 +23,8 @@ export interface ViewDefinition { defaults: T | (() => T) | (() => Promise); slice: Slice; }; - readonly mount: (params: ViewMountParameters) => Promise<() => void>; + readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; + readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; readonly defaultPath: string; readonly appExtentions: { savedObject: { diff --git a/src/plugins/data_explorer/public/services/view_service/view.ts b/src/plugins/data_explorer/public/services/view_service/view.ts index 1e994a21b031..6268aa731497 100644 --- a/src/plugins/data_explorer/public/services/view_service/view.ts +++ b/src/plugins/data_explorer/public/services/view_service/view.ts @@ -13,7 +13,8 @@ export class View implements IView { public readonly defaultPath: string; public readonly appExtentions: IView['appExtentions']; readonly shouldShow?: (state: any) => boolean; - readonly mount: IView['mount']; + readonly Canvas: IView['Canvas']; + readonly Panel: IView['Panel']; constructor(options: ViewDefinition) { this.id = options.id; @@ -22,6 +23,7 @@ export class View implements IView { this.defaultPath = options.defaultPath; this.appExtentions = options.appExtentions; this.shouldShow = options.shouldShow; - this.mount = options.mount; + this.Canvas = options.Canvas; + this.Panel = options.Panel; } } diff --git a/src/plugins/data_explorer/public/utils/state_management/hooks.ts b/src/plugins/data_explorer/public/utils/state_management/hooks.ts index 607fe05b1623..d4194da3702f 100644 --- a/src/plugins/data_explorer/public/utils/state_management/hooks.ts +++ b/src/plugins/data_explorer/public/utils/state_management/hooks.ts @@ -3,9 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import type { RootState, AppDispatch } from './store'; // Use throughout the app instead of plain `useDispatch` and `useSelector` export const useTypedDispatch = () => useDispatch(); -export const useTypedSelector: TypedUseSelectorHook = useSelector; +export const useTypedSelector: ( + selector: (state: TState) => TSelected, + equalityFn?: (left: TSelected, right: TSelected) => boolean +) => TSelected = useSelector; diff --git a/src/plugins/data_explorer/public/utils/state_management/preload.ts b/src/plugins/data_explorer/public/utils/state_management/preload.ts index 9cb85c3f4912..399f2d806c0a 100644 --- a/src/plugins/data_explorer/public/utils/state_management/preload.ts +++ b/src/plugins/data_explorer/public/utils/state_management/preload.ts @@ -17,7 +17,7 @@ export const getPreloadedState = async ( // initialize the default state for each view const views = services.viewRegistry.all(); - views.forEach(async (view) => { + const promises = views.map(async (view) => { if (!view.ui) { return; } @@ -31,6 +31,7 @@ export const getPreloadedState = async ( rootState[view.id] = defaults; } }); + await Promise.all(promises); return rootState; }; diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx index cfac288cd3bc..d664c5e1d6d4 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -4,11 +4,9 @@ */ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { TypedUseSelectorHook, createDispatchHook, createSelectorHook } from 'react-redux'; -import { createContext } from 'react'; import { Filter, Query } from '../../../../../data/public'; import { DiscoverServices } from '../../../build_services'; -import { DataExplorerRootState } from '../../../../../data_explorer/public'; +import { RootState } from '../../../../../data_explorer/public'; export interface DiscoverState { /** @@ -19,10 +17,6 @@ export interface DiscoverState { * Array of applied filters */ filters?: Filter[]; - /** - * id of the used index pattern - */ - index?: string; /** * Used interval of the histogram */ @@ -41,14 +35,13 @@ export interface DiscoverState { savedQuery?: string; } -export interface RootState extends DataExplorerRootState { +export interface DiscoverRootState extends RootState { discover: DiscoverState; } const initialState = {} as DiscoverState; export const getPreloadedState = async ({ data }: DiscoverServices): Promise => { - // console.log(data.query.timefilter.timefilter.getRefreshInterval().value.toString()); return { ...initialState, interval: data.query.timefilter.timefilter.getRefreshInterval().value.toString(), @@ -67,6 +60,8 @@ export const discoverSlice = createSlice({ ...state, ...action.payload, }; + + return state; }, }, }); @@ -76,12 +71,4 @@ export const setState = discoverSlice.actions.setState as (payload: T) => Pay export const updateState = discoverSlice.actions.updateState as ( payload: Partial ) => PayloadAction>; - export const { reducer } = discoverSlice; -export const contextDiscover = createContext({}); - -export const useTypedSelector: TypedUseSelectorHook = createSelectorHook( - contextDiscover -); - -export const useDispatch = createDispatchHook(contextDiscover); diff --git a/src/plugins/discover/public/application/utils/state_management/index.ts b/src/plugins/discover/public/application/utils/state_management/index.ts index a850c1690e3b..d72cc772e6c4 100644 --- a/src/plugins/discover/public/application/utils/state_management/index.ts +++ b/src/plugins/discover/public/application/utils/state_management/index.ts @@ -2,3 +2,16 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + +import { TypedUseSelectorHook } from 'react-redux'; +import { RootState, useTypedDispatch, useTypedSelector } from '../../../../../data_explorer/public'; +import { DiscoverState } from './discover_slice'; + +export * from './discover_slice'; + +export interface DiscoverRootState extends RootState { + discover: DiscoverState; +} + +export const useSelector: TypedUseSelectorHook = useTypedSelector; +export const useDispatch = useTypedDispatch; diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx index 7031c1e00f48..0246d97851ae 100644 --- a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx @@ -8,11 +8,7 @@ import { AppMountParameters } from '../../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; import { TopNav } from './top_nav'; -import { - updateState, - useDispatch, - useTypedSelector, -} from '../../utils/state_management/discover_slice'; +import { updateState, useDispatch, useSelector } from '../../utils/state_management'; interface CanvasProps { opts: { @@ -22,15 +18,13 @@ interface CanvasProps { export const Canvas = ({ opts }: CanvasProps) => { const { services } = useOpenSearchDashboards(); - const { - discover: { interval }, - } = useTypedSelector((state) => state); + const interval = useSelector((state) => state.discover.interval); const dispatch = useDispatch(); return (
- Canvas + Interval: { dispatch(updateState({ interval: e.target.value })); }} /> +

Services: {services.docLinks.DOC_LINK_VERSION}

); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 66140a696ad6..34fd6a0bf103 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -4,32 +4,21 @@ */ import React from 'react'; -import ReactDOM from 'react-dom'; -import { Provider } from 'react-redux'; -import { ViewMountParameters } from '../../../../../data_explorer/public'; +import { ViewProps } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; -import { DiscoverServices } from '../../../build_services'; import { Canvas } from './canvas'; -import { contextDiscover } from '../../utils/state_management/discover_slice'; +import { getServices } from '../../../opensearch_dashboards_services'; -export const renderCanvas = ( - { canvasElement, appParams, store }: ViewMountParameters, - services: DiscoverServices -) => { - const { setHeaderActionMenu } = appParams; - - ReactDOM.render( +// eslint-disable-next-line import/no-default-export +export default function CanvasApp({ setHeaderActionMenu }: ViewProps) { + const services = getServices(); + return ( - - - - , - canvasElement + +
); - - return () => ReactDOM.unmountComponentAtNode(canvasElement); -}; +} diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index f92e5af0bfdf..c05807d3a63a 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -4,19 +4,17 @@ */ import React from 'react'; -import ReactDOM from 'react-dom'; -import { ViewMountParameters } from '../../../../../data_explorer/public'; +import { ViewProps } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; -import { DiscoverServices } from '../../../build_services'; import { Panel } from './panel'; +import { getServices } from '../../../opensearch_dashboards_services'; -export const renderPanel = ({ panelElement }: ViewMountParameters, services: DiscoverServices) => { - ReactDOM.render( +// eslint-disable-next-line import/no-default-export +export default function PanelApp(props: ViewProps) { + const services = getServices(); + return ( - , - panelElement + ); - - return () => ReactDOM.unmountComponentAtNode(panelElement); -}; +} diff --git a/src/plugins/discover/public/application/view_components/panel/panel.tsx b/src/plugins/discover/public/application/view_components/panel/panel.tsx index fe3b36f6e87b..fda7f8a44318 100644 --- a/src/plugins/discover/public/application/view_components/panel/panel.tsx +++ b/src/plugins/discover/public/application/view_components/panel/panel.tsx @@ -4,7 +4,9 @@ */ import React from 'react'; +import { useSelector } from '../../utils/state_management'; export const Panel = () => { - return
Side Panel
; + const interval = useSelector((state) => state.discover.interval); + return
{interval}
; }; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 0d7fa433cee1..a2b70f5c5099 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -83,11 +83,11 @@ export interface DiscoverServices { visualizations: VisualizationsStart; } -export async function buildServices( +export function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, context: PluginInitializerContext -): Promise { +): DiscoverServices { const services: SavedObjectOpenSearchDashboardsServices = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 590d95c13c67..3fd5acfa7404 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -30,6 +30,7 @@ import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { stringify } from 'query-string'; import rison from 'rison-node'; +import { lazy } from 'react'; import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; @@ -163,7 +164,7 @@ export class DiscoverPlugin private stopUrlTracking: (() => void) | undefined = undefined; private servicesInitialized: boolean = false; private urlGenerator?: DiscoverStart['urlGenerator']; - private initializeServices?: () => Promise<{ core: CoreStart; plugins: DiscoverStartPlugins }>; + private initializeServices?: () => { core: CoreStart; plugins: DiscoverStartPlugins }; setup(core: CoreSetup, plugins: DiscoverSetupPlugins) { // TODO: Remove this before merge to main @@ -323,9 +324,6 @@ export class DiscoverPlugin }); } - // TODO: Carry this over to the view - // make sure the index pattern list is up to date - // await dataStart.indexPatterns.clearCache(); return () => { appUnMounted(); }; @@ -374,26 +372,16 @@ export class DiscoverPlugin }, ui: { defaults: async () => { - await this.initializeServices?.(); + this.initializeServices?.(); const services = getServices(); return await getPreloadedState(services); }, slice: discoverSlice, }, shouldShow: () => true, - mount: async (params) => { - // TODO: Remove this before merge to main - // eslint-disable-next-line no-console - console.log('DiscoverPlugin.dataExplorer.mount()'); - const { renderCanvas, renderPanel } = await import('./application/view_components'); - const [coreStart, pluginsStart] = await core.getStartServices(); - const services = await buildServices(coreStart, pluginsStart, this.initializerContext); - - renderCanvas(params, services); - renderPanel(params, services); - - return () => {}; - }, + // ViewCompon + Canvas: lazy(() => import('./application/view_components/canvas')), + Panel: lazy(() => import('./application/view_components/panel')), }); // this.registerEmbeddable(core, plugins); @@ -414,17 +402,19 @@ export class DiscoverPlugin console.log('DiscoverPlugin.start()'); setUiActions(plugins.uiActions); - this.initializeServices = async () => { + this.initializeServices = () => { if (this.servicesInitialized) { return { core, plugins }; } - const services = await buildServices(core, plugins, this.initializerContext); + const services = buildServices(core, plugins, this.initializerContext); setServices(services); this.servicesInitialized = true; return { core, plugins }; }; + this.initializeServices(); + return { urlGenerator: this.urlGenerator, savedSearchLoader: createSavedSearchesLoader({ From 031fc07d04003cb8440b5e6ed1691a0bd9fb3ff4 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 17 Jul 2023 09:16:09 +0000 Subject: [PATCH 06/16] Adds global state persistence to data explorer Signed-off-by: Ashwin P Chandran --- .../public/components/sidebar/index.tsx | 25 +++++----- src/plugins/data_explorer/public/plugin.ts | 46 ++++++++++++++++++- src/plugins/data_explorer/public/types.ts | 8 ++-- .../utils/state_management/metadata_slice.ts | 14 ++++-- .../public/utils/use/use_view.ts | 23 +++++++--- 5 files changed, 85 insertions(+), 31 deletions(-) diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index 3900d2bd9467..f58bc776982b 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -5,17 +5,12 @@ import React, { useMemo, FC, useEffect, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { - EuiPanel, - EuiComboBox, - EuiSelect, - EuiSelectOption, - EuiComboBoxOptionOption, -} from '@elastic/eui'; +import { EuiPanel, EuiComboBox, EuiSelect, EuiComboBoxOptionOption } from '@elastic/eui'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { useView } from '../../utils/use'; import { DataExplorerServices } from '../../types'; import { useTypedDispatch, useTypedSelector, setIndexPattern } from '../../utils/state_management'; +import { setView } from '../../utils/state_management/metadata_slice'; export const Sidebar: FC = ({ children }) => { const { indexPattern: indexPatternId } = useTypedSelector((state) => state.metadata); @@ -24,7 +19,7 @@ export const Sidebar: FC = ({ children }) => { const [selectedOption, setSelectedOption] = useState>(); const { view, viewRegistry } = useView(); const views = viewRegistry.all(); - const viewOptions: EuiSelectOption[] = useMemo( + const viewOptions = useMemo( () => views.map(({ id, title }) => ({ value: id, @@ -87,14 +82,16 @@ export const Sidebar: FC = ({ children }) => { return; } - dispatch( - setIndexPattern({ - state: value, - }) - ); + dispatch(setIndexPattern(value)); + }} + /> + { + dispatch(setView(e.target.value)); }} /> - {children} diff --git a/src/plugins/data_explorer/public/plugin.ts b/src/plugins/data_explorer/public/plugin.ts index a78d16246943..5935ceb44d80 100644 --- a/src/plugins/data_explorer/public/plugin.ts +++ b/src/plugins/data_explorer/public/plugin.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { BehaviorSubject } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; import { AppMountParameters, CoreSetup, @@ -10,6 +12,7 @@ import { Plugin, AppNavLinkStatus, ScopedHistory, + AppUpdater, } from '../../../core/public'; import { DataExplorerPluginSetup, @@ -22,9 +25,11 @@ import { PLUGIN_ID, PLUGIN_NAME } from '../common'; import { ViewService } from './services/view_service'; import { createOsdUrlStateStorage, + createOsdUrlTracker, withNotifyOnErrors, } from '../../opensearch_dashboards_utils/public'; import { getPreloadedStore } from './utils/state_management'; +import { opensearchFilters } from '../../data/public'; export class DataExplorerPlugin implements @@ -35,16 +40,47 @@ export class DataExplorerPlugin DataExplorerPluginStartDependencies > { private viewService = new ViewService(); + private appStateUpdater = new BehaviorSubject(() => ({})); + private stopUrlTracking?: () => void; private currentHistory?: ScopedHistory; public setup( - core: CoreSetup + core: CoreSetup, + { data }: DataExplorerPluginSetupDependencies ): DataExplorerPluginSetup { const viewService = this.viewService; // TODO: Remove this before merge to main // eslint-disable-next-line no-console console.log('data_explorer: Setup'); + const { appMounted, appUnMounted, stop: stopUrlTracker } = createOsdUrlTracker({ + baseUrl: core.http.basePath.prepend(`/app/${PLUGIN_ID}`), + defaultSubUrl: '#/', + storageKey: `lastUrl:${core.http.basePath.get()}:${PLUGIN_ID}`, + navLinkUpdater$: this.appStateUpdater, + toastNotifications: core.notifications.toasts, + stateParams: [ + { + osdUrlKey: '_g', + stateUpdate$: data.query.state$.pipe( + filter( + ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) + ), + map(({ state }) => ({ + ...state, + filters: state.filters?.filter(opensearchFilters.isFilterPinned), + })) + ), + }, + ], + getHistory: () => { + return this.currentHistory!; + }, + }); + this.stopUrlTracking = () => { + stopUrlTracker(); + }; + // Register an application into the side navigation menu core.application.register({ id: PLUGIN_ID, @@ -83,9 +119,11 @@ export class DataExplorerPlugin services.store = store; const unmount = renderApp(coreStart, services, params, store); + appMounted(); return () => { unsubscribeStore(); + appUnMounted(); unmount(); }; }, @@ -103,5 +141,9 @@ export class DataExplorerPlugin return {}; } - public stop() {} + public stop() { + if (this.stopUrlTracking) { + this.stopUrlTracking(); + } + } } diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts index b99ca9640b0e..1c7b21c191dc 100644 --- a/src/plugins/data_explorer/public/types.ts +++ b/src/plugins/data_explorer/public/types.ts @@ -8,7 +8,7 @@ import { EmbeddableStart } from '../../embeddable/public'; import { ExpressionsStart } from '../../expressions/public'; import { ViewServiceStart, ViewServiceSetup } from './services/view_service'; import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; -import { DataPublicPluginStart } from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { Store } from './utils/state_management'; export type DataExplorerPluginSetup = ViewServiceSetup; @@ -16,8 +16,10 @@ export type DataExplorerPluginSetup = ViewServiceSetup; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DataExplorerPluginStart {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataExplorerPluginSetupDependencies {} +export interface DataExplorerPluginSetupDependencies { + data: DataPublicPluginSetup; +} + export interface DataExplorerPluginStartDependencies { expressions: ExpressionsStart; embeddable: EmbeddableStart; diff --git a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts index 2b1ee7d43759..e9fe84713120 100644 --- a/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts +++ b/src/plugins/data_explorer/public/utils/state_management/metadata_slice.ts @@ -9,6 +9,7 @@ import { DataExplorerServices } from '../../types'; export interface MetadataState { indexPattern?: string; originatingApp?: string; + view?: string; } const initialState: MetadataState = {}; @@ -36,11 +37,14 @@ export const slice = createSlice({ name: 'metadata', initialState, reducers: { - setIndexPattern: (state, action: PayloadAction<{ state?: string }>) => { - state.indexPattern = action.payload.state; + setIndexPattern: (state, action: PayloadAction) => { + state.indexPattern = action.payload; }, - setOriginatingApp: (state, action: PayloadAction<{ state?: string }>) => { - state.originatingApp = action.payload.state; + setOriginatingApp: (state, action: PayloadAction) => { + state.originatingApp = action.payload; + }, + setView: (state, action: PayloadAction) => { + state.view = action.payload; }, setState: (_state, action: PayloadAction) => { return action.payload; @@ -49,4 +53,4 @@ export const slice = createSlice({ }); export const { reducer } = slice; -export const { setIndexPattern, setOriginatingApp, setState } = slice.actions; +export const { setIndexPattern, setOriginatingApp, setView, setState } = slice.actions; diff --git a/src/plugins/data_explorer/public/utils/use/use_view.ts b/src/plugins/data_explorer/public/utils/use/use_view.ts index edda514a113e..10f67c08907d 100644 --- a/src/plugins/data_explorer/public/utils/use/use_view.ts +++ b/src/plugins/data_explorer/public/utils/use/use_view.ts @@ -3,24 +3,33 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useEffect, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { View } from '../../services/view_service/view'; import { DataExplorerServices } from '../../types'; +import { useTypedDispatch, useTypedSelector } from '../state_management'; +import { setView } from '../state_management/metadata_slice'; export const useView = () => { - // TODO: Move the view to the redux store once the store is ready - const [view, setView] = useState(); - const { appId } = useParams<{ appId: string }>(); + const viewId = useTypedSelector((state) => state.metadata.view); const { services: { viewRegistry }, } = useOpenSearchDashboards(); + const dispatch = useTypedDispatch(); + const { appId } = useParams<{ appId: string }>(); + + const view = useMemo(() => { + if (!viewId) return undefined; + return viewRegistry.get(viewId); + }, [viewId, viewRegistry]); useEffect(() => { const currentView = viewRegistry.get(appId); - setView(currentView); - }, [appId, viewRegistry]); + + if (!currentView) return; + + dispatch(setView(currentView?.id)); + }, [appId, dispatch, viewRegistry]); return { view, viewRegistry }; }; From 12e42c472616328766726d243392bab58601670a Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Wed, 19 Jul 2023 08:06:11 +0000 Subject: [PATCH 07/16] initial dnd --- .github/workflows/build_and_test_workflow.yml | 4 +- .github/workflows/cypress_workflow.yml | 4 +- .../sidebar/change_indexpattern.tsx | 131 ------ .../sidebar/discover_index_pattern.test.tsx | 111 ----- .../sidebar/discover_index_pattern.tsx | 104 ----- .../sidebar/discover_index_pattern_title.tsx | 95 ----- .../components/sidebar/discover_sidebar.tsx | 379 ++++++++++-------- .../components/sidebar/lib/group_fields.tsx | 7 + .../utils/state_management/discover_slice.tsx | 56 ++- .../view_components/panel/panel.tsx | 66 ++- 10 files changed, 338 insertions(+), 619 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx delete mode 100644 src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx delete mode 100644 src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx delete mode 100644 src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx diff --git a/.github/workflows/build_and_test_workflow.yml b/.github/workflows/build_and_test_workflow.yml index 8fd3a402d547..27fbbb2c4e63 100644 --- a/.github/workflows/build_and_test_workflow.yml +++ b/.github/workflows/build_and_test_workflow.yml @@ -3,10 +3,10 @@ name: Build and test -# trigger on every commit push and PR for all branches except pushes for backport branches +# trigger on every commit push and PR for all branches except pushes for backport branches and feature branches on: push: - branches: ['**', '!backport/**'] + branches: ['**', '!backport/**', '!feature/**'] paths-ignore: - '**/*.md' - 'docs/**' diff --git a/.github/workflows/cypress_workflow.yml b/.github/workflows/cypress_workflow.yml index 5e78785f9b88..1c15ad3f18ed 100644 --- a/.github/workflows/cypress_workflow.yml +++ b/.github/workflows/cypress_workflow.yml @@ -1,9 +1,9 @@ name: Run cypress tests -# trigger on every PR for all branches +# trigger on every PR for all branches except feature branches on: pull_request: - branches: [ '**' ] + branches: [ '**', '!feature/**' ] paths-ignore: - '**/*.md' diff --git a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx b/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx deleted file mode 100644 index 553031f06721..000000000000 --- a/src/plugins/discover/public/application/components/sidebar/change_indexpattern.tsx +++ /dev/null @@ -1,131 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@osd/i18n'; -import React, { useState } from 'react'; -import { - EuiButtonEmpty, - EuiPopover, - EuiPopoverTitle, - EuiSelectable, - EuiButtonEmptyProps, -} from '@elastic/eui'; -import { EuiSelectableProps } from '@elastic/eui/src/components/selectable/selectable'; -import { IndexPatternRef } from './types'; - -export type ChangeIndexPatternTriggerProps = EuiButtonEmptyProps & { - label: string; - title?: string; -}; - -export function ChangeIndexPattern({ - indexPatternRefs, - indexPatternId, - onChangeIndexPattern, - trigger, - selectableProps, -}: { - trigger: ChangeIndexPatternTriggerProps; - indexPatternRefs: IndexPatternRef[]; - onChangeIndexPattern: (newId: string) => void; - indexPatternId?: string; - selectableProps?: EuiSelectableProps; -}) { - const [isPopoverOpen, setPopoverIsOpen] = useState(false); - - const createTrigger = function () { - const { label, title, ...rest } = trigger; - return ( - setPopoverIsOpen(!isPopoverOpen)} - {...rest} - > - {label} - - ); - }; - - return ( - setPopoverIsOpen(false)} - className="eui-textTruncate" - anchorClassName="eui-textTruncate" - display="block" - panelPaddingSize="s" - ownFocus - > -
- - {i18n.translate('discover.fieldChooser.indexPattern.changeIndexPatternTitle', { - defaultMessage: 'Change index pattern', - })} - - ({ - label: title, - key: id, - value: id, - checked: id === indexPatternId ? 'on' : undefined, - }))} - onChange={(choices) => { - const choice = (choices.find(({ checked }) => checked) as unknown) as { - value: string; - }; - onChangeIndexPattern(choice.value); - setPopoverIsOpen(false); - }} - searchProps={{ - compressed: true, - ...(selectableProps ? selectableProps.searchProps : undefined), - }} - > - {(list, search) => ( - <> - {search} - {list} - - )} - -
-
- ); -} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx deleted file mode 100644 index 9298aef92cf0..000000000000 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; - -// @ts-ignore -import { ShallowWrapper } from 'enzyme'; -import { ChangeIndexPattern } from './change_indexpattern'; -import { SavedObject } from 'opensearch-dashboards/server'; -import { DiscoverIndexPattern } from './discover_index_pattern'; -import { EuiSelectable } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/public'; - -const indexPattern = { - id: 'test1', - title: 'test1 title', -} as IIndexPattern; - -const indexPattern1 = { - id: 'test1', - attributes: { - title: 'test1 titleToDisplay', - }, -} as SavedObject; - -const indexPattern2 = { - id: 'test2', - attributes: { - title: 'test2 titleToDisplay', - }, -} as SavedObject; - -const defaultProps = { - indexPatternList: [indexPattern1, indexPattern2], - selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(async () => {}), -}; - -function getIndexPatternPickerList(instance: ShallowWrapper) { - return instance.find(ChangeIndexPattern).first().dive().find(EuiSelectable); -} - -function getIndexPatternPickerOptions(instance: ShallowWrapper) { - return getIndexPatternPickerList(instance).prop('options'); -} - -function selectIndexPatternPickerOption(instance: ShallowWrapper, selectedLabel: string) { - const options: Array<{ label: string; checked?: 'on' | 'off' }> = getIndexPatternPickerOptions( - instance - ).map((option: any) => - option.label === selectedLabel - ? { ...option, checked: 'on' } - : { ...option, checked: undefined } - ); - return getIndexPatternPickerList(instance).prop('onChange')!(options); -} - -describe('DiscoverIndexPattern', () => { - test('Invalid props dont cause an exception', () => { - const props = { - indexPatternList: null, - selectedIndexPattern: null, - setIndexPattern: jest.fn(), - } as any; - - expect(shallow()).toMatchSnapshot(`""`); - }); - test('should list all index patterns', () => { - const instance = shallow(); - - expect(getIndexPatternPickerOptions(instance)!.map((option: any) => option.label)).toEqual([ - 'test1 titleToDisplay', - 'test2 titleToDisplay', - ]); - }); - - test('should switch data panel to target index pattern', () => { - const instance = shallow(); - - selectIndexPatternPickerOption(instance, 'test2 titleToDisplay'); - expect(defaultProps.setIndexPattern).toHaveBeenCalledWith('test2'); - }); -}); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx deleted file mode 100644 index 95154bec1939..000000000000 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React, { useState, useEffect } from 'react'; -import { SavedObject } from 'opensearch-dashboards/public'; -import { IIndexPattern, IndexPatternAttributes } from 'src/plugins/data/public'; -import { I18nProvider } from '@osd/i18n/react'; - -import { IndexPatternRef } from './types'; -import { ChangeIndexPattern } from './change_indexpattern'; -export interface DiscoverIndexPatternProps { - /** - * list of available index patterns, if length > 1, component offers a "change" link - */ - indexPatternList: Array>; - /** - * currently selected index pattern, due to angular issues it's undefined at first rendering - */ - selectedIndexPattern: IIndexPattern; - /** - * triggered when user selects a new index pattern - */ - setIndexPattern: (id: string) => void; -} - -/** - * Component allows you to select an index pattern in discovers side bar - */ -export function DiscoverIndexPattern({ - indexPatternList, - selectedIndexPattern, - setIndexPattern, -}: DiscoverIndexPatternProps) { - const options: IndexPatternRef[] = (indexPatternList || []).map((entity) => ({ - id: entity.id, - title: entity.attributes!.title, - })); - const { id: selectedId, title: selectedTitle } = selectedIndexPattern || {}; - - const [selected, setSelected] = useState({ - id: selectedId, - title: selectedTitle || '', - }); - useEffect(() => { - const { id, title } = selectedIndexPattern; - const indexPattern = indexPatternList.find((pattern) => pattern.id === id); - const titleToDisplay = indexPattern ? indexPattern.attributes!.title : title; - setSelected({ id, title: titleToDisplay }); - }, [indexPatternList, selectedIndexPattern]); - if (!selectedId) { - return null; - } - - return ( -
- - { - const indexPattern = options.find((pattern) => pattern.id === id); - if (indexPattern) { - setIndexPattern(id); - setSelected(indexPattern); - } - }} - /> - -
- ); -} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx b/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx deleted file mode 100644 index 30b50a9006c8..000000000000 --- a/src/plugins/discover/public/application/components/sidebar/discover_index_pattern_title.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { EuiToolTip, EuiFlexItem, EuiFlexGroup, EuiTitle, EuiButtonEmpty } from '@elastic/eui'; - -import { FormattedMessage } from '@osd/i18n/react'; -import { i18n } from '@osd/i18n'; -export interface DiscoverIndexPatternTitleProps { - /** - * determines whether the change link is displayed - */ - isChangeable: boolean; - /** - * function triggered when the change link is clicked - */ - onChange: () => void; - /** - * title of the current index pattern - */ - title: string; -} - -/** - * Component displaying the title of the current selected index pattern - * and if changeable is true, a link is provided to change the index pattern - */ -export function DiscoverIndexPatternTitle({ - isChangeable, - onChange, - title, -}: DiscoverIndexPatternTitleProps) { - return ( - - - - -

{title}

-
-
-
- {isChangeable && ( - - - } - > - onChange()} - iconSide="right" - iconType="arrowDown" - color="text" - /> - - - )} -
- ); -} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 865aff590286..320e8ccec93e 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -31,14 +31,19 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@osd/i18n'; -import { EuiButtonIcon, EuiTitle, EuiSpacer } from '@elastic/eui'; -import { sortBy } from 'lodash'; +import { + EuiButtonIcon, + EuiTitle, + EuiSpacer, + EuiDragDropContext, + DropResult, + EuiDroppable, + EuiDraggable, + EuiPanel, +} from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { DiscoverField } from './discover_field'; -import { DiscoverIndexPattern } from './discover_index_pattern'; import { DiscoverFieldSearch } from './discover_field_search'; -import { IndexPatternAttributes } from '../../../../../data/common'; -import { SavedObject } from '../../../../../../core/types'; import { FIELDS_LIMIT_SETTING } from '../../../../common'; import { groupFields } from './lib/group_fields'; import { IndexPatternField, IndexPattern, UI_SETTINGS } from '../../../../../data/public'; @@ -61,13 +66,13 @@ export interface DiscoverSidebarProps { */ hits: Array>; /** - * List of available index patterns + * Callback function when selecting a field */ - indexPatternList: Array>; + onAddField: (fieldName: string, index?: number) => void; /** - * Callback function when selecting a field + * Callback function when rearranging fields */ - onAddField: (fieldName: string) => void; + onReorderFields: (sourceIdx: number, destinationIdx: number) => void; /** * Callback function when adding a filter from sidebar */ @@ -81,22 +86,17 @@ export interface DiscoverSidebarProps { * Currently selected index pattern */ selectedIndexPattern?: IndexPattern; - /** - * Callback function to select another index pattern - */ - setIndexPattern: (id: string) => void; } export function DiscoverSidebar({ columns, fieldCounts, hits, - indexPatternList, onAddField, onAddFilter, onRemoveField, + onReorderFields, selectedIndexPattern, - setIndexPattern, }: DiscoverSidebarProps) { const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState(null); @@ -148,6 +148,36 @@ export function DiscoverSidebar({ return result; }, [fields]); + const onDragEnd = useCallback( + ({ source, destination }: DropResult) => { + if (!source || !destination || !fields) return; + + // Rearranging fields within the selected fields list + if ( + source.droppableId === 'SELECTED_FIELDS' && + destination.droppableId === 'SELECTED_FIELDS' + ) { + onReorderFields(source.index, destination.index); + return; + } + // Dropping fields into the selected fields list + if ( + source.droppableId !== 'SELECTED_FIELDS' && + destination.droppableId === 'SELECTED_FIELDS' + ) { + const fieldListMap = { + POPULAR_FIELDS: popularFields, + UNPOPULAR_FIELDS: unpopularFields, + }; + const fieldList = fieldListMap[source.droppableId as keyof typeof fieldListMap]; + const field = fieldList[source.index]; + onAddField(field.name, destination.index); + return; + } + }, + [fields, onAddField, onReorderFields, popularFields, unpopularFields] + ); + if (!selectedIndexPattern || !fields) { return null; } @@ -160,166 +190,187 @@ export function DiscoverSidebar({ defaultMessage: 'Index and fields', })} > - o.attributes.title)} - /> -
-
- - -
-
- {fields.length > 0 && ( - <> - -

- -

-
- -
    - {selectedFields.map((field: IndexPatternField) => { - return ( -
  • - +
    +
    + + +
    +
    + {fields.length > 0 && ( + + <> + +

    + -

  • - ); - })} -
-
- -

+

+
+ +
    + {selectedFields.map((field: IndexPatternField, index) => { + return ( + + + + + + ); + })} +
+
+ +

+ +

+
+
+ setShowFields(!showFields)} + aria-label={ + showFields + ? i18n.translate( + 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', + { + defaultMessage: 'Hide fields', + } + ) + : i18n.translate( + 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', + { + defaultMessage: 'Show fields', + } + ) + } + /> +
+
+ + + )} + + {popularFields.length > 0 && ( +
+ + + -
- setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> -
+
    + {popularFields.map((field: IndexPatternField) => { + return ( +
  • + +
  • + ); + })} +
- - )} - {popularFields.length > 0 && ( -
- - - -
    + - {popularFields.map((field: IndexPatternField) => { + {unpopularFields.map((field: IndexPatternField, index) => { return ( -
  • - -
  • + + + + ); })} -
-
- )} - -
    - {unpopularFields.map((field: IndexPatternField) => { - return ( -
  • - -
  • - ); - })} -
-
+ + +
+ ); diff --git a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx index fad1db402467..dff60827ccd2 100644 --- a/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx +++ b/src/plugins/discover/public/application/components/sidebar/lib/group_fields.tsx @@ -83,5 +83,12 @@ export function groupFields( } } + // sort the selected fields by the column order + result.selected.sort((a, b) => { + const aIndex = columns.indexOf(a.name); + const bIndex = columns.indexOf(b.name); + return aIndex - bIndex; + }); + return result; } diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx index d664c5e1d6d4..564ac38f7ed4 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -52,10 +52,51 @@ export const discoverSlice = createSlice({ name: 'discover', initialState, reducers: { - setState(state: T, action: PayloadAction) { + setState(state, action: PayloadAction) { return action.payload; }, - updateState(state: T, action: PayloadAction>) { + addColumn( + state, + action: PayloadAction<{ + column: string; + index?: number; + }> + ) { + const { column, index } = action.payload; + const columns = [...(state.columns || [])]; + if (index !== undefined) { + columns.splice(index, 0, column); + } else { + columns.push(column); + } + state = { + ...state, + columns, + }; + + return state; + }, + removeColumn(state, action: PayloadAction) { + state = { + ...state, + columns: (state.columns || []).filter((column) => column !== action.payload), + }; + + return state; + }, + reorderColumn(state, action: PayloadAction<{ source: number; destination: number }>) { + const { source, destination } = action.payload; + const columns = [...(state.columns || [])]; + const [removed] = columns.splice(source, 1); + columns.splice(destination, 0, removed); + state = { + ...state, + columns, + }; + + return state; + }, + updateState(state, action: PayloadAction>) { state = { ...state, ...action.payload, @@ -67,8 +108,11 @@ export const discoverSlice = createSlice({ }); // Exposing the state functions as generics -export const setState = discoverSlice.actions.setState as (payload: T) => PayloadAction; -export const updateState = discoverSlice.actions.updateState as ( - payload: Partial -) => PayloadAction>; +export const { + addColumn, + removeColumn, + reorderColumn, + setState, + updateState, +} = discoverSlice.actions; export const { reducer } = discoverSlice; diff --git a/src/plugins/discover/public/application/view_components/panel/panel.tsx b/src/plugins/discover/public/application/view_components/panel/panel.tsx index fda7f8a44318..a7c225588679 100644 --- a/src/plugins/discover/public/application/view_components/panel/panel.tsx +++ b/src/plugins/discover/public/application/view_components/panel/panel.tsx @@ -3,10 +3,68 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; -import { useSelector } from '../../utils/state_management'; +import React, { useEffect, useState } from 'react'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { + addColumn, + removeColumn, + reorderColumn, + useDispatch, + useSelector, +} from '../../utils/state_management'; +import { DiscoverServices } from '../../../build_services'; +import { DiscoverSidebar } from '../../components/sidebar'; +import { IndexPattern } from '../../../opensearch_dashboards_services'; export const Panel = () => { - const interval = useSelector((state) => state.discover.interval); - return
{interval}
; + const { indexPatternId, columns } = useSelector((state) => ({ + columns: state.discover.columns, + indexPatternId: state.metadata.indexPattern, + })); + const dispatch = useDispatch(); + const [indexPattern, setIndexPattern] = useState(); + + const { services } = useOpenSearchDashboards(); + + useEffect(() => { + const fetchIndexPattern = async () => { + const currentIndexPattern = await services.data.indexPatterns.get(indexPatternId || ''); + setIndexPattern(currentIndexPattern); + }; + fetchIndexPattern(); + }, [indexPatternId, services.data.indexPatterns]); + + return ( + ({ ...acc, [field.name]: 1 }), + {} as Record + ) || {} + } + hits={[]} + onAddField={(fieldName, index) => { + dispatch( + addColumn({ + column: fieldName, + index, + }) + ); + }} + onRemoveField={(fieldName) => { + dispatch(removeColumn(fieldName)); + }} + onReorderFields={(source, destination) => { + dispatch( + reorderColumn({ + source, + destination, + }) + ); + }} + selectedIndexPattern={indexPattern} + onAddFilter={() => {}} + /> + ); }; From 6ef7c8ae2199e509185ad7bd237ab25b445f6f21 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 20 Jul 2023 11:41:08 +0000 Subject: [PATCH 08/16] basic side panel Signed-off-by: Ashwin P Chandran --- .../public/components/app_container.scss | 5 + .../public/components/app_container.tsx | 4 + .../public/components/sidebar/index.tsx | 5 +- .../public/utils/state_management/store.ts | 23 +- .../components/sidebar/discover_field.tsx | 123 +++---- .../sidebar/discover_field_search.tsx | 5 +- .../components/sidebar/discover_sidebar.scss | 70 +--- .../components/sidebar/discover_sidebar.tsx | 301 ++++++++---------- 8 files changed, 237 insertions(+), 299 deletions(-) create mode 100644 src/plugins/data_explorer/public/components/app_container.scss diff --git a/src/plugins/data_explorer/public/components/app_container.scss b/src/plugins/data_explorer/public/components/app_container.scss new file mode 100644 index 000000000000..3a3d856e3bdf --- /dev/null +++ b/src/plugins/data_explorer/public/components/app_container.scss @@ -0,0 +1,5 @@ +// Needed to allow the sidebar to scroll +.deSidebar { + display: flex; + flex-direction: column; +} diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index 91b75a12423f..b8cdee7e78b0 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -10,6 +10,7 @@ import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; import { NoView } from './no_view'; import { View } from '../services/view_service/view'; +import './app_container.scss'; export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { // TODO: Make this more robust. @@ -30,6 +31,9 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa } + pageSideBarProps={{ + className: 'deSidebar', + }} className="dePageTemplate" template="default" restrictWidth={false} diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index f58bc776982b..42d1df70c1e4 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -5,7 +5,7 @@ import React, { useMemo, FC, useEffect, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { EuiPanel, EuiComboBox, EuiSelect, EuiComboBoxOptionOption } from '@elastic/eui'; +import { EuiPanel, EuiComboBox, EuiSelect, EuiComboBoxOptionOption, EuiSpacer } from '@elastic/eui'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { useView } from '../../utils/use'; import { DataExplorerServices } from '../../types'; @@ -58,7 +58,7 @@ export const Sidebar: FC = ({ children }) => { return ( <> - + { dispatch(setIndexPattern(value)); }} /> + ; +const commonReducers = { + metadata: metadataReducer, +}; + +let dynamicReducers: { [key: string]: Reducer; } = { - metadata: metadataReducer, + ...commonReducers, }; const rootReducer = combineReducers(dynamicReducers); @@ -60,7 +61,15 @@ export const getPreloadedStore = async (services: DataExplorerServices) => { // the store subscriber will automatically detect changes and call handleChange function const unsubscribe = store.subscribe(handleChange); - return { store, unsubscribe }; + const onUnsubscribe = () => { + dynamicReducers = { + ...commonReducers, + }; + + unsubscribe(); + }; + + return { store, unsubscribe: onUnsubscribe }; }; export const registerSlice = (slice: Slice) => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index e807267435eb..f14da1912b3f 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -29,7 +29,15 @@ */ import React, { useState } from 'react'; -import { EuiPopover, EuiPopoverTitle, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { + EuiPopover, + EuiPopoverTitle, + EuiButtonIcon, + EuiToolTip, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { DiscoverFieldDetails } from './discover_field_details'; import { FieldIcon, FieldButton } from '../../../../../opensearch_dashboards_react/public'; @@ -79,17 +87,17 @@ export interface DiscoverFieldProps { useShortDots?: boolean; } -export function DiscoverField({ - columns, +export const DiscoverField = ({ field, - indexPattern, + selected, onAddField, onRemoveField, + columns, + indexPattern, onAddFilter, getDetails, - selected, useShortDots, -}: DiscoverFieldProps) { +}: DiscoverFieldProps) => { const addLabelAria = i18n.translate('discover.fieldChooser.discoverField.addButtonAriaLabel', { defaultMessage: 'Add {field} to table', values: { field: field.name }, @@ -112,10 +120,6 @@ export function DiscoverField({ } }; - function togglePopover() { - setOpen(!infoIsOpen); - } - function wrapOnDot(str?: string) { // u200B is a non-width white-space character, which allows // the browser to efficiently word-wrap right after the dot @@ -123,15 +127,11 @@ export function DiscoverField({ return str ? str.replace(/\./g, '.\u200B') : ''; } - const dscFieldIcon = ( - - ); - const fieldName = ( {useShortDots ? wrapOnDot(shortenDottedString(field.name)) : wrapOnDot(field.displayName)} @@ -190,12 +190,19 @@ export function DiscoverField({ } if (field.type === '_source') { + // TODO: This is not the correct implementation of the source field details return ( + } fieldAction={actionButton} fieldName={fieldName} /> @@ -203,43 +210,51 @@ export function DiscoverField({ } return ( - { - togglePopover(); - }} - dataTestSubj={`field-${field.name}-showDetails`} - fieldIcon={dscFieldIcon} - fieldAction={actionButton} - fieldName={fieldName} + + + - } - isOpen={infoIsOpen} - closePopover={() => setOpen(false)} - anchorPosition="rightUp" - panelClassName="dscSidebarItem__fieldPopoverPanel" - > - - {' '} - {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} - - {infoIsOpen && ( - - )} - + + + {fieldName} + + + setOpen(false)} + anchorPosition="rightUp" + button={ + setOpen((state) => !state)} + /> + } + panelClassName="dscSidebarItem__fieldPopoverPanel" + > + + {' '} + {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} + + {infoIsOpen && ( + + )} + + + {actionButton} + ); -} +}; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 4a1390cb1955..30bc4e00544d 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -46,6 +46,7 @@ import { EuiFormRow, EuiButtonGroup, EuiOutsideClickDetector, + EuiPanel, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; @@ -261,7 +262,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { ); return ( - + - + ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index 9c80e0afa600..8487630e5013 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -1,79 +1,17 @@ -.dscSidebar__container { - padding-left: 0 !important; - padding-right: 0 !important; - background-color: transparent; - border-right-color: transparent; - border-bottom-color: transparent; -} - -.dscIndexPattern__container { +// Needed for scrollable sidebar +.sidebar-list { display: flex; - align-items: center; - height: $euiSize * 3; - margin-top: -$euiSizeS; -} - -.dscIndexPattern__triggerButton { - @include euiTitle("xs"); - - line-height: $euiSizeXXL; -} - -.dscFieldList { - list-style: none; - margin-bottom: 0; + flex-direction: column; } .dscFieldListHeader { - padding: $euiSizeS $euiSizeS 0 $euiSizeS; - background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); + padding-left: $euiSizeS; } .dscFieldList--popular { background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); } -.dscFieldChooser { - padding-left: $euiSize; -} - -.dscFieldChooser__toggle { - color: $euiColorMediumShade; - margin-left: $euiSizeS !important; -} - -.dscSidebarItem { - &:hover, - &:focus-within, - &[class*="-isActive"] { - .dscSidebarItem__action { - opacity: 1; - } - } -} - -/** - * 1. Only visually hide the action, so that it's still accessible to screen readers. - * 2. When tabbed to, this element needs to be visible for keyboard accessibility. - */ -.dscSidebarItem__action { - opacity: 0; /* 1 */ - transition: none; - - &:focus { - opacity: 1; /* 2 */ - } - - font-size: $euiFontSizeXS; - padding: 2px 6px !important; - height: 22px !important; - min-width: auto !important; - - .euiButton__content { - padding: 0 4px; - } -} - .dscFieldSearch { padding: $euiSizeS; } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 320e8ccec93e..0554ed1c7344 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -32,9 +32,7 @@ import './discover_sidebar.scss'; import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { i18n } from '@osd/i18n'; import { - EuiButtonIcon, EuiTitle, - EuiSpacer, EuiDragDropContext, DropResult, EuiDroppable, @@ -98,7 +96,6 @@ export function DiscoverSidebar({ onReorderFields, selectedIndexPattern, }: DiscoverSidebarProps) { - const [showFields, setShowFields] = useState(false); const [fields, setFields] = useState(null); const [fieldFilterState, setFieldFilterState] = useState(getDefaultFieldFilter()); const services = useMemo(() => getServices(), []); @@ -185,190 +182,158 @@ export function DiscoverSidebar({ return (
-
-
- - -
-
+
+ + +
{fields.length > 0 && ( - - <> - -

- -

-
- -
    - {selectedFields.map((field: IndexPatternField, index) => { - return ( - - - - - - ); - })} -
-
- -

- -

-
-
- setShowFields(!showFields)} - aria-label={ - showFields - ? i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionHideAriaLabel', - { - defaultMessage: 'Hide fields', - } - ) - : i18n.translate( - 'discover.fieldChooser.filter.indexAndFieldsSectionShowAriaLabel', - { - defaultMessage: 'Show fields', - } - ) - } - /> -
-
- -
- )} - - {popularFields.length > 0 && ( -
- - -
    - {popularFields.map((field: IndexPatternField) => { + + {selectedFields.map((field: IndexPatternField, index) => { return ( -
  • - -
  • + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + ); })} -
-
- )} + + +

+ +

+
-
    - - {unpopularFields.map((field: IndexPatternField, index) => { - return ( - 0 && ( +
    + + + + - { + return ( + + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + + ); + })} + +
    + )} + + {unpopularFields.map((field: IndexPatternField, index) => { + return ( + - - - - ); - })} - -
+ {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + + + + + ); + })} + + + )}
From 8d9a70c6cec860d7e9592fa43d46894db499c259 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Fri, 21 Jul 2023 00:28:31 +0000 Subject: [PATCH 09/16] updated side panel styles with oui --- .../public/components/app_container.scss | 5 +- .../public/components/app_container.tsx | 24 ++--- .../public/components/sidebar/index.tsx | 89 +++++++++++-------- .../sidebar/discover_field_details.scss | 6 -- .../sidebar/discover_field_details.tsx | 1 - .../sidebar/discover_field_search.tsx | 66 +++++++------- .../components/sidebar/discover_sidebar.scss | 34 ------- .../components/sidebar/discover_sidebar.tsx | 53 ++++++----- 8 files changed, 122 insertions(+), 156 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/sidebar/discover_field_details.scss diff --git a/src/plugins/data_explorer/public/components/app_container.scss b/src/plugins/data_explorer/public/components/app_container.scss index 3a3d856e3bdf..5cc1ddc8f622 100644 --- a/src/plugins/data_explorer/public/components/app_container.scss +++ b/src/plugins/data_explorer/public/components/app_container.scss @@ -1,5 +1,4 @@ -// Needed to allow the sidebar to scroll .deSidebar { - display: flex; - flex-direction: column; + max-width: 462px; + min-width: 400px; } diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index b8cdee7e78b0..b8a3aea94051 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { EuiPageTemplate } from '@elastic/eui'; +import { EuiPage } from '@elastic/eui'; import { Suspense } from 'react'; import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; @@ -23,26 +23,14 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa // Render the application DOM. // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. return ( - - Loading...}> - - - - } - pageSideBarProps={{ - className: 'deSidebar', - }} - className="dePageTemplate" - template="default" - restrictWidth={false} - paddingSize="none" - > + {/* TODO: improve loading state */} Loading...}> + + + - + ); }; diff --git a/src/plugins/data_explorer/public/components/sidebar/index.tsx b/src/plugins/data_explorer/public/components/sidebar/index.tsx index 42d1df70c1e4..a4769fbcf0bc 100644 --- a/src/plugins/data_explorer/public/components/sidebar/index.tsx +++ b/src/plugins/data_explorer/public/components/sidebar/index.tsx @@ -5,7 +5,14 @@ import React, { useMemo, FC, useEffect, useState } from 'react'; import { i18n } from '@osd/i18n'; -import { EuiPanel, EuiComboBox, EuiSelect, EuiComboBoxOptionOption, EuiSpacer } from '@elastic/eui'; +import { + EuiComboBox, + EuiSelect, + EuiComboBoxOptionOption, + EuiSpacer, + EuiSplitPanel, + EuiPageSideBar, +} from '@elastic/eui'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { useView } from '../../utils/use'; import { DataExplorerServices } from '../../types'; @@ -57,44 +64,50 @@ export const Sidebar: FC = ({ children }) => { }, [indexPatternId, options]); return ( - <> - - { - // TODO: There are many issues with this approach, but it's a start - // 1. Combo box can delete a selected index pattern. This should not be possible - // 2. Combo box is severely truncated. This should be fixed in the EUI component - // 3. The onchange can fire with a option that is not valid. discuss where to handle this. - // 4. value is optional. If the combobox needs to act as a slecet, this should be required. - const { value } = selected[0] || {}; + + + + { + // TODO: There are many issues with this approach, but it's a start + // 1. Combo box can delete a selected index pattern. This should not be possible + // 2. Combo box is severely truncated. This should be fixed in the EUI component + // 3. The onchange can fire with a option that is not valid. discuss where to handle this. + // 4. value is optional. If the combobox needs to act as a slecet, this should be required. + const { value } = selected[0] || {}; - if (!value) { - toasts.addWarning({ - id: 'index-pattern-not-found', - title: i18n.translate('dataExplorer.indexPatternError', { - defaultMessage: 'Index pattern not found', - }), - }); - return; - } + if (!value) { + toasts.addWarning({ + id: 'index-pattern-not-found', + title: i18n.translate('dataExplorer.indexPatternError', { + defaultMessage: 'Index pattern not found', + }), + }); + return; + } - dispatch(setIndexPattern(value)); - }} - /> - - { - dispatch(setView(e.target.value)); - }} - /> - - {children} - + dispatch(setIndexPattern(value)); + }} + /> + + { + dispatch(setView(e.target.value)); + }} + fullWidth + /> + + + {children} + + + ); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss b/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss deleted file mode 100644 index 7bf0892d0148..000000000000 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.scss +++ /dev/null @@ -1,6 +0,0 @@ -.dscFieldDetails__visualizeBtn { - @include euiFontSizeXS; - - height: $euiSizeL !important; - min-width: $euiSize * 4; -} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx index 906c173ed07d..ce22761e75fa 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_details.tsx @@ -40,7 +40,6 @@ import { } from './lib/visualize_trigger_utils'; import { Bucket, FieldDetails } from './types'; import { IndexPatternField, IndexPattern } from '../../../../../data/public'; -import './discover_field_details.scss'; interface DiscoverFieldDetailsProps { columns: string[]; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 30bc4e00544d..3541516ae50c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -31,11 +31,9 @@ import React, { OptionHTMLAttributes, ReactNode, useState } from 'react'; import { i18n } from '@osd/i18n'; import { - EuiFacetButton, EuiFieldSearch, EuiFlexGroup, EuiFlexItem, - EuiIcon, EuiPopover, EuiPopoverFooter, EuiPopoverTitle, @@ -47,6 +45,8 @@ import { EuiButtonGroup, EuiOutsideClickDetector, EuiPanel, + EuiFilterButton, + EuiFilterGroup, } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; @@ -174,23 +174,6 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { handleValueChange('missing', missingValue); }; - const buttonContent = ( - } - isSelected={activeFiltersCount > 0} - quantity={activeFiltersCount} - onClick={handleFacetButtonClicked} - > - - - ); - const select = ( id: string, selectOptions: Array<{ text: ReactNode } & OptionHTMLAttributes>, @@ -246,7 +229,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { }; const selectionPanel = ( -
+ {buttonGroup('aggregatable', aggregatableLabel)} @@ -258,26 +241,26 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { {select('type', typeOptions, values.type)} -
+
); return ( - - - + + + {}} isDisabled={!isPopoverOpen}> onChange('name', event.currentTarget.value)} placeholder={searchPlaceholder} value={value} /> - - -
- {}} isDisabled={!isPopoverOpen}> + + + + { setPopoverOpen(false); }} - button={buttonContent} + button={ + 0} + aria-label={filterBtnAriaLabel} + data-test-subj="toggleFieldFilterButton" + numFilters={3} + onClick={handleFacetButtonClicked} + numActiveFilters={activeFiltersCount} + isSelected={isPopoverOpen} + > + + + } > {i18n.translate('discover.fieldChooser.filter.filterByTypeLabel', { @@ -307,8 +307,8 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { /> - -
-
+ + + ); } diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss index 8487630e5013..f547dbe9deeb 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.scss @@ -1,37 +1,3 @@ -// Needed for scrollable sidebar -.sidebar-list { - display: flex; - flex-direction: column; -} - .dscFieldListHeader { padding-left: $euiSizeS; } - -.dscFieldList--popular { - background-color: lightOrDarkTheme(tint($euiColorPrimary, 90%), $euiColorLightShade); -} - -.dscFieldSearch { - padding: $euiSizeS; -} - -.dscFieldSearch__toggleButton { - width: calc(100% - #{$euiSizeS}); - color: $euiColorPrimary; - padding-left: $euiSizeXS; - margin-left: $euiSizeXS; -} - -.dscFieldSearch__filterWrapper { - flex-grow: 0; -} - -.dscFieldSearch__formWrapper { - padding: $euiSizeM; -} - -.dscFieldDetails { - color: $euiTextColor; - margin-bottom: $euiSizeS; -} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 0554ed1c7344..4b84a3ed1ce3 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -38,6 +38,7 @@ import { EuiDroppable, EuiDraggable, EuiPanel, + EuiSplitPanel, } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@osd/i18n/react'; import { DiscoverField } from './discover_field'; @@ -181,21 +182,26 @@ export function DiscoverSidebar({ return ( -
- -
+ + + - -
+ + {fields.length > 0 && ( <> @@ -247,7 +253,15 @@ export function DiscoverSidebar({ {popularFields.length > 0 && ( -
+ - + {popularFields.map((field: IndexPatternField, index) => { return ( -
+ )} )} -
-
-
+ + +
); } From ace6ad621ce8880371701e94da17d04b5ccd0480 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Mon, 24 Jul 2023 22:30:36 +0000 Subject: [PATCH 10/16] minor fixes --- .../data_explorer/public/components/app_container.tsx | 6 ++++-- .../application/components/sidebar/discover_field.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index b8a3aea94051..c22c82e48877 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { EuiPage } from '@elastic/eui'; +import { EuiPage, EuiPageBody } from '@elastic/eui'; import { Suspense } from 'react'; import { AppMountParameters } from '../../../../core/public'; import { Sidebar } from './sidebar'; @@ -29,7 +29,9 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa - + + + ); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index f14da1912b3f..37f04f392ea7 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -210,7 +210,7 @@ export const DiscoverField = ({ } return ( - + Date: Tue, 25 Jul 2023 10:24:40 +0000 Subject: [PATCH 11/16] data explorer app state syncing with url works --- .../data_explorer/public/components/app.tsx | 23 +++++++- .../public/components/app_container.tsx | 19 +++++-- src/plugins/data_explorer/public/index.ts | 2 +- .../public/services/view_service/types.ts | 7 ++- src/plugins/data_explorer/public/types.ts | 5 -- .../view_components/canvas/index.tsx | 18 +++++- .../view_components/panel/panel.tsx | 1 + .../discover/public/get_redirect_state.ts | 43 ++++++++++++++ .../public/opensearch_dashboards_services.ts | 5 -- src/plugins/discover/public/plugin.ts | 57 +++---------------- src/plugins/discover_legacy/public/plugin.ts | 8 +-- 11 files changed, 111 insertions(+), 77 deletions(-) create mode 100644 src/plugins/discover/public/get_redirect_state.ts diff --git a/src/plugins/data_explorer/public/components/app.tsx b/src/plugins/data_explorer/public/components/app.tsx index 40d23d97356b..ff6b5931a404 100644 --- a/src/plugins/data_explorer/public/components/app.tsx +++ b/src/plugins/data_explorer/public/components/app.tsx @@ -3,13 +3,34 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import { AppMountParameters } from '../../../../core/public'; import { useView } from '../utils/use'; import { AppContainer } from './app_container'; +import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; +import { DataExplorerServices } from '../types'; +import { syncQueryStateWithUrl } from '../../../data/public'; export const DataExplorerApp = ({ params }: { params: AppMountParameters }) => { const { view } = useView(); + const { + services: { + data: { query }, + osdUrlStateStorage, + }, + } = useOpenSearchDashboards(); + const { pathname } = useLocation(); + + useEffect(() => { + // syncs `_g` portion of url with query services + const { stop } = syncQueryStateWithUrl(query, osdUrlStateStorage); + + return () => stop(); + + // this effect should re-run when pathname is changed to preserve querystring part, + // so the global state is always preserved + }, [query, osdUrlStateStorage, pathname]); return ; }; diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index c22c82e48877..dfc710e87bfa 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useState } from 'react'; import { EuiPage, EuiPageBody } from '@elastic/eui'; import { Suspense } from 'react'; import { AppMountParameters } from '../../../../core/public'; @@ -13,24 +13,33 @@ import { View } from '../services/view_service/view'; import './app_container.scss'; export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => { + // To support redirects, we need to store the redirect state in a stateful component. + const [redirectState] = useState(() => { + return params.history.location.state; + }); + // TODO: Make this more robust. if (!view) { return ; } + const viewParams = { + ...params, + redirectState, + }; + const { Canvas, Panel } = view; // Render the application DOM. - // Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract. return ( - + {/* TODO: improve loading state */} Loading...}> - + - + diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index 0a0575e339c1..00ee4cc832ba 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -12,6 +12,6 @@ import { DataExplorerPlugin } from './plugin'; export function plugin() { return new DataExplorerPlugin(); } -export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types'; +export { DataExplorerPluginSetup, DataExplorerPluginStart } from './types'; export { ViewProps, ViewDefinition } from './services/view_service'; export { RootState, useTypedSelector, useTypedDispatch } from './utils/state_management'; diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index 2aa3915da468..8a8e56551ed1 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -5,16 +5,17 @@ import { Slice } from '@reduxjs/toolkit'; import { LazyExoticComponent } from 'react'; +import { LocationState } from 'history'; import { AppMountParameters } from '../../../../../core/public'; -// TODO: State management props - interface ViewListItem { id: string; label: string; } -export type ViewProps = AppMountParameters; +export interface ViewProps extends AppMountParameters { + redirectState: LocationState; +} export interface ViewDefinition { readonly id: string; diff --git a/src/plugins/data_explorer/public/types.ts b/src/plugins/data_explorer/public/types.ts index 1c7b21c191dc..5f677fb46cfd 100644 --- a/src/plugins/data_explorer/public/types.ts +++ b/src/plugins/data_explorer/public/types.ts @@ -26,11 +26,6 @@ export interface DataExplorerPluginStartDependencies { data: DataPublicPluginStart; } -export interface ViewRedirectParams { - view: string; - path?: string; -} - export interface DataExplorerServices extends CoreStart { store?: Store; viewRegistry: ViewServiceStart; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 34fd6a0bf103..a67a60d96445 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -3,15 +3,29 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect } from 'react'; import { ViewProps } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; import { Canvas } from './canvas'; import { getServices } from '../../../opensearch_dashboards_services'; +import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; +import { createOsdUrlStateStorage } from '../../../../../opensearch_dashboards_utils/public'; // eslint-disable-next-line import/no-default-export -export default function CanvasApp({ setHeaderActionMenu }: ViewProps) { +export default function CanvasApp({ setHeaderActionMenu, redirectState, history }: ViewProps) { const services = getServices(); + + useEffect(() => { + const osdUrlStateStorage = createOsdUrlStateStorage({ + history, + useHash: services.uiSettings.get('state:storeInSessionStorage'), + }); + connectStorageToQueryState(services.data.query, osdUrlStateStorage, { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + }); + }, [history, services.data.query, services.uiSettings]); + return ( { ({ ...acc, [field.name]: 1 }), {} as Record diff --git a/src/plugins/discover/public/get_redirect_state.ts b/src/plugins/discover/public/get_redirect_state.ts new file mode 100644 index 000000000000..b8d41cb22886 --- /dev/null +++ b/src/plugins/discover/public/get_redirect_state.ts @@ -0,0 +1,43 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { matchPath } from 'react-router-dom'; + +interface RedirectState { + indexPattern: string; + id?: string; + appState?: any; + index?: string; + docView: string; +} + +/** + * Returns the redirect state for a given path, based on a list of path patterns. + * @param path The path to match against the patterns. + * @param pathPatterns An array of path patterns to match against the path. + * @returns The redirect state if a match is found, otherwise null. + */ +export function getRedirectState(path: string): RedirectState | null { + const pathPatterns = [ + { + pattern: '#/context/:indexPattern/:id\\?:appState', + extraState: { docView: 'context' }, + }, + { pattern: '#/doc/:indexPattern/:index', extraState: { docView: 'doc' } }, + ]; + + for (let i = 0; i < pathPatterns.length; i++) { + const redirectState = matchPath(path, { + path: pathPatterns[i].pattern, + exact: false, + })?.params; + + if (redirectState) { + return { ...redirectState, ...pathPatterns[i].extraState }; + } + } + + return null; +} diff --git a/src/plugins/discover/public/opensearch_dashboards_services.ts b/src/plugins/discover/public/opensearch_dashboards_services.ts index 8531564e0cc7..67037c840ca1 100644 --- a/src/plugins/discover/public/opensearch_dashboards_services.ts +++ b/src/plugins/discover/public/opensearch_dashboards_services.ts @@ -74,11 +74,6 @@ export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGe AppMountParameters['setHeaderActionMenu'] >('headerActionMenuMounter'); -export const [getUrlTracker, setUrlTracker] = createGetterSetter<{ - setTrackedUrl: (url: string) => void; - restorePreviousUrl: () => void; -}>('urlTracker'); - export const [getDocViewsRegistry, setDocViewsRegistry] = createGetterSetter( 'DocViewsRegistry' ); diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 3fd5acfa7404..fc1081208142 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -5,7 +5,6 @@ import { i18n } from '@osd/i18n'; import { BehaviorSubject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; import { AppMountParameters, @@ -33,7 +32,7 @@ import rison from 'rison-node'; import { lazy } from 'react'; import { DataPublicPluginStart, DataPublicPluginSetup, opensearchFilters } from '../../data/public'; import { SavedObjectLoader } from '../../saved_objects/public'; -import { createOsdUrlTracker, url } from '../../opensearch_dashboards_utils/public'; +import { url } from '../../opensearch_dashboards_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; @@ -45,12 +44,10 @@ import { JsonCodeBlock } from './application/components/json_code_block/json_cod import { setDocViewsRegistry, setDocViewsLinksRegistry, - setUrlTracker, setServices, setHeaderActionMenuMounter, setUiActions, setScopedHistory, - getScopedHistory, syncHistoryLocations, getServices, } from './opensearch_dashboards_services'; @@ -63,13 +60,14 @@ import { } from './url_generator'; // import { SearchEmbeddableFactory } from './application/embeddable'; import { NEW_DISCOVER_APP, PLUGIN_ID } from '../common'; -import { DataExplorerPluginSetup, ViewRedirectParams } from '../../data_explorer/public'; +import { DataExplorerPluginSetup } from '../../data_explorer/public'; import { registerFeature } from './register_feature'; import { DiscoverState, discoverSlice, getPreloadedState, } from './application/utils/state_management/discover_slice'; +import { getRedirectState } from './get_redirect_state'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -245,42 +243,6 @@ export class DiscoverPlugin order: 2, }); - const { - appMounted, - appUnMounted, - stop: stopUrlTracker, - setActiveUrl: setTrackedUrl, - restorePreviousUrl, - } = createOsdUrlTracker({ - // we pass getter here instead of plain `history`, - // so history is lazily created (when app is mounted) - // this prevents redundant `#` when not in discover app - getHistory: getScopedHistory, - baseUrl, - defaultSubUrl: '#/', - storageKey: `lastUrl:${core.http.basePath.get()}:discover`, - navLinkUpdater$: this.appStateUpdater, - toastNotifications: core.notifications.toasts, - stateParams: [ - { - osdUrlKey: '_g', - stateUpdate$: plugins.data.query.state$.pipe( - filter( - ({ changes }) => !!(changes.globalFilters || changes.time || changes.refreshInterval) - ), - map(({ state }) => ({ - ...state, - filters: state.filters?.filter(opensearchFilters.isFilterPinned), - })) - ), - }, - ], - }); - setUrlTracker({ setTrackedUrl, restorePreviousUrl }); - this.stopUrlTracking = () => { - stopUrlTracker(); - }; - core.application.register({ id: PLUGIN_ID, title: 'Discover', @@ -299,7 +261,6 @@ export class DiscoverPlugin setScopedHistory(params.history); setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); - appMounted(); const { core: { application: { navigateToApp }, @@ -308,6 +269,8 @@ export class DiscoverPlugin // This is for instances where the user navigates to the app from the application nav menu const path = window.location.hash; + const redirectState = getRedirectState(path); + // debugger; const v2Enabled = await core.uiSettings.get(NEW_DISCOVER_APP); if (!v2Enabled) { navigateToApp('discoverLegacy', { @@ -318,15 +281,11 @@ export class DiscoverPlugin navigateToApp('data-explorer', { replace: true, path: `/${PLUGIN_ID}`, - state: { - path, - } as ViewRedirectParams, + state: redirectState, }); } - return () => { - appUnMounted(); - }; + return () => {}; }, }); @@ -379,7 +338,7 @@ export class DiscoverPlugin slice: discoverSlice, }, shouldShow: () => true, - // ViewCompon + // ViewComponent Canvas: lazy(() => import('./application/view_components/canvas')), Panel: lazy(() => import('./application/view_components/panel')), }); diff --git a/src/plugins/discover_legacy/public/plugin.ts b/src/plugins/discover_legacy/public/plugin.ts index 85cd68a07791..1173d676e28c 100644 --- a/src/plugins/discover_legacy/public/plugin.ts +++ b/src/plugins/discover_legacy/public/plugin.ts @@ -89,7 +89,6 @@ import { } from './url_generator'; import { SearchEmbeddableFactory } from './application/embeddable'; import { AppNavLinkStatus } from '../../../core/public'; -import { ViewRedirectParams } from '../../data_explorer/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -331,12 +330,9 @@ export class DiscoverPlugin const v2Enabled = core.uiSettings.get(NEW_DISCOVER_APP); if (v2Enabled) { - navigateToApp('data-explorer', { + navigateToApp('discover', { replace: true, - path: `/discover`, - state: { - path, - } as ViewRedirectParams, + path, }); } setScopedHistory(params.history); From d7e84473205990b06d251d0cab7d06305dc7cc37 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 25 Jul 2023 10:50:46 +0000 Subject: [PATCH 12/16] simplified query state --- src/plugins/data_explorer/public/index.ts | 2 +- .../application/view_components/canvas/canvas.tsx | 11 ++++++++++- .../application/view_components/canvas/index.tsx | 15 +-------------- src/plugins/discover/public/build_services.ts | 7 +++++-- .../public/opensearch_dashboards_services.ts | 4 ++-- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index 00ee4cc832ba..298cdcc00ded 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -12,6 +12,6 @@ import { DataExplorerPlugin } from './plugin'; export function plugin() { return new DataExplorerPlugin(); } -export { DataExplorerPluginSetup, DataExplorerPluginStart } from './types'; +export { DataExplorerPluginSetup, DataExplorerPluginStart, DataExplorerServices } from './types'; export { ViewProps, ViewDefinition } from './services/view_service'; export { RootState, useTypedSelector, useTypedDispatch } from './utils/state_management'; diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx index 0246d97851ae..8c97afd0aea9 100644 --- a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx @@ -3,12 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect } from 'react'; import { AppMountParameters } from '../../../../../../core/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { DiscoverServices } from '../../../build_services'; import { TopNav } from './top_nav'; import { updateState, useDispatch, useSelector } from '../../utils/state_management'; +import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; interface CanvasProps { opts: { @@ -21,6 +22,14 @@ export const Canvas = ({ opts }: CanvasProps) => { const interval = useSelector((state) => state.discover.interval); const dispatch = useDispatch(); + // Connect the query service to the url state + useEffect(() => { + connectStorageToQueryState(services.data.query, services.osdUrlStateStorage, { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + }); + }, [services.data.query, services.osdUrlStateStorage, services.uiSettings]); + return (
diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index a67a60d96445..71ab8013f989 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -3,29 +3,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect } from 'react'; +import React from 'react'; import { ViewProps } from '../../../../../data_explorer/public'; import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; import { Canvas } from './canvas'; import { getServices } from '../../../opensearch_dashboards_services'; -import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; -import { createOsdUrlStateStorage } from '../../../../../opensearch_dashboards_utils/public'; // eslint-disable-next-line import/no-default-export export default function CanvasApp({ setHeaderActionMenu, redirectState, history }: ViewProps) { const services = getServices(); - useEffect(() => { - const osdUrlStateStorage = createOsdUrlStateStorage({ - history, - useHash: services.uiSettings.get('state:storeInSessionStorage'), - }); - connectStorageToQueryState(services.data.query, osdUrlStateStorage, { - filters: opensearchFilters.FilterStateStore.APP_STATE, - query: true, - }); - }, [history, services.data.query, services.uiSettings]); - return ( string; capabilities: Capabilities; chrome: ChromeStart; @@ -87,7 +88,7 @@ export function buildServices( core: CoreStart, plugins: DiscoverStartPlugins, context: PluginInitializerContext -): DiscoverServices { +): BuildDiscoverServices { const services: SavedObjectOpenSearchDashboardsServices = { savedObjectsClient: core.savedObjects.client, indexPatterns: plugins.data.indexPatterns, @@ -124,3 +125,5 @@ export function buildServices( visualizations: plugins.visualizations, }; } + +export type DiscoverServices = BuildDiscoverServices & DataExplorerServices; diff --git a/src/plugins/discover/public/opensearch_dashboards_services.ts b/src/plugins/discover/public/opensearch_dashboards_services.ts index 67037c840ca1..43032637a2a1 100644 --- a/src/plugins/discover/public/opensearch_dashboards_services.ts +++ b/src/plugins/discover/public/opensearch_dashboards_services.ts @@ -32,7 +32,7 @@ import _ from 'lodash'; import { createHashHistory } from 'history'; import { ScopedHistory, AppMountParameters } from 'opensearch-dashboards/public'; import { UiActionsStart } from 'src/plugins/ui_actions/public'; -import { DiscoverServices } from './build_services'; +import { BuildDiscoverServices, DiscoverServices } from './build_services'; import { createGetterSetter } from '../../opensearch_dashboards_utils/public'; import { search } from '../../data/public'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; @@ -56,7 +56,7 @@ export function getAngularModule() { return angularModule; } -export function getServices(): DiscoverServices { +export function getServices(): BuildDiscoverServices { if (!services) { throw new Error('Discover services are not yet available'); } From 00a1d79de8953216445a06d754d5f29742f629cd Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Thu, 27 Jul 2023 03:07:36 +0000 Subject: [PATCH 13/16] migrating discover url state works --- .../public/services/view_service/types.ts | 5 +- .../utils/state_management/discover_slice.tsx | 7 +- .../view_components/canvas/index.tsx | 2 +- .../discover/public/get_redirect_state.ts | 43 ------ src/plugins/discover/public/migrate_state.ts | 123 ++++++++++++++++++ src/plugins/discover/public/plugin.ts | 7 +- 6 files changed, 132 insertions(+), 55 deletions(-) delete mode 100644 src/plugins/discover/public/get_redirect_state.ts create mode 100644 src/plugins/discover/public/migrate_state.ts diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index 8a8e56551ed1..ca0cf421c6c1 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -5,7 +5,6 @@ import { Slice } from '@reduxjs/toolkit'; import { LazyExoticComponent } from 'react'; -import { LocationState } from 'history'; import { AppMountParameters } from '../../../../../core/public'; interface ViewListItem { @@ -13,9 +12,7 @@ interface ViewListItem { label: string; } -export interface ViewProps extends AppMountParameters { - redirectState: LocationState; -} +export type ViewProps = AppMountParameters; export interface ViewDefinition { readonly id: string; diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx index 564ac38f7ed4..120312222898 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -5,7 +5,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Filter, Query } from '../../../../../data/public'; -import { DiscoverServices } from '../../../build_services'; +import { BuildDiscoverServices } from '../../../build_services'; import { RootState } from '../../../../../data_explorer/public'; export interface DiscoverState { @@ -41,10 +41,11 @@ export interface DiscoverRootState extends RootState { const initialState = {} as DiscoverState; -export const getPreloadedState = async ({ data }: DiscoverServices): Promise => { +export const getPreloadedState = async ({ + data, +}: BuildDiscoverServices): Promise => { return { ...initialState, - interval: data.query.timefilter.timefilter.getRefreshInterval().value.toString(), }; }; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 71ab8013f989..28d1eb57e12f 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -10,7 +10,7 @@ import { Canvas } from './canvas'; import { getServices } from '../../../opensearch_dashboards_services'; // eslint-disable-next-line import/no-default-export -export default function CanvasApp({ setHeaderActionMenu, redirectState, history }: ViewProps) { +export default function CanvasApp({ setHeaderActionMenu }: ViewProps) { const services = getServices(); return ( diff --git a/src/plugins/discover/public/get_redirect_state.ts b/src/plugins/discover/public/get_redirect_state.ts deleted file mode 100644 index b8d41cb22886..000000000000 --- a/src/plugins/discover/public/get_redirect_state.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { matchPath } from 'react-router-dom'; - -interface RedirectState { - indexPattern: string; - id?: string; - appState?: any; - index?: string; - docView: string; -} - -/** - * Returns the redirect state for a given path, based on a list of path patterns. - * @param path The path to match against the patterns. - * @param pathPatterns An array of path patterns to match against the path. - * @returns The redirect state if a match is found, otherwise null. - */ -export function getRedirectState(path: string): RedirectState | null { - const pathPatterns = [ - { - pattern: '#/context/:indexPattern/:id\\?:appState', - extraState: { docView: 'context' }, - }, - { pattern: '#/doc/:indexPattern/:index', extraState: { docView: 'doc' } }, - ]; - - for (let i = 0; i < pathPatterns.length; i++) { - const redirectState = matchPath(path, { - path: pathPatterns[i].pattern, - exact: false, - })?.params; - - if (redirectState) { - return { ...redirectState, ...pathPatterns[i].extraState }; - } - } - - return null; -} diff --git a/src/plugins/discover/public/migrate_state.ts b/src/plugins/discover/public/migrate_state.ts new file mode 100644 index 000000000000..1b463e2167c1 --- /dev/null +++ b/src/plugins/discover/public/migrate_state.ts @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { matchPath } from 'react-router-dom'; +import { getStateFromOsdUrl, setStateToOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { Filter, Query } from '../../data/public'; + +interface CommonParams { + appState?: string; +} + +interface ContextParams extends CommonParams { + indexPattern: string; + id: string; +} + +interface DocParams extends CommonParams { + indexPattern: string; + index: string; +} + +export interface LegacyDiscoverState { + /** + * Columns displayed in the table + */ + columns?: string[]; + /** + * Array of applied filters + */ + filters?: Filter[]; + /** + * id of the used index pattern + */ + index?: string; + /** + * Used interval of the histogram + */ + interval?: string; + /** + * Lucence or DQL query + */ + query?: Query; + /** + * Array of the used sorting [[field,direction],...] + */ + sort?: string[][]; + /** + * id of the used saved query + */ + savedQuery?: string; +} + +// TODO: Write unit tests once all routes have been migrated. +/** + * Migrates legacy URLs to the current URL format. + * @param oldPath The legacy hash that contains the state. + * @param newPath The new base path. + */ +export function migrateUrlState(oldPath: string, newPath = '/'): string { + let path = newPath; + const pathPatterns = [ + { + pattern: '#/context/:indexPattern/:id\\?:appState', + extraState: { docView: 'context' }, + path: `context`, + }, + { + pattern: '#/doc/:indexPattern/:index\\?:appState', + extraState: { docView: 'doc' }, + path: `doc`, + }, + { pattern: '#/\\?:appState', extraState: {}, path: `discover` }, + ]; + + // Get the first matching path pattern. + const matchingPathPattern = pathPatterns.find((pathPattern) => + matchPath(oldPath, { path: pathPattern.pattern }) + ); + + if (!matchingPathPattern) { + return path; + } + + // Migrate the path. + switch (matchingPathPattern.path) { + case `discover`: + const params = matchPath(oldPath, { + path: matchingPathPattern.pattern, + })!.params; + + const appState = getStateFromOsdUrl('_a', `/#?${params.appState}`); + + if (!appState) return path; + + const { columns, filters, index, interval, query, sort, savedQuery } = appState; + + const _q = { + query, + filters, + }; + + const _a = { + discover: { + columns, + interval, + sort, + savedQuery, + }, + metadata: { + indexPattern: index, + }, + }; + + path = setStateToOsdUrl('_a', _a, { useHash: false }, path); + path = setStateToOsdUrl('_q', _q, { useHash: false }, path); + + break; + } + + return path; +} diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index fc1081208142..5c5dc6fcf2c2 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -67,7 +67,7 @@ import { discoverSlice, getPreloadedState, } from './application/utils/state_management/discover_slice'; -import { getRedirectState } from './get_redirect_state'; +import { migrateUrlState } from './migrate_state'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -269,7 +269,6 @@ export class DiscoverPlugin // This is for instances where the user navigates to the app from the application nav menu const path = window.location.hash; - const redirectState = getRedirectState(path); // debugger; const v2Enabled = await core.uiSettings.get(NEW_DISCOVER_APP); if (!v2Enabled) { @@ -278,10 +277,10 @@ export class DiscoverPlugin path, }); } else { + const newPath = migrateUrlState(path); navigateToApp('data-explorer', { replace: true, - path: `/${PLUGIN_ID}`, - state: redirectState, + path: `/${PLUGIN_ID}${newPath}`, }); } From c8c7ac4ad5dfbdf223d19b5d75ee22b9e95b406e Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 1 Aug 2023 01:26:15 +0000 Subject: [PATCH 14/16] link table columns to state and minor cleanup --- .../public/components/app_container.tsx | 2 +- .../components/data_grid/data_grid_table.tsx | 18 ++-- .../data_grid/data_grid_table_context.tsx | 8 +- .../data_grid_table_docview_expand_button.tsx | 10 +- .../data_grid/data_grid_table_flyout.tsx | 92 +++++-------------- .../doc_viewer_links/doc_viewer_links.tsx | 4 +- .../components/sidebar/discover_field.tsx | 5 + .../application/components/table/table.scss | 3 + .../application/components/table/table.tsx | 1 + .../utils/state_management/discover_slice.tsx | 9 +- .../canvas/discover_table_app.tsx | 47 +++++----- .../utils/get_sort_for_search_source.ts | 2 +- src/plugins/discover/public/plugin.ts | 8 ++ 13 files changed, 93 insertions(+), 116 deletions(-) create mode 100644 src/plugins/discover/public/application/components/table/table.scss diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index aaff9f3c7e92..6ffa5727715c 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -28,7 +28,7 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa - + diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx index 5de13826d92b..43e62c0f96b0 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table.tsx @@ -10,7 +10,7 @@ import { fetchTableDataCell } from './data_grid_table_cell_value'; import { buildDataGridColumns, computeVisibleColumns } from './data_grid_table_columns'; import { DocViewExpandButton } from './data_grid_table_docview_expand_button'; import { DataGridFlyout } from './data_grid_table_flyout'; -import { DataGridContext } from './data_grid_table_context'; +import { DiscoverGridContextProvider } from './data_grid_table_context'; import { toolbarVisibility } from './constants'; import { DocViewFilterFn } from '../../doc_views/doc_views_types'; import { DiscoverServices } from '../../../build_services'; @@ -44,7 +44,7 @@ export const DataGridTable = ({ displayTimeColumn, services, }: DataGridTableProps) => { - const [docViewExpand, setDocViewExpand] = useState(undefined); + const [expandedHit, setExpandedHit] = useState(); const rowCount = useMemo(() => (rows ? rows.length : 0), [rows]); const pagination = usePagination(rowCount); @@ -94,11 +94,11 @@ export const DataGridTable = ({ }, []); return ( - - {docViewExpand && ( + {expandedHit && ( setDocViewExpand(undefined)} + onClose={() => setExpandedHit(undefined)} services={services} /> )} - + ); }; diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx index 87112c4bff45..c3568d44082a 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_context.tsx @@ -5,13 +5,13 @@ import React from 'react'; import { IndexPattern } from '../../../opensearch_dashboards_services'; -import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { DocViewFilterFn, OpenSearchSearchHit } from '../../doc_views/doc_views_types'; export interface DataGridContextProps { - docViewExpand: any; + expandedHit?: OpenSearchSearchHit; onFilter: DocViewFilterFn; - setDocViewExpand: (hit: any) => void; - rows: any[]; + setExpandedHit: (hit?: OpenSearchSearchHit) => void; + rows: OpenSearchSearchHit[]; indexPattern: IndexPattern; } diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_expand_button.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_expand_button.tsx index 30aa4e45d0c7..beb8e7de278a 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_expand_button.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_docview_expand_button.tsx @@ -3,22 +3,22 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useContext } from 'react'; +import React from 'react'; import { EuiToolTip, EuiButtonIcon, EuiDataGridCellValueElementProps } from '@elastic/eui'; -import { DataGridContext } from './data_grid_table_context'; +import { useDataGridContext } from './data_grid_table_context'; export const DocViewExpandButton = ({ rowIndex, setCellProps, }: EuiDataGridCellValueElementProps) => { - const { docViewExpand, setDocViewExpand, rows } = useContext(DataGridContext); + const { expandedHit, setExpandedHit, rows } = useDataGridContext(); const currentExpanded = rows[rowIndex]; - const isCurrentExpanded = currentExpanded === docViewExpand; + const isCurrentExpanded = currentExpanded === expandedHit; return ( setDocViewExpand(isCurrentExpanded ? undefined : currentExpanded)} + onClick={() => setExpandedHit(isCurrentExpanded ? undefined : currentExpanded)} iconType={isCurrentExpanded ? 'minimize' : 'expand'} aria-label={`Expand row ${rowIndex}`} /> diff --git a/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx b/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx index b4a8643ba4c8..957679a2faef 100644 --- a/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx +++ b/src/plugins/discover/public/application/components/data_grid/data_grid_table_flyout.tsx @@ -4,9 +4,6 @@ */ import React from 'react'; -import { i18n } from '@osd/i18n'; -import { stringify } from 'query-string'; -import rison from 'rison-node'; import { EuiFlyout, @@ -15,15 +12,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle, - EuiLink, - EuiSpacer, } from '@elastic/eui'; import { DocViewer } from '../doc_viewer/doc_viewer'; import { IndexPattern } from '../../../opensearch_dashboards_services'; import { DocViewFilterFn } from '../../doc_views/doc_views_types'; -import { DiscoverServices } from '../../../build_services'; -import { url } from '../../../../../opensearch_dashboards_utils/common'; -import { opensearchFilters } from '../../../../../data/public'; +import { DocViewerLinks } from '../doc_viewer_links/doc_viewer_links'; interface Props { columns: string[]; @@ -33,7 +26,6 @@ interface Props { onClose: () => void; onFilter: DocViewFilterFn; onRemoveColumn: (column: string) => void; - services: DiscoverServices; } export function DataGridFlyout({ @@ -44,76 +36,40 @@ export function DataGridFlyout({ onClose, onFilter, onRemoveColumn, - services, }: Props) { - const generateSurroundingDocumentsUrl = (hitId: string, indexPatternId: string) => { - const globalFilters = services.filterManager.getGlobalFilters(); - const appFilters = services.filterManager.getAppFilters(); - - const hash = stringify( - url.encodeQuery({ - _g: rison.encode({ - filters: globalFilters || [], - }), - _a: rison.encode({ - columns, - filters: (appFilters || []).map(opensearchFilters.disableFilter), - }), - }), - { encode: false, sort: false } - ); - - return `#/context/${encodeURIComponent(indexPatternId)}/${encodeURIComponent(hitId)}?${hash}`; - }; - - const generateSingleDocumentUrl = (hitObj: any, indexPatternId: string) => { - return `#/doc/${indexPatternId}/${hitObj._index}?id=${encodeURIComponent(hit._id)}`; - }; - // TODO: replace EuiLink with doc_view_links registry + // TODO: Also move the flyout higher in the react tree to prevent redrawing the table component and slowing down page performance return (

Document Details

- - - - - {i18n.translate('discover.docTable.tableRow.viewSingleDocumentLinkText', { - defaultMessage: 'View single document', - })} - - - - - {i18n.translate('discover.docTable.tableRow.viewSurroundingDocumentsLinkText', { - defaultMessage: 'View surrounding documents', - })} - - -
- { - onRemoveColumn(columnName); - onClose(); - }} - onAddColumn={(columnName: string) => { - onAddColumn(columnName); - onClose(); - }} - filter={(mapping, value, mode) => { - onFilter(mapping, value, mode); - onClose(); - }} - /> + + + + + { + onRemoveColumn(columnName); + onClose(); + }} + onAddColumn={(columnName: string) => { + onAddColumn(columnName); + onClose(); + }} + filter={(mapping, value, mode) => { + onFilter(mapping, value, mode); + onClose(); + }} + /> +
diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx index 9efb0693fde6..f529273991a0 100644 --- a/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer_links/doc_viewer_links.tsx @@ -24,9 +24,9 @@ export function DocViewerLinks(renderProps: DocViewLinkRenderProps) { }); return ( - + {listItems.map((item, index) => ( - + ))} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 37f04f392ea7..1f2e386780ec 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -109,6 +109,10 @@ export const DiscoverField = ({ values: { field: field.name }, } ); + const infoLabelAria = i18n.translate('discover.fieldChooser.discoverField.infoButtonAriaLabel', { + defaultMessage: 'View {field} summary', + values: { field: field.name }, + }); const [infoIsOpen, setOpen] = useState(false); @@ -233,6 +237,7 @@ export const DiscoverField = ({ iconType="inspect" size="xs" onClick={() => setOpen((state) => !state)} + aria-label={infoLabelAria} /> } panelClassName="dscSidebarItem__fieldPopoverPanel" diff --git a/src/plugins/discover/public/application/components/table/table.scss b/src/plugins/discover/public/application/components/table/table.scss new file mode 100644 index 000000000000..30ba5fea2a4e --- /dev/null +++ b/src/plugins/discover/public/application/components/table/table.scss @@ -0,0 +1,3 @@ +.truncate-by-height { + overflow: hidden; +} diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 56da534240f9..3ef8e026702e 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -33,6 +33,7 @@ import { escapeRegExp } from 'lodash'; import { DocViewTableRow } from './table_row'; import { arrayContainsObjects } from './table_helper'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; +import './table.scss'; const COLLAPSE_LINE_LENGTH = 350; diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx index ee9fea1de0dc..f4f1ad716148 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -12,7 +12,7 @@ export interface DiscoverState { /** * Columns displayed in the table */ - columns?: string[]; + columns: string[]; /** * Array of applied filters */ @@ -28,7 +28,7 @@ export interface DiscoverState { /** * Array of the used sorting [[field,direction],...] */ - sort?: string[][]; + sort: Array<[string, string]>; /** * id of the used saved query */ @@ -39,7 +39,10 @@ export interface DiscoverRootState extends RootState { discover: DiscoverState; } -const initialState = {} as DiscoverState; +const initialState: DiscoverState = { + columns: [], + sort: [], +}; export const getPreloadedState = async ({ data }: DiscoverServices): Promise => { return { diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx index b40492da8db3..d0d01ce32687 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx @@ -3,8 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; import { DataGridTable } from '../../components/data_grid/data_grid_table'; +import { addColumn, removeColumn, useDispatch, useSelector } from '../../utils/state_management'; export const DiscoverTableApplication = ({ data$, indexPattern, savedSearch, services }) => { const [fetchState, setFetchState] = useState({ @@ -14,6 +14,9 @@ export const DiscoverTableApplication = ({ data$, indexPattern, savedSearch, ser rows: [], }); + const { columns } = useSelector((state) => state.discover); + const dispatch = useDispatch(); + const { rows } = fetchState; useEffect(() => { @@ -30,33 +33,31 @@ export const DiscoverTableApplication = ({ data$, indexPattern, savedSearch, ser }; }, [data$, fetchState]); - // ToDo: implement columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns using config, indexPattern, appState + // ToDo: implement columns, onMoveColumn, onSetColumns using config, indexPattern, appState if (rows.length === 0) { return
{'loading...'}
; } else { return ( - - - -
- {}} - onFilter={() => {}} - onRemoveColumn={() => {}} - onSetColumns={() => {}} - onSort={() => {}} - sort={[]} - rows={rows} - displayTimeColumn={true} - services={services} - /> -
-
-
-
+ + dispatch( + addColumn({ + column, + }) + ) + } + onFilter={() => {}} + onRemoveColumn={(column) => dispatch(removeColumn(column))} + onSetColumns={() => {}} + onSort={() => {}} + sort={[]} + rows={rows} + displayTimeColumn={true} + services={services} + /> ); } }; diff --git a/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts index 0953ccd25b47..b19128a432e0 100644 --- a/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts +++ b/src/plugins/discover/public/application/view_components/utils/get_sort_for_search_source.ts @@ -28,7 +28,7 @@ * under the License. */ -import { OpenSearchQuerySortValue, IndexPattern } from '../../../../opensearch_dashboards_services'; +import { OpenSearchQuerySortValue, IndexPattern } from '../../../opensearch_dashboards_services'; import { getSort } from './get_sort'; import { getDefaultSort } from './get_default_sort'; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 6ac98373f61a..692e53f6fe1d 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -36,6 +36,7 @@ import { url } from '../../opensearch_dashboards_utils/public'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { UrlGeneratorState } from '../../share/public'; import { DocViewInput, DocViewInputFn } from './application/doc_views/doc_views_types'; +import { DocViewLink } from './application/doc_views_links/doc_views_links_types'; import { DocViewsRegistry } from './application/doc_views/doc_views_registry'; import { DocViewsLinksRegistry } from './application/doc_views_links/doc_views_links_registry'; import { DocViewTable } from './application/components/table/table'; @@ -85,6 +86,10 @@ export interface DiscoverSetup { */ addDocView(docViewRaw: DocViewInput | DocViewInputFn): void; }; + + docViewsLinks: { + addDocViewLink(docViewLinkRaw: DocViewLink): void; + }; } export interface DiscoverStart { @@ -339,6 +344,9 @@ export class DiscoverPlugin docViews: { addDocView: this.docViewsRegistry.addDocView.bind(this.docViewsRegistry), }, + docViewsLinks: { + addDocViewLink: this.docViewsLinksRegistry.addDocViewLink.bind(this.docViewsLinksRegistry), + }, }; } From c825ddb9dbcb00c0c1ab5212f88ae7ad0517b7d8 Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Tue, 1 Aug 2023 10:14:25 +0000 Subject: [PATCH 15/16] Adds shared context and simplifies app structure --- .../public/components/app_container.tsx | 16 ++-- .../public/services/view_service/types.ts | 3 + .../public/services/view_service/view.ts | 2 + .../public/utils/state_management/store.ts | 1 + .../public/application/utils/columns.test.ts | 24 +++++ .../public/application/utils/columns.ts | 43 +++++++++ .../state_management/discover_slice.test.tsx | 67 +++++++++++++ .../utils/state_management/discover_slice.tsx | 38 ++------ .../view_components/canvas/canvas.tsx | 39 -------- .../view_components/canvas/discover_table.tsx | 94 ++++++++++--------- .../canvas/discover_table_app.tsx | 63 ------------- .../canvas/discover_table_service.tsx | 49 ---------- .../view_components/canvas/index.tsx | 15 ++- .../utils/use_discover_canvas_service.ts | 49 ---------- .../view_components/context/index.tsx | 42 +++++++++ .../view_components/panel/index.tsx | 65 +++++++++++-- .../view_components/panel/panel.tsx | 71 -------------- .../utils/create_search_source.ts | 45 +++++++++ .../utils/update_data_source.ts | 42 --------- .../utils/use_index_pattern.ts | 44 +++++++++ .../{use_saved_search.ts => use_search.ts} | 61 +++++++----- src/plugins/discover/public/build_services.ts | 1 + src/plugins/discover/public/plugin.ts | 1 + 23 files changed, 443 insertions(+), 432 deletions(-) create mode 100644 src/plugins/discover/public/application/utils/columns.test.ts create mode 100644 src/plugins/discover/public/application/utils/columns.ts create mode 100644 src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx delete mode 100644 src/plugins/discover/public/application/view_components/canvas/canvas.tsx delete mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx delete mode 100644 src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx delete mode 100644 src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts create mode 100644 src/plugins/discover/public/application/view_components/context/index.tsx delete mode 100644 src/plugins/discover/public/application/view_components/panel/panel.tsx create mode 100644 src/plugins/discover/public/application/view_components/utils/create_search_source.ts delete mode 100644 src/plugins/discover/public/application/view_components/utils/update_data_source.ts create mode 100644 src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts rename src/plugins/discover/public/application/view_components/utils/{use_saved_search.ts => use_search.ts} (62%) diff --git a/src/plugins/data_explorer/public/components/app_container.tsx b/src/plugins/data_explorer/public/components/app_container.tsx index 6ffa5727715c..d5db220d6406 100644 --- a/src/plugins/data_explorer/public/components/app_container.tsx +++ b/src/plugins/data_explorer/public/components/app_container.tsx @@ -18,19 +18,21 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa return ; } - const { Canvas, Panel } = view; + const { Canvas, Panel, Context } = view; // Render the application DOM. return ( {/* TODO: improve loading state */} Loading...
}> - - - - - - + + + + + + + +
); diff --git a/src/plugins/data_explorer/public/services/view_service/types.ts b/src/plugins/data_explorer/public/services/view_service/types.ts index ca0cf421c6c1..4138c7d18b82 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -23,6 +23,9 @@ export interface ViewDefinition { }; readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; + readonly Context: LazyExoticComponent< + (props: React.PropsWithChildren) => React.ReactElement + >; readonly defaultPath: string; readonly appExtentions: { savedObject: { diff --git a/src/plugins/data_explorer/public/services/view_service/view.ts b/src/plugins/data_explorer/public/services/view_service/view.ts index 6268aa731497..ebdab31fddc5 100644 --- a/src/plugins/data_explorer/public/services/view_service/view.ts +++ b/src/plugins/data_explorer/public/services/view_service/view.ts @@ -15,6 +15,7 @@ export class View implements IView { readonly shouldShow?: (state: any) => boolean; readonly Canvas: IView['Canvas']; readonly Panel: IView['Panel']; + readonly Context: IView['Context']; constructor(options: ViewDefinition) { this.id = options.id; @@ -25,5 +26,6 @@ export class View implements IView { this.shouldShow = options.shouldShow; this.Canvas = options.Canvas; this.Panel = options.Panel; + this.Context = options.Context; } } diff --git a/src/plugins/data_explorer/public/utils/state_management/store.ts b/src/plugins/data_explorer/public/utils/state_management/store.ts index fec0572ecf9a..cd967d25fc20 100644 --- a/src/plugins/data_explorer/public/utils/state_management/store.ts +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -14,6 +14,7 @@ const commonReducers = { }; let dynamicReducers: { + metadata: typeof metadataReducer; [key: string]: Reducer; } = { ...commonReducers, diff --git a/src/plugins/discover/public/application/utils/columns.test.ts b/src/plugins/discover/public/application/utils/columns.test.ts new file mode 100644 index 000000000000..43c4b0555553 --- /dev/null +++ b/src/plugins/discover/public/application/utils/columns.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { buildColumns } from './columns'; + +describe('buildColumns', () => { + it('returns ["_source"] if columns is empty', () => { + expect(buildColumns([])).toEqual(['_source']); + }); + + it('returns columns if there is only one column', () => { + expect(buildColumns(['foo'])).toEqual(['foo']); + }); + + it('removes "_source" if there are more than one columns', () => { + expect(buildColumns(['foo', '_source', 'bar'])).toEqual(['foo', 'bar']); + }); + + it('returns columns if there are more than one columns but no "_source"', () => { + expect(buildColumns(['foo', 'bar'])).toEqual(['foo', 'bar']); + }); +}); diff --git a/src/plugins/discover/public/application/utils/columns.ts b/src/plugins/discover/public/application/utils/columns.ts new file mode 100644 index 000000000000..062ca24e3ba4 --- /dev/null +++ b/src/plugins/discover/public/application/utils/columns.ts @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Any modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Helper function to provide a fallback to a single _source column if the given array of columns + * is empty, and removes _source if there are more than 1 columns given + * @param columns + */ +export function buildColumns(columns: string[]) { + if (columns.length > 1 && columns.indexOf('_source') !== -1) { + return columns.filter((col) => col !== '_source'); + } else if (columns.length !== 0) { + return columns; + } + return ['_source']; +} diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx new file mode 100644 index 000000000000..71e848bac15e --- /dev/null +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { discoverSlice, DiscoverState } from './discover_slice'; + +describe('discoverSlice', () => { + let initialState: DiscoverState; + + beforeEach(() => { + initialState = { + columns: [], + sort: [], + }; + }); + + it('should handle setState', () => { + const newState = { + columns: ['column1', 'column2'], + sort: [['field1', 'asc']], + }; + const action = { type: 'discover/setState', payload: newState }; + const result = discoverSlice.reducer(initialState, action); + expect(result).toEqual(newState); + }); + + it('should handle addColumn', () => { + const action1 = { type: 'discover/addColumn', payload: { column: 'column1' } }; + const result1 = discoverSlice.reducer(initialState, action1); + expect(result1.columns).toEqual(['column1']); + + const action2 = { type: 'discover/addColumn', payload: { column: 'column2', index: 0 } }; + const result2 = discoverSlice.reducer(result1, action2); + expect(result2.columns).toEqual(['column2', 'column1']); + }); + + it('should handle removeColumn', () => { + initialState = { + columns: ['column1', 'column2'], + sort: [], + }; + const action = { type: 'discover/removeColumn', payload: 'column1' }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column2']); + }); + + it('should handle reorderColumn', () => { + initialState = { + columns: ['column1', 'column2', 'column3'], + sort: [], + }; + const action = { type: 'discover/reorderColumn', payload: { source: 0, destination: 2 } }; + const result = discoverSlice.reducer(initialState, action); + expect(result.columns).toEqual(['column2', 'column3', 'column1']); + }); + + it('should handle updateState', () => { + initialState = { + columns: ['column1', 'column2'], + sort: [['field1', 'asc']], + }; + const action = { type: 'discover/updateState', payload: { sort: [['field2', 'desc']] } }; + const result = discoverSlice.reducer(initialState, action); + expect(result.sort).toEqual([['field2', 'desc']]); + }); +}); diff --git a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx index f4f1ad716148..998a5340faf6 100644 --- a/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx +++ b/src/plugins/discover/public/application/utils/state_management/discover_slice.tsx @@ -7,6 +7,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { Filter, Query } from '../../../../../data/public'; import { DiscoverServices } from '../../../build_services'; import { RootState } from '../../../../../data_explorer/public'; +import { buildColumns } from '../columns'; export interface DiscoverState { /** @@ -57,54 +58,35 @@ export const discoverSlice = createSlice({ setState(state, action: PayloadAction) { return action.payload; }, - addColumn( - state, - action: PayloadAction<{ - column: string; - index?: number; - }> - ) { + addColumn(state, action: PayloadAction<{ column: string; index?: number }>) { const { column, index } = action.payload; const columns = [...(state.columns || [])]; - if (index !== undefined) { - columns.splice(index, 0, column); - } else { - columns.push(column); - } - state = { - ...state, - columns, - }; - - return state; + if (index !== undefined) columns.splice(index, 0, column); + else columns.push(column); + return { ...state, columns: buildColumns(columns) }; }, removeColumn(state, action: PayloadAction) { - state = { + const columns = (state.columns || []).filter((column) => column !== action.payload); + return { ...state, - columns: (state.columns || []).filter((column) => column !== action.payload), + columns: buildColumns(columns), }; - - return state; }, reorderColumn(state, action: PayloadAction<{ source: number; destination: number }>) { const { source, destination } = action.payload; const columns = [...(state.columns || [])]; const [removed] = columns.splice(source, 1); columns.splice(destination, 0, removed); - state = { + return { ...state, columns, }; - - return state; }, updateState(state, action: PayloadAction>) { - state = { + return { ...state, ...action.payload, }; - - return state; }, }, }); diff --git a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx b/src/plugins/discover/public/application/view_components/canvas/canvas.tsx deleted file mode 100644 index 326706b7753e..000000000000 --- a/src/plugins/discover/public/application/view_components/canvas/canvas.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from 'react'; -import { AppMountParameters } from '../../../../../../core/public'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { DiscoverViewServices } from '../../../build_services'; -import { TopNav } from './top_nav'; -import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; -import { DiscoverTable } from './discover_table'; - -interface CanvasProps { - opts: { - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; - }; -} - -export const Canvas = ({ opts }: CanvasProps) => { - const { services } = useOpenSearchDashboards(); - const { history: getHistory } = services; - const history = getHistory(); - - // Connect the query service to the url state - useEffect(() => { - connectStorageToQueryState(services.data.query, services.osdUrlStateStorage, { - filters: opensearchFilters.FilterStateStore.APP_STATE, - query: true, - }); - }, [services.data.query, services.osdUrlStateStorage, services.uiSettings]); - - return ( -
- - -
- ); -}; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx index 84af208dbff8..e92fe96b7d3d 100644 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/discover_table.tsx @@ -5,62 +5,70 @@ import React, { useState, useEffect } from 'react'; import { History } from 'history'; -import { IndexPattern } from '../../../opensearch_dashboards_services'; -import { DiscoverServices } from '../../../build_services'; -import { SavedSearch } from '../../../saved_searches'; -import { DiscoverTableService } from './discover_table_service'; -import { fetchIndexPattern, fetchSavedSearch } from '../utils/index_pattern_helper'; +import { DiscoverViewServices } from '../../../build_services'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DataGridTable } from '../../components/data_grid/data_grid_table'; +import { useDiscoverContext } from '../context'; +import { addColumn, removeColumn, useDispatch, useSelector } from '../../utils/state_management'; +import { SearchData } from '../utils/use_search'; -export interface DiscoverTableProps { - services: DiscoverServices; +interface Props { history: History; } -export const DiscoverTable = ({ history, services }: DiscoverTableProps) => { - const { core, chrome, data, uiSettings: config, toastNotifications } = services; - const [savedSearch, setSavedSearch] = useState(); - const [indexPattern, setIndexPattern] = useState(undefined); - // TODO: get id from data explorer since it is handling the routing logic - // Original angular code: const savedSearchId = $route.current.params.id; - const savedSearchId = ''; - useEffect(() => { - const fetchData = async () => { - const indexPatternData = await fetchIndexPattern(data, config); - setIndexPattern(indexPatternData.loaded); +export const DiscoverTable = ({ history }: Props) => { + const { services } = useOpenSearchDashboards(); + const { data$, indexPattern } = useDiscoverContext(); + const [fetchState, setFetchState] = useState({ + status: data$.getValue().status, + rows: [], + }); - const savedSearchData = await fetchSavedSearch( - core, - '', // basePath - history, - savedSearchId, - services, - toastNotifications - ); - if (savedSearchData && !savedSearchData?.searchSource.getField('index')) { - savedSearchData.searchSource.setField('index', indexPatternData); - } - setSavedSearch(savedSearchData); + const { columns } = useSelector((state) => state.discover); + const dispatch = useDispatch(); + + const { rows } = fetchState || {}; - if (savedSearchId) { - chrome.recentlyAccessed.add( - savedSearchData.getFullPath(), - savedSearchData.title, - savedSearchData.id - ); + useEffect(() => { + const subscription = data$.subscribe((next) => { + if (next.status !== fetchState.status || (next.rows && next.rows !== fetchState.rows)) { + setFetchState({ ...fetchState, ...next }); } + }); + return () => { + subscription.unsubscribe(); }; - fetchData(); - }, [data, config, core, chrome, toastNotifications, history, savedSearchId, services]); + }, [data$, fetchState]); - if (!savedSearch || !savedSearch.searchSource || !indexPattern) { - // TODO: handle loading state + if (indexPattern === undefined) { + // TODO: handle better return null; } + + if (!rows || rows.length === 0) { + // TODO: handle better + return
{'loading...'}
; + } + return ( - + dispatch( + addColumn({ + column, + }) + ) + } + onFilter={() => {}} + onRemoveColumn={(column) => dispatch(removeColumn(column))} + onSetColumns={() => {}} + onSort={() => {}} + sort={[]} + rows={rows} + displayTimeColumn={true} + services={services} /> ); }; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx deleted file mode 100644 index d0d01ce32687..000000000000 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table_app.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import React, { useState, useEffect } from 'react'; -import { DataGridTable } from '../../components/data_grid/data_grid_table'; -import { addColumn, removeColumn, useDispatch, useSelector } from '../../utils/state_management'; - -export const DiscoverTableApplication = ({ data$, indexPattern, savedSearch, services }) => { - const [fetchState, setFetchState] = useState({ - status: data$.getValue().status, - fetchCounter: 0, - fieldCounts: {}, - rows: [], - }); - - const { columns } = useSelector((state) => state.discover); - const dispatch = useDispatch(); - - const { rows } = fetchState; - - useEffect(() => { - const subscription = data$.subscribe((next) => { - if ( - (next.status && next.status !== fetchState.status) || - (next.rows && next.rows !== fetchState.rows) - ) { - setFetchState({ ...fetchState, ...next }); - } - }); - return () => { - subscription.unsubscribe(); - }; - }, [data$, fetchState]); - - // ToDo: implement columns, onMoveColumn, onSetColumns using config, indexPattern, appState - - if (rows.length === 0) { - return
{'loading...'}
; - } else { - return ( - - dispatch( - addColumn({ - column, - }) - ) - } - onFilter={() => {}} - onRemoveColumn={(column) => dispatch(removeColumn(column))} - onSetColumns={() => {}} - onSort={() => {}} - sort={[]} - rows={rows} - displayTimeColumn={true} - services={services} - /> - ); - } -}; diff --git a/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx b/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx deleted file mode 100644 index d0b0b0d1543e..000000000000 --- a/src/plugins/discover/public/application/view_components/canvas/discover_table_service.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from 'react'; -import { IndexPattern } from '../../../opensearch_dashboards_services'; -import { DiscoverServices } from '../../../build_services'; -import { SavedSearch } from '../../../saved_searches'; -import { useDiscoverTableService } from './utils/use_discover_canvas_service'; -import { DiscoverTableApplication } from './discover_table_app'; - -export interface DiscoverTableAppProps { - services: DiscoverServices; - savedSearch: SavedSearch; - indexPattern: IndexPattern; -} - -export const DiscoverTableService = ({ - services, - savedSearch, - indexPattern, -}: DiscoverTableAppProps) => { - const { data$, refetch$ } = useDiscoverTableService({ - services, - savedSearch, - indexPattern, - }); - - // trigger manual fetch - // TODO: remove this once we implement refetch data: - // Based on the controller, refetch$ should emit next when - // 1) appStateContainer interval and sort change - // 2) savedSearch id changes - // 3) timefilter.getRefreshInterval().pause === false - // 4) TopNavMenu updateQuery() is called - useEffect(() => { - refetch$.next(); - }, [refetch$]); - - return ( - - ); -}; diff --git a/src/plugins/discover/public/application/view_components/canvas/index.tsx b/src/plugins/discover/public/application/view_components/canvas/index.tsx index 34fd6a0bf103..cdfbbfe9b161 100644 --- a/src/plugins/discover/public/application/view_components/canvas/index.tsx +++ b/src/plugins/discover/public/application/view_components/canvas/index.tsx @@ -4,21 +4,20 @@ */ import React from 'react'; +import { TopNav } from './top_nav'; import { ViewProps } from '../../../../../data_explorer/public'; -import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; -import { Canvas } from './canvas'; -import { getServices } from '../../../opensearch_dashboards_services'; +import { DiscoverTable } from './discover_table'; // eslint-disable-next-line import/no-default-export -export default function CanvasApp({ setHeaderActionMenu }: ViewProps) { - const services = getServices(); +export default function DiscoverCanvas({ setHeaderActionMenu, history }: ViewProps) { return ( - - + - + + ); } diff --git a/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts b/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts deleted file mode 100644 index 38d2b18b7b9b..000000000000 --- a/src/plugins/discover/public/application/view_components/canvas/utils/use_discover_canvas_service.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useMemo, useEffect } from 'react'; -import { DiscoverServices } from '../../../../build_services'; -import { SavedSearch } from '../../../../saved_searches'; -import { useSavedSearch } from '../../utils/use_saved_search'; -import { IndexPattern } from '../../../../opensearch_dashboards_services'; - -export interface DiscoverTableServiceProps { - services: DiscoverServices; - savedSearch: SavedSearch; - indexPattern: IndexPattern; -} - -export const useDiscoverTableService = ({ - services, - savedSearch, - indexPattern, -}: DiscoverTableServiceProps) => { - const searchSource = useMemo(() => { - savedSearch.searchSource.setField('index', indexPattern); - return savedSearch.searchSource; - }, [savedSearch, indexPattern]); - - const { data$, refetch$ } = useSavedSearch({ - searchSource, - services, - indexPattern, - }); - - useEffect(() => { - const dataSubscription = data$.subscribe((data) => {}); - const refetchSubscription = refetch$.subscribe((refetch) => {}); - - return () => { - dataSubscription.unsubscribe(); - refetchSubscription.unsubscribe(); - }; - }, [data$, refetch$]); - - return { - data$, - refetch$, - indexPattern, - }; -}; diff --git a/src/plugins/discover/public/application/view_components/context/index.tsx b/src/plugins/discover/public/application/view_components/context/index.tsx new file mode 100644 index 000000000000..29daca731714 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/context/index.tsx @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useEffect } from 'react'; +import { DataExplorerServices, ViewProps } from '../../../../../data_explorer/public'; +import { + OpenSearchDashboardsContextProvider, + useOpenSearchDashboards, +} from '../../../../../opensearch_dashboards_react/public'; +import { getServices } from '../../../opensearch_dashboards_services'; +import { useSearch, SearchContextValue } from '../utils/use_search'; +import { connectStorageToQueryState, opensearchFilters } from '../../../../../data/public'; + +const SearchContext = React.createContext({} as SearchContextValue); + +// eslint-disable-next-line import/no-default-export +export default function DiscoverContext({ children }: React.PropsWithChildren) { + const services = getServices(); + const searchParams = useSearch(services); + + const { + services: { osdUrlStateStorage }, + } = useOpenSearchDashboards(); + + // Connect the query service to the url state + useEffect(() => { + connectStorageToQueryState(services.data.query, osdUrlStateStorage, { + filters: opensearchFilters.FilterStateStore.APP_STATE, + query: true, + }); + }, [osdUrlStateStorage, services.data.query, services.uiSettings]); + + return ( + + {children} + + ); +} + +export const useDiscoverContext = () => React.useContext(SearchContext); diff --git a/src/plugins/discover/public/application/view_components/panel/index.tsx b/src/plugins/discover/public/application/view_components/panel/index.tsx index c05807d3a63a..f0c97eb56d02 100644 --- a/src/plugins/discover/public/application/view_components/panel/index.tsx +++ b/src/plugins/discover/public/application/view_components/panel/index.tsx @@ -3,18 +3,65 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { ViewProps } from '../../../../../data_explorer/public'; -import { OpenSearchDashboardsContextProvider } from '../../../../../opensearch_dashboards_react/public'; -import { Panel } from './panel'; -import { getServices } from '../../../opensearch_dashboards_services'; +import { + addColumn, + removeColumn, + reorderColumn, + useDispatch, + useSelector, +} from '../../utils/state_management'; +import { DiscoverSidebar } from '../../components/sidebar'; +import { useDiscoverContext } from '../context'; +import { SearchData } from '../utils/use_search'; // eslint-disable-next-line import/no-default-export -export default function PanelApp(props: ViewProps) { - const services = getServices(); +export default function DiscoverPanel(props: ViewProps) { + const { data$, indexPattern } = useDiscoverContext(); + const [fetchState, setFetchState] = useState(data$.getValue()); + + const { columns } = useSelector((state) => ({ + columns: state.discover.columns, + indexPatternId: state.metadata.indexPattern, + })); + const dispatch = useDispatch(); + + useEffect(() => { + const subscription = data$.subscribe((next) => { + setFetchState(next); + }); + return () => { + subscription.unsubscribe(); + }; + }, [data$, fetchState]); + return ( - - - + { + dispatch( + addColumn({ + column: fieldName, + index, + }) + ); + }} + onRemoveField={(fieldName) => { + dispatch(removeColumn(fieldName)); + }} + onReorderFields={(source, destination) => { + dispatch( + reorderColumn({ + source, + destination, + }) + ); + }} + selectedIndexPattern={indexPattern} + onAddFilter={() => {}} + /> ); } diff --git a/src/plugins/discover/public/application/view_components/panel/panel.tsx b/src/plugins/discover/public/application/view_components/panel/panel.tsx deleted file mode 100644 index e83128695695..000000000000 --- a/src/plugins/discover/public/application/view_components/panel/panel.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect, useState } from 'react'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { - addColumn, - removeColumn, - reorderColumn, - useDispatch, - useSelector, -} from '../../utils/state_management'; -import { DiscoverServices } from '../../../build_services'; -import { DiscoverSidebar } from '../../components/sidebar'; -import { IndexPattern } from '../../../opensearch_dashboards_services'; - -export const Panel = () => { - const { indexPatternId, columns } = useSelector((state) => ({ - columns: state.discover.columns, - indexPatternId: state.metadata.indexPattern, - })); - const dispatch = useDispatch(); - const [indexPattern, setIndexPattern] = useState(); - - const { services } = useOpenSearchDashboards(); - - useEffect(() => { - const fetchIndexPattern = async () => { - const currentIndexPattern = await services.data.indexPatterns.get(indexPatternId || ''); - setIndexPattern(currentIndexPattern); - }; - fetchIndexPattern(); - }, [indexPatternId, services.data.indexPatterns]); - - return ( - ({ ...acc, [field.name]: 1 }), - {} as Record - ) || {} - } - hits={[]} - onAddField={(fieldName, index) => { - dispatch( - addColumn({ - column: fieldName, - index, - }) - ); - }} - onRemoveField={(fieldName) => { - dispatch(removeColumn(fieldName)); - }} - onReorderFields={(source, destination) => { - dispatch( - reorderColumn({ - source, - destination, - }) - ); - }} - selectedIndexPattern={indexPattern} - onAddFilter={() => {}} - /> - ); -}; diff --git a/src/plugins/discover/public/application/view_components/utils/create_search_source.ts b/src/plugins/discover/public/application/view_components/utils/create_search_source.ts new file mode 100644 index 000000000000..d03c604c5915 --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/create_search_source.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { IndexPattern } from 'src/plugins/data/public'; +import { DiscoverServices } from '../../../build_services'; +import { SortOrder } from '../../../saved_searches/types'; +import { getSortForSearchSource } from './get_sort_for_search_source'; +import { SORT_DEFAULT_ORDER_SETTING, SAMPLE_SIZE_SETTING } from '../../../../common'; + +interface Props { + indexPattern: IndexPattern; + services: DiscoverServices; + sort: SortOrder[] | undefined; +} + +export const createSearchSource = async ({ indexPattern, services, sort }: Props) => { + const { uiSettings, data } = services; + const sortForSearchSource = getSortForSearchSource( + sort, + indexPattern, + uiSettings.get(SORT_DEFAULT_ORDER_SETTING) + ); + const size = uiSettings.get(SAMPLE_SIZE_SETTING); + const filters = data.query.filterManager.getFilters(); + const searchSource = await data.search.searchSource.create({ + index: indexPattern, + sort: sortForSearchSource, + size, + query: data.query.queryString.getQuery() || null, + highlightAll: true, + version: true, + }); + + // Add time filter + const timefilter = data.query.timefilter.timefilter; + const timeRangeFilter = timefilter.createFilter(indexPattern); + if (timeRangeFilter) { + filters.push(timeRangeFilter); + } + searchSource.setField('filter', filters); + + return searchSource; +}; diff --git a/src/plugins/discover/public/application/view_components/utils/update_data_source.ts b/src/plugins/discover/public/application/view_components/utils/update_data_source.ts deleted file mode 100644 index 00ab963e8863..000000000000 --- a/src/plugins/discover/public/application/view_components/utils/update_data_source.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ISearchSource, IndexPattern } from 'src/plugins/data/public'; -import { DiscoverServices } from '../../../build_services'; -import { SortOrder } from '../../../saved_searches/types'; -import { getSortForSearchSource } from './get_sort_for_search_source'; -import { SORT_DEFAULT_ORDER_SETTING, SAMPLE_SIZE_SETTING } from '../../../../common'; - -export interface UpdateDataSourceProps { - searchSource: ISearchSource; - indexPattern: IndexPattern; - services: DiscoverServices; - sort: SortOrder[] | undefined; -} - -export const updateDataSource = ({ - searchSource, - indexPattern, - services, - sort, -}: UpdateDataSourceProps) => { - const { uiSettings, data } = services; - const sortForSearchSource = getSortForSearchSource( - sort, - indexPattern, - uiSettings.get(SORT_DEFAULT_ORDER_SETTING) - ); - const size = uiSettings.get(SAMPLE_SIZE_SETTING); - const updatedSearchSource = searchSource - .setField('index', indexPattern) - .setField('sort', sortForSearchSource) - .setField('size', size) - .setField('query', data.query.queryString.getQuery() || null) - .setField('filter', data.query.filterManager.getFilters()) - .setField('highlightAll', true) - .setField('version', true); - - return updatedSearchSource; -}; diff --git a/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts new file mode 100644 index 000000000000..fa8e5d5e884b --- /dev/null +++ b/src/plugins/discover/public/application/view_components/utils/use_index_pattern.ts @@ -0,0 +1,44 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; +import { IndexPattern } from '../../../../../data/public'; +import { useSelector } from '../../utils/state_management'; +import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; +import { DiscoverViewServices } from '../../../build_services'; + +export const useIndexPattern = () => { + const indexPatternId = useSelector((state) => state.metadata.indexPattern); + const [indexPattern, setIndexPattern] = useState(undefined); + + const { + services: { data, toastNotifications }, + } = useOpenSearchDashboards(); + + useEffect(() => { + if (!indexPatternId) return; + const indexPatternMissingWarning = i18n.translate( + 'discover.valueIsNotConfiguredIndexPatternIDWarningTitle', + { + defaultMessage: '{id} is not a configured index pattern ID', + values: { + id: `"${indexPatternId}"`, + }, + } + ); + + data.indexPatterns + .get(indexPatternId) + .then(setIndexPattern) + .catch(() => { + toastNotifications.addDanger({ + title: indexPatternMissingWarning, + }); + }); + }, [indexPatternId, data.indexPatterns, toastNotifications]); + + return indexPattern; +}; diff --git a/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts similarity index 62% rename from src/plugins/discover/public/application/view_components/utils/use_saved_search.ts rename to src/plugins/discover/public/application/view_components/utils/use_search.ts index 61d802d45140..5726abf70d82 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_saved_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -4,14 +4,15 @@ */ import { useCallback, useMemo, useRef } from 'react'; -import { ISearchSource, IndexPattern } from 'src/plugins/data/public'; import { BehaviorSubject, Subject, merge } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { useEffect } from 'react'; import { DiscoverServices } from '../../../build_services'; -import { validateTimeRange } from '../../../application/helpers/validate_time_range'; -import { updateDataSource } from './update_data_source'; +import { validateTimeRange } from '../../helpers/validate_time_range'; +import { createSearchSource } from './create_search_source'; +import { useIndexPattern } from './use_index_pattern'; +import { OpenSearchSearchHit } from '../../doc_views/doc_views_types'; export enum FetchStatus { UNINITIALIZED = 'uninitialized', @@ -20,29 +21,34 @@ export enum FetchStatus { ERROR = 'error', } -export interface SavedSearchData { +export interface SearchData { status: FetchStatus; fetchCounter?: number; fieldCounts?: Record; fetchError?: Error; hits?: number; - rows?: any[]; // TODO: type + rows?: OpenSearchSearchHit[]; } -export type SavedSearchRefetch = 'refetch' | undefined; +export type SearchRefetch = 'refetch' | undefined; -export type DataSubject = BehaviorSubject; -export type RefetchSubject = BehaviorSubject; +export type DataSubject = BehaviorSubject; +export type RefetchSubject = Subject; -export const useSavedSearch = ({ - indexPattern, - searchSource, - services, -}: { - indexPattern: IndexPattern; - searchSource: ISearchSource; - services: DiscoverServices; -}) => { +/** + * A hook that provides functionality for fetching and managing discover search data. + * @returns { data: DataSubject, refetch$: RefetchSubject, indexPattern: IndexPattern } - data is a BehaviorSubject that emits the current search data, refetch$ is a Subject that can be used to trigger a refetch. + * @example + * const { data$, refetch$ } = useSearch(); + * useEffect(() => { + * const subscription = data$.subscribe((d) => { + * // do something with the data + * }); + * return () => subscription.unsubscribe(); + * }, [data$]); + */ +export const useSearch = (services: DiscoverServices) => { + const indexPattern = useIndexPattern(); const { data, filterManager } = services; const timefilter = data.query.timefilter.timefilter; const fetchStateRef = useRef<{ @@ -56,23 +62,23 @@ export const useSavedSearch = ({ }); const data$ = useMemo( - () => new BehaviorSubject({ state: FetchStatus.UNINITIALIZED }), + () => new BehaviorSubject({ status: FetchStatus.UNINITIALIZED }), [] ); - const refetch$ = useMemo(() => new Subject(), []); + const refetch$ = useMemo(() => new Subject(), []); const fetch = useCallback(async () => { - if (!validateTimeRange(timefilter.getTime(), services.toastNotifications)) { + if (!validateTimeRange(timefilter.getTime(), services.toastNotifications) || !indexPattern) { return Promise.reject(); } if (fetchStateRef.current.abortController) fetchStateRef.current.abortController.abort(); fetchStateRef.current.abortController = new AbortController(); const sort = undefined; - const updatedSearchSource = updateDataSource({ searchSource, indexPattern, services, sort }); + const searchSource = await createSearchSource({ indexPattern, services, sort }); try { - const fetchResp = await updatedSearchSource.fetch({ + const fetchResp = await searchSource.fetch({ abortSignal: fetchStateRef.current.abortController.signal, }); const hits = fetchResp.hits.total as number; @@ -95,13 +101,14 @@ export const useSavedSearch = ({ } catch (err) { // TODO: handle the error } - }, [data$, timefilter, services, searchSource, indexPattern, fetchStateRef]); + }, [data$, timefilter, services, indexPattern]); useEffect(() => { const fetch$ = merge( refetch$, filterManager.getFetches$(), timefilter.getFetch$(), + timefilter.getTimeUpdate$(), timefilter.getAutoRefreshFetch$(), data.query.queryString.getUpdates$() ).pipe(debounceTime(100)); @@ -113,12 +120,15 @@ export const useSavedSearch = ({ } catch (error) { data$.next({ status: FetchStatus.ERROR, - fetchError: error, + fetchError: error as Error, }); } })(); }); + // kick off initial fetch + refetch$.next(); + return () => { subscription.unsubscribe(); }; @@ -127,5 +137,8 @@ export const useSavedSearch = ({ return { data$, refetch$, + indexPattern, }; }; + +export type SearchContextValue = ReturnType; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index a82fe34bfb13..ebe4e80a70c5 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -126,4 +126,5 @@ export function buildServices( }; } +// Any component inside the panel and canvas views has access to both these services. export type DiscoverViewServices = DiscoverServices & DataExplorerServices; diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 692e53f6fe1d..8a581a3d246f 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -336,6 +336,7 @@ export class DiscoverPlugin // ViewComponent Canvas: lazy(() => import('./application/view_components/canvas')), Panel: lazy(() => import('./application/view_components/panel')), + Context: lazy(() => import('./application/view_components/context')), }); // this.registerEmbeddable(core, plugins); From 3376dae6ed5b0e2b22abc23e6365090add240f8c Mon Sep 17 00:00:00 2001 From: Ashwin P Chandran Date: Fri, 4 Aug 2023 01:12:58 +0000 Subject: [PATCH 16/16] fixes unit tests Signed-off-by: Ashwin P Chandran --- .../doc_viewer_links.test.tsx.snap | 4 + .../discover_index_pattern.test.tsx.snap | 3 - .../sidebar/discover_field.test.tsx | 44 +++++---- .../components/sidebar/discover_field.tsx | 98 ++++++++----------- .../sidebar/discover_field_search.test.tsx | 14 +-- .../sidebar/discover_field_search.tsx | 7 +- .../sidebar/discover_sidebar.test.tsx | 64 ++++++------ .../components/sidebar/discover_sidebar.tsx | 15 ++- .../view_components/panel/index.tsx | 1 - .../public/helpers/find_test_subject.ts | 4 +- src/test_utils/public/testing_lib_helpers.tsx | 22 +++++ 11 files changed, 146 insertions(+), 130 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/sidebar/__snapshots__/discover_index_pattern.test.tsx.snap create mode 100644 src/test_utils/public/testing_lib_helpers.tsx diff --git a/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap index 95fb0c377180..2a3b5a3aa998 100644 --- a/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap +++ b/src/plugins/discover/public/application/components/doc_viewer_links/__snapshots__/doc_viewer_links.test.tsx.snap @@ -3,14 +3,17 @@ exports[`Dont Render if generateCb.hide 1`] = ` `; exports[`Render with 2 different links 1`] = ` with 2 different links 1`] = ` /> ({ }), })); -function getComponent({ +function getProps({ selected = false, showDetails = false, useShortDots = false, @@ -110,24 +108,33 @@ function getComponent({ selected, useShortDots, }; - const comp = mountWithIntl(); - return { comp, props }; + + return props; } describe('discover sidebar field', function () { - it('should allow selecting fields', function () { - const { comp, props } = getComponent({}); - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow selecting fields', async function () { + const props = getProps({}); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-bytes')); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - const { comp, props } = getComponent({ selected: true }); - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow deselecting fields', async function () { + const props = getProps({ selected: true }); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-bytes')); + expect(props.onRemoveField).toHaveBeenCalledWith('bytes'); }); - it('should trigger getDetails', function () { - const { comp, props } = getComponent({ selected: true }); - findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + it('should trigger getDetails', async function () { + const props = getProps({ selected: true }); + render(); + + await fireEvent.click(screen.getByTestId('field-bytes-showDetails')); + expect(props.getDetails).toHaveBeenCalledWith(props.field); }); it('should not allow clicking on _source', function () { @@ -142,11 +149,12 @@ describe('discover sidebar field', function () { }, '_source' ); - const { comp, props } = getComponent({ + const props = getProps({ selected: true, field, }); - findTestSubject(comp, 'field-_source-showDetails').simulate('click'); - expect(props.getDetails).not.toHaveBeenCalled(); + render(); + + expect(screen.queryByTestId('field-_source-showDetails')).toBeNull(); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx index 1f2e386780ec..98789598713c 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field.tsx @@ -113,6 +113,7 @@ export const DiscoverField = ({ defaultMessage: 'View {field} summary', values: { field: field.name }, }); + const isSourceField = field.name === '_source'; const [infoIsOpen, setOpen] = useState(false); @@ -142,7 +143,7 @@ export const DiscoverField = ({ ); let actionButton; - if (field.name !== '_source' && !selected) { + if (!isSourceField && !selected) { actionButton = ( ); - } else if (field.name !== '_source' && selected) { + } else if (!isSourceField && selected) { actionButton = ( - } - fieldAction={actionButton} - fieldName={fieldName} - /> - ); - } - return ( @@ -225,41 +206,44 @@ export const DiscoverField = ({ {fieldName} - - setOpen(false)} - anchorPosition="rightUp" - button={ - setOpen((state) => !state)} - aria-label={infoLabelAria} - /> - } - panelClassName="dscSidebarItem__fieldPopoverPanel" - > - - {' '} - {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { - defaultMessage: 'Top 5 values', - })} - - {infoIsOpen && ( - - )} - - - {actionButton} + {!isSourceField && ( + + setOpen(false)} + anchorPosition="rightUp" + button={ + setOpen((state) => !state)} + aria-label={infoLabelAria} + data-test-subj={`field-${field.name}-showDetails`} + /> + } + panelClassName="dscSidebarItem__fieldPopoverPanel" + > + + {' '} + {i18n.translate('discover.fieldChooser.discoverField.fieldTopValuesLabel', { + defaultMessage: 'Top 5 values', + })} + + {infoIsOpen && ( + + )} + + + )} + {!isSourceField && {actionButton}} ); }; diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx index f78505e11f1e..bcf72ae57326 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.test.tsx @@ -32,7 +32,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/helpers'; -import { DiscoverFieldSearch, Props } from './discover_field_search'; +import { DiscoverFieldSearch, NUM_FILTERS, Props } from './discover_field_search'; import { EuiButtonGroupProps, EuiPopover } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; @@ -63,7 +63,7 @@ describe('DiscoverFieldSearch', () => { const onChange = jest.fn(); const component = mountComponent({ ...defaultProps, ...{ onChange } }); let btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBeFalsy(); + expect(btn.hasClass('euiFilterButton-hasActiveFilters')).toBeFalsy(); btn.simulate('click'); const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { @@ -72,7 +72,7 @@ describe('DiscoverFieldSearch', () => { }); component.update(); btn = findTestSubject(component, 'toggleFieldFilterButton'); - expect(btn.hasClass('euiFacetButton--isSelected')).toBe(true); + expect(btn.hasClass('euiFilterButton-hasActiveFilters')).toBe(true); expect(onChange).toBeCalledWith('aggregatable', true); }); @@ -82,8 +82,8 @@ describe('DiscoverFieldSearch', () => { btn.simulate('click'); btn = findTestSubject(component, 'toggleFieldFilterButton'); const badge = btn.find('.euiNotificationBadge'); - // no active filters - expect(badge.text()).toEqual('0'); + // available filters + expect(badge.text()).toEqual(NUM_FILTERS.toString()); // change value of aggregatable select const aggregatableButtonGroup = findButtonGroup(component, 'aggregatable'); act(() => { @@ -114,10 +114,10 @@ describe('DiscoverFieldSearch', () => { const btn = findTestSubject(component, 'toggleFieldFilterButton'); btn.simulate('click'); const badge = btn.find('.euiNotificationBadge'); - expect(badge.text()).toEqual('0'); + expect(badge.text()).toEqual(NUM_FILTERS.toString()); const missingSwitch = findTestSubject(component, 'missingSwitch'); missingSwitch.simulate('change', { target: { value: false } }); - expect(badge.text()).toEqual('0'); + expect(badge.text()).toEqual(NUM_FILTERS.toString()); }); test('change in filters triggers onChange', () => { diff --git a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx index 556dc861a9b3..8d90e0ae1099 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_field_search.tsx @@ -50,6 +50,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@osd/i18n/react'; +export const NUM_FILTERS = 3; + export interface State { searchable: string; aggregatable: string; @@ -214,7 +216,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { legend={legend} options={toggleButtons(id)} idSelected={`${id}-${values[id]}`} - onChange={(optionId) => handleValueChange(id, optionId.replace(`${id}-`, ''))} + onChange={(optionId: string) => handleValueChange(id, optionId.replace(`${id}-`, ''))} buttonSize="compressed" isFullWidth data-test-subj={`${id}ButtonGroup`} @@ -245,7 +247,6 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { onChange('name', event.currentTarget.value)} placeholder={searchPlaceholder} @@ -272,7 +273,7 @@ export function DiscoverFieldSearch({ onChange, value, types }: Props) { hasActiveFilters={activeFiltersCount > 0} aria-label={filterBtnAriaLabel} data-test-subj="toggleFieldFilterButton" - numFilters={3} + numFilters={NUM_FILTERS} onClick={handleFacetButtonClicked} numActiveFilters={activeFiltersCount} isSelected={isPopoverOpen} diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx index fa692ca22b5b..6fee8dde6b60 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.test.tsx @@ -29,19 +29,15 @@ */ import _ from 'lodash'; -import { ReactWrapper } from 'enzyme'; -import { findTestSubject } from 'test_utils/helpers'; // @ts-ignore import realHits from 'fixtures/real_hits.js'; // @ts-ignore import stubbedLogstashFields from 'fixtures/logstash_fields'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { render, screen, within, fireEvent } from '@testing-library/react'; import React from 'react'; import { DiscoverSidebar, DiscoverSidebarProps } from './discover_sidebar'; import { coreMock } from '../../../../../../core/public/mocks'; -import { IndexPatternAttributes } from '../../../../../data/common'; import { getStubIndexPattern } from '../../../../../data/public/test_utils'; -import { SavedObject } from '../../../../../../core/types'; jest.mock('../../../opensearch_dashboards_services', () => ({ getServices: () => ({ @@ -74,7 +70,7 @@ jest.mock('./lib/get_index_pattern_field_list', () => ({ getIndexPatternFieldList: jest.fn((indexPattern) => indexPattern.fields), })); -function getCompProps() { +function getCompProps(): DiscoverSidebarProps { const indexPattern = getStubIndexPattern( 'logstash-*', (cfg: any) => cfg, @@ -88,12 +84,6 @@ function getCompProps() { Record >; - const indexPatternList = [ - { id: '0', attributes: { title: 'b' } } as SavedObject, - { id: '1', attributes: { title: 'a' } } as SavedObject, - { id: '2', attributes: { title: 'c' } } as SavedObject, - ]; - const fieldCounts: Record = {}; for (const hit of hits) { @@ -105,44 +95,48 @@ function getCompProps() { columns: ['extension'], fieldCounts, hits, - indexPatternList, onAddFilter: jest.fn(), onAddField: jest.fn(), onRemoveField: jest.fn(), selectedIndexPattern: indexPattern, - setIndexPattern: jest.fn(), - state: {}, + onReorderFields: jest.fn(), }; } describe('discover sidebar', function () { - let props: DiscoverSidebarProps; - let comp: ReactWrapper; + it('should have Selected Fields and Available Fields with Popular Fields sections', async function () { + render(); - beforeAll(() => { - props = getCompProps(); - comp = mountWithIntl(); - }); + const popular = screen.getByTestId('fieldList-popular'); + const selected = screen.getByTestId('fieldList-selected'); + const unpopular = screen.getByTestId('fieldList-unpopular'); - it('should have Selected Fields and Available Fields with Popular Fields sections', function () { - const popular = findTestSubject(comp, 'fieldList-popular'); - const selected = findTestSubject(comp, 'fieldList-selected'); - const unpopular = findTestSubject(comp, 'fieldList-unpopular'); - expect(popular.children().length).toBe(1); - expect(unpopular.children().length).toBe(7); - expect(selected.children().length).toBe(1); + expect(within(popular).getAllByTestId('fieldList-field').length).toBe(1); + expect(within(unpopular).getAllByTestId('fieldList-field').length).toBe(7); + expect(within(selected).getAllByTestId('fieldList-field').length).toBe(1); }); - it('should allow selecting fields', function () { - findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); + it('should allow selecting fields', async function () { + const props = getCompProps(); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-bytes')); + expect(props.onAddField).toHaveBeenCalledWith('bytes'); }); - it('should allow deselecting fields', function () { - findTestSubject(comp, 'fieldToggle-extension').simulate('click'); + it('should allow deselecting fields', async function () { + const props = getCompProps(); + render(); + + await fireEvent.click(screen.getByTestId('fieldToggle-extension')); + expect(props.onRemoveField).toHaveBeenCalledWith('extension'); }); - it('should allow adding filters', function () { - findTestSubject(comp, 'field-extension-showDetails').simulate('click'); - findTestSubject(comp, 'plus-extension-gif').simulate('click'); + it('should allow adding filters', async function () { + const props = getCompProps(); + render(); + + await fireEvent.click(screen.getByTestId('field-extension-showDetails')); + await fireEvent.click(screen.getByTestId('plus-extension-gif')); expect(props.onAddFilter).toHaveBeenCalled(); }); }); diff --git a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx index 4b84a3ed1ce3..b4ed88e02ed9 100644 --- a/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx +++ b/src/plugins/discover/public/application/components/sidebar/discover_sidebar.tsx @@ -212,7 +212,11 @@ export function DiscoverSidebar({ /> - + {selectedFields.map((field: IndexPatternField, index) => { return ( - {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} - {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} - {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} + {/* The panel cannot exist in the DiscoverField component if the on focus highlight during keyboard navigation is needed */} ({ columns: state.discover.columns, - indexPatternId: state.metadata.indexPattern, })); const dispatch = useDispatch(); diff --git a/src/test_utils/public/helpers/find_test_subject.ts b/src/test_utils/public/helpers/find_test_subject.ts index 98687e3f0eef..ccb17b336059 100644 --- a/src/test_utils/public/helpers/find_test_subject.ts +++ b/src/test_utils/public/helpers/find_test_subject.ts @@ -54,8 +54,8 @@ const MATCHERS: Matcher[] = [ * @param testSubjectSelector The data test subject selector * @param matcher optional matcher */ -export const findTestSubject = ( - reactWrapper: ReactWrapper, +export const findTestSubject = ( + reactWrapper: ReactWrapper, testSubjectSelector: T, matcher: Matcher = '~=' ) => { diff --git a/src/test_utils/public/testing_lib_helpers.tsx b/src/test_utils/public/testing_lib_helpers.tsx new file mode 100644 index 000000000000..1e39a0cdcecc --- /dev/null +++ b/src/test_utils/public/testing_lib_helpers.tsx @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { ReactElement } from 'react'; +import { render as rtlRender } from '@testing-library/react'; +import { I18nProvider } from '@osd/i18n/react'; + +// src: https://testing-library.com/docs/example-react-intl/#creating-a-custom-render-function +function render(ui: ReactElement, { ...renderOptions } = {}) { + const Wrapper: React.FC = ({ children }) => { + return {children}; + }; + return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); +} + +// re-export everything +export * from '@testing-library/react'; + +// override render method +export { render };