16115: Brings back the Visibility Level form.
authorLucas Di Pentima <lucas.dipentima@curii.com>
Mon, 23 May 2022 18:43:58 +0000 (15:43 -0300)
committerLucas Di Pentima <lucas.dipentima@curii.com>
Mon, 23 May 2022 18:43:58 +0000 (15:43 -0300)
To avoid confusing users, this form is back and it's rendered as the first
row of the permissions list instead of being hidden inside an 'Advanced' mode.

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas.dipentima@curii.com>

src/store/sharing-dialog/sharing-dialog-actions.ts
src/store/sharing-dialog/sharing-dialog-types.ts
src/views-components/sharing-dialog/sharing-dialog-component.tsx
src/views-components/sharing-dialog/sharing-management-form-component.tsx
src/views-components/sharing-dialog/sharing-public-access-form-component.tsx [new file with mode: 0644]
src/views-components/sharing-dialog/sharing-public-access-form.tsx [new file with mode: 0644]
src/views-components/sharing-dialog/visibility-level-select.tsx [new file with mode: 0644]

index ffd81fb7fd88b60fc208b247c48baa325ce5736c..367eea814281824f8eb161d8afede65639ffc223 100644 (file)
@@ -10,6 +10,9 @@ import {
     SharingManagementFormData,
     SharingInvitationFormData,
     getSharingMangementFormData,
+    SharingPublicAccessFormData,
+    VisibilityLevel,
+    SHARING_PUBLIC_ACCESS_FORM_NAME,
 } from './sharing-dialog-types';
 import { Dispatch } from 'redux';
 import { ServiceRepository } from "services/services";
@@ -18,7 +21,7 @@ import { initialize, getFormValues, reset } from 'redux-form';
 import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
 import { RootState } from 'store/store';
 import { getDialog } from 'store/dialog/dialog-reducer';
-import { PermissionLevel } from 'models/permission';
+import { PermissionLevel, PermissionResource } from 'models/permission';
 import { differenceWith } from "lodash";
 import { withProgress } from "store/progress-indicator/with-progress";
 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
@@ -28,6 +31,8 @@ import {
     ResourceObjectType
 } from "models/resource";
 import { resourcesActions } from "store/resources/resources-actions";
+import { getPublicGroupUuid } from "store/workflow-panel/workflow-panel-actions";
+import { getSharingPublicAccessFormData } from './sharing-dialog-types';
 
 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
     (dispatch: Dispatch) => {
@@ -44,6 +49,7 @@ export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
 
 export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => {
     dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
+    await dispatch<any>(savePublicPermissionChanges);
     await dispatch<any>(saveManagementChanges);
     await dispatch<any>(sendInvitations);
     dispatch(reset(SHARING_INVITATION_FORM_NAME));
@@ -109,7 +115,7 @@ export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, g
     }
 };
 
-const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { permissionService, apiClientAuthorizationService }: ServiceRepository) => {
+const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
 
     const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
     if (dialog) {
@@ -143,6 +149,7 @@ export const initializeManagementForm = async (dispatch: Dispatch, getState: ()
         dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
         const resourceUuid = dialog?.data.resourceUuid;
         const { items: permissionLinks } = await permissionService.listResourcePermissions(resourceUuid);
+        dispatch<any>(initializePublicAccessForm(permissionLinks));
         const filters = new FilterBuilder()
             .addIn('uuid', permissionLinks.map(({ tailUuid }) => tailUuid))
             .getFilters();
@@ -161,6 +168,8 @@ export const initializeManagementForm = async (dispatch: Dispatch, getState: ()
         };
 
         const managementPermissions = permissionLinks
+            .filter(item =>
+                item.tailUuid !== getPublicGroupUuid(getState()))
             .map(({ tailUuid, name, uuid }) => ({
                 email: getEmail(tailUuid),
                 permissions: name as PermissionLevel,
@@ -176,17 +185,63 @@ export const initializeManagementForm = async (dispatch: Dispatch, getState: ()
         dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
     };
 
+const initializePublicAccessForm = (permissionLinks: PermissionResource[]) =>
+    (dispatch: Dispatch, getState: () => RootState, ) => {
+        const [publicPermission] = permissionLinks
+            .filter(item => item.tailUuid === getPublicGroupUuid(getState()));
+        const publicAccessFormData: SharingPublicAccessFormData = publicPermission
+            ? {
+                visibility: VisibilityLevel.PUBLIC,
+                permissionUuid: publicPermission.uuid,
+            }
+            : {
+                visibility: permissionLinks.length > 0
+                    ? VisibilityLevel.SHARED
+                    : VisibilityLevel.PRIVATE,
+                permissionUuid: '',
+            };
+        dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData));
+    };
+
+const savePublicPermissionChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+    const state = getState();
+    const { user } = state.auth;
+    const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
+    if (dialog && user) {
+        const { permissionUuid, visibility } = getSharingPublicAccessFormData(state);
+        if (permissionUuid) {
+            if (visibility === VisibilityLevel.PUBLIC) {
+                await permissionService.update(permissionUuid, {
+                    name: PermissionLevel.CAN_READ
+                });
+            } else {
+                await permissionService.delete(permissionUuid);
+            }
+        } else if (visibility === VisibilityLevel.PUBLIC) {
+            await permissionService.create({
+                ownerUuid: user.uuid,
+                headUuid: dialog.data.resourceUuid,
+                tailUuid: getPublicGroupUuid(state),
+                name: PermissionLevel.CAN_READ,
+            });
+        }
+    }
+};
+
 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
     const state = getState();
     const { user } = state.auth;
     const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
     if (dialog && user) {
         const { initialPermissions, permissions } = getSharingMangementFormData(state);
-        const cancelledPermissions = differenceWith(
-            initialPermissions,
-            permissions,
-            (a, b) => a.permissionUuid === b.permissionUuid
-        );
+        const { visibility } = getSharingPublicAccessFormData(state);
+        const cancelledPermissions = visibility === VisibilityLevel.PRIVATE
+            ? initialPermissions
+            : differenceWith(
+                initialPermissions,
+                permissions,
+                (a, b) => a.permissionUuid === b.permissionUuid
+            );
 
         const deletions = cancelledPermissions.map(({ permissionUuid }) =>
             permissionService.delete(permissionUuid));
index 7ca8b5c5686c515eba2498e613d525e9a33fa45d..a05224e2373753a705821d4d639368545df9d8d2 100644 (file)
@@ -7,9 +7,21 @@ import { getFormValues, isDirty } from 'redux-form';
 import { RootState } from 'store/store';
 
 export const SHARING_DIALOG_NAME = 'SHARING_DIALOG_NAME';
+export const SHARING_PUBLIC_ACCESS_FORM_NAME = 'SHARING_PUBLIC_ACCESS_FORM_NAME';
 export const SHARING_MANAGEMENT_FORM_NAME = 'SHARING_MANAGEMENT_FORM_NAME';
 export const SHARING_INVITATION_FORM_NAME = 'SHARING_INVITATION_FORM_NAME';
 
+export enum VisibilityLevel {
+    PRIVATE = 'Private',
+    SHARED = 'Shared',
+    PUBLIC = 'Public',
+}
+
+export interface SharingPublicAccessFormData {
+    visibility: VisibilityLevel;
+    permissionUuid: string;
+}
+
 export interface SharingManagementFormData {
     permissions: SharingManagementFormDataRow[];
     initialPermissions: SharingManagementFormDataRow[];
@@ -35,6 +47,10 @@ export interface SharingInvitationFormPersonData {
 export const getSharingMangementFormData = (state: any) =>
     getFormValues(SHARING_MANAGEMENT_FORM_NAME)(state) as SharingManagementFormData;
 
+export const getSharingPublicAccessFormData = (state: any) =>
+    getFormValues(SHARING_PUBLIC_ACCESS_FORM_NAME)(state) as SharingPublicAccessFormData;
+
 export const hasChanges = (state: RootState) =>
+    isDirty(SHARING_PUBLIC_ACCESS_FORM_NAME)(state) ||
     isDirty(SHARING_MANAGEMENT_FORM_NAME)(state) ||
     isDirty(SHARING_INVITATION_FORM_NAME)(state);
index 0fa0056c018c06e125b80a3b0793333224ec32c9..cd7ea9bb39a9e8dab099fbc7572767d2a6a685e8 100644 (file)
@@ -38,6 +38,7 @@ import {
 } from 'material-ui-pickers';
 import DateFnsUtils from "@date-io/date-fns";
 import moment from 'moment';
+import { SharingPublicAccessForm } from './sharing-public-access-form';
 
 export interface SharingDialogDataProps {
     open: boolean;
@@ -101,9 +102,12 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => {
         <DialogContent>
             { tabNr === SharingDialogTab.PERMISSIONS &&
             <Grid container direction='column' spacing={24}>
-              <Grid item>
-                  <SharingManagementForm />
-              </Grid>
+                <Grid item>
+                    <SharingPublicAccessForm />
+                </Grid>
+                <Grid item>
+                    <SharingManagementForm />
+                </Grid>
             </Grid>
             }
             { tabNr === SharingDialogTab.URLS &&
index 2ebf8c2d8c806aff8fccfccf034682b627c56c8a..d4d1095292748a629e502064da862bc12c6bd4d3 100644 (file)
@@ -21,13 +21,8 @@ export default () =>
     <FieldArray name='permissions' component={SharingManagementFieldArray as any} />;
 
 const SharingManagementFieldArray = ({ fields }: WrappedFieldArrayProps<{ email: string }>) =>
-    <div>
-        {
-        fields.length > 0
-        ? fields.map((field, index, fields) =>
-                <PermissionManagementRow key={field} {...{ field, index, fields }} />)
-        : <Typography>No permissions set</Typography>
-        }
+    <div>{ fields.map((field, index, fields) =>
+        <PermissionManagementRow key={field} {...{ field, index, fields }} />) }
         <Divider />
     </div>;
 
diff --git a/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx b/src/views-components/sharing-dialog/sharing-public-access-form-component.tsx
new file mode 100644 (file)
index 0000000..7ec7116
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { Grid, StyleRulesCallback, Divider, Typography } from '@material-ui/core';
+import { Field, WrappedFieldProps } from 'redux-form';
+import { WithStyles } from '@material-ui/core/styles';
+import withStyles from '@material-ui/core/styles/withStyles';
+import { VisibilityLevelSelect } from './visibility-level-select';
+import { VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
+
+const sharingPublicAccessStyles: StyleRulesCallback<'root'> = theme => ({
+    root: {
+        padding: `${theme.spacing.unit * 2}px 0`,
+    }
+});
+
+const SharingPublicAccessForm = withStyles(sharingPublicAccessStyles)(
+    ({ classes, visibility }: WithStyles<'root'> & { visibility: VisibilityLevel }) =>
+        <>
+            <Divider />
+            <Grid container alignItems='center' spacing={8} className={classes.root}>
+                <Grid item xs={8}>
+                    <Typography variant='subtitle1'>
+                        {renderVisibilityInfo(visibility)}
+                    </Typography>
+                </Grid>
+                <Grid item xs={4} container wrap='nowrap'>
+                    <Field name='visibility' component={VisibilityLevelSelectComponent} />
+                </Grid>
+            </Grid>
+        </>
+);
+
+const renderVisibilityInfo = (visibility: VisibilityLevel) => {
+    switch (visibility) {
+        case VisibilityLevel.PUBLIC:
+            return 'Anyone can access';
+        case VisibilityLevel.SHARED:
+            return 'Specific people can access';
+        case VisibilityLevel.PRIVATE:
+            return 'Only you can access';
+        default:
+            return '';
+    }
+};
+
+export default ({ visibility }: { visibility: VisibilityLevel }) =>
+    <SharingPublicAccessForm {...{ visibility }} />;
+
+const VisibilityLevelSelectComponent = ({ input }: WrappedFieldProps) =>
+    <VisibilityLevelSelect fullWidth disableUnderline {...input} />;
+
diff --git a/src/views-components/sharing-dialog/sharing-public-access-form.tsx b/src/views-components/sharing-dialog/sharing-public-access-form.tsx
new file mode 100644 (file)
index 0000000..8ee1d94
--- /dev/null
@@ -0,0 +1,24 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { reduxForm } from 'redux-form';
+import { compose } from 'redux';
+import { connect } from 'react-redux';
+import SharingPublicAccessFormComponent from './sharing-public-access-form-component';
+import { SHARING_PUBLIC_ACCESS_FORM_NAME, VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
+import { RootState } from 'store/store';
+import { getSharingPublicAccessFormData } from '../../store/sharing-dialog/sharing-dialog-types';
+
+export const SharingPublicAccessForm = compose(
+    reduxForm(
+        { form: SHARING_PUBLIC_ACCESS_FORM_NAME }
+    ),
+    connect(
+        (state: RootState) => {
+            const { visibility } = getSharingPublicAccessFormData(state) || { visibility: VisibilityLevel.PRIVATE };
+            return { visibility };
+        }
+    )
+)(SharingPublicAccessFormComponent);
+
diff --git a/src/views-components/sharing-dialog/visibility-level-select.tsx b/src/views-components/sharing-dialog/visibility-level-select.tsx
new file mode 100644 (file)
index 0000000..434b8f5
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { MenuItem, Select, withStyles, StyleRulesCallback } from '@material-ui/core';
+import Lock from '@material-ui/icons/Lock';
+import People from '@material-ui/icons/People';
+import Public from '@material-ui/icons/Public';
+import { WithStyles } from '@material-ui/core/styles';
+import { SelectProps } from '@material-ui/core/Select';
+import { SelectItem } from './select-item';
+import { VisibilityLevel } from 'store/sharing-dialog/sharing-dialog-types';
+
+
+type VisibilityLevelSelectClasses = 'value';
+
+const VisibilityLevelSelectStyles: StyleRulesCallback<VisibilityLevelSelectClasses> = theme => ({
+    value: {
+        marginLeft: theme.spacing.unit,
+    }
+});
+export const VisibilityLevelSelect = withStyles(VisibilityLevelSelectStyles)(
+    ({ classes, ...props }: SelectProps & WithStyles<VisibilityLevelSelectClasses>) =>
+        <Select
+            {...props}
+            renderValue={renderPermissionItem}
+            inputProps={{ classes }}>
+            <MenuItem value={VisibilityLevel.PUBLIC}>
+                {renderPermissionItem(VisibilityLevel.PUBLIC)}
+            </MenuItem>
+            <MenuItem value={VisibilityLevel.SHARED}>
+                {renderPermissionItem(VisibilityLevel.SHARED)}
+            </MenuItem>
+            <MenuItem value={VisibilityLevel.PRIVATE}>
+                {renderPermissionItem(VisibilityLevel.PRIVATE)}
+            </MenuItem>
+        </Select>);
+
+const renderPermissionItem = (value: string) =>
+    <SelectItem {...{ value, icon: getIcon(value) }} />;
+
+const getIcon = (value: string) => {
+    switch (value) {
+        case VisibilityLevel.PUBLIC:
+            return Public;
+        case VisibilityLevel.SHARED:
+            return People;
+        case VisibilityLevel.PRIVATE:
+            return Lock;
+        default:
+            return Lock;
+    }
+};
+