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..f9b9c206c7a2 100644 --- a/src/plugins/data_explorer/public/utils/state_management/preload.ts +++ b/src/plugins/data_explorer/public/utils/state_management/preload.ts @@ -7,6 +7,7 @@ import { PreloadedState } from '@reduxjs/toolkit'; import { getPreloadedState as getPreloadedMetadataState } from './metadata_slice'; import { RootState } from './store'; import { DataExplorerServices } from '../../types'; +import { DefaultViewState } from '../../services/view_service'; export const getPreloadedState = async ( services: DataExplorerServices @@ -22,19 +23,37 @@ 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 id = slice.name; + rootState[id] = state.state; + } catch (e) { + // eslint-disable-next-line no-console + console.error(`Error initializing slice: ${e}`); + } + }) + ); + } else { + slices.forEach((slice) => { + const id = slice.name; + rootState[id] = 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..ec7c3f4aacbd 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); + + // 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,13 @@ 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[]) => { + slices.forEach((slice) => { + if (dynamicReducers[slice.name]) { + throw new Error(`Slice ${slice.name} already registered`); + } + dynamicReducers[slice.name] = slice.reducer; + }); }; // Infer the `RootState` and `AppDispatch` types from the store itself 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/opensearch_dashboards.json b/src/plugins/vis_builder/opensearch_dashboards.json index 477deb4db841..65d3c142108b 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", @@ -21,4 +22,4 @@ "visDefaultEditor", "visTypeVislib" ] -} \ No newline at end of file +} 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/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/config_panel.scss b/src/plugins/vis_builder/public/application/components/config_panel/index.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/index.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/index.tsx similarity index 78% 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/index.tsx index ec3b6b60a096..74a20781655b 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/config_panel.tsx +++ b/src/plugins/vis_builder/public/application/components/config_panel/index.tsx @@ -6,16 +6,15 @@ 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 { useSelector } from '../../utils/state_management'; import { mapSchemaToAggPanel } from './schema_to_dropbox'; import { SecondaryPanel } from './secondary_panel'; +import './index.scss'; + export function ConfigPanel() { const vizType = useVisualizationType(); - const editingState = useTypedSelector( - (state) => state.visualization.activeVisualization?.draftAgg - ); + const editingState = useSelector((state) => state.vbVisualization.activeVisualization?.draftAgg); const schemas = vizType.ui.containerConfig.data.schemas; if (!schemas) return null; 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 89% 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..9d927eefd858 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,33 @@ 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 { useDispatch, useSelector } 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 { draftAgg, aggConfigParams } = useSelector( + (state) => state.vbVisualization.activeVisualization! ); - const isEditorValid = useTypedSelector((state) => !state.metadata.editor.errors[PANEL_KEY]); + const isEditorValid = useSelector((state) => !state.vbEditor.errors[PANEL_KEY]); const [touched, setTouched] = useState(false); - const dispatch = useTypedDispatch(); + const dispatch = useDispatch(); const vizType = useVisualizationType(); - const indexPattern = useIndexPatterns().selected; - const { services } = useOpenSearchDashboards(); + const { indexPattern } = useVisBuilderContext(); + 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 98% 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..cda71d20f6d1 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 @@ -8,7 +8,7 @@ import { cloneDeep } from 'lodash'; import { BucketAggType, IndexPatternField, propFilter } from '../../../../../../data/common'; import { Schema } from '../../../../../../vis_default_editor/public'; import { COUNT_FIELD, FieldDragDataType } from '../../../utils/drag_drop/types'; -import { useTypedDispatch } from '../../../utils/state_management'; +import { useDispatch } from '../../../utils/state_management'; import { DropboxDisplay, DropboxProps } from '../dropbox'; import { useDrop } from '../../../utils/drag_drop'; import { @@ -31,7 +31,7 @@ export const useDropbox = (props: UseDropboxProps): DropboxProps => { const { id: dropboxId, label, schema } = props; const [validAggTypes, setValidAggTypes] = useState([]); const { aggConfigs, indexPattern, aggs, timeRange } = useAggs(); - const dispatch = useTypedDispatch(); + const dispatch = useDispatch(); const { services: { data: { 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 100% 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 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 100% 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 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 92% 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 index 3998e146933e..535ec0edfa86 100644 --- 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 @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@osd/i18n'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { setSearchField } from '../../utils/state_management/visualization_slice'; -import { useTypedDispatch } from '../../utils/state_management'; +import { useDispatch } from '../../utils/state_management'; export interface Props { /** @@ -25,7 +25,7 @@ export function FieldSearch({ value }: Props) { defaultMessage: 'Search field names', }); - const dispatch = useTypedDispatch(); + const dispatch = useDispatch(); return ( 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..53d209a260c3 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 @@ -7,17 +7,19 @@ .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..9bfdc1a80163 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 '.'; 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 89% 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..4d701a495876 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,14 @@ 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 { useSelector } from '../../utils/state_management'; +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 +25,20 @@ interface IFieldCategories { } export const FieldSelector = () => { - const indexPattern = useIndexPatterns().selected; - const fieldSearchValue = useTypedSelector((state) => state.visualization.searchField); + const { indexPattern } = useVisBuilderContext(); + const fieldSearchValue = useSelector((state) => state.vbVisualization.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..d01d1aee10f5 100644 --- a/src/plugins/vis_builder/public/application/components/option.scss +++ b/src/plugins/vis_builder/public/application/components/option.scss @@ -1,3 +1,7 @@ +/* + * 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..be65571070ef 100644 --- a/src/plugins/vis_builder/public/application/components/right_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/right_nav.tsx @@ -14,14 +14,13 @@ 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, + useDispatch, + useSelector, } from '../utils/state_management'; import { getPersistedAggParams } from '../utils/get_persisted_agg_params'; @@ -30,11 +29,11 @@ export const RightNavUI = () => { const [confirmAggs, setConfirmAggs] = useState(); const { services: { types }, - } = useOpenSearchDashboards(); - const dispatch = useTypedDispatch(); + } = useOpenSearchDashboards(); + const dispatch = useDispatch(); const StyleSection = ui.containerConfig.style.render; - const { activeVisualization } = useTypedSelector((state) => state.visualization); + const { activeVisualization } = useSelector((state) => state.vbVisualization); 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 deleted file mode 100644 index 1a69071a697e..000000000000 --- a/src/plugins/vis_builder/public/application/components/side_nav.scss +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ -@import "../util"; -@import "../variables"; - -.vbSidenav { - @include scrollNavParent(auto 1fr); - - &.left { - border-right: $euiBorderThin; - grid-area: leftNav; - } - - &.right { - border-left: $euiBorderThin; - grid-area: rightNav; - height: 100%; - } - - &__header { - padding: $euiSizeS; - border-bottom: $euiBorderThin; - background-color: $euiColorEmptyShade; - } - - &__style { - @include euiYScrollWithShadows; - } -} - -.vbTypeSelector__icon { - margin-right: $euiSizeS; -} - -.vbSidenavTabs { - .euiTab__content { - text-transform: capitalize; - } - - @include scrollNavParent(min-content 1fr); - - & > [role="tabpanel"] { - @include scrollNavParent; - } -} - -.vbDatasourceSelect { - max-width: calc(#{$vbLeftNavWidth} - 1px); -} 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..f42b082233ae 100644 --- a/src/plugins/vis_builder/public/application/components/top_nav.tsx +++ b/src/plugins/vis_builder/public/application/components/top_nav.tsx @@ -4,34 +4,42 @@ */ import React, { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { matchPath } from 'react-router-dom'; 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 { useIndexPattern, useSavedVisBuilderVis } from '../utils/use'; +import { useSelector, useDispatch } 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'; -export const TopNav = () => { +export const TopNav = ({ + setHeaderActionMenu, +}: { + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; +}) => { // id will only be set for the edit route - const { id: visualizationIdFromUrl } = useParams<{ id: string }>(); - const { services } = useOpenSearchDashboards(); + const hashPath = window.location.hash.split('?')[0]; // hack to remove query params since matchPath considers them part of the id + const visualizationIdFromUrl = matchPath<{ id?: string }>(hashPath, { + path: '#/edit/:id', + })?.params.id; + const { services } = useOpenSearchDashboards(); + const { - setHeaderActionMenu, navigation: { ui: { TopNavMenu }, }, appName, } = services; - const rootState = useTypedSelector((state) => state); - const dispatch = useTypedDispatch(); + const rootState = useSelector((state) => state); + const dispatch = useDispatch(); const saveDisabledReason = useCanSave(); const savedVisBuilderVis = useSavedVisBuilderVis(visualizationIdFromUrl); @@ -39,9 +47,9 @@ export const TopNav = () => { filters: opensearchFilters.FilterStateStore.APP_STATE, query: true, }); - const { selected: indexPattern } = useIndexPatterns(); + const indexPattern = useIndexPattern(services); const [config, setConfig] = useState(); - const originatingApp = useTypedSelector((state) => { + const originatingApp = useSelector((state) => { return state.metadata.originatingApp; }); @@ -75,7 +83,7 @@ export const TopNav = () => { // 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..e62c543a4428 100644 --- a/src/plugins/vis_builder/public/application/components/workspace.tsx +++ b/src/plugins/vis_builder/public/application/components/workspace.tsx @@ -8,10 +8,11 @@ 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 { useDispatch, useSelector, setUIStateState } from '../utils/state_management'; import { useAggs, useVisualizationType } from '../utils/use'; +import { useIndexPattern } from '../utils/use'; import { PersistedState } from '../../../../visualizations/public'; import hand_field from '../../assets/hand_field.svg'; @@ -20,37 +21,38 @@ import fields_bg from '../../assets/fields_bg.svg'; import './workspace.scss'; import { ExperimentalInfo } from './experimental_info'; import { handleVisEvent } from '../utils/handle_vis_event'; +import { useVisBuilderContext } from '../view_components/context'; 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 dispatch = useTypedDispatch(); + const rootState = useSelector((state) => state); + const dispatch = useDispatch(); // 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 uiState = useMemo(() => new PersistedState(rootState.vbUi), []); + const indexId = rootState.metadata.indexPattern ? rootState.metadata.indexPattern : ''; useEffect(() => { - if (rootState.metadata.editor.state === 'loaded') { - uiState.setSilent(rootState.ui); + if (rootState.vbEditor.status === 'loaded') { + uiState.setSilent(rootState.vbUi); } // To update uiState once saved object data is loaded // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rootState.metadata.editor.state, uiState]); + }, [rootState.vbEditor.status, uiState]); useEffect(() => { uiState.on('change', (args) => { @@ -64,7 +66,7 @@ export const WorkspaceUI = () => { const schemas = ui.containerConfig.data.schemas; const noAggs = (aggConfigs?.aggs?.length ?? 0) === 0; - const schemaValidation = validateSchemaState(schemas, rootState.visualization); + const schemaValidation = validateSchemaState(schemas, rootState.vbVisualization); const aggValidation = validateAggregations(aggConfigs?.aggs || []); if (!aggValidation.valid || !schemaValidation.valid) { @@ -81,13 +83,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 }) => { 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..d6ebb4b2d704 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,8 @@ * specific language governing permissions and limitations * under the License. */ - +import _ from 'lodash'; +import { createHashHistory } from 'history'; import React from 'react'; import { i18n } from '@osd/i18n'; import { TopNavMenuData } from '../../../../navigation/public'; @@ -36,13 +37,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 { setStatus } 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 +58,7 @@ export const getTopNavConfig = ( dispatch, originatingApp, }: TopNavConfigParams, - services: VisBuilderServices + services: VisBuilderViewServices ) => { const { i18n: { Context: I18nContext }, @@ -220,12 +221,9 @@ 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(setStatus({ status: 'clean' })); } 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..57ee3fa5e07a --- /dev/null +++ b/src/plugins/vis_builder/public/application/utils/state_management/editor_slice.ts @@ -0,0 +1,59 @@ +/* + * 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 + */ +type EditorStatus = 'loading' | 'loaded' | 'clean' | 'dirty'; + +export interface EditorState { + errors: { + // Errors for each section in the editor + [key: string]: boolean; + }; + status: EditorStatus; +} + +const initialState: EditorState = { + errors: {}, + status: 'loading', +}; + +export const getPreloadedState = async ( + services: VisBuilderServices +): Promise> => { + const preloadedState: DefaultViewState = { + state: { + ...initialState, + }, + }; + return preloadedState; +}; + +export const slice = createSlice({ + name: 'vbEditor', + 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; + }, + setState: (_state, action: PayloadAction) => { + return action.payload; + }, + }, +}); + +export const { reducer } = slice; +export const { setError, setStatus, 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..3db4e53f8fb3 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,38 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { setEditorState } from '../metadata_slice'; -import { RootState, Store } from '../store'; +import { setStatus } from '../editor_slice'; +import { RootState, Store } from '../../../../../../data_explorer/public'; +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: RootState, + previousState: RootState, + services: VisBuilderViewServices +) => { + const editor = state.vbEditor; + const prevEditor = previousState.vbEditor; + const renderState = { + vbStyle: state.vbStyle, + vbUi: state.vbUi, + vbVisualization: state.vbVisualization, + }; + const prevRenderState = { + vbStyle: previousState.vbStyle, + vbUi: previousState.vbUi, + vbVisualization: previousState.vbVisualization, + }; // 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..a85fb9c52e2c 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,8 @@ 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 { RootState, Store } from '../../../../../../data_explorer/public'; import { setAggParamValue } from '../visualization_slice'; /** @@ -16,11 +16,11 @@ import { setAggParamValue } from '../visualization_slice'; export const handlerParentAggs = async ( store: Store, state: RootState, - services: VisBuilderServices + previousState: RootState, + services: VisBuilderViewServices ) => { - const { - visualization: { activeVisualization, indexPattern = '' }, - } = state; + const { activeVisualization } = state.vbVisualization; + const { indexPattern = '' } = state.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..88312bb97c69 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 @@ -3,6 +3,73 @@ * SPDX-License-Identifier: Apache-2.0 */ -export * from './store'; -export * from './hooks'; +import { TypedUseSelectorHook } from 'react-redux'; +import { + AppDispatch, + RootState, + MetadataState, + setIndexPattern as updateIndexPattern, + useTypedDispatch, + useTypedSelector, +} from '../../../../../data_explorer/public'; +import { + 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 './handlers'; export * from './shared_actions'; + +export interface VisBuilderRootState extends RootState { + vbEditor: EditorState; + vbStyle: StyleState; + vbUi: UIStateState; + vbVisualization: VisualizationState; +} + +export const useSelector: TypedUseSelectorHook = useTypedSelector; +export const useDispatch = useTypedDispatch; +export { + editorSlice, + styleSlice, + uiStateSlice, + visualizationSlice, + getEditorSlicePreloadedState, + getStyleSlicePreloadedState, + getUiStateSlicePreloadedState, + getVisualizationSlicePreloadedState, + EditorState, + StyleState, + UIStateState, + VisualizationState, + setEditorState, + setStyleState, + setVisualizationState, + setUIStateState, +}; +export { updateIndexPattern, AppDispatch, MetadataState }; + +export type RenderState = Pick< + VisBuilderRootState, + 'vbStyle' | 'vbUi' | 'vbVisualization' | 'metadata' +>; 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/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..5cbcdf41010e 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 @@ -6,6 +6,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { VisBuilderServices } from '../../../types'; import { setActiveVisualization } from './shared_actions'; +import { DefaultViewState } from '../../../../../data_explorer/public'; export type StyleState = T; @@ -13,21 +14,24 @@ const initialState = {} as StyleState; export const getPreloadedState = async ({ types, - data, -}: VisBuilderServices): Promise => { - let preloadedState = initialState; +}: VisBuilderServices): Promise> => { + const preloadedState: DefaultViewState = { + state: { + ...initialState, + }, + }; const defaultVisualization = types.all()[0]; const defaultState = defaultVisualization.ui.containerConfig.style.defaults; if (defaultState) { - preloadedState = defaultState; + preloadedState.state = defaultState; } return preloadedState; }; export const styleSlice = createSlice({ - name: 'style', + name: 'vbStyle', initialState, reducers: { setState(state: T, action: PayloadAction>) { 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..205ce29c26b0 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,23 +2,27 @@ * 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'; export type UIStateState = T; const initialState = {} as UIStateState; -export const getPreloadedState = async ({ - types, - data, -}: VisBuilderServices): Promise => { - return initialState; +export const getPreloadedState = async ( + services: VisBuilderServices +): Promise> => { + const preloadedState: DefaultViewState = { + state: { + ...initialState, + }, + }; + return preloadedState; }; export const uiStateSlice = createSlice({ - name: 'ui', + name: 'vbUi', initialState, reducers: { setState(state: T, action: PayloadAction>) { 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..d34df1202cb6 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 @@ -7,9 +7,9 @@ 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'; export interface VisualizationState { - indexPattern?: string; searchField: string; activeVisualization?: { name: string; @@ -25,30 +25,30 @@ const initialState: VisualizationState = { export const getPreloadedState = async ({ types, data, -}: VisBuilderServices): Promise => { - const preloadedState = { ...initialState }; +}: VisBuilderServices): Promise> => { + const preloadedState: DefaultViewState = { + state: { + ...initialState, + }, + }; const defaultVisualization = types.all()[0]; - const defaultIndexPattern = await data.indexPatterns.getDefault(); const name = defaultVisualization.name; - if (name && defaultIndexPattern) { - preloadedState.activeVisualization = { + if (name) { + preloadedState.state.activeVisualization = { name, aggConfigParams: [], }; - - preloadedState.indexPattern = defaultIndexPattern.id; } return preloadedState; }; export const slice = createSlice({ - name: 'visualization', + name: 'vbVisualization', initialState, reducers: { - setIndexPattern: (state, action: PayloadAction) => { - state.indexPattern = action.payload; + setActiveVisualization: (state, action: PayloadAction) => { state.activeVisualization!.aggConfigParams = []; state.activeVisualization!.draftAgg = undefined; }, @@ -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..d97554322590 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,31 +6,30 @@ 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 { useSelector, useDispatch } from '../state_management'; +import { useIndexPattern } from './use_index_pattern'; /** * 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 indexPattern = useIndexPattern(services); const [timeRange, setTimeRange] = useState(timefilter.getTime()); - const aggConfigParams = useTypedSelector( - (state) => state.visualization.activeVisualization?.aggConfigParams + const aggConfigParams = useSelector( + (state) => state.vbVisualization.activeVisualization?.aggConfigParams ); - const dispatch = useTypedDispatch(); + const dispatch = useDispatch(); const aggConfigs = useMemo(() => { const configs = 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..056c9036bf0a 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,21 @@ */ import { i18n } from '@osd/i18n'; -import { useTypedSelector } from '../state_management'; +import { useSelector } from '../state_management'; export const useCanSave = () => { - const isEmpty = useTypedSelector( - (state) => state.visualization.activeVisualization?.aggConfigParams?.length === 0 + const isEmpty = useSelector( + (state) => state.vbVisualization.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 hasNoChange = useSelector((state) => state.vbEditor.status !== 'dirty'); + const hasDraftAgg = useSelector((state) => !!state.vbVisualization.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 +26,8 @@ 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 (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..dacc38f5231c --- /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 { useSelector, updateIndexPattern } from '../state_management'; +import { getIndexPatternId } from '../helpers/index_pattern_helper'; + +export const useIndexPattern = (services: VisBuilderViewServices): IndexPattern => { + const indexPatternIdFromState = useSelector((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..196bf396fdcd 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 @@ -12,33 +12,33 @@ import { SavedObjectNotFound, } from '../../../../../opensearch_dashboards_utils/public'; import { EDIT_PATH, PLUGIN_ID } from '../../../../common'; -import { VisBuilderServices } from '../../../types'; +import { VisBuilderViewServices, VisBuilderServices } from '../../../types'; import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs'; import { - useTypedDispatch, + useDispatch, setStyleState, setVisualizationState, setUIStateState, } 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(); + const { services } = useOpenSearchDashboards(); const [savedVisState, setSavedVisState] = useState(undefined); - const dispatch = useTypedDispatch(); + const dispatch = useDispatch(); useEffect(() => { const { application: { navigateToApp }, chrome, - history, http: { basePath }, toastNotifications, savedVisBuilderLoader, + history, } = services; const toastNotification = (message: string) => { toastNotifications.addDanger({ @@ -51,7 +51,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 @@ -62,16 +62,16 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined chrome.setBreadcrumbs(getEditBreadcrumbs(title, navigateToApp)); chrome.docTitle.change(title); - dispatch(setUIStateState(state.ui)); - dispatch(setStyleState(state.style)); - dispatch(setVisualizationState(state.visualization)); - dispatch(setEditorState({ state: 'loaded' })); + dispatch(setUIStateState(state.vbUi)); + dispatch(setStyleState(state.vbStyle)); + dispatch(setVisualizationState(state.vbVisualization)); + dispatch(setStatus({ status: 'loaded' })); } else { chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp)); } setSavedVisState(savedVisBuilderVis); - dispatch(setEditorState({ state: 'clean' })); + dispatch(setStatus({ status: 'clean' })); } catch (error) { const managementRedirectTarget = { [PLUGIN_ID]: { 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..bf79bc18bbc7 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,14 @@ 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 { useSelector } from '../state_management'; export const useVisualizationType = (): VisualizationType => { - const { activeVisualization } = useTypedSelector((state) => state.visualization); + const { activeVisualization } = useSelector((state) => state.vbVisualization); const { services: { types }, - } = useOpenSearchDashboards(); + } = useOpenSearchDashboards(); const visualizationType = types.get(activeVisualization?.name ?? ''); 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 83% 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..6790e5acc50a 100644 --- a/src/plugins/vis_builder/public/application/_variables.scss +++ b/src/plugins/vis_builder/public/application/view_components/canvas/_variables.scss @@ -6,4 +6,5 @@ @import "@elastic/eui/src/global_styling/variables/form"; $osdHeaderOffset: $euiHeaderHeightCompensation; -$vbLeftNavWidth: 462px; +$vbLeftNavWidth: 1585px; +$vbRightNavWidth: 1550px; 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 78% 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..6a48abcee871 100644 --- a/src/plugins/vis_builder/public/application/app.scss +++ b/src/plugins/vis_builder/public/application/view_components/canvas/canvas.scss @@ -8,8 +8,8 @@ padding: 0; display: grid; grid-template: - "topNav topNav" min-content - "leftNav workspaceNav" 1fr / #{$vbLeftNavWidth} 1fr; + "topNav topNav topNav" min-content + "leftNav workspaceNav rightNav" 1fr / #{$vbLeftNavWidth} 1fr #{$vbRightNavWidth}; 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..3d85b192f62d --- /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'; + +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/components/data_tab/index.scss b/src/plugins/vis_builder/public/application/view_components/panel/index.scss similarity index 83% rename from src/plugins/vis_builder/public/application/components/data_tab/index.scss rename to src/plugins/vis_builder/public/application/view_components/panel/index.scss index 764ed72fd373..3feaf0ceb751 100644 --- a/src/plugins/vis_builder/public/application/components/data_tab/index.scss +++ b/src/plugins/vis_builder/public/application/view_components/panel/index.scss @@ -8,5 +8,5 @@ @include scrollNavParent; display: grid; - grid-template-columns: 50% 50%; + grid-template-columns: 100%; } 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..96d7f3375983 --- /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 './index.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/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..debfe7a90b9c --- /dev/null +++ b/src/plugins/vis_builder/public/application/view_components/utils/use_vis_builder_state.ts @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisBuilderViewServices } from '../../../types'; +import { useIndexPattern } from '../../utils/use'; + +export const useVisBuilderState = (services: VisBuilderViewServices) => { + const indexPattern = useIndexPattern(services); + + return { indexPattern }; +}; + +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..d23e92bfd4ec 100644 --- a/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx +++ b/src/plugins/vis_builder/public/embeddable/vis_builder_embeddable.tsx @@ -116,7 +116,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..399a997a4ce0 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.vbVisualization.indexPattern || '' ); const indexPatterns = indexPattern ? [indexPattern] : []; diff --git a/src/plugins/vis_builder/public/plugin.ts b/src/plugins/vis_builder/public/plugin.ts index 4e8f020d1fe8..61f5eefbe67d 100644 --- a/src/plugins/vis_builder/public/plugin.ts +++ b/src/plugins/vis_builder/public/plugin.ts @@ -5,7 +5,13 @@ import { i18n } from '@osd/i18n'; import { BehaviorSubject } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; +import { lazy } from 'react'; +import { + // createOsdUrlStateStorage, + createOsdUrlTracker, + // createStartServicesGetter, + withNotifyOnErrors, +} from '../../opensearch_dashboards_utils/public'; import { AppMountParameters, AppNavLinkStatus, @@ -14,7 +20,6 @@ import { CoreStart, Plugin, PluginInitializerContext, - ScopedHistory, } from '../../../core/public'; import { VisBuilderPluginSetupDependencies, @@ -32,30 +37,39 @@ 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'; export class VisBuilderPlugin implements @@ -68,42 +82,14 @@ export class VisBuilderPlugin 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 +101,73 @@ 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(); - - // 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(() => { 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, - }; - - // Instantiate the store - const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services); - const unmount = renderApp(params, services, store); + // 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. + // this.currentHistory.listen(() => { + // window.dispatchEvent(new HashChangeEvent('hashchange')); + // }); + + // This is for instances where the user navigates to the app from the application nav menu + const path = window.location.hash; + navigateToApp('data-explorer', { + replace: true, + path: `/${PLUGIN_ID}${path}`, + }); - // 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(); + + return [ + getEditorSlicePreloadedState(services), + getStyleSlicePreloadedState(services), + getUiStateSlicePreloadedState(services), + getVisualizationSlicePreloadedState(services), + ]; + }, + 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 +208,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 +220,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..ee6befa93773 100644 --- a/src/plugins/vis_builder/public/saved_visualizations/transforms.ts +++ b/src/plugins/vis_builder/public/saved_visualizations/transforms.ts @@ -7,27 +7,28 @@ 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.styleState = JSON.stringify(state.style); + obj.visualizationState = JSON.stringify(state.vbVisualization); + obj.styleState = JSON.stringify(state.vbStyle); obj.searchSourceFields = { index: indexPattern }; - obj.uiState = JSON.stringify(state.ui); + obj.uiState = JSON.stringify(state.vbUi); return obj; }; @@ -47,6 +48,8 @@ export const getStateFromSavedObject = ( const visualizationState: VisualizationState = { searchField: '', ...vizStateWithoutIndex, + }; + const metadataState: MetadataState = { indexPattern: obj.searchSourceFields?.index, }; @@ -62,7 +65,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', @@ -75,9 +78,10 @@ export const getStateFromSavedObject = ( title, description, state: { - visualization: visualizationState, - style: styleState, - ui: uiState, + vbVisualization: visualizationState, + vbStyle: styleState, + vbUi: 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..0395b0d5aff1 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 @@ -6,6 +6,7 @@ import { IconType } from '@elastic/eui'; import { IExpressionLoaderParams } from '../../../../expressions/public'; import { RenderState } from '../../application/utils/state_management'; import { VisualizationTypeOptions } from './types'; +import { IndexPattern } from '../../../../data/common'; type IVisualizationType = VisualizationTypeOptions; @@ -18,7 +19,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..ca2f137f89c8 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 @@ -17,13 +17,13 @@ import { SwitchOption, } from '../../../../../charts/public'; import { - useTypedDispatch, - useTypedSelector, + useDispatch, + useSelector, 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'; const METRIC_COLOR_MODES = [ { @@ -47,8 +47,8 @@ const METRIC_COLOR_MODES = [ ]; function MetricVizOptions() { - const styleState = useTypedSelector((state) => state.style) as MetricOptionsDefaults; - const dispatch = useTypedDispatch(); + const styleState = useSelector((state) => state.vbStyle) as MetricOptionsDefaults; + const dispatch = useDispatch(); const { metric } = styleState; const setOption = useCallback( 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..0ab9c0f1de4a 100644 --- a/src/plugins/vis_builder/public/visualizations/metric/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/metric/to_expression.ts @@ -5,7 +5,7 @@ import { SchemaConfig } from '../../../../visualizations/public'; import { MetricVisExpressionFunctionDefinition } from '../../../../vis_type_metric/public'; -import { AggConfigs, IAggConfig } from '../../../../data/common'; +import { AggConfigs, IAggConfig, IndexPattern } from '../../../../data/common'; import { buildExpression, buildExpressionFunction } from '../../../../expressions/public'; import { RenderState } from '../../application/utils/state_management'; import { MetricOptionsDefaults } from './metric_viz_type'; @@ -83,11 +83,14 @@ const getVisSchemas = (aggConfigs: AggConfigs): any => { }; export interface MetricRootState extends RenderState { - style: MetricOptionsDefaults; + vbStyle: MetricOptionsDefaults; } -export const toExpression = async ({ style: styleState, visualization }: MetricRootState) => { - const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization); +export const toExpression = async ( + { vbStyle: styleState, vbVisualization }: MetricRootState, + indexId: string +) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(vbVisualization, 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..84abc24469e5 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 @@ -11,16 +11,16 @@ import { Draft } from 'immer'; import { EuiIconTip } from '@elastic/eui'; import { NumberInputOption, SwitchOption } from '../../../../../charts/public'; import { - useTypedDispatch, - useTypedSelector, + useDispatch, + useSelector, setStyleState, } from '../../../application/utils/state_management'; import { TableOptionsDefaults } from '../table_viz_type'; -import { Option } from '../../../application/app'; +import { Option } from '../../../application/components/option'; function TableVizOptions() { - const styleState = useTypedSelector((state) => state.style) as TableOptionsDefaults; - const dispatch = useTypedDispatch(); + const styleState = useSelector((state) => state.vbStyle) as TableOptionsDefaults; + const dispatch = useDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { 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..7c85f19b7816 100644 --- a/src/plugins/vis_builder/public/visualizations/table/to_expression.ts +++ b/src/plugins/vis_builder/public/visualizations/table/to_expression.ts @@ -93,11 +93,18 @@ const getVisSchemas = (aggConfigs: AggConfigs, showMetricsAtAllLevels: boolean): }; export interface TableRootState extends RenderState { - style: TableOptionsDefaults; + vbStyle: TableOptionsDefaults; } -export const toExpression = async ({ style: styleState, visualization }: TableRootState) => { - const { aggConfigs, expressionFns } = await getAggExpressionFunctions(visualization, styleState); +export const toExpression = async ( + { vbStyle: styleState, vbVisualization }: TableRootState, + indexId: string +) => { + const { aggConfigs, expressionFns } = await getAggExpressionFunctions( + vbVisualization, + 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..d75a8f004994 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,15 +6,15 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import produce, { Draft } from 'immer'; -import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { useDispatch, useSelector } 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'; function AreaVisOptions() { - const styleState = useTypedSelector((state) => state.style) as AreaOptionsDefaults; - const dispatch = useTypedDispatch(); + const styleState = useSelector((state) => state.vbStyle) as AreaOptionsDefaults; + const dispatch = useDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { 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..376d88ecc7d4 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, + { vbStyle: styleState, vbVisualization }: VislibRootState, + indexId: string, searchContext: IExpressionLoaderParams['searchContext'] ) => { - const { aggConfigs, expressionFns, indexPattern } = await getAggExpressionFunctions( - visualization - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(vbVisualization, 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/common/types.ts b/src/plugins/vis_builder/public/visualizations/vislib/common/types.ts index fa63b41c826b..1091ef271c65 100644 --- a/src/plugins/vis_builder/public/visualizations/vislib/common/types.ts +++ b/src/plugins/vis_builder/public/visualizations/vislib/common/types.ts @@ -14,5 +14,5 @@ export interface BasicOptionsDefaults { } export interface VislibRootState extends RenderState { - style: T; + vbStyle: T; } 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..e7c7474ccce0 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,15 +6,15 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import produce, { Draft } from 'immer'; -import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { useDispatch, useSelector } 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'; function HistogramVisOptions() { - const styleState = useTypedSelector((state) => state.style) as HistogramOptionsDefaults; - const dispatch = useTypedDispatch(); + const styleState = useSelector((state) => state.vbStyle) as HistogramOptionsDefaults; + const dispatch = useDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { 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..287ff8c7c218 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, + { vbStyle: styleState, vbVisualization }: VislibRootState, + indexId: string, searchContext: IExpressionLoaderParams['searchContext'] ) => { - const { aggConfigs, expressionFns, indexPattern } = await getAggExpressionFunctions( - visualization - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(vbVisualization, 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..63b3bbd6e512 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,15 +6,15 @@ import React, { useCallback } from 'react'; import { i18n } from '@osd/i18n'; import produce, { Draft } from 'immer'; -import { useTypedDispatch, useTypedSelector } from '../../../../application/utils/state_management'; +import { useDispatch, useSelector } 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'; function LineVisOptions() { - const styleState = useTypedSelector((state) => state.style) as LineOptionsDefaults; - const dispatch = useTypedDispatch(); + const styleState = useSelector((state) => state.vbStyle) as LineOptionsDefaults; + const dispatch = useDispatch(); const setOption = useCallback( (callback: (draft: Draft) => void) => { 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..d2996c7fd956 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, + { vbStyle: styleState, vbVisualization }: VislibRootState, + indexId: string, searchContext: IExpressionLoaderParams['searchContext'] ) => { - const { aggConfigs, expressionFns, indexPattern } = await getAggExpressionFunctions( - visualization - ); + const { aggConfigs, expressionFns } = await getAggExpressionFunctions(vbVisualization, 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,