Skip to content

Commit

Permalink
[Data Explorer] State management + enhancements (#4580)
Browse files Browse the repository at this point in the history
* Adds toggle between legacy and new discover

Signed-off-by: Ashwin P Chandran <[email protected]>

* Fixes header offset

Signed-off-by: Ashwin P Chandran <[email protected]>

* adds basic state management

Signed-off-by: Ashwin P Chandran <[email protected]>

* attempt 1 at dynamic  state management

Signed-off-by: Ashwin P Chandran <[email protected]>

* Working multi view state management

Signed-off-by: Ashwin P Chandran <[email protected]>

* Adds global state persistence to data explorer

Signed-off-by: Ashwin P Chandran <[email protected]>

---------

Signed-off-by: Ashwin P Chandran <[email protected]>
  • Loading branch information
ashwin-pc authored Jul 27, 2023
1 parent 146cc36 commit 521f306
Show file tree
Hide file tree
Showing 27 changed files with 722 additions and 216 deletions.
14 changes: 11 additions & 3 deletions src/plugins/data_explorer/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@
"opensearchDashboardsVersion": "opensearchDashboards",
"server": true,
"ui": true,
"requiredPlugins": ["data", "navigation"],
"requiredPlugins": [
"data",
"navigation",
"embeddable",
"expressions"
],
"optionalPlugins": [],
"requiredBundles": ["opensearchDashboardsReact"]
}
"requiredBundles": [
"opensearchDashboardsReact",
"opensearchDashboardsUtils"
]
}
23 changes: 14 additions & 9 deletions src/plugins/data_explorer/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,33 @@

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider as ReduxProvider } from 'react-redux';
import { Router, Route, Switch } from 'react-router-dom';
import { AppMountParameters, CoreStart } from '../../../core/public';
import { OpenSearchDashboardsContextProvider } from '../../opensearch_dashboards_react/public';
import { DataExplorerServices } from './types';
import { DataExplorerApp } from './components/app';
import { Store } from './utils/state_management';

export const renderApp = (
{ notifications, http }: CoreStart,
core: CoreStart,
services: DataExplorerServices,
params: AppMountParameters
params: AppMountParameters,
store: Store
) => {
const { history, element } = params;
ReactDOM.render(
<Router history={history}>
<OpenSearchDashboardsContextProvider services={services}>
<services.i18n.Context>
<Switch>
<Route path={[`/:appId`, '/']} exact={false}>
<DataExplorerApp params={params} />
</Route>
</Switch>
</services.i18n.Context>
<ReduxProvider store={store}>
<services.i18n.Context>
<Switch>
<Route path={[`/:appId`, '/']} exact={false}>
<DataExplorerApp params={params} />
</Route>
</Switch>
</services.i18n.Context>
</ReduxProvider>
</OpenSearchDashboardsContextProvider>
</Router>,
element
Expand Down
61 changes: 10 additions & 51 deletions src/plugins/data_explorer/public/components/app_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,31 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useLayoutEffect, useRef, useState } from 'react';
import React from 'react';
import { EuiPageTemplate } from '@elastic/eui';
import { Suspense } from 'react';
import { AppMountParameters } from '../../../../core/public';
import { Sidebar } from './sidebar';
import { NoView } from './no_view';
import { View } from '../services/view_service/view';

export const AppContainer = ({ view, params }: { view?: View; params: AppMountParameters }) => {
const [showSpinner, setShowSpinner] = useState(false);
const canvasRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const unmountRef = useRef<any>(null);

useLayoutEffect(() => {
const unmount = () => {
if (unmountRef.current) {
unmountRef.current();
unmountRef.current = null;
}
};

// Do nothing if the view is not defined or if the view is the same as the previous view
if (!view || (unmountRef.current && unmountRef.current.viewId === view.id)) {
return;
}

// unmount the previous view
unmount();

const mount = async () => {
setShowSpinner(true);
try {
unmountRef.current =
(await view.mount({
canvasElement: canvasRef.current!,
panelElement: panelRef.current!,
appParams: params,
})) || null;
} catch (e) {
// TODO: add error UI
// eslint-disable-next-line no-console
console.error(e);
} finally {
// if (canvasRef.current && panelRef.current) {
if (canvasRef.current) {
setShowSpinner(false);
}
}
};

mount();

return unmount;
}, [params, view]);

// TODO: Make this more robust.
if (!view) {
return <NoView />;
}

const { Canvas, Panel } = view;

// Render the application DOM.
// Note that `navigation.ui.TopNavMenu` is a stateful component exported on the `navigation` plugin's start contract.
return (
<EuiPageTemplate
pageSideBar={
<Sidebar>
<div ref={panelRef} />
<Suspense fallback={<div>Loading...</div>}>
<Panel {...params} />
</Suspense>
</Sidebar>
}
className="dePageTemplate"
Expand All @@ -78,8 +36,9 @@ export const AppContainer = ({ view, params }: { view?: View; params: AppMountPa
paddingSize="none"
>
{/* TODO: improve loading state */}
{showSpinner && <div>Loading...</div>}
<div key={view.id} ref={canvasRef} />
<Suspense fallback={<div>Loading...</div>}>
<Canvas {...params} />
</Suspense>
</EuiPageTemplate>
);
};
40 changes: 0 additions & 40 deletions src/plugins/data_explorer/public/components/sidebar.tsx

This file was deleted.

99 changes: 99 additions & 0 deletions src/plugins/data_explorer/public/components/sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useMemo, FC, useEffect, useState } from 'react';
import { i18n } from '@osd/i18n';
import { EuiPanel, EuiComboBox, EuiSelect, EuiComboBoxOptionOption } from '@elastic/eui';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { useView } from '../../utils/use';
import { DataExplorerServices } from '../../types';
import { useTypedDispatch, useTypedSelector, setIndexPattern } from '../../utils/state_management';
import { setView } from '../../utils/state_management/metadata_slice';

export const Sidebar: FC = ({ children }) => {
const { indexPattern: indexPatternId } = useTypedSelector((state) => state.metadata);
const dispatch = useTypedDispatch();
const [options, setOptions] = useState<Array<EuiComboBoxOptionOption<string>>>([]);
const [selectedOption, setSelectedOption] = useState<EuiComboBoxOptionOption<string>>();
const { view, viewRegistry } = useView();
const views = viewRegistry.all();
const viewOptions = useMemo(
() =>
views.map(({ id, title }) => ({
value: id,
text: title,
})),
[views]
);

const {
services: {
data: { indexPatterns },
notifications: { toasts },
},
} = useOpenSearchDashboards<DataExplorerServices>();

useEffect(() => {
const fetchIndexPatterns = async () => {
await indexPatterns.ensureDefaultIndexPattern();
const cache = await indexPatterns.getCache();
const currentOptions = (cache || []).map((indexPattern) => ({
label: indexPattern.attributes.title,
value: indexPattern.id,
}));
setOptions(currentOptions);
};
fetchIndexPatterns();
}, [indexPatterns]);

// Set option to the current index pattern
useEffect(() => {
if (indexPatternId) {
const option = options.find((o) => o.value === indexPatternId);
setSelectedOption(option);
}
}, [indexPatternId, options]);

return (
<>
<EuiPanel borderRadius="none" hasShadow={false}>
<EuiComboBox
placeholder="Select a datasource"
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selectedOption ? [selectedOption] : []}
onChange={(selected) => {
// TODO: There are many issues with this approach, but it's a start
// 1. Combo box can delete a selected index pattern. This should not be possible
// 2. Combo box is severely truncated. This should be fixed in the EUI component
// 3. The onchange can fire with a option that is not valid. discuss where to handle this.
// 4. value is optional. If the combobox needs to act as a slecet, this should be required.
const { value } = selected[0] || {};

if (!value) {
toasts.addWarning({
id: 'index-pattern-not-found',
title: i18n.translate('dataExplorer.indexPatternError', {
defaultMessage: 'Index pattern not found',
}),
});
return;
}

dispatch(setIndexPattern(value));
}}
/>
<EuiSelect
options={viewOptions}
value={view?.id}
onChange={(e) => {
dispatch(setView(e.target.value));
}}
/>
</EuiPanel>
{children}
</>
);
};
3 changes: 2 additions & 1 deletion src/plugins/data_explorer/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export function plugin() {
return new DataExplorerPlugin();
}
export { DataExplorerPluginSetup, DataExplorerPluginStart, ViewRedirectParams } from './types';
export { ViewMountParameters, ViewDefinition } from './services/view_service';
export { ViewProps, ViewDefinition } from './services/view_service';
export { RootState, useTypedSelector, useTypedDispatch } from './utils/state_management';
Loading

0 comments on commit 521f306

Please sign in to comment.