Skip to content

Commit

Permalink
[Backport 2.x] [VisBuilder] Adds UIState to vis, adds index patterns …
Browse files Browse the repository at this point in the history
…to embeddable, bug fixes (#3874)

* [VisBuilder] Adds UIState to vis, adds index patterns to embeddable, bug fixes (#3751)

* adds uiActions to visBuilder
* prevents multiple errors on load
* fixes visbuilder type errors
* fixes save
* adds ui state to vis builder
* fixes tests
* Adds support in embeddables for multiple indices
* Moves ui state to separate slice
* Adds changelog

Signed-off-by: Ashwin P Chandran <ashwinpc@amazon.com>

---------

Signed-off-by: Ashwin P Chandran <ashwinpc@amazon.com>
Co-authored-by: Josh Romero <rmerqg@amazon.com>
(cherry picked from commit ee32d20)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md

* add changelog

Signed-off-by: Josh Romero <rmerqg@amazon.com>

---------

Signed-off-by: Josh Romero <rmerqg@amazon.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Josh Romero <rmerqg@amazon.com>
  • Loading branch information
3 people authored Apr 18, 2023
1 parent 7447079 commit 0ae76d2
Show file tree
Hide file tree
Showing 20 changed files with 207 additions and 60 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Notifications] Adds id to toast api for deduplication ([#3752](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3752))
- [UI] Add support for comma delimiters in the global filter bar ([#3686](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3686))
- [VisBuilder] Add UI actions handler ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Add persistence to visualizations inner state ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751))
- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3397))

### 🐛 Bug Fixes
Expand All @@ -35,6 +36,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [VisBuilder] Fix multiple warnings thrown on page load ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Fix Firefox legend selection issue ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Fix type errors ([#3732](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Fix indexpattern selection in filter bar ([#3751](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3751))

### 🚞 Infrastructure

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface VisBuilderSavedObjectAttributes extends SavedObjectAttributes {
visualizationState?: string;
updated_at?: string;
styleState?: string;
uiState?: string;
version: number;
searchSourceFields?: {
index?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,6 @@ const OptionItem = ({ icon, title }: { icon: IconType; title: string }) => (
</>
);

// The app uses EuiResizableContainer that triggers a rerender for ever mouseover action.
// The app uses EuiResizableContainer that triggers a rerender for every mouseover action.
// To prevent this child component from unnecessarily rerendering in that instance, it needs to be memoized
export const RightNav = React.memo(RightNavUI);
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react
import { IExpressionLoaderParams } from '../../../../expressions/public';
import { VisBuilderServices } from '../../types';
import { validateSchemaState, validateAggregations } from '../utils/validations';
import { useTypedSelector } from '../utils/state_management';
import { useTypedDispatch, useTypedSelector, setUIStateState } from '../utils/state_management';
import { useAggs, useVisualizationType } from '../utils/use';
import { PersistedState } from '../../../../visualizations/public';

Expand Down Expand Up @@ -39,8 +39,25 @@ export const WorkspaceUI = () => {
timeRange: data.query.timefilter.timefilter.getTime(),
});
const rootState = useTypedSelector((state) => state);
// Visualizations require the uiState to persist even when the expression changes
const uiState = useMemo(() => new PersistedState(), []);
const dispatch = useTypedDispatch();
// Visualizations require the uiState object to persist even when the expression changes
// eslint-disable-next-line react-hooks/exhaustive-deps
const uiState = useMemo(() => new PersistedState(rootState.ui), []);

useEffect(() => {
if (rootState.metadata.editor.state === 'loaded') {
uiState.setSilent(rootState.ui);
}
// To update uiState once saved object data is loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rootState.metadata.editor.state, uiState]);

useEffect(() => {
uiState.on('change', (args) => {
// Store changes to UI state
dispatch(setUIStateState(uiState.toJSON()));
});
}, [dispatch, uiState]);

useEffect(() => {
async function loadExpression() {
Expand Down Expand Up @@ -137,6 +154,6 @@ export const WorkspaceUI = () => {
);
};

// The app uses EuiResizableContainer that triggers a rerender for ever mouseover action.
// The app uses EuiResizableContainer that triggers a rerender for every mouseover action.
// To prevent this child component from unnecessarily rerendering in that instance, it needs to be memoized
export const Workspace = React.memo(WorkspaceUI);
63 changes: 41 additions & 22 deletions src/plugins/vis_builder/public/application/utils/schema.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
{
"type": "object",
"properties": {
"styleState": {
"type": "object"
},
"visualizationState": {
"type": "object",
"properties": {
"activeVisualization": {
"type": "object",
"properties": {
"name": { "type": "string" },
"aggConfigParams": { "type": "array" }
"type": "object",
"properties": {
"styleState": {
"type": "object"
},
"visualizationState": {
"type": "object",
"properties": {
"activeVisualization": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"required": ["name", "aggConfigParams"],
"additionalProperties": false
"aggConfigParams": {
"type": "array"
}
},
"indexPattern": { "type": "string" },
"searchField": { "type": "string" }
"required": [
"name",
"aggConfigParams"
],
"additionalProperties": false
},
"required": ["searchField"],
"additionalProperties": false
}
"indexPattern": {
"type": "string"
},
"searchField": {
"type": "string"
}
},
"required": [
"searchField"
],
"additionalProperties": false
},
"required": ["styleState", "visualizationState"],
"additionalProperties": false
"uiState": {
"type": "object"
}
},
"required": [
"styleState",
"visualizationState"
],
"additionalProperties": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { VisBuilderServices } from '../../../types';
* 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' | 'clean' | 'dirty';
type EditorState = 'loading' | 'loaded' | 'clean' | 'dirty';

export interface MetadataState {
editor: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 (
Expand All @@ -16,10 +17,12 @@ export const getPreloadedState = async (
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,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('test redux state persistence', () => {
style: 'style',
visualization: 'visualization',
metadata: 'metadata',
ui: 'ui',
};
});

Expand All @@ -33,6 +34,7 @@ describe('test redux state persistence', () => {
editor: { errors: {}, state: 'loading' },
originatingApp: undefined,
},
ui: {},
};

const returnStates = await loadReduxState(mockServices);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,21 @@ export const loadReduxState = async (services: VisBuilderServices) => {
const serializedState = services.osdUrlStateStorage.get<RootState>('_a');
if (serializedState !== null) return serializedState;
} catch (err) {
/* eslint-disable no-console */
// eslint-disable-next-line no-console
console.error(err);
/* eslint-enable no-console */
}

return await getPreloadedState(services);
};

export const persistReduxState = (
{ style, visualization, metadata },
{ style, visualization, metadata, ui }: RootState,
services: VisBuilderServices
) => {
try {
services.osdUrlStateStorage.set<RootState>(
'_a',
{ style, visualization, metadata },
{ style, visualization, metadata, ui },
{
replace: true,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ 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,
Expand Down Expand Up @@ -60,3 +62,5 @@ 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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { VisBuilderServices } from '../../../types';

export type UIStateState<T = any> = T;

const initialState = {} as UIStateState;

export const getPreloadedState = async ({
types,
data,
}: VisBuilderServices): Promise<UIStateState> => {
return initialState;
};

export const uiStateSlice = createSlice({
name: 'ui',
initialState,
reducers: {
setState<T>(state: T, action: PayloadAction<UIStateState<T>>) {
return action.payload;
},
updateState<T>(state: T, action: PayloadAction<Partial<UIStateState<T>>>) {
state = {
...state,
...action.payload,
};
},
},
});

// Exposing the state functions as generics
export const setState = uiStateSlice.actions.setState as <T>(payload: T) => PayloadAction<T>;
export const updateState = uiStateSlice.actions.updateState as <T>(
payload: Partial<T>
) => PayloadAction<Partial<T>>;

export const { reducer } = uiStateSlice;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import {
import { EDIT_PATH, PLUGIN_ID } from '../../../../common';
import { VisBuilderServices } from '../../../types';
import { getCreateBreadcrumbs, getEditBreadcrumbs } from '../breadcrumbs';
import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management';
import {
useTypedDispatch,
setStyleState,
setVisualizationState,
setUIStateState,
} from '../state_management';
import { useOpenSearchDashboards } from '../../../../../opensearch_dashboards_react/public';
import { setEditorState } from '../state_management/metadata_slice';
import { getStateFromSavedObject } from '../../../saved_visualizations/transforms';
Expand Down Expand Up @@ -46,6 +51,7 @@ export const useSavedVisBuilderVis = (visualizationIdFromUrl: string | undefined

const loadSavedVisBuilderVis = async () => {
try {
dispatch(setEditorState({ state: 'loading' }));
const savedVisBuilderVis = await getSavedVisBuilderVis(
savedVisBuilderLoader,
visualizationIdFromUrl
Expand All @@ -56,8 +62,10 @@ 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' }));
} else {
chrome.setBreadcrumbs(getCreateBreadcrumbs(navigateToApp));
}
Expand Down
Loading

0 comments on commit 0ae76d2

Please sign in to comment.