Skip to content

Commit

Permalink
[1][VisBuilder Migration] Add initial setup and migrate state management
Browse files Browse the repository at this point in the history
This PR completes Task 1 and 2 in opensearch-project#5407.

* Reconstruct and allow VisBuilder to be rendered from DataExplorer
* Follow proposal task 2 option 1 to migrate state management to DataExplorer

Issue Resolve
opensearch-project#5492
opensearch-project#5493

[2][VisBuilder Migration] Add context and implement side panel

* add useVisBuilderContext
* modify preloadedState in Data Explorer
* implement side panel

Issue Resolve:
opensearch-project#5522

Signed-off-by: ananzh <[email protected]>

[3][VisBuilder Migration] Combine components into VisBuilderCanvas

Signed-off-by: ananzh <[email protected]>
  • Loading branch information
ananzh committed Dec 27, 2023
1 parent d8cbc17 commit e96a1de
Show file tree
Hide file tree
Showing 111 changed files with 1,325 additions and 1,194 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dashboarding"
],
"private": true,
"version": "3.0.0",
"version": "2.10.0",
"branch": "main",
"types": "./opensearch_dashboards.d.ts",
"tsdocMetadata": "./build/tsdoc-metadata.json",
Expand Down
10 changes: 9 additions & 1 deletion src/core/public/application/scoped_history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,10 @@ export class ScopedHistory<HistoryLocationState = unknown>
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;
Expand Down Expand Up @@ -340,4 +343,9 @@ export class ScopedHistory<HistoryLocationState = unknown>
});
});
}

private isPathnameAcceptable(pathname: string): boolean {
const normalizedPathname = pathname.replace('/data-explorer', '');
return normalizedPathname.startsWith(this.basePath);
}
}
2 changes: 2 additions & 0 deletions src/plugins/data_explorer/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions src/plugins/data_explorer/public/services/view_service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,12 +21,19 @@ export interface DefaultViewState<T = unknown> {

export type ViewProps = AppMountParameters;

type SideEffect<T = any> = (store: Store, state: T, previousState?: T, services?: T) => void;

export interface ViewDefinition<T = any> {
readonly id: string;
readonly title: string;
readonly ui?: {
defaults: DefaultViewState | (() => DefaultViewState) | (() => Promise<DefaultViewState>);
slice: Slice<T>;
defaults:
| DefaultViewState
| (() => DefaultViewState)
| (() => Promise<DefaultViewState>)
| (() => Promise<Array<Promise<DefaultViewState<any>>>>);
slices: Array<Slice<T>>;
sideEffects?: Array<SideEffect<T>>;
};
readonly Canvas: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
readonly Panel: LazyExoticComponent<(props: ViewProps) => React.ReactElement>;
Expand Down
38 changes: 29 additions & 9 deletions src/plugins/data_explorer/public/utils/state_management/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,39 @@ export const getPreloadedState = async (
return;
}

const { defaults } = view.ui;
const { defaults, slices } = view.ui;

try {
// defaults can be a function or an object
const preloadedState = typeof defaults === 'function' ? await defaults() : defaults;
rootState[view.id] = preloadedState.state;

// if the view wants to override the root state, we do that here
if (preloadedState.root) {
rootState = {
...rootState,
...preloadedState.root,
};
if (Array.isArray(preloadedState)) {
await Promise.all(
preloadedState.map(async (statePromise, index) => {
try {
const state = await statePromise;
const slice = slices[index];
const prefixedSliceName =
slice.name === view.id ? slice.name : `${view.id}-${slice.name}`;
rootState[prefixedSliceName] = state.state;
} catch (e) {
// eslint-disable-next-line no-console
console.error(`Error initializing slice: ${e}`);
}
})
);
} else {
slices.forEach((slice) => {
const prefixedSliceName =
slice.name === view.id ? slice.name : `${view.id}-${slice.name}`;
rootState[prefixedSliceName] = preloadedState.state;
});
// if the view wants to override the root state, we do that here
if (preloadedState.root) {
rootState = {
...rootState,
...preloadedState.root,
};
}
}
} catch (e) {
// eslint-disable-next-line no-console
Expand Down
36 changes: 28 additions & 8 deletions src/plugins/data_explorer/public/utils/state_management/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,18 @@ export const configurePreloadedStore = (preloadedState: PreloadedState<RootState
export const getPreloadedStore = async (services: DataExplorerServices) => {
// For each view preload the data and register the slice
const views = services.viewRegistry.all();
const viewSideEffectsMap: Record<string, Function[]> = {};

views.forEach((view) => {
if (!view.ui) return;

const { slice } = view.ui;
registerSlice(slice);
const { slices, sideEffects } = view.ui;
registerSlices(slices, view.id);

// Save side effects if they exist
if (sideEffects) {
viewSideEffectsMap[view.id] = sideEffects;
}
});

const preloadedState = await loadReduxState(services);
Expand All @@ -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;
};
Expand Down Expand Up @@ -103,11 +120,14 @@ export const getPreloadedStore = async (services: DataExplorerServices) => {
return { store, unsubscribe: onUnsubscribe };
};

export const registerSlice = (slice: Slice) => {
if (dynamicReducers[slice.name]) {
throw new Error(`Slice ${slice.name} already registered`);
}
dynamicReducers[slice.name] = slice.reducer;
export const registerSlices = (slices: Slice[], id: string) => {
slices.forEach((slice) => {
const prefixedSliceName = slice.name === id ? slice.name : `${id}-${slice.name}`;
if (dynamicReducers[prefixedSliceName]) {
throw new Error(`Slice ${prefixedSliceName} already registered`);
}
dynamicReducers[prefixedSliceName] = slice.reducer;
});
};

// Infer the `RootState` and `AppDispatch` types from the store itself
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/discover/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export class DiscoverPlugin
const services = getServices();
return await getPreloadedState(services);
},
slice: discoverSlice,
slices: [discoverSlice],
},
shouldShow: () => true,
// ViewComponent
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/vis_builder/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

export const PLUGIN_ID = 'vis-builder';
// treat PLUGIN_ID as a literal type 'vis-builder' rather than just string
export const PLUGIN_ID = 'vis-builder' as const;
export const PLUGIN_NAME = 'VisBuilder';
export const VISUALIZE_ID = 'visualize';
export const EDIT_PATH = '/edit';
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/vis_builder/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"navigation",
"savedObjects",
"visualizations",
"uiActions"
"uiActions",
"dataExplorer"
],
"requiredBundles": [
"charts",
Expand Down
1 change: 1 addition & 0 deletions src/plugins/vis_builder/public/application/_util.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

@mixin scrollNavParent($template-row: none) {
display: grid;
min-height: 0;
Expand Down
84 changes: 0 additions & 84 deletions src/plugins/vis_builder/public/application/app.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@
import { EuiForm } from '@elastic/eui';
import React from 'react';
import { useVisualizationType } from '../../utils/use';
import { useTypedSelector } from '../../utils/state_management';
import './config_panel.scss';
import { mapSchemaToAggPanel } from './schema_to_dropbox';
import { SecondaryPanel } from './secondary_panel';

import './config_panel.scss';
import '../side_nav.scss';
import { useVisBuilderContext } from '../../view_components/context';

export function ConfigPanel() {
const vizType = useVisualizationType();
const editingState = useTypedSelector(
(state) => state.visualization.activeVisualization?.draftAgg
);
const { rootState } = useVisBuilderContext();
const editingState = rootState.visualization.activeVisualization?.draftAgg;
const schemas = vizType.ui.containerConfig.data.schemas;

if (!schemas) return null;

const mainPanel = mapSchemaToAggPanel(schemas);

return (
<EuiForm className={`vbConfig ${editingState ? 'showSecondary' : ''}`}>
<div className="vbConfig__section">{mainPanel}</div>
<SecondaryPanel />
</EuiForm>
<section className="vbSidenav config">
<EuiForm className={`vbConfig ${editingState ? 'showSecondary' : ''}`}>
<div className="vbConfig__section">{mainPanel}</div>
<SecondaryPanel />
</EuiForm>
</section>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,31 @@ import { cloneDeep, get } from 'lodash';
import { useDebounce } from 'react-use';
import { i18n } from '@osd/i18n';
import { EuiCallOut } from '@elastic/eui';
import { useTypedDispatch, useTypedSelector } from '../../utils/state_management';
import { useTypedDispatch } from '../../utils/state_management';
import { DefaultEditorAggParams } from '../../../../../vis_default_editor/public';
import { Title } from './title';
import { useIndexPatterns, useVisualizationType } from '../../utils/use';
import { useVisualizationType } from '../../utils/use';
import {
OpenSearchDashboardsContextProvider,
useOpenSearchDashboards,
} from '../../../../../opensearch_dashboards_react/public';
import { VisBuilderServices } from '../../../types';
import { useVisBuilderContext } from '../../view_components/context';
import { VisBuilderViewServices } from '../../../types';
import { AggParam, IAggType, IFieldParamType } from '../../../../../data/public';
import { saveDraftAgg, editDraftAgg } from '../../utils/state_management/visualization_slice';
import { setError } from '../../utils/state_management/metadata_slice';
import { setError } from '../../utils/state_management/editor_slice';
import { Storage } from '../../../../../opensearch_dashboards_utils/public';

const PANEL_KEY = 'SECONDARY_PANEL';

export function SecondaryPanel() {
const { draftAgg, aggConfigParams } = useTypedSelector(
(state) => state.visualization.activeVisualization!
);
const isEditorValid = useTypedSelector((state) => !state.metadata.editor.errors[PANEL_KEY]);
const { indexPattern, rootState } = useVisBuilderContext();
const { draftAgg, aggConfigParams } = rootState.visualization.activeVisualization!;
const isEditorValid = rootState.editor.errors[PANEL_KEY];
const [touched, setTouched] = useState(false);
const dispatch = useTypedDispatch();
const vizType = useVisualizationType();
const indexPattern = useIndexPatterns().selected;
const { services } = useOpenSearchDashboards<VisBuilderServices>();
const { services } = useOpenSearchDashboards<VisBuilderViewServices>();
const {
data: {
search: { aggs: aggService },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { useOpenSearchDashboards } from '../../../../../../opensearch_dashboards_react/public';
import { VisBuilderServices } from '../../../../types';
import { useAggs } from '../../../utils/use';
import { useVisBuilderContext } from '../../../view_components/context';

const filterByName = propFilter('name');
const filterByType = propFilter('type');
Expand All @@ -30,7 +31,8 @@ export interface UseDropboxProps extends Pick<DropboxProps, 'id' | 'label'> {
export const useDropbox = (props: UseDropboxProps): DropboxProps => {
const { id: dropboxId, label, schema } = props;
const [validAggTypes, setValidAggTypes] = useState<string[]>([]);
const { aggConfigs, indexPattern, aggs, timeRange } = useAggs();
const { indexPattern } = useVisBuilderContext();
const { aggConfigs, aggs, timeRange } = useAggs();
const dispatch = useTypedDispatch();
const {
services: {
Expand Down
Loading

0 comments on commit e96a1de

Please sign in to comment.