opensearch-project / OpenSearch-Dashboards

📊 Open source visualization dashboards for OpenSearch.
https://opensearch.org/docs/latest/dashboards/index/
Apache License 2.0
1.69k stars 893 forks source link

Vis-Builder Integration with Data Explorer Proposal #5407

Open ananzh opened 1 year ago

ananzh commented 1 year ago

Overview

The Data Explorer project has taken a step forward in streamlining the data exploration experience in OpenSearch Dashboards (OSD) by addressing security concerns and offering a unified platform. As a strategic move, it's essential to integrate the vis-builder plugin with Data Explorer to provide a comprehensive data analysis framework for users.

Vis-builder has been instrumental in visual data exploration, offering capabilities like chart creation and advanced visualization settings. Its incorporation into Data Explorer would enhance the platform's overall data analytics and visualization potential.

There are two stages of the work:

Stage 1: Incorporate the vis-builder plugin as a view inside the Data Explorer.

Goals

Note: This proposal focuses primarily on Stage 1.

Stage 2: Enhancements (currently under design & discussion)

Goals

Note: Not the scope of current proposal.

Objective

To seamlessly incorporate the vis-builder plugin as a view inside the Data Explorer to offer a unified data exploration experience, thereby enriching the data analysis capabilities of OSD.

Tasks

[Task 1] Reconstruct and Reroute VisBuilder Plugin

[Task 2] State Management Migration

There are issues to migrate states. First, simplify metadata slice which should only ffocus on the editor's state.

Second migrate simplified metadata slice and other 3 slices, there are two proposals.

Proposal 1: Allowing Data Explorer to Register Multiple Slices for a Single View

Objective

Retain the modularity of individual slices for each component of the Vis Builder while simplifying integration with the Data Explorer.

Steps

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;
  });
};
export interface VisBuilderState {
  vbEditor: EditorState;
  vbStyle: StyleState;
  vbUi: UIStateState;
  vbVisualization: VisualizationState;
}
export type RenderState = Omit<VisBuilderState, 'vbEditor'>;

Pros and Cons

Proposal 2: Combining VisBuilder Slices and register one slice to Data Explorer

This proposal defines a combined VisBuilderState which encompasses all the individual sub-states. Data Explorer current setting is one slice/view. A view, for example Discover, needs to specify a slice like below and data explorer register a slice via registerSlice.

ui: {
        defaults: async () => {
          this.initializeServices?.();
          const services = getServices();
          return await getPreloadedState(services);
        },
        slice: discoverSlice,
   }

export const registerSlice = (slice: Slice) => {
  if (dynamicReducers[slice.name]) {
    throw new Error(`Slice ${slice.name} already registered`);
  }
  dynamicReducers[slice.name] = slice.reducer;
};    

Different from Discover, VisBuilder has multiple slices. Then for consistency and manage purpose, it might be ideal to keep the one slice for one view.

Steps

Pros and Cons

[Task 3] Context Provider Migration/Creation

[Task 4] UI modifications

The main part of this task is to divide src/plugins/vis_builder/public/application/app.tsx into a canvas area (VisBuilderCanvas) and a side panel (VisBuilderPanel). The canvas is where users interact with visualizations, and the side panel only provides field selector. Below is the brief re-construction:

[Task 5] Comprehensive test for VisBuilder

Tentative Timeline

Based on 2-3 devs

Week 1-2

Week 3-4

Week 5-6

Additional Considerations

ashwin-pc commented 1 year ago

Great proposal @ananzh! Definitely more inclined to option one. Here are a few suggestions to that.

  1. Prefixing the slice with the view Id should suffice for name conflicts. If we want to make using the state easier, we can add helper methods that make it easier to work with prefixed slice names.
  2. In case you don't have it on your radar, you also need to ensure that legacy urls for VisBuilder still work after this migration. You can follow the pattern used during the discover migration. E.g. saved object urls, edit and create urls etc.

Thanks for the detailed description of the migration!

ananzh commented 10 months ago

Prefixing the slice

To address the challenge of managing states in a centralized system with different applications like VisBuilder and DataExplorer, while avoiding naming conflicts and ensuring easy access to the states, we propose the following steps:

Prefix State Keys with view.id

Prefixing state keys with the view.id to avoid naming conflicts. Then we can maintain the original state names within each view (like style, visualization, and ui in vis builder) but prepend the view's identifier when integrating them into the RootState. The only exception is metadata, because it is duplicate as the one in RootState.

State naming in Data Explorer

The register slice id in Data Explorer store (src/plugins/data_explorer/public/utils/state_management/preload.ts):

const id = slice.name == view.id? slice.name: `${view.id}-${slice.name}`;

Therefore in plugin.ts, each view needs to register a proper ID. If there are multiple slices in one view, the slice id would be ${view.id}-${slice.name}. For example, in the following vis_builder, the view.id is vis_builder

export const PLUGIN_ID = 'vis-builder';

dataExplorer.registerView<any>({
      id: PLUGIN_ID,
      title: PLUGIN_NAME,
      defaultPath: '#/',
      appExtentions: {
        savedObject: {
          docTypes: [VISBUILDER_SAVED_OBJECT],
          toListItem: (obj) => ({
            id: obj.id,
            label: obj.title,
          }),
        },
      },

…

There are four slices: ui, style, editor, visualization. The registered slice names in Data Explorer are vis-builder-ui, vis-builder-style, vis-builder-editor and vis-builder-visualization.

For view with single state, then the easiest naming is to have state name equal to PLUGIN_ID. For example, in Discover, both registered view ID and state is discover. Then in Data Explorer, the registered slice name is discover. Discover does not need further state access.

Dynamic State Access

To access these namespaced states easily, we can create a selector function that dynamically constructs the state key based on the view's ID. For example, within VisBuilder, we use a generic selector that automatically appends vis-builder- to the state keys. Here are some reference helper functions.

* access one specific state from the entire state object

type ValidStateKeys = 'editor' | 'ui' | 'visualization' | 'style'; type ValidStateTypes = StyleState | UiState | VisualizationState | EditorState;

const getDynamicState = (rootState: VisBuilderRootState, stateKey: ValidStateKeys): T => { const dynamicKey = ${PLUGIN_ID}-${stateKey}; return rootState[dynamicKey] as T; };

To use:

const rootState = useSelector(state => state); const editorState = getDynamicState(rootState, 'editor');

// TypeScript will ensure that editorState is of type EditorState if (editorState && editorState.status === 'loaded') { ... }



### Dispatch Mechanism in Centralized State Management
In the centralized state management system, where different slices of state are prefixed with their respective view IDs (like vis-builder-editor for the editor slice), we actually don't need to modify the actions:

* Action Type Prefixing: Redux Toolkit automatically namespaces action types with the slice name, making them unique. For example, the setStatus action from the editor slice has a type like "editor/setState".

* Reducer Handling: When `setStatus` is dispatched, Redux invokes the reducer associated with "editor/setStatus". The namespacing of slices in the global state, such as `vis-builder-editor`, does not require any additional handling when dispatching actions.

* No Need for Explicit State Specification: There is no need to manually specify which part of the state should be affected by an action. The linkage between the action and its target slice (the `editor` slice will point to `vis-builder-editor` reference in the global root state)  is managed by the setup and registration of reducers and slices.

* Simple Dispatching: Actions are dispatched using their creators as usual. Redux handles routing these actions to the appropriate slice reducer based on their types.
ashwin-pc commented 10 months ago

How will this work with auto complete and typescript types and suggestions? today selecting using the selector is typesafe

ananzh commented 10 months ago

@ashwin-pc the above is more general discussion on how to migrate/work with a centralized state management store. In vis-builder, we will add one helper function

export const getViewState = (stateKey: 'editor' | 'ui' | 'visualization' | 'style') => 
  (state: VisBuilderRootState) => state[`${PLUGIN_ID}-${stateKey}`];

To use it:

const styleState = useTypedSelector(getViewState('style'));

The stateKey is limited to specific strings ('editor', 'ui', 'visualization', 'style'), which correspond to the keys in VisBuilderRootState. This provides the following benefits:

Type Safety: Ensures that only valid keys for the VisBuilder's state are used, reducing the risk of runtime errors due to incorrect state keys.

Autocomplete: When using this selector function, developers will receive autocomplete suggestions for the stateKey parameter, which can speed up development and reduce errors.

Error Detection: If a developer tries to use an invalid key, TypeScript will flag it as an error, catching potential issues early in the development process.

The types defined in Data Explorer is more general to fit more views. But VisBuilder and other views could maintain type safety across the entire application.

ananzh commented 10 months ago

@ashwin-pc I updated the previous comment. For access one specific state object directly and access one specific state from the entire state object, which one you like? Currently I am using the first one. Restrict the type to view specific slices. No need to load the entire state. What do you think?

ashwin-pc commented 10 months ago

Oh I like it if the helper exists. I just don't want to have to remember how to get to the state value without auto complete 😂