diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx index 100c1b4001fc..459cec4245b7 100644 --- a/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx +++ b/src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx @@ -3,10 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiButton, EuiButtonEmpty, + EuiComboBox, + EuiComboBoxOptionOption, EuiFieldText, EuiModal, EuiModalBody, @@ -16,11 +18,21 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import { WorkspaceAttribute } from 'opensearch-dashboards/public'; +import { WorkspaceAttribute, WorkspaceObject } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; +import { useObservable } from 'react-use'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceClient } from '../../workspace_client'; +type WorkspaceOption = EuiComboBoxOptionOption; + +function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { + return { + label: workspace.name, + key: workspace.id, + value: workspace, + }; +} interface DeleteWorkspaceModalProps { onClose: () => void; selectedWorkspace?: WorkspaceAttribute | null; @@ -31,9 +43,54 @@ export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { const [value, setValue] = useState(''); const { onClose, selectedWorkspace, returnToHome } = props; const { - services: { application, notifications, http, workspaceClient }, + services: { application, notifications, http, workspaceClient, workspaces }, } = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); + const [workspaceOptions, setWorkspaceOptions] = useState([]); + const [targetWorkspaceOption, setTargetWorkspaceOption] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const targetWorkspaceId = targetWorkspaceOption?.at(0)?.key; + const onTargetWorkspaceChange = (targetOption: WorkspaceOption[]) => { + setTargetWorkspaceOption(targetOption); + }; + + const workspaceList = useObservable(workspaces!.workspaceList$); + + useEffect(() => { + if (workspaceList) { + const initWorkspaceOptions = [ + ...workspaceList! + .filter((workspace) => !workspace.libraryReadonly) + .filter((workspace) => workspace.id !== selectedWorkspace?.id) + .map((workspace) => workspaceToOption(workspace)), + ]; + setWorkspaceOptions(initWorkspaceOptions); + } + }, [workspaceList]); + + const moveObjectsToTargetWorkspace = async () => { + setIsLoading(true); + try { + const result = await workspaceClient.moveAllObjects( + selectedWorkspace?.id as string, + targetWorkspaceId as string + ); + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.deleteWorkspaceModal.move.successNotification', { + defaultMessage: 'Moved ' + result.length + ' saved objects successfully', + }), + }); + } catch (e) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.deleteWorkspaceModal.move.dangerNotification', { + defaultMessage: 'Unable to move saved objects', + }), + }); + } finally { + setIsLoading(false); + } + }; + const deleteWorkspace = async () => { if (selectedWorkspace?.id) { let result; @@ -83,6 +140,37 @@ export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { +
+ + Before deleting the workspace, you have the option to keep the saved objects by moving + them to a target workspace. + + + + + + + + Move All + + +

The following workspace will be permanently deleted. This action cannot be undone.

    @@ -108,7 +196,7 @@ export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) { onClick={deleteWorkspace} fill color="danger" - disabled={value !== 'delete'} + disabled={value !== 'delete' || isLoading} > Delete diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts index 811d26cbf48d..85a0f4044ad1 100644 --- a/src/plugins/workspace/public/workspace_client.ts +++ b/src/plugins/workspace/public/workspace_client.ts @@ -285,6 +285,27 @@ export class WorkspaceClient { return result; } + /** + * Move all saved objects to a target workspace + * + * @param {string} sourceWorkspaceId + * @param {string} targetWorkspaceId + * @returns + */ + public async moveAllObjects(sourceWorkspaceId: string, targetWorkspaceId: string): Promise { + const path = this.getPath('_move_objects'); + const body = { + sourceWorkspaceId, + targetWorkspaceId, + }; + const result = await this.safeFetch(path, { + method: 'POST', + body: JSON.stringify(body), + }); + + return result; + } + public stop() { this.workspaces.workspaceList$.unsubscribe(); this.workspaces.currentWorkspaceId$.unsubscribe(); diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts index 42ff4bba6001..7e87f39db945 100644 --- a/src/plugins/workspace/server/routes/index.ts +++ b/src/plugins/workspace/server/routes/index.ts @@ -4,7 +4,13 @@ */ import { schema } from '@osd/config-schema'; -import { ensureRawRequest } from '../../../../core/server'; +import { + SavedObjectsAddToWorkspacesOptions, + SavedObjectsAddToWorkspacesResponse, + SavedObjectsFindOptions, + SavedObjectsShareObjects, + ensureRawRequest, +} from '../../../../core/server'; import { CoreSetup, Logger, WorkspacePermissionMode } from '../../../../core/server'; import { IWorkspaceClientImpl, WorkspacePermissionItem } from '../types'; @@ -218,4 +224,58 @@ export function registerRoutes({ return res.ok({ body: result }); }) ); + router.post( + { + path: `${WORKSPACES_API_BASE_URL}/_move_objects`, + validate: { + body: schema.object({ + sourceWorkspaceId: schema.string(), + targetWorkspaceId: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const MAX_OBJECTS_AMOUNT: number = 200; + const { sourceWorkspaceId, targetWorkspaceId } = req.body; + const savedObjectsClient = context.core.savedObjects.client; + const allowedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map((type) => type.name); + + const shareOptions: SavedObjectsAddToWorkspacesOptions = { + workspaces: [sourceWorkspaceId], + refresh: false, + }; + let results: SavedObjectsAddToWorkspacesResponse[] = []; + let page: number = 1; + while (true) { + const findOptions: SavedObjectsFindOptions = { + workspaces: [sourceWorkspaceId], + type: allowedTypes, + sortField: 'updated_at', + sortOrder: 'desc', + perPage: MAX_OBJECTS_AMOUNT, + page: page++, + }; + const response = await savedObjectsClient.find(findOptions); + if (!response) break; + const objects: SavedObjectsShareObjects[] = response.saved_objects + .filter((obj) => !obj.workspaces?.includes(targetWorkspaceId)) + .map((obj) => ({ + id: obj.id, + type: obj.type, + })); + if (objects.length > 0) { + const result = await savedObjectsClient.addToWorkspaces( + objects, + [targetWorkspaceId], + shareOptions + ); + results = results.concat(result); + } + if (response.saved_objects.length < MAX_OBJECTS_AMOUNT) break; + } + return res.ok({ body: results }); + }) + ); }