From 5a94320291a2fc8c637c1e9f57a67f37293b6c19 Mon Sep 17 00:00:00 2001 From: Ashwin Pc Date: Fri, 3 Dec 2021 00:15:34 +0000 Subject: [PATCH] Adds state management to Drag and Drop Signed-off-by: Ashwin Pc --- package.json | 1 + src/plugins/wizard/public/application/app.tsx | 19 ++--- .../components/data_tab/config_panel.tsx | 20 +---- .../components/data_tab/config_section.tsx | 73 ++++++++++++------- .../components/data_tab/field_selector.tsx | 8 +- .../application/components/data_tab/index.tsx | 9 +-- .../application/components/side_nav.tsx | 56 +++----------- .../public/application/components/top_nav.tsx | 9 +-- .../wizard/public/application/index.tsx | 10 ++- .../utils/state_management/config_slice.ts | 62 ++++++++++++++++ .../state_management/datasource_slice.ts | 45 ++++++++++++ .../utils/state_management/hooks.ts | 11 +++ .../utils/state_management/index.ts | 7 ++ .../utils/state_management/store.ts | 19 +++++ yarn.lock | 17 +++++ 15 files changed, 248 insertions(+), 118 deletions(-) create mode 100644 src/plugins/wizard/public/application/utils/state_management/config_slice.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/hooks.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/index.ts create mode 100644 src/plugins/wizard/public/application/utils/state_management/store.ts diff --git a/package.json b/package.json index b50a51e7558d..010accdd5e29 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "@osd/ace": "1.0.0", "@osd/monaco": "1.0.0", "@osd/ui-shared-deps": "1.0.0", + "@reduxjs/toolkit": "^1.6.2", "@types/yauzl": "^2.9.1", "JSONStream": "1.3.5", "abortcontroller-polyfill": "^1.4.0", diff --git a/src/plugins/wizard/public/application/app.tsx b/src/plugins/wizard/public/application/app.tsx index 37244f16ca51..84302c54f51f 100644 --- a/src/plugins/wizard/public/application/app.tsx +++ b/src/plugins/wizard/public/application/app.tsx @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { I18nProvider } from '@osd/i18n/react'; import { EuiPage } from '@elastic/eui'; -import { DataPublicPluginStart, IndexPattern } from '../../../data/public'; +import { DataPublicPluginStart } from '../../../data/public'; import { SideNav } from './components/side_nav'; import { DragDropProvider } from './utils/drag_drop/drag_drop_context'; import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public'; @@ -15,13 +15,15 @@ import { Workspace } from './components/workspace'; import './app.scss'; import { TopNav } from './components/top_nav'; +import { useTypedDispatch } from './utils/state_management'; +import { setIndexPattern } from './utils/state_management/datasource_slice'; export const WizardApp = () => { const { services: { data }, } = useOpenSearchDashboards(); - const [indexPattern, setIndexPattern] = useIndexPattern(data); + useIndexPattern(data); // Render the application DOM. return ( @@ -29,7 +31,7 @@ export const WizardApp = () => { - + @@ -39,16 +41,15 @@ export const WizardApp = () => { // TODO: Temporary. Need to update it fetch the index pattern cohesively function useIndexPattern(data: DataPublicPluginStart) { - const [indexPattern, setIndexPattern] = useState(null); + const dispatch = useTypedDispatch(); + useEffect(() => { const fetchIndexPattern = async () => { const defaultIndexPattern = await data.indexPatterns.getDefault(); if (defaultIndexPattern) { - setIndexPattern(defaultIndexPattern); + dispatch(setIndexPattern(defaultIndexPattern)); } }; fetchIndexPattern(); - }, [data.indexPatterns]); - - return [indexPattern, setIndexPattern] as const; + }, [data.indexPatterns, dispatch]); } diff --git a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx index 262fd4c06e42..ec910b7352de 100644 --- a/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/config_panel.tsx @@ -6,25 +6,13 @@ import { EuiForm, EuiTitle } from '@elastic/eui'; import React from 'react'; import { i18n } from '@osd/i18n'; -import { BUCKET_TYPES, METRIC_TYPES } from '../../../../../data/public'; import { ConfigSection } from './config_section'; import './config_panel.scss'; - -// TODO: Temp. Remove once visualizations can be refgistered and editor configs can be passed along -const CONFIG = { - x: { - title: 'X Axis', - allowedAggregation: BUCKET_TYPES.TERMS, - }, - y: { - title: 'Y Axis', - allowedAggregation: METRIC_TYPES.AVG, - }, -}; +import { useTypedSelector } from '../../utils/state_management'; export function ConfigPanel() { - const sections = CONFIG; + const { configSections } = useTypedSelector((state) => state.config); return ( @@ -35,8 +23,8 @@ export function ConfigPanel() { })} - {Object.entries(sections).map(([sectionId, sectionProps], index) => ( - {}} /> + {Object.entries(configSections).map(([sectionId, sectionProps], index) => ( + ))} ); diff --git a/src/plugins/wizard/public/application/components/data_tab/config_section.tsx b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx index 88511756222f..64f74824d71a 100644 --- a/src/plugins/wizard/public/application/components/data_tab/config_section.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/config_section.tsx @@ -3,26 +3,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiButtonIcon, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui'; +import { EuiButtonIcon, EuiPanel, EuiText, EuiTitle } from '@elastic/eui'; import { i18n } from '@osd/i18n'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback } from 'react'; import { IndexPatternField } from 'src/plugins/data/common'; import { useDrop } from '../../utils/drag_drop'; +import { useTypedDispatch, useTypedSelector } from '../../utils/state_management'; +import { + addConfigSectionField, + removeConfigSectionField, +} from '../../utils/state_management/config_slice'; import './config_section.scss'; interface ConfigSectionProps { id: string; title: string; - onChange: Function; } -export const ConfigSection = ({ title, id, onChange }: ConfigSectionProps) => { - const [currentField, setCurrentField] = useState(); +export const ConfigSection = ({ title, id }: ConfigSectionProps) => { + const dispatch = useTypedDispatch(); + const { fields } = useTypedSelector((state) => state.config.configSections[id]); - const dropHandler = useCallback((field: IndexPatternField) => { - setCurrentField(field); - }, []); + const dropHandler = useCallback( + (field: IndexPatternField) => { + dispatch( + addConfigSectionField({ + sectionId: id, + field, + }) + ); + }, + [dispatch, id] + ); const [dropProps, { isValidDropTarget, dragData }] = useDrop('dataPlane', dropHandler); const dropTargetString = dragData @@ -31,24 +44,32 @@ export const ConfigSection = ({ title, id, onChange }: ConfigSectionProps) => { defaultMessage: 'Click or drop to add', }); - useEffect(() => { - onChange(id, currentField); - }, [id, currentField, onChange]); - return ( - - {currentField ? ( - - - {currentField.displayName} - - setCurrentField(undefined)} - /> - +
+ +

{title}

+
+ {fields.length ? ( + fields.map((field, index) => ( + + + {field.displayName} + + + dispatch( + removeConfigSectionField({ + sectionId: id, + field, + }) + ) + } + /> + + )) ) : (
{ {dropTargetString}
)} - +
); }; diff --git a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx index 53d385944966..17d561752277 100644 --- a/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/field_selector.tsx @@ -22,10 +22,7 @@ import { import { FieldSelectorField } from './field_selector_field'; import './field_selector.scss'; - -interface FieldSelectorDeps { - indexFields: IndexPatternField[]; -} +import { useTypedSelector } from '../../utils/state_management'; interface IFieldCategories { categorical: IndexPatternField[]; @@ -40,7 +37,8 @@ const META_FIELDS: string[] = [ OPENSEARCH_FIELD_TYPES._TYPE, ]; -export const FieldSelector = ({ indexFields }: FieldSelectorDeps) => { +export const FieldSelector = () => { + const indexFields = useTypedSelector((state) => state.dataSource.visualizableFields); const fields = indexFields?.reduce( (fieldGroups, currentField) => { const category = getFieldCategory(currentField); diff --git a/src/plugins/wizard/public/application/components/data_tab/index.tsx b/src/plugins/wizard/public/application/components/data_tab/index.tsx index e7e5f02e6d02..dd062f3a787d 100644 --- a/src/plugins/wizard/public/application/components/data_tab/index.tsx +++ b/src/plugins/wizard/public/application/components/data_tab/index.tsx @@ -4,20 +4,15 @@ */ import React from 'react'; -import { IndexPatternField } from '../../../../../data/public'; import { FieldSelector } from './field_selector'; import { ConfigPanel } from './config_panel'; import './index.scss'; -interface DataTabDeps { - indexFields: IndexPatternField[]; -} - -export const DataTab = ({ indexFields }: DataTabDeps) => { +export const DataTab = () => { return (
- +
); diff --git a/src/plugins/wizard/public/application/components/side_nav.tsx b/src/plugins/wizard/public/application/components/side_nav.tsx index a35c039e57f7..2f9eab83fad3 100644 --- a/src/plugins/wizard/public/application/components/side_nav.tsx +++ b/src/plugins/wizard/public/application/components/side_nav.tsx @@ -3,33 +3,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { i18n } from '@osd/i18n'; -import { - EuiFormLabel, - EuiFlexGroup, - EuiFlexItem, - EuiTabbedContent, - EuiTabbedContentTab, -} from '@elastic/eui'; +import { EuiFormLabel, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; -import { IndexPattern, IndexPatternField, OSD_FIELD_TYPES } from '../../../../data/public'; import { DataTab } from './data_tab'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WizardServices } from '../../types'; import { StyleTab } from './style_tab'; import './side_nav.scss'; +import { useTypedDispatch, useTypedSelector } from '../utils/state_management'; +import { setIndexPattern } from '../utils/state_management/datasource_slice'; -interface SideNavDeps { - indexPattern: IndexPattern | null; - setIndexPattern: React.Dispatch>; -} - -const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER]; - -export const SideNav = ({ indexPattern, setIndexPattern }: SideNavDeps) => { +export const SideNav = () => { const { services: { data, @@ -37,24 +25,8 @@ export const SideNav = ({ indexPattern, setIndexPattern }: SideNavDeps) => { }, } = useOpenSearchDashboards(); const { IndexPatternSelect } = data.ui; - const [indexFields, setIndexFields] = useState([]); - - // Fetch the default index pattern using the `data.indexPatterns` service, as the component is mounted. - useEffect(() => { - const setDefaultIndexPattern = async () => { - const defaultIndexPattern = await data.indexPatterns.getDefault(); - setIndexPattern(defaultIndexPattern); - }; - - setDefaultIndexPattern(); - }, [data, setIndexPattern]); - - // Update the fields list every time the index pattern is modified. - useEffect(() => { - const fields = indexPattern?.fields; - - setIndexFields(fields?.filter(isValidField) || []); - }, [indexPattern]); + const { indexPattern } = useTypedSelector((state) => state.dataSource); + const dispatch = useTypedDispatch(); const tabs: EuiTabbedContentTab[] = [ { @@ -62,7 +34,7 @@ export const SideNav = ({ indexPattern, setIndexPattern }: SideNavDeps) => { name: i18n.translate('wizard.nav.dataTab.title', { defaultMessage: 'Data', }), - content: , + content: , }, { id: 'style-tab', @@ -89,7 +61,7 @@ export const SideNav = ({ indexPattern, setIndexPattern }: SideNavDeps) => { indexPatternId={indexPattern?.id || ''} onChange={async (newIndexPatternId: any) => { const newIndexPattern = await data.indexPatterns.get(newIndexPatternId); - setIndexPattern(newIndexPattern); + dispatch(setIndexPattern(newIndexPattern)); }} isClearable={false} /> @@ -98,13 +70,3 @@ export const SideNav = ({ indexPattern, setIndexPattern }: SideNavDeps) => { ); }; - -// TODO: Temporary validate function -// Need to identify hopw to get fieldCounts to use the standard filter and group functions -function isValidField(field: IndexPatternField): boolean { - const isAggregatable = field.aggregatable === true; - const isNotScripted = !field.scripted; - const isAllowed = ALLOWED_FIELDS.includes(field.type); - - return isAggregatable && isNotScripted && isAllowed; -} diff --git a/src/plugins/wizard/public/application/components/top_nav.tsx b/src/plugins/wizard/public/application/components/top_nav.tsx index e0c9d47efefa..5afa39f7bafd 100644 --- a/src/plugins/wizard/public/application/components/top_nav.tsx +++ b/src/plugins/wizard/public/application/components/top_nav.tsx @@ -3,14 +3,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useState } from 'react'; -import { IndexPattern } from '../../../../data/public'; +import React, { useMemo } from 'react'; import { PLUGIN_ID } from '../../../common'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { getTopNavconfig } from '../utils/get_top_nav_config'; import { WizardServices } from '../../types'; import './top_nav.scss'; +import { useTypedSelector } from '../utils/state_management'; export const TopNav = () => { const { services } = useOpenSearchDashboards(); @@ -22,8 +22,7 @@ export const TopNav = () => { } = services; const config = useMemo(() => getTopNavconfig(services), [services]); - // TODO: Set index pattern/data source here. Filters wont show up until you do - const [indexPatterns, setIndexPatterns] = useState([]); + const { indexPattern } = useTypedSelector((state) => state.dataSource); return (
@@ -34,7 +33,7 @@ export const TopNav = () => { showSearchBar={true} useDefaultBehaviors={true} screenTitle="Test" - indexPatterns={indexPatterns} + indexPatterns={indexPattern ? [indexPattern] : []} />
); diff --git a/src/plugins/wizard/public/application/index.tsx b/src/plugins/wizard/public/application/index.tsx index b778d2e139d4..b48044e8b2e8 100644 --- a/src/plugins/wizard/public/application/index.tsx +++ b/src/plugins/wizard/public/application/index.tsx @@ -6,10 +6,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router } from 'react-router-dom'; +import { Provider as ReduxProvider } from 'react-redux'; import { AppMountParameters } from '../../../../core/public'; import { WizardServices } from '../types'; import { WizardApp } from './app'; import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public'; +import { store } from './utils/state_management'; export const renderApp = ( { appBasePath, element }: AppMountParameters, @@ -18,9 +20,11 @@ export const renderApp = ( ReactDOM.render( - - - + + + + + , element diff --git a/src/plugins/wizard/public/application/utils/state_management/config_slice.ts b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts new file mode 100644 index 000000000000..5d8908596104 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/config_slice.ts @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IndexPatternField } from '../../../../../data/public'; + +interface ConfigSections { + [id: string]: { + title: string; + fields: IndexPatternField[]; + }; +} +interface ConfigState { + configSections: ConfigSections; +} + +// TODO: Temp. Remove once visualizations can be refgistered and editor configs can be passed along +// TODO: this is a placeholder while the config section is iorned out +const initialState: ConfigState = { + configSections: { + x: { + title: 'X Axis', + fields: [], + }, + y: { + title: 'Y Axis', + fields: [], + }, + }, +}; + +interface SectionField { + sectionId: string; + field: IndexPatternField; +} + +export const slice = createSlice({ + name: 'configuration', + initialState, + reducers: { + addConfigSectionField: (state, action: PayloadAction) => { + const { field, sectionId } = action.payload; + if (state.configSections[sectionId]) { + state.configSections[sectionId].fields.push(field); + } + }, + removeConfigSectionField: (state, action: PayloadAction) => { + const { field, sectionId } = action.payload; + if (state.configSections[sectionId]) { + const fieldIndex = state.configSections[sectionId].fields.findIndex( + (configField) => configField === field + ); + if (fieldIndex !== -1) state.configSections[sectionId].fields.splice(fieldIndex, 1); + } + }, + }, +}); + +export const { reducer } = slice; +export const { addConfigSectionField, removeConfigSectionField } = slice.actions; diff --git a/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts new file mode 100644 index 000000000000..9bffb56ededa --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/datasource_slice.ts @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { IndexPattern } from 'src/plugins/data/common'; + +import { IndexPatternField, OSD_FIELD_TYPES } from '../../../../../data/public'; + +const ALLOWED_FIELDS: string[] = [OSD_FIELD_TYPES.STRING, OSD_FIELD_TYPES.NUMBER]; + +interface DataSourceState { + indexPattern: IndexPattern | null; + visualizableFields: IndexPatternField[]; +} + +const initialState: DataSourceState = { + indexPattern: null, + visualizableFields: [], +}; + +export const slice = createSlice({ + name: 'dataSource', + initialState, + reducers: { + setIndexPattern: (state, action: PayloadAction) => { + state.indexPattern = action.payload; + state.visualizableFields = action.payload.fields.filter(isVisualizable); + }, + }, +}); + +export const { reducer } = slice; +export const { setIndexPattern } = slice.actions; + +// TODO: Temporary validate function +// Need to identify hopw to get fieldCounts to use the standard filter and group functions +function isVisualizable(field: IndexPatternField): boolean { + const isAggregatable = field.aggregatable === true; + const isNotScripted = !field.scripted; + const isAllowed = ALLOWED_FIELDS.includes(field.type); + + return isAggregatable && isNotScripted && isAllowed; +} diff --git a/src/plugins/wizard/public/application/utils/state_management/hooks.ts b/src/plugins/wizard/public/application/utils/state_management/hooks.ts new file mode 100644 index 000000000000..823c34528c90 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/hooks.ts @@ -0,0 +1,11 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import type { RootState, AppDispatch } from './store'; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useTypedDispatch = () => useDispatch(); +export const useTypedSelector: TypedUseSelectorHook = useSelector; diff --git a/src/plugins/wizard/public/application/utils/state_management/index.ts b/src/plugins/wizard/public/application/utils/state_management/index.ts new file mode 100644 index 000000000000..edb5c2a17184 --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './store'; +export * from './hooks'; diff --git a/src/plugins/wizard/public/application/utils/state_management/store.ts b/src/plugins/wizard/public/application/utils/state_management/store.ts new file mode 100644 index 000000000000..c3c94fa673fe --- /dev/null +++ b/src/plugins/wizard/public/application/utils/state_management/store.ts @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { configureStore } from '@reduxjs/toolkit'; +import { reducer as dataSourceReducer } from './datasource_slice'; +import { reducer as configReducer } from './config_slice'; + +export const store = configureStore({ + reducer: { + dataSource: dataSourceReducer, + config: configReducer, + }, +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; diff --git a/yarn.lock b/yarn.lock index e17b00317f04..b1ad99c32f59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2552,6 +2552,16 @@ prop-types "^15.6.1" react-lifecycles-compat "^3.0.4" +"@reduxjs/toolkit@^1.6.2": + version "1.6.2" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.6.2.tgz#2f2b5365df77dd6697da28fdf44f33501ed9ba37" + integrity sha512-HbfI/hOVrAcMGAYsMWxw3UJyIoAS9JTdwddsjlr5w3S50tXhWb+EMyhIw+IAvCVCLETkzdjgH91RjDSYZekVBA== + dependencies: + immer "^9.0.6" + redux "^4.1.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -20441,6 +20451,13 @@ redux@^4.0.5: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" + integrity sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw== + dependencies: + "@babel/runtime" "^7.9.2" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460"