From e96a1de06069a8e887ac6489f1c9b1ae6ba068d9 Mon Sep 17 00:00:00 2001 From: ananzh Date: Mon, 16 Oct 2023 21:06:19 +0000 Subject: [PATCH] [1][VisBuilder Migration] Add initial setup and migrate state management This PR completes Task 1 and 2 in https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5407. * Reconstruct and allow VisBuilder to be rendered from DataExplorer * Follow proposal task 2 option 1 to migrate state management to DataExplorer Issue Resolve https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5492 https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5493 [2][VisBuilder Migration] Add context and implement side panel * add useVisBuilderContext * modify preloadedState in Data Explorer * implement side panel Issue Resolve: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5522 Signed-off-by: ananzh [3][VisBuilder Migration] Combine components into VisBuilderCanvas Signed-off-by: ananzh --- package.json | 2 +- src/core/public/application/scoped_history.ts | 10 +- src/plugins/data_explorer/public/index.ts | 2 + .../public/services/view_service/types.ts | 12 +- .../public/utils/state_management/preload.ts | 38 +++- .../public/utils/state_management/store.ts | 36 +++- src/plugins/discover/public/plugin.ts | 2 +- src/plugins/vis_builder/common/index.ts | 3 +- .../vis_builder/opensearch_dashboards.json | 3 +- .../vis_builder/public/application/_util.scss | 1 + .../vis_builder/public/application/app.tsx | 84 -------- .../config_panel.scss | 0 .../config_panel.tsx | 21 +- .../{data_tab => config_panel}/dropbox.scss | 0 .../{data_tab => config_panel}/dropbox.tsx | 0 .../schema_to_dropbox.tsx | 0 .../secondary_panel.tsx | 19 +- .../{data_tab => config_panel}/title.tsx | 0 .../{data_tab => config_panel}/use/index.ts | 0 .../use/use_dropbox.tsx | 4 +- .../use/use_prefers_reduced_motion.ts | 0 .../components/data_source_select.tsx | 51 ----- .../application/components/data_tab/index.tsx | 21 -- .../{data_tab => field_selector}/field.scss | 1 + .../field.test.tsx | 0 .../{data_tab => field_selector}/field.tsx | 0 .../field_bucket.scss | 5 + .../field_bucket.tsx | 0 .../field_details.test.tsx | 0 .../field_details.tsx | 5 +- .../field_search.tsx | 0 .../index.scss} | 7 +- .../index.test.tsx} | 2 +- .../index.tsx} | 18 +- .../{data_tab => field_selector}/types.ts | 0 .../utils/field_calculator.test.ts | 0 .../utils/field_calculator.ts | 0 .../utils/get_available_fields.test.ts | 0 .../utils/get_available_fields.ts | 0 .../utils/get_field_details.test.ts | 0 .../utils/get_field_details.ts | 0 .../utils/index.ts | 0 .../application/components/left_nav.tsx | 20 -- .../public/application/components/option.scss | 5 + .../application/components/right_nav.tsx | 10 +- .../components/searchable_dropdown.scss | 39 ---- .../components/searchable_dropdown.tsx | 177 ---------------- .../application/components/side_nav.scss | 17 +- .../application/components/top_nav.scss | 1 + .../public/application/components/top_nav.tsx | 75 ++++--- .../application/components/workspace.tsx | 52 +++-- .../vis_builder/public/application/index.tsx | 40 ---- .../utils/get_top_nav_config.test.tsx | 4 +- .../application/utils/get_top_nav_config.tsx | 29 +-- .../utils/helpers/index_pattern_helper.ts | 71 +++++++ .../public/application/utils/mocks.ts | 16 +- .../public/application/utils/schema.json | 3 - .../utils/state_management/editor_slice.ts | 67 ++++++ .../state_management/handlers/editor_state.ts | 35 +++- .../utils/state_management/handlers/index.ts | 7 + .../state_management/handlers/parent_aggs.ts | 16 +- .../utils/state_management/hooks.ts | 11 - .../utils/state_management/index.ts | 97 ++++++++- .../utils/state_management/metadata_slice.ts | 71 ------- .../utils/state_management/prefix_helper.ts | 81 +++++++ .../utils/state_management/preload.ts | 28 --- .../redux_persistence.test.tsx | 55 ----- .../state_management/redux_persistence.ts | 37 ---- .../utils/state_management/store.ts | 66 ------ .../utils/state_management/style_slice.ts | 19 +- .../utils/state_management/ui_state_slice.ts | 15 +- .../state_management/visualization_slice.ts | 41 ++-- .../public/application/utils/use/index.ts | 2 +- .../public/application/utils/use/use_aggs.ts | 26 +-- .../application/utils/use/use_can_save.ts | 18 +- .../utils/use/use_index_pattern.ts | 62 ++++++ .../utils/use/use_index_pattern.tsx | 52 ----- .../utils/use/use_on_add_filter.ts | 8 +- .../application/utils/use/use_sample_hits.ts | 8 +- .../utils/use/use_saved_vis_builder_vis.ts | 45 ++-- .../utils/use/use_visualization_type.ts | 9 +- .../view_components/canvas/_utils.scss | 13 ++ .../canvas}/_variables.scss | 2 +- .../canvas/canvas.scss} | 5 +- .../view_components/canvas/index.tsx | 73 +++++++ .../view_components/context/index.tsx | 37 ++++ .../view_components/panel/index.tsx | 18 ++ .../panel/panel.scss} | 5 +- .../utils/use_vis_builder_state.ts | 28 +++ .../embeddable/vis_builder_embeddable.tsx | 5 +- .../vis_builder_embeddable_factory.tsx | 2 +- src/plugins/vis_builder/public/extract_id.ts | 22 ++ .../vis_builder/public/migrate_state.ts | 120 +++++++++++ src/plugins/vis_builder/public/plugin.ts | 197 +++++++++--------- .../vis_builder/public/plugin_services.ts | 29 ++- .../public/saved_visualizations/transforms.ts | 23 +- .../public/services/type_service/types.ts | 3 +- .../type_service/visualization_type.tsx | 3 +- src/plugins/vis_builder/public/types.ts | 43 +++- .../common/expression_helpers.ts | 8 +- .../metric/components/metric_viz_options.tsx | 12 +- .../visualizations/metric/to_expression.ts | 7 +- .../table/components/table_viz_options.tsx | 12 +- .../visualizations/table/to_expression.ts | 11 +- .../area/components/area_vis_options.tsx | 11 +- .../vislib/area/to_expression.ts | 8 +- .../components/histogram_vis_options.tsx | 11 +- .../vislib/histogram/to_expression.ts | 8 +- .../line/components/line_vis_options.tsx | 11 +- .../vislib/line/to_expression.ts | 8 +- .../server/capabilities_provider.ts | 2 +- 111 files changed, 1325 insertions(+), 1194 deletions(-) delete mode 100644 src/plugins/vis_builder/public/application/app.tsx rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/config_panel.scss (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/config_panel.tsx (55%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/dropbox.scss (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/dropbox.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/schema_to_dropbox.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/secondary_panel.tsx (90%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/title.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/use/index.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/use/use_dropbox.tsx (97%) rename src/plugins/vis_builder/public/application/components/{data_tab => config_panel}/use/use_prefers_reduced_motion.ts (100%) delete mode 100644 src/plugins/vis_builder/public/application/components/data_source_select.tsx delete mode 100644 src/plugins/vis_builder/public/application/components/data_tab/index.tsx rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field.scss (99%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field.test.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field_bucket.scss (60%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field_bucket.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field_details.test.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field_details.tsx (94%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/field_search.tsx (100%) rename src/plugins/vis_builder/public/application/components/{data_tab/field_selector.scss => field_selector/index.scss} (86%) rename src/plugins/vis_builder/public/application/components/{data_tab/field_selector.test.tsx => field_selector/index.test.tsx} (98%) rename src/plugins/vis_builder/public/application/components/{data_tab/field_selector.tsx => field_selector/index.tsx} (91%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/types.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/field_calculator.test.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/field_calculator.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/get_available_fields.test.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/get_available_fields.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/get_field_details.test.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/get_field_details.ts (100%) rename src/plugins/vis_builder/public/application/components/{data_tab => field_selector}/utils/index.ts (100%) delete mode 100644 src/plugins/vis_builder/public/application/components/left_nav.tsx delete mode 100644 src/plugins/vis_builder/public/application/components/searchable_dropdown.scss delete mode 100644 src/plugins/vis_builder/public/application/components/searchable_dropdown.tsx delete mode 100644 src/plugins/vis_builder/public/application/index.tsx create mode 100644 src/plugins/vis_builder/public/application/utils/helpers/index_pattern_helper.ts create mode 100644 src/plugins/vis_builder/public/application/utils/state_management/editor_slice.ts create mode 100644 src/plugins/vis_builder/public/application/utils/state_management/handlers/index.ts delete mode 100644 src/plugins/vis_builder/public/application/utils/state_management/hooks.ts delete mode 100644 src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts create mode 100644 src/plugins/vis_builder/public/application/utils/state_management/prefix_helper.ts delete mode 100644 src/plugins/vis_builder/public/application/utils/state_management/preload.ts delete mode 100644 src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx delete mode 100644 src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts delete mode 100644 src/plugins/vis_builder/public/application/utils/state_management/store.ts create mode 100644 src/plugins/vis_builder/public/application/utils/use/use_index_pattern.ts delete mode 100644 src/plugins/vis_builder/public/application/utils/use/use_index_pattern.tsx create mode 100644 src/plugins/vis_builder/public/application/view_components/canvas/_utils.scss rename src/plugins/vis_builder/public/application/{ => view_components/canvas}/_variables.scss (91%) rename src/plugins/vis_builder/public/application/{app.scss => view_components/canvas/canvas.scss} (83%) create mode 100644 src/plugins/vis_builder/public/application/view_components/canvas/index.tsx create mode 100644 src/plugins/vis_builder/public/application/view_components/context/index.tsx create mode 100644 src/plugins/vis_builder/public/application/view_components/panel/index.tsx rename src/plugins/vis_builder/public/application/{components/data_tab/index.scss => view_components/panel/panel.scss} (75%) create mode 100644 src/plugins/vis_builder/public/application/view_components/utils/use_vis_builder_state.ts create mode 100644 src/plugins/vis_builder/public/extract_id.ts create mode 100644 src/plugins/vis_builder/public/migrate_state.ts diff --git a/package.json b/package.json index b41c6b834fd9..b05b48c004df 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dashboarding" ], "private": true, - "version": "3.0.0", + "version": "2.10.0", "branch": "main", "types": "./opensearch_dashboards.d.ts", "tsdocMetadata": "./build/tsdoc-metadata.json", diff --git a/src/core/public/application/scoped_history.ts b/src/core/public/application/scoped_history.ts index 487093be191f..c3f6ecca6aba 100644 --- a/src/core/public/application/scoped_history.ts +++ b/src/core/public/application/scoped_history.ts @@ -309,7 +309,10 @@ export class ScopedHistory private setupHistoryListener() { const unlisten = this.parentHistory.listen((location, action) => { // If the user navigates outside the scope of this basePath, tear it down. - if (!location.pathname.startsWith(this.basePath)) { + if ( + !location.pathname.startsWith(this.basePath) && + !this.isPathnameAcceptable(location.pathname) + ) { unlisten(); this.isActive = false; return; @@ -340,4 +343,9 @@ export class ScopedHistory }); }); } + + private isPathnameAcceptable(pathname: string): boolean { + const normalizedPathname = pathname.replace('/data-explorer', ''); + return normalizedPathname.startsWith(this.basePath); + } } diff --git a/src/plugins/data_explorer/public/index.ts b/src/plugins/data_explorer/public/index.ts index cb33d2b7d90c..2fa71eaced27 100644 --- a/src/plugins/data_explorer/public/index.ts +++ b/src/plugins/data_explorer/public/index.ts @@ -15,6 +15,8 @@ export function plugin() { export { DataExplorerPluginSetup, DataExplorerPluginStart, DataExplorerServices } from './types'; export { ViewProps, ViewDefinition, DefaultViewState } from './services/view_service'; export { + AppDispatch, + MetadataState, RootState, Store, useTypedSelector, 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 5ecba7920b63..d8ef71274082 100644 --- a/src/plugins/data_explorer/public/services/view_service/types.ts +++ b/src/plugins/data_explorer/public/services/view_service/types.ts @@ -7,6 +7,7 @@ import { Slice } from '@reduxjs/toolkit'; import { LazyExoticComponent } from 'react'; import { AppMountParameters } from '../../../../../core/public'; import { RootState } from '../../utils/state_management'; +import { Store } from '../../utils/state_management'; interface ViewListItem { id: string; @@ -20,12 +21,19 @@ export interface DefaultViewState { export type ViewProps = AppMountParameters; +type SideEffect = (store: Store, state: T, previousState?: T, services?: T) => void; + export interface ViewDefinition { readonly id: string; readonly title: string; readonly ui?: { - defaults: DefaultViewState | (() => DefaultViewState) | (() => Promise); - slice: Slice; + defaults: + | DefaultViewState + | (() => DefaultViewState) + | (() => Promise) + | (() => Promise>>>); + slices: Array>; + sideEffects?: Array>; }; readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>; 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 fe5c23bd366c..0c3fa1196dbf 100644 --- a/src/plugins/data_explorer/public/utils/state_management/preload.ts +++ b/src/plugins/data_explorer/public/utils/state_management/preload.ts @@ -22,19 +22,39 @@ export const getPreloadedState = async ( return; } - const { defaults } = view.ui; + const { defaults, slices } = view.ui; try { // defaults can be a function or an object const preloadedState = typeof defaults === 'function' ? await defaults() : defaults; - rootState[view.id] = preloadedState.state; - - // if the view wants to override the root state, we do that here - if (preloadedState.root) { - rootState = { - ...rootState, - ...preloadedState.root, - }; + if (Array.isArray(preloadedState)) { + await Promise.all( + preloadedState.map(async (statePromise, index) => { + try { + const state = await statePromise; + const slice = slices[index]; + const prefixedSliceName = + slice.name === view.id ? slice.name : `${view.id}-${slice.name}`; + rootState[prefixedSliceName] = state.state; + } catch (e) { + // eslint-disable-next-line no-console + console.error(`Error initializing slice: ${e}`); + } + }) + ); + } else { + slices.forEach((slice) => { + const prefixedSliceName = + slice.name === view.id ? slice.name : `${view.id}-${slice.name}`; + rootState[prefixedSliceName] = preloadedState.state; + }); + // if the view wants to override the root state, we do that here + if (preloadedState.root) { + rootState = { + ...rootState, + ...preloadedState.root, + }; + } } } catch (e) { // eslint-disable-next-line no-console 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 daf0b3d7e369..9f5f739aed6a 100644 --- a/src/plugins/data_explorer/public/utils/state_management/store.ts +++ b/src/plugins/data_explorer/public/utils/state_management/store.ts @@ -53,11 +53,18 @@ export const configurePreloadedStore = (preloadedState: PreloadedState { // For each view preload the data and register the slice const views = services.viewRegistry.all(); + const viewSideEffectsMap: Record = {}; + views.forEach((view) => { if (!view.ui) return; - const { slice } = view.ui; - registerSlice(slice); + const { slices, sideEffects } = view.ui; + registerSlices(slices, view.id); + + // Save side effects if they exist + if (sideEffects) { + viewSideEffectsMap[view.id] = sideEffects; + } }); const preloadedState = await loadReduxState(services); @@ -72,7 +79,17 @@ export const getPreloadedStore = async (services: DataExplorerServices) => { if (isEqual(state, previousState)) return; - // Add Side effects here to apply after changes to the store are made. None for now. + // Execute view-specific side effects. + Object.entries(viewSideEffectsMap).forEach(([viewId, effects]) => { + effects.forEach((effect) => { + try { + effect(store, state, previousState, services); + } catch (e) { + // eslint-disable-next-line no-console + console.error(`Error executing side effect for view ${viewId}:`, e); + } + }); + }); previousState = state; }; @@ -103,11 +120,14 @@ export const getPreloadedStore = async (services: DataExplorerServices) => { return { store, unsubscribe: onUnsubscribe }; }; -export const registerSlice = (slice: Slice) => { - if (dynamicReducers[slice.name]) { - throw new Error(`Slice ${slice.name} already registered`); - } - dynamicReducers[slice.name] = slice.reducer; +export const registerSlices = (slices: Slice[], id: string) => { + slices.forEach((slice) => { + const prefixedSliceName = slice.name === id ? slice.name : `${id}-${slice.name}`; + if (dynamicReducers[prefixedSliceName]) { + throw new Error(`Slice ${prefixedSliceName} already registered`); + } + dynamicReducers[prefixedSliceName] = slice.reducer; + }); }; // Infer the `RootState` and `AppDispatch` types from the store itself diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index f8e0f254f925..16dc539ac3f5 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -329,7 +329,7 @@ export class DiscoverPlugin const services = getServices(); return await getPreloadedState(services); }, - slice: discoverSlice, + slices: [discoverSlice], }, shouldShow: () => true, // ViewComponent diff --git a/src/plugins/vis_builder/common/index.ts b/src/plugins/vis_builder/common/index.ts index 8e50b07f7dc0..d0bf27be5eac 100644 --- a/src/plugins/vis_builder/common/index.ts +++ b/src/plugins/vis_builder/common/index.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -export const PLUGIN_ID = 'vis-builder'; +// treat PLUGIN_ID as a literal type 'vis-builder' rather than just string +export const PLUGIN_ID = 'vis-builder' as const; export const PLUGIN_NAME = 'VisBuilder'; export const VISUALIZE_ID = 'visualize'; export const EDIT_PATH = '/edit'; diff --git a/src/plugins/vis_builder/opensearch_dashboards.json b/src/plugins/vis_builder/opensearch_dashboards.json index 477deb4db841..2919ea8d8314 100644 --- a/src/plugins/vis_builder/opensearch_dashboards.json +++ b/src/plugins/vis_builder/opensearch_dashboards.json @@ -12,7 +12,8 @@ "navigation", "savedObjects", "visualizations", - "uiActions" + "uiActions", + "dataExplorer" ], "requiredBundles": [ "charts", diff --git a/src/plugins/vis_builder/public/application/_util.scss b/src/plugins/vis_builder/public/application/_util.scss index 165879c2ab12..95d16a338a0c 100644 --- a/src/plugins/vis_builder/public/application/_util.scss +++ b/src/plugins/vis_builder/public/application/_util.scss @@ -2,6 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + @mixin scrollNavParent($template-row: none) { display: grid; min-height: 0; diff --git a/src/plugins/vis_builder/public/application/app.tsx b/src/plugins/vis_builder/public/application/app.tsx deleted file mode 100644 index 9a3367651fc2..000000000000 --- a/src/plugins/vis_builder/public/application/app.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from 'react'; -import { I18nProvider } from '@osd/i18n/react'; -import { EuiPage, EuiResizableContainer } from '@elastic/eui'; -import { useLocation } from 'react-router-dom'; -import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; -import { LeftNav } from './components/left_nav'; -import { TopNav } from './components/top_nav'; -import { Workspace } from './components/workspace'; -import { RightNav } from './components/right_nav'; -import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../types'; -import { syncQueryStateWithUrl } from '../../../data/public'; - -import './app.scss'; - -export const VisBuilderApp = () => { - 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]); - - // Render the application DOM. - return ( - - - - - - - {(EuiResizablePanel, EuiResizableButton) => ( - <> - - - - - - - - - )} - - - - - ); -}; - -export { Option } from './components/option'; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/config_panel.scss b/src/plugins/vis_builder/public/application/components/config_panel/config_panel.scss similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/config_panel.scss rename to src/plugins/vis_builder/public/application/components/config_panel/config_panel.scss diff --git a/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx b/src/plugins/vis_builder/public/application/components/config_panel/config_panel.tsx similarity index 55% rename from src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx rename to src/plugins/vis_builder/public/application/components/config_panel/config_panel.tsx index ec3b6b60a096..5edb3ce82a3b 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx +++ b/src/plugins/vis_builder/public/application/components/config_panel/config_panel.tsx @@ -6,16 +6,17 @@ import { EuiForm } from '@elastic/eui'; import React from 'react'; import { useVisualizationType } from '../../utils/use'; -import { useTypedSelector } from '../../utils/state_management'; -import './config_panel.scss'; import { mapSchemaToAggPanel } from './schema_to_dropbox'; import { SecondaryPanel } from './secondary_panel'; +import './config_panel.scss'; +import '../side_nav.scss'; +import { useVisBuilderContext } from '../../view_components/context'; + export function ConfigPanel() { const vizType = useVisualizationType(); - const editingState = useTypedSelector( - (state) => state.visualization.activeVisualization?.draftAgg - ); + const { rootState } = useVisBuilderContext(); + const editingState = rootState.visualization.activeVisualization?.draftAgg; const schemas = vizType.ui.containerConfig.data.schemas; if (!schemas) return null; @@ -23,9 +24,11 @@ export function ConfigPanel() { const mainPanel = mapSchemaToAggPanel(schemas); return ( - -
{mainPanel}
- -
+
+ +
{mainPanel}
+ +
+
); } diff --git a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.scss b/src/plugins/vis_builder/public/application/components/config_panel/dropbox.scss similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/dropbox.scss rename to src/plugins/vis_builder/public/application/components/config_panel/dropbox.scss diff --git a/src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx b/src/plugins/vis_builder/public/application/components/config_panel/dropbox.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/dropbox.tsx rename to src/plugins/vis_builder/public/application/components/config_panel/dropbox.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/schema_to_dropbox.tsx b/src/plugins/vis_builder/public/application/components/config_panel/schema_to_dropbox.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/schema_to_dropbox.tsx rename to src/plugins/vis_builder/public/application/components/config_panel/schema_to_dropbox.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx b/src/plugins/vis_builder/public/application/components/config_panel/secondary_panel.tsx similarity index 90% rename from src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx rename to src/plugins/vis_builder/public/application/components/config_panel/secondary_panel.tsx index 18a1991f6d80..338152c929ee 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/secondary_panel.tsx +++ b/src/plugins/vis_builder/public/application/components/config_panel/secondary_panel.tsx @@ -8,32 +8,31 @@ import { cloneDeep, get } from 'lodash'; import { useDebounce } from 'react-use'; import { i18n } from '@osd/i18n'; import { EuiCallOut } from '@elastic/eui'; -import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; +import { useTypedDispatch } from '../../utils/state_management'; import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public'; import { Title } from './title'; -import { useIndexPatterns, useVisualizationType } from '../../utils/use'; +import { useVisualizationType } from '../../utils/use'; import { OpenSearchDashboardsContextProvider, useOpenSearchDashboards, } from '../../../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../../../types'; +import { useVisBuilderContext } from '../../view_components/context'; +import { VisBuilderViewServices } from '../../../types'; import { AggParam, IAggType, IFieldParamType } from '../../../../../data/public'; import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_slice'; -import { setError } from '../../utils/state_management/metadata_slice'; +import { setError } from '../../utils/state_management/editor_slice'; import { Storage } from '../../../../../opensearch_dashboards_utils/public'; const PANEL_KEY = 'SECONDARY_PANEL'; export function SecondaryPanel() { - const { draftAgg, aggConfigParams } = useTypedSelector( - (state) => state.visualization.activeVisualization! - ); - const isEditorValid = useTypedSelector((state) => !state.metadata.editor.errors[PANEL_KEY]); + const { indexPattern, rootState } = useVisBuilderContext(); + const { draftAgg, aggConfigParams } = rootState.visualization.activeVisualization!; + const isEditorValid = rootState.editor.errors[PANEL_KEY]; const [touched, setTouched] = useState(false); const dispatch = useTypedDispatch(); const vizType = useVisualizationType(); - const indexPattern = useIndexPatterns().selected; - const { services } = useOpenSearchDashboards(); + const { services } = useOpenSearchDashboards(); const { data: { search: { aggs: aggService }, diff --git a/src/plugins/vis_builder/public/application/components/data_tab/title.tsx b/src/plugins/vis_builder/public/application/components/config_panel/title.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/title.tsx rename to src/plugins/vis_builder/public/application/components/config_panel/title.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/use/index.ts b/src/plugins/vis_builder/public/application/components/config_panel/use/index.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/use/index.ts rename to src/plugins/vis_builder/public/application/components/config_panel/use/index.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx b/src/plugins/vis_builder/public/application/components/config_panel/use/use_dropbox.tsx similarity index 97% rename from src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx rename to src/plugins/vis_builder/public/application/components/config_panel/use/use_dropbox.tsx index c41e4bc08662..91c0152c9603 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/use/use_dropbox.tsx +++ b/src/plugins/vis_builder/public/application/components/config_panel/use/use_dropbox.tsx @@ -19,6 +19,7 @@ import { import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public'; import { VisBuilderServices } from '../../../../types'; import { useAggs } from '../../../utils/use'; +import { useVisBuilderContext } from '../../../view_components/context'; const filterByName = propFilter('name'); const filterByType = propFilter('type'); @@ -30,7 +31,8 @@ export interface UseDropboxProps extends Pick { export const useDropbox = (props: UseDropboxProps): DropboxProps => { const { id: dropboxId, label, schema } = props; const [validAggTypes, setValidAggTypes] = useState([]); - const { aggConfigs, indexPattern, aggs, timeRange } = useAggs(); + const { indexPattern } = useVisBuilderContext(); + const { aggConfigs, aggs, timeRange } = useAggs(); const dispatch = useTypedDispatch(); const { services: { diff --git a/src/plugins/vis_builder/public/application/components/data_tab/use/use_prefers_reduced_motion.ts b/src/plugins/vis_builder/public/application/components/config_panel/use/use_prefers_reduced_motion.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/use/use_prefers_reduced_motion.ts rename to src/plugins/vis_builder/public/application/components/config_panel/use/use_prefers_reduced_motion.ts diff --git a/src/plugins/vis_builder/public/application/components/data_source_select.tsx b/src/plugins/vis_builder/public/application/components/data_source_select.tsx deleted file mode 100644 index 36638e0cb63b..000000000000 --- a/src/plugins/vis_builder/public/application/components/data_source_select.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { i18n } from '@osd/i18n'; -import { EuiIcon } from '@elastic/eui'; -import { SearchableDropdown, SearchableDropdownOption } from './searchable_dropdown'; -import { useIndexPatterns } from '../utils/use'; -import { useTypedDispatch } from '../utils/state_management'; -import { setIndexPattern } from '../utils/state_management/visualization_slice'; -import { IndexPattern } from '../../../../data/public'; - -function indexPatternEquality(A?: SearchableDropdownOption, B?: SearchableDropdownOption): boolean { - return !A || !B ? false : A.id === B.id; -} - -function toSearchableDropdownOption(indexPattern: IndexPattern): SearchableDropdownOption { - return { - id: indexPattern.id || '', - label: indexPattern.title, - searchableLabel: indexPattern.title, - prepend: , - }; -} - -export const DataSourceSelect = () => { - const { indexPatterns, loading, error, selected } = useIndexPatterns(); - const dispatch = useTypedDispatch(); - - // TODO: Should be a standard EUI component - return ( - { - const foundOption = indexPatterns.filter((s) => s.id === option.id)[0]; - if (foundOption !== undefined && typeof foundOption.id === 'string') { - dispatch(setIndexPattern(foundOption.id)); - } - }} - prepend={i18n.translate('visBuilder.nav.dataSource.selector.title', { - defaultMessage: 'Data Source', - })} - error={error} - loading={loading} - options={indexPatterns.map(toSearchableDropdownOption)} - equality={indexPatternEquality} - /> - ); -}; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/index.tsx b/src/plugins/vis_builder/public/application/components/data_tab/index.tsx deleted file mode 100644 index 5f71e38141d3..000000000000 --- a/src/plugins/vis_builder/public/application/components/data_tab/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { FieldSelector } from './field_selector'; - -import './index.scss'; -import { ConfigPanel } from './config_panel'; - -export const DATA_TAB_ID = 'data_tab'; - -export const DataTab = () => { - return ( -
- - -
- ); -}; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.scss b/src/plugins/vis_builder/public/application/components/field_selector/field.scss similarity index 99% rename from src/plugins/vis_builder/public/application/components/data_tab/field.scss rename to src/plugins/vis_builder/public/application/components/field_selector/field.scss index 2c4a96c6ec56..f9ae79de9f69 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field.scss +++ b/src/plugins/vis_builder/public/application/components/field_selector/field.scss @@ -2,6 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + .vbFieldButton { @include euiBottomShadowSmall; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.test.tsx b/src/plugins/vis_builder/public/application/components/field_selector/field.test.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/field.test.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/field.test.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field.tsx b/src/plugins/vis_builder/public/application/components/field_selector/field.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/field.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/field.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.scss b/src/plugins/vis_builder/public/application/components/field_selector/field_bucket.scss similarity index 60% rename from src/plugins/vis_builder/public/application/components/data_tab/field_bucket.scss rename to src/plugins/vis_builder/public/application/components/field_selector/field_bucket.scss index 50951d850a62..4e3acd5d538e 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.scss +++ b/src/plugins/vis_builder/public/application/components/field_selector/field_bucket.scss @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + .vbFieldDetails__barContainer { // Constrains value to the flex item, and allows for truncation when necessary min-width: 0; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx b/src/plugins/vis_builder/public/application/components/field_selector/field_bucket.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/field_bucket.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/field_bucket.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx b/src/plugins/vis_builder/public/application/components/field_selector/field_details.test.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/field_details.test.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/field_details.test.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx b/src/plugins/vis_builder/public/application/components/field_selector/field_details.tsx similarity index 94% rename from src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/field_details.tsx index cf6f4974bb18..40edcee9c17d 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_details.tsx +++ b/src/plugins/vis_builder/public/application/components/field_selector/field_details.tsx @@ -9,7 +9,8 @@ import { i18n } from '@osd/i18n'; import { IndexPatternField } from '../../../../../data/public'; -import { useIndexPatterns, useOnAddFilter } from '../../utils/use'; +import { useOnAddFilter } from '../../utils/use'; +import { useVisBuilderContext } from '../../view_components/context'; import { FieldBucket } from './field_bucket'; import { Bucket, FieldDetails } from './types'; @@ -22,7 +23,7 @@ export function FieldDetailsView({ field, details }: FieldDetailsProps) { const { buckets, error, exists, total } = details; const onAddFilter = useOnAddFilter(); - const indexPattern = useIndexPatterns().selected; + const { indexPattern } = useVisBuilderContext(); const { metaFields = [] } = indexPattern ?? {}; const isMetaField = metaFields.includes(field.name); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_search.tsx b/src/plugins/vis_builder/public/application/components/field_selector/field_search.tsx similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/field_search.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/field_search.tsx diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss b/src/plugins/vis_builder/public/application/components/field_selector/index.scss similarity index 86% rename from src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss rename to src/plugins/vis_builder/public/application/components/field_selector/index.scss index 88cca98db86e..ade14d8d50e3 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.scss +++ b/src/plugins/vis_builder/public/application/components/field_selector/index.scss @@ -2,22 +2,25 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + @import "../../util"; .vbFieldSelector { @include scrollNavParent(auto 1fr); + width: 100%; padding: $euiSizeS; &__fieldGroups { @include euiYScrollWithShadows; overflow-y: auto; - margin-right: -$euiSizeS; + margin-right: 0; padding-right: $euiSizeS; - margin-left: -$euiSizeS; + margin-left: 0; padding-left: $euiSizeS; margin-top: $euiSizeS; + width: 100%; } &__fieldGroup { diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx b/src/plugins/vis_builder/public/application/components/field_selector/index.test.tsx similarity index 98% rename from src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/index.test.tsx index 980cfb50c666..e21e2ddbdb3d 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.test.tsx +++ b/src/plugins/vis_builder/public/application/components/field_selector/index.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { FilterManager, IndexPatternField } from '../../../../../data/public'; -import { FieldGroup } from './field_selector'; +import { FieldGroup } from './index'; const mockUseIndexPatterns = jest.fn(() => ({ selected: 'mockIndexPattern' })); const mockUseOnAddFilter = jest.fn(); diff --git a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx b/src/plugins/vis_builder/public/application/components/field_selector/index.tsx similarity index 91% rename from src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx rename to src/plugins/vis_builder/public/application/components/field_selector/index.tsx index 5c82419d5531..da99feba2e1e 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/vis_builder/public/application/components/field_selector/index.tsx @@ -9,13 +9,13 @@ import { EuiFlexItem, EuiAccordion, EuiNotificationBadge, EuiTitle } from '@elas import { IndexPattern, IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; import { COUNT_FIELD } from '../../utils/drag_drop'; -import { useTypedSelector } from '../../utils/state_management'; -import { useIndexPatterns, useSampleHits } from '../../utils/use'; +import { useVisBuilderContext } from '../../view_components/context'; +import { useSampleHits } from '../../utils/use/use_sample_hits'; import { FieldSearch } from './field_search'; import { Field, DraggableFieldButton } from './field'; import { FieldDetails } from './types'; import { getAvailableFields, getDetails } from './utils'; -import './field_selector.scss'; +import './index.scss'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -24,18 +24,20 @@ interface IFieldCategories { } export const FieldSelector = () => { - const indexPattern = useIndexPatterns().selected; - const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + const { indexPattern, rootState } = useVisBuilderContext(); + const fieldSearchValue = rootState.visualization.searchField; // TODO: instead of a single fetch of sampled hits for all fields, we should just use the agg service to get top hits or terms per field: https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2780 const hits = useSampleHits(); const [filteredFields, setFilteredFields] = useState([]); useEffect(() => { const indexFields = indexPattern?.fields.getAll() ?? []; - const filteredSubset = getAvailableFields(indexFields).filter((field) => + const filteredSubset = getAvailableFields(indexFields).filter((field) => { // case-insensitive field search - field.displayName.toLowerCase().includes(fieldSearchValue.toLowerCase()) - ); + const displayName = field.displayName; + + return displayName.toLowerCase().includes(fieldSearchValue.toLowerCase()); + }); setFilteredFields(filteredSubset); return; diff --git a/src/plugins/vis_builder/public/application/components/data_tab/types.ts b/src/plugins/vis_builder/public/application/components/field_selector/types.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/types.ts rename to src/plugins/vis_builder/public/application/components/field_selector/types.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.test.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/field_calculator.test.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.test.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/field_calculator.test.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/field_calculator.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/field_calculator.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/field_calculator.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.test.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/get_available_fields.test.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.test.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/get_available_fields.test.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/get_available_fields.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/get_available_fields.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/get_available_fields.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.test.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/get_field_details.test.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.test.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/get_field_details.test.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/get_field_details.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/get_field_details.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/get_field_details.ts diff --git a/src/plugins/vis_builder/public/application/components/data_tab/utils/index.ts b/src/plugins/vis_builder/public/application/components/field_selector/utils/index.ts similarity index 100% rename from src/plugins/vis_builder/public/application/components/data_tab/utils/index.ts rename to src/plugins/vis_builder/public/application/components/field_selector/utils/index.ts diff --git a/src/plugins/vis_builder/public/application/components/left_nav.tsx b/src/plugins/vis_builder/public/application/components/left_nav.tsx deleted file mode 100644 index a4aa72bcff06..000000000000 --- a/src/plugins/vis_builder/public/application/components/left_nav.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import './side_nav.scss'; -import { DataSourceSelect } from './data_source_select'; -import { DataTab } from './data_tab'; - -export const LeftNav = () => { - return ( -
-
- -
- -
- ); -}; diff --git a/src/plugins/vis_builder/public/application/components/option.scss b/src/plugins/vis_builder/public/application/components/option.scss index 7410489ad0b7..1e07fd99bf95 100644 --- a/src/plugins/vis_builder/public/application/components/option.scss +++ b/src/plugins/vis_builder/public/application/components/option.scss @@ -1,3 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + .vbOption { background-color: $euiColorEmptyShade; padding: $euiSizeM; diff --git a/src/plugins/vis_builder/public/application/components/right_nav.tsx b/src/plugins/vis_builder/public/application/components/right_nav.tsx index fde4f3110d1c..bb818a77e3ba 100644 --- a/src/plugins/vis_builder/public/application/components/right_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/right_nav.tsx @@ -14,27 +14,29 @@ import { import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import { useVisualizationType } from '../utils/use'; -import './side_nav.scss'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../../types'; +import { VisBuilderViewServices } from '../../types'; import { ActiveVisPayload, setActiveVisualization, useTypedDispatch, useTypedSelector, + getViewSliceFromSelector, } from '../utils/state_management'; import { getPersistedAggParams } from '../utils/get_persisted_agg_params'; +import './side_nav.scss'; + export const RightNavUI = () => { const { ui, name: activeVisName } = useVisualizationType(); const [confirmAggs, setConfirmAggs] = useState(); const { services: { types }, - } = useOpenSearchDashboards(); + } = useOpenSearchDashboards(); const dispatch = useTypedDispatch(); const StyleSection = ui.containerConfig.style.render; - const { activeVisualization } = useTypedSelector((state) => state.visualization); + const { activeVisualization } = useTypedSelector(getViewSliceFromSelector('visualization')); const aggConfigParams = useMemo(() => activeVisualization?.aggConfigParams ?? [], [ activeVisualization, ]); diff --git a/src/plugins/vis_builder/public/application/components/searchable_dropdown.scss b/src/plugins/vis_builder/public/application/components/searchable_dropdown.scss deleted file mode 100644 index 4de43233d1ef..000000000000 --- a/src/plugins/vis_builder/public/application/components/searchable_dropdown.scss +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -@import "../variables"; - -.searchableDropdown { - overflow: "hidden"; - - .euiFormControlLayout__childrenWrapper { - display: flex; - } - - &--topDisplay { - padding-right: $euiSizeL; - font-size: $euiFontSizeS; - flex-grow: 1; - - .euiButtonEmpty__content { - justify-content: flex-start; - } - } - - &--fixedWidthChild { - width: calc(#{$vbLeftNavWidth} - #{$euiSizeXL} * 2); - } - - &--selectableWrapper .euiSelectableList { - // When clicking on the selectable content it will "highlight" itself with a box shadow - // This turns that off - box-shadow: none !important; - margin: ($euiFormControlPadding * -1) - 4; - } - - .euiPopover, - .euiPopover__anchor { - width: 100%; - } -} diff --git a/src/plugins/vis_builder/public/application/components/searchable_dropdown.tsx b/src/plugins/vis_builder/public/application/components/searchable_dropdown.tsx deleted file mode 100644 index 36d926457da3..000000000000 --- a/src/plugins/vis_builder/public/application/components/searchable_dropdown.tsx +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState, useEffect } from 'react'; -import { - EuiLoadingSpinner, - EuiFormControlLayout, - EuiPopoverTitle, - EuiButtonEmpty, - EuiPopover, - EuiSelectable, - EuiTextColor, -} from '@elastic/eui'; -import './searchable_dropdown.scss'; - -export interface SearchableDropdownOption { - id: string; - label: string; - searchableLabel: string; - prepend: any; -} - -interface SearchableDropdownProps { - selected?: SearchableDropdownOption; - onChange: (selection) => void; - options: SearchableDropdownOption[]; - loading: boolean; - error?: Error; - prepend: string; - // not just the first time! - onOpen?: () => void; - equality: (A, B) => boolean; -} - -type DisplayError = any; - -function displayError(error: DisplayError) { - return typeof error === 'object' ? error.toString() : <>{error}; -} - -export const SearchableDropdown = ({ - onChange, - equality, - selected, - options, - error, - loading, - prepend, - onOpen, -}: SearchableDropdownProps) => { - const [localOptions, setLocalOptions] = useState(undefined); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const onButtonClick = () => { - if (!isPopoverOpen && typeof onOpen === 'function') { - onOpen(); - } - setIsPopoverOpen(!isPopoverOpen); - }; - const closePopover = () => setIsPopoverOpen(false); - - function selectNewOption(newOptions) { - // alright, the EUI Selectable is pretty ratchet - // this is as smarmy as it is because it needs to be - - // first go through and count all the "checked" options - const selectedCount = newOptions.filter((o) => o.checked === 'on').length; - - // if the count is 0, the user just "unchecked" our selection and we can just do nothing - if (selectedCount === 0) { - setIsPopoverOpen(false); - return; - } - - // then, if there's more than two selections, the Selectable left the previous selection as "checked" - // so we need to go and "uncheck" it - for (let i = 0; i < newOptions.length; i++) { - if (equality(newOptions[i], selected) && selectedCount > 1) { - delete newOptions[i].checked; - } - } - - // finally, we can pick the checked option as the actual selection - const newSelection = newOptions.filter((o) => o.checked === 'on')[0]; - - setLocalOptions(newOptions); - setIsPopoverOpen(false); - onChange(newSelection); - } - - useEffect(() => { - setLocalOptions( - options.map((o) => ({ - ...o, - checked: equality(o, selected) ? 'on' : undefined, - })) - ); - }, [selected, options, equality]); - - const listDisplay = (list, search) => - loading ? ( -
- -
- ) : error !== undefined ? ( - displayError(error) - ) : ( - <> - - {search} - - {list} - - ); - - const selectable = ( -
- - {listDisplay} - -
- ); - - const selectedText = - selected === undefined ? ( - {loading ? 'Loading' : 'Select an option'} - ) : ( - <> - {selected.prepend} {selected.label} - - ); - - const selectedView = ( - - {selectedText} - - ); - - const formControl = ( - - {selectedView} - - ); - - return ( -
- -
{selectable}
-
-
- ); -}; diff --git a/src/plugins/vis_builder/public/application/components/side_nav.scss b/src/plugins/vis_builder/public/application/components/side_nav.scss index 1a69071a697e..78afe6f88313 100644 --- a/src/plugins/vis_builder/public/application/components/side_nav.scss +++ b/src/plugins/vis_builder/public/application/components/side_nav.scss @@ -2,13 +2,14 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -@import "../util"; -@import "../variables"; + +@import "../view_components/canvas/utils"; +@import "../view_components/canvas/variables"; .vbSidenav { @include scrollNavParent(auto 1fr); - &.left { + &.config { border-right: $euiBorderThin; grid-area: leftNav; } @@ -19,12 +20,6 @@ height: 100%; } - &__header { - padding: $euiSizeS; - border-bottom: $euiBorderThin; - background-color: $euiColorEmptyShade; - } - &__style { @include euiYScrollWithShadows; } @@ -45,7 +40,3 @@ @include scrollNavParent; } } - -.vbDatasourceSelect { - max-width: calc(#{$vbLeftNavWidth} - 1px); -} diff --git a/src/plugins/vis_builder/public/application/components/top_nav.scss b/src/plugins/vis_builder/public/application/components/top_nav.scss index 36349ed8cdba..b306a2e1ba96 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.scss +++ b/src/plugins/vis_builder/public/application/components/top_nav.scss @@ -2,6 +2,7 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + .vbTopNav { grid-area: topNav; border-bottom: $euiBorderThin; diff --git a/src/plugins/vis_builder/public/application/components/top_nav.tsx b/src/plugins/vis_builder/public/application/components/top_nav.tsx index 768f2db35465..5aaa3087ab00 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx @@ -3,79 +3,74 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import React, { useEffect, useState, useCallback } from 'react'; import { useUnmount } from 'react-use'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getTopNavConfig } from '../utils/get_top_nav_config'; -import { VisBuilderServices } from '../../types'; +import { VisBuilderViewServices } from '../../types'; import './top_nav.scss'; -import { useIndexPatterns, useSavedVisBuilderVis } from '../utils/use'; -import { useTypedSelector, useTypedDispatch } from '../utils/state_management'; -import { setEditorState } from '../utils/state_management/metadata_slice'; +import { useTypedDispatch } from '../utils/state_management'; +import { setStatus } from '../utils/state_management/editor_slice'; import { useCanSave } from '../utils/use/use_can_save'; import { saveStateToSavedObject } from '../../saved_visualizations/transforms'; import { TopNavMenuData } from '../../../../navigation/public'; import { opensearchFilters, connectStorageToQueryState } from '../../../../data/public'; +import { AppMountParameters } from '../../../../../core/public'; +import { useVisBuilderContext } from '../view_components/context'; + +export const TopNav = ({ + setHeaderActionMenu, +}: { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +}) => { + const { services } = useOpenSearchDashboards(); -export const TopNav = () => { - // id will only be set for the edit route - const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); - const { services } = useOpenSearchDashboards(); const { - setHeaderActionMenu, navigation: { ui: { TopNavMenu }, }, appName, } = services; - const rootState = useTypedSelector((state) => state); + const { indexPattern, rootState, savedVisBuilderId, savedVisBuilderVis } = useVisBuilderContext(); const dispatch = useTypedDispatch(); const saveDisabledReason = useCanSave(); - const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl); connectStorageToQueryState(services.data.query, services.osdUrlStateStorage, { filters: opensearchFilters.FilterStateStore.APP_STATE, query: true, }); - const { selected: indexPattern } = useIndexPatterns(); const [config, setConfig] = useState(); - const originatingApp = useTypedSelector((state) => { - return state.metadata.originatingApp; - }); - - useEffect(() => { - const getConfig = () => { - if (!savedVisBuilderVis || !indexPattern) return; + const getConfig = useCallback(() => { + if (!savedVisBuilderVis || !indexPattern) return; - return getTopNavConfig( - { - visualizationIdFromUrl, - savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern), - saveDisabledReason, - dispatch, - originatingApp, - }, - services - ); - }; - - setConfig(getConfig()); + return getTopNavConfig( + { + visualizationIdFromUrl: savedVisBuilderId, + savedVisBuilderVis: saveStateToSavedObject(savedVisBuilderVis, rootState, indexPattern), + saveDisabledReason, + dispatch, + originatingApp: rootState.metadata.originatingApp, + }, + services + ); }, [ - rootState, savedVisBuilderVis, - services, - visualizationIdFromUrl, + rootState, + indexPattern, saveDisabledReason, dispatch, - indexPattern, - originatingApp, + services, + savedVisBuilderId, ]); + useEffect(() => { + setConfig(getConfig()); + }, [getConfig]); + // reset validity before component destroyed useUnmount(() => { - dispatch(setEditorState({ state: 'loading' })); + dispatch(setStatus({ status: 'loading' })); }); return ( diff --git a/src/plugins/vis_builder/public/application/components/workspace.tsx b/src/plugins/vis_builder/public/application/components/workspace.tsx index 31880e93bb7f..eaaa33b6fcc9 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.tsx +++ b/src/plugins/vis_builder/public/application/components/workspace.tsx @@ -8,11 +8,12 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiPanel } from '@e import React, { useState, useMemo, useEffect, useLayoutEffect } from 'react'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { IExpressionLoaderParams } from '../../../../expressions/public'; -import { VisBuilderServices } from '../../types'; +import { VisBuilderViewServices } from '../../types'; import { validateSchemaState, validateAggregations } from '../utils/validations'; -import { useTypedDispatch, useTypedSelector, setUIStateState } from '../utils/state_management'; +import { useTypedDispatch, setUIStateState } from '../utils/state_management'; import { useAggs, useVisualizationType } from '../utils/use'; import { PersistedState } from '../../../../visualizations/public'; +import { useVisBuilderContext } from '../view_components/context'; import hand_field from '../../assets/hand_field.svg'; import fields_bg from '../../assets/fields_bg.svg'; @@ -22,42 +23,42 @@ import { ExperimentalInfo } from './experimental_info'; import { handleVisEvent } from '../utils/handle_vis_event'; export const WorkspaceUI = () => { + const { services } = useOpenSearchDashboards(); const { - services: { - expressions: { ReactExpressionRenderer }, - notifications: { toasts }, - data, - uiActions, - }, - } = useOpenSearchDashboards(); + expressions: { ReactExpressionRenderer }, + notifications: { toasts }, + data, + uiActions, + } = services; const { toExpression, ui } = useVisualizationType(); - const { aggConfigs, indexPattern } = useAggs(); + const { aggConfigs } = useAggs(); const [expression, setExpression] = useState(); const [searchContext, setSearchContext] = useState({ query: data.query.queryString.getQuery(), filters: data.query.filterManager.getFilters(), timeRange: data.query.timefilter.timefilter.getTime(), }); - const rootState = useTypedSelector((state) => state); + const { indexPattern, rootState } = useVisBuilderContext(); const dispatch = useTypedDispatch(); // Visualizations require the uiState object to persist even when the expression changes // eslint-disable-next-line react-hooks/exhaustive-deps - const uiState = useMemo(() => new PersistedState(rootState.ui), []); + const persistedUiState = useMemo(() => new PersistedState(rootState.ui), []); + const indexId = rootState.metadata.indexPattern ? rootState.metadata.indexPattern : ''; useEffect(() => { - if (rootState.metadata.editor.state === 'loaded') { - uiState.setSilent(rootState.ui); + if (rootState.editor.status === 'loaded') { + persistedUiState.setSilent(rootState.ui); } // To update uiState once saved object data is loaded // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rootState.metadata.editor.state, uiState]); + }, [rootState.editor.status, persistedUiState]); useEffect(() => { - uiState.on('change', (args) => { + persistedUiState.on('change', (args) => { // Store changes to UI state - dispatch(setUIStateState(uiState.toJSON())); + dispatch(setUIStateState(persistedUiState.toJSON())); }); - }, [dispatch, uiState]); + }, [dispatch, persistedUiState]); useEffect(() => { async function loadExpression() { @@ -81,13 +82,20 @@ export const WorkspaceUI = () => { return; } - - const exp = await toExpression(rootState, searchContext); + const exp = await toExpression(rootState, indexId, searchContext); setExpression(exp); } loadExpression(); - }, [rootState, toExpression, toasts, ui.containerConfig.data.schemas, searchContext, aggConfigs]); + }, [ + rootState, + toExpression, + toasts, + ui.containerConfig.data.schemas, + searchContext, + aggConfigs, + indexId, + ]); useLayoutEffect(() => { const subscription = data.query.state$.subscribe(({ state }) => { @@ -115,7 +123,7 @@ export const WorkspaceUI = () => { handleVisEvent(event, uiActions, indexPattern?.timeFieldName)} /> ) : ( diff --git a/src/plugins/vis_builder/public/application/index.tsx b/src/plugins/vis_builder/public/application/index.tsx deleted file mode 100644 index 89a67648a7dd..000000000000 --- a/src/plugins/vis_builder/public/application/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Router, Route, Switch } from 'react-router-dom'; -import { Provider as ReduxProvider } from 'react-redux'; -import { Store } from 'redux'; -import { AppMountParameters } from '../../../../core/public'; -import { VisBuilderServices } from '../types'; -import { VisBuilderApp } from './app'; -import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; -import { EDIT_PATH } from '../../common'; - -export const renderApp = ( - { element, history }: AppMountParameters, - services: VisBuilderServices, - store: Store -) => { - ReactDOM.render( - - - - - - - - - - - - - , - element - ); - - return () => ReactDOM.unmountComponentAtNode(element); -}; diff --git a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.test.tsx b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.test.tsx index 353b9d90e1ff..d60c2c51cf29 100644 --- a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.test.tsx +++ b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.test.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { VisBuilderServices } from '../../types'; +import { VisBuilderViewServices } from '../../types'; import { getOnSave } from './get_top_nav_config'; import { createVisBuilderServicesMock } from './mocks'; @@ -12,7 +12,7 @@ describe('getOnSave', () => { let originatingApp: string | undefined; let visualizationIdFromUrl: string; let dispatch: any; - let mockServices: jest.Mocked; + let mockServices: jest.Mocked; let onSaveProps: { newTitle: string; newCopyOnSave: boolean; diff --git a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx index 2a30e1700b43..56aa8baa9d18 100644 --- a/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/vis_builder/public/application/utils/get_top_nav_config.tsx @@ -27,7 +27,7 @@ * specific language governing permissions and limitations * under the License. */ - +import _ from 'lodash'; import React from 'react'; import { i18n } from '@osd/i18n'; import { TopNavMenuData } from '../../../../navigation/public'; @@ -36,13 +36,13 @@ import { SavedObjectSaveOpts, showSaveModal, } from '../../../../saved_objects/public'; -import { VisBuilderServices } from '../..'; -import { VisBuilderSavedObject } from '../../types'; -import { AppDispatch } from './state_management'; +import { VisBuilderViewServices, VisBuilderSavedObject } from '../../types'; import { EDIT_PATH, VISBUILDER_SAVED_OBJECT } from '../../../common'; -import { setEditorState } from './state_management/metadata_slice'; +import { setState } from './state_management/editor_slice'; +import { AppDispatch } from './state_management'; + export interface TopNavConfigParams { - visualizationIdFromUrl: string; + visualizationIdFromUrl: string | undefined; savedVisBuilderVis: VisBuilderSavedObject; saveDisabledReason?: string; dispatch: AppDispatch; @@ -57,7 +57,7 @@ export const getTopNavConfig = ( dispatch, originatingApp, }: TopNavConfigParams, - services: VisBuilderServices + services: VisBuilderViewServices ) => { const { i18n: { Context: I18nContext }, @@ -172,7 +172,12 @@ export const getOnSave = ( returnToOrigin: boolean; newDescription?: string; }) => { - const { embeddable, toastNotifications, application, history } = services; + const { + embeddable, + toastNotifications, + application, + history, + } = services; const stateTransfer = embeddable.getStateTransfer(); if (!savedVisBuilderVis) { @@ -220,12 +225,10 @@ export const getOnSave = ( // Update URL if (id !== visualizationIdFromUrl) { - history.push({ - ...history.location, - pathname: `${EDIT_PATH}/${id}`, - }); + history.push(`${EDIT_PATH}/${id}`); } - dispatch(setEditorState({ state: 'clean' })); + + dispatch(setState({ errors: {}, status: 'clean', savedVisBuilderId: id })); } else { // reset title if save not successful savedVisBuilderVis.title = currentTitle; diff --git a/src/plugins/vis_builder/public/application/utils/helpers/index_pattern_helper.ts b/src/plugins/vis_builder/public/application/utils/helpers/index_pattern_helper.ts new file mode 100644 index 000000000000..8bc75df0a9e7 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/helpers/index_pattern_helper.ts @@ -0,0 +1,71 @@ +/* + * 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 { IIndexPattern } from '../../../../../data/common/index_patterns'; + +export function findIndexPatternById( + indexPatterns: IIndexPattern[], + id: string +): IIndexPattern | undefined { + if (!Array.isArray(indexPatterns) || !id) { + return; + } + return indexPatterns.find((o) => o.id === id); +} + +/** + * Checks if the given defaultIndex exists and returns + * the first available index pattern id if not + */ +export function getFallbackIndexPatternId( + indexPatterns: IIndexPattern[], + defaultIndex: string = '' +): string { + if (defaultIndex && findIndexPatternById(indexPatterns, defaultIndex)) { + return defaultIndex; + } + return !indexPatterns || !indexPatterns.length || !indexPatterns[0].id ? '' : indexPatterns[0].id; +} + +/** + * A given index pattern id is checked for existence and a fallback is provided if it doesn't exist + * The provided defaultIndex is usually configured in Advanced Settings, if it's also invalid + * the first entry of the given list of Indexpatterns is used + */ +export function getIndexPatternId( + id: string = '', + indexPatterns: IIndexPattern[], + defaultIndex: string = '' +): string { + if (!id || !findIndexPatternById(indexPatterns, id)) { + return getFallbackIndexPatternId(indexPatterns, defaultIndex); + } + return id; +} diff --git a/src/plugins/vis_builder/public/application/utils/mocks.ts b/src/plugins/vis_builder/public/application/utils/mocks.ts index 25b9847986de..c47bb700e118 100644 --- a/src/plugins/vis_builder/public/application/utils/mocks.ts +++ b/src/plugins/vis_builder/public/application/utils/mocks.ts @@ -3,14 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ScopedHistory } from '../../../../../core/public'; -import { coreMock, scopedHistoryMock } from '../../../../../core/public/mocks'; +import { coreMock } from '../../../../../core/public/mocks'; import { dataPluginMock } from '../../../../data/public/mocks'; import { embeddablePluginMock } from '../../../../embeddable/public/mocks'; import { expressionsPluginMock } from '../../../../expressions/public/mocks'; import { navigationPluginMock } from '../../../../navigation/public/mocks'; import { createOsdUrlStateStorage } from '../../../../opensearch_dashboards_utils/public'; -import { VisBuilderServices } from '../../types'; +import { VisBuilderViewServices } from '../../types'; export const createVisBuilderServicesMock = () => { const coreStartMock = coreMock.createStart(); @@ -32,15 +31,14 @@ export const createVisBuilderServicesMock = () => { } as any, setHeaderActionMenu: () => {}, applicationMock, - history: { - push: jest.fn(), - location: { pathname: '' }, - }, + // history: { + // push: jest.fn(), + // location: { pathname: '' }, + // }, toastNotifications, i18n: i18nContextMock, data: indexPatternMock, embeddable: embeddableMock, - scopedHistory: (scopedHistoryMock.create() as unknown) as ScopedHistory, osdUrlStateStorage: osdUrlStateStorageMock, types: { all: () => [ @@ -59,5 +57,5 @@ export const createVisBuilderServicesMock = () => { }, }; - return (visBuilderServicesMock as unknown) as jest.Mocked; + return (visBuilderServicesMock as unknown) as jest.Mocked; }; diff --git a/src/plugins/vis_builder/public/application/utils/schema.json b/src/plugins/vis_builder/public/application/utils/schema.json index 7cf8bbc2534f..9e71d8bac898 100644 --- a/src/plugins/vis_builder/public/application/utils/schema.json +++ b/src/plugins/vis_builder/public/application/utils/schema.json @@ -23,9 +23,6 @@ ], "additionalProperties": false }, - "indexPattern": { - "type": "string" - }, "searchField": { "type": "string" } diff --git a/src/plugins/vis_builder/public/application/utils/state_management/editor_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/editor_slice.ts new file mode 100644 index 000000000000..e0ca7c22e9b5 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/editor_slice.ts @@ -0,0 +1,67 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { VisBuilderServices } from '../../../types'; +import { DefaultViewState } from '../../../../../data_explorer/public'; + +/* + * Initial state: default state when opening visBuilder plugin + * Clean state: when viz finished loading and ready to be edited + * Dirty state: when there are changes applied to the viz after it finished loading + */ +export type EditorStatus = 'loading' | 'loaded' | 'clean' | 'dirty'; + +export interface EditorState { + errors: { + // Errors for each section in the editor + [key: string]: boolean; + }; + status: EditorStatus; + savedVisBuilderId: string; +} + +const initialState: EditorState = { + errors: {}, + status: 'loading', + savedVisBuilderId: '', +}; + +export const getPreloadedState = async ( + services: VisBuilderServices +): Promise> => { + const preloadedState: DefaultViewState = { + state: { + ...initialState, + }, + }; + return preloadedState; +}; + +export const slice = createSlice({ + name: 'editor', + initialState, + reducers: { + setError: (state, action: PayloadAction<{ key: string; error: boolean }>) => { + const { key, error } = action.payload; + state.errors[key] = error; + }, + setStatus: (state, action: PayloadAction<{ status: EditorStatus }>) => { + state.status = action.payload.status; + }, + setSavedVisBuilderId(state, action: PayloadAction) { + return { + ...state, + savedVisBuilderId: action.payload, + }; + }, + setState: (_state, action: PayloadAction) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setError, setStatus, setSavedVisBuilderId, setState } = slice.actions; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts b/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts index 279a6cf43687..49ae7f6f6375 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/handlers/editor_state.ts @@ -3,22 +3,41 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { setEditorState } from '../metadata_slice'; -import { RootState, Store } from '../store'; +import { setStatus } from '../editor_slice'; +import { Store } from '../../../../../../data_explorer/public'; +import { PrefixedVisBuilderRootState, getVisBuilderRootState } from '..'; +import { VisBuilderViewServices } from '../../../../types'; -export const handlerEditorState = (store: Store, state: RootState, previousState: RootState) => { - const { metadata, ...renderState } = state; - const { metadata: prevMetadata, ...prevRenderState } = previousState; +export const handlerEditorState = ( + store: Store, + state: PrefixedVisBuilderRootState, + previousState: PrefixedVisBuilderRootState, + services: VisBuilderViewServices +) => { + const rootState = getVisBuilderRootState(state); + const previousRootState = getVisBuilderRootState(previousState); + const editor = rootState.editor; + const prevEditor = previousRootState.editor; + const renderState = { + style: rootState.style, + ui: rootState.ui, + visualization: rootState.visualization, + }; + const prevRenderState = { + style: previousRootState.style, + ui: previousRootState.ui, + visualization: previousRootState.visualization, + }; // Need to make sure the editorStates are in the clean states(not the initial states) to indicate the viz finished loading // Because when loading a saved viz from saved object, the previousStore will differ from // the currentStore even tho there is no changes applied ( aggParams will // first be empty, and it then will change to not empty once the viz finished loading) if ( - prevMetadata.editor.state === 'clean' && - metadata.editor.state === 'clean' && + prevEditor.status === 'clean' && + editor.status === 'clean' && JSON.stringify(renderState) !== JSON.stringify(prevRenderState) ) { - store.dispatch(setEditorState({ state: 'dirty' })); + store.dispatch(setStatus({ status: 'dirty' })); } }; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/handlers/index.ts b/src/plugins/vis_builder/public/application/utils/state_management/handlers/index.ts new file mode 100644 index 000000000000..1557ac80add2 --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/handlers/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './editor_state'; +export * from './parent_aggs'; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts b/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts index 255699852c8e..82dfcb5e446c 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/handlers/parent_aggs.ts @@ -5,8 +5,9 @@ import { findLast } from 'lodash'; import { BUCKET_TYPES, IMetricAggType, search } from '../../../../../../data/public'; -import { VisBuilderServices } from '../../../../types'; -import { RootState, Store } from '../store'; +import { VisBuilderViewServices } from '../../../../types'; +import { Store } from '../../../../../../data_explorer/public'; +import { PrefixedVisBuilderRootState, getVisBuilderRootState } from '..'; import { setAggParamValue } from '../visualization_slice'; /** @@ -15,12 +16,13 @@ import { setAggParamValue } from '../visualization_slice'; */ export const handlerParentAggs = async ( store: Store, - state: RootState, - services: VisBuilderServices + state: PrefixedVisBuilderRootState, + previousState: PrefixedVisBuilderRootState, + services: VisBuilderViewServices ) => { - const { - visualization: { activeVisualization, indexPattern = '' }, - } = state; + const rootState = getVisBuilderRootState(state); + const { activeVisualization } = rootState.visualization; + const { indexPattern = '' } = rootState.metadata; const { data: { diff --git a/src/plugins/vis_builder/public/application/utils/state_management/hooks.ts b/src/plugins/vis_builder/public/application/utils/state_management/hooks.ts deleted file mode 100644 index 607fe05b1623..000000000000 --- a/src/plugins/vis_builder/public/application/utils/state_management/hooks.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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/vis_builder/public/application/utils/state_management/index.ts b/src/plugins/vis_builder/public/application/utils/state_management/index.ts index 5a3e34c8da69..da53119e8b40 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/index.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/index.ts @@ -2,7 +2,100 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ +import { TypedUseSelectorHook } from 'react-redux'; +import { VisBuilderServices } from '../../../types'; +import { + AppDispatch, + RootState, + MetadataState, + setIndexPattern as updateIndexPattern, + useTypedDispatch, + useTypedSelector as useSelector, +} from '../../../../../data_explorer/public'; +import { + EditorStatus, + setState as setEditorState, + slice as editorSlice, + EditorState, + getPreloadedState as getEditorSlicePreloadedState, +} from './editor_slice'; +import { + setState as setStyleState, + styleSlice, + StyleState, + getPreloadedState as getStyleSlicePreloadedState, +} from './style_slice'; +import { + setState as setUIStateState, + uiStateSlice, + UIStateState, + getPreloadedState as getUiStateSlicePreloadedState, +} from './ui_state_slice'; +import { + setState as setVisualizationState, + slice as visualizationSlice, + VisualizationState, + getPreloadedState as getVisualizationSlicePreloadedState, +} from './visualization_slice'; -export * from './store'; -export * from './hooks'; +export * from './handlers'; export * from './shared_actions'; +export * from './prefix_helper'; + +export type VisBuilderRootStateKeys = 'editor' | 'ui' | 'visualization' | 'style'; + +export interface VisBuilderRootState extends RootState { + editor: EditorState; + style: StyleState; + ui: UIStateState; + visualization: VisualizationState; +} + +// TODO: resolve prettier error +// export type PrefixedVisBuilderRootState = { +// [K in VisBuilderRootStateKeys as `${typeof PLUGIN_ID}-${K}`]: VisBuilderRootState[K]; +// } & { +// metadata: VisBuilderRootState['metadata']; +// }; + +export interface PrefixedVisBuilderRootState { + 'vis-builder-editor': EditorState; + 'vis-builder-ui': UIStateState; + 'vis-builder-visualization': VisualizationState; + 'vis-builder-style': StyleState; + metadata: MetadataState; +} + +export const useTypedSelector: TypedUseSelectorHook = useSelector; +export { + EditorStatus, + editorSlice, + styleSlice, + uiStateSlice, + visualizationSlice, + getEditorSlicePreloadedState, + getStyleSlicePreloadedState, + getUiStateSlicePreloadedState, + getVisualizationSlicePreloadedState, + EditorState, + StyleState, + UIStateState, + VisualizationState, + setEditorState, + setStyleState, + setVisualizationState, + setUIStateState, + useTypedDispatch, + updateIndexPattern, + AppDispatch, + MetadataState, +}; + +type RenderStateKeys = 'style' | 'ui' | 'visualization' | 'metadata'; + +export type RenderState = Pick; + +export interface SliceProps { + services: VisBuilderServices; + savedVisBuilderState?: RenderState; +} diff --git a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts deleted file mode 100644 index 880c15f3e44a..000000000000 --- a/src/plugins/vis_builder/public/application/utils/state_management/metadata_slice.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { VisBuilderServices } from '../../../types'; - -/* - * Initial state: default state when opening visBuilder plugin - * Clean state: when viz finished loading and ready to be edited - * Dirty state: when there are changes applied to the viz after it finished loading - */ -type EditorState = 'loading' | 'loaded' | 'clean' | 'dirty'; - -export interface MetadataState { - editor: { - errors: { - // Errors for each section in the editor - [key: string]: boolean; - }; - state: EditorState; - }; - originatingApp?: string; -} - -const initialState: MetadataState = { - editor: { - errors: {}, - state: 'loading', - }, - originatingApp: undefined, -}; - -export const getPreloadedState = async ({ - types, - data, - embeddable, - scopedHistory, -}: VisBuilderServices): Promise => { - const { originatingApp } = - embeddable - .getStateTransfer(scopedHistory) - .getIncomingEditorState({ keysToRemoveAfterFetch: ['id', 'input'] }) || {}; - const preloadedState = { ...initialState, originatingApp }; - - return preloadedState; -}; - -export const slice = createSlice({ - name: 'metadata', - initialState, - reducers: { - setError: (state, action: PayloadAction<{ key: string; error: boolean }>) => { - const { key, error } = action.payload; - state.editor.errors[key] = error; - }, - setEditorState: (state, action: PayloadAction<{ state: EditorState }>) => { - state.editor.state = 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 { setError, setEditorState, setOriginatingApp, setState } = slice.actions; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/prefix_helper.ts b/src/plugins/vis_builder/public/application/utils/state_management/prefix_helper.ts new file mode 100644 index 000000000000..5166ada23ddf --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/prefix_helper.ts @@ -0,0 +1,81 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PLUGIN_ID } from '../../../../common'; +import { + VisBuilderRootState, + EditorState, + StyleState, + UIStateState, + VisualizationState, + VisBuilderRootStateKeys, + PrefixedVisBuilderRootState, +} from '.'; + +// Mapping from VisBuilder View state keys to their respective state types. Limit to View state. +interface ValidStateTypesMapping { + editor: EditorState; + ui: UIStateState; + visualization: VisualizationState; + style: StyleState; +} + +const validKeys = new Set([ + 'vis-builder-editor', + 'vis-builder-ui', + 'vis-builder-visualization', + 'vis-builder-style', + 'metadata', +]); + +/** + * Retrieves a specific slice from the root state based on a given key. + * The function dynamically constructs the key by adding the PLUGIN_ID prefix. + * + * @param {PrefixedVisBuilderRootState} rootState - The root state object with prefixed keys. + * @param {K} stateKey - The key of the state slice to retrieve. + * @returns {ValidStateTypesMapping[K]} - The state slice corresponding to the provided key. + */ +export const getViewSliceFromRoot = ( + rootState: PrefixedVisBuilderRootState, + stateKey: K +) => { + const dynamicKey = `${PLUGIN_ID}-${stateKey}` as keyof PrefixedVisBuilderRootState; + return rootState[dynamicKey] as ValidStateTypesMapping[K]; +}; + +/** + * Selector function to access a specific slice from the state. + * This function creates a selector that can be used in components to select a part of the state. + * + * @param {VisBuilderRootStateKeys} stateKey - The key of the state slice to select. + * @returns - A selector function for the specified state slice. + */ +export const getViewSliceFromSelector = (stateKey: VisBuilderRootStateKeys) => ( + state: PrefixedVisBuilderRootState +) => state[`${PLUGIN_ID}-${stateKey}`]; + +/** + * Transforms the PrefixedVisBuilderRootState back into VisBuilderRootState. + * This function iterates over the keys of the state and transforms them back to the original format. + * + * @param {PrefixedVisBuilderRootState} rootState - The prefixed root state object. + * @returns {VisBuilderRootState} - The transformed root state with original keys. + */ +export const getVisBuilderRootState = ( + rootState: PrefixedVisBuilderRootState +): VisBuilderRootState => { + const transformedState = {}; + + Object.keys(rootState).forEach((prefixedKey) => { + if (validKeys.has(prefixedKey)) { + const key = + prefixedKey === 'metadata' ? prefixedKey : prefixedKey.replace(`${PLUGIN_ID}-`, ''); + transformedState[key] = rootState[prefixedKey]; + } + }); + + return transformedState as VisBuilderRootState; +}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/preload.ts b/src/plugins/vis_builder/public/application/utils/state_management/preload.ts deleted file mode 100644 index f7a0f6bd7ad3..000000000000 --- a/src/plugins/vis_builder/public/application/utils/state_management/preload.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { PreloadedState } from '@reduxjs/toolkit'; -import { VisBuilderServices } from '../../..'; -import { getPreloadedState as getPreloadedStyleState } from './style_slice'; -import { getPreloadedState as getPreloadedVisualizationState } from './visualization_slice'; -import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; -import { getPreloadedState as getPreloadedUIState } from './ui_state_slice'; -import { RootState } from './store'; - -export const getPreloadedState = async ( - services: VisBuilderServices -): Promise> => { - const styleState = await getPreloadedStyleState(services); - const visualizationState = await getPreloadedVisualizationState(services); - const metadataState = await getPreloadedMetadataState(services); - const uiState = await getPreloadedUIState(services); - - return { - style: styleState, - visualization: visualizationState, - metadata: metadataState, - ui: uiState, - }; -}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx deleted file mode 100644 index a46d5c027656..000000000000 --- a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { VisBuilderServices } from '../../../types'; -import { createVisBuilderServicesMock } 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 = createVisBuilderServicesMock(); - reduxStateParams = { - style: 'style', - visualization: 'visualization', - metadata: 'metadata', - ui: 'ui', - }; - }); - - test('test load redux state when url is empty', async () => { - const defaultStates: RootState = { - style: 'style default states', - visualization: { - searchField: '', - activeVisualization: { name: 'viz', aggConfigParams: [] }, - indexPattern: 'id', - }, - metadata: { - editor: { errors: {}, state: 'loading' }, - originatingApp: undefined, - }, - ui: {}, - }; - - 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/vis_builder/public/application/utils/state_management/redux_persistence.ts b/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts deleted file mode 100644 index 3ebfa47268ec..000000000000 --- a/src/plugins/vis_builder/public/application/utils/state_management/redux_persistence.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { VisBuilderServices } from '../../../types'; -import { getPreloadedState } from './preload'; -import { RootState } from './store'; - -export const loadReduxState = async (services: VisBuilderServices) => { - 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 = ( - { style, visualization, metadata, ui }: RootState, - services: VisBuilderServices -) => { - try { - services.osdUrlStateStorage.set( - '_a', - { style, visualization, metadata, ui }, - { - replace: true, - } - ); - } catch (err) { - return; - } -}; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/store.ts b/src/plugins/vis_builder/public/application/utils/state_management/store.ts deleted file mode 100644 index 8fe5c23fd657..000000000000 --- a/src/plugins/vis_builder/public/application/utils/state_management/store.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { combineReducers, configureStore, PreloadedState } from '@reduxjs/toolkit'; -import { isEqual } from 'lodash'; -import { reducer as styleReducer } from './style_slice'; -import { reducer as visualizationReducer } from './visualization_slice'; -import { reducer as metadataReducer } from './metadata_slice'; -import { reducer as uiStateReducer } from './ui_state_slice'; -import { VisBuilderServices } from '../../..'; -import { loadReduxState, persistReduxState } from './redux_persistence'; -import { handlerEditorState } from './handlers/editor_state'; -import { handlerParentAggs } from './handlers/parent_aggs'; - -const rootReducer = combineReducers({ - ui: uiStateReducer, - style: styleReducer, - visualization: visualizationReducer, - metadata: metadataReducer, -}); - -export const configurePreloadedStore = (preloadedState: PreloadedState) => { - return configureStore({ - reducer: rootReducer, - preloadedState, - }); -}; - -export const getPreloadedStore = async (services: VisBuilderServices) => { - 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; - - // Side effects to apply after changes to the store are made - handlerEditorState(store, state, previousState); - handlerParentAggs(store, state, services); - - previousState = state; - }; - - // the store subscriber will automatically detect changes and call handleChange function - const unsubscribe = store.subscribe(handleChange); - - return { store, unsubscribe }; -}; - -// 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 { setState as setStyleState, StyleState } from './style_slice'; -export { setState as setVisualizationState, VisualizationState } from './visualization_slice'; -export { MetadataState } from './metadata_slice'; -export { setState as setUIStateState, UIStateState } from './ui_state_slice'; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/style_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/style_slice.ts index fe4e246ac528..af5d0d36c63a 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/style_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/style_slice.ts @@ -4,24 +4,25 @@ */ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { VisBuilderServices } from '../../../types'; import { setActiveVisualization } from './shared_actions'; +import { DefaultViewState } from '../../../../../data_explorer/public'; +import { SliceProps } from './index'; export type StyleState = T; const initialState = {} as StyleState; export const getPreloadedState = async ({ - types, - data, -}: VisBuilderServices): Promise => { - let preloadedState = initialState; - + services, + savedVisBuilderState, +}: SliceProps): Promise> => { + const { types } = services; const defaultVisualization = types.all()[0]; const defaultState = defaultVisualization.ui.containerConfig.style.defaults; - if (defaultState) { - preloadedState = defaultState; - } + + const preloadedState: DefaultViewState = { + state: savedVisBuilderState?.style || defaultState || initialState, + }; return preloadedState; }; diff --git a/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts index 826fe9d9873d..4735a4231467 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/ui_state_slice.ts @@ -2,19 +2,22 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ - import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { VisBuilderServices } from '../../../types'; +import { DefaultViewState } from '../../../../../data_explorer/public'; +import { SliceProps } from './index'; export type UIStateState = T; const initialState = {} as UIStateState; export const getPreloadedState = async ({ - types, - data, -}: VisBuilderServices): Promise => { - return initialState; + services, + savedVisBuilderState, +}: SliceProps): Promise> => { + const preloadedState: DefaultViewState = { + state: savedVisBuilderState?.ui || initialState, + }; + return preloadedState; }; export const uiStateSlice = createSlice({ diff --git a/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts b/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts index 6662f9f43d71..dedcd776d50f 100644 --- a/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts +++ b/src/plugins/vis_builder/public/application/utils/state_management/visualization_slice.ts @@ -5,11 +5,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { CreateAggConfigParams } from '../../../../../data/common'; -import { VisBuilderServices } from '../../../types'; import { setActiveVisualization } from './shared_actions'; +import { DefaultViewState } from '../../../../../data_explorer/public'; +import { SliceProps } from './index'; export interface VisualizationState { - indexPattern?: string; searchField: string; activeVisualization?: { name: string; @@ -23,22 +23,27 @@ const initialState: VisualizationState = { }; export const getPreloadedState = async ({ - types, - data, -}: VisBuilderServices): Promise => { - const preloadedState = { ...initialState }; - + services, + savedVisBuilderState, +}: SliceProps): Promise> => { + const { types } = services; const defaultVisualization = types.all()[0]; - const defaultIndexPattern = await data.indexPatterns.getDefault(); const name = defaultVisualization.name; - if (name && defaultIndexPattern) { - preloadedState.activeVisualization = { - name, - aggConfigParams: [], - }; + // Define the default activeVisualization + const defaultActiveVisualization = name + ? { + name, + aggConfigParams: [], + } + : undefined; - preloadedState.indexPattern = defaultIndexPattern.id; - } + // Use the saved state if available, otherwise use the default + const preloadedState: DefaultViewState = { + state: savedVisBuilderState?.visualization || { + ...initialState, + activeVisualization: defaultActiveVisualization, + }, + }; return preloadedState; }; @@ -47,11 +52,6 @@ export const slice = createSlice({ name: 'visualization', initialState, reducers: { - setIndexPattern: (state, action: PayloadAction) => { - state.indexPattern = action.payload; - state.activeVisualization!.aggConfigParams = []; - state.activeVisualization!.draftAgg = undefined; - }, setSearchField: (state, action: PayloadAction) => { state.searchField = action.payload; }, @@ -131,7 +131,6 @@ export const slice = createSlice({ export const { reducer } = slice; export const { - setIndexPattern, setSearchField, editDraftAgg, saveDraftAgg, diff --git a/src/plugins/vis_builder/public/application/utils/use/index.ts b/src/plugins/vis_builder/public/application/utils/use/index.ts index 1cc0b28dc89a..777dfbec3b1f 100644 --- a/src/plugins/vis_builder/public/application/utils/use/index.ts +++ b/src/plugins/vis_builder/public/application/utils/use/index.ts @@ -4,7 +4,7 @@ */ export { useAggs } from './use_aggs'; -export { useIndexPatterns } from './use_index_pattern'; +export { useIndexPattern } from './use_index_pattern'; export { useOnAddFilter } from './use_on_add_filter'; export { useSampleHits } from './use_sample_hits'; export { useSavedVisBuilderVis } from './use_saved_vis_builder_vis'; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_aggs.ts b/src/plugins/vis_builder/public/application/utils/use/use_aggs.ts index 19a3589a9cb7..24094aa75a08 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_aggs.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_aggs.ts @@ -6,30 +6,27 @@ import { cloneDeep } from 'lodash'; import { useLayoutEffect, useMemo, useState } from 'react'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../../../types'; -import { useTypedSelector, useTypedDispatch } from '../state_management'; -import { useIndexPatterns } from './use_index_pattern'; +import { VisBuilderViewServices } from '../../../types'; +import { useTypedDispatch } from '../state_management'; +import { useVisBuilderContext } from '../../view_components/context'; /** * Returns common agg parameters from the store and app context * @returns { indexPattern, aggConfigs, aggs, timeRange } */ export const useAggs = () => { + const { services } = useOpenSearchDashboards(); const { - services: { - data: { - search: { aggs: aggService }, - query: { - timefilter: { timefilter }, - }, + data: { + search: { aggs: aggService }, + query: { + timefilter: { timefilter }, }, }, - } = useOpenSearchDashboards(); - const indexPattern = useIndexPatterns().selected; + } = services; const [timeRange, setTimeRange] = useState(timefilter.getTime()); - const aggConfigParams = useTypedSelector( - (state) => state.visualization.activeVisualization?.aggConfigParams - ); + const { indexPattern, rootState } = useVisBuilderContext(); + const aggConfigParams = rootState.visualization.activeVisualization?.aggConfigParams; const dispatch = useTypedDispatch(); const aggConfigs = useMemo(() => { @@ -49,7 +46,6 @@ export const useAggs = () => { }, [dispatch, timefilter]); return { - indexPattern, aggConfigs, aggs: aggConfigs?.aggs ?? [], timeRange, diff --git a/src/plugins/vis_builder/public/application/utils/use/use_can_save.ts b/src/plugins/vis_builder/public/application/utils/use/use_can_save.ts index 7da320d266f3..a85beaddbc10 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_can_save.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_can_save.ts @@ -4,23 +4,19 @@ */ import { i18n } from '@osd/i18n'; -import { useTypedSelector } from '../state_management'; +import { useVisBuilderContext } from '../../view_components/context'; export const useCanSave = () => { - const isEmpty = useTypedSelector( - (state) => state.visualization.activeVisualization?.aggConfigParams?.length === 0 - ); - const hasNoChange = useTypedSelector((state) => state.metadata.editor.state !== 'dirty'); - const hasDraftAgg = useTypedSelector( - (state) => !!state.visualization.activeVisualization?.draftAgg - ); - const errorMsg = getErrorMsg(isEmpty, hasNoChange, hasDraftAgg); + const { rootState } = useVisBuilderContext(); + const isEmpty = rootState.visualization.activeVisualization?.aggConfigParams?.length === 0; + const hasDraftAgg = !!rootState.visualization.activeVisualization?.draftAgg; + const errorMsg = getErrorMsg(isEmpty, hasDraftAgg); return errorMsg; }; // TODO: Need to finalize the error messages -const getErrorMsg = (isEmpty, hasNoChange, hasDraftAgg) => { +const getErrorMsg = (isEmpty, hasDraftAgg) => { const i18nTranslate = (key: string, defaultMessage: string) => i18n.translate(`visBuilder.saveVisualizationTooltip.${key}`, { defaultMessage, @@ -28,8 +24,6 @@ const getErrorMsg = (isEmpty, hasNoChange, hasDraftAgg) => { if (isEmpty) { return i18nTranslate('empty', 'The canvas is empty. Add some aggregations before saving.'); - } else if (hasNoChange) { - return i18nTranslate('noChange', 'Add some changes before saving.'); } else if (hasDraftAgg) { return i18nTranslate( 'hasDraftAgg', diff --git a/src/plugins/vis_builder/public/application/utils/use/use_index_pattern.ts b/src/plugins/vis_builder/public/application/utils/use/use_index_pattern.ts new file mode 100644 index 000000000000..23f2a8cccbaa --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/use/use_index_pattern.ts @@ -0,0 +1,62 @@ +/* + * 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 { VisBuilderViewServices } from '../../../types'; +import { useTypedSelector, updateIndexPattern } from '../state_management'; +import { getIndexPatternId } from '../helpers/index_pattern_helper'; + +export const useIndexPattern = (services: VisBuilderViewServices): IndexPattern => { + const indexPatternIdFromState = useTypedSelector((state) => state.metadata.indexPattern); + const [indexPattern, setIndexPattern] = useState(); + const { data, toastNotifications, uiSettings: config, store } = services; + + useEffect(() => { + let isMounted = true; + + const fetchIndexPatternDetails = (id: string) => { + data.indexPatterns + .get(id) + .then((result) => { + if (isMounted) { + setIndexPattern(result); + } + }) + .catch(() => { + if (isMounted) { + const indexPatternMissingWarning = i18n.translate( + 'discover.valueIsNotConfiguredIndexPatternIDWarningTitle', + { + defaultMessage: '{id} is not a configured index pattern ID', + values: { + id: `"${id}"`, + }, + } + ); + toastNotifications.addDanger({ + title: indexPatternMissingWarning, + }); + } + }); + }; + + if (!indexPatternIdFromState) { + data.indexPatterns.getCache().then((indexPatternList) => { + const newId = getIndexPatternId('', indexPatternList, config.get('defaultIndex')); + store!.dispatch(updateIndexPattern(newId)); + fetchIndexPatternDetails(newId); + }); + } else { + fetchIndexPatternDetails(indexPatternIdFromState); + } + + return () => { + isMounted = false; + }; + }, [indexPatternIdFromState, data.indexPatterns, toastNotifications, config, store]); + + return indexPattern; +}; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_index_pattern.tsx b/src/plugins/vis_builder/public/application/utils/use/use_index_pattern.tsx deleted file mode 100644 index 0ce64a36b2ba..000000000000 --- a/src/plugins/vis_builder/public/application/utils/use/use_index_pattern.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { useEffect, useState } from 'react'; -import { IndexPattern } from '../../../../../data/public'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../../../types'; -import { useTypedSelector } from '../state_management'; - -export const useIndexPatterns = () => { - const { indexPattern: indexId = '' } = useTypedSelector((state) => state.visualization); - const [indexPatterns, setIndexPatterns] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(undefined); - const { - services: { data }, - } = useOpenSearchDashboards(); - - let foundSelected: IndexPattern | undefined; - if (!loading && !error) { - foundSelected = indexPatterns.filter((p) => p.id === indexId)[0]; - if (foundSelected === undefined) { - setError( - new Error("Attempted to select an index pattern that wasn't in the index pattern list") - ); - } - } - - useEffect(() => { - const handleUpdate = async () => { - try { - const ids = await data.indexPatterns.getIds(true); - const patterns = await Promise.all(ids.map((id) => data.indexPatterns.get(id))); - setIndexPatterns(patterns); - } catch (e) { - setError(e as Error); - } finally { - setLoading(false); - } - }; - - handleUpdate(); - }, [data.indexPatterns]); - - return { - indexPatterns, - error, - loading, - selected: foundSelected, - }; -}; diff --git a/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts b/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts index 791521fccad5..01a9a817ff69 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_on_add_filter.ts @@ -6,8 +6,8 @@ import { useCallback } from 'react'; import { IndexPatternField, opensearchFilters } from '../../../../../data/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../../../types'; -import { useIndexPatterns } from './use_index_pattern'; +import { VisBuilderViewServices } from '../../../types'; +import { useVisBuilderContext } from '../../view_components/context'; export const useOnAddFilter = () => { const { @@ -16,8 +16,8 @@ export const useOnAddFilter = () => { query: { filterManager }, }, }, - } = useOpenSearchDashboards(); - const indexPattern = useIndexPatterns().selected; + } = useOpenSearchDashboards(); + const { indexPattern } = useVisBuilderContext(); const { id = '' } = indexPattern ?? {}; return useCallback( (fieldToFilter: IndexPatternField | string, value: string, operation: '+' | '-') => { diff --git a/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts b/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts index f3ed75a4dd6a..e6867e7e83da 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_sample_hits.ts @@ -7,8 +7,8 @@ import { useEffect, useLayoutEffect, useState } from 'react'; import { SortDirection } from '../../../../../data/public'; import { IExpressionLoaderParams } from '../../../../../expressions/public'; import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { VisBuilderServices } from '../../../types'; -import { useIndexPatterns } from './use_index_pattern'; +import { VisBuilderViewServices } from '../../../types'; +import { useVisBuilderContext } from '../../view_components/context'; export const useSampleHits = () => { const { @@ -24,8 +24,8 @@ export const useSampleHits = () => { }, uiSettings: config, }, - } = useOpenSearchDashboards(); - const indexPattern = useIndexPatterns().selected; + } = useOpenSearchDashboards(); + const { indexPattern } = useVisBuilderContext(); const [hits, setHits] = useState>>([]); const [searchContext, setSearchContext] = useState({ query: queryString.getQuery(), diff --git a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts index 44ffbaf75953..19a101784028 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_saved_vis_builder_vis.ts @@ -19,27 +19,29 @@ import { setStyleState, setVisualizationState, setUIStateState, + setEditorState, } from '../state_management'; -import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; -import { setEditorState } from '../state_management/metadata_slice'; +import { setStatus } from '../state_management/editor_slice'; import { getStateFromSavedObject } from '../../../saved_visualizations/transforms'; // This function can be used when instantiating a saved vis or creating a new one // using url parameters, embedding and destroying it in DOM -export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined) => { - const { services } = useOpenSearchDashboards(); +export const useSavedVisBuilderVis = ( + services: VisBuilderServices, + visualizationIdFromUrl: string | undefined +) => { const [savedVisState, setSavedVisState] = useState(undefined); const dispatch = useTypedDispatch(); + const { + application: { navigateToApp }, + chrome, + http: { basePath }, + toastNotifications, + savedVisBuilderLoader, + history, + } = services; useEffect(() => { - const { - application: { navigateToApp }, - chrome, - history, - http: { basePath }, - toastNotifications, - savedVisBuilderLoader, - } = services; const toastNotification = (message: string) => { toastNotifications.addDanger({ title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', { @@ -51,7 +53,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined const loadSavedVisBuilderVis = async () => { try { - dispatch(setEditorState({ state: 'loading' })); + dispatch(setStatus({ status: 'loading' })); const savedVisBuilderVis = await getSavedVisBuilderVis( savedVisBuilderLoader, visualizationIdFromUrl @@ -65,13 +67,13 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined dispatch(setUIStateState(state.ui)); dispatch(setStyleState(state.style)); dispatch(setVisualizationState(state.visualization)); - dispatch(setEditorState({ state: 'loaded' })); + dispatch(setStatus({ status: 'loaded' })); } else { chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp)); } setSavedVisState(savedVisBuilderVis); - dispatch(setEditorState({ state: 'clean' })); + dispatch(setEditorState({ errors: {}, status: 'clean', savedVisBuilderId: savedVisBuilderVis.id })); } catch (error) { const managementRedirectTarget = { [PLUGIN_ID]: { @@ -102,12 +104,21 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined }; loadSavedVisBuilderVis(); - }, [dispatch, services, visualizationIdFromUrl]); + }, [ + dispatch, + basePath, + chrome, + history, + navigateToApp, + savedVisBuilderLoader, + toastNotifications, + visualizationIdFromUrl, + ]); return savedVisState; }; -async function getSavedVisBuilderVis( +export async function getSavedVisBuilderVis( savedVisBuilderLoader: VisBuilderServices['savedVisBuilderLoader'], visBuilderVisId?: string ) { diff --git a/src/plugins/vis_builder/public/application/utils/use/use_visualization_type.ts b/src/plugins/vis_builder/public/application/utils/use/use_visualization_type.ts index 2785f51a924d..4f8efe316722 100644 --- a/src/plugins/vis_builder/public/application/utils/use/use_visualization_type.ts +++ b/src/plugins/vis_builder/public/application/utils/use/use_visualization_type.ts @@ -5,14 +5,15 @@ import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public'; import { VisualizationType } from '../../../services/type_service/visualization_type'; -import { VisBuilderServices } from '../../../types'; -import { useTypedSelector } from '../state_management'; +import { VisBuilderViewServices } from '../../../types'; +import { useVisBuilderContext } from '../../view_components/context'; export const useVisualizationType = (): VisualizationType => { - const { activeVisualization } = useTypedSelector((state) => state.visualization); + const { rootState } = useVisBuilderContext(); + const { activeVisualization } = rootState.visualization; const { services: { types }, - } = useOpenSearchDashboards(); + } = useOpenSearchDashboards(); const visualizationType = types.get(activeVisualization?.name ?? ''); diff --git a/src/plugins/vis_builder/public/application/view_components/canvas/_utils.scss b/src/plugins/vis_builder/public/application/view_components/canvas/_utils.scss new file mode 100644 index 000000000000..95d16a338a0c --- /dev/null +++ b/src/plugins/vis_builder/public/application/view_components/canvas/_utils.scss @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +@mixin scrollNavParent($template-row: none) { + display: grid; + min-height: 0; + + @if $template-row != "none" { + grid-template-rows: $template-row; + } +} diff --git a/src/plugins/vis_builder/public/application/_variables.scss b/src/plugins/vis_builder/public/application/view_components/canvas/_variables.scss similarity index 91% rename from src/plugins/vis_builder/public/application/_variables.scss rename to src/plugins/vis_builder/public/application/view_components/canvas/_variables.scss index e72314b3a3bc..425d78ad1f03 100644 --- a/src/plugins/vis_builder/public/application/_variables.scss +++ b/src/plugins/vis_builder/public/application/view_components/canvas/_variables.scss @@ -2,8 +2,8 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + @import "@elastic/eui/src/global_styling/variables/header"; @import "@elastic/eui/src/global_styling/variables/form"; $osdHeaderOffset: $euiHeaderHeightCompensation; -$vbLeftNavWidth: 462px; diff --git a/src/plugins/vis_builder/public/application/app.scss b/src/plugins/vis_builder/public/application/view_components/canvas/canvas.scss similarity index 83% rename from src/plugins/vis_builder/public/application/app.scss rename to src/plugins/vis_builder/public/application/view_components/canvas/canvas.scss index 204511389301..f137af0622cc 100644 --- a/src/plugins/vis_builder/public/application/app.scss +++ b/src/plugins/vis_builder/public/application/view_components/canvas/canvas.scss @@ -2,14 +2,15 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + @import "variables"; .vbLayout { padding: 0; display: grid; grid-template: - "topNav topNav" min-content - "leftNav workspaceNav" 1fr / #{$vbLeftNavWidth} 1fr; + "topNav" min-content + "workspaceNav" 1fr / 1fr; height: calc(100vh - #{$osdHeaderOffset}); &__resizeContainer { diff --git a/src/plugins/vis_builder/public/application/view_components/canvas/index.tsx b/src/plugins/vis_builder/public/application/view_components/canvas/index.tsx new file mode 100644 index 000000000000..2ce4e0916513 --- /dev/null +++ b/src/plugins/vis_builder/public/application/view_components/canvas/index.tsx @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiPage, EuiResizableContainer } from '@elastic/eui'; +import { I18nProvider } from '@osd/i18n/react'; +import { ViewProps } from '../../../../../data_explorer/public'; +import { TopNav } from '../../components/top_nav'; +import { Workspace } from '../../components/workspace'; +import { RightNav } from '../../components/right_nav'; +import { ConfigPanel } from '../../components/config_panel/config_panel'; + +import './canvas.scss'; + +// eslint-disable-next-line import/no-default-export +export default function VisBuilderCanvas({ setHeaderActionMenu, history }: ViewProps) { + return ( + + + + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + + + + + )} + + + + ); +} diff --git a/src/plugins/vis_builder/public/application/view_components/context/index.tsx b/src/plugins/vis_builder/public/application/view_components/context/index.tsx new file mode 100644 index 000000000000..c6ff16d6b1ba --- /dev/null +++ b/src/plugins/vis_builder/public/application/view_components/context/index.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { DataExplorerServices, ViewProps } from '../../../../../data_explorer/public'; +import { + OpenSearchDashboardsContextProvider, + useOpenSearchDashboards, +} from '../../../../../opensearch_dashboards_react/public'; +import { VisBuilderViewServices } from '../../../types'; +import { useVisBuilderState, VisBuilderContextValue } from '../utils/use_vis_builder_state'; +import { getVisBuilderServices } from '../../../plugin_services'; +import { DragDropProvider } from '../../../application/utils/drag_drop'; + +// Define the context for VisBuilder +const VBContext = React.createContext({} as VisBuilderContextValue); + +// eslint-disable-next-line import/no-default-export +export default function VisBuilderContext({ children }: React.PropsWithChildren) { + const { services: deServices } = useOpenSearchDashboards(); + const visBuilderServices = getVisBuilderServices(); + const services: VisBuilderViewServices = { ...deServices, ...visBuilderServices }; + const visBuilderParams = useVisBuilderState(services); + + return ( + + + {children} + + + ); +} + +// Export the useVisBuilderContext hook for VisBuilder +export const useVisBuilderContext = () => React.useContext(VBContext); diff --git a/src/plugins/vis_builder/public/application/view_components/panel/index.tsx b/src/plugins/vis_builder/public/application/view_components/panel/index.tsx new file mode 100644 index 000000000000..d77ca9c6303f --- /dev/null +++ b/src/plugins/vis_builder/public/application/view_components/panel/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { FieldSelector } from '../../components/field_selector'; +import { ViewProps } from '../../../../../data_explorer/public'; +import './panel.scss'; + +// eslint-disable-next-line import/no-default-export +export default function VisBuilderPanel(props: ViewProps) { + return ( +
+ +
+ ); +} diff --git a/src/plugins/vis_builder/public/application/components/data_tab/index.scss b/src/plugins/vis_builder/public/application/view_components/panel/panel.scss similarity index 75% rename from src/plugins/vis_builder/public/application/components/data_tab/index.scss rename to src/plugins/vis_builder/public/application/view_components/panel/panel.scss index 764ed72fd373..977784232f0d 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/index.scss +++ b/src/plugins/vis_builder/public/application/view_components/panel/panel.scss @@ -2,11 +2,12 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ + @import "../../util"; -.vbDataTab { +.vbFieldSelector { @include scrollNavParent; display: grid; - grid-template-columns: 50% 50%; + grid-template-columns: 100%; } diff --git a/src/plugins/vis_builder/public/application/view_components/utils/use_vis_builder_state.ts b/src/plugins/vis_builder/public/application/view_components/utils/use_vis_builder_state.ts new file mode 100644 index 000000000000..b80a91bc78f0 --- /dev/null +++ b/src/plugins/vis_builder/public/application/view_components/utils/use_vis_builder_state.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisBuilderViewServices } from '../../../types'; +import { useIndexPattern } from '../../utils/use'; +import { useTypedSelector, getVisBuilderRootState } from '../../utils/state_management'; +import { useSavedVisBuilderVis } from '../../utils/use'; +import { extractSavedVisBuilderId } from '../../../extract_id'; +import { syncQueryStateWithUrl } from '../../../../../data/public'; + +export const useVisBuilderState = (services: VisBuilderViewServices) => { + const { + data: { query }, + osdUrlStateStorage, + } = services; + const indexPattern = useIndexPattern(services); + const rootState = useTypedSelector((state) => getVisBuilderRootState(state)); + const path = window.location.pathname; + const savedVisBuilderId = rootState.editor.savedVisBuilderId || extractSavedVisBuilderId(path); + const savedVisBuilderVis = useSavedVisBuilderVis(services, savedVisBuilderId); + // syncs `_g` portion of url with query services + syncQueryStateWithUrl(query, osdUrlStateStorage); + return { indexPattern, rootState, savedVisBuilderId, savedVisBuilderVis }; +}; + +export type VisBuilderContextValue = ReturnType; diff --git a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx index a931877ffe6d..1a610422e4b5 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx @@ -138,6 +138,7 @@ export class VisBuilderEmbeddable extends Embeddable { if (!this.input.disableTriggers) { const indexPattern = await getIndexPatterns().get( - this.savedVis?.state.visualization.indexPattern ?? '' + this.savedVis?.state.metadata.indexPattern ?? '' ); handleVisEvent(event, getUIActions(), indexPattern.timeFieldName); diff --git a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx index 3c0bf0337369..b3a7c3f775ad 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable_factory.tsx @@ -89,7 +89,7 @@ export class VisBuilderEmbeddableFactory const savedVis = getStateFromSavedObject(savedObject); const indexPatternService = this.deps.start().plugins.data.indexPatterns; const indexPattern = await indexPatternService.get( - savedVis.state.visualization.indexPattern || '' + savedVis.state.metadata.indexPattern || '' ); const indexPatterns = indexPattern ? [indexPattern] : []; diff --git a/src/plugins/vis_builder/public/extract_id.ts b/src/plugins/vis_builder/public/extract_id.ts new file mode 100644 index 000000000000..a0660a5a0dd6 --- /dev/null +++ b/src/plugins/vis_builder/public/extract_id.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export function extractSavedVisBuilderId(path: string) { + const editIndex = path.indexOf('/edit/'); + if (editIndex === -1) { + return ''; + } + + // Extract the path starting from '/edit/' + const pathFromEdit = path.substring(editIndex); + + // Split the path and take the first two segments ('edit' and the ID) + const segments = pathFromEdit.split('/').filter(Boolean); + if (segments.length >= 2) { + return `${segments[1]}`; + } + + return ''; // no saved id +} \ No newline at end of file diff --git a/src/plugins/vis_builder/public/migrate_state.ts b/src/plugins/vis_builder/public/migrate_state.ts new file mode 100644 index 000000000000..90d0c9fd5ec8 --- /dev/null +++ b/src/plugins/vis_builder/public/migrate_state.ts @@ -0,0 +1,120 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { matchPath } from "react-router-dom"; +import { PLUGIN_ID } from "../common"; +import { getStateFromOsdUrl, setStateToOsdUrl } from '../../opensearch_dashboards_utils/public'; +import { EditorState, UIStateState, VisualizationState, StyleState, EditorStatus } from "./application/utils/state_management"; + +interface VisBuilderParams { + id?: string; +} + +interface LegacyMetadataState { + editor: { + errors: { + // Errors for each section in the editor + [key: string]: boolean; + }; + state: EditorStatus; + }; + originatingApp?: string; +} + +interface LegacyVisualizationState extends VisualizationState { + indexPattern?: string +} + +export interface LegacyVisBuilderState { + metadata: LegacyMetadataState, + style: StyleState, + ui: UIStateState, + visualization: LegacyVisualizationState +} + +// 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: '/edit/:id', + extraState: {}, + path: `savedVisBuilder`, + }, + { pattern: '#/', extraState: {}, path: `vis-builder` }, + ]; + + // Get the first matching path pattern. + const matchingPathPattern = pathPatterns.find((pathPattern) => + matchPath(oldPath, { path: pathPattern.pattern, strict: false }) + ); + + if (!matchingPathPattern) { + return path; + } + + // Migrate the path. + switch (matchingPathPattern.path) { + case `vis-builder`: + case `savedVisBuilder`: + const params = matchPath(oldPath, { + path: matchingPathPattern.pattern, + })!.params; + + // if there is a saved search id, use the saved search path + if (params.id) { + path = `${path}edit/${params.id}`; + } + + const appState = getStateFromOsdUrl('_a', oldPath); + const _q = getStateFromOsdUrl('_q', oldPath); + const _g = getStateFromOsdUrl('_g', oldPath); + + if (!appState) return path; + + const { metadata, style, ui, visualization } = appState; + + // transform vis builder metadata state to editor state + const transformedEditorState: EditorState = { + errors: metadata.editor.errors, + status: metadata.editor.state as EditorStatus, + savedVisBuilderId: params.id || '', + }; + + + // remove index pattern from vis builder visualization state + const transformedVisualizationState = { ...visualization }; + delete transformedVisualizationState.indexPattern; + + // contstruct new state + const newState = { + 'vis-builder-editor': transformedEditorState, + 'vis-builder-style': style, + 'vis-builder-ui': ui, + 'vis-builder-visualization': transformedVisualizationState, + }; + + const _a = { + ...newState, + metadata: { + indexPattern: visualization.indexPattern, + view: `${PLUGIN_ID}` + } + } + + path = setStateToOsdUrl('_a', _a, { useHash: false }, path); + path = setStateToOsdUrl('_q', _q, { useHash: false }, path); + path = setStateToOsdUrl('_g', _g, { useHash: false }, path); + + break; + } + + return path; + } \ No newline at end of file diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 4e8f020d1fe8..e73a04157d66 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -4,17 +4,14 @@ */ import { i18n } from '@osd/i18n'; -import { BehaviorSubject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { lazy } from 'react'; import { AppMountParameters, AppNavLinkStatus, - AppUpdater, CoreSetup, CoreStart, Plugin, PluginInitializerContext, - ScopedHistory, } from '../../../core/public'; import { VisBuilderPluginSetupDependencies, @@ -32,30 +29,43 @@ import { VIS_BUILDER_CHART_TYPE, } from '../common'; import { TypeService } from './services/type_service'; -import { getPreloadedStore } from './application/utils/state_management'; import { + editorSlice, + styleSlice, + uiStateSlice, + visualizationSlice, + handlerEditorState, + handlerParentAggs, + getEditorSlicePreloadedState, + getStyleSlicePreloadedState, + getUiStateSlicePreloadedState, + getVisualizationSlicePreloadedState, +} from './application/utils/state_management'; +import { + setExpressionLoader, + setReactExpressionRenderer, setSearchService, setIndexPatterns, setHttp, setSavedVisBuilderLoader, - setExpressionLoader, setTimeFilter, setUISettings, + setUIActions, setTypeService, - setReactExpressionRenderer, setQueryService, - setUIActions, + setHeaderActionMenuMounter, + getVisBuilderServices, + setVisBuilderServices, } from './plugin_services'; import { createSavedVisBuilderLoader } from './saved_visualizations'; import { registerDefaultTypes } from './visualizations'; import { ConfigSchema } from '../config'; -import { - createOsdUrlStateStorage, - createOsdUrlTracker, - createStartServicesGetter, - withNotifyOnErrors, -} from '../../opensearch_dashboards_utils/public'; -import { opensearchFilters } from '../../data/public'; +import { createStartServicesGetter } from '../../opensearch_dashboards_utils/public'; +import { buildVisBuilderServices } from './types'; +import { extractSavedVisBuilderId } from './extract_id'; +import { getStateFromSavedObject } from '../public/saved_visualizations/transforms'; +import { getSavedVisBuilderVis } from '../public/application/utils/use/use_saved_vis_builder_vis'; +import { migrateUrlState } from './migrate_state'; export class VisBuilderPlugin implements @@ -66,44 +76,15 @@ export class VisBuilderPlugin VisBuilderPluginStartDependencies > { private typeService = new TypeService(); - private appStateUpdater = new BehaviorSubject(() => ({})); private stopUrlTracking?: () => void; - private currentHistory?: ScopedHistory; + private currentHistory?: any; constructor(public initializerContext: PluginInitializerContext) {} public setup( core: CoreSetup, - { embeddable, visualizations, data }: VisBuilderPluginSetupDependencies + { embeddable, visualizations, dataExplorer }: VisBuilderPluginSetupDependencies ) { - 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 Default Visualizations const typeService = this.typeService; registerDefaultTypes(typeService.setup()); @@ -115,66 +96,95 @@ export class VisBuilderPlugin navLinkStatus: AppNavLinkStatus.hidden, defaultPath: '#/', mount: async (params: AppMountParameters) => { - // Load application bundle - const { renderApp } = await import('./application'); - // Get start services as specified in opensearch_dashboards.json - const [coreStart, pluginsStart, selfStart] = await core.getStartServices(); - const { savedObjects, navigation, expressions } = pluginsStart; + const [coreStart] = await core.getStartServices(); + // const { savedObjects, navigation, expressions } = pluginsStart; + const { + application: { navigateToApp }, + } = coreStart; + setHeaderActionMenuMounter(params.setHeaderActionMenu); this.currentHistory = params.history; - // make sure the index pattern list is up to date - pluginsStart.data.indexPatterns.clearCache(); - // make sure a default index pattern exists - // if not, the page will be redirected to management and visualize won't be rendered - // TODO: Add the redirect - await pluginsStart.data.indexPatterns.ensureDefaultIndexPattern(); - - appMounted(); + const unlistenParentHistory = this.currentHistory.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - const unlistenParentHistory = this.currentHistory.listen(() => { + this.currentHistory.listen(() => { window.dispatchEvent(new HashChangeEvent('hashchange')); }); - const services: VisBuilderServices = { - ...coreStart, - appName: PLUGIN_ID, - scopedHistory: this.currentHistory, - history: this.currentHistory, - osdUrlStateStorage: createOsdUrlStateStorage({ - history: this.currentHistory, - useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), - ...withNotifyOnErrors(coreStart.notifications.toasts), - }), - toastNotifications: coreStart.notifications.toasts, - data: pluginsStart.data, - savedObjectsPublic: savedObjects, - navigation, - expressions, - setHeaderActionMenu: params.setHeaderActionMenu, - types: typeService.start(), - savedVisBuilderLoader: selfStart.savedVisBuilderLoader, - embeddable: pluginsStart.embeddable, - dashboard: pluginsStart.dashboard, - uiActions: pluginsStart.uiActions, - }; + // This is for instances where the user navigates to the app from the application nav menu + const path = window.location.pathname; + const hash = window.location.hash; + const id = extractSavedVisBuilderId(path); + const editPath = id ? `${EDIT_PATH}/${id}` : hash; + const migratePath = hash? migrateUrlState(`${editPath}/${hash}`) : migrateUrlState(editPath); - // Instantiate the store - const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); - const unmount = renderApp(params, services, store); + navigateToApp('data-explorer', { + replace: true, + path: `/${PLUGIN_ID}${migratePath}`, + }); - // Render the application return () => { unlistenParentHistory(); - unmount(); - appUnMounted(); - unsubscribeStore(); }; }, }); + // Register view in data explorer + dataExplorer.registerView({ + id: PLUGIN_ID, + title: PLUGIN_NAME, + defaultPath: '#/', + appExtentions: { + savedObject: { + docTypes: [VISBUILDER_SAVED_OBJECT], + toListItem: (obj) => ({ + id: obj.id, + label: obj.title, + }), + }, + }, + ui: { + defaults: async () => { + const services: VisBuilderServices = getVisBuilderServices(); + const { savedVisBuilderLoader } = services; + + const path = window.location.pathname; + const savedVisBuilderId = extractSavedVisBuilderId(path); + let savedVisBuilderState; + + if (savedVisBuilderId) { + const savedVisBuilderVis = await getSavedVisBuilderVis( + savedVisBuilderLoader, + savedVisBuilderId + ); + savedVisBuilderState = getStateFromSavedObject(savedVisBuilderVis).state; + } + const sliceProps = { + services, + savedVisBuilderState: savedVisBuilderState || undefined, + }; + + return [ + getEditorSlicePreloadedState(services), + getStyleSlicePreloadedState(sliceProps), + getUiStateSlicePreloadedState(sliceProps), + getVisualizationSlicePreloadedState(sliceProps), + ]; + }, + slices: [editorSlice, styleSlice, uiStateSlice, visualizationSlice], + sideEffects: [handlerEditorState, handlerParentAggs], + }, + shouldShow: () => true, + // ViewComponent + Canvas: lazy(() => import('./application/view_components/canvas')), + Panel: lazy(() => import('./application/view_components/panel')), + Context: lazy(() => import('./application/view_components/context')), + }); + // Register embeddable const start = createStartServicesGetter(core.getStartServices); const embeddableFactory = new VisBuilderEmbeddableFactory({ start }); @@ -215,11 +225,9 @@ export class VisBuilderPlugin }; } - public start( - core: CoreStart, - { expressions, data, uiActions }: VisBuilderPluginStartDependencies - ): VisBuilderStart { + public start(core: CoreStart, plugins: VisBuilderPluginStartDependencies): VisBuilderStart { const typeService = this.typeService.start(); + const { expressions, data, uiActions } = plugins; const savedVisBuilderLoader = createSavedVisBuilderLoader({ savedObjectsClient: core.savedObjects.client, @@ -229,6 +237,9 @@ export class VisBuilderPlugin overlays: core.overlays, }); + const services = buildVisBuilderServices(core, plugins, savedVisBuilderLoader, typeService); + setVisBuilderServices(services); + // Register plugin services setSearchService(data.search); setExpressionLoader(expressions.ExpressionLoader); diff --git a/src/plugins/vis_builder/public/plugin_services.ts b/src/plugins/vis_builder/public/plugin_services.ts index 844a56566d0e..9b9da876f0f7 100644 --- a/src/plugins/vis_builder/public/plugin_services.ts +++ b/src/plugins/vis_builder/public/plugin_services.ts @@ -3,13 +3,36 @@ * SPDX-License-Identifier: Apache-2.0 */ +import _ from 'lodash'; +import { createHashHistory } from 'history'; import { createGetterSetter } from '../../opensearch_dashboards_utils/common'; import { DataPublicPluginStart, TimefilterContract } from '../../data/public'; import { SavedVisBuilderLoader } from './saved_visualizations'; -import { HttpStart, IUiSettingsClient } from '../../../core/public'; +import { HttpStart, IUiSettingsClient, AppMountParameters } from '../../../core/public'; import { ExpressionsStart } from '../../expressions/public'; import { TypeServiceStart } from './services/type_service'; import { UiActionsStart } from '../../ui_actions/public'; +import { VisBuilderServices } from './types'; + +let visBuilderServices: VisBuilderServices | null = null; + +export const getHistory = _.once(() => createHashHistory()); +export const syncHistoryLocations = () => { + const h = getHistory(); + Object.assign(h.location, createHashHistory().location); + return h; +}; + +export function getVisBuilderServices(): VisBuilderServices { + if (!visBuilderServices) { + throw new Error('VisBuilder services have not been initialized.'); + } + return visBuilderServices; +} + +export function setVisBuilderServices(newServices: VisBuilderServices) { + visBuilderServices = newServices; +} export const [getSearchService, setSearchService] = createGetterSetter< DataPublicPluginStart['search'] @@ -43,3 +66,7 @@ export const [getUIActions, setUIActions] = createGetterSetter(' export const [getQueryService, setQueryService] = createGetterSetter< DataPublicPluginStart['query'] >('Query'); + +export const [getHeaderActionMenuMounter, setHeaderActionMenuMounter] = createGetterSetter< + AppMountParameters['setHeaderActionMenu'] +>('headerActionMenuMounter'); diff --git a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts index 9f8dd705e3e4..38e6b0a6d628 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts @@ -4,27 +4,27 @@ */ import { i18n } from '@osd/i18n'; -import produce from 'immer'; import { IndexPattern } from '../../../data/public'; import { InvalidJSONProperty } from '../../../opensearch_dashboards_utils/public'; -import { RenderState, RootState, VisualizationState } from '../application/utils/state_management'; +import { + RenderState, + VisBuilderRootState, + VisualizationState, + MetadataState, +} from '../application/utils/state_management'; import { validateVisBuilderState } from '../application/utils/validations'; import { VisBuilderSavedObject } from '../types'; import { VisBuilderSavedObjectAttributes } from '../../common'; export const saveStateToSavedObject = ( obj: VisBuilderSavedObject, - state: RootState, + state: VisBuilderRootState, indexPattern: IndexPattern ): VisBuilderSavedObject => { - if (state.visualization.indexPattern !== indexPattern.id) + if (state.metadata.indexPattern !== indexPattern.id) throw new Error('indexPattern id should match the value in redux state'); - obj.visualizationState = JSON.stringify( - produce(state.visualization, (draft: VisualizationState) => { - delete draft.indexPattern; - }) - ); + obj.visualizationState = JSON.stringify(state.visualization); obj.styleState = JSON.stringify(state.style); obj.searchSourceFields = { index: indexPattern }; obj.uiState = JSON.stringify(state.ui); @@ -47,6 +47,8 @@ export const getStateFromSavedObject = ( const visualizationState: VisualizationState = { searchField: '', ...vizStateWithoutIndex, + }; + const metadataState: MetadataState = { indexPattern: obj.searchSourceFields?.index, }; @@ -62,7 +64,7 @@ export const getStateFromSavedObject = ( ); } - if (!visualizationState.indexPattern) { + if (!metadataState.indexPattern) { throw new Error( i18n.translate('visBuilder.getStateFromSavedObject.missingIndexPattern', { defaultMessage: 'The saved object is missing an index pattern', @@ -78,6 +80,7 @@ export const getStateFromSavedObject = ( visualization: visualizationState, style: styleState, ui: uiState, + metadata: metadataState, }, }; }; diff --git a/src/plugins/vis_builder/public/services/type_service/types.ts b/src/plugins/vis_builder/public/services/type_service/types.ts index edcc0b659fc7..eaa86b481ed1 100644 --- a/src/plugins/vis_builder/public/services/type_service/types.ts +++ b/src/plugins/vis_builder/public/services/type_service/types.ts @@ -31,6 +31,7 @@ export interface VisualizationTypeOptions { }; readonly toExpression: ( state: RenderState, - searchContext: IExpressionLoaderParams['searchContext'] + indexId: string, + searchContext?: IExpressionLoaderParams['searchContext'] ) => Promise; } diff --git a/src/plugins/vis_builder/public/services/type_service/visualization_type.tsx b/src/plugins/vis_builder/public/services/type_service/visualization_type.tsx index 2f863316435e..908f05e50fc5 100644 --- a/src/plugins/vis_builder/public/services/type_service/visualization_type.tsx +++ b/src/plugins/vis_builder/public/services/type_service/visualization_type.tsx @@ -18,7 +18,8 @@ export class VisualizationType implements IVisualizationType { public readonly ui: IVisualizationType['ui']; public readonly toExpression: ( state: RenderState, - searchContext: IExpressionLoaderParams['searchContext'] + indexPattern: string, + searchContext?: IExpressionLoaderParams['searchContext'] ) => Promise; constructor(options: VisualizationTypeOptions) { diff --git a/src/plugins/vis_builder/public/types.ts b/src/plugins/vis_builder/public/types.ts index 1ba8843e016a..c135a2c46e07 100644 --- a/src/plugins/vis_builder/public/types.ts +++ b/src/plugins/vis_builder/public/types.ts @@ -10,13 +10,14 @@ import { DashboardStart } from '../../dashboard/public'; import { VisualizationsSetup } from '../../visualizations/public'; import { ExpressionsStart } from '../../expressions/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; -import { DataPublicPluginStart } from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { TypeServiceSetup, TypeServiceStart } from './services/type_service'; import { SavedObjectLoader } from '../../saved_objects/public'; -import { AppMountParameters, CoreStart, ToastsStart, ScopedHistory } from '../../../core/public'; -import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public'; -import { DataPublicPluginSetup } from '../../data/public'; +import { CoreStart, ScopedHistory, ToastsStart } from '../../../core/public'; import { UiActionsStart } from '../../ui_actions/public'; +import { DataExplorerPluginSetup, DataExplorerServices } from '../../data_explorer/public'; +import { PLUGIN_ID } from '../common'; +import { syncHistoryLocations } from './plugin_services'; export type VisBuilderSetup = TypeServiceSetup; export interface VisBuilderStart extends TypeServiceStart { @@ -27,6 +28,7 @@ export interface VisBuilderPluginSetupDependencies { embeddable: EmbeddableSetup; visualizations: VisualizationsSetup; data: DataPublicPluginSetup; + dataExplorer: DataExplorerPluginSetup; } export interface VisBuilderPluginStartDependencies { embeddable: EmbeddableStart; @@ -40,7 +42,6 @@ export interface VisBuilderPluginStartDependencies { export interface VisBuilderServices extends CoreStart { appName: string; - setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; savedVisBuilderLoader: VisBuilderStart['savedVisBuilderLoader']; toastNotifications: ToastsStart; savedObjectsPublic: SavedObjectsStart; @@ -48,12 +49,10 @@ export interface VisBuilderServices extends CoreStart { data: DataPublicPluginStart; types: TypeServiceStart; expressions: ExpressionsStart; - history: History; embeddable: EmbeddableStart; - scopedHistory: ScopedHistory; - osdUrlStateStorage: IOsdUrlStateStorage; dashboard: DashboardStart; uiActions: UiActionsStart; + history: History; } export interface ISavedVis { @@ -66,4 +65,32 @@ export interface ISavedVis { version?: number; } +export function buildVisBuilderServices( + core: CoreStart, + plugins: VisBuilderPluginStartDependencies, + savedVisBuilderLoader: any, + typeService: TypeServiceStart +): VisBuilderServices { + // Construct and return the services object + const services: VisBuilderServices = { + // Populate with all necessary services + appName: PLUGIN_ID, + savedVisBuilderLoader, + toastNotifications: core.notifications.toasts, + savedObjectsPublic: plugins.savedObjects, + navigation: plugins.navigation, + data: plugins.data, + types: typeService, + expressions: plugins.expressions, + embeddable: plugins.embeddable, + dashboard: plugins.dashboard, + uiActions: plugins.uiActions, + history: syncHistoryLocations(), + }; + + return services; +} + export interface VisBuilderSavedObject extends SavedObject, ISavedVis {} +// Any component inside the panel and canvas views has access to both these services. +export type VisBuilderViewServices = VisBuilderServices & DataExplorerServices; diff --git a/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts b/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts index f50ab9172cdb..8267c6d561ab 100644 --- a/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts +++ b/src/plugins/vis_builder/public/visualizations/common/expression_helpers.ts @@ -13,13 +13,14 @@ import { StyleState } from '../../application/utils/state_management'; export const getAggExpressionFunctions = async ( visualization: VisualizationState, + indexId: string, style?: StyleState ) => { - const { activeVisualization, indexPattern: indexId = '' } = visualization; + const { activeVisualization } = visualization; const { aggConfigParams } = activeVisualization || {}; - const indexPatternsService = getIndexPatterns(); const indexPattern = await indexPatternsService.get(indexId); + // aggConfigParams is the serealizeable aggConfigs that need to be reconstructed here using the agg servce const aggConfigs = getSearchService().aggs.createAggConfigs( indexPattern, @@ -35,7 +36,7 @@ export const getAggExpressionFunctions = async ( const opensearchaggs = buildExpressionFunction( 'opensearchaggs', { - index: indexId, + index: indexPattern.id ? indexPattern.id : '', metricsAtAllLevels: style?.showMetricsAtAllLevels || false, partialRows: style?.showPartialRows || false, aggConfigs: JSON.stringify(aggConfigs.aggs), @@ -45,7 +46,6 @@ export const getAggExpressionFunctions = async ( return { aggConfigs, - indexPattern, expressionFns: [opensearchDashboards, opensearchaggs], }; }; diff --git a/src/plugins/vis_builder/public/visualizations/metric/components/metric_viz_options.tsx b/src/plugins/vis_builder/public/visualizations/metric/components/metric_viz_options.tsx index 4a626cb01179..6b4e164cd6f2 100644 --- a/src/plugins/vis_builder/public/visualizations/metric/components/metric_viz_options.tsx +++ b/src/plugins/vis_builder/public/visualizations/metric/components/metric_viz_options.tsx @@ -16,14 +16,11 @@ import { RangeOption, SwitchOption, } from '../../../../../charts/public'; -import { - useTypedDispatch, - useTypedSelector, - setStyleState, -} from '../../../application/utils/state_management'; +import { useTypedDispatch, setStyleState } from '../../../application/utils/state_management'; import { MetricOptionsDefaults } from '../metric_viz_type'; import { PersistedState } from '../../../../../visualizations/public'; -import { Option } from '../../../application/app'; +import { Option } from '../../../application/components/option'; +import { useVisBuilderContext } from '../../../application/view_components/context'; const METRIC_COLOR_MODES = [ { @@ -47,7 +44,8 @@ const METRIC_COLOR_MODES = [ ]; function MetricVizOptions() { - const styleState = useTypedSelector((state) => state.style) as MetricOptionsDefaults; + const { rootState } = useVisBuilderContext(); + const styleState = rootState.style as MetricOptionsDefaults; const dispatch = useTypedDispatch(); const { metric } = styleState; diff --git a/src/plugins/vis_builder/public/visualizations/metric/to_expression.ts b/src/plugins/vis_builder/public/visualizations/metric/to_expression.ts index f7d8b4aa2a4c..5f219bdcc65f 100644 --- a/src/plugins/vis_builder/public/visualizations/metric/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/metric/to_expression.ts @@ -86,8 +86,11 @@ export interface MetricRootState extends RenderState { style: MetricOptionsDefaults; } -export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { - const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); +export const toExpression = async ( + { style: styleState, visualization }: MetricRootState, + indexId: string +) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, indexId); // TODO: Update to use the getVisSchemas function from the Visualizations plugin // const schemas = getVisSchemas(vis, params); diff --git a/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx index a77a0811e609..40587c5608d8 100644 --- a/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx +++ b/src/plugins/vis_builder/public/visualizations/table/components/table_viz_options.tsx @@ -10,16 +10,14 @@ import produce from 'immer'; import { Draft } from 'immer'; import { EuiIconTip } from '@elastic/eui'; import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; -import { - useTypedDispatch, - useTypedSelector, - setStyleState, -} from '../../../application/utils/state_management'; +import { useTypedDispatch, setStyleState } from '../../../application/utils/state_management'; import { TableOptionsDefaults } from '../table_viz_type'; -import { Option } from '../../../application/app'; +import { Option } from '../../../application/components/option'; +import { useVisBuilderContext } from '../../../application/view_components/context'; function TableVizOptions() { - const styleState = useTypedSelector((state) => state.style) as TableOptionsDefaults; + const { rootState } = useVisBuilderContext(); + const styleState = rootState.style as TableOptionsDefaults; const dispatch = useTypedDispatch(); const setOption = useCallback( diff --git a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts index bbec4c1cc7e9..ca0e87e9b00e 100644 --- a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts @@ -96,8 +96,15 @@ export interface TableRootState extends RenderState { style: TableOptionsDefaults; } -export const toExpression = async ({ style: styleState, visualization }: TableRootState) => { - const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, styleState); +export const toExpression = async ( + { style: styleState, visualization }: TableRootState, + indexId: string +) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions( + visualization, + indexId, + styleState + ); const { showPartialRows, showMetricsAtAllLevels } = styleState; const schemas = getVisSchemas(aggConfigs, showMetricsAtAllLevels); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/area/components/area_vis_options.tsx b/src/plugins/vis_builder/public/visualizations/vislib/area/components/area_vis_options.tsx index 4b3116c83992..84f6f97bd592 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/area/components/area_vis_options.tsx +++ b/src/plugins/vis_builder/public/visualizations/vislib/area/components/area_vis_options.tsx @@ -6,20 +6,21 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import produce, { Draft } from 'immer'; -import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { useTypedDispatch, setStyleState } from '../../../../application/utils/state_management'; import { AreaOptionsDefaults } from '../area_vis_type'; -import { setState } from '../../../../application/utils/state_management/style_slice'; -import { Option } from '../../../../application/app'; +import { Option } from '../../../../application/components/option'; import { BasicVisOptions } from '../../common/basic_vis_options'; +import { useVisBuilderContext } from '../../../../application/view_components/context'; function AreaVisOptions() { - const styleState = useTypedSelector((state) => state.style) as AreaOptionsDefaults; + const { rootState } = useVisBuilderContext(); + const styleState = rootState.style as AreaOptionsDefaults; const dispatch = useTypedDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { const newState = produce(styleState, callback); - dispatch(setState(newState)); + dispatch(setStyleState(newState)); }, [dispatch, styleState] ); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/area/to_expression.ts b/src/plugins/vis_builder/public/visualizations/vislib/area/to_expression.ts index 4481dce24619..283875331efa 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/area/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/vislib/area/to_expression.ts @@ -13,15 +13,17 @@ import { AreaOptionsDefaults } from './area_vis_type'; import { getAggExpressionFunctions } from '../../common/expression_helpers'; import { VislibRootState, getValueAxes, getPipelineParams } from '../common'; import { createVis } from '../common/create_vis'; +import { getIndexPatterns } from '../../../plugin_services'; export const toExpression = async ( { style: styleState, visualization }: VislibRootState, + indexId: string, searchContext: IExpressionLoaderParams['searchContext'] ) => { - const { aggConfigs, expressionFns, indexPattern } = await getAggExpressionFunctions( - visualization - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, indexId); const { addLegend, addTooltip, legendPosition, type } = styleState; + const indexPatternsService = getIndexPatterns(); + const indexPattern = await indexPatternsService.get(indexId); const vis = await createVis(type, aggConfigs, indexPattern, searchContext?.timeRange); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx b/src/plugins/vis_builder/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx index 873b26ca4301..20c1d82ec8f9 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx +++ b/src/plugins/vis_builder/public/visualizations/vislib/histogram/components/histogram_vis_options.tsx @@ -6,20 +6,21 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import produce, { Draft } from 'immer'; -import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { useTypedDispatch, setStyleState } from '../../../../application/utils/state_management'; import { HistogramOptionsDefaults } from '../histogram_vis_type'; import { BasicVisOptions } from '../../common/basic_vis_options'; -import { setState } from '../../../../application/utils/state_management/style_slice'; -import { Option } from '../../../../application/app'; +import { Option } from '../../../../application/components/option'; +import { useVisBuilderContext } from '../../../../application/view_components/context'; function HistogramVisOptions() { - const styleState = useTypedSelector((state) => state.style) as HistogramOptionsDefaults; + const { rootState } = useVisBuilderContext(); + const styleState = rootState.style as HistogramOptionsDefaults; const dispatch = useTypedDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { const newState = produce(styleState, callback); - dispatch(setState(newState)); + dispatch(setStyleState(newState)); }, [dispatch, styleState] ); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/histogram/to_expression.ts b/src/plugins/vis_builder/public/visualizations/vislib/histogram/to_expression.ts index 2f75ed326913..c26d71eca405 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/histogram/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/vislib/histogram/to_expression.ts @@ -13,15 +13,17 @@ import { HistogramOptionsDefaults } from './histogram_vis_type'; import { getAggExpressionFunctions } from '../../common/expression_helpers'; import { VislibRootState, getValueAxes, getPipelineParams } from '../common'; import { createVis } from '../common/create_vis'; +import { getIndexPatterns } from '../../../plugin_services'; export const toExpression = async ( { style: styleState, visualization }: VislibRootState, + indexId: string, searchContext: IExpressionLoaderParams['searchContext'] ) => { - const { aggConfigs, expressionFns, indexPattern } = await getAggExpressionFunctions( - visualization - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, indexId); const { addLegend, addTooltip, legendPosition, type } = styleState; + const indexPatternsService = getIndexPatterns(); + const indexPattern = await indexPatternsService.get(indexId); const vis = await createVis(type, aggConfigs, indexPattern, searchContext?.timeRange); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/line/components/line_vis_options.tsx b/src/plugins/vis_builder/public/visualizations/vislib/line/components/line_vis_options.tsx index a5bb1994c92a..b2cad5330e4b 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/line/components/line_vis_options.tsx +++ b/src/plugins/vis_builder/public/visualizations/vislib/line/components/line_vis_options.tsx @@ -6,20 +6,21 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import produce, { Draft } from 'immer'; -import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { useTypedDispatch, setStyleState } from '../../../../application/utils/state_management'; import { LineOptionsDefaults } from '../line_vis_type'; -import { setState } from '../../../../application/utils/state_management/style_slice'; -import { Option } from '../../../../application/app'; +import { Option } from '../../../../application/components/option'; import { BasicVisOptions } from '../../common/basic_vis_options'; +import { useVisBuilderContext } from '../../../../application/view_components/context'; function LineVisOptions() { - const styleState = useTypedSelector((state) => state.style) as LineOptionsDefaults; + const { rootState } = useVisBuilderContext(); + const styleState = rootState.style as LineOptionsDefaults; const dispatch = useTypedDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { const newState = produce(styleState, callback); - dispatch(setState(newState)); + dispatch(setStyleState(newState)); }, [dispatch, styleState] ); diff --git a/src/plugins/vis_builder/public/visualizations/vislib/line/to_expression.ts b/src/plugins/vis_builder/public/visualizations/vislib/line/to_expression.ts index 41a6d505c724..bad6d04c403b 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/line/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/vislib/line/to_expression.ts @@ -13,15 +13,17 @@ import { LineOptionsDefaults } from './line_vis_type'; import { getAggExpressionFunctions } from '../../common/expression_helpers'; import { VislibRootState, getValueAxes, getPipelineParams } from '../common'; import { createVis } from '../common/create_vis'; +import { getIndexPatterns } from '../../../plugin_services'; export const toExpression = async ( { style: styleState, visualization }: VislibRootState, + indexId: string, searchContext: IExpressionLoaderParams['searchContext'] ) => { - const { aggConfigs, expressionFns, indexPattern } = await getAggExpressionFunctions( - visualization - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, indexId); const { addLegend, addTooltip, legendPosition, type } = styleState; + const indexPatternsService = getIndexPatterns(); + const indexPattern = await indexPatternsService.get(indexId); const vis = await createVis(type, aggConfigs, indexPattern, searchContext?.timeRange); diff --git a/src/plugins/vis_builder/server/capabilities_provider.ts b/src/plugins/vis_builder/server/capabilities_provider.ts index c810efabdfe5..54699da885e3 100644 --- a/src/plugins/vis_builder/server/capabilities_provider.ts +++ b/src/plugins/vis_builder/server/capabilities_provider.ts @@ -4,7 +4,7 @@ */ export const capabilitiesProvider = () => ({ - 'visualization-visbuilder': { + 'visualization-visbuilder-new': { // TODO: investigate which capabilities we need to provide // createNew: true, // createShortUrl: true,