Skip to content

Commit

Permalink
Add datasource picker to import saved object flyout when multiple dat…
Browse files Browse the repository at this point in the history
…a source is enabled (#5781)

* add datasource picker to saved object management page when multiple data source is enabled

Signed-off-by: Lu Yu <[email protected]>

* add changelog

Signed-off-by: Lu Yu <[email protected]>

* change name to cluster selector and move to higher level

Signed-off-by: Lu Yu <[email protected]>

---------

Signed-off-by: Lu Yu <[email protected]>
  • Loading branch information
BionIT authored Feb 5, 2024
1 parent aea8716 commit 8e93c54
Show file tree
Hide file tree
Showing 25 changed files with 493 additions and 86 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- [Custom Branding] Relative URL should be allowed for logos ([#5572](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5572))
- [Discover] Enhanced the data source selector with added sorting functionality ([#5609](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/5609))
- [Multiple Datasource] Add datasource picker component and use it in devtools and tutorial page when multiple datasource is enabled ([#5756](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5756))
- [Multiple Datasource] Add datasource picker to import saved object flyout when multiple data source is enabled ([#5781](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/5781))

### 🐛 Bug Fixes

Expand Down
4 changes: 2 additions & 2 deletions src/plugins/data_source_management/opensearch_dashboards.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
"ui": true,
"requiredPlugins": ["management", "dataSource", "indexPatternManagement"],
"optionalPlugins": [],
"requiredBundles": ["opensearchDashboardsReact"],
"extraPublicDirs": ["public/components/utils", "public/components/data_source_picker/data_source_picker"]
"requiredBundles": ["opensearchDashboardsReact", "dataSource"],
"extraPublicDirs": ["public/components/utils"]
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { ShallowWrapper, shallow } from 'enzyme';
import { ClusterSelector } from './cluster_selector';
import { SavedObjectsClientContract } from '../../../../../core/public';
import { notificationServiceMock } from '../../../../../core/public/mocks';
import React from 'react';

describe('ClusterSelector', () => {
let component: ShallowWrapper<any, Readonly<{}>, React.Component<{}, {}, any>>;

let client: SavedObjectsClientContract;
const { toasts } = notificationServiceMock.createStartContract();

beforeEach(() => {
client = {
find: jest.fn().mockResolvedValue([]),
} as any;
component = shallow(
<ClusterSelector
savedObjectsClient={client}
notifications={toasts}
onSelectedDataSource={jest.fn()}
disabled={false}
fullWidth={false}
/>
);
});

it('should render normally', () => {
expect(component).toMatchSnapshot();
expect(client.find).toBeCalledWith({
fields: ['id', 'description', 'title'],
perPage: 10000,
type: 'data-source',
});
expect(toasts.addWarning).toBeCalledTimes(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,44 @@
*/

import React from 'react';
import { getDataSources } from '../utils';
import { i18n } from '@osd/i18n';
import { EuiComboBox } from '@elastic/eui';
import { SavedObjectsClientContract, ToastsStart } from 'opensearch-dashboards/public';
import { getDataSources } from '../utils';

export const LocalCluster = {
export const LocalCluster: ClusterOption = {
label: i18n.translate('dataSource.localCluster', {
defaultMessage: 'Local cluster',
}),
id: '',
};

export class DataSourcePicker extends React.Component {
constructor(props) {
interface ClusterSelectorProps {
savedObjectsClient: SavedObjectsClientContract;
notifications: ToastsStart;
onSelectedDataSource: (clusterOption: ClusterOption[]) => void;
disabled: boolean;
fullWidth: boolean;
}

interface ClusterSelectorState {
clusterOptions: ClusterOption[];
selectedOption: ClusterOption[];
}

export interface ClusterOption {
label: string;
id: string;
}

export class ClusterSelector extends React.Component<ClusterSelectorProps, ClusterSelectorState> {
private _isMounted: boolean = false;

constructor(props: ClusterSelectorProps) {
super(props);

this.state = {
dataSources: [],
clusterOptions: [],
selectedOption: [LocalCluster],
};
}
Expand All @@ -34,17 +55,17 @@ export class DataSourcePicker extends React.Component {
getDataSources(this.props.savedObjectsClient)
.then((fetchedDataSources) => {
if (fetchedDataSources?.length) {
const dataSourceOptions = fetchedDataSources.map((dataSource) => ({
const clusterOptions = fetchedDataSources.map((dataSource) => ({
id: dataSource.id,
label: dataSource.title,
}));

dataSourceOptions.push(LocalCluster);
clusterOptions.push(LocalCluster);

if (!this._isMounted) return;
this.setState({
...this.state,
dataSources: dataSourceOptions,
clusterOptions,
});
}
})
Expand All @@ -68,21 +89,23 @@ export class DataSourcePicker extends React.Component {
render() {
return (
<EuiComboBox
aria-label={i18n.translate('dataSourceComboBoxAriaLabel', {
aria-label={i18n.translate('clusterSelectorComboBoxAriaLabel', {
defaultMessage: 'Select a data source',
})}
placeholder={i18n.translate('dataSourceComboBoxPlaceholder', {
placeholder={i18n.translate('clusterSelectorComboBoxPlaceholder', {
defaultMessage: 'Select a data source',
})}
singleSelection={{ asPlainText: true }}
options={this.state.dataSources}
options={this.state.clusterOptions}
selectedOptions={this.state.selectedOption}
onChange={(e) => this.onChange(e)}
prepend={i18n.translate('dataSourceComboBoxPrepend', {
prepend={i18n.translate('clusterSelectorComboBoxPrepend', {
defaultMessage: 'Data source',
})}
compressed
isDisabled={this.props.disabled}
fullWidth={this.props.fullWidth || false}
data-test-subj={'clusterSelectorComboBox'}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { ClusterSelector } from './cluster_selector';
1 change: 1 addition & 0 deletions src/plugins/data_source_management/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export function plugin() {
return new DataSourceManagementPlugin();
}
export { DataSourceManagementPluginStart } from './types';
export { ClusterSelector } from './components/cluster_selector';
8 changes: 4 additions & 4 deletions src/plugins/dev_tools/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ import {
ScopedHistory,
} from 'src/core/public';

// eslint-disable-next-line @osd/eslint/no-restricted-paths
import { DataSourcePicker } from '../../data_source_management/public/components/data_source_picker/data_source_picker';
import { ClusterSelector } from '../../data_source_management/public';
import { DevToolApp } from './dev_tool';
import { DevToolsSetupDependencies } from './plugin';
import { addHelpMenuToAppChrome } from './utils/util';
Expand Down Expand Up @@ -131,12 +130,13 @@ function DevToolsWrapper({
</EuiToolTip>
))}
{dataSourceEnabled ? (
<div className="devAppDataSourcePicker">
<DataSourcePicker
<div className="devAppClusterSelector">
<ClusterSelector
savedObjectsClient={savedObjects.client}
notifications={toasts}
onSelectedDataSource={onChange}
disabled={!dataSourceEnabled}
fullWidth={false}
/>
</div>
) : null}
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/dev_tools/public/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
flex-grow: 1;
}

.devAppDataSourcePicker {
.devAppClusterSelector {
margin: 7px 8px 0 0;
min-width: 400px;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ import PropTypes from 'prop-types';
import { Synopsis } from './synopsis';
import { SampleDataSetCards } from './sample_data_set_cards';
import { getServices } from '../opensearch_dashboards_services';
// eslint-disable-next-line @osd/eslint/no-restricted-paths
import { DataSourcePicker } from '../../../../data_source_management/public/components/data_source_picker/data_source_picker';

import {
EuiPage,
Expand All @@ -53,6 +51,7 @@ import {
import { getTutorials } from '../load_tutorials';
import { injectI18n, FormattedMessage } from '@osd/i18n/react';
import { i18n } from '@osd/i18n';
import { ClusterSelector } from '../../../../data_source_management/public';

const ALL_TAB_ID = 'all';
const SAMPLE_DATA_TAB_ID = 'sampleData';
Expand Down Expand Up @@ -228,8 +227,8 @@ class TutorialDirectoryUi extends React.Component {
const { isDataSourceEnabled } = this.state;

return isDataSourceEnabled ? (
<div className="sampledataSourcePicker">
<DataSourcePicker
<div className="sampleDataClusterSelector">
<ClusterSelector
savedObjectsClient={getServices().savedObjectsClient}
notifications={getServices().toastNotifications}
onSelectedDataSource={this.onSelectedDataSourceChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
"discover",
"home",
"visBuilder",
"visAugmenter"
"visAugmenter",
"dataSource"
],
"extraPublicDirs": ["public/lib"],
"requiredBundles": ["opensearchDashboardsReact", "home"]
"requiredBundles": ["opensearchDashboardsReact", "home", "dataSourceManagement"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,15 @@ interface ImportResponse {
export async function importFile(
http: HttpStart,
file: File,
{ createNewCopies, overwrite }: ImportMode
{ createNewCopies, overwrite }: ImportMode,
selectedDataSourceId?: string
) {
const formData = new FormData();
formData.append('file', file);
const query = createNewCopies ? { createNewCopies } : { overwrite };
if (selectedDataSourceId) {
query.dataSourceId = selectedDataSourceId;
}
return await http.post<ImportResponse>('/api/saved_objects/_import', {
body: formData,
headers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,16 @@ async function callResolveImportErrorsApi(
http: HttpStart,
file: File,
retries: any,
createNewCopies: boolean
createNewCopies: boolean,
selectedDataSourceId?: string
): Promise<SavedObjectsImportResponse> {
const formData = new FormData();
formData.append('file', file);
formData.append('retries', JSON.stringify(retries));
const query = createNewCopies ? { createNewCopies } : {};
if (selectedDataSourceId) {
query.dataSourceId = selectedDataSourceId;
}
return http.post<any>('/api/saved_objects/_resolve_import_errors', {
headers: {
// Important to be undefined, it forces proper headers to be set for FormData
Expand Down Expand Up @@ -167,6 +171,7 @@ export async function resolveImportErrors({
http,
getConflictResolutions,
state,
selectedDataSourceId,
}: {
http: HttpStart;
getConflictResolutions: (
Expand All @@ -180,6 +185,7 @@ export async function resolveImportErrors({
file?: File;
importMode: { createNewCopies: boolean; overwrite: boolean };
};
selectedDataSourceId: string;
}) {
const retryDecisionCache = new Map<string, RetryDecision>();
const replaceReferencesCache = new Map<string, Reference[]>();
Expand Down Expand Up @@ -264,7 +270,13 @@ export async function resolveImportErrors({
}

// Call API
const response = await callResolveImportErrorsApi(http, file!, retries, createNewCopies);
const response = await callResolveImportErrorsApi(
http,
file!,
retries,
createNewCopies,
selectedDataSourceId
);
importCount = response.successCount; // reset the success count since we retry all successful results each time
failedImports = [];
for (const { error, ...obj } of response.errors || []) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ interface MountParams {
core: CoreSetup<StartDependencies, SavedObjectsManagementPluginStart>;
serviceRegistry: ISavedObjectsManagementServiceRegistry;
mountParams: ManagementAppMountParams;
dataSourceEnabled: boolean;
}

let allowedObjectTypes: string[] | undefined;
Expand All @@ -58,6 +59,7 @@ export const mountManagementSection = async ({
core,
mountParams,
serviceRegistry,
dataSourceEnabled,
}: MountParams) => {
const [coreStart, { data, uiActions }, pluginStart] = await core.getStartServices();
const { element, history, setBreadcrumbs } = mountParams;
Expand Down Expand Up @@ -108,6 +110,7 @@ export const mountManagementSection = async ({
namespaceRegistry={pluginStart.namespaces}
allowedTypes={allowedObjectTypes}
setBreadcrumbs={setBreadcrumbs}
dataSourceEnabled={dataSourceEnabled}
/>
</Suspense>
</RedirectToHomeIfUnauthorized>
Expand Down
Loading

0 comments on commit 8e93c54

Please sign in to comment.