From 8e93c54c7b77f8795428ee5feb3e215f54fec52b Mon Sep 17 00:00:00 2001 From: Lu Yu Date: Mon, 5 Feb 2024 14:22:59 -0800 Subject: [PATCH] Add datasource picker to import saved object flyout when multiple data source is enabled (#5781) * add datasource picker to saved object management page when multiple data source is enabled Signed-off-by: Lu Yu * add changelog Signed-off-by: Lu Yu * change name to cluster selector and move to higher level Signed-off-by: Lu Yu --------- Signed-off-by: Lu Yu --- CHANGELOG.md | 1 + .../opensearch_dashboards.json | 4 +- .../cluster_selector.test.tsx.snap | 31 ++++ .../cluster_selector.test.tsx | 42 +++++ .../cluster_selector.tsx} | 47 +++-- .../components/cluster_selector/index.ts | 6 + .../data_source_management/public/index.ts | 1 + src/plugins/dev_tools/public/application.tsx | 8 +- src/plugins/dev_tools/public/index.scss | 2 +- .../components/tutorial_directory.js | 7 +- .../opensearch_dashboards.json | 5 +- .../public/lib/import_file.ts | 6 +- .../public/lib/resolve_import_errors.ts | 16 +- .../management_section/mount_section.tsx | 3 + .../__snapshots__/flyout.test.tsx.snap | 161 +++++++++++++++++- .../objects_table/components/flyout.test.tsx | 30 +++- .../objects_table/components/flyout.tsx | 76 ++++++++- .../components/import_mode_control.tsx | 56 +++--- .../objects_table/saved_objects_table.tsx | 4 + .../saved_objects_table_page.tsx | 3 + .../saved_objects_management/public/plugin.ts | 5 +- test/functional/apps/dashboard/time_zones.js | 4 +- .../apps/management/_import_objects.js | 48 ++++-- .../management/_mgmt_import_saved_objects.js | 4 +- .../management/saved_objects_page.ts | 9 +- 25 files changed, 493 insertions(+), 86 deletions(-) create mode 100644 src/plugins/data_source_management/public/components/cluster_selector/__snapshots__/cluster_selector.test.tsx.snap create mode 100644 src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.test.tsx rename src/plugins/data_source_management/public/components/{data_source_picker/data_source_picker.js => cluster_selector/cluster_selector.tsx} (58%) create mode 100644 src/plugins/data_source_management/public/components/cluster_selector/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bbed149a4d..85310fe5c4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/plugins/data_source_management/opensearch_dashboards.json b/src/plugins/data_source_management/opensearch_dashboards.json index 6d0ed5b98d6a..cfcfdd2ce430 100644 --- a/src/plugins/data_source_management/opensearch_dashboards.json +++ b/src/plugins/data_source_management/opensearch_dashboards.json @@ -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"] } diff --git a/src/plugins/data_source_management/public/components/cluster_selector/__snapshots__/cluster_selector.test.tsx.snap b/src/plugins/data_source_management/public/components/cluster_selector/__snapshots__/cluster_selector.test.tsx.snap new file mode 100644 index 000000000000..3012d5c61bcd --- /dev/null +++ b/src/plugins/data_source_management/public/components/cluster_selector/__snapshots__/cluster_selector.test.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ClusterSelector should render normally 1`] = ` + +`; diff --git a/src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.test.tsx b/src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.test.tsx new file mode 100644 index 000000000000..5202b3c43067 --- /dev/null +++ b/src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.test.tsx @@ -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, React.Component<{}, {}, any>>; + + let client: SavedObjectsClientContract; + const { toasts } = notificationServiceMock.createStartContract(); + + beforeEach(() => { + client = { + find: jest.fn().mockResolvedValue([]), + } as any; + component = shallow( + + ); + }); + + 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); + }); +}); diff --git a/src/plugins/data_source_management/public/components/data_source_picker/data_source_picker.js b/src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.tsx similarity index 58% rename from src/plugins/data_source_management/public/components/data_source_picker/data_source_picker.js rename to src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.tsx index 2747a5f886ce..c30c7624d516 100644 --- a/src/plugins/data_source_management/public/components/data_source_picker/data_source_picker.js +++ b/src/plugins/data_source_management/public/components/cluster_selector/cluster_selector.tsx @@ -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 { + private _isMounted: boolean = false; + + constructor(props: ClusterSelectorProps) { super(props); this.state = { - dataSources: [], + clusterOptions: [], selectedOption: [LocalCluster], }; } @@ -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, }); } }) @@ -68,21 +89,23 @@ export class DataSourcePicker extends React.Component { render() { return ( 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'} /> ); } diff --git a/src/plugins/data_source_management/public/components/cluster_selector/index.ts b/src/plugins/data_source_management/public/components/cluster_selector/index.ts new file mode 100644 index 000000000000..8e65dc8dc698 --- /dev/null +++ b/src/plugins/data_source_management/public/components/cluster_selector/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { ClusterSelector } from './cluster_selector'; diff --git a/src/plugins/data_source_management/public/index.ts b/src/plugins/data_source_management/public/index.ts index acae34449ce3..5cd2c6c96d11 100644 --- a/src/plugins/data_source_management/public/index.ts +++ b/src/plugins/data_source_management/public/index.ts @@ -11,3 +11,4 @@ export function plugin() { return new DataSourceManagementPlugin(); } export { DataSourceManagementPluginStart } from './types'; +export { ClusterSelector } from './components/cluster_selector'; diff --git a/src/plugins/dev_tools/public/application.tsx b/src/plugins/dev_tools/public/application.tsx index e03733927d7a..f27729d56867 100644 --- a/src/plugins/dev_tools/public/application.tsx +++ b/src/plugins/dev_tools/public/application.tsx @@ -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'; @@ -131,12 +130,13 @@ function DevToolsWrapper({ ))} {dataSourceEnabled ? ( -
- +
) : null} diff --git a/src/plugins/dev_tools/public/index.scss b/src/plugins/dev_tools/public/index.scss index 38bb0d31f166..a526bde3b659 100644 --- a/src/plugins/dev_tools/public/index.scss +++ b/src/plugins/dev_tools/public/index.scss @@ -22,7 +22,7 @@ flex-grow: 1; } -.devAppDataSourcePicker { +.devAppClusterSelector { margin: 7px 8px 0 0; min-width: 400px; } diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 3e8ba2d10c80..9f71652bfd8b 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -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, @@ -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'; @@ -228,8 +227,8 @@ class TutorialDirectoryUi extends React.Component { const { isDataSourceEnabled } = this.state; return isDataSourceEnabled ? ( -
- + ('/api/saved_objects/_import', { body: formData, headers: { diff --git a/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts index 34b0dfa5be0b..585102ee5b8e 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_import_errors.ts @@ -89,12 +89,16 @@ async function callResolveImportErrorsApi( http: HttpStart, file: File, retries: any, - createNewCopies: boolean + createNewCopies: boolean, + selectedDataSourceId?: string ): Promise { 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('/api/saved_objects/_resolve_import_errors', { headers: { // Important to be undefined, it forces proper headers to be set for FormData @@ -167,6 +171,7 @@ export async function resolveImportErrors({ http, getConflictResolutions, state, + selectedDataSourceId, }: { http: HttpStart; getConflictResolutions: ( @@ -180,6 +185,7 @@ export async function resolveImportErrors({ file?: File; importMode: { createNewCopies: boolean; overwrite: boolean }; }; + selectedDataSourceId: string; }) { const retryDecisionCache = new Map(); const replaceReferencesCache = new Map(); @@ -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 || []) { diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index a1c7b5343eb1..f56ece8b51d2 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -44,6 +44,7 @@ interface MountParams { core: CoreSetup; serviceRegistry: ISavedObjectsManagementServiceRegistry; mountParams: ManagementAppMountParams; + dataSourceEnabled: boolean; } let allowedObjectTypes: string[] | undefined; @@ -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; @@ -108,6 +110,7 @@ export const mountManagementSection = async ({ namespaceRegistry={pluginStart.namespaces} allowedTypes={allowedObjectTypes} setBreadcrumbs={setBreadcrumbs} + dataSourceEnabled={dataSourceEnabled} /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index c91fcbd7769e..b40211fffe34 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -184,6 +184,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "post": [MockFunction], "put": [MockFunction], }, + "selectedDataSourceId": undefined, "state": Object { "conflictedIndexPatterns": undefined, "conflictedSavedObjectsLinkedToSavedSearches": undefined, @@ -214,7 +215,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` }, "importCount": 0, "importMode": Object { - "createNewCopies": false, + "createNewCopies": true, "overwrite": true, }, "indexPatterns": Array [ @@ -525,6 +526,159 @@ Array [ ] `; +exports[`Flyout should render cluster selector and import options 1`] = ` + + + +

+ +

+
+
+ + + + } + labelType="label" + > + + } + onChange={[Function]} + /> + +
+ + + + Import options + + , + } + } + > + + + + + + +
+
+
+ + + + + + + + + + + + + + +
+`; + exports[`Flyout should render import step 1`] = ` @@ -588,11 +742,12 @@ exports[`Flyout should render import step 1`] = ` diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 29f235d9fcfd..19d3c8424a79 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -40,7 +40,7 @@ import { import React from 'react'; import { shallowWithI18nProvider } from 'test_utils/enzyme_helpers'; -import { coreMock } from '../../../../../../core/public/mocks'; +import { coreMock, notificationServiceMock } from '../../../../../../core/public/mocks'; import { serviceRegistryMock } from '../../../services/service_registry.mock'; import { Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; @@ -99,6 +99,21 @@ describe('Flyout', () => { expect(component).toMatchSnapshot(); }); + it('should render cluster selector and import options', async () => { + const component = shallowRender({ + ...defaultProps, + dataSourceEnabled: true, + notifications: notificationServiceMock.createStartContract(), + }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + expect(component).toMatchSnapshot(); + }); + it('should allow picking a file', async () => { const component = shallowRender(defaultProps); @@ -192,10 +207,15 @@ describe('Flyout', () => { component.setState({ file: mockFile, isLegacyFile: false }); await component.instance().import(); - expect(importFileMock).toHaveBeenCalledWith(defaultProps.http, mockFile, { - createNewCopies: false, - overwrite: true, - }); + expect(importFileMock).toHaveBeenCalledWith( + defaultProps.http, + mockFile, + { + createNewCopies: true, + overwrite: true, + }, + undefined + ); expect(component.state()).toMatchObject({ conflictedIndexPatterns: undefined, conflictedSavedObjectsLinkedToSavedSearches: undefined, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index ad082513b277..82b763d18db1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -50,10 +50,12 @@ import { EuiCallOut, EuiSpacer, EuiLink, + EuiFormFieldset, } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; import { OverlayStart, HttpStart } from 'src/core/public'; +import { ClusterSelector } from '../../../../../data_source_management/public'; import { IndexPatternsContract, IIndexPattern, @@ -78,8 +80,7 @@ import { FailedImportConflict, RetryDecision } from '../../../lib/resolve_import import { OverwriteModal } from './overwrite_modal'; import { ImportModeControl, ImportMode } from './import_mode_control'; import { ImportSummary } from './import_summary'; - -const CREATE_NEW_COPIES_DEFAULT = false; +const CREATE_NEW_COPIES_DEFAULT = true; const OVERWRITE_ALL_DEFAULT = true; export interface FlyoutProps { @@ -92,6 +93,9 @@ export interface FlyoutProps { overlays: OverlayStart; http: HttpStart; search: DataPublicPluginStart['search']; + dataSourceEnabled: boolean; + savedObjects: SavedObjectsClientContract; + notifications: NotificationsStart; } export interface FlyoutState { @@ -110,6 +114,7 @@ export interface FlyoutState { loadingMessage?: string; isLegacyFile: boolean; status: string; + selectedDataSourceId: string; } interface ConflictingRecord { @@ -184,12 +189,12 @@ export class Flyout extends Component { */ import = async () => { const { http } = this.props; - const { file, importMode } = this.state; + const { file, importMode, selectedDataSourceId } = this.state; this.setState({ status: 'loading', error: undefined }); // Import the file try { - const response = await importFile(http, file!, importMode); + const response = await importFile(http, file!, importMode, selectedDataSourceId); this.setState(processImportResponse(response), () => { // Resolve import errors right away if there's no index patterns to match // This will ask about overwriting each object, etc @@ -251,6 +256,7 @@ export class Flyout extends Component { http: this.props.http, state: this.state, getConflictResolutions: this.getConflictResolutions, + selectedDataSourceId: this.state.selectedDataSourceId, }); this.setState(updatedState); } catch (e) { @@ -618,6 +624,8 @@ export class Flyout extends Component { importMode, } = this.state; + const { dataSourceEnabled } = this.props; + if (status === 'loading') { return ( @@ -749,7 +757,7 @@ export class Flyout extends Component { label={ } > @@ -765,14 +773,70 @@ export class Flyout extends Component { onChange={this.setImportFile} /> + + {this.renderImportControl(importMode, isLegacyFile, dataSourceEnabled)} + + ); + } + + onSelectedDataSourceChange = (e) => { + const dataSourceId = e[0] ? e[0].id : undefined; + this.setState({ selectedDataSourceId: dataSourceId }); + }; + + renderImportControl(importMode: ImportMode, isLegacyFile: boolean, dataSourceEnabled: boolean) { + if (dataSourceEnabled) { + return this.renderImportControlForDataSource(importMode, isLegacyFile); + } + return ( + + this.changeImportMode(newValues)} + optionLabel={i18n.translate( + 'savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle', + { defaultMessage: 'Import options' } + )} + /> + + ); + } + + renderImportControlForDataSource(importMode: ImportMode, isLegacyFile: boolean) { + return ( +
+ + + Import options + + ), + }} + > + + + this.changeImportMode(newValues)} + optionLabel={i18n.translate( + 'savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle', + { defaultMessage: 'Conflict management' } + )} /> - +
); } diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx index beabcfbb6308..d0c800553996 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx @@ -46,6 +46,7 @@ export interface ImportModeControlProps { initialValues: ImportMode; isLegacyFile: boolean; updateSelection: (result: ImportMode) => void; + optionLabel: string; } export interface ImportMode { @@ -70,7 +71,7 @@ const createNewCopiesEnabled = { id: 'createNewCopiesEnabled', text: i18n.translate( 'savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledTitle', - { defaultMessage: 'Create new objects with random IDs' } + { defaultMessage: 'Create new objects with unique IDs' } ), tooltip: i18n.translate( 'savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledText', @@ -93,10 +94,6 @@ const overwriteDisabled = { { defaultMessage: 'Request action on conflict' } ), }; -const importOptionsTitle = i18n.translate( - 'savedObjectsManagement.objectsTable.importModeControl.importOptionsTitle', - { defaultMessage: 'Import options' } -); const createLabel = ({ text, tooltip }: { text: string; tooltip: string }) => ( @@ -109,10 +106,23 @@ const createLabel = ({ text, tooltip }: { text: string; tooltip: string }) => ( ); +const overwriteRadio = (disabled: boolean, overwrite: boolean, onChange) => { + return ( + onChange({ overwrite: id === overwriteEnabled.id })} + disabled={disabled} + data-test-subj={'savedObjectsManagement-importModeControl-overwriteRadioGroup'} + /> + ); +}; + export const ImportModeControl = ({ initialValues, isLegacyFile, updateSelection, + optionLabel, }: ImportModeControlProps) => { const [createNewCopies, setCreateNewCopies] = useState(initialValues.createNewCopies); const [overwrite, setOverwrite] = useState(initialValues.overwrite); @@ -126,18 +136,8 @@ export const ImportModeControl = ({ updateSelection({ createNewCopies, overwrite, ...partial }); }; - const overwriteRadio = ( - onChange({ overwrite: id === overwriteEnabled.id })} - disabled={createNewCopies} - data-test-subj={'savedObjectsManagement-importModeControl-overwriteRadioGroup'} - /> - ); - if (isLegacyFile) { - return overwriteRadio; + return overwriteRadio(false, overwrite, onChange); } return ( @@ -145,28 +145,30 @@ export const ImportModeControl = ({ legend={{ children: ( - {importOptionsTitle} + {optionLabel} ), }} > + onChange({ createNewCopies: true })} + data-test-subj={'savedObjectsManagement-importModeControl-createNewCopiesEnabled'} + /> + + + onChange({ createNewCopies: false })} + data-test-subj={'savedObjectsManagement-importModeControl-createNewCopiesDisabled'} > - {overwriteRadio} + {overwriteRadio(createNewCopies, overwrite, onChange)} - - - - onChange({ createNewCopies: true })} - /> ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index 2f78f307d165..8e2f2a18f8f4 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -114,6 +114,7 @@ export interface SavedObjectsTableProps { goInspectObject: (obj: SavedObjectWithMetadata) => void; canGoInApp: (obj: SavedObjectWithMetadata) => boolean; dateFormat: string; + dataSourceEnabled: boolean; } export interface SavedObjectsTableState { @@ -558,6 +559,9 @@ export class SavedObjectsTable extends Component ); } diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index 09937388ba57..1776f3e7bfd9 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -49,6 +49,7 @@ const SavedObjectsTablePage = ({ columnRegistry, namespaceRegistry, setBreadcrumbs, + dataSourceEnabled, }: { coreStart: CoreStart; dataStart: DataPublicPluginStart; @@ -58,6 +59,7 @@ const SavedObjectsTablePage = ({ columnRegistry: SavedObjectsManagementColumnServiceStart; namespaceRegistry: SavedObjectsManagementNamespaceServiceStart; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; + dataSourceEnabled: boolean; }) => { const capabilities = coreStart.application.capabilities; const itemsPerPage = coreStart.uiSettings.get('savedObjects:perPage', 50); @@ -102,6 +104,7 @@ const SavedObjectsTablePage = ({ const { inAppUrl } = savedObject.meta; return inAppUrl ? Boolean(get(capabilities, inAppUrl.uiCapabilitiesPath)) : false; }} + dataSourceEnabled={dataSourceEnabled} /> ); }; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 14beb73386a8..690dee9c21e4 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -31,6 +31,7 @@ import { i18n } from '@osd/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { DataSourcePluginSetup } from 'src/plugins/data_source/public'; import { VisBuilderStart } from '../../vis_builder/public'; import { ManagementSetup } from '../../management/public'; import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; @@ -73,6 +74,7 @@ export interface SetupDependencies { management: ManagementSetup; home?: HomePublicPluginSetup; uiActions: UiActionsSetup; + dataSource?: DataSourcePluginSetup; } export interface StartDependencies { @@ -100,7 +102,7 @@ export class SavedObjectsManagementPlugin public setup( core: CoreSetup, - { home, management, uiActions }: SetupDependencies + { home, management, uiActions, dataSource }: SetupDependencies ): SavedObjectsManagementPluginSetup { const actionSetup = this.actionService.setup(); const columnSetup = this.columnService.setup(); @@ -136,6 +138,7 @@ export class SavedObjectsManagementPlugin core, serviceRegistry: this.serviceRegistry, mountParams, + dataSourceEnabled: !!dataSource, }); }, }); diff --git a/test/functional/apps/dashboard/time_zones.js b/test/functional/apps/dashboard/time_zones.js index 13a424bd7ea6..4c82cfe8006c 100644 --- a/test/functional/apps/dashboard/time_zones.js +++ b/test/functional/apps/dashboard/time_zones.js @@ -54,7 +54,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.navigateTo(); await PageObjects.settings.clickOpenSearchDashboardsSavedObjects(); await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'timezonetest_6_2_4.json') + path.join(__dirname, 'exports', 'timezonetest_6_2_4.json'), + true, + true ); await PageObjects.savedObjects.checkImportSucceeded(); await PageObjects.common.navigateToApp('dashboard'); diff --git a/test/functional/apps/management/_import_objects.js b/test/functional/apps/management/_import_objects.js index 2c432964f309..a4a919aedcd9 100644 --- a/test/functional/apps/management/_import_objects.js +++ b/test/functional/apps/management/_import_objects.js @@ -226,7 +226,9 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects', async function () { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects.json') + path.join(__dirname, 'exports', '_import_objects.json'), + true, + true ); await PageObjects.savedObjects.checkImportSucceeded(); await PageObjects.savedObjects.clickImportDone(); @@ -237,7 +239,9 @@ export default function ({ getService, getPageObjects }) { it('should provide dialog to allow the importing of saved objects with index pattern conflicts', async function () { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects-conflicts.json') + path.join(__dirname, 'exports', '_import_objects-conflicts.json'), + true, + true ); await PageObjects.savedObjects.checkImportLegacyWarning(); await PageObjects.savedObjects.checkImportConflictsWarning(); @@ -258,7 +262,8 @@ export default function ({ getService, getPageObjects }) { // so that we can override the existing visualization. await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_exists.json'), - false + false, + true ); await PageObjects.savedObjects.checkImportLegacyWarning(); @@ -278,7 +283,8 @@ export default function ({ getService, getPageObjects }) { // so that we can be prompted to override the existing visualization. await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_exists.json'), - false + false, + true ); await PageObjects.savedObjects.checkImportLegacyWarning(); @@ -298,7 +304,8 @@ export default function ({ getService, getPageObjects }) { // so that we can override the existing visualization. await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_objects_multiple_exists.json'), - false + false, + true ); await PageObjects.savedObjects.checkImportLegacyWarning(); @@ -325,7 +332,8 @@ export default function ({ getService, getPageObjects }) { // so that we can override the existing visualization. await PageObjects.savedObjects.importFile( path.join(__dirname, 'exports', '_import_index_patterns_multiple_exists.json'), - false + false, + true ); // Override the index patterns. @@ -343,13 +351,17 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects linked to saved searches', async function () { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_saved_search.json') + path.join(__dirname, 'exports', '_import_objects_saved_search.json'), + true, + true ); await PageObjects.savedObjects.checkImportSucceeded(); await PageObjects.savedObjects.clickImportDone(); await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') + path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'), + true, + true ); await PageObjects.savedObjects.checkImportSucceeded(); await PageObjects.savedObjects.clickImportDone(); @@ -361,7 +373,9 @@ export default function ({ getService, getPageObjects }) { it('should not import saved objects linked to saved searches when saved search does not exist', async function () { await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') + path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'), + true, + true ); await PageObjects.savedObjects.checkImportFailedWarning(); await PageObjects.savedObjects.clickImportDone(); @@ -374,7 +388,9 @@ export default function ({ getService, getPageObjects }) { it('should not import saved objects linked to saved searches when saved search index pattern does not exist', async function () { // First, import the saved search await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_saved_search.json') + path.join(__dirname, 'exports', '_import_objects_saved_search.json'), + true, + true ); // Wait for all the saves to happen await PageObjects.savedObjects.checkImportSucceeded(); @@ -387,7 +403,9 @@ export default function ({ getService, getPageObjects }) { // Last, import a saved object connected to the saved search // This should NOT show the conflicts await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json') + path.join(__dirname, 'exports', '_import_objects_connected_to_saved_search.json'), + true, + true ); // Wait for all the saves to happen await PageObjects.savedObjects.checkNoneImported(); @@ -401,7 +419,9 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects with index patterns when index patterns already exists', async () => { // First, import the objects await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') + path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'), + true, + true ); await PageObjects.savedObjects.clickImportDone(); @@ -417,7 +437,9 @@ export default function ({ getService, getPageObjects }) { // Then, import the objects await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json') + path.join(__dirname, 'exports', '_import_objects_with_index_patterns.json'), + true, + true ); await PageObjects.savedObjects.checkImportSucceeded(); await PageObjects.savedObjects.clickImportDone(); diff --git a/test/functional/apps/management/_mgmt_import_saved_objects.js b/test/functional/apps/management/_mgmt_import_saved_objects.js index 631b4e85cb8b..c5f852bae5c0 100644 --- a/test/functional/apps/management/_mgmt_import_saved_objects.js +++ b/test/functional/apps/management/_mgmt_import_saved_objects.js @@ -53,7 +53,9 @@ export default function ({ getService, getPageObjects }) { it('should import saved objects mgmt', async function () { await PageObjects.settings.clickOpenSearchDashboardsSavedObjects(); await PageObjects.savedObjects.importFile( - path.join(__dirname, 'exports', 'mgmt_import_objects.json') + path.join(__dirname, 'exports', 'mgmt_import_objects.json'), + true, + true ); await PageObjects.settings.associateIndexPattern( '4c3f3c30-ac94-11e8-a651-614b2788174a', diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts index 3a6a987b3b68..d0014065e216 100644 --- a/test/functional/page_objects/management/saved_objects_page.ts +++ b/test/functional/page_objects/management/saved_objects_page.ts @@ -50,15 +50,22 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv await this.waitTableIsLoaded(); } - async importFile(path: string, overwriteAll = true) { + async importFile(path: string, overwriteAll = true, isLegacy = false) { log.debug(`importFile(${path})`); log.debug(`Clicking importObjects`); await testSubjects.click('importObjects'); await PageObjects.common.setFileInputPath(path); + if (!isLegacy) { + await testSubjects.click( + 'savedObjectsManagement-importModeControl-createNewCopiesDisabled' + ); + } + if (!overwriteAll) { log.debug(`Toggling overwriteAll`); + const radio = await testSubjects.find( 'savedObjectsManagement-importModeControl-overwriteRadioGroup' );