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: duplicate selected objects #113

Merged
merged 10 commits into from
Aug 30, 2023
584 changes: 576 additions & 8 deletions src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('SavedObjectTypeRegistry', () => {
users: ['user1'],
groups: ['group1'],
};
const result = ACL.genereateGetPermittedSavedObjectsQueryDSL(['read'], principals, 'workspace');
const result = ACL.generateGetPermittedSavedObjectsQueryDSL(['read'], principals, 'workspace');
expect(result).toEqual({
query: {
bool: {
Expand Down
2 changes: 1 addition & 1 deletion src/core/server/saved_objects/permission_control/acl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class ACL {
/**
* generate query DSL by the specific conditions, used for fetching saved objects from the saved objects index
*/
public static genereateGetPermittedSavedObjectsQueryDSL(
public static generateGetPermittedSavedObjectsQueryDSL(
permissionTypes: string[],
principals: Principals,
savedObjectType?: string | string[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class SavedObjectsPermissionControl {
permissionModes: SavedObjectsPermissionModes
) {
const principals = this.getPrincipalsFromRequest(request);
const queryDSL = ACL.genereateGetPermittedSavedObjectsQueryDSL(permissionModes, principals, [
const queryDSL = ACL.generateGetPermittedSavedObjectsQueryDSL(permissionModes, principals, [
WORKSPACE_TYPE,
]);
const repository = this.getInternalRepository();
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
import { WorkspacePermissionMode } from '../../../../core/public';

export async function getWorkspacesWithWritePermission(http: HttpStart) {
return await http.post('/api/workspaces/_list', {
body: JSON.stringify({
permissionModes: [WorkspacePermissionMode.Management, WorkspacePermissionMode.LibraryWrite],
}),
});
}
1 change: 1 addition & 0 deletions src/plugins/saved_objects_management/public/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ export { createFieldList } from './create_field_list';
export { getAllowedTypes } from './get_allowed_types';
export { filterQuery } from './filter_query';
export { copySavedObjects } from './copy_saved_objects';
export { getWorkspacesWithWritePermission } from './get_workspaces_with_write_permission';

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import {
EuiSpacer,
EuiComboBox,
EuiFormRow,
EuiSwitch,
EuiCheckbox,
EuiComboBoxOptionOption,
EuiInMemoryTable,
EuiToolTip,
EuiIcon,
EuiCallOut,
EuiText,
} from '@elastic/eui';
import { WorkspaceAttribute, WorkspaceStart } from 'opensearch-dashboards/public';
import { i18n } from '@osd/i18n';
Expand All @@ -46,11 +47,12 @@ interface Props {
targetWorkspace: string
) => Promise<void>;
onClose: () => void;
seletedSavedObjects: SavedObjectWithMetadata[];
getCopyWorkspaces: () => Promise<WorkspaceAttribute[]>;
selectedSavedObjects: SavedObjectWithMetadata[];
}

interface State {
allSeletedObjects: SavedObjectWithMetadata[];
allSelectedObjects: SavedObjectWithMetadata[];
workspaceOptions: WorkspaceOption[];
allWorkspaceOptions: WorkspaceOption[];
targetWorkspaceOption: WorkspaceOption[];
Expand All @@ -65,7 +67,7 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
super(props);

this.state = {
allSeletedObjects: this.props.seletedSavedObjects,
allSelectedObjects: this.props.selectedSavedObjects,
workspaceOptions: [],
allWorkspaceOptions: [],
targetWorkspaceOption: [],
Expand All @@ -79,21 +81,21 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
};

async componentDidMount() {
const { workspaces } = this.props;
const workspaceList = workspaces.workspaceList$;
const { workspaces, getCopyWorkspaces } = this.props;
const workspaceList = await getCopyWorkspaces();
const currentWorkspace = workspaces.currentWorkspace$;

if (!!currentWorkspace?.value?.name) {
const currentWorkspaceName = currentWorkspace.value.name;
const filteredWorkspaceOptions = workspaceList.value
const filteredWorkspaceOptions = workspaceList
.map(this.workspaceToOption)
.filter((item) => item.label !== currentWorkspaceName);
.filter((item: WorkspaceOption) => item.label !== currentWorkspaceName);
this.setState({
workspaceOptions: filteredWorkspaceOptions,
allWorkspaceOptions: filteredWorkspaceOptions,
});
} else {
const allWorkspaceOptions = workspaceList.value.map(this.workspaceToOption);
const allWorkspaceOptions = workspaceList.map(this.workspaceToOption);
this.setState({
workspaceOptions: allWorkspaceOptions,
allWorkspaceOptions,
Expand Down Expand Up @@ -152,18 +154,18 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
workspaceOptions,
targetWorkspaceOption,
isIncludeReferencesDeepChecked,
allSeletedObjects,
allSelectedObjects,
} = this.state;
const targetWorkspaceId = targetWorkspaceOption?.at(0)?.key;
const includedSeletedObjects = allSeletedObjects.filter((item) =>
const includedSelectedObjects = allSelectedObjects.filter((item) =>
!!targetWorkspaceId && !!item.workspaces
? !item.workspaces.includes(targetWorkspaceId)
: true && item.type !== SAVED_OBJECT_TYPE_WORKSAPCE
: item.type !== SAVED_OBJECT_TYPE_WORKSAPCE
);
const ignoredSeletedObjectsLength = allSeletedObjects.length - includedSeletedObjects.length;
const ignoredSelectedObjectsLength = allSelectedObjects.length - includedSelectedObjects.length;

let confirmCopyButtonEnabled = false;
if (!!targetWorkspaceId && includedSeletedObjects.length > 0) {
if (!!targetWorkspaceId && includedSelectedObjects.length > 0) {
confirmCopyButtonEnabled = true;
}

Expand All @@ -175,7 +177,7 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
);
const warningMessageForMultipleSavedObjects = (
<p>
<b style={{ color: '#000' }}>{ignoredSeletedObjectsLength}</b> saved objects will{' '}
<b style={{ color: '#000' }}>{ignoredSelectedObjectsLength}</b> saved objects will{' '}
<b style={{ color: '#000' }}>not</b> be copied, because they have already existed in the
selected workspace or they are worksapces themselves.
</p>
Expand All @@ -187,9 +189,9 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
title="Some saved objects will be ignored."
color="warning"
iconType="help"
aria-disabled={ignoredSeletedObjectsLength === 0}
aria-disabled={ignoredSelectedObjectsLength === 0}
>
{ignoredSeletedObjectsLength === 1
{ignoredSelectedObjectsLength === 1
? warningMessageForOnlyOneSavedObject
: warningMessageForMultipleSavedObjects}
</EuiCallOut>
Expand All @@ -207,7 +209,11 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
<EuiModalHeaderTitle>
<FormattedMessage
id="savedObjectsManagement.objectsTable.copyModal.title"
defaultMessage="Copy saved objects"
defaultMessage={
'Duplicate ' +
allSelectedObjects.length.toString() +
(allSelectedObjects.length > 1 ? ' objects?' : ' object?')
}
/>
</EuiModalHeaderTitle>
</EuiModalHeader>
Expand All @@ -218,36 +224,61 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
label={
<FormattedMessage
id="savedObjectsManagement.objectsTable.copyModal.targetWorkspacelabel"
defaultMessage="Select a workspace to copy to"
defaultMessage="Destination workspace"
/>
}
>
<EuiComboBox
options={workspaceOptions}
onChange={this.onTargetWorkspaceChange}
selectedOptions={targetWorkspaceOption}
singleSelection={{ asPlainText: true }}
onSearchChange={this.onSearchWorkspaceChange}
isClearable={false}
isInvalid={!confirmCopyButtonEnabled}
/>
<>
<EuiText size="s" color="subdued">
{'Specify a workspace where the objects will be duplicated.'}
</EuiText>
<EuiSpacer size="s" />
<EuiComboBox
options={workspaceOptions}
onChange={this.onTargetWorkspaceChange}
selectedOptions={targetWorkspaceOption}
singleSelection={{ asPlainText: true }}
onSearchChange={this.onSearchWorkspaceChange}
isClearable={false}
isInvalid={!confirmCopyButtonEnabled}
/>
</>
</EuiFormRow>

<EuiSpacer size="m" />
<EuiSwitch
name="includeReferencesDeep"

<EuiFormRow
fullWidth
label={
<FormattedMessage
id="savedObjectsManagement.objectsTable.copyModal.includeReferencesDeepLabel"
defaultMessage="Include related objects"
id="savedObjectsManagement.objectsTable.copyModal.relatedObjects"
defaultMessage="Related Objects"
/>
}
checked={isIncludeReferencesDeepChecked}
onChange={this.changeIncludeReferencesDeep}
/>
>
<>
<EuiText size="s" color="subdued">
{
'We recommended duplicating related objects to ensure your duplicated objects will continue to function.'
}
</EuiText>
<EuiSpacer size="s" />
<EuiCheckbox
id={'includeReferencesDeep'}
label={
<FormattedMessage
id="savedObjectsManagement.objectsTable.copyModal.includeReferencesDeepLabel"
defaultMessage="Duplicate related objects"
/>
}
checked={isIncludeReferencesDeepChecked}
onChange={this.changeIncludeReferencesDeep}
/>
</>
</EuiFormRow>

<EuiSpacer size="m" />
{ignoredSeletedObjectsLength === 0 ? null : ignoreSomeObjectsChildren}
{ignoredSelectedObjectsLength === 0 ? null : ignoreSomeObjectsChildren}
<p>
<FormattedMessage
id="savedObjectsManagement.objectsTable.copyModal.tableTitle"
Expand All @@ -256,7 +287,7 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
</p>
<EuiSpacer size="m" />
<EuiInMemoryTable
items={includedSeletedObjects}
items={includedSelectedObjects}
columns={[
{
field: 'type',
Expand Down Expand Up @@ -301,13 +332,13 @@ export class SavedObjectsCopyModal extends React.Component<Props, State> {
<EuiButton
fill
data-test-subj="copyConfirmButton"
onClick={() => this.copySavedObjects(includedSeletedObjects)}
onClick={() => this.copySavedObjects(includedSelectedObjects)}
isLoading={this.state.isLoading}
disabled={!confirmCopyButtonEnabled}
>
<FormattedMessage
id="savedObjectsManagement.objectsTable.copyModal.confirmButtonLabel"
defaultMessage="Confirm copy"
defaultMessage="Duplicate"
/>
</EuiButton>
</EuiModalFooter>
Expand Down
Loading