From 37f15363bc151661fafa411cda60018e167befaa Mon Sep 17 00:00:00 2001 From: Jincheng Wan <45655760+Kapian1234@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:11:12 +0800 Subject: [PATCH] [Workspace] Add privacy levels to the workspace (#8907) * Add privacy selector UI at create page Signed-off-by: Kapian1234 * Add privacy settings UI at collaborstors page Signed-off-by: Kapian1234 * Changeset file for PR #8907 created/updated * Add privacy settings panel at collaborstors page Signed-off-by: Kapian1234 * Fix the height of cards at workspace create page Signed-off-by: Kapian1234 * Refactor workspace creator form with privacyType and setPrivacyType Signed-off-by: Lin Wang * Resolve some issues Signed-off-by: Kapian1234 * Add privacy settings control at details page Signed-off-by: Kapian1234 * Add single star user check for add collaborators modal Signed-off-by: Lin Wang * Disable save button if the selected privay type remains unchanged at collaborators page Signed-off-by: Kapian1234 * Fix workspace creator UT Signed-off-by: Lin Wang * Fix failed snapshots Signed-off-by: Lin Wang * Add missing UT for workspace form utils Signed-off-by: Lin Wang * Update UT for useWorkspaceForm Signed-off-by: Lin Wang * Add unit tests for privacy settings at workspace create and details Signed-off-by: Kapian1234 * / Signed-off-by: Kapian1234 * Fix redirect to workspace landing page Signed-off-by: Lin Wang * Remove changes to configuration files Signed-off-by: Kapian1234 * Remove changes to configuration files Signed-off-by: Kapian1234 * Add unit test for notification toasts Signed-off-by: Kapian1234 * Add test id for privacy setting selector Signed-off-by: Lin Wang * Add unit test for Collaborators Link at workspace details Signed-off-by: Kapian1234 * Use constant for collaborators link Signed-off-by: Kapian1234 * Fix issues of spelling and importing path Signed-off-by: Kapian1234 * Remove the validation for the existence of an owner in permission settings Signed-off-by: Kapian1234 * Add test id for privacy setting button in collaborators page Signed-off-by: Lin Wang * Fix the discard operation Signed-off-by: Kapian1234 * Add workspace privacy to summary card Signed-off-by: Kapian1234 * Add additional privacy description at workspace create page Signed-off-by: Kapian1234 * Remove unit tests related to missing owner in permission settings Signed-off-by: Kapian1234 * Add permissionEnabled check for summary card Signed-off-by: Kapian1234 * Disallow * input for user groups Signed-off-by: Kapian1234 * Rename the converter and move options outside the function Signed-off-by: Kapian1234 * Add learn more flyout at collaborators page Signed-off-by: Kapian1234 * Fix unit tests and update snapshots Signed-off-by: Kapian1234 * Remove the scss file for WorkspacePrivacyFlyout Signed-off-by: Kapian1234 * Address some issues Signed-off-by: Kapian1234 --------- Signed-off-by: Kapian1234 Signed-off-by: Lin Wang Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com> Co-authored-by: Lin Wang Co-authored-by: SuZhou-Joe --- changelogs/fragments/8907.yml | 2 + .../public/doc_links/doc_links_service.ts | 4 + .../advanced_settings.test.tsx.snap | 24 + .../dashboard_top_nav.test.tsx.snap | 18 + .../language_selector.test.tsx.snap | 6 + .../add_collaborators_modal.test.tsx | 10 + .../add_collaborators_modal.tsx | 20 + .../workspace_collaborators.test.tsx.snap | 2364 +++++++++-------- .../workspace_collaborators.tsx | 19 +- .../components/workspace_creator/utils.ts | 1 + .../workspace_creator.test.tsx | 25 +- .../workspace_creator/workspace_creator.tsx | 6 +- .../workspace_creator_form.tsx | 16 + .../workspace_form_summary_panel.tsx | 16 + .../workspace_detail.test.tsx.snap | 71 + .../workspace_detail.test.tsx | 12 + .../workspace_detail_form_content.tsx | 48 +- .../components/workspace_form/constants.ts | 54 + .../workspace_form/use_workspace_form.test.ts | 41 +- .../workspace_form/use_workspace_form.ts | 24 +- .../components/workspace_form/utils.test.ts | 233 +- .../public/components/workspace_form/utils.ts | 49 + ...ollaborator_privacy_setting_panel.test.tsx | 201 ++ ...ace_collaborator_privacy_setting_panel.tsx | 155 ++ .../workspace_collaborator_table.tsx | 58 +- .../workspace_form/workspace_form_context.tsx | 3 + .../workspace_privacy_setting.scss | 8 + .../workspace_privacy_setting_panel.test.tsx | 73 + .../workspace_privacy_setting_panel.tsx | 100 + .../workspace_privacy_setting_select.tsx | 53 + 30 files changed, 2542 insertions(+), 1172 deletions(-) create mode 100644 changelogs/fragments/8907.yml create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.test.tsx create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.tsx create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting.scss create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.test.tsx create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_panel.tsx create mode 100644 src/plugins/workspace/public/components/workspace_form/workspace_privacy_setting_select.tsx 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/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 7994ce0212b6..ac299977d911 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -436,6 +436,10 @@ export class DocLinksService { // https://opensearch.org/docs/latest/dashboards/management/advanced-settings/ advancedSettings: `${OPENSEARCH_DASHBOARDS_VERSIONED_DOCS}management/advanced-settings/`, }, + workspace: { + // https://opensearch.org/docs/latest/dashboards/workspace/workspace-acl/ + acl: `${OPENSEARCH_DASHBOARDS_VERSIONED_DOCS}workspace/workspace-acl/`, + }, }, noDocumentation: { auditbeat: `${OPENSEARCH_WEBSITE_DOCS}tools/index/#downloads`, diff --git a/src/plugins/advanced_settings/public/management_app/__snapshots__/advanced_settings.test.tsx.snap b/src/plugins/advanced_settings/public/management_app/__snapshots__/advanced_settings.test.tsx.snap index 62701d05d2c8..2bfa367a4bff 100644 --- a/src/plugins/advanced_settings/public/management_app/__snapshots__/advanced_settings.test.tsx.snap +++ b/src/plugins/advanced_settings/public/management_app/__snapshots__/advanced_settings.test.tsx.snap @@ -418,6 +418,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -1073,6 +1076,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -2007,6 +2013,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -2419,6 +2428,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -2831,6 +2843,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -3243,6 +3258,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -3710,6 +3728,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } @@ -4124,6 +4145,9 @@ exports[`AdvancedSettings should render normally when use updated UX 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, } } diff --git a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap index 362c190ec6af..3bf47018e304 100644 --- a/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap +++ b/src/plugins/dashboard/public/application/components/dashboard_top_nav/__snapshots__/dashboard_top_nav.test.tsx.snap @@ -795,6 +795,9 @@ exports[`Dashboard top nav render in embed mode 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, @@ -1877,6 +1880,9 @@ exports[`Dashboard top nav render in embed mode, and force hide filter bar 1`] = "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, @@ -2959,6 +2965,9 @@ exports[`Dashboard top nav render in embed mode, components can be forced show b "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, @@ -4041,6 +4050,9 @@ exports[`Dashboard top nav render in full screen mode with appended URL param bu "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, @@ -5123,6 +5135,9 @@ exports[`Dashboard top nav render in full screen mode, no componenets should be "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, @@ -6205,6 +6220,9 @@ exports[`Dashboard top nav render with all components 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, diff --git a/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap b/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap index b80a10c1fb85..be80b5be7182 100644 --- a/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap +++ b/src/plugins/data/public/ui/query_editor/__snapshots__/language_selector.test.tsx.snap @@ -561,6 +561,9 @@ exports[`LanguageSelector should select DQL if language is kuery 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, @@ -1231,6 +1234,9 @@ exports[`LanguageSelector should select lucene if language is lucene 1`] = ` "visualize": Object { "guide": "https://opensearch.org/docs/mocked-test-branch/dashboards/visualize/viz-index/", }, + "workspace": Object { + "acl": "https://opensearch.org/docs/mocked-test-branch/dashboards/workspace/workspace-acl/", + }, }, }, }, 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..86125bec76cd 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,26 @@ export const AddCollaboratorsModal = ({ const [isAdding, setIsAdding] = useState(false); const handleAddCollaborators = async () => { + const singleStarIds = validInnerCollaborators.flatMap(({ id, collaboratorId }) => + collaboratorId.trim() === '*' ? id : [] + ); + if (singleStarIds.length > 0) { + setErrors( + singleStarIds.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..0dee2f423b84 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,764 @@ 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 +784,764 @@ 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..7c06ae1a654d 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 { @@ -42,7 +43,6 @@ export const WorkspaceCollaborators = () => { collaboratorTypes: WorkspaceCollaboratorTypesService; workspaceClient: WorkspaceClient; }>(); - const displayedCollaboratorTypes = useObservable(collaboratorTypes.getTypes$()) ?? []; const currentWorkspace = useObservable( @@ -116,13 +116,20 @@ export const WorkspaceCollaborators = () => { ]} setMountPoint={application?.setAppRightControls} /> - - + - + + + + +
); }; diff --git a/src/plugins/workspace/public/components/workspace_creator/utils.ts b/src/plugins/workspace/public/components/workspace_creator/utils.ts index a88076aa8aee..675895b32c9f 100644 --- a/src/plugins/workspace/public/components/workspace_creator/utils.ts +++ b/src/plugins/workspace/public/components/workspace_creator/utils.ts @@ -12,6 +12,7 @@ export enum RightSidebarScrollField { UseCase = 'useCase', DataSource = 'dataSource', Collaborators = 'collaborators', + PrivacyType = 'privacyType', } export const generateRightSidebarScrollProps = (key: RightSidebarScrollField) => { 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..6e92f40fca26 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 && ( + + )} @@ -122,6 +137,7 @@ export const WorkspaceCreatorForm = (props: WorkspaceCreatorFormProps) => { application={application} isSubmitting={props.isSubmitting} dataSourceEnabled={!!isDataSourceEnabled} + privacyType={privacyType} />
diff --git a/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.tsx b/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.tsx index 2caad01a77f0..83781c183f9c 100644 --- a/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.tsx +++ b/src/plugins/workspace/public/components/workspace_creator/workspace_form_summary_panel.tsx @@ -21,6 +21,7 @@ import { WorkspaceFormDataState } from '../workspace_form'; import { WorkspaceUseCase } from '../../types'; import { RightSidebarScrollField, RIGHT_SIDEBAR_SCROLL_KEY } from './utils'; import { WorkspaceCreateActionPanel } from './workspace_create_action_panel'; +import { privacyType2TextMap, WorkspacePrivacyItemType } from '../workspace_form/constants'; const SCROLL_FIELDS = { [RightSidebarScrollField.Name]: i18n.translate('workspace.form.summary.panel.name.title', { @@ -50,6 +51,12 @@ const SCROLL_FIELDS = { defaultMessage: 'Collaborators', } ), + [RightSidebarScrollField.PrivacyType]: i18n.translate( + 'workspace.form.summary.panel.privacyType.title', + { + defaultMessage: 'Workspace privacy', + } + ), }; export const FieldSummaryItem = ({ @@ -138,6 +145,7 @@ interface WorkspaceFormSummaryPanelProps { application: ApplicationStart; isSubmitting: boolean; dataSourceEnabled: boolean; + privacyType: WorkspacePrivacyItemType; } export const WorkspaceFormSummaryPanel = ({ @@ -147,9 +155,11 @@ export const WorkspaceFormSummaryPanel = ({ application, isSubmitting, dataSourceEnabled, + privacyType, }: WorkspaceFormSummaryPanelProps) => { const useCase = availableUseCases.find((item) => item.id === formData.useCase); const useCaseIcon = useCase?.icon || 'logoOpenSearch'; + const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled; return ( )} + {isPermissionEnabled && ( + + {privacyType && {privacyType2TextMap[privacyType].title}} + + )} +
+
+
+
+

+ 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..18b354b18e62 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,9 +8,13 @@ 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 { WORKSPACE_COLLABORATORS_APP_ID } from '../../../common/constants'; import { detailsName, detailsColorLabel, @@ -18,6 +22,8 @@ import { detailsColorHelpText, detailsDescriptionIntroduction, detailsUseCaseHelpText, + workspacePrivacyTitle, + privacyType2TextMap, } from '../workspace_form/constants'; import { CoreStart } from '../../../../../core/public'; import { getFirstUseCaseOfFeatureConfigs } from '../../utils'; @@ -27,6 +33,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 +52,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 +148,42 @@ export const WorkspaceDetailFormContent = ({ /> + {isPermissionEnabled && ( + {workspacePrivacyTitle}} + description={ + application.navigateToApp(WORKSPACE_COLLABORATORS_APP_ID)} + > + + + ), + }} + /> + } + > + {isEditing ? ( + + ) : ( + + + {privacyType2TextMap[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 e6dfa705d6a2..c914515a082a 100644 --- a/src/plugins/workspace/public/components/workspace_form/constants.ts +++ b/src/plugins/workspace/public/components/workspace_form/constants.ts @@ -17,6 +17,12 @@ export enum WorkspacePermissionItemType { Group = 'group', } +export enum WorkspacePrivacyItemType { + PrivateToCollaborators = 'private-to-collaborators', + AnyoneCanView = 'anyone-can-view', + AnyoneCanEdit = 'anyone-can-edit', +} + export const optionIdToWorkspacePermissionModesMap: { [key: string]: WorkspacePermissionMode[]; } = { @@ -101,6 +107,54 @@ export const detailsColorHelpText = i18n.translate( } ); +export const workspacePrivacyTitle = i18n.translate( + 'workspace.form.collaborators.panels.privacy.title', + { + defaultMessage: 'Workspace privacy', + } +); + +export const privacyType2TextMap = { + [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.', + }), + additionalDescription: i18n.translate( + 'workspace.privacy.privateToCollaborators.additionalDescription', + { + defaultMessage: + 'You can add collaborators who can view or edit workspace and assign workspace administrators once the workspace is created.', + } + ), + }, + [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.', + }), + additionalDescription: i18n.translate('workspace.privacy.anyoneCanView.additionalDescription', { + defaultMessage: + 'You can add collaborators who can edit workspace and assign workspace administrators once the workspace is created.', + }), + }, + [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.', + }), + additionalDescription: i18n.translate('workspace.privacy.anyoneCanEdit.additionalDescription', { + defaultMessage: 'You can assign workspace administrators once the workspace is created.', + }), + }, +}; + 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 a014ca26302c..896958b5f54f 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 @@ -5,7 +5,12 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { applicationServiceMock } from '../../../../../core/public/mocks'; -import { WorkspaceOperationType } from './constants'; +import { PermissionModeId } from '../../../../../core/public'; +import { + optionIdToWorkspacePermissionModesMap, + WorkspaceOperationType, + WorkspacePrivacyItemType, +} from './constants'; import { WorkspaceFormSubmitData, WorkspaceFormErrorCode } from './types'; import { useWorkspaceForm } from './use_workspace_form'; import { waitFor } from '@testing-library/dom'; @@ -153,4 +158,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 1b1bd250b2cd..070f6aac7e71 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(); @@ -70,6 +76,10 @@ export const useWorkspaceForm = ({ ? getNumberOfChanges(formData, defaultValuesRef.current) : 0; + const privacyType = useMemo(() => convertPermissionsToPrivacyType(permissionSettings), [ + permissionSettings, + ]); + if (!formIdRef.current) { formIdRef.current = workspaceHtmlIdGenerator(); } @@ -139,11 +149,21 @@ 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 ?? ''); setDescription(resetValues?.description ?? ''); setColor(resetValues?.color); + setPermissionSettings(resetValues?.permissionSettings ?? []); setFeatureConfigs(resetValues?.features ?? []); setFormErrors({}); setIsEditing(false); @@ -154,12 +174,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 f0bf7d234b74..abce541a2248 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, EMPTY_PERMISSIONS, } from './utils'; -import { WorkspacePermissionItemType, optionIdToWorkspacePermissionModesMap } from './constants'; +import { + WorkspacePermissionItemType, + optionIdToWorkspacePermissionModesMap, + WorkspacePrivacyItemType, +} from './constants'; import { DataSourceConnectionType } from '../../../common/types'; import { WorkspaceFormErrorCode } from './types'; import { PermissionModeId, WorkspacePermissionMode } from '../../../../../core/public'; @@ -405,6 +411,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', () => { @@ -497,3 +595,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 c4d69cf1fbe1..22ff4894a1d4 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 { @@ -341,5 +342,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 +): WorkspaceFormDataState['permissionSettings'] => { + 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..2de4c840ec5b --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.test.tsx @@ -0,0 +1,201 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { + privacyType2TextMap, + WorkspacePermissionItemType, + WorkspacePrivacyItemType, +} from './constants'; +import { WorkspacePermissionMode } from '../../../../../core/types'; +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( + privacyType2TextMap[WorkspacePrivacyItemType.PrivateToCollaborators].title, + { exact: false } + ) + ).toBeInTheDocument(); + + const { renderResult: anyoneCanViewWorkspace } = setup({ + permissionSettings: permissionSettingsView, + }); + expect( + anyoneCanViewWorkspace.getByText( + privacyType2TextMap[WorkspacePrivacyItemType.AnyoneCanView].title, + { exact: false } + ) + ).toBeInTheDocument(); + + const { renderResult: anyoneCanEditWorkspace } = setup({ + permissionSettings: permissionSettingsEdit, + }); + expect( + anyoneCanEditWorkspace.getByText( + privacyType2TextMap[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( + privacyType2TextMap[WorkspacePrivacyItemType.PrivateToCollaborators].title + ) + ); + fireEvent.click( + renderResult.getByText(privacyType2TextMap[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( + privacyType2TextMap[WorkspacePrivacyItemType.PrivateToCollaborators].title + ) + ); + fireEvent.click( + renderResult.getByText(privacyType2TextMap[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( + privacyType2TextMap[WorkspacePrivacyItemType.PrivateToCollaborators].title + ) + ); + fireEvent.click( + renderResult.getByText(privacyType2TextMap[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', + }); + }); + }); + + it('should close the modal after clicking the close button', async () => { + const { renderResult } = setup(); + fireEvent.click(renderResult.getByText('Edit')); + expect(renderResult.queryByText('Save changes')).toBeInTheDocument(); + fireEvent.click(renderResult.getByLabelText('Closes this modal window')); + expect(renderResult.queryByText('Save changes')).not.toBeInTheDocument(); + }); + + it('should close the modal after clicking the cancel button', async () => { + const { renderResult } = setup(); + fireEvent.click(renderResult.getByText('Edit')); + expect(renderResult.queryByText('Save changes')).toBeInTheDocument(); + fireEvent.click(renderResult.getByText('Cancel')); + expect(renderResult.queryByText('Save changes')).not.toBeInTheDocument(); + }); +}); 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..d5745808c402 --- /dev/null +++ b/src/plugins/workspace/public/components/workspace_form/workspace_collaborator_privacy_setting_panel.tsx @@ -0,0 +1,155 @@ +/* + * 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 { i18n } from '@osd/i18n'; +import { WorkspacePrivacyItemType, privacyType2TextMap, workspacePrivacyTitle } from './constants'; +import { WorkspacePermissionSetting } from './types'; +import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public'; +import { CoreStart, IWorkspaceResponse } from '../../../../../core/public'; +import { + convertPermissionsToPrivacyType, + getPermissionSettingsWithPrivacyType, + isWorkspacePermissionSetting, +} 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).filter( + isWorkspacePermissionSetting + ) + ); + } 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: privacyType2TextMap[privacyType].title, + description: privacyType2TextMap[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..f1a4bb7b8594 --- /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 { privacyType2TextMap, 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( + privacyType2TextMap[WorkspacePrivacyItemType.PrivateToCollaborators].title, + { exact: false } + ) + ).toBeInTheDocument(); + + expect( + renderResult + .getByText(privacyType2TextMap[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..399c4826a041 --- /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 { privacyType2TextMap, WorkspacePrivacyItemType, workspacePrivacyTitle } from './constants'; +import './workspace_privacy_setting.scss'; +import { + generateRightSidebarScrollProps, + RightSidebarScrollField, +} from '../workspace_creator/utils'; + +const options = [ + WorkspacePrivacyItemType.PrivateToCollaborators, + WorkspacePrivacyItemType.AnyoneCanView, + WorkspacePrivacyItemType.AnyoneCanEdit, +].map((value) => ({ + id: value, + label: privacyType2TextMap[value].title, + description: privacyType2TextMap[value].description, +})); + +export interface WorkspacePrivacySettingProps { + privacyType: WorkspacePrivacyItemType; + onPrivacyTypeChange: (newPrivacyType: WorkspacePrivacyItemType) => void; + goToCollaborators: boolean; + onGoToCollaboratorsChange: (value: boolean) => void; +} + +export const WorkspacePrivacySettingPanel = ({ + privacyType, + onPrivacyTypeChange, + goToCollaborators, + onGoToCollaboratorsChange, +}: WorkspacePrivacySettingProps) => { + 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} +
+
+ ))} +
+
+ + {privacyType2TextMap[privacyType].additionalDescription} + + 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..4a8f138533db --- /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 { privacyType2TextMap, 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: privacyType2TextMap[value].title, + dropdownDisplay: ( + <> + {privacyType2TextMap[value].title} + + + {privacyType2TextMap[value].description} + + + ), + })); + return ( + <> + + onSelectedPrivacyTypeChange(value)} + data-test-subj="workspacePrivacySettingSelector" + /> + + + {privacyType2TextMap[selectedPrivacyType].description} + + + ); +};