Skip to content

Commit

Permalink
add move all saved objects feature
Browse files Browse the repository at this point in the history
  • Loading branch information
yubonluo committed Mar 12, 2024
1 parent 9a0e1af commit ab0dad8
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<WorkspaceAttribute>;

function workspaceToOption(workspace: WorkspaceAttribute): WorkspaceOption {
return {
label: workspace.name,
key: workspace.id,
value: workspace,
};
}
interface DeleteWorkspaceModalProps {
onClose: () => void;
selectedWorkspace?: WorkspaceAttribute | null;
Expand All @@ -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<WorkspaceOption[]>([]);
const [targetWorkspaceOption, setTargetWorkspaceOption] = useState<WorkspaceOption[]>([]);
const [isLoading, setIsLoading] = useState(false);
const targetWorkspaceId = targetWorkspaceOption?.at(0)?.key;
const onTargetWorkspaceChange = (targetOption: WorkspaceOption[]) => {
setTargetWorkspaceOption(targetOption);
};

const workspaceList = useObservable<WorkspaceObject[]>(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 saved ' + result.length + ' 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;
Expand Down Expand Up @@ -83,6 +140,37 @@ export function DeleteWorkspaceModal(props: DeleteWorkspaceModalProps) {
</EuiModalHeader>

<EuiModalBody>
<div style={{ lineHeight: 1.5 }}>
<EuiText>
Before deleting the workspace, you have the option to keep the saved objects by moving
them to a target workspace.
</EuiText>
<EuiSpacer size="s" />

<EuiComboBox
placeholder="Please select a target workspace"
options={workspaceOptions}
selectedOptions={targetWorkspaceOption}
onChange={onTargetWorkspaceChange}
singleSelection={{ asPlainText: true }}
isClearable={false}
isInvalid={!targetWorkspaceId}
/>
<EuiSpacer size="m" />

<EuiButton
data-test-subj="Move All button"
onClick={moveObjectsToTargetWorkspace}
fill
color="primary"
size="s"
disabled={!targetWorkspaceId || isLoading}
isLoading={isLoading}
>
Move All
</EuiButton>
<EuiSpacer />
</div>
<div style={{ lineHeight: 1.5 }}>
<p>The following workspace will be permanently deleted. This action cannot be undone.</p>
<ul style={{ listStyleType: 'disc', listStylePosition: 'inside' }}>
Expand Down
21 changes: 21 additions & 0 deletions src/plugins/workspace/public/workspace_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
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();
Expand Down
62 changes: 61 additions & 1 deletion src/plugins/workspace/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
})
);
}

0 comments on commit ab0dad8

Please sign in to comment.