From 251df79fec7e55645389b9c0e79f3694260c86ef Mon Sep 17 00:00:00 2001 From: Yuye Zhu Date: Wed, 20 Sep 2023 13:14:17 +0800 Subject: [PATCH] feat: Duplicate dashboard visualize (#148) * rename copy to duplicate Signed-off-by: yuye-aws * duplicate in visualization Signed-off-by: yuye-aws * duplicate in dashboard Signed-off-by: yuye-aws * resolve conflict Signed-off-by: yuye-aws * update test and snapshots Signed-off-by: yuye-aws * re-duplicate if some objects cannot be duplicated Signed-off-by: yuye-aws * remove clone for dashboard Signed-off-by: yuye-aws * rename duplicateState to duplicateMode Signed-off-by: yuye-aws * change workspace prop to currentWorkspace in SavedObjectsDuplicateModal Signed-off-by: yuye-aws * change wording Signed-off-by: yuye-aws * move duplicate modal to saved_objects for reuse Signed-off-by: yuye-aws * move duplicate modal to saved objects management for reuse Signed-off-by: yuye-aws * remove minimal duplicate modal props logic Signed-off-by: yuye-aws * refactor duplicate modal props for dashboard and visualization Signed-off-by: yuye-aws * Update getDuplicateWorkspaces function Co-authored-by: Yulong Ruan * update function onDuplicate for dashboard Signed-off-by: yuye-aws * Update doDuplicate for visualization Co-authored-by: Yulong Ruan * refactor function getDuplicateWorkspaces Signed-off-by: yuye-aws * add i18n context to saved objects table duplicate modal Signed-off-by: yuye-aws * refactor duplicate modal logic in saved object table Signed-off-by: yuye-aws * add error message for partial duplicate failed Signed-off-by: yuye-aws * merge commits Signed-off-by: yuye-aws * add type info for dashboard and visualization Signed-off-by: yuye-aws * remote create vis reference logic Signed-off-by: yuye-aws * Revert "remove clone for dashboard" This reverts commit 84f77fb044dfbc62d208584ed8e74018fe59306f. * hide duplicate when workspace disabled in dashboard Signed-off-by: yuye-aws * feat: skip permission validate when no workspaces and permissions attributes (#163) * feat: skip permission validate when saved object without workspaces and permissions attributes Signed-off-by: Lin Wang * feat: add annontation to skip permission check Signed-off-by: Lin Wang * refactor: remove bind and simplify validate logic Signed-off-by: Lin Wang * feat: remove library write for object based ACL Signed-off-by: Lin Wang --------- Signed-off-by: Lin Wang * remove get workspaces with write permission logic and add readonly props to workspace attribute Signed-off-by: yuye-aws * change type definition logic Signed-off-by: yuye-aws * Fix typo (#176) --------- Signed-off-by: Yulong Ruan * remove exit workspace logic (#179) Signed-off-by: yuye-aws * rename copy to duplicate Signed-off-by: yuye-aws * duplicate in visualization Signed-off-by: yuye-aws * duplicate in dashboard Signed-off-by: yuye-aws * resolve conflict Signed-off-by: yuye-aws * update test and snapshots Signed-off-by: yuye-aws * re-duplicate if some objects cannot be duplicated Signed-off-by: yuye-aws * remove clone for dashboard Signed-off-by: yuye-aws * rename duplicateState to duplicateMode Signed-off-by: yuye-aws * change workspace prop to currentWorkspace in SavedObjectsDuplicateModal Signed-off-by: yuye-aws * change wording Signed-off-by: yuye-aws * move duplicate modal to saved_objects for reuse Signed-off-by: yuye-aws * move duplicate modal to saved objects management for reuse Signed-off-by: yuye-aws * remove minimal duplicate modal props logic Signed-off-by: yuye-aws * refactor duplicate modal props for dashboard and visualization Signed-off-by: yuye-aws * Update getDuplicateWorkspaces function Co-authored-by: Yulong Ruan * update function onDuplicate for dashboard Signed-off-by: yuye-aws * Update doDuplicate for visualization Co-authored-by: Yulong Ruan * refactor function getDuplicateWorkspaces Signed-off-by: yuye-aws * add i18n context to saved objects table duplicate modal Signed-off-by: yuye-aws * refactor duplicate modal logic in saved object table Signed-off-by: yuye-aws * add error message for partial duplicate failed Signed-off-by: yuye-aws * merge commits Signed-off-by: yuye-aws * add type info for dashboard and visualization Signed-off-by: yuye-aws * remote create vis reference logic Signed-off-by: yuye-aws * Revert "remove clone for dashboard" This reverts commit 84f77fb044dfbc62d208584ed8e74018fe59306f. * hide duplicate when workspace disabled in dashboard Signed-off-by: yuye-aws * remove get workspaces with write permission logic and add readonly props to workspace attribute Signed-off-by: yuye-aws * change type definition logic Signed-off-by: yuye-aws * rename variable and function name Signed-off-by: yuye-aws * change permission mode to get target workspaces when duplicate Signed-off-by: yuye-aws --------- Signed-off-by: yuye-aws Signed-off-by: Lin Wang Signed-off-by: Yulong Ruan Co-authored-by: Yulong Ruan Co-authored-by: Lin Wang Co-authored-by: Yulong Ruan # Conflicts: # src/core/public/workspace/workspaces_service.ts # src/plugins/saved_objects_management/public/constants.ts # src/plugins/saved_objects_management/public/management_section/objects_table/components/duplicate_modal.tsx # src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx # src/plugins/workspace/server/saved_objects/workspace_saved_objects_client_wrapper.ts --- .../saved_objects_management/public/index.ts | 15 +- ..._objects.ts => duplicate_saved_objects.ts} | 2 +- .../get_workspaces_with_write_permission.ts | 21 -- .../public/lib/index.ts | 3 +- .../public/management_section/index.ts | 1 + .../saved_objects_table.test.tsx.snap | 6 +- .../{copy_modal.tsx => duplicate_modal.tsx} | 136 ++++++----- .../objects_table/components/header.test.tsx | 11 +- .../objects_table/components/header.tsx | 8 +- .../objects_table/components/index.ts | 2 + .../components/show_duplicate_modal.tsx | 64 ++++++ .../objects_table/components/table.tsx | 12 +- .../management_section/objects_table/index.ts | 3 +- .../objects_table/saved_objects_table.tsx | 213 +++++++++--------- .../saved_objects_table_page.tsx | 1 + .../workspace/public/workspace_client.ts | 16 +- 16 files changed, 304 insertions(+), 210 deletions(-) rename src/plugins/saved_objects_management/public/lib/{copy_saved_objects.ts => duplicate_saved_objects.ts} (93%) delete mode 100644 src/plugins/saved_objects_management/public/lib/get_workspaces_with_write_permission.ts rename src/plugins/saved_objects_management/public/management_section/objects_table/components/{copy_modal.tsx => duplicate_modal.tsx} (75%) create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/show_duplicate_modal.tsx diff --git a/src/plugins/saved_objects_management/public/index.ts b/src/plugins/saved_objects_management/public/index.ts index 317b3079efa0..8db0d65c934c 100644 --- a/src/plugins/saved_objects_management/public/index.ts +++ b/src/plugins/saved_objects_management/public/index.ts @@ -46,11 +46,22 @@ export { ISavedObjectsManagementServiceRegistry, SavedObjectsManagementServiceRegistryEntry, } from './services'; -export { ProcessedImportResponse, processImportResponse, FailedImport } from './lib'; +export { + ProcessedImportResponse, + processImportResponse, + FailedImport, + duplicateSavedObjects, + getSavedObjectLabel, +} from './lib'; export { SavedObjectRelation, SavedObjectWithMetadata, SavedObjectMetadata } from './types'; export { SAVED_OBJECT_DELETE_TRIGGER, savedObjectDeleteTrigger } from './triggers'; export { SavedObjectDeleteContext } from './ui_actions_bootstrap'; - +export { SAVED_OBJECT_TYPE_WORKSPACE } from './constants'; +export { + showDuplicateModal, + SavedObjectsDuplicateModal, + DuplicateMode, +} from './management_section'; export function plugin(initializerContext: PluginInitializerContext) { return new SavedObjectsManagementPlugin(); } diff --git a/src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/duplicate_saved_objects.ts similarity index 93% rename from src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts rename to src/plugins/saved_objects_management/public/lib/duplicate_saved_objects.ts index c28893589367..1128cef9d2f3 100644 --- a/src/plugins/saved_objects_management/public/lib/copy_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/duplicate_saved_objects.ts @@ -11,7 +11,7 @@ import { HttpStart } from 'src/core/public'; -export async function copySavedObjects( +export async function duplicateSavedObjects( http: HttpStart, objects: any[], includeReferencesDeep: boolean = true, diff --git a/src/plugins/saved_objects_management/public/lib/get_workspaces_with_write_permission.ts b/src/plugins/saved_objects_management/public/lib/get_workspaces_with_write_permission.ts deleted file mode 100644 index 2a8b47a86aae..000000000000 --- a/src/plugins/saved_objects_management/public/lib/get_workspaces_with_write_permission.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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], - }), - }); -} diff --git a/src/plugins/saved_objects_management/public/lib/index.ts b/src/plugins/saved_objects_management/public/lib/index.ts index a25aaf669066..80630b8780e7 100644 --- a/src/plugins/saved_objects_management/public/lib/index.ts +++ b/src/plugins/saved_objects_management/public/lib/index.ts @@ -57,5 +57,4 @@ export { extractExportDetails, SavedObjectsExportResultDetails } from './extract 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'; +export { duplicateSavedObjects } from './duplicate_saved_objects'; diff --git a/src/plugins/saved_objects_management/public/management_section/index.ts b/src/plugins/saved_objects_management/public/management_section/index.ts index 333bee71b0c0..1f29fa548559 100644 --- a/src/plugins/saved_objects_management/public/management_section/index.ts +++ b/src/plugins/saved_objects_management/public/management_section/index.ts @@ -29,3 +29,4 @@ */ export { mountManagementSection } from './mount_section'; +export { showDuplicateModal, SavedObjectsDuplicateModal, DuplicateMode } from './objects_table'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index e3a2ca0a557f..7ed6ab3337ac 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -204,7 +204,7 @@ exports[`SavedObjectsTable should render normally 1`] = `
; -interface Props { - workspaces: WorkspacesStart; - onCopy: ( +export enum DuplicateMode { + Selected = 'selected', + All = 'all', +} +export interface ShowDuplicateModalProps { + onDuplicate: ( savedObjects: SavedObjectWithMetadata[], includeReferencesDeep: boolean, targetWorkspace: string ) => Promise; - onClose: () => void; - copyState: CopyState; - getCopyWorkspaces: () => Promise; + http: HttpSetup; + workspaces: WorkspacesStart; + duplicateMode: DuplicateMode; + notifications: NotificationsStart; selectedSavedObjects: SavedObjectWithMetadata[]; } +interface Props extends ShowDuplicateModalProps { + onClose: () => void; +} + interface State { allSelectedObjects: SavedObjectWithMetadata[]; workspaceOptions: WorkspaceOption[]; @@ -66,7 +78,7 @@ function capitalizeFirstLetter(str: string): string { return str.charAt(0).toUpperCase() + str.slice(1); } -export class SavedObjectsCopyModal extends React.Component { +export class SavedObjectsDuplicateModal extends React.Component { private isMounted = false; constructor(props: Props) { @@ -100,15 +112,15 @@ export class SavedObjectsCopyModal extends React.Component { }; async componentDidMount() { - const { workspaces, getCopyWorkspaces } = this.props; - const workspaceList = await getCopyWorkspaces(); + const { workspaces } = this.props; const currentWorkspace = workspaces.currentWorkspace$.value; const currentWorkspaceName = currentWorkspace?.name; + const targetWorkspaces = this.getTargetWorkspaces(); // current workspace is the first option const workspaceOptions = [ ...(currentWorkspace ? [this.workspaceToOption(currentWorkspace, currentWorkspaceName)] : []), - ...workspaceList + ...targetWorkspaces .filter((workspace: WorkspaceAttribute) => workspace.name !== currentWorkspaceName) .map((workspace: WorkspaceAttribute) => this.workspaceToOption(workspace, currentWorkspaceName) @@ -120,8 +132,8 @@ export class SavedObjectsCopyModal extends React.Component { allWorkspaceOptions: workspaceOptions, }); - const { copyState } = this.props; - if (copyState === CopyState.All) { + const { duplicateMode } = this.props; + if (duplicateMode === DuplicateMode.All) { const { allSelectedObjects } = this.state; const categorizedObjects = groupBy(allSelectedObjects, (object) => object.type); const savedObjectTypeInfoMap = new Map(); @@ -138,14 +150,20 @@ export class SavedObjectsCopyModal extends React.Component { this.isMounted = false; } - copySavedObjects = async (savedObjects: SavedObjectWithMetadata[]) => { + getTargetWorkspaces = () => { + const { workspaces } = this.props; + const workspaceList = workspaces.workspaceList$.value; + return workspaceList.filter((workspace) => !workspace.libraryReadonly); + }; + + duplicateSavedObjects = async (savedObjects: SavedObjectWithMetadata[]) => { this.setState({ isLoading: true, }); const targetWorkspace = this.state.targetWorkspaceOption[0].key; - await this.props.onCopy( + await this.props.onDuplicate( savedObjects, this.state.isIncludeReferencesDeepChecked, targetWorkspace! @@ -188,7 +206,7 @@ export class SavedObjectsCopyModal extends React.Component { } }; - renderCopyObjectCategory = ( + renderDuplicateObjectCategory = ( savedObjectType: string, savedObjectTypeCount: number, savedObjectTypeChecked: boolean @@ -199,7 +217,10 @@ export class SavedObjectsCopyModal extends React.Component { key={savedObjectType} label={ { ); }; - renderCopyObjectCategories = () => { + renderDuplicateObjectCategories = () => { const { savedObjectTypeInfoMap } = this.state; const checkboxList: JSX.Element[] = []; savedObjectTypeInfoMap.forEach( ([savedObjectTypeCount, savedObjectTypeChecked], savedObjectType) => checkboxList.push( - this.renderCopyObjectCategory( + this.renderDuplicateObjectCategory( savedObjectType, savedObjectTypeCount, savedObjectTypeChecked @@ -240,10 +261,10 @@ export class SavedObjectsCopyModal extends React.Component { isIncludeReferencesDeepChecked, allSelectedObjects, } = this.state; - const { copyState } = this.props; + const { duplicateMode, onClose } = this.props; const targetWorkspaceId = targetWorkspaceOption?.at(0)?.key; let selectedObjects = allSelectedObjects; - if (copyState === CopyState.All) { + if (duplicateMode === DuplicateMode.All) { selectedObjects = selectedObjects.filter((item) => this.isSavedObjectTypeIncluded(item.type)); } const includedSelectedObjects = selectedObjects.filter((item) => @@ -252,17 +273,11 @@ export class SavedObjectsCopyModal extends React.Component { const ignoredSelectedObjectsLength = selectedObjects.length - includedSelectedObjects.length; - let confirmCopyButtonEnabled = false; + let confirmDuplicateButtonEnabled = false; if (!!targetWorkspaceId && includedSelectedObjects.length > 0) { - confirmCopyButtonEnabled = true; + confirmDuplicateButtonEnabled = true; } - const confirmMessageForAllObjects = `Duplicate (${includedSelectedObjects.length})`; - const confirmMessageForSingleOrSelectedObjects = 'Duplicate'; - const confirmMessage = - copyState === CopyState.All - ? confirmMessageForAllObjects - : confirmMessageForSingleOrSelectedObjects; const warningMessageForOnlyOneSavedObject = (

1 saved object will not be @@ -295,17 +310,17 @@ export class SavedObjectsCopyModal extends React.Component { return ( { fullWidth label={ } @@ -335,20 +350,20 @@ export class SavedObjectsCopyModal extends React.Component { singleSelection={{ asPlainText: true }} onSearchChange={this.onSearchWorkspaceChange} isClearable={false} - isInvalid={!confirmCopyButtonEnabled} + isInvalid={!confirmDuplicateButtonEnabled} /> - {copyState && this.renderCopyObjectCategories()} - {copyState && } + {duplicateMode === DuplicateMode.All && this.renderDuplicateObjectCategories()} + {duplicateMode === DuplicateMode.All && } } @@ -364,7 +379,7 @@ export class SavedObjectsCopyModal extends React.Component { id={'includeReferencesDeep'} label={ } @@ -378,7 +393,7 @@ export class SavedObjectsCopyModal extends React.Component { {ignoredSelectedObjectsLength === 0 ? null : ignoreSomeObjectsChildren}

@@ -389,26 +404,29 @@ export class SavedObjectsCopyModal extends React.Component { { field: 'type', name: i18n.translate( - 'savedObjectsManagement.objectsTable.copyModal.typeColumnName', + 'savedObjectsManagement.objectsTable.duplicateModal.typeColumnName', { defaultMessage: 'Type' } ), width: '50px', render: (type, object) => ( - + ), }, { field: 'id', - name: i18n.translate('savedObjectsManagement.objectsTable.copyModal.idColumnName', { - defaultMessage: 'Id', - }), + name: i18n.translate( + 'savedObjectsManagement.objectsTable.duplicateModal.idColumnName', + { + defaultMessage: 'Id', + } + ), }, { field: 'meta.title', name: i18n.translate( - 'savedObjectsManagement.objectsTable.copyModal.titleColumnName', + 'savedObjectsManagement.objectsTable.duplicateModal.titleColumnName', { defaultMessage: 'Title' } ), }, @@ -419,23 +437,27 @@ export class SavedObjectsCopyModal extends React.Component { - + this.copySavedObjects(includedSelectedObjects)} + data-test-subj="duplicateConfirmButton" + onClick={() => this.duplicateSavedObjects(includedSelectedObjects)} isLoading={this.state.isLoading} - disabled={!confirmCopyButtonEnabled} + disabled={!confirmDuplicateButtonEnabled} > diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx index 18dfb3a7ae17..d98fe7257fbd 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.test.tsx @@ -39,9 +39,10 @@ describe('Header', () => { onImport: () => {}, onRefresh: () => {}, onCopy: () => {}, + onDuplicate: () => {}, title: 'Saved Objects', selectedCount: 0, - totalCount: 4, + objectCount: 4, filteredCount: 2, showDuplicateAll: false, hideImport: false, @@ -60,9 +61,10 @@ describe('Header - workspace enabled', () => { onImport: () => {}, onRefresh: () => {}, onCopy: () => {}, + onDuplicate: () => {}, title: 'Saved Objects', selectedCount: 0, - totalCount: 4, + objectCount: 4, filteredCount: 2, showDuplicateAll: true, hideImport: false, @@ -70,7 +72,7 @@ describe('Header - workspace enabled', () => { const component = shallow(
); - expect(component.find('EuiButtonEmpty[data-test-subj="copyObjects"]').exists()).toBe(true); + expect(component.find('EuiButtonEmpty[data-test-subj="duplicateObjects"]').exists()).toBe(true); }); it('should hide `Import` button for application home state', () => { @@ -79,9 +81,10 @@ describe('Header - workspace enabled', () => { onImport: () => {}, onRefresh: () => {}, onCopy: () => {}, + onDuplicate: () => {}, title: 'Saved Objects', selectedCount: 0, - totalCount: 4, + objectCount: 4, filteredCount: 2, showDuplicateAll: true, hideImport: true, diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx index 244c05cab36d..47df820a9e8e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/header.tsx @@ -43,7 +43,7 @@ import { FormattedMessage } from '@osd/i18n/react'; export const Header = ({ onExportAll, onImport, - onCopy, + onDuplicate, onRefresh, filteredCount, objectCount, @@ -51,7 +51,7 @@ export const Header = ({ }: { onExportAll: () => void; onImport: () => void; - onCopy: () => void; + onDuplicate: () => void; onRefresh: () => void; filteredCount: number; objectCount: number; @@ -76,8 +76,8 @@ export const Header = ({ { + ReactDOM.unmountComponentAtNode(container); + document.body.removeChild(container); + }; + + const { + http, + workspaces, + onDuplicate, + duplicateMode, + notifications, + selectedSavedObjects, + } = showDuplicateModalProps; + + const onDuplicateConfirmed: ShowDuplicateModalProps['onDuplicate'] = async (...args) => { + await onDuplicate(...args); + closeModal(); + }; + + const duplicateModal = ( + + ); + + document.body.appendChild(container); + + ReactDOM.render({duplicateModal}, container); +} diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx index 6cd220fb0c07..a6629084d053 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.tsx @@ -70,8 +70,8 @@ export interface TableProps { filters: any[]; canDelete: boolean; onDelete: () => void; - onCopySelected: () => void; - onCopySingle: (object: SavedObjectWithMetadata) => void; + onDuplicateSelected: () => void; + onDuplicateSingle: (object: SavedObjectWithMetadata) => void; onActionRefresh: (object: SavedObjectWithMetadata) => void; onExport: (includeReferencesDeep: boolean) => void; goInspectObject: (obj: SavedObjectWithMetadata) => void; @@ -173,8 +173,8 @@ export class Table extends PureComponent { filters, selectionConfig: selection, onDelete, - onCopySelected, - onCopySingle, + onDuplicateSelected, + onDuplicateSingle, onActionRefresh, selectedSavedObjects, onTableChange, @@ -331,7 +331,7 @@ export class Table extends PureComponent { type: 'icon', icon: 'copyClipboard', isPrimary: true, - onClick: (object: SavedObjectWithMetadata) => onCopySingle(object), + onClick: (object: SavedObjectWithMetadata) => onDuplicateSingle(object), 'data-test-subj': 'savedObjectsTableAction-duplicate', }, ] @@ -454,7 +454,7 @@ export class Table extends PureComponent { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts index 75d036961186..3270414b9e9f 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/index.ts @@ -28,4 +28,5 @@ * under the License. */ -export { SavedObjectsTable, CopyState } from './saved_objects_table'; +export { SavedObjectsTable } from './saved_objects_table'; +export { showDuplicateModal, SavedObjectsDuplicateModal, DuplicateMode } from './components'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index fa339f7c9c5e..9ac3044aff73 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -78,7 +78,6 @@ import { SavedObjectCountOptions, getRelationships, getSavedObjectLabel, - getWorkspacesWithWritePermission, fetchExportObjects, fetchExportByTypeAndSearch, filterQuery, @@ -86,7 +85,7 @@ import { findObject, extractExportDetails, SavedObjectsExportResultDetails, - copySavedObjects, + duplicateSavedObjects, } from '../../lib'; import { SavedObjectWithMetadata } from '../../types'; import { @@ -95,21 +94,15 @@ import { SavedObjectsManagementColumnServiceStart, SavedObjectsManagementNamespaceServiceStart, } from '../../services'; -import { Header, Table, Flyout, Relationships } from './components'; +import { Header, Table, Flyout, Relationships, SavedObjectsDuplicateModal } from './components'; import { DataPublicPluginStart } from '../../../../data/public'; -import { SavedObjectsCopyModal } from './components/copy_modal'; -export enum CopyState { - Single = 'single', - Selected = 'selected', - All = 'all', -} +import { DuplicateMode } from './'; interface ExportAllOption { id: string; label: string; } - export interface SavedObjectsTableProps { allowedTypes: string[]; serviceRegistry: ISavedObjectsManagementServiceRegistry; @@ -140,10 +133,10 @@ export interface SavedObjectsTableState { savedObjectCounts: Record>; activeQuery: Query; selectedSavedObjects: SavedObjectWithMetadata[]; - copySelectedSavedObjects: SavedObjectWithMetadata[]; + duplicateSelectedSavedObjects: SavedObjectWithMetadata[]; isShowingImportFlyout: boolean; - isShowingCopyModal: boolean; - copyState: CopyState; + duplicateMode: DuplicateMode; + isShowingDuplicateModal: boolean; isSearching: boolean; filteredItemCount: number; isShowingRelationships: boolean; @@ -181,10 +174,10 @@ export class SavedObjectsTable extends Component>, activeQuery: Query.parse(''), selectedSavedObjects: [], - copySelectedSavedObjects: [], + duplicateSelectedSavedObjects: [], isShowingImportFlyout: false, - isShowingCopyModal: false, - copyState: CopyState.Selected, + duplicateMode: DuplicateMode.Selected, + isShowingDuplicateModal: false, isSearching: false, filteredItemCount: 0, isShowingRelationships: false, @@ -507,67 +500,6 @@ export class SavedObjectsTable extends Component => { - const { notifications, http } = this.props; - let result; - try { - result = await getWorkspacesWithWritePermission(http); - } catch (error) { - notifications?.toasts.addDanger({ - title: i18n.translate( - 'savedObjectsManagement.objectsTable.copyWorkspaces.dangerNotification', - { - defaultMessage: 'Unable to get workspaces with write permission', - } - ), - text: error instanceof Error ? error.message : JSON.stringify(error), - }); - } - if (result?.success) { - return result.result?.workspaces ?? []; - } else { - return []; - } - }; - - onCopy = async ( - savedObjects: SavedObjectWithMetadata[], - includeReferencesDeep: boolean, - targetWorkspace: string - ) => { - const { notifications, http } = this.props; - const objectsToCopy = savedObjects.map((obj) => ({ id: obj.id, type: obj.type })); - let result; - try { - result = await copySavedObjects(http, objectsToCopy, includeReferencesDeep, targetWorkspace); - if (result.success) { - notifications.toasts.addSuccess({ - title: i18n.translate('savedObjectsManagement.objectsTable.copy.successNotification', { - defaultMessage: - 'Copy ' + savedObjects.length.toString() + ' saved objects successfully', - }), - }); - } else { - const failedCount = savedObjects.length - result.successCount; - notifications.toasts.addSuccess({ - title: i18n.translate('savedObjectsManagement.objectsTable.copy.dangerNotification', { - defaultMessage: 'Unable to copy ' + failedCount.toString() + ' saved objects', - }), - }); - } - } catch (e) { - notifications.toasts.addDanger({ - title: i18n.translate('savedObjectsManagement.objectsTable.copy.dangerNotification', { - defaultMessage: 'Unable to copy all saved objects', - }), - }); - throw e; - } - - this.hideCopyModal(); - this.refreshObjects(); - }; - onExport = async (includeReferencesDeep: boolean) => { const { selectedSavedObjects } = this.state; const { notifications, http } = this.props; @@ -674,14 +606,6 @@ export class SavedObjectsTable extends Component { - this.setState({ isShowingCopyModal: true }); - }; - - hideCopyModal = () => { - this.setState({ isShowingCopyModal: false }); - }; - onDelete = () => { this.setState({ isShowingDeleteConfirmModal: true }); }; @@ -755,22 +679,95 @@ export class SavedObjectsTable extends Component { + this.setState({ isShowingDuplicateModal: true }); + }; - if (!isShowingCopyModal) { + hideDuplicateModal = () => { + this.setState({ isShowingDuplicateModal: false }); + }; + + renderDuplicateModal() { + const { workspaces, http, notifications } = this.props; + const { isShowingDuplicateModal, duplicateSelectedSavedObjects, duplicateMode } = this.state; + + if (!isShowingDuplicateModal) { return null; } + const onDuplicate = async ( + savedObjects: SavedObjectWithMetadata[], + includeReferencesDeep: boolean, + targetWorkspace: string + ) => { + const objectsToDuplicate = savedObjects.map((obj) => ({ id: obj.id, type: obj.type })); + let result; + try { + result = await duplicateSavedObjects( + http, + objectsToDuplicate, + includeReferencesDeep, + targetWorkspace + ); + if (result.success) { + notifications.toasts.addSuccess({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.duplicate.successNotification', + { + defaultMessage: + 'Duplicate ' + savedObjects.length.toString() + ' saved objects successfully', + } + ), + }); + } else if (result.errors) { + const errorsIds = result.errors.map((item: { id: string }) => item.id); + notifications.toasts.addDanger({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.duplicate.dangerNotification', + { + defaultMessage: + 'Unable to duplicate ' + + savedObjects.length.toString() + + ' saved objects. These objects cannot be duplicated:' + + errorsIds.join(','), + } + ), + }); + } else { + notifications.toasts.addDanger({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.duplicate.dangerNotification', + { + defaultMessage: + 'Unable to duplicate ' + savedObjects.length.toString() + ' saved objects', + } + ), + }); + } + } catch (e) { + notifications.toasts.addDanger({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.duplicate.dangerNotification', + { + defaultMessage: + 'Unable to duplicate ' + savedObjects.length.toString() + ' saved objects', + } + ), + }); + } + this.hideDuplicateModal(); + await this.refreshObjects(); + }; + return ( - ); } @@ -1102,16 +1099,16 @@ export class SavedObjectsTable extends Component this.setState({ isShowingExportAllOptionsModal: true })} onImport={this.showImportFlyout} showDuplicateAll={workspaceEnabled} - onCopy={() => + onDuplicate={() => this.setState({ - copySelectedSavedObjects: savedObjects, - isShowingCopyModal: true, - copyState: CopyState.All, + duplicateSelectedSavedObjects: savedObjects, + isShowingDuplicateModal: true, + duplicateMode: DuplicateMode.All, }) } onRefresh={this.refreshObjects} @@ -1133,18 +1130,18 @@ export class SavedObjectsTable extends Component + onDuplicateSelected={() => this.setState({ - isShowingCopyModal: true, - copyState: CopyState.Selected, - copySelectedSavedObjects: selectedSavedObjects, + isShowingDuplicateModal: true, + duplicateMode: DuplicateMode.Selected, + duplicateSelectedSavedObjects: selectedSavedObjects, }) } - onCopySingle={(object) => + onDuplicateSingle={(object) => this.setState({ - copySelectedSavedObjects: [object], - isShowingCopyModal: true, - copyState: CopyState.Single, + duplicateSelectedSavedObjects: [object], + isShowingDuplicateModal: true, + duplicateMode: DuplicateMode.Selected, }) } onActionRefresh={this.refreshObject} diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index ad5f83fc8238..69c3d858320b 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -37,6 +37,7 @@ import { ISavedObjectsManagementServiceRegistry, SavedObjectsManagementActionServiceStart, SavedObjectsManagementColumnServiceStart, + SavedObjectsManagementNamespaceServiceStart, } from '../services'; import { SavedObjectsTable } from './objects_table'; diff --git a/src/plugins/workspace/public/workspace_client.ts b/src/plugins/workspace/public/workspace_client.ts index 2342494670df..a7b8df5c9bdc 100644 --- a/src/plugins/workspace/public/workspace_client.ts +++ b/src/plugins/workspace/public/workspace_client.ts @@ -113,7 +113,21 @@ export class WorkspaceClient { }); if (result?.success) { - this.workspaces.workspaceList$.next(result.result.workspaces); + const resultWithWritePermission = await this.list({ + perPage: 999, + permissionModes: [WorkspacePermissionMode.LibraryWrite], + }); + if (resultWithWritePermission?.success) { + const workspaceIdsWithWritePermission = resultWithWritePermission.result.workspaces.map( + (workspace: WorkspaceAttribute) => workspace.id + ); + let workspaces = result.result.workspaces; + workspaces = result.result.workspaces.map((workspace: WorkspaceAttribute) => ({ + ...workspace, + libraryReadonly: !workspaceIdsWithWritePermission.includes(workspace.id), + })); + this.workspaces.workspaceList$.next(workspaces); + } } }