Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add multi data source support #315

Merged
merged 4 commits into from
Apr 22, 2024
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions opensearch_dashboards.json
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
@@ -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>
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
@@ -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;
@@ -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 {
@@ -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