Skip to content

Commit

Permalink
adds basic state management
Browse files Browse the repository at this point in the history
Signed-off-by: Ashwin P Chandran <ashwinpc@amazon.com>
  • Loading branch information
ashwin-pc committed Jul 14, 2023
1 parent a1d1270 commit 4a72f86
Show file tree
Hide file tree
Showing 16 changed files with 435 additions and 67 deletions.
4 changes: 2 additions & 2 deletions src/plugins/data_explorer/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"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
40 changes: 0 additions & 40 deletions src/plugins/data_explorer/public/components/sidebar.tsx

This file was deleted.

102 changes: 102 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,102 @@
/*
* 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,
EuiSelectOption,
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';

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: EuiSelectOption[] = 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({
state: value,
})
);
}}
/>
<EuiSelect options={viewOptions} value={view?.id} />
</EuiPanel>
{children}
</>
);
};
34 changes: 30 additions & 4 deletions src/plugins/data_explorer/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
CoreStart,
Plugin,
AppNavLinkStatus,
ScopedHistory,
} from '../../../core/public';
import {
DataExplorerPluginSetup,
Expand All @@ -19,6 +20,11 @@ import {
} from './types';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { ViewService } from './services/view_service';
import {
createOsdUrlStateStorage,
withNotifyOnErrors,
} from '../../opensearch_dashboards_utils/public';
import { getPreloadedStore } from './utils/state_management';

export class DataExplorerPlugin
implements
Expand All @@ -29,32 +35,52 @@ export class DataExplorerPlugin
DataExplorerPluginStartDependencies
> {
private viewService = new ViewService();
private currentHistory?: ScopedHistory;

public setup(
core: CoreSetup<DataExplorerPluginStartDependencies, DataExplorerPluginStart>
): DataExplorerPluginSetup {
const viewService = this.viewService;

// Register an application into the side navigation menu
core.application.register({
id: PLUGIN_ID,
title: PLUGIN_NAME,
navLinkStatus: AppNavLinkStatus.hidden,
async mount(params: AppMountParameters) {
mount: async (params: AppMountParameters) => {
// Load application bundle
const { renderApp } = await import('./application');

const [coreStart, pluginsStart] = await core.getStartServices();
this.currentHistory = params.history;

// make sure the index pattern list is up to date
pluginsStart.data.indexPatterns.clearCache();

const services: DataExplorerServices = {
...coreStart,
scopedHistory: this.currentHistory,
data: pluginsStart.data,
embeddable: pluginsStart.embeddable,
expressions: pluginsStart.expressions,
osdUrlStateStorage: createOsdUrlStateStorage({
history: this.currentHistory,
useHash: coreStart.uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(coreStart.notifications.toasts),
}),
viewRegistry: viewService.start(),
};

// Load application bundle
const { renderApp } = await import('./application');
// Get start services as specified in opensearch_dashboards.json
// Render the application
return renderApp(coreStart, services, params);
const { store, unsubscribe: unsubscribeStore } = await getPreloadedStore(services);

const unmount = renderApp(coreStart, services, params, store);

return () => {
unsubscribeStore();
unmount();
};
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface ViewDefinition<T = any> {
readonly id: string;
readonly title: string;
readonly ui?: {
defaults: T;
defaults: T | (() => T);
reducer: (state: T, action: any) => T;
};
readonly mount: (params: ViewMountParameters) => Promise<() => void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ import { View } from './view';
* @internal
*/
export class ViewService implements CoreService<ViewServiceSetup, ViewServiceStart> {
private views: Record<string, ViewDefinition> = {};
private views: Record<string, View> = {};

private registerView(viewDefinition: View) {
if (this.views[viewDefinition.id]) {
throw new Error(`A view with this the id ${viewDefinition.id} already exists!`);
private registerView(view: View) {
if (this.views[view.id]) {
throw new Error(`A view with this the id ${view.id} already exists!`);
}
this.views[viewDefinition.id] = viewDefinition;
this.views[view.id] = view;
}

public setup() {
Expand Down
21 changes: 15 additions & 6 deletions src/plugins/data_explorer/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,23 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { CoreStart } from 'opensearch-dashboards/public';
import { ViewService } from './services/view_service';
import { CoreStart, ScopedHistory } from 'opensearch-dashboards/public';
import { EmbeddableStart } from '../../embeddable/public';
import { ExpressionsStart } from '../../expressions/public';
import { ViewServiceStart, ViewServiceSetup } from './services/view_service';
import { IOsdUrlStateStorage } from '../../opensearch_dashboards_utils/public';
import { DataPublicPluginStart } from '../../data/public';

export interface DataExplorerPluginSetup {
registerView: ViewService['registerView'];
}
export type DataExplorerPluginSetup = ViewServiceSetup;

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DataExplorerPluginStart {}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DataExplorerPluginSetupDependencies {}
export interface DataExplorerPluginStartDependencies {
expressions: ExpressionsStart;
embeddable: EmbeddableStart;
data: DataPublicPluginStart;
}

Expand All @@ -25,5 +29,10 @@ export interface ViewRedirectParams {
}

export interface DataExplorerServices extends CoreStart {
viewRegistry: ReturnType<ViewService['start']>;
viewRegistry: ViewServiceStart;
expressions: ExpressionsStart;
embeddable: EmbeddableStart;
data: DataPublicPluginStart;
scopedHistory: ScopedHistory;
osdUrlStateStorage: IOsdUrlStateStorage;
}
35 changes: 35 additions & 0 deletions src/plugins/data_explorer/public/utils/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { ScopedHistory } from '../../../../core/public';
import { coreMock, scopedHistoryMock } from '../../../../core/public/mocks';
import { dataPluginMock } from '../../../data/public/mocks';
import { embeddablePluginMock } from '../../../embeddable/public/mocks';
import { expressionsPluginMock } from '../../../expressions/public/mocks';
import { createOsdUrlStateStorage } from '../../../opensearch_dashboards_utils/public';
import { DataExplorerServices } from '../types';

export const createDataExplorerServicesMock = () => {
const coreStartMock = coreMock.createStart();
const dataMock = dataPluginMock.createStartContract();
const embeddableMock = embeddablePluginMock.createStartContract();
const expressionMock = expressionsPluginMock.createStartContract();
const osdUrlStateStorageMock = createOsdUrlStateStorage({ useHash: false });

const dataExplorerServicesMock: DataExplorerServices = {
...coreStartMock,
expressions: expressionMock,
data: dataMock,
osdUrlStateStorage: osdUrlStateStorageMock,
embeddable: embeddableMock,
scopedHistory: (scopedHistoryMock.create() as unknown) as ScopedHistory,
viewRegistry: {
get: jest.fn(),
all: jest.fn(),
},
};

return (dataExplorerServicesMock as unknown) as jest.Mocked<DataExplorerServices>;
};
11 changes: 11 additions & 0 deletions src/plugins/data_explorer/public/utils/state_management/hooks.ts
Original file line number Diff line number Diff line change
@@ -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 the app instead of plain `useDispatch` and `useSelector`
export const useTypedDispatch = () => useDispatch<AppDispatch>();
export const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './store';
export * from './hooks';
Loading

0 comments on commit 4a72f86

Please sign in to comment.