From f7c18b4f7ded323cf29e7188d4c3de9d688ee8f2 Mon Sep 17 00:00:00 2001 From: yubonluo Date: Fri, 22 Mar 2024 15:32:16 +0800 Subject: [PATCH] refactor dupicate_model code and fix test error --- .../header/__snapshots__/header.test.tsx.snap | 1317 +---------------- src/core/server/saved_objects/routes/index.ts | 2 +- .../routes/integration_tests/copy.test.ts | 4 +- .../__snapshots__/table.test.tsx.snap | 4 +- .../components/duplicate_modal.tsx | 168 +-- .../objects_table/components/header.test.tsx | 26 - .../objects_table/components/table.tsx | 2 +- .../objects_table/components/utils.ts | 40 + .../objects_table/saved_objects_table.tsx | 20 +- .../workspace/public/workspace_client.ts | 3 +- 10 files changed, 148 insertions(+), 1438 deletions(-) create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/utils.ts diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 00a13549f828..8d244a212d1f 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -1928,169 +1928,6 @@ exports[`Header handles visibility and lock changes 1`] = ` } } survey="/" - workspaces={ - Object { - "currentWorkspace$": BehaviorSubject { - "_isScalar": false, - "_value": null, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, - "currentWorkspaceId$": BehaviorSubject { - "_isScalar": false, - "_value": "", - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - ], - "thrownError": null, - }, - "hasFetchedWorkspaceList$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, - "renderWorkspaceMenu": [MockFunction], - "workspaceEnabled$": BehaviorSubject { - "_isScalar": false, - "_value": false, - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [ - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - Subscriber { - "_parentOrParents": null, - "_subscriptions": Array [ - SubjectSubscription { - "_parentOrParents": [Circular], - "_subscriptions": null, - "closed": false, - "subject": [Circular], - "subscriber": [Circular], - }, - ], - "closed": false, - "destination": SafeSubscriber { - "_complete": undefined, - "_context": [Circular], - "_error": undefined, - "_next": [Function], - "_parentOrParents": null, - "_parentSubscriber": [Circular], - "_subscriptions": null, - "closed": false, - "destination": Object { - "closed": true, - "complete": [Function], - "error": [Function], - "next": [Function], - }, - "isStopped": false, - "syncErrorThrowable": false, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - "isStopped": false, - "syncErrorThrowable": true, - "syncErrorThrown": false, - "syncErrorValue": null, - }, - ], - "thrownError": null, - }, - "workspaceList$": BehaviorSubject { - "_isScalar": false, - "_value": Array [], - "closed": false, - "hasError": false, - "isStopped": false, - "observers": Array [], - "thrownError": null, - }, - } - } >
- - - - - - - -
-
- -
- - -
-
- -
- -
- - - -
-
- -
- -
- - - OpenSearch Dashboards - - -
-
-
-
-
-
-
-
-
-
- - - - - - - -

- Recently Visited -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" - data-test-opensearch-logo="clock" - data-test-subj="collapsibleNavGroup-recentlyVisited" - id="mockId" - initialIsOpen={true} - isLoading={false} - isLoadingMessage={false} - onToggle={[Function]} - paddingSize="none" - > -
-
- -
-
- - - -
-
-
-
+
+ + + + + + +
+
+ +
{ const result = await supertest(httpSetup.server.listener).post(URL).send({}).expect(400); expect(result.body.message).toMatchInlineSnapshot( - `"[request body.objects]: expected value of type [array] but got [undefined]"` + `"[request body.targetWorkspace]: expected value of type [string] but got [undefined]"` ); }); @@ -157,7 +157,7 @@ describe(`POST ${URL}`, () => { .expect(400); expect(result.body.message).toMatchInlineSnapshot( - `"Trying to copy object(s) with unsupported types: unknown:my-pattern"` + `"Trying to export object(s) with non-exportable types: unknown:my-pattern"` ); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index 3845f4bf450b..e22f7f3a0128 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -167,7 +167,7 @@ exports[`Table prevents saved objects from being deleted 1`] = ` "data-test-subj": "savedObjectsTableAction-relationships", "description": "View the relationships this saved object has to other saved objects", "icon": "kqlSelector", - "name": "View object relationships", + "name": "Relationships", "onClick": [Function], "type": "icon", }, @@ -392,7 +392,7 @@ exports[`Table should render normally 1`] = ` "data-test-subj": "savedObjectsTableAction-relationships", "description": "View the relationships this saved object has to other saved objects", "icon": "kqlSelector", - "name": "View object relationships", + "name": "Relationships", "onClick": [Function], "type": "icon", }, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx index 5966bb75b78f..dec18dbffb13 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx @@ -1,12 +1,6 @@ /* + * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Any modifications Copyright OpenSearch Contributors. See - * GitHub history for details. */ import React from 'react'; @@ -24,24 +18,23 @@ import { EuiComboBox, EuiFormRow, EuiCheckbox, - EuiComboBoxOptionOption, EuiInMemoryTable, EuiToolTip, EuiIcon, EuiCallOut, EuiText, + EuiTextColor, } from '@elastic/eui'; -import { - HttpSetup, - NotificationsStart, - WorkspaceAttribute, - WorkspacesStart, -} from 'opensearch-dashboards/public'; +import { HttpSetup, NotificationsStart, WorkspacesStart } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { SavedObjectWithMetadata } from '../../../../common'; import { getSavedObjectLabel } from '../../../../public'; - -type WorkspaceOption = EuiComboBoxOptionOption; +import { + WorkspaceOption, + capitalizeFirstLetter, + getTargetWorkspacesOptions, + workspaceToOption, +} from './utils'; export enum DuplicateMode { Selected = 'selected', @@ -67,82 +60,44 @@ interface Props extends ShowDuplicateModalProps { interface State { allSelectedObjects: SavedObjectWithMetadata[]; workspaceOptions: WorkspaceOption[]; - allWorkspaceOptions: WorkspaceOption[]; targetWorkspaceOption: WorkspaceOption[]; isLoading: boolean; isIncludeReferencesDeepChecked: boolean; savedObjectTypeInfoMap: Map; } -function capitalizeFirstLetter(str: string): string { - return str.charAt(0).toUpperCase() + str.slice(1); -} - export class SavedObjectsDuplicateModal extends React.Component { private isMounted = false; constructor(props: Props) { super(props); - this.state = { - allSelectedObjects: this.props.selectedSavedObjects, - workspaceOptions: [], - allWorkspaceOptions: [], - targetWorkspaceOption: [], - isLoading: false, - isIncludeReferencesDeepChecked: true, - savedObjectTypeInfoMap: new Map(), - }; - } - - workspaceToOption = ( - workspace: WorkspaceAttribute, - currentWorkspaceName?: string - ): WorkspaceOption => { - // add (current) after current workspace name - let workspaceName = workspace.name; - if (workspace.name === currentWorkspaceName) { - workspaceName += ' (current)'; - } - return { - label: workspaceName, - key: workspace.id, - value: workspace, - }; - }; - - async componentDidMount() { - const { workspaces } = this.props; + const { workspaces, duplicateMode } = props; const currentWorkspace = workspaces.currentWorkspace$.value; - const currentWorkspaceName = currentWorkspace?.name; - const targetWorkspaces = this.getTargetWorkspaces(); - - // current workspace is the first option - const workspaceOptions = [ - ...(currentWorkspace ? [this.workspaceToOption(currentWorkspace, currentWorkspaceName)] : []), - ...targetWorkspaces - .filter((workspace: WorkspaceAttribute) => workspace.name !== currentWorkspaceName) - .map((workspace: WorkspaceAttribute) => - this.workspaceToOption(workspace, currentWorkspaceName) - ), - ]; + const currentWorkspaceId = currentWorkspace?.id; + const targetWorkspacesOptions = getTargetWorkspacesOptions(workspaces, currentWorkspaceId); - this.setState({ - workspaceOptions, - allWorkspaceOptions: workspaceOptions, - }); - - const { duplicateMode } = this.props; + // If user click 'Duplicate All' button, saved objects will be categoried by type. + const savedObjectTypeInfoMap = new Map(); if (duplicateMode === DuplicateMode.All) { - const { allSelectedObjects } = this.state; - const categorizedObjects = groupBy(allSelectedObjects, (object) => object.type); - const savedObjectTypeInfoMap = new Map(); + const categorizedObjects = groupBy(props.selectedSavedObjects, (object) => object.type); for (const [savedObjectType, savedObjects] of Object.entries(categorizedObjects)) { savedObjectTypeInfoMap.set(savedObjectType, [savedObjects.length, true]); } - this.setState({ savedObjectTypeInfoMap }); } + this.state = { + allSelectedObjects: props.selectedSavedObjects, + // current workspace is the first option + workspaceOptions: [ + ...(currentWorkspace ? [workspaceToOption(currentWorkspace, currentWorkspaceId)] : []), + ...targetWorkspacesOptions, + ], + targetWorkspaceOption: [], + isLoading: false, + isIncludeReferencesDeepChecked: true, + savedObjectTypeInfoMap, + }; this.isMounted = true; } @@ -150,12 +105,6 @@ export class SavedObjectsDuplicateModal extends React.Component { this.isMounted = false; } - getTargetWorkspaces = () => { - const { workspaces } = this.props; - const workspaceList = workspaces.workspaceList$.value; - return workspaceList.filter((workspace) => !workspace.libraryReadonly); - }; - duplicateSavedObjects = async (savedObjects: SavedObjectWithMetadata[]) => { this.setState({ isLoading: true, @@ -176,14 +125,6 @@ export class SavedObjectsDuplicateModal extends React.Component { } }; - onSearchWorkspaceChange = (searchValue: string) => { - this.setState({ - workspaceOptions: this.state.allWorkspaceOptions.filter((item) => - item.label.includes(searchValue) - ), - }); - }; - onTargetWorkspaceChange = (targetWorkspaceOption: WorkspaceOption[]) => { this.setState({ targetWorkspaceOption, @@ -196,6 +137,7 @@ export class SavedObjectsDuplicateModal extends React.Component { })); }; + // Choose whether to copy a certain type or not. changeIncludeSavedObjectType = (savedObjectType: string) => { const { savedObjectTypeInfoMap } = this.state; const savedObjectTypeInfo = savedObjectTypeInfoMap.get(savedObjectType); @@ -206,6 +148,7 @@ export class SavedObjectsDuplicateModal extends React.Component { } }; + // A checkbox showing the type and count of save objects. renderDuplicateObjectCategory = ( savedObjectType: string, savedObjectTypeCount: number, @@ -267,6 +210,8 @@ export class SavedObjectsDuplicateModal extends React.Component { if (duplicateMode === DuplicateMode.All) { selectedObjects = selectedObjects.filter((item) => this.isSavedObjectTypeIncluded(item.type)); } + // If the target workspace is selected, all saved objects will be retained. + // If the target workspace has been selected, filter out the saved objects that belongs to the workspace. const includedSelectedObjects = selectedObjects.filter((item) => !!targetWorkspaceId && !!item.workspaces ? !item.workspaces.includes(targetWorkspaceId) : true ); @@ -279,19 +224,21 @@ export class SavedObjectsDuplicateModal extends React.Component { } const warningMessageForOnlyOneSavedObject = ( -

- 1 saved object will not be - copied, because it has already existed in the selected workspace or it is worksapce itself. -

+ + 1 saved object will{' '} + not be copied, because it has already existed in + the selected workspace or it is worksapce itself. + ); const warningMessageForMultipleSavedObjects = ( -

- {ignoredSelectedObjectsLength} saved objects will{' '} - not be copied, because they have already existed in the - selected workspace or they are workspaces themselves. -

+ + {ignoredSelectedObjectsLength} saved objects + will not be copied, because they have already + existed in selected workspace or they are workspaces themselves. + ); + // Show the number and reason why some saved objects cannot be duplicated. const ignoreSomeObjectsChildren: React.ReactChild = ( <> { - } + label={i18n.translate( + 'savedObjectsManagement.objectsTable.duplicateModal.targetWorkspacelabel', + { defaultMessage: 'Destination workspace' } + )} > <> @@ -348,7 +293,6 @@ export class SavedObjectsDuplicateModal extends React.Component { onChange={this.onTargetWorkspaceChange} selectedOptions={targetWorkspaceOption} singleSelection={{ asPlainText: true }} - onSearchChange={this.onSearchWorkspaceChange} isClearable={false} isInvalid={!confirmDuplicateButtonEnabled} /> @@ -361,12 +305,10 @@ export class SavedObjectsDuplicateModal extends React.Component { - } + label={i18n.translate( + 'savedObjectsManagement.objectsTable.duplicateModal.relatedObjects', + { defaultMessage: 'Related Objects' } + )} > <> @@ -377,12 +319,10 @@ export class SavedObjectsDuplicateModal extends React.Component { - } + label={i18n.translate( + 'savedObjectsManagement.objectsTable.duplicateModal.includeReferencesDeepLabel', + { defaultMessage: 'Duplicate related objects' } + )} checked={isIncludeReferencesDeepChecked} onChange={this.changeIncludeReferencesDeep} /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx index d98fe7257fbd..1fd730973181 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx @@ -38,14 +38,11 @@ describe('Header', () => { onExportAll: () => {}, onImport: () => {}, onRefresh: () => {}, - onCopy: () => {}, onDuplicate: () => {}, title: 'Saved Objects', - selectedCount: 0, objectCount: 4, filteredCount: 2, showDuplicateAll: false, - hideImport: false, }; const component = shallow(
); @@ -60,38 +57,15 @@ describe('Header - workspace enabled', () => { onExportAll: () => {}, onImport: () => {}, onRefresh: () => {}, - onCopy: () => {}, onDuplicate: () => {}, title: 'Saved Objects', - selectedCount: 0, objectCount: 4, filteredCount: 2, showDuplicateAll: true, - hideImport: false, }; const component = shallow(
); expect(component.find('EuiButtonEmpty[data-test-subj="duplicateObjects"]').exists()).toBe(true); }); - - it('should hide `Import` button for application home state', () => { - const props = { - onExportAll: () => {}, - onImport: () => {}, - onRefresh: () => {}, - onCopy: () => {}, - onDuplicate: () => {}, - title: 'Saved Objects', - selectedCount: 0, - objectCount: 4, - filteredCount: 2, - showDuplicateAll: true, - hideImport: true, - }; - - const component = shallow(
); - - expect(component.find('EuiButtonEmpty[data-test-subj="importObjects"]').exists()).toBe(false); - }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index a6629084d053..0a9f6e510568 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -303,7 +303,7 @@ export class Table extends PureComponent { { name: i18n.translate( 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName', - { defaultMessage: 'View object relationships' } + { defaultMessage: 'Relationships' } ), description: i18n.translate( 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription', diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/utils.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/utils.ts new file mode 100644 index 000000000000..8376aa4f00b9 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/utils.ts @@ -0,0 +1,40 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { WorkspaceAttribute, WorkspacesStart } from 'opensearch-dashboards/public'; + +export type WorkspaceOption = EuiComboBoxOptionOption; + +export function workspaceToOption( + workspace: WorkspaceAttribute, + currentWorkspaceId?: string +): WorkspaceOption { + // add (current) after current workspace name + let workspaceName = workspace.name; + if (workspace.id === currentWorkspaceId) { + workspaceName += ' (current)'; + } + return { + label: workspaceName, + key: workspace.id, + value: workspace, + }; +} + +export function getTargetWorkspacesOptions( + workspaces: WorkspacesStart, + currentWorkspaceId?: string +): WorkspaceOption[] { + const workspaceList = workspaces.workspaceList$.value; + const targetWorkspaces = workspaceList.filter( + (workspace) => workspace.id !== currentWorkspaceId && !workspace.libraryReadonly + ); + return targetWorkspaces.map((workspace) => workspaceToOption(workspace, currentWorkspaceId)); +} + +export function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} 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 9ac3044aff73..6ec95815716a 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 @@ -719,27 +719,17 @@ export class SavedObjectsTable extends Component item.id); - notifications.toasts.addDanger({ - title: i18n.translate( - 'savedObjectsManagement.objectsTable.duplicate.dangerNotification', - { - defaultMessage: - 'Unable to duplicate ' + - savedObjects.length.toString() + - ' saved objects. These objects cannot be duplicated:' + - errorsIds.join(','), - } - ), - }); } else { + const errorIdMessages = result.errors + ? 'These objects cannot be duplicated:' + + result.errors.map((item: { id: string }) => item.id).join(',') + : ''; notifications.toasts.addDanger({ title: i18n.translate( 'savedObjectsManagement.objectsTable.duplicate.dangerNotification', { defaultMessage: - 'Unable to duplicate ' + savedObjects.length.toString() + ' saved objects', + 'Unable to duplicate ' + savedObjects.length.toString() + errorIdMessages, } ), }); diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts index a7b8df5c9bdc..400e102f264a 100644 --- a/src/plugins/workspace/public/workspace_client.ts +++ b/src/plugins/workspace/public/workspace_client.ts @@ -121,8 +121,7 @@ export class WorkspaceClient { const workspaceIdsWithWritePermission = resultWithWritePermission.result.workspaces.map( (workspace: WorkspaceAttribute) => workspace.id ); - let workspaces = result.result.workspaces; - workspaces = result.result.workspaces.map((workspace: WorkspaceAttribute) => ({ + const workspaces = result.result.workspaces.map((workspace: WorkspaceAttribute) => ({ ...workspace, libraryReadonly: !workspaceIdsWithWritePermission.includes(workspace.id), }));