Skip to content

Commit c3f87fa

Browse files
authored
add multi data source support (#315) (#317)
* 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 <wonglam@amazon.com>
1 parent de46a91 commit c3f87fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2077
-128
lines changed

opensearch_dashboards.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
"dashboard",
1313
"opensearchUiShared"
1414
],
15-
"optionalPlugins": []
15+
"optionalPlugins": ["dataSource", "dataSourceManagement"]
1616
}

public/apis/connector.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ interface GetAllInternalConnectorResponse {
2323
}
2424

2525
export class Connector {
26-
public getAll() {
27-
return InnerHttpProvider.getHttp().get<GetAllConnectorResponse>(CONNECTOR_API_ENDPOINT);
26+
public getAll({ dataSourceId }: { dataSourceId?: string }) {
27+
return InnerHttpProvider.getHttp().get<GetAllConnectorResponse>(CONNECTOR_API_ENDPOINT, {
28+
query: {
29+
data_source_id: dataSourceId,
30+
},
31+
});
2832
}
2933

30-
public getAllInternal() {
34+
public getAllInternal({ dataSourceId }: { dataSourceId?: string }) {
3135
return InnerHttpProvider.getHttp().get<GetAllInternalConnectorResponse>(
32-
INTERNAL_CONNECTOR_API_ENDPOINT
36+
INTERNAL_CONNECTOR_API_ENDPOINT,
37+
{
38+
query: {
39+
data_source_id: dataSourceId,
40+
},
41+
}
3342
);
3443
}
3544
}

public/apis/model.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ export class Model {
3636
states?: MODEL_STATE[];
3737
nameOrId?: string;
3838
extraQuery?: Record<string, any>;
39+
dataSourceId?: string;
3940
}) {
40-
const { extraQuery, ...restQuery } = query;
41+
const { extraQuery, dataSourceId, ...restQuery } = query;
4142
return InnerHttpProvider.getHttp().get<ModelSearchResponse>(MODEL_API_ENDPOINT, {
42-
query: extraQuery ? { ...restQuery, extra_query: JSON.stringify(extraQuery) } : restQuery,
43+
query: extraQuery
44+
? { ...restQuery, extra_query: JSON.stringify(extraQuery), data_source_id: dataSourceId }
45+
: { ...restQuery, data_source_id: dataSourceId },
4346
});
4447
}
4548
}

public/apis/profile.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@ export interface ModelDeploymentProfile {
1414
}
1515

1616
export class Profile {
17-
public getModel(modelId: string) {
17+
public getModel(modelId: string, { dataSourceId }: { dataSourceId?: string }) {
1818
return InnerHttpProvider.getHttp().get<ModelDeploymentProfile>(
19-
`${DEPLOYED_MODEL_PROFILE_API_ENDPOINT}/${modelId}`
19+
`${DEPLOYED_MODEL_PROFILE_API_ENDPOINT}/${modelId}`,
20+
{
21+
query: {
22+
data_source_id: dataSourceId,
23+
},
24+
}
2025
);
2126
}
2227
}

public/application.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { APIProvider } from './apis/api_provider';
1414
import { OpenSearchDashboardsContextProvider } from '../../../src/plugins/opensearch_dashboards_react/public';
1515

1616
export const renderApp = (
17-
{ element, history, appBasePath }: AppMountParameters,
17+
{ element, history, appBasePath, setHeaderActionMenu }: AppMountParameters,
1818
services: MLServices
1919
) => {
2020
InnerHttpProvider.setHttp(services.http);
@@ -31,6 +31,10 @@ export const renderApp = (
3131
chrome={services.chrome}
3232
data={services.data}
3333
uiSettingsClient={services.uiSettings}
34+
savedObjects={services.savedObjects}
35+
setActionMenu={setHeaderActionMenu}
36+
dataSource={services.dataSource}
37+
dataSourceManagement={services.dataSourceManagement}
3438
/>
3539
</services.i18n.Context>
3640
</OpenSearchDashboardsContextProvider>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React, { useContext } from 'react';
7+
import userEvent from '@testing-library/user-event';
8+
import { render, screen, waitFor } from '../../../test/test_utils';
9+
import { DataSourceTopNavMenu, DataSourceTopNavMenuProps } from '../data_source_top_nav_menu';
10+
import { coreMock } from '../../../../../src/core/public/mocks';
11+
import { DataSourceContext } from '../../contexts';
12+
13+
function setup(options: Partial<DataSourceTopNavMenuProps> = {}) {
14+
const user = userEvent.setup({});
15+
const coreStart = coreMock.createStart();
16+
const DataSourceMenu = ({ componentConfig: { onSelectedDataSources } }) => (
17+
<div>
18+
<div>Data Source Menu</div>
19+
<div>
20+
<button
21+
onClick={() => {
22+
onSelectedDataSources([]);
23+
}}
24+
aria-label="invalidDataSource"
25+
>
26+
Invalid data source
27+
</button>
28+
<button
29+
onClick={() => {
30+
onSelectedDataSources([{ id: 'ds1', label: 'Data Source 1' }]);
31+
}}
32+
aria-label="validDataSource"
33+
>
34+
Valid data source
35+
</button>
36+
</div>
37+
</div>
38+
);
39+
40+
const DataSourceConsumer = () => {
41+
const { selectedDataSourceOption } = useContext(DataSourceContext);
42+
43+
return (
44+
<div>
45+
<input
46+
value={
47+
selectedDataSourceOption === undefined
48+
? 'undefined'
49+
: JSON.stringify(selectedDataSourceOption)
50+
}
51+
aria-label="selectedDataSourceOption"
52+
onChange={() => {}}
53+
/>
54+
</div>
55+
);
56+
};
57+
58+
const renderResult = render(
59+
<>
60+
<DataSourceTopNavMenu
61+
notifications={coreStart.notifications}
62+
savedObjects={coreStart.savedObjects}
63+
dataSourceManagement={{
64+
registerAuthenticationMethod: jest.fn(),
65+
ui: {
66+
DataSourceSelector: () => null,
67+
getDataSourceMenu: () => DataSourceMenu,
68+
},
69+
}}
70+
setActionMenu={jest.fn()}
71+
{...options}
72+
/>
73+
<DataSourceConsumer />
74+
</>
75+
);
76+
return { user, renderResult };
77+
}
78+
79+
describe('<DataSourceTopNavMenu />', () => {
80+
it('should not render data source menu when data source management not defined', () => {
81+
setup({
82+
dataSourceManagement: undefined,
83+
});
84+
expect(screen.queryByText('Data Source Menu')).not.toBeInTheDocument();
85+
});
86+
87+
it('should render data source menu and data source context', () => {
88+
setup();
89+
expect(screen.getByText('Data Source Menu')).toBeInTheDocument();
90+
expect(screen.getByLabelText('selectedDataSourceOption')).toHaveValue('null');
91+
});
92+
93+
it('should set selected data source option to undefined', async () => {
94+
const { user } = setup();
95+
expect(screen.getByText('Data Source Menu')).toBeInTheDocument();
96+
await user.click(screen.getByLabelText('invalidDataSource'));
97+
await waitFor(() => {
98+
expect(screen.getByLabelText('selectedDataSourceOption')).toHaveValue('undefined');
99+
});
100+
});
101+
102+
it('should set selected data source option to valid data source', async () => {
103+
const { user } = setup();
104+
expect(screen.getByText('Data Source Menu')).toBeInTheDocument();
105+
await user.click(screen.getByLabelText('validDataSource'));
106+
await waitFor(() => {
107+
expect(screen.getByLabelText('selectedDataSourceOption')).toHaveValue(
108+
JSON.stringify({ id: 'ds1', label: 'Data Source 1' })
109+
);
110+
});
111+
});
112+
});

public/components/app.tsx

+53-19
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,20 @@ import { EuiPage, EuiPageBody } from '@elastic/eui';
1010
import { ROUTES } from '../../common/router';
1111
import { routerPaths } from '../../common/router_paths';
1212

13-
import { CoreStart, IUiSettingsClient } from '../../../../src/core/public';
13+
import {
14+
CoreStart,
15+
IUiSettingsClient,
16+
MountPoint,
17+
SavedObjectsStart,
18+
} from '../../../../src/core/public';
1419
import { NavigationPublicPluginStart } from '../../../../src/plugins/navigation/public';
1520
import { DataPublicPluginStart } from '../../../../src/plugins/data/public';
21+
import type { DataSourceManagementPluginSetup } from '../../../../src/plugins/data_source_management/public';
22+
import type { DataSourcePluginSetup } from '../../../../src/plugins/data_source/public';
23+
import { DataSourceContextProvider } from '../contexts/data_source_context';
1624

1725
import { GlobalBreadcrumbs } from './global_breadcrumbs';
26+
import { DataSourceTopNavMenu } from './data_source_top_nav_menu';
1827

1928
interface MlCommonsPluginAppDeps {
2029
basename: string;
@@ -24,6 +33,10 @@ interface MlCommonsPluginAppDeps {
2433
chrome: CoreStart['chrome'];
2534
data: DataPublicPluginStart;
2635
uiSettingsClient: IUiSettingsClient;
36+
savedObjects: SavedObjectsStart;
37+
dataSource?: DataSourcePluginSetup;
38+
dataSourceManagement?: DataSourceManagementPluginSetup;
39+
setActionMenu: (menuMount: MountPoint | undefined) => void;
2740
}
2841

2942
export interface ComponentsCommonProps {
@@ -38,27 +51,48 @@ export const MlCommonsPluginApp = ({
3851
http,
3952
chrome,
4053
data,
54+
dataSource,
55+
dataSourceManagement,
56+
savedObjects,
57+
setActionMenu,
4158
}: MlCommonsPluginAppDeps) => {
59+
const dataSourceEnabled = !!dataSource;
4260
return (
4361
<I18nProvider>
44-
<>
45-
<EuiPage>
46-
<EuiPageBody component="main">
47-
<Switch>
48-
{ROUTES.map(({ path, Component, exact }) => (
49-
<Route
50-
key={path}
51-
path={path}
52-
render={() => <Component http={http} notifications={notifications} data={data} />}
53-
exact={exact ?? false}
54-
/>
55-
))}
56-
<Redirect from={routerPaths.root} to={routerPaths.overview} />
57-
</Switch>
58-
</EuiPageBody>
59-
</EuiPage>
60-
<GlobalBreadcrumbs chrome={chrome} basename={basename} />
61-
</>
62+
<DataSourceContextProvider
63+
initialValue={{
64+
dataSourceEnabled,
65+
}}
66+
>
67+
<>
68+
<EuiPage>
69+
<EuiPageBody component="main">
70+
<Switch>
71+
{ROUTES.map(({ path, Component, exact }) => (
72+
<Route
73+
key={path}
74+
path={path}
75+
render={() => (
76+
<Component http={http} notifications={notifications} data={data} />
77+
)}
78+
exact={exact ?? false}
79+
/>
80+
))}
81+
<Redirect from={routerPaths.root} to={routerPaths.overview} />
82+
</Switch>
83+
</EuiPageBody>
84+
</EuiPage>
85+
<GlobalBreadcrumbs chrome={chrome} basename={basename} />
86+
{dataSourceEnabled && (
87+
<DataSourceTopNavMenu
88+
notifications={notifications}
89+
dataSourceManagement={dataSourceManagement}
90+
setActionMenu={setActionMenu}
91+
savedObjects={savedObjects}
92+
/>
93+
)}
94+
</>
95+
</DataSourceContextProvider>
6296
</I18nProvider>
6397
);
6498
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React, { useMemo, useContext, useCallback } from 'react';
7+
8+
import type { CoreStart, MountPoint, SavedObjectsStart } from '../../../../src/core/public';
9+
import type {
10+
DataSourceManagementPluginSetup,
11+
DataSourceSelectableConfig,
12+
} from '../../../../src/plugins/data_source_management/public';
13+
import { DataSourceContext } from '../contexts/data_source_context';
14+
15+
export interface DataSourceTopNavMenuProps {
16+
notifications: CoreStart['notifications'];
17+
savedObjects: SavedObjectsStart;
18+
dataSourceManagement?: DataSourceManagementPluginSetup;
19+
setActionMenu: (menuMount: MountPoint | undefined) => void;
20+
}
21+
22+
export const DataSourceTopNavMenu = ({
23+
savedObjects,
24+
notifications,
25+
setActionMenu,
26+
dataSourceManagement,
27+
}: DataSourceTopNavMenuProps) => {
28+
const DataSourceMenu = useMemo(() => dataSourceManagement?.ui.getDataSourceMenu(), [
29+
dataSourceManagement,
30+
]);
31+
const { selectedDataSourceOption, setSelectedDataSourceOption } = useContext(DataSourceContext);
32+
const activeOption = useMemo(() => (selectedDataSourceOption ? [selectedDataSourceOption] : []), [
33+
selectedDataSourceOption,
34+
]);
35+
36+
const handleDataSourcesSelected = useCallback<
37+
DataSourceSelectableConfig['onSelectedDataSources']
38+
>(
39+
(dataSourceOptions) => {
40+
setSelectedDataSourceOption(dataSourceOptions[0]);
41+
},
42+
[setSelectedDataSourceOption]
43+
);
44+
45+
if (!DataSourceMenu) {
46+
return null;
47+
}
48+
return (
49+
<DataSourceMenu
50+
componentType="DataSourceSelectable"
51+
componentConfig={{
52+
notifications,
53+
savedObjects: savedObjects.client,
54+
onSelectedDataSources: handleDataSourcesSelected,
55+
activeOption,
56+
}}
57+
setMenuMountPoint={setActionMenu}
58+
/>
59+
);
60+
};

0 commit comments

Comments
 (0)