From 2296b15173e34ec27cdea73bb07cdcab6abda99d Mon Sep 17 00:00:00 2001 From: yubonluo <15242088755@163.com> Date: Fri, 8 Mar 2024 17:46:04 +0800 Subject: [PATCH] move all saved objects to target workspace --- .../saved_objects/simple_saved_object.ts | 3 + .../saved_objects/service/lib/repository.ts | 3 +- .../service/saved_objects_client.ts | 1 + .../delete_workspace_modal.tsx | 168 +++++++++++++++++- .../get_allowed_types.ts | 21 +++ .../move_to_target_workspace.ts | 27 +++ src/plugins/workspace/server/routes/index.ts | 55 ++++++ 7 files changed, 273 insertions(+), 5 deletions(-) create mode 100644 src/plugins/workspace/public/components/delete_workspace_modal/get_allowed_types.ts create mode 100644 src/plugins/workspace/public/components/delete_workspace_modal/move_to_target_workspace.ts diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index 71b1445e95aa..e2f9154deceb 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -52,6 +52,7 @@ export class SimpleSavedObject { public error: SavedObjectType['error']; public references: SavedObjectType['references']; public updated_at: SavedObjectType['updated_at']; + public workspaces: SavedObjectType['workspaces']; constructor( private client: SavedObjectsClientContract, @@ -64,6 +65,7 @@ export class SimpleSavedObject { references, migrationVersion, updated_at: updateAt, + workspaces, }: SavedObjectType ) { this.id = id; @@ -73,6 +75,7 @@ export class SimpleSavedObject { this._version = version; this.migrationVersion = migrationVersion; this.updated_at = updateAt; + this.workspaces = workspaces || []; if (error) { this.error = error; } diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 314f96b2016c..ad2883825c14 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -1552,7 +1552,7 @@ export class SavedObjectsRepository { let bulkGetRequestIndexCounter = 0; const expectedBulkGetResults: Either[] = objects.map((object) => { - const { type, id } = object; + const { type, id, workspaces } = object; if (!this._allowedTypes.includes(type)) { return { @@ -1586,6 +1586,7 @@ export class SavedObjectsRepository { [type]: attributes, updated_at: time, ...(Array.isArray(references) && { references }), + ...(workspaces && { workspaces }), }; const requiresNamespacesCheck = this._registry.isMultiNamespace(object.type); diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index c9990977bb48..6f0667fc093e 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -123,6 +123,7 @@ export interface SavedObjectsBulkUpdateObject * Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. **/ namespace?: string; + workspaces?: string[]; } /** 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..848fd6e2b00d 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,28 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; -import { WorkspaceAttribute } from 'opensearch-dashboards/public'; +import { SavedObjectsFindOptions, WorkspaceAttribute } from 'opensearch-dashboards/public'; import { i18n } from '@osd/i18n'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceClient } from '../../workspace_client'; +import { getAllowedTypes } from './get_allowed_types'; +import { moveToTargetWorkspace } from './move_to_target_workspace'; +type WorkspaceOption = EuiComboBoxOptionOption; +interface SaveObjectToMove { + id: string; + type: string; + workspaces: string[]; + attributes: any; +} + +function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption { + return { + label: workspace.name, + key: workspace.id, + value: workspace, + }; +} interface DeleteWorkspaceModalProps { onClose: () => void; selectedWorkspace?: WorkspaceAttribute | null; @@ -31,9 +50,119 @@ 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, savedObjects, workspaces }, } = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>(); + const savedObjectsClient = savedObjects!.client; + const [workspaceOptions, setWorkspaceOptions] = useState([]); + const [targetWorkspaceOption, setTargetWorkspaceOption] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const targetWorkspaceId = targetWorkspaceOption?.at(0)?.key; + let confirmDuplicateButtonEnabled = false; + const [savedObjectsCount, setSavedObjectsCount] = useState(0); + const [allowedTypes, setAllowedTypes] = useState([]); + const maxObjectsAmount: number = 200; + const onTargetWorkspaceChange = (targetOption: WorkspaceOption[]) => { + setTargetWorkspaceOption(targetOption); + }; + + if (!!targetWorkspaceId && savedObjectsCount > 0) { + confirmDuplicateButtonEnabled = true; + } + + useEffect(() => { + const workspaceList = workspaces!.workspaceList$.value; + const initWorkspaceOptions = [ + ...workspaceList + .filter((workspace: WorkspaceAttribute) => !workspace.libraryReadonly) + .filter((workspace: WorkspaceAttribute) => workspace.id !== selectedWorkspace?.id) + .map((workspace: WorkspaceAttribute) => workspaceToOption(workspace)), + ]; + setWorkspaceOptions(initWorkspaceOptions); + fetchSavedObjectsCount(); + }, [workspaces]); + + const fetchSavedObjectsCount = async () => { + const types = await getAllowedTypes(http!); + const findOptions: SavedObjectsFindOptions = { + workspaces: [selectedWorkspace?.id as string], + fields: ['id'], + type: types, + perPage: 1, + }; + + try { + const resp = await savedObjectsClient.find(findOptions); + setAllowedTypes(types); + setSavedObjectsCount(resp.total); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate( + 'workspace.deleteWorkspaceModal.unableFindSavedObjectsNotificationMessage', + { defaultMessage: 'Unable find saved objects count' } + ), + text: `${error}`, + }); + } + }; + + const fetchObjects = async () => { + const findOptions: SavedObjectsFindOptions = { + workspaces: [selectedWorkspace?.id as string], + type: allowedTypes, + perPage: maxObjectsAmount, + }; + + try { + return await savedObjectsClient.find(findOptions); + } catch (error) { + notifications?.toasts.addDanger({ + title: i18n.translate( + 'workspace.deleteWorkspaceModal.unableFindSavedObjectsNotificationMessage', + { defaultMessage: 'Unable find saved objects' } + ), + text: `${error}`, + }); + } + }; + + const moveObjectsToTargetWorkspace = async () => { + setIsLoading(true); + try { + for (let i = 1; i <= Math.ceil(savedObjectsCount / maxObjectsAmount); i++) { + const resp = await fetchObjects(); + if (resp!.total === 0) break; + const objects: SaveObjectToMove[] = resp!.savedObjects.map((obj) => ({ + id: obj.id, + type: obj.type, + workspaces: obj.workspaces!, + attributes: obj.attributes, + })); + await moveToTargetWorkspace( + http!, + objects, + selectedWorkspace?.id as string, + targetWorkspaceId as string + ); + if (resp!.total < maxObjectsAmount) break; + } + setSavedObjectsCount(0); + notifications?.toasts.addSuccess({ + title: i18n.translate('workspace.deleteWorkspaceModal.move.successNotification', { + defaultMessage: 'Move ' + savedObjectsCount + ' saved objects successfully', + }), + }); + } catch (e) { + notifications?.toasts.addDanger({ + title: i18n.translate('workspace.deleteWorkspaceModal.move.dangerNotification', { + defaultMessage: 'Unable to move ' + savedObjectsCount + ' saved objects', + }), + }); + } finally { + setIsLoading(false); + } + }; + const deleteWorkspace = async () => { if (selectedWorkspace?.id) { let result; @@ -83,6 +212,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 +268,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/components/delete_workspace_modal/get_allowed_types.ts b/src/plugins/workspace/public/components/delete_workspace_modal/get_allowed_types.ts new file mode 100644 index 000000000000..a13e3c615aa4 --- /dev/null +++ b/src/plugins/workspace/public/components/delete_workspace_modal/get_allowed_types.ts @@ -0,0 +1,21 @@ +/* + * 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 { HttpStart } from 'src/core/public'; + +interface GetAllowedTypesResponse { + types: string[]; +} + +export async function getAllowedTypes(http: HttpStart) { + const response = await http.get('/api/workspaces/_allowed_types'); + return response.types; +} diff --git a/src/plugins/workspace/public/components/delete_workspace_modal/move_to_target_workspace.ts b/src/plugins/workspace/public/components/delete_workspace_modal/move_to_target_workspace.ts new file mode 100644 index 000000000000..0fd5fe4653a8 --- /dev/null +++ b/src/plugins/workspace/public/components/delete_workspace_modal/move_to_target_workspace.ts @@ -0,0 +1,27 @@ +/* + * 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 { HttpStart } from 'src/core/public'; + +export async function moveToTargetWorkspace( + http: HttpStart, + objects: any[], + sourceWorkspaceId: string, + targetWorkspaceId: string +) { + return await http.post('/api/workspaces/_move_objects', { + body: JSON.stringify({ + objects, + sourceWorkspaceId, + targetWorkspaceId, + }), + }); +} diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts index 42ff4bba6001..d6ed954015d3 100644 --- a/src/plugins/workspace/server/routes/index.ts +++ b/src/plugins/workspace/server/routes/index.ts @@ -218,4 +218,59 @@ export function registerRoutes({ return res.ok({ body: result }); }) ); + router.get( + { + path: `${WORKSPACES_API_BASE_URL}/_allowed_types`, + validate: false, + }, + async (context, req, res) => { + const allowedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map((type) => type.name); + + return res.ok({ + body: { + types: allowedTypes, + }, + }); + } + ); + router.post( + { + path: `${WORKSPACES_API_BASE_URL}/_move_objects`, + validate: { + body: schema.object({ + objects: schema.maybe( + schema.arrayOf( + schema.object({ + type: schema.string(), + id: schema.string(), + workspaces: schema.arrayOf(schema.string()), + attributes: schema.recordOf(schema.string(), schema.any()), + }) + ) + ), + sourceWorkspaceId: schema.string(), + targetWorkspaceId: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const savedObjectsClient = context.core.savedObjects.client; + const { objects, sourceWorkspaceId, targetWorkspaceId } = req.body; + const objectsToMove = objects.map((obj) => { + const sourceIndex = obj.workspaces.indexOf(sourceWorkspaceId); + if (sourceIndex !== -1) { + obj.workspaces.splice(sourceIndex, 1); + } + const targetIndex = obj.workspaces.indexOf(targetWorkspaceId); + if (targetIndex === -1) { + obj.workspaces.push(targetWorkspaceId); + } + return obj; + }); + const result = await savedObjectsClient.bulkUpdate(objectsToMove); + return res.ok({ body: result }); + }) + ); }