diff --git a/changelogs/fragments/8907.yml b/changelogs/fragments/8907.yml new file mode 100644 index 000000000000..c797c5a557c2 --- /dev/null +++ b/changelogs/fragments/8907.yml @@ -0,0 +1,2 @@ +feat: +- Add privacy levels to the workspace ([#8907](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8907)) \ No newline at end of file diff --git a/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.test.tsx b/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.test.tsx index fdc56756dcf2..21ba92fa011e 100644 --- a/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.test.tsx +++ b/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.test.tsx @@ -125,4 +125,14 @@ describe('AddCollaboratorsModal', () => { expect(addCollaboratorsButton).not.toBeDisabled(); }); }); + + it('should show "Invalid Collaborator ID format" for "*" collaborator id', async () => { + render(); + const collaboratorInput = screen.getByLabelText(defaultProps.inputLabel); + fireEvent.change(collaboratorInput, { target: { value: '*' } }); + + expect(screen.queryByText('Invalid Collaborator ID format')).toBeNull(); + fireEvent.click(screen.getByRole('button', { name: 'Add collaborators' })); + expect(screen.getByText('Invalid Collaborator ID format')).toBeInTheDocument(); + }); }); diff --git a/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.tsx b/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.tsx index 07d2d8a90054..76c269802c54 100644 --- a/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.tsx +++ b/src/plugins/workspace/public/components/add_collaborators_modal/add_collaborators_modal.tsx @@ -84,6 +84,29 @@ export const AddCollaboratorsModal = ({ const [isAdding, setIsAdding] = useState(false); const handleAddCollaborators = async () => { + const singleStarUserIds = + permissionType === 'user' + ? validInnerCollaborators.flatMap(({ id, collaboratorId }) => + collaboratorId.trim() === '*' ? id : [] + ) + : []; + if (singleStarUserIds.length > 0) { + setErrors( + singleStarUserIds.reduce( + (previousErrors, id) => ({ + ...previousErrors, + [id]: i18n.translate('workspace.addCollaboratorsModal.errors.invalidUserFormat', { + defaultMessage: 'Invalid {inputLabel} format', + values: { + inputLabel, + }, + }), + }), + {} + ) + ); + return; + } const collaboratorId2IdsMap = validInnerCollaborators.reduce<{ [key: string]: number[]; }>((previousValue, collaborator) => { diff --git a/src/plugins/workspace/public/components/workspace_collaborators/__snapshots__/workspace_collaborators.test.tsx.snap b/src/plugins/workspace/public/components/workspace_collaborators/__snapshots__/workspace_collaborators.test.tsx.snap index ee482aebbac2..4b300c2eb316 100644 --- a/src/plugins/workspace/public/components/workspace_collaborators/__snapshots__/workspace_collaborators.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_collaborators/__snapshots__/workspace_collaborators.test.tsx.snap @@ -9,713 +9,763 @@ Object { class="euiPage euiPage--paddingMedium euiPage--grow" data-test-subj="workspace-collaborators-panel" > -
-
+
+
+

+ Workspace privacy +

+
+
+
+ +
+
+
+
+ Private to collaborators (Only collaborators can access the workspace.) +
+
+
+
+
+
+
-
- +
+ class="euiFormControlLayoutCustomIcon" + > + +
-
-
- + +
-
-
- + +
-
-
-
-
-
+
+
+
-
- + +
+ +
+
-
-
- - - - + + - - - + - - - - - - + + + + - + - + + - + + -
- Access level -
-
+
+ +
+
+
+ +
- + + + - - - + -
-
+ class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + +
+
-
- -
+ - - - - - - - - - - + - - -
-
-
+
+
-
+ class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + +
+
-
-
- + - ID + + ID + - - - + - Type - - - - + Type + + + - Access level + + Access level + - - - + - Actions + + Actions + - -
-
+
-
+ class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + +
+
-
-
-
+
- ID - -
+ ID +
+
+ + admin + +
+
- - admin - - - -
+
+ — +
+
- Type - -
+ Access level +
+
+ Admin +
+
- — - - + Actions + +
+
+
+ +
+
+
+
- Admin - - -
+ ID +
+
+ + foo + +
+
- Actions - -
+ Type +
+
+ — +
+
+ Access level +
+
+ Read and write +
+
+
+ Actions +
+
- + +
- -
+
-
+
- ID - -
+ ID +
+
+ + bar + +
+
- - foo - - - -
- Type -
-
- — -
-
-
- Access level -
-
- Read and write -
-
-
- Actions -
-
+ Type +
-
- -
+ —
- -
-
+
- -
+ Access level
-
-
-
- ID -
-
- - bar - -
-
-
- Type -
-
- — -
-
-
- Access level -
-
- Read only -
-
-
- Actions -
-
+
+ Actions +
+
- + +
- -
-
-
-
-
+ + + + +
+
+
- -
-
-
-
-
+
+
+ + +
    +
  • + +
  • +
+ + +
@@ -733,713 +783,763 @@ Object { class="euiPage euiPage--paddingMedium euiPage--grow" data-test-subj="workspace-collaborators-panel" > -
-
+
+
+
+
+
+

+ Workspace privacy +

+
+
+
+ +
+
+
+ Private to collaborators (Only collaborators can access the workspace.) +
+
+
+
+
-
- +
+ class="euiFormControlLayoutCustomIcon" + > + +
-
-
- + +
-
-
- + +
-
-
-
-
-
+
+
+
-
- + +
+ +
+
-
-
- - - - + + - - + - - - - - - - + + + + -
-
+ class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + +
+
-
- -
- - + - + + - - - + -
-
+ class="euiCheckbox euiCheckbox--inList euiCheckbox--noLabel" + > + +
+
-
- -
- + - + + - + + -
- Actions -
-
+
+ +
+
+
+ +
- - - - - - - + + - - -
-
-
+
+
-
-
-
-
- - - ID - - - - + +
+
+ +
- Type + + ID + - - - + - Access level + + Type + - - - + - Actions + + Access level + - -
+ + + + Actions + + +
-
- ID -
-
+
- - admin - - - -
- Type -
-
- — -
-
-
- Access level -
-
+
+ + admin + +
+
- Admin - - -
+ Type +
+
+ — +
+
- Actions - -
+ Access level +
+
+ Admin +
+
+ Actions +
+
- + +
- -
+
-
- ID -
-
+
- - foo - - - -
- Type -
-
+
+ + foo + +
+
- — - - -
+ Type +
+
+ — +
+
- Access level - -
+ Access level +
+
+ Read and write +
+
- Read and write - - + Actions + +
+
+
+ +
+
+
+
+ ID +
+
-
- -
+ bar +
- -
-
+
- -
+ Type
-
-
-
- ID -
-
- - bar - -
-
-
- Type -
-
- — -
-
-
- Access level -
-
- Read only -
-
-
+
- Actions - -
+ Access level +
+
+ Read only +
+
+ Actions +
+
- + +
- -
-
-
-
-
+ + + + +
+
+
- + +
-
-
- + + + + + +
diff --git a/src/plugins/workspace/public/components/workspace_collaborators/workspace_collaborators.tsx b/src/plugins/workspace/public/components/workspace_collaborators/workspace_collaborators.tsx index 399127a594c8..362ac2111b6e 100644 --- a/src/plugins/workspace/public/components/workspace_collaborators/workspace_collaborators.tsx +++ b/src/plugins/workspace/public/components/workspace_collaborators/workspace_collaborators.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { EuiPage, EuiPanel } from '@elastic/eui'; +import { EuiPage, EuiPanel, EuiSpacer } from '@elastic/eui'; import { i18n } from '@osd/i18n'; import { useObservable } from 'react-use'; @@ -25,6 +25,7 @@ import { } from '../workspace_form'; import { WorkspaceAttributeWithPermission } from '../../../../../core/types'; import { WorkspaceClient } from '../../workspace_client'; +import { WorkspaceCollaboratorPrivacySettingPanel } from '../workspace_form/workspace_collaborator_privacy_setting_panel'; export const WorkspaceCollaborators = () => { const { @@ -116,13 +117,20 @@ export const WorkspaceCollaborators = () => { ]} setMountPoint={application?.setAppRightControls} /> - - + - + + + + +
); }; diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx index 9f6dc00ec9a7..97f273279879 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.test.tsx @@ -521,8 +521,8 @@ describe('WorkspaceCreator', () => { jest.useRealTimers(); }); - it('should redirect to workspace setting collaborators page if permission enabled', async () => { - const { getByTestId } = render(); + it('should redirect to workspace setting collaborators page if jump to collaborators checked', async () => { + const { getByTestId } = render(); const navigateToCollaboratorsMock = jest.fn(); jest .spyOn(workspaceUtilsExports, 'navigateToAppWithinWorkspace') @@ -536,6 +536,7 @@ describe('WorkspaceCreator', () => { fireEvent.input(nameInput, { target: { value: 'test workspace name' }, }); + fireEvent.click(getByTestId('jumpToCollaboratorsCheckbox')); fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); jest.useFakeTimers(); jest.runAllTimers(); @@ -548,4 +549,24 @@ describe('WorkspaceCreator', () => { }); jest.useRealTimers(); }); + + it('should redirect to workspace use case landing page if jump to collaborators not checked', async () => { + const { getByTestId } = render(); + + // Ensure workspace create form rendered + await waitFor(() => { + expect(getByTestId('workspaceForm-bottomBar-createButton')).toBeInTheDocument(); + }); + const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText'); + fireEvent.input(nameInput, { + target: { value: 'test workspace name' }, + }); + fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton')); + jest.useFakeTimers(); + jest.runAllTimers(); + await waitFor(() => { + expect(setHrefSpy).toHaveBeenCalledWith(expect.stringContaining('/app/discover')); + }); + jest.useRealTimers(); + }); }); diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx index b2b219ac7864..551563f4e04b 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator.tsx @@ -58,6 +58,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { navigationUI: NavigationPublicPluginStart['ui']; }>(); const [isFormSubmitting, setIsFormSubmitting] = useState(false); + const [goToCollaborators, setGoToCollaborators] = useState(false); const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled; const { isOnlyAllowEssential, availableUseCases } = useFormAvailableUseCases({ @@ -145,7 +146,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { ?.features[0].id; // Redirect page after one second, leave one second time to show create successful toast. window.setTimeout(() => { - if (isPermissionEnabled) { + if (isPermissionEnabled && goToCollaborators) { navigateToAppWithinWorkspace( { application, http }, newWorkspaceId, @@ -186,6 +187,7 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { isFormSubmitting, availableUseCases, isPermissionEnabled, + goToCollaborators, ] ); @@ -225,6 +227,8 @@ export const WorkspaceCreator = (props: WorkspaceCreatorProps) => { availableUseCases={availableUseCases} defaultValues={defaultWorkspaceFormValues} isSubmitting={isFormSubmitting} + goToCollaborators={goToCollaborators} + onGoToCollaboratorsChange={setGoToCollaborators} /> )} diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx index c6c044f37289..ce0812c82714 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_creator_form.tsx @@ -19,9 +19,12 @@ import { generateRightSidebarScrollProps, RightSidebarScrollField } from './util import { CreatorDetailsPanel } from './creator_details_panel'; import './workspace_creator_form.scss'; +import { WorkspacePrivacySettingPanel } from '../workspace_form/workspace_privacy_setting_panel'; interface WorkspaceCreatorFormProps extends WorkspaceFormProps { isSubmitting: boolean; + goToCollaborators: boolean; + onGoToCollaboratorsChange: (value: boolean) => void; } export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { @@ -42,9 +45,12 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { handleColorChange, handleUseCaseChange, setSelectedDataSourceConnections, + privacyType, + setPrivacyType, } = useWorkspaceForm(props); const isDashboardAdmin = application?.capabilities?.dashboards?.isDashboardAdmin ?? false; + const isPermissionEnabled = !!application?.capabilities.workspaces.permissionEnabled; return ( @@ -110,6 +116,15 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { )} + + {isDashboardAdmin && isPermissionEnabled && ( + + )} diff --git a/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap b/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap index 76acc578aeae..9686f5266f01 100644 --- a/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap +++ b/src/plugins/workspace/public/components/workspace_detail/__snapshots__/workspace_detail.test.tsx.snap @@ -486,6 +486,77 @@ exports[`WorkspaceDetail render workspace detail page normally 1`] = `
+
+
+
+

+ Workspace privacy +

+
+
+ + Manage who can view or edit workspace and assign workspace administrators on the + + page. + +
+
+
+
+
+
+ +
+
+
+
+ Only collaborators can access the workspace. +
+
+
+
+
+
+
diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx index be98fd944939..053812e82282 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail.test.tsx @@ -81,6 +81,7 @@ const deleteFn = jest.fn().mockReturnValue({ const submitFn = jest.fn(); const onAppLeaveFn = jest.fn(); +const navigateToAppFn = jest.fn(); const WorkspaceDetailPage = (props: any) => { const values = props.defaultValues || defaultValues; @@ -97,9 +98,13 @@ const WorkspaceDetailPage = (props: any) => { application: { ...mockCoreStart.application, // applications$: new BehaviorSubject>(PublicAPPInfoMap as any), + navigateToApp: navigateToAppFn, capabilities: { ...mockCoreStart.application.capabilities, dashboards: { isDashboardAdmin: true }, + workspaces: { + permissionEnabled: true, + }, }, }, workspaces: createWorkspacesSetupContractMockWithValue(), @@ -275,4 +280,11 @@ describe('WorkspaceDetail', () => { expect(alertSpy).toBeCalledTimes(0); alertSpy.mockRestore(); }); + + it('should navigate to collaborators page when clicking the collaborators link', async () => { + const { getByText } = render(WorkspaceDetailPage({})); + fireEvent.click(getByText('Collaborators')); + + expect(navigateToAppFn).toHaveBeenCalledWith('workspace_collaborators'); + }); }); diff --git a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_form_content.tsx b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_form_content.tsx index ad797641a0cd..87d6e6f16c36 100644 --- a/src/plugins/workspace/public/components/workspace_detail/workspace_detail_form_content.tsx +++ b/src/plugins/workspace/public/components/workspace_detail/workspace_detail_form_content.tsx @@ -8,8 +8,11 @@ import { EuiCompressedColorPicker, EuiCompressedFormRow, EuiDescribedFormGroup, + EuiText, + EuiLink, } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@osd/i18n/react'; import { useObservable } from 'react-use'; import { detailsName, @@ -18,6 +21,8 @@ import { detailsColorHelpText, detailsDescriptionIntroduction, detailsUseCaseHelpText, + workspacePrivacyTitle, + privacyType2CopyMap, } from '../workspace_form/constants'; import { CoreStart } from '../../../../../core/public'; import { getFirstUseCaseOfFeatureConfigs } from '../../utils'; @@ -27,6 +32,7 @@ import { WorkspaceUseCase as WorkspaceUseCaseObject } from '../../types'; import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; import { WorkspaceNameField } from '../workspace_form/fields/workspace_name_field'; import { WorkspaceDescriptionField } from '../workspace_form/fields/workspace_description_field'; +import { WorkspacePrivacySettingSelect } from '../workspace_form/workspace_privacy_setting_select'; interface WorkspaceDetailFormContentProps { availableUseCases: Array< @@ -45,13 +51,16 @@ export const WorkspaceDetailFormContent = ({ setDescription, handleColorChange, handleUseCaseChange, + privacyType, + setPrivacyType, } = useWorkspaceFormContext(); const { - services: { workspaces }, + services: { workspaces, application }, } = useOpenSearchDashboards(); const [value, setValue] = useState(formData.useCase); const currentWorkspace = useObservable(workspaces.currentWorkspace$); const currentUseCase = getFirstUseCaseOfFeatureConfigs(currentWorkspace?.features ?? []); + const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled; useEffect(() => { setValue(formData.useCase); @@ -138,6 +147,40 @@ export const WorkspaceDetailFormContent = ({ /> + {isPermissionEnabled && ( + {workspacePrivacyTitle}} + description={ + application.navigateToApp('workspace_collaborators')}> + + + ), + }} + /> + } + > + {isEditing ? ( + + ) : ( + + + {privacyType2CopyMap[privacyType].description} + + + )} + + )} ); }; diff --git a/src/plugins/workspace/public/components/workspace_form/constants.ts b/src/plugins/workspace/public/components/workspace_form/constants.ts index 3609facf1566..82977d9cc79a 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -18,6 +18,12 @@ export enum WorkspacePermissionItemType { Group = 'group', } +export enum WorkspacePrivacyItemType { + PrivateToCollaborators = 'privat-to-collaborators', + AnyoneCanView = 'anyone-can-view', + AnyoneCanEdit = 'anyone-can-edit', +} + export const optionIdToWorkspacePermissionModesMap: { [key: string]: WorkspacePermissionMode[]; } = { @@ -102,6 +108,40 @@ export const detailsColorHelpText = i18n.translate( } ); +export const workspacePrivacyTitle = i18n.translate( + 'workspace.form.collaborators.panels.privacy.title', + { + defaultMessage: 'Workspace privacy', + } +); + +export const privacyType2CopyMap = { + [WorkspacePrivacyItemType.PrivateToCollaborators]: { + title: i18n.translate('workspace.privacy.privateToCollaborators.title', { + defaultMessage: 'Private to collaborators', + }), + description: i18n.translate('workspace.privacy.privateToCollaborators.description', { + defaultMessage: 'Only collaborators can access the workspace.', + }), + }, + [WorkspacePrivacyItemType.AnyoneCanView]: { + title: i18n.translate('workspace.privacy.anyoneCanView.title', { + defaultMessage: 'Anyone can view', + }), + description: i18n.translate('workspace.privacy.anyoneCanView.description', { + defaultMessage: 'Anyone can view workspace assets.', + }), + }, + [WorkspacePrivacyItemType.AnyoneCanEdit]: { + title: i18n.translate('workspace.privacy.anyoneCanEdit.title', { + defaultMessage: 'Anyone can edit', + }), + description: i18n.translate('workspace.privacy.anyoneCanEdit.description', { + defaultMessage: 'Anyone can view and edit workspace assets.', + }), + }, +}; + export const PERMISSION_TYPE_LABEL_ID = 'workspace-form-permission-type-label'; export const PERMISSION_COLLABORATOR_LABEL_ID = 'workspace-form-permission-collaborator-label'; export const PERMISSION_ACCESS_LEVEL_LABEL_ID = 'workspace-form-permission-access-level-label'; diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts index 19b9299c52dc..08c6396a8ec1 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.test.ts @@ -7,7 +7,13 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { applicationServiceMock } from '../../../../../core/public/mocks'; import { WorkspacePermissionMode } from '../../../common/constants'; -import { WorkspaceOperationType, WorkspacePermissionItemType } from './constants'; +import { PermissionModeId } from '../../../../../core/public'; +import { + optionIdToWorkspacePermissionModesMap, + WorkspaceOperationType, + WorkspacePermissionItemType, + WorkspacePrivacyItemType, +} from './constants'; import { WorkspaceFormSubmitData, WorkspaceFormErrorCode } from './types'; import { useWorkspaceForm } from './use_workspace_form'; import { waitFor } from '@testing-library/dom'; @@ -195,4 +201,38 @@ describe('useWorkspaceForm', () => { expect(renderResult.result.current.formData.permissionSettings).toStrictEqual([]); }); }); + + it('should return permissions settings after setPrivacyType called', async () => { + const onSubmitMock = jest.fn().mockResolvedValue({ success: true }); + const { renderResult } = setup({ + defaultValues: { + name: 'current-workspace-name', + features: ['use-case-observability'], + }, + onSubmit: onSubmitMock, + }); + act(() => { + renderResult.result.current.setPrivacyType(WorkspacePrivacyItemType.AnyoneCanEdit); + }); + await waitFor(() => { + expect(renderResult.result.current.formData.permissionSettings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'user', + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.ReadAndWrite], + }), + ]) + ); + }); + + const oldPermissionSettings = renderResult.result.current.formData.permissionSettings; + + act(() => { + renderResult.result.current.setPrivacyType(WorkspacePrivacyItemType.AnyoneCanEdit); + }); + await waitFor(() => { + expect(renderResult.result.current.formData.permissionSettings).toBe(oldPermissionSettings); + }); + }); }); diff --git a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts index d9e4a24f3399..978741f867dd 100644 --- a/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts +++ b/src/plugins/workspace/public/components/workspace_form/use_workspace_form.ts @@ -16,7 +16,13 @@ import { WorkspacePermissionSetting, WorkspaceFormDataState, } from './types'; -import { getNumberOfChanges, getNumberOfErrors, validateWorkspaceForm } from './utils'; +import { + convertPermissionsToPrivacyType, + getNumberOfChanges, + getNumberOfErrors, + getPermissionSettingsWithPrivacyType, + validateWorkspaceForm, +} from './utils'; import { WorkspacePermissionItemType } from './constants'; const workspaceHtmlIdGenerator = htmlIdGenerator(); @@ -71,6 +77,10 @@ export const useWorkspaceForm = ({ ? getNumberOfChanges(formData, defaultValuesRef.current) : 0; + const privacyType = useMemo(() => convertPermissionsToPrivacyType(permissionSettings), [ + permissionSettings, + ]); + if (!formIdRef.current) { formIdRef.current = workspaceHtmlIdGenerator(); } @@ -143,6 +153,15 @@ export const useWorkspaceForm = ({ setColor(text); }, []); + const setPrivacyType = useCallback((newPrivacyType) => { + setPermissionSettings((prevPermissionSettings) => { + if (convertPermissionsToPrivacyType(prevPermissionSettings) === newPrivacyType) { + return prevPermissionSettings; + } + return getPermissionSettingsWithPrivacyType(prevPermissionSettings, newPrivacyType); + }); + }, []); + const handleResetForm = useCallback(() => { const resetValues = defaultValuesRef.current; setName(resetValues?.name ?? ''); @@ -158,12 +177,14 @@ export const useWorkspaceForm = ({ formData, isEditing, formErrors, + privacyType, setIsEditing, applications, numberOfErrors, numberOfChanges, handleResetForm, setName, + setPrivacyType, setDescription, handleFormSubmit, handleColorChange, diff --git a/src/plugins/workspace/public/components/workspace_form/utils.test.ts b/src/plugins/workspace/public/components/workspace_form/utils.test.ts index aee0a1e1ca56..13d2e4919dcc 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.test.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.test.ts @@ -11,9 +11,15 @@ import { getNumberOfErrors, isWorkspacePermissionSetting, getPermissionModeName, + getPermissionSettingsWithPrivacyType, + convertPermissionsToPrivacyType, } from './utils'; import { WorkspacePermissionMode } from '../../../common/constants'; -import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap } from './constants'; +import { + WorkspacePermissionItemType, + optionIdToWorkspacePermissionModesMap, + WorkspacePrivacyItemType, +} from './constants'; import { DataSourceConnectionType } from '../../../common/types'; import { WorkspaceFormErrorCode } from './types'; import { PermissionModeId } from '../../../../../core/public'; @@ -553,6 +559,98 @@ describe('getNumberOfChanges', () => { ) ).toEqual(1); }); + it('should return consistent permissions changes count', () => { + expect( + getNumberOfChanges( + { + name: 'foo', + features: ['bar'], + }, + { + name: 'foo', + features: ['bar'], + } + ) + ).toEqual(0); + expect( + getNumberOfChanges( + { + name: 'foo', + permissionSettings: [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ], + }, + { + name: 'foo', + permissionSettings: [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ], + } + ) + ).toEqual(0); + expect( + getNumberOfChanges( + { + name: 'foo', + permissionSettings: [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ], + }, + { + name: 'foo', + permissionSettings: [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.ReadAndWrite], + }, + ], + } + ) + ).toEqual(1); + expect( + getNumberOfChanges( + { + name: 'foo', + permissionSettings: [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ], + }, + { + name: 'foo', + permissionSettings: [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: 'user-id', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.ReadAndWrite], + }, + ], + } + ) + ).toEqual(1); + }); }); describe('isWorkspacePermissionSetting', () => { @@ -645,3 +743,136 @@ describe('getPermissionModeName', () => { expect(result).toBe('Read only'); }); }); + +describe('convertPermissionsToPrivacyType', () => { + it('should return AnyoneCanEdit when LibraryWrite permission is present for *', () => { + const permissionSettings = [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: [WorkspacePermissionMode.LibraryWrite], + }, + ]; + expect(convertPermissionsToPrivacyType(permissionSettings)).toEqual( + WorkspacePrivacyItemType.AnyoneCanEdit + ); + }); + + it('should return AnyoneCanView when LibraryRead permission is present for *', () => { + const permissionSettings = [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: [WorkspacePermissionMode.LibraryRead], + }, + ]; + expect(convertPermissionsToPrivacyType(permissionSettings)).toEqual( + WorkspacePrivacyItemType.AnyoneCanView + ); + }); + + it('should return PrivateToCollaborators when no * permission is present', () => { + const permissionSettings = [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: 'user1', + modes: [WorkspacePermissionMode.LibraryRead], + }, + ]; + expect(convertPermissionsToPrivacyType(permissionSettings)).toEqual( + WorkspacePrivacyItemType.PrivateToCollaborators + ); + }); +}); + +describe('getPermissionSettingsWithPrivacyType', () => { + it('should update star user to read permission when privacyType is AnyoneCanView', () => { + const expectedPermissionSettings = [ + { + id: 1, + type: WorkspacePermissionItemType.User, + userId: 'user1', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + { + id: 3, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ]; + expect( + getPermissionSettingsWithPrivacyType( + [ + expectedPermissionSettings[0], + { + id: 2, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.ReadAndWrite], + }, + ], + WorkspacePrivacyItemType.AnyoneCanView + ) + ).toEqual(expectedPermissionSettings); + }); + + it('should update star user to read and write permission when privacyType is AnyoneCanEdit', () => { + const expectedPermissionSettings = [ + { + id: 1, + type: WorkspacePermissionItemType.User, + userId: 'user1', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + { + id: 3, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.ReadAndWrite], + }, + ]; + expect( + getPermissionSettingsWithPrivacyType( + [ + expectedPermissionSettings[0], + { + id: 2, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ], + WorkspacePrivacyItemType.AnyoneCanEdit + ) + ).toEqual(expectedPermissionSettings); + }); + + it('should remove * permission when privacyType is PrivateToCollaborators', () => { + const expectedPermissionSettings = [ + { + id: 1, + type: WorkspacePermissionItemType.User, + userId: 'user1', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ]; + expect( + getPermissionSettingsWithPrivacyType( + [ + expectedPermissionSettings[0], + { + id: 2, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: optionIdToWorkspacePermissionModesMap[PermissionModeId.Read], + }, + ], + WorkspacePrivacyItemType.PrivateToCollaborators + ) + ).toEqual(expectedPermissionSettings); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/utils.ts b/src/plugins/workspace/public/components/workspace_form/utils.ts index 1233bfbc0d53..94ac3e79a203 100644 --- a/src/plugins/workspace/public/components/workspace_form/utils.ts +++ b/src/plugins/workspace/public/components/workspace_form/utils.ts @@ -12,6 +12,7 @@ import { optionIdToWorkspacePermissionModesMap, permissionModeOptions, WorkspacePermissionItemType, + WorkspacePrivacyItemType, } from './constants'; import { @@ -386,16 +387,6 @@ interface PermissionSettingLike Omit, 'type'> { type?: string; } -const isSamePermissionSetting = (a: PermissionSettingLike, b: PermissionSettingLike) => { - return ( - a.id === b.id && - a.type === b.type && - a.userId === b.userId && - a.group === b.group && - a.modes?.length === b.modes?.length && - a.modes?.every((mode) => b.modes?.includes(mode)) - ); -}; export const isWorkspacePermissionSetting = ( permissionSetting: PermissionSettingLike @@ -446,5 +437,53 @@ export const getNumberOfChanges = ( ) { count++; } + if ( + convertPermissionsToPrivacyType(newFormData.permissionSettings ?? []) !== + convertPermissionsToPrivacyType(initialFormData.permissionSettings ?? []) + ) { + count++; + } return count; }; + +export const convertPermissionsToPrivacyType = ( + permissionSettings: WorkspaceFormDataState['permissionSettings'] +) => { + const modes = permissionSettings.find( + (item) => item.type === WorkspacePermissionItemType.User && item.userId === '*' + )?.modes; + if (modes?.includes(WorkspacePermissionMode.LibraryWrite)) { + return WorkspacePrivacyItemType.AnyoneCanEdit; + } + if (modes?.includes(WorkspacePermissionMode.LibraryRead)) { + return WorkspacePrivacyItemType.AnyoneCanView; + } + return WorkspacePrivacyItemType.PrivateToCollaborators; +}; + +export const getPermissionSettingsWithPrivacyType = ( + permissionSettings: WorkspaceFormDataState['permissionSettings'], + privacyType: WorkspacePrivacyItemType +) => { + const newSettings = permissionSettings.filter( + (item) => !(item.type === WorkspacePermissionItemType.User && item.userId === '*') + ); + + if ( + privacyType === WorkspacePrivacyItemType.AnyoneCanView || + privacyType === WorkspacePrivacyItemType.AnyoneCanEdit + ) { + newSettings.push({ + id: generateNextPermissionSettingsId(permissionSettings), + type: WorkspacePermissionItemType.User, + userId: '*', + modes: + optionIdToWorkspacePermissionModesMap[ + privacyType === WorkspacePrivacyItemType.AnyoneCanView + ? PermissionModeId.Read + : PermissionModeId.ReadAndWrite + ], + }); + } + return newSettings; +}; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.test.tsx new file mode 100644 index 000000000000..6f3cd4c04f22 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.test.tsx @@ -0,0 +1,185 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { + privacyType2CopyMap, + WorkspacePermissionItemType, + WorkspacePrivacyItemType, +} from './constants'; +import { WorkspacePermissionMode } from '../../../common/constants'; +import { + WorkspaceCollaboratorPrivacySettingPanel, + WorkspaceCollaboratorPrivacySettingProps, +} from './workspace_collaborator_privacy_setting_panel'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; + +const permissionSettingsView = [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: 'foo', + modes: [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.Read], + }, + { + id: 1, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.Read], + }, +]; +const permissionSettingsEdit = [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: 'foo', + modes: [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.Read], + }, + { + id: 1, + type: WorkspacePermissionItemType.User, + userId: '*', + modes: [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Read], + }, +]; + +jest.mock('../../../../opensearch_dashboards_react/public', () => { + return { + useOpenSearchDashboards: jest.fn().mockReturnValue({ + services: { + notifications: { + toasts: { + addError: jest.fn(), + addSuccess: jest.fn(), + }, + }, + }, + }), + }; +}); +const setup = (options?: Partial) => { + const handleSubmitPermissionSettingsMock = jest.fn(); + const permissionSettings = [ + { + id: 0, + type: WorkspacePermissionItemType.User, + userId: 'foo', + modes: [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.Read], + }, + ]; + const renderResult = render( + + ); + return { + renderResult, + handleSubmitPermissionSettingsMock, + }; +}; + +describe('WorkspaceCollaboratorPrivacySettingPanel', () => { + it('should render correct privacyType with permission settings', () => { + const { renderResult: privateWorkspace } = setup(); + expect( + privateWorkspace.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.PrivateToCollaborators].title, + { exact: false } + ) + ).toBeInTheDocument(); + + const { renderResult: anyoneCanViewWorkspace } = setup({ + permissionSettings: permissionSettingsView, + }); + expect( + anyoneCanViewWorkspace.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.AnyoneCanView].title, + { exact: false } + ) + ).toBeInTheDocument(); + + const { renderResult: anyoneCanEditWorkspace } = setup({ + permissionSettings: permissionSettingsEdit, + }); + expect( + anyoneCanEditWorkspace.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.AnyoneCanEdit].title, + { exact: false } + ) + ).toBeInTheDocument(); + }); + + it('should call handleSubmitPermissionSettings with new privacy type', () => { + const { renderResult, handleSubmitPermissionSettingsMock } = setup(); + + expect(handleSubmitPermissionSettingsMock).not.toHaveBeenCalled(); + fireEvent.click(renderResult.getByText('Edit')); + fireEvent.click( + renderResult.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.PrivateToCollaborators].title + ) + ); + fireEvent.click( + renderResult.getByText(privacyType2CopyMap[WorkspacePrivacyItemType.AnyoneCanView].title) + ); + fireEvent.click(renderResult.getByText('Save changes')); + expect(handleSubmitPermissionSettingsMock).toHaveBeenCalledWith(permissionSettingsView); + }); + + it('should call addSuccess when successfully update the privacy type', async () => { + const mockHandleSubmitPermissionSettings = jest.fn(); + mockHandleSubmitPermissionSettings.mockResolvedValue({ success: true }); + const { renderResult } = setup({ + handleSubmitPermissionSettings: mockHandleSubmitPermissionSettings, + }); + + expect(mockHandleSubmitPermissionSettings).not.toHaveBeenCalled(); + fireEvent.click(renderResult.getByText('Edit')); + fireEvent.click( + renderResult.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.PrivateToCollaborators].title + ) + ); + fireEvent.click( + renderResult.getByText(privacyType2CopyMap[WorkspacePrivacyItemType.AnyoneCanEdit].title) + ); + fireEvent.click(renderResult.getByText('Save changes')); + const addSuccessMock = useOpenSearchDashboards().services.notifications?.toasts.addSuccess; + await waitFor(() => { + expect(addSuccessMock).toHaveBeenCalledWith({ + title: 'Change workspace privacy successfully.', + }); + }); + }); + + it('should call addError when error update the privacy type', async () => { + const mockHandleSubmitPermissionSettings = jest.fn(); + mockHandleSubmitPermissionSettings.mockRejectedValue(new Error('Something went wrong')); + const { renderResult } = setup({ + handleSubmitPermissionSettings: mockHandleSubmitPermissionSettings, + }); + + expect(mockHandleSubmitPermissionSettings).not.toHaveBeenCalled(); + fireEvent.click(renderResult.getByText('Edit')); + fireEvent.click( + renderResult.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.PrivateToCollaborators].title + ) + ); + fireEvent.click( + renderResult.getByText(privacyType2CopyMap[WorkspacePrivacyItemType.AnyoneCanEdit].title) + ); + fireEvent.click(renderResult.getByText('Save changes')); + const addErrorMock = useOpenSearchDashboards().services.notifications?.toasts.addError; + await waitFor(() => { + expect(addErrorMock).toHaveBeenCalledWith(expect.any(Error), { + title: 'Error updating workspace privacy type', + }); + }); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.tsx new file mode 100644 index 000000000000..9dda4301572f --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.tsx @@ -0,0 +1,148 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState, useMemo } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiPanel, + EuiSmallButton, + EuiSmallButtonEmpty, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import _ from 'lodash'; +import { i18n } from '@osd/i18n'; +import { WorkspacePrivacyItemType, privacyType2CopyMap, workspacePrivacyTitle } from './constants'; +import { WorkspacePermissionSetting } from './types'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { CoreStart, IWorkspaceResponse } from '../../../../../core/public'; +import { convertPermissionsToPrivacyType, getPermissionSettingsWithPrivacyType } from './utils'; +import { WorkspacePrivacySettingSelect } from './workspace_privacy_setting_select'; + +export interface WorkspaceCollaboratorPrivacySettingProps { + permissionSettings: Array< + Pick & Partial + >; + handleSubmitPermissionSettings: ( + permissionSettings: WorkspacePermissionSetting[] + ) => Promise>; +} + +export const WorkspaceCollaboratorPrivacySettingPanel = ({ + permissionSettings, + handleSubmitPermissionSettings, +}: WorkspaceCollaboratorPrivacySettingProps) => { + const { + services: { notifications }, + } = useOpenSearchDashboards<{ + CoreStart: CoreStart; + }>(); + + const [isOpen, setIsOpen] = useState(false); + const [selectedPrivacyType, setSelectedPrivacyType] = useState( + WorkspacePrivacyItemType.PrivateToCollaborators + ); + + const privacyType = useMemo(() => convertPermissionsToPrivacyType(permissionSettings), [ + permissionSettings, + ]); + + const handleModalOpen = () => { + setSelectedPrivacyType(privacyType); + setIsOpen(true); + }; + + const handleChange = async () => { + let result; + try { + result = await handleSubmitPermissionSettings( + getPermissionSettingsWithPrivacyType( + permissionSettings, + selectedPrivacyType + ) as WorkspacePermissionSetting[] + ); + } catch (error) { + notifications?.toasts?.addError(error, { + title: i18n.translate('workspace.collaborator.changePrivacyType.failed.message', { + defaultMessage: `Error updating workspace privacy type`, + }), + }); + return; + } + if (result?.success) { + notifications?.toasts?.addSuccess({ + title: i18n.translate('workspace.collaborator.changePrivacyType.success.message', { + defaultMessage: `Change workspace privacy successfully.`, + }), + }); + } + setIsOpen(false); + }; + + return ( + + + + +

{workspacePrivacyTitle}

+
+
+ + + {i18n.translate('workspace.form.collaborators.panels.privacy.edit', { + defaultMessage: 'Edit', + })} + + +
+ + + {i18n.translate('workspace.form.collaborators.panels.privacy.description', { + defaultMessage: '{title} ({description})', + values: { + title: privacyType2CopyMap[privacyType].title, + description: privacyType2CopyMap[privacyType].description, + }, + })} + + {isOpen && ( + setIsOpen(false)}> + + {workspacePrivacyTitle} + + + + + + setIsOpen(false)}> + {i18n.translate('workspace.form.collaborators.panels.privacy.modal.cancel', { + defaultMessage: 'Cancel', + })} + + + {i18n.translate('workspace.form.collaborators.panels.privacy.modal.save', { + defaultMessage: 'Save changes', + })} + + + + )} +
+ ); +}; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx index fad09555b312..bde614b6c409 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_table.tsx @@ -164,35 +164,37 @@ export const WorkspaceCollaboratorTable = ({ } = useOpenSearchDashboards(); const items: PermissionSettingWithAccessLevelAndDisplayedType[] = useMemo(() => { - return permissionSettings.map((setting) => { - const collaborator = isWorkspacePermissionSetting(setting) - ? convertPermissionSettingToWorkspaceCollaborator(setting) - : undefined; - const basicSettings = { - ...setting, - // This is used for table display and search match. - displayedType: collaborator - ? getDisplayedType(displayedCollaboratorTypes, collaborator) - : undefined, - accessLevel: collaborator - ? WORKSPACE_ACCESS_LEVEL_NAMES[collaborator.accessLevel] - : undefined, - }; - // Unique primary key and filter null value - if (setting.type === WorkspacePermissionItemType.User) { - return { - ...basicSettings, - // Id represents the index of the permission setting in the array, will use primaryId for displayed id - primaryId: setting.userId, + return permissionSettings + .map((setting) => { + const collaborator = isWorkspacePermissionSetting(setting) + ? convertPermissionSettingToWorkspaceCollaborator(setting) + : undefined; + const basicSettings = { + ...setting, + // This is used for table display and search match. + displayedType: collaborator + ? getDisplayedType(displayedCollaboratorTypes, collaborator) + : undefined, + accessLevel: collaborator + ? WORKSPACE_ACCESS_LEVEL_NAMES[collaborator.accessLevel] + : undefined, }; - } else if (setting.type === WorkspacePermissionItemType.Group) { - return { - ...basicSettings, - primaryId: setting.group, - }; - } - return basicSettings; - }); + // Unique primary key and filter null value + if (setting.type === WorkspacePermissionItemType.User) { + return { + ...basicSettings, + // Id represents the index of the permission setting in the array, will use primaryId for displayed id + primaryId: setting.userId, + }; + } else if (setting.type === WorkspacePermissionItemType.Group) { + return { + ...basicSettings, + primaryId: setting.group, + }; + } + return basicSettings; + }) + .filter((item) => !(item.type === WorkspacePermissionItemType.User && item.userId === '*')); }, [permissionSettings, displayedCollaboratorTypes]); const adminCollaboratorsNum = useMemo(() => { diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx index 45d77f7aef5f..f07c0c7cfb30 100644 --- a/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx +++ b/src/plugins/workspace/public/components/workspace_form/workspace_form_context.tsx @@ -11,6 +11,7 @@ import { AppMountParameters, PublicAppInfo } from '../../../../../core/public'; import { useWorkspaceForm } from './use_workspace_form'; import { WorkspaceFormDataState } from '../workspace_form'; import { WorkspacePermissionSetting } from './types'; +import { WorkspacePrivacyItemType } from './constants'; interface WorkspaceFormContextProps { formId: string; @@ -35,6 +36,8 @@ interface WorkspaceFormContextProps { handleSubmitPermissionSettings: ( permissionSettings: WorkspacePermissionSetting[] ) => Promise; + privacyType: WorkspacePrivacyItemType; + setPrivacyType: (newPrivacyType: WorkspacePrivacyItemType) => void; } const initialContextValue: WorkspaceFormContextProps = {} as WorkspaceFormContextProps; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting.scss b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting.scss new file mode 100644 index 000000000000..5c969feeefe8 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.workspace-privacy-setting-item { + height: 100%; +} diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.test.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.test.tsx new file mode 100644 index 000000000000..933a1f17c925 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { privacyType2CopyMap, WorkspacePrivacyItemType } from './constants'; +import { + WorkspacePrivacySettingPanel, + WorkspacePrivacySettingProps, +} from './workspace_privacy_setting_panel'; + +const setup = (options?: Partial) => { + const onPrivacyTypeChangeMock = jest.fn(); + const onGoToCollaboratorsChangeMock = jest.fn(); + const renderResult = render( + + ); + return { + renderResult, + onPrivacyTypeChangeMock, + onGoToCollaboratorsChangeMock, + }; +}; + +describe('WorkspaceCollaboratorPrivacySettingPanel', () => { + it('should show private to collaborators as default', () => { + const { renderResult } = setup(); + expect( + renderResult.getByText( + privacyType2CopyMap[WorkspacePrivacyItemType.PrivateToCollaborators].title, + { exact: false } + ) + ).toBeInTheDocument(); + + expect( + renderResult + .getByText(privacyType2CopyMap[WorkspacePrivacyItemType.PrivateToCollaborators].title, { + exact: false, + }) + .closest('.euiCheckableCard') + ).toHaveClass('euiCheckableCard-isChecked'); + }); + + it('should call onPrivacyTypeChange when choosing a new privacy type', async () => { + const { renderResult, onPrivacyTypeChangeMock } = setup(); + + expect(onPrivacyTypeChangeMock).not.toHaveBeenCalled(); + const anyOneCanViewCard = renderResult.getAllByTestId('workspace-privacyType-Card')[1]; + + fireEvent.click(anyOneCanViewCard); + await waitFor(() => { + expect(onPrivacyTypeChangeMock).toHaveBeenCalledWith(WorkspacePrivacyItemType.AnyoneCanView); + }); + }); + + it('should call onGoToCollaboratorsChange with the checkbox checked on', async () => { + const { renderResult, onGoToCollaboratorsChangeMock } = setup(); + + expect(onGoToCollaboratorsChangeMock).not.toHaveBeenCalled(); + fireEvent.click(renderResult.getByTestId('jumpToCollaboratorsCheckbox')); + await waitFor(() => { + expect(onGoToCollaboratorsChangeMock).toHaveBeenCalledWith(true); + }); + }); +}); diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.tsx new file mode 100644 index 000000000000..9724734058a8 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.tsx @@ -0,0 +1,100 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { + EuiCheckableCard, + EuiCheckbox, + EuiCompressedFormRow, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSpacer, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; +import { i18n } from '@osd/i18n'; +import { privacyType2CopyMap, WorkspacePrivacyItemType, workspacePrivacyTitle } from './constants'; +import './workspace_privacy_setting.scss'; + +export interface WorkspacePrivacySettingProps { + privacyType: WorkspacePrivacyItemType; + onPrivacyTypeChange: (newPrivacyType: WorkspacePrivacyItemType) => void; + goToCollaborators: boolean; + onGoToCollaboratorsChange: (value: boolean) => void; +} + +export const WorkspacePrivacySettingPanel = ({ + privacyType, + onPrivacyTypeChange, + goToCollaborators, + onGoToCollaboratorsChange, +}: WorkspacePrivacySettingProps) => { + const options = [ + WorkspacePrivacyItemType.PrivateToCollaborators, + WorkspacePrivacyItemType.AnyoneCanView, + WorkspacePrivacyItemType.AnyoneCanEdit, + ].map((value) => ({ + id: value, + label: privacyType2CopyMap[value].title, + description: privacyType2CopyMap[value].description, + })); + + return ( + + +

+ {i18n.translate('workspace.form.panels.privacy.title', { + defaultMessage: 'Set up privacy', + })} +

+
+ + {i18n.translate('workspace.form.panels.privacy.description', { + defaultMessage: 'Who has access to the workspace', + })} + + + + + {options.map(({ id, label, description }) => ( + + +

{label}

+ + } + onChange={() => onPrivacyTypeChange(id)} + checked={privacyType === id} + > + {description} +
+
+ ))} +
+
+ + + {i18n.translate('workspace.form.panels.privacy.jumpToCollaborators.description', { + defaultMessage: 'You can assign workspace administrators once the workspace is created.', + })} + + + onGoToCollaboratorsChange(event.target.checked)} + label={i18n.translate('workspace.form.panels.privacy.jumpToCollaborators.label', { + defaultMessage: 'Go to configure the collaborators right after creating the workspace.', + })} + data-test-subj="jumpToCollaboratorsCheckbox" + /> +
+ ); +}; diff --git a/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_select.tsx b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_select.tsx new file mode 100644 index 000000000000..afbc7a6b6561 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_select.tsx @@ -0,0 +1,53 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { EuiCompressedSuperSelect, EuiFormRow, EuiSpacer, EuiText } from '@elastic/eui'; +import { privacyType2CopyMap, WorkspacePrivacyItemType, workspacePrivacyTitle } from './constants'; + +export interface WorkspacePrivacySettingSelectProps { + selectedPrivacyType: WorkspacePrivacyItemType; + onSelectedPrivacyTypeChange: (newType: WorkspacePrivacyItemType) => void; +} + +export const WorkspacePrivacySettingSelect = ({ + selectedPrivacyType, + onSelectedPrivacyTypeChange, +}: WorkspacePrivacySettingSelectProps) => { + const options = [ + WorkspacePrivacyItemType.PrivateToCollaborators, + WorkspacePrivacyItemType.AnyoneCanView, + WorkspacePrivacyItemType.AnyoneCanEdit, + ].map((value) => ({ + value, + inputDisplay: privacyType2CopyMap[value].title, + dropdownDisplay: ( + <> + {privacyType2CopyMap[value].title} + + + {privacyType2CopyMap[value].description} + + + ), + })); + return ( + <> + + onSelectedPrivacyTypeChange(value)} + data-test-subj="workspacePrivacySettingSelector" + /> + + + {privacyType2CopyMap[selectedPrivacyType].description} + + + ); +};