Skip to content

Commit

Permalink
[Workspace] Add permission tab to workspace create update page (#6378) (
Browse files Browse the repository at this point in the history
#6578)

* Allow workspace update with partial attirbutes

Signed-off-by: Lin Wang <[email protected]>

* Add permissions tab for workspace creator and update page

Signed-off-by: Lin Wang <[email protected]>

* Add change log for adding permission tab

Signed-off-by: Lin Wang <[email protected]>

* Optimize permissions to permissions settings convertation

Signed-off-by: Lin Wang <[email protected]>

* Address PR comments

Signed-off-by: Lin Wang <[email protected]>

* Update comments for getPermissionModeId

Signed-off-by: Lin Wang <[email protected]>

---------

Signed-off-by: Lin Wang <[email protected]>
(cherry picked from commit d911fa7)
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent e356afc commit 78bf7cf
Show file tree
Hide file tree
Showing 22 changed files with 1,502 additions and 78 deletions.
2 changes: 2 additions & 0 deletions src/core/types/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,5 @@ export interface SavedObjectError {
statusCode: number;
metadata?: Record<string, unknown>;
}

export type SavedObjectPermissions = Permissions;
6 changes: 6 additions & 0 deletions src/core/types/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { Permissions } from '../server/saved_objects';

export interface WorkspaceAttribute {
id: string;
name: string;
Expand All @@ -12,3 +14,7 @@ export interface WorkspaceAttribute {
icon?: string;
reserved?: boolean;
}

export interface WorkspaceAttributeWithPermission extends WorkspaceAttribute {
permissions?: Permissions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const WorkspaceCreator = (props: any) => {
...{
application: {
...mockCoreStart.application,
capabilities: {
...mockCoreStart.application.capabilities,
workspaces: {
permissionEnabled: true,
},
},
navigateToApp,
getUrlForApp: jest.fn(() => '/app/workspace_overview'),
applications$: new BehaviorSubject<Map<string, PublicAppInfo>>(PublicAPPInfoMap as any),
Expand Down Expand Up @@ -145,7 +151,8 @@ describe('WorkspaceCreator', () => {
name: 'test workspace name',
color: '#000000',
description: 'test workspace description',
})
}),
undefined
);
await waitFor(() => {
expect(notificationToastsAddSuccess).toHaveBeenCalled();
Expand All @@ -168,7 +175,8 @@ describe('WorkspaceCreator', () => {
expect.objectContaining({
name: 'test workspace name',
features: expect.arrayContaining(['app1', 'app2', 'app3']),
})
}),
undefined
);
await waitFor(() => {
expect(notificationToastsAddSuccess).toHaveBeenCalled();
Expand All @@ -180,7 +188,7 @@ describe('WorkspaceCreator', () => {
});

it('should show danger toasts after create workspace failed', async () => {
workspaceClientCreate.mockReturnValue({ result: { id: 'failResult' }, success: false });
workspaceClientCreate.mockReturnValueOnce({ result: { id: 'failResult' }, success: false });
const { getByTestId } = render(<WorkspaceCreator />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
Expand All @@ -195,7 +203,7 @@ describe('WorkspaceCreator', () => {
});

it('should show danger toasts after call create workspace API failed', async () => {
workspaceClientCreate.mockImplementation(async () => {
workspaceClientCreate.mockImplementationOnce(async () => {
throw new Error();
});
const { getByTestId } = render(<WorkspaceCreator />);
Expand All @@ -210,4 +218,38 @@ describe('WorkspaceCreator', () => {
});
expect(notificationToastsAddSuccess).not.toHaveBeenCalled();
});

it('create workspace with customized permissions', async () => {
const { getByTestId, getByText, getAllByText } = render(<WorkspaceCreator />);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
fireEvent.click(getByText('Users & Permissions'));
fireEvent.click(getByTestId('workspaceForm-permissionSettingPanel-user-addNew'));
const userIdInput = getAllByText('Select')[0];
fireEvent.click(userIdInput);
fireEvent.input(getByTestId('comboBoxSearchInput'), {
target: { value: 'test user id' },
});
fireEvent.blur(getByTestId('comboBoxSearchInput'));
fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton'));
expect(workspaceClientCreate).toHaveBeenCalledWith(
expect.objectContaining({
name: 'test workspace name',
}),
{
read: {
users: ['test user id'],
},
library_read: {
users: ['test user id'],
},
}
);
await waitFor(() => {
expect(notificationToastsAddSuccess).toHaveBeenCalled();
});
expect(notificationToastsAddDanger).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ import { WorkspaceForm, WorkspaceFormSubmitData, WorkspaceOperationType } from '
import { WORKSPACE_OVERVIEW_APP_ID } from '../../../common/constants';
import { formatUrlWithWorkspaceId } from '../../../../../core/public/utils';
import { WorkspaceClient } from '../../workspace_client';
import { convertPermissionSettingsToPermissions } from '../workspace_form';

export const WorkspaceCreator = () => {
const {
services: { application, notifications, http, workspaceClient },
} = useOpenSearchDashboards<{ workspaceClient: WorkspaceClient }>();
const isPermissionEnabled = application?.capabilities.workspaces.permissionEnabled;

const handleWorkspaceFormSubmit = useCallback(
async (data: WorkspaceFormSubmitData) => {
let result;
try {
result = await workspaceClient.create(data);
const { permissionSettings, ...attributes } = data;
result = await workspaceClient.create(
attributes,
convertPermissionSettingsToPermissions(permissionSettings)
);
if (result?.success) {
notifications?.toasts.addSuccess({
title: i18n.translate('workspace.create.success', {
Expand Down Expand Up @@ -80,6 +86,8 @@ export const WorkspaceCreator = () => {
application={application}
onSubmit={handleWorkspaceFormSubmit}
operationType={WorkspaceOperationType.Create}
permissionEnabled={isPermissionEnabled}
permissionLastAdminItemDeletable
/>
)}
</EuiPageContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { WorkspacePermissionMode } from '../../../common/constants';

export enum WorkspaceOperationType {
Create = 'create',
Update = 'update',
Expand All @@ -11,4 +14,51 @@ export enum WorkspaceOperationType {
export enum WorkspaceFormTabs {
NotSelected,
FeatureVisibility,
UsersAndPermissions,
}

export enum WorkspacePermissionItemType {
User = 'user',
Group = 'group',
}

export enum PermissionModeId {
Read = 'read',
ReadAndWrite = 'read+write',
Admin = 'admin',
}

export const permissionModeOptions = [
{
id: PermissionModeId.Read,
label: i18n.translate('workspace.form.permissionSettingPanel.permissionModeOptions.read', {
defaultMessage: 'Read',
}),
},
{
id: PermissionModeId.ReadAndWrite,
label: i18n.translate(
'workspace.form.permissionSettingPanel.permissionModeOptions.readAndWrite',
{
defaultMessage: 'Read & Write',
}
),
},
{
id: PermissionModeId.Admin,
label: i18n.translate('workspace.form.permissionSettingPanel.permissionModeOptions.admin', {
defaultMessage: 'Admin',
}),
},
];

export const optionIdToWorkspacePermissionModesMap: {
[key: string]: WorkspacePermissionMode[];
} = {
[PermissionModeId.Read]: [WorkspacePermissionMode.LibraryRead, WorkspacePermissionMode.Read],
[PermissionModeId.ReadAndWrite]: [
WorkspacePermissionMode.LibraryWrite,
WorkspacePermissionMode.Read,
],
[PermissionModeId.Admin]: [WorkspacePermissionMode.LibraryWrite, WorkspacePermissionMode.Write],
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
export { WorkspaceForm } from './workspace_form';
export { WorkspaceFormSubmitData } from './types';
export { WorkspaceOperationType } from './constants';
export {
convertPermissionsToPermissionSettings,
convertPermissionSettingsToPermissions,
} from './utils';
26 changes: 24 additions & 2 deletions src/plugins/workspace/public/components/workspace_form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,30 @@
* SPDX-License-Identifier: Apache-2.0
*/

import type { WorkspaceOperationType } from './constants';
import type { ApplicationStart } from '../../../../../core/public';
import type { WorkspacePermissionMode } from '../../../common/constants';
import type { WorkspaceOperationType, WorkspacePermissionItemType } from './constants';

export type WorkspacePermissionSetting =
| {
id: number;
type: WorkspacePermissionItemType.User;
userId: string;
modes: WorkspacePermissionMode[];
}
| {
id: number;
type: WorkspacePermissionItemType.Group;
group: string;
modes: WorkspacePermissionMode[];
};

export interface WorkspaceFormSubmitData {
name: string;
description?: string;
features?: string[];
color?: string;
permissionSettings?: WorkspacePermissionSetting[];
}

export interface WorkspaceFormData extends WorkspaceFormSubmitData {
Expand All @@ -28,11 +44,17 @@ export interface WorkspaceFeatureGroup {
features: WorkspaceFeature[];
}

export type WorkspaceFormErrors = { [key in keyof WorkspaceFormData]?: string };
export type WorkspaceFormErrors = {
[key in keyof Omit<WorkspaceFormData, 'permissionSettings'>]?: string;
} & {
permissionSettings?: { [key: number]: string };
};

export interface WorkspaceFormProps {
application: ApplicationStart;
onSubmit?: (formData: WorkspaceFormSubmitData) => void;
defaultValues?: WorkspaceFormData;
operationType?: WorkspaceOperationType;
permissionEnabled?: boolean;
permissionLastAdminItemDeletable?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@

import { useCallback, useState, FormEventHandler, useRef, useMemo, useEffect } from 'react';
import { htmlIdGenerator, EuiFieldTextProps, EuiColorPickerProps } from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { useApplications } from '../../hooks';
import { featureMatchesConfig } from '../../utils';

import { WorkspaceFormTabs } from './constants';
import { WorkspaceFormProps, WorkspaceFormErrors } from './types';
import { appendDefaultFeatureIds, getNumberOfErrors, isValidFormTextInput } from './utils';
import { WorkspaceFormProps, WorkspaceFormErrors, WorkspacePermissionSetting } from './types';
import { appendDefaultFeatureIds, getNumberOfErrors, validateWorkspaceForm } from './utils';

const workspaceHtmlIdGenerator = htmlIdGenerator();

Expand All @@ -36,6 +35,13 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works
const [selectedFeatureIds, setSelectedFeatureIds] = useState(
appendDefaultFeatureIds(defaultFeatures)
);
const [permissionSettings, setPermissionSettings] = useState<
Array<Pick<WorkspacePermissionSetting, 'id'> & Partial<WorkspacePermissionSetting>>
>(
defaultValues?.permissionSettings && defaultValues.permissionSettings.length > 0
? defaultValues.permissionSettings
: []
);

const [formErrors, setFormErrors] = useState<WorkspaceFormErrors>({});
const numberOfErrors = useMemo(() => getNumberOfErrors(formErrors), [formErrors]);
Expand All @@ -45,6 +51,7 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works
description,
features: selectedFeatureIds,
color,
permissionSettings,
});
const getFormDataRef = useRef(getFormData);
getFormDataRef.current = getFormData;
Expand All @@ -56,32 +63,8 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works
const handleFormSubmit = useCallback<FormEventHandler>(
(e) => {
e.preventDefault();
let currentFormErrors: WorkspaceFormErrors = {};
const formData = getFormDataRef.current();
if (!formData.name) {
currentFormErrors = {
...currentFormErrors,
name: i18n.translate('workspace.form.detail.name.empty', {
defaultMessage: "Name can't be empty.",
}),
};
}
if (formData.name && !isValidFormTextInput(formData.name)) {
currentFormErrors = {
...currentFormErrors,
name: i18n.translate('workspace.form.detail.name.invalid', {
defaultMessage: 'Invalid workspace name',
}),
};
}
if (formData.description && !isValidFormTextInput(formData.description)) {
currentFormErrors = {
...currentFormErrors,
description: i18n.translate('workspace.form.detail.description.invalid', {
defaultMessage: 'Invalid workspace description',
}),
};
}
const currentFormErrors: WorkspaceFormErrors = validateWorkspaceForm(formData);
setFormErrors(currentFormErrors);
if (getNumberOfErrors(currentFormErrors) > 0) {
return;
Expand All @@ -101,7 +84,11 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works
formData.features = defaultValues?.features ?? [];
}

onSubmit?.({ ...formData, name: formData.name! });
onSubmit?.({
...formData,
name: formData.name!,
permissionSettings: formData.permissionSettings as WorkspacePermissionSetting[],
});
},
[defaultFeatures, onSubmit, defaultValues?.features]
);
Expand All @@ -122,6 +109,10 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works
setSelectedTab(WorkspaceFormTabs.FeatureVisibility);
}, []);

const handleTabPermissionClick = useCallback(() => {
setSelectedTab(WorkspaceFormTabs.UsersAndPermissions);
}, []);

const handleFeaturesChange = useCallback((featureIds: string[]) => {
setSelectedFeatureIds(featureIds);
}, []);
Expand All @@ -143,6 +134,8 @@ export const useWorkspaceForm = ({ application, defaultValues, onSubmit }: Works
handleFeaturesChange,
handleNameInputChange,
handleTabFeatureClick,
setPermissionSettings,
handleTabPermissionClick,
handleDescriptionInputChange,
};
};
Loading

0 comments on commit 78bf7cf

Please sign in to comment.