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 <ananzh@amazon.com>

[3][VisBuilder Migration] Combine components into VisBuilderCanvas

Signed-off-by: ananzh <ananzh@amazon.com>
  • Loading branch information
ananzh committed Dec 27, 2023
1 parent d8cbc17 commit aaa6a7e
Show file tree
Hide file tree
Showing 115 changed files with 1,360 additions and 1,209 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { ScopedHistory } from '../../../../core/public';
import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks';
import { dataPluginMock } from '../../../data/public/mocks';
import { embeddablePluginMock } from '../../../embeddable/public/mocks';
import { expressionsPluginMock } from '../../../expressions/public/mocks';
import { createOsdUrlStateStorage } from '../../../opensearch_dashboards_utils/public';
import { DataExplorerServices } from '../types';
import { ScopedHistory } from '../../../core/public';
import { coreMock, scopedHistoryMock } from '../../../core/public/mocks';
import { dataPluginMock } from '../../data/public/mocks';
import { embeddablePluginMock } from '../../embeddable/public/mocks';
import { expressionsPluginMock } from '../../expressions/public/mocks';
import { createOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public';
import { DataExplorerServices } from './types';

export const createDataExplorerServicesMock = () => {
export const createDataExplorerStartServicesMock = () => {
const coreStartMock = coreMock.createStart();
const dataMock = dataPluginMock.createStartContract();
const embeddableMock = embeddablePluginMock.createStartContract();
Expand All @@ -33,3 +33,16 @@ export const createDataExplorerServicesMock = () => {

return (dataExplorerServicesMock as unknown) as jest.Mocked<DataExplorerServices>;
};

export const createDataExplorerSetupServicesMock = () => {
const setupMock = {
registerView: jest.fn(),
};

return setupMock;
};

export const dataExplorerPluginMock = {
createDataExplorerStartServicesMock,
createDataExplorerSetupServicesMock,
};
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
*/

import { DataExplorerServices } from '../../types';
import { createDataExplorerServicesMock } from '../mocks';
import { createDataExplorerStartServicesMock } from '../../mocks';
import { loadReduxState, persistReduxState } from './redux_persistence';

describe('test redux state persistence', () => {
let mockServices: jest.Mocked<DataExplorerServices>;
let reduxStateParams: any;

beforeEach(() => {
mockServices = createDataExplorerServicesMock();
mockServices = createDataExplorerStartServicesMock();
reduxStateParams = {
discover: 'visualization',
metadata: 'metadata',
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>
);
}
Loading

0 comments on commit aaa6a7e

Please sign in to comment.