Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] [VisBuilder] Adds UIState to vis, adds index patterns to embeddable, bug fixes #3874

Merged
merged 2 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Data] Add geo shape filter field ([#3605](https:/opensearch-project/OpenSearch-Dashboards/pull/3605))
- [UI] Add support for comma delimiters in the global filter bar ([#3686](https:/opensearch-project/OpenSearch-Dashboards/pull/3686))
- [VisBuilder] Add UI actions handler ([#3732](https:/opensearch-project/OpenSearch-Dashboards/pull/3732))
- [VisBuilder] Add persistence to visualizations inner state ([#3751](https:/opensearch-project/OpenSearch-Dashboards/pull/3751))
- [Table Visualization] Move format table, consolidate types and add unit tests ([#3397](https:/opensearch-project/OpenSearch-Dashboards/pull/3397))

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