Skip to content

Commit

Permalink
add multi data source support (#315) (#317)
Browse files Browse the repository at this point in the history
* add multi data source support



* add unit tests for server side



* Use dataSource to check if data source enabled



* Add comments for DO_NOT_FETCH



---------

Signed-off-by: Lin Wang <[email protected]>
  • Loading branch information
wanglam authored Apr 23, 2024
1 parent de46a91 commit c3f87fa
Show file tree
Hide file tree
Showing 50 changed files with 2,077 additions and 128 deletions.
2 changes: 1 addition & 1 deletion opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"dashboard",
"opensearchUiShared"
],
"optionalPlugins": []
"optionalPlugins": ["dataSource", "dataSourceManagement"]
}
17 changes: 13 additions & 4 deletions public/apis/connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ interface GetAllInternalConnectorResponse {
}

export class Connector {
public getAll() {
return InnerHttpProvider.getHttp().get<GetAllConnectorResponse>(CONNECTOR_API_ENDPOINT);
public getAll({ dataSourceId }: { dataSourceId?: string }) {
return InnerHttpProvider.getHttp().get<GetAllConnectorResponse>(CONNECTOR_API_ENDPOINT, {
query: {
data_source_id: dataSourceId,
},
});
}

public getAllInternal() {
public getAllInternal({ dataSourceId }: { dataSourceId?: string }) {
return InnerHttpProvider.getHttp().get<GetAllInternalConnectorResponse>(
INTERNAL_CONNECTOR_API_ENDPOINT
INTERNAL_CONNECTOR_API_ENDPOINT,
{
query: {
data_source_id: dataSourceId,
},
}
);
}
}
7 changes: 5 additions & 2 deletions public/apis/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ export class Model {
states?: MODEL_STATE[];
nameOrId?: string;
extraQuery?: Record<string, any>;
dataSourceId?: string;
}) {
const { extraQuery, ...restQuery } = query;
const { extraQuery, dataSourceId, ...restQuery } = query;
return InnerHttpProvider.getHttp().get<ModelSearchResponse>(MODEL_API_ENDPOINT, {
query: extraQuery ? { ...restQuery, extra_query: JSON.stringify(extraQuery) } : restQuery,
query: extraQuery
? { ...restQuery, extra_query: JSON.stringify(extraQuery), data_source_id: dataSourceId }
: { ...restQuery, data_source_id: dataSourceId },
});
}
}
9 changes: 7 additions & 2 deletions public/apis/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ export interface ModelDeploymentProfile {
}

export class Profile {
public getModel(modelId: string) {
public getModel(modelId: string, { dataSourceId }: { dataSourceId?: string }) {
return InnerHttpProvider.getHttp().get<ModelDeploymentProfile>(
`${DEPLOYED_MODEL_PROFILE_API_ENDPOINT}/${modelId}`
`${DEPLOYED_MODEL_PROFILE_API_ENDPOINT}/${modelId}`,
{
query: {
data_source_id: dataSourceId,
},
}
);
}
}
6 changes: 5 additions & 1 deletion public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { APIProvider } from './apis/api_provider';
import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public';

export const renderApp = (
{ element, history, appBasePath }: AppMountParameters,
{ element, history, appBasePath, setHeaderActionMenu }: AppMountParameters,
services: MLServices
) => {
InnerHttpProvider.setHttp(services.http);
Expand All @@ -31,6 +31,10 @@ export const renderApp = (
chrome={services.chrome}
data={services.data}
uiSettingsClient={services.uiSettings}
savedObjects={services.savedObjects}
setActionMenu={setHeaderActionMenu}
dataSource={services.dataSource}
dataSourceManagement={services.dataSourceManagement}
/>
</services.i18n.Context>
</OpenSearchDashboardsContextProvider>
Expand Down
112 changes: 112 additions & 0 deletions public/components/__tests__/data_source_top_nav_menu.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useContext } from 'react';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from '../../../test/test_utils';
import { DataSourceTopNavMenu, DataSourceTopNavMenuProps } from '../data_source_top_nav_menu';
import { coreMock } from '../../../../../src/core/public/mocks';
import { DataSourceContext } from '../../contexts';

function setup(options: Partial<DataSourceTopNavMenuProps> = {}) {
const user = userEvent.setup({});
const coreStart = coreMock.createStart();
const DataSourceMenu = ({ componentConfig: { onSelectedDataSources } }) => (
<div>
<div>Data Source Menu</div>
<div>
<button
onClick={() => {
onSelectedDataSources([]);
}}
aria-label="invalidDataSource"
>
Invalid data source
</button>
<button
onClick={() => {
onSelectedDataSources([{ id: 'ds1', label: 'Data Source 1' }]);
}}
aria-label="validDataSource"
>
Valid data source
</button>
</div>
</div>
);

const DataSourceConsumer = () => {
const { selectedDataSourceOption } = useContext(DataSourceContext);

return (
<div>
<input
value={
selectedDataSourceOption === undefined
? 'undefined'
: JSON.stringify(selectedDataSourceOption)
}
aria-label="selectedDataSourceOption"
onChange={() => {}}
/>
</div>
);
};

const renderResult = render(
<>
<DataSourceTopNavMenu
notifications={coreStart.notifications}
savedObjects={coreStart.savedObjects}
dataSourceManagement={{
registerAuthenticationMethod: jest.fn(),
ui: {
DataSourceSelector: () => null,
getDataSourceMenu: () => DataSourceMenu,
},
}}
setActionMenu={jest.fn()}
{...options}
/>
<DataSourceConsumer />
</>
);
return { user, renderResult };
}

describe('<DataSourceTopNavMenu />', () => {
it('should not render data source menu when data source management not defined', () => {
setup({
dataSourceManagement: undefined,
});
expect(screen.queryByText('Data Source Menu')).not.toBeInTheDocument();
});

it('should render data source menu and data source context', () => {
setup();
expect(screen.getByText('Data Source Menu')).toBeInTheDocument();
expect(screen.getByLabelText('selectedDataSourceOption')).toHaveValue('null');
});

it('should set selected data source option to undefined', async () => {
const { user } = setup();
expect(screen.getByText('Data Source Menu')).toBeInTheDocument();
await user.click(screen.getByLabelText('invalidDataSource'));
await waitFor(() => {
expect(screen.getByLabelText('selectedDataSourceOption')).toHaveValue('undefined');
});
});

it('should set selected data source option to valid data source', async () => {
const { user } = setup();
expect(screen.getByText('Data Source Menu')).toBeInTheDocument();
await user.click(screen.getByLabelText('validDataSource'));
await waitFor(() => {
expect(screen.getByLabelText('selectedDataSourceOption')).toHaveValue(
JSON.stringify({ id: 'ds1', label: 'Data Source 1' })
);
});
});
});
72 changes: 53 additions & 19 deletions public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ import { EuiPage, EuiPageBody } from '@elastic/eui';
import { ROUTES } from '../../common/router';
import { routerPaths } from '../../common/router_paths';

import { CoreStart, IUiSettingsClient } from '../../../../src/core/public';
import {
CoreStart,
IUiSettingsClient,
MountPoint,
SavedObjectsStart,
} from '../../../../src/core/public';
import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public';
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
import type { DataSourceManagementPluginSetup } from '../../../../src/plugins/data_source_management/public';
import type { DataSourcePluginSetup } from '../../../../src/plugins/data_source/public';
import { DataSourceContextProvider } from '../contexts/data_source_context';

import { GlobalBreadcrumbs } from './global_breadcrumbs';
import { DataSourceTopNavMenu } from './data_source_top_nav_menu';

interface MlCommonsPluginAppDeps {
basename: string;
Expand All @@ -24,6 +33,10 @@ interface MlCommonsPluginAppDeps {
chrome: CoreStart['chrome'];
data: DataPublicPluginStart;
uiSettingsClient: IUiSettingsClient;
savedObjects: SavedObjectsStart;
dataSource?: DataSourcePluginSetup;
dataSourceManagement?: DataSourceManagementPluginSetup;
setActionMenu: (menuMount: MountPoint | undefined) => void;
}

export interface ComponentsCommonProps {
Expand All @@ -38,27 +51,48 @@ export const MlCommonsPluginApp = ({
http,
chrome,
data,
dataSource,
dataSourceManagement,
savedObjects,
setActionMenu,
}: MlCommonsPluginAppDeps) => {
const dataSourceEnabled = !!dataSource;
return (
<I18nProvider>
<>
<EuiPage>
<EuiPageBody component="main">
<Switch>
{ROUTES.map(({ path, Component, exact }) => (
<Route
key={path}
path={path}
render={() => <Component http={http} notifications={notifications} data={data} />}
exact={exact ?? false}
/>
))}
<Redirect from={routerPaths.root} to={routerPaths.overview} />
</Switch>
</EuiPageBody>
</EuiPage>
<GlobalBreadcrumbs chrome={chrome} basename={basename} />
</>
<DataSourceContextProvider
initialValue={{
dataSourceEnabled,
}}
>
<>
<EuiPage>
<EuiPageBody component="main">
<Switch>
{ROUTES.map(({ path, Component, exact }) => (
<Route
key={path}
path={path}
render={() => (
<Component http={http} notifications={notifications} data={data} />
)}
exact={exact ?? false}
/>
))}
<Redirect from={routerPaths.root} to={routerPaths.overview} />
</Switch>
</EuiPageBody>
</EuiPage>
<GlobalBreadcrumbs chrome={chrome} basename={basename} />
{dataSourceEnabled && (
<DataSourceTopNavMenu
notifications={notifications}
dataSourceManagement={dataSourceManagement}
setActionMenu={setActionMenu}
savedObjects={savedObjects}
/>
)}
</>
</DataSourceContextProvider>
</I18nProvider>
);
};
60 changes: 60 additions & 0 deletions public/components/data_source_top_nav_menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useMemo, useContext, useCallback } from 'react';

import type { CoreStart, MountPoint, SavedObjectsStart } from '../../../../src/core/public';
import type {
DataSourceManagementPluginSetup,
DataSourceSelectableConfig,
} from '../../../../src/plugins/data_source_management/public';
import { DataSourceContext } from '../contexts/data_source_context';

export interface DataSourceTopNavMenuProps {
notifications: CoreStart['notifications'];
savedObjects: SavedObjectsStart;
dataSourceManagement?: DataSourceManagementPluginSetup;
setActionMenu: (menuMount: MountPoint | undefined) => void;
}

export const DataSourceTopNavMenu = ({
savedObjects,
notifications,
setActionMenu,
dataSourceManagement,
}: DataSourceTopNavMenuProps) => {
const DataSourceMenu = useMemo(() => dataSourceManagement?.ui.getDataSourceMenu(), [
dataSourceManagement,
]);
const { selectedDataSourceOption, setSelectedDataSourceOption } = useContext(DataSourceContext);
const activeOption = useMemo(() => (selectedDataSourceOption ? [selectedDataSourceOption] : []), [
selectedDataSourceOption,
]);

const handleDataSourcesSelected = useCallback<
DataSourceSelectableConfig['onSelectedDataSources']
>(
(dataSourceOptions) => {
setSelectedDataSourceOption(dataSourceOptions[0]);
},
[setSelectedDataSourceOption]
);

if (!DataSourceMenu) {
return null;
}
return (
<DataSourceMenu
componentType="DataSourceSelectable"
componentConfig={{
notifications,
savedObjects: savedObjects.client,
onSelectedDataSources: handleDataSourcesSelected,
activeOption,
}}
setMenuMountPoint={setActionMenu}
/>
);
};
Loading

0 comments on commit c3f87fa

Please sign in to comment.