From b33221afefb43ca5856fc8984f27b1f10adf714e Mon Sep 17 00:00:00 2001 From: "opensearch-workspace-development[bot]" <144193788+opensearch-workspace-development[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:06:50 +0800 Subject: [PATCH] feat: duplicate all and single objects (#121) (#147) * implement all duplicate copy modal * add spacer after checkbox list * add fail message for copy saved objects * change title wording to manage library * single duplicate * change wording * remove comment * bug fix: keep selected saved objects info when cancel duplicate all * fix typo * use icu syntax in copy message * bug fix: keep selected saved objects info when cancel duplicate single * set current workspace as the first option * update snapshot * resolve conflict * update snapshot * bug fix for saved object table * update snapshot * remove unused file * change i18n constant * remove empty push * hide duplicate when workspace is disabled * update snapshots --------- (cherry picked from commit 62317683c78c801277e0a0ce1f46d188f75a06d5) Signed-off-by: yuye-aws Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- .../public/constants.ts | 6 +- .../saved_objects_table.test.tsx.snap | 5 +- .../__snapshots__/table.test.tsx.snap | 4 +- .../objects_table/components/copy_modal.tsx | 156 ++++++++++++++---- .../objects_table/components/header.tsx | 6 +- .../objects_table/components/table.tsx | 31 +++- .../management_section/objects_table/index.ts | 2 +- .../objects_table/saved_objects_table.tsx | 73 ++++++-- .../saved_objects_management/public/plugin.ts | 7 +- 9 files changed, 226 insertions(+), 64 deletions(-) diff --git a/src/plugins/saved_objects_management/public/constants.ts b/src/plugins/saved_objects_management/public/constants.ts index edb249187a2b..e33e782a7501 100644 --- a/src/plugins/saved_objects_management/public/constants.ts +++ b/src/plugins/saved_objects_management/public/constants.ts @@ -5,10 +5,10 @@ import { i18n } from '@osd/i18n'; -export const ALL_LIBRARY_OBJECTS_WORDINGS = i18n.translate( - 'savedObjectsManagement.allLibraryObjects', +export const MANAGE_LIBRARY_TITLE_WORDINGS = i18n.translate( + 'savedObjectsManagement.manageLibrary', { - defaultMessage: 'All library objects', + defaultMessage: 'Manage library', } ); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 47680741209e..131ea78797b7 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -210,11 +210,11 @@ exports[`SavedObjectsTable should render normally 1`] = `
; @@ -47,6 +48,7 @@ interface Props { targetWorkspace: string ) => Promise; onClose: () => void; + copyState: CopyState; getCopyWorkspaces: () => Promise; selectedSavedObjects: SavedObjectWithMetadata[]; } @@ -58,6 +60,11 @@ interface State { targetWorkspaceOption: WorkspaceOption[]; isLoading: boolean; isIncludeReferencesDeepChecked: boolean; + savedObjectTypeInfoMap: Map; +} + +function capitalizeFirstLetter(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); } export class SavedObjectsCopyModal extends React.Component { @@ -73,33 +80,56 @@ export class SavedObjectsCopyModal extends React.Component { targetWorkspaceOption: [], isLoading: false, isIncludeReferencesDeepChecked: true, + savedObjectTypeInfoMap: new Map(), }; } - workspaceToOption = (workspace: WorkspaceAttribute): WorkspaceOption => { - return { label: workspace.name, key: workspace.id, value: workspace }; + 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, getCopyWorkspaces } = this.props; const workspaceList = await getCopyWorkspaces(); - const currentWorkspace = workspaces.currentWorkspace$; + const currentWorkspace = workspaces.currentWorkspace$.value; + const currentWorkspaceName = currentWorkspace?.name; - if (!!currentWorkspace?.value?.name) { - const currentWorkspaceName = currentWorkspace.value.name; - const filteredWorkspaceOptions = workspaceList - .map(this.workspaceToOption) - .filter((item: WorkspaceOption) => item.label !== currentWorkspaceName); - this.setState({ - workspaceOptions: filteredWorkspaceOptions, - allWorkspaceOptions: filteredWorkspaceOptions, - }); - } else { - const allWorkspaceOptions = workspaceList.map(this.workspaceToOption); - this.setState({ - workspaceOptions: allWorkspaceOptions, - allWorkspaceOptions, - }); + // current workspace is the first option + const workspaceOptions = [ + ...(currentWorkspace ? [this.workspaceToOption(currentWorkspace, currentWorkspaceName)] : []), + ...workspaceList + .filter((workspace: WorkspaceAttribute) => workspace.name !== currentWorkspaceName) + .map((workspace: WorkspaceAttribute) => + this.workspaceToOption(workspace, currentWorkspaceName) + ), + ]; + + this.setState({ + workspaceOptions, + allWorkspaceOptions: workspaceOptions, + }); + + const { copyState } = this.props; + if (copyState === CopyState.All) { + const { allSelectedObjects } = this.state; + const categorizedObjects = groupBy(allSelectedObjects, (object) => object.type); + const savedObjectTypeInfoMap = new Map(); + for (const [savedObjectType, savedObjects] of Object.entries(categorizedObjects)) { + savedObjectTypeInfoMap.set(savedObjectType, [savedObjects.length, true]); + } + this.setState({ savedObjectTypeInfoMap }); } this.isMounted = true; @@ -149,6 +179,61 @@ export class SavedObjectsCopyModal extends React.Component { })); }; + changeIncludeSavedObjectType = (savedObjectType: string) => { + const { savedObjectTypeInfoMap } = this.state; + const savedObjectTypeInfo = savedObjectTypeInfoMap.get(savedObjectType); + if (savedObjectTypeInfo) { + const [count, checked] = savedObjectTypeInfo; + savedObjectTypeInfoMap.set(savedObjectType, [count, !checked]); + this.setState({ savedObjectTypeInfoMap }); + } + }; + + renderCopyObjectCategory = ( + savedObjectType: string, + savedObjectTypeCount: number, + savedObjectTypeChecked: boolean + ) => { + return ( + + } + checked={savedObjectTypeChecked} + onChange={() => this.changeIncludeSavedObjectType(savedObjectType)} + /> + ); + }; + + renderCopyObjectCategories = () => { + const { savedObjectTypeInfoMap } = this.state; + const checkboxList: JSX.Element[] = []; + savedObjectTypeInfoMap.forEach( + ([savedObjectTypeCount, savedObjectTypeChecked], savedObjectType) => + checkboxList.push( + this.renderCopyObjectCategory( + savedObjectType, + savedObjectTypeCount, + savedObjectTypeChecked + ) + ) + ); + return checkboxList; + }; + + isSavedObjectTypeIncluded = (savedObjectType: string) => { + const { savedObjectTypeInfoMap } = this.state; + const savedObjectTypeInfo = savedObjectTypeInfoMap.get(savedObjectType); + return savedObjectTypeInfo && savedObjectTypeInfo[1]; + }; + render() { const { workspaceOptions, @@ -156,19 +241,31 @@ export class SavedObjectsCopyModal extends React.Component { isIncludeReferencesDeepChecked, allSelectedObjects, } = this.state; + const { copyState } = this.props; const targetWorkspaceId = targetWorkspaceOption?.at(0)?.key; - const includedSelectedObjects = allSelectedObjects.filter((item) => + let selectedObjects = allSelectedObjects; + if (copyState === CopyState.All) { + selectedObjects = selectedObjects.filter((item) => this.isSavedObjectTypeIncluded(item.type)); + } + const includedSelectedObjects = selectedObjects.filter((item) => !!targetWorkspaceId && !!item.workspaces ? !item.workspaces.includes(targetWorkspaceId) : item.type !== SAVED_OBJECT_TYPE_WORKSAPCE ); - const ignoredSelectedObjectsLength = allSelectedObjects.length - includedSelectedObjects.length; + + const ignoredSelectedObjectsLength = selectedObjects.length - includedSelectedObjects.length; let confirmCopyButtonEnabled = false; if (!!targetWorkspaceId && includedSelectedObjects.length > 0) { confirmCopyButtonEnabled = true; } + const confirmMessageForAllObjects = `Duplicate (${includedSelectedObjects.length})`; + const confirmMessageForSingleOrSelectedObjects = 'Duplicate'; + const confirmMessage = + copyState === CopyState.All + ? confirmMessageForAllObjects + : confirmMessageForSingleOrSelectedObjects; const warningMessageForOnlyOneSavedObject = (

1 saved object will not be @@ -179,7 +276,7 @@ export class SavedObjectsCopyModal extends React.Component {

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

); @@ -209,11 +306,12 @@ export class SavedObjectsCopyModal extends React.Component { 1 ? ' objects?' : ' object?') - } + defaultMessage="Duplicate {copyState, select, all {all objects} other {{objectCount, plural, =1 {{objectName}} other {# objects}}}}?" + values={{ + copyState, + objectName: allSelectedObjects[0].meta.title, + objectCount: allSelectedObjects.length, + }} /> @@ -246,6 +344,8 @@ export class SavedObjectsCopyModal extends React.Component { + {copyState && this.renderCopyObjectCategories()} + {copyState && } { > diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx index fa3d36c73c14..21682896d871 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx @@ -47,7 +47,7 @@ export const Header = ({ onRefresh, filteredCount, title, - selectedCount, + objectCount, hideImport = false, showDuplicateAll = false, }: { @@ -57,7 +57,7 @@ export const Header = ({ onRefresh: () => void; filteredCount: number; title: string; - selectedCount: number; + objectCount: number; hideImport: boolean; showDuplicateAll: boolean; }) => ( @@ -77,7 +77,7 @@ export const Header = ({ size="s" data-test-subj="copyObjects" onClick={onCopy} - disabled={selectedCount === 0} + disabled={objectCount === 0} > void; - onCopy: () => void; + onCopySelected: () => void; + onCopySingle: (object: SavedObjectWithMetadata) => void; onActionRefresh: (object: SavedObjectWithMetadata) => void; onExport: (includeReferencesDeep: boolean) => void; goInspectObject: (obj: SavedObjectWithMetadata) => void; @@ -79,7 +80,7 @@ export interface TableProps { items: SavedObjectWithMetadata[]; itemId: string | (() => string); totalItemCount: number; - onQueryChange: (query: any, filterFields: string[]) => void; + onQueryChange: (query: any, filterFields?: string[]) => void; onTableChange: (table: any) => void; isSearching: boolean; onShowRelationships: (object: SavedObjectWithMetadata) => void; @@ -171,7 +172,8 @@ export class Table extends PureComponent { filters, selectionConfig: selection, onDelete, - onCopy, + onCopySelected, + onCopySingle, onActionRefresh, selectedSavedObjects, onTableChange, @@ -296,7 +298,7 @@ export class Table extends PureComponent { { name: i18n.translate( 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionName', - { defaultMessage: 'Relationships' } + { defaultMessage: 'View object relationships' } ), description: i18n.translate( 'savedObjectsManagement.objectsTable.table.columnActions.viewRelationshipsActionDescription', @@ -310,6 +312,25 @@ export class Table extends PureComponent { onClick: (object) => onShowRelationships(object), 'data-test-subj': 'savedObjectsTableAction-relationships', }, + ...(showDuplicate + ? [ + { + name: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnActions.duplicateActionName', + { defaultMessage: 'Duplicate' } + ), + description: i18n.translate( + 'savedObjectsManagement.objectsTable.table.columnActions.duplicateActionDescription', + { defaultMessage: 'Duplicate this saved object' } + ), + type: 'icon', + icon: 'copyClipboard', + isPrimary: true, + onClick: (object: SavedObjectWithMetadata) => onCopySingle(object), + 'data-test-subj': 'savedObjectsTableAction-duplicate', + }, + ] + : []), ...actionRegistry.getAll().map((action) => { return { ...action.euiAction, @@ -428,7 +449,7 @@ export class Table extends PureComponent { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts index b2153648057f..75d036961186 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts @@ -28,4 +28,4 @@ * under the License. */ -export { SavedObjectsTable } from './saved_objects_table'; +export { SavedObjectsTable, CopyState } from './saved_objects_table'; 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 8f6ca7be6a5e..66cf041eaefb 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 @@ -99,6 +99,12 @@ import { DataPublicPluginStart } from '../../../../data/public'; import { SavedObjectsCopyModal } from './components/copy_modal'; import { PUBLIC_WORKSPACE_ID, MANAGEMENT_WORKSPACE_ID } from '../../../../../core/public'; +export enum CopyState { + Single = 'single', + Selected = 'selected', + All = 'all', +} + interface ExportAllOption { id: string; label: string; @@ -134,8 +140,10 @@ export interface SavedObjectsTableState { savedObjectCounts: Record>; activeQuery: Query; selectedSavedObjects: SavedObjectWithMetadata[]; + copySelectedSavedObjects: SavedObjectWithMetadata[]; isShowingImportFlyout: boolean; isShowingCopyModal: boolean; + copyState: CopyState; isSearching: boolean; filteredItemCount: number; isShowingRelationships: boolean; @@ -173,8 +181,10 @@ export class SavedObjectsTable extends Component>, activeQuery: Query.parse(''), selectedSavedObjects: [], + copySelectedSavedObjects: [], isShowingImportFlyout: false, isShowingCopyModal: false, + copyState: CopyState.Selected, isSearching: false, filteredItemCount: 0, isShowingRelationships: false, @@ -521,13 +531,28 @@ export class SavedObjectsTable extends Component { const { notifications, http } = this.props; const objectsToCopy = savedObjects.map((obj) => ({ id: obj.id, type: obj.type })); - + let result; try { - await copySavedObjects(http, objectsToCopy, includeReferencesDeep, targetWorkspace); + result = await copySavedObjects(http, objectsToCopy, includeReferencesDeep, targetWorkspace); + if (result.success) { + notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.copy.successNotification', { + defaultMessage: + 'Copy ' + savedObjects.length.toString() + ' saved objects successfully', + }), + }); + } else { + const failedCount = savedObjects.length - result.successCount; + notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.copy.dangerNotification', { + defaultMessage: 'Unable to copy ' + failedCount.toString() + ' saved objects', + }), + }); + } } catch (e) { notifications.toasts.addDanger({ title: i18n.translate('savedObjectsManagement.objectsTable.copy.dangerNotification', { - defaultMessage: 'Unable to copy saved objects', + defaultMessage: 'Unable to copy all saved objects', }), }); throw e; @@ -535,11 +560,6 @@ export class SavedObjectsTable extends Component { @@ -726,7 +746,8 @@ export class SavedObjectsTable extends Component @@ -1074,13 +1096,19 @@ export class SavedObjectsTable extends Component this.setState({ isShowingExportAllOptionsModal: true })} onImport={this.showImportFlyout} - onCopy={() => this.setState({ isShowingCopyModal: true })} + hideImport={hideImport} + showDuplicateAll={workspaceEnabled} + onCopy={() => + this.setState({ + copySelectedSavedObjects: savedObjects, + isShowingCopyModal: true, + copyState: CopyState.All, + }) + } onRefresh={this.refreshObjects} filteredCount={filteredItemCount} title={this.props.title} - selectedCount={selectedSavedObjects.length} - hideImport={hideImport} - showDuplicateAll={workspaceEnabled} + objectCount={savedObjects.length} /> @@ -1097,7 +1125,20 @@ export class SavedObjectsTable extends Component this.setState({ isShowingCopyModal: true })} + onCopySelected={() => + this.setState({ + isShowingCopyModal: true, + copyState: CopyState.Selected, + copySelectedSavedObjects: selectedSavedObjects, + }) + } + onCopySingle={(object) => + this.setState({ + copySelectedSavedObjects: [object], + isShowingCopyModal: true, + copyState: CopyState.Single, + }) + } onActionRefresh={this.refreshObject} goInspectObject={this.props.goInspectObject} pageIndex={page} diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index 294f6325c526..eb4ab34088cc 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -57,8 +57,7 @@ import { registerServices } from './register_services'; import { bootstrap } from './ui_actions_bootstrap'; import { DEFAULT_APP_CATEGORIES } from '../../../core/public'; import { - ALL_LIBRARY_OBJECTS_TITLE_WORDINGS, - ALL_LIBRARY_OBJECTS_WORDINGS, + MANAGE_LIBRARY_TITLE_WORDINGS, SAVED_OBJECT_MANAGEMENT_TITLE_WORDINGS, SAVED_QUERIES_WORDINGS, SAVED_SEARCHES_WORDINGS, @@ -136,11 +135,11 @@ export class SavedObjectsManagementPlugin id: 'objects_all', appRoute: '/app/objects', exactRoute: true, - title: ALL_LIBRARY_OBJECTS_WORDINGS, + title: MANAGE_LIBRARY_TITLE_WORDINGS, order: 10000, category: DEFAULT_APP_CATEGORIES.opensearchDashboards, mount: mountWrapper({ - title: ALL_LIBRARY_OBJECTS_TITLE_WORDINGS, + title: MANAGE_LIBRARY_TITLE_WORDINGS, }), });