Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move all saved objects to target workspace #292

Draft
wants to merge 8 commits into
base: workspace
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
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]);

Check failure on line 69 in src/plugins/workspace/public/components/delete_workspace_modal/delete_workspace_modal.tsx

View workflow job for this annotation

GitHub Actions / Build and Verify on Linux (ciGroup1)

React Hook useEffect has a missing dependency: 'selectedWorkspace?.id'. Either include it or remove the dependency array

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;
Expand Down Expand Up @@ -83,6 +140,37 @@
</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 All @@ -108,7 +196,7 @@
onClick={deleteWorkspace}
fill
color="danger"
disabled={value !== 'delete'}
disabled={value !== 'delete' || isLoading}
>
Delete
</EuiButton>
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 });
})
);
}
Loading