18123: Add edit permission level dialog for group members and outgoing permissions.
authorStephen Smith <stephen@curii.com>
Mon, 8 Nov 2021 15:09:33 +0000 (10:09 -0500)
committerStephen Smith <stephen@curii.com>
Fri, 19 Nov 2021 19:36:15 +0000 (14:36 -0500)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

src/store/group-details-panel/group-details-panel-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/dialog-forms/edit-permission-level-dialog.tsx [new file with mode: 0644]
src/views/group-details-panel/group-details-panel.tsx
src/views/groups-panel/groups-panel.tsx
src/views/workbench/workbench.tsx

index 22247a8fffcc988c4632c658aa958d7a4d17b93b..26ba537d7aa9da254a8b0c0c47cb3534387bf6ba 100644 (file)
@@ -8,14 +8,16 @@ import { propertiesActions } from 'store/properties/properties-actions';
 import { getProperty } from 'store/properties/properties';
 import { Participant } from 'views-components/sharing-dialog/participant-select';
 import { dialogActions } from 'store/dialog/dialog-actions';
-import { reset, startSubmit } from 'redux-form';
+import { initialize, reset, startSubmit } from 'redux-form';
 import { addGroupMember, deleteGroupMember } from 'store/groups-panel/groups-panel-actions';
 import { getResource } from 'store/resources/resources';
 import { GroupResource } from 'models/group';
+import { Resource } from 'models/resource';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
-import { PermissionResource } from 'models/permission';
+import { PermissionResource, PermissionLevel } from 'models/permission';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { PermissionSelectValue, parsePermissionLevel, formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
 
 export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
 export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
@@ -24,6 +26,10 @@ export const ADD_GROUP_MEMBERS_FORM = 'addGroupMembers';
 export const ADD_GROUP_MEMBERS_USERS_FIELD_NAME = 'users';
 export const MEMBER_ATTRIBUTES_DIALOG = 'memberAttributesDialog';
 export const MEMBER_REMOVE_DIALOG = 'memberRemoveDialog';
+export const EDIT_PERMISSION_LEVEL_DIALOG = 'editPermissionLevel';
+export const EDIT_PERMISSION_LEVEL_FORM = 'editPermissionLevel';
+export const EDIT_PERMISSION_LEVEL_FIELD_NAME = 'name';
+export const EDIT_PERMISSION_LEVEL_UUID_FIELD_NAME = 'uuid';
 
 export const GroupMembersPanelActions = bindDataExplorerActions(GROUP_DETAILS_MEMBERS_PANEL_ID);
 export const GroupPermissionsPanelActions = bindDataExplorerActions(GROUP_DETAILS_PERMISSIONS_PANEL_ID);
@@ -42,6 +48,11 @@ export interface AddGroupMembersFormData {
     [ADD_GROUP_MEMBERS_USERS_FIELD_NAME]: Participant[];
 }
 
+export interface EditPermissionLevelFormData {
+    [EDIT_PERMISSION_LEVEL_UUID_FIELD_NAME]: string;
+    [EDIT_PERMISSION_LEVEL_FIELD_NAME]: PermissionSelectValue;
+}
+
 export const openAddGroupMembersDialog = () =>
     (dispatch: Dispatch) => {
         dispatch(dialogActions.OPEN_DIALOG({ id: ADD_GROUP_MEMBERS_DIALOG, data: {} }));
@@ -80,6 +91,32 @@ export const addGroupMembers = ({ users }: AddGroupMembersFormData) =>
         }
     };
 
+export const openEditPermissionLevelDialog = (linkUuid: string, resourceUuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        const link = getResource<PermissionResource>(linkUuid)(getState().resources);
+        const resource = getResource<Resource>(resourceUuid)(getState().resources);
+
+        if (link) {
+            dispatch(reset(EDIT_PERMISSION_LEVEL_FORM));
+            dispatch<any>(initialize(EDIT_PERMISSION_LEVEL_FORM, {[EDIT_PERMISSION_LEVEL_UUID_FIELD_NAME]: link.uuid, [EDIT_PERMISSION_LEVEL_FIELD_NAME]: formatPermissionLevel(link.name as PermissionLevel)}));
+            dispatch(dialogActions.OPEN_DIALOG({ id: EDIT_PERMISSION_LEVEL_DIALOG, data: resource }));
+        }
+    };
+
+export const editPermissionLevel = (data: EditPermissionLevelFormData) =>
+    async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+        try {
+            await permissionService.update(data[EDIT_PERMISSION_LEVEL_UUID_FIELD_NAME], {name: parsePermissionLevel(data[EDIT_PERMISSION_LEVEL_FIELD_NAME])});
+            dispatch(dialogActions.CLOSE_DIALOG({ id: EDIT_PERMISSION_LEVEL_DIALOG }));
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Permission level changed.', hideDuration: 2000 }));
+        } catch (e) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'Failed to update permission',
+                kind: SnackbarKind.ERROR,
+            }));
+        }
+    };
+
 export const openGroupMemberAttributes = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const { resources } = getState();
index 71b82b6f36997f074f15412b4cd62717d9c47e61..73ef32b06f8abfa0525f2a823864e2642df7fb1b 100644 (file)
@@ -6,7 +6,7 @@ import React from 'react';
 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
-import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon } from 'components/icon/icon';
+import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon, RenameIcon } from 'components/icon/icon';
 import { formatDate, formatFileSize, formatTime } from 'common/formatters';
 import { resourceLabel } from 'common/labels';
 import { connect, DispatchProp } from 'react-redux';
@@ -23,21 +23,25 @@ import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
 import { toggleIsActive, toggleIsAdmin } from 'store/users/users-actions';
 import { LinkResource } from 'models/link';
-import { navigateTo } from 'store/navigation/navigation-action';
+import { navigateTo, navigateToGroupDetails } from 'store/navigation/navigation-action';
 import { withResourceData } from 'views-components/data-explorer/with-resources';
 import { CollectionResource } from 'models/collection';
 import { IllegalNamingWarning } from 'components/warning/warning';
 import { loadResource } from 'store/resources/resources-actions';
 import { GroupClass } from 'models/group';
-import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
+import { openRemoveGroupMemberDialog, openEditPermissionLevelDialog } from 'store/group-details-panel/group-details-panel-actions';
+import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
+import { PermissionLevel } from 'models/permission';
 
-const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
-    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
+const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
+
+    const navFunc = ("groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo);
+    return <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item)}
         </Grid>
         <Grid item>
-            <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
+            <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
                 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
                     ? <IllegalNamingWarning name={item.name} />
                     : null}
@@ -51,6 +55,7 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
             </Typography>
         </Grid>
     </Grid>;
+};
 
 export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -287,21 +292,31 @@ export const ResourceLinkClass = connect(
         return resource || { linkClass: '' };
     })(renderLinkClass);
 
-const renderLink = (dispatch: Dispatch, item: Resource) => {
-    var displayName = '';
-
-    if ((item as UserResource).kind === ResourceKind.USER
-          && typeof (item as UserResource).firstName !== 'undefined') {
+const getResourceDisplayName = (resource: Resource): string => {
+    if ((resource as UserResource).kind === ResourceKind.USER
+          && typeof (resource as UserResource).firstName !== 'undefined') {
         // We can be sure the resource is UserResource
-        displayName = getUserDisplayName(item as UserResource);
+        return getUserDisplayName(resource as UserResource);
     } else {
-        displayName = (item as GroupContentsResource).name;
+        return (resource as GroupContentsResource).name;
     }
+}
+
+const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
+    var displayName = getResourceDisplayName(item);
 
     return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
         {resourceLabel(item.kind)}: {displayName || item.uuid}
     </Typography>;
-}
+};
+
+const renderResource = (dispatch: Dispatch, item: Resource) => {
+    var displayName = getResourceDisplayName(item);
+
+    return <Typography variant='body2'>
+        {resourceLabel(item.kind)}: {displayName || item.uuid}
+    </Typography>;
+};
 
 export const ResourceLinkTail = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -312,7 +327,7 @@ export const ResourceLinkTail = connect(
             item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE }
         };
     })((props: { item: Resource } & DispatchProp<any>) =>
-        renderLink(props.dispatch, props.item));
+        renderResourceLink(props.dispatch, props.item));
 
 export const ResourceLinkHead = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -323,7 +338,7 @@ export const ResourceLinkHead = connect(
             item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE }
         };
     })((props: { item: Resource } & DispatchProp<any>) =>
-        renderLink(props.dispatch, props.item));
+        renderResourceLink(props.dispatch, props.item));
 
 export const ResourceLinkUuid = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -384,6 +399,49 @@ export const ResourceLinkTailUsername = connect(
         return resource || { username: '' };
     })(renderUsername);
 
+const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, resource: Resource) => {
+    return <Typography noWrap>
+        {formatPermissionLevel(link.name as PermissionLevel)}
+        <IconButton onClick={() => dispatch<any>(openEditPermissionLevelDialog(link.uuid, resource.uuid))}>
+            <RenameIcon />
+        </IconButton>
+    </Typography>;
+}
+
+export const ResourceLinkHeadPermissionLevel = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const resource = getResource<Resource>(link?.headUuid || '')(state.resources);
+
+        return {
+            link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
+            resource: resource || { uuid: '', kind: ResourceKind.NONE }
+        };
+    })((props: { link: LinkResource, resource: Resource } & DispatchProp<any>) =>
+        renderPermissionLevel(props.dispatch, props.link, props.resource));
+
+export const ResourceLinkTailPermissionLevel = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const resource = getResource<Resource>(link?.tailUuid || '')(state.resources);
+
+        return {
+            link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
+            resource: resource || { uuid: '', kind: ResourceKind.NONE }
+        };
+    })((props: { link: LinkResource, resource: Resource } & DispatchProp<any>) =>
+        renderPermissionLevel(props.dispatch, props.link, props.resource));
+
+// Displays resource type and display name without link
+export const ResourceLabel = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<Resource>(props.uuid)(state.resources);
+        return {
+            item: resource || { uuid: '', kind: ResourceKind.NONE }
+        };
+    })((props: { item: Resource } & DispatchProp<any>) =>
+        renderResource(props.dispatch, props.item));
+
 // Process Resources
 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
     return (
diff --git a/src/views-components/dialog-forms/edit-permission-level-dialog.tsx b/src/views-components/dialog-forms/edit-permission-level-dialog.tsx
new file mode 100644 (file)
index 0000000..5479a0c
--- /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 { compose } from "redux";
+import { reduxForm, InjectedFormProps, WrappedFieldProps, Field } from 'redux-form';
+import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
+import { FormDialog } from 'components/form-dialog/form-dialog';
+import { EDIT_PERMISSION_LEVEL_DIALOG, EDIT_PERMISSION_LEVEL_FORM, EditPermissionLevelFormData, EDIT_PERMISSION_LEVEL_FIELD_NAME, editPermissionLevel } from 'store/group-details-panel/group-details-panel-actions';
+import { require } from 'validators/require';
+import { PermissionSelect } from 'views-components/sharing-dialog/permission-select';
+import { Grid } from '@material-ui/core';
+import { Resource } from 'models/resource';
+import { ResourceLabel } from 'views-components/data-explorer/renderers';
+
+export const EditPermissionLevelDialog = compose(
+    withDialog(EDIT_PERMISSION_LEVEL_DIALOG),
+    reduxForm<EditPermissionLevelFormData>({
+        form: EDIT_PERMISSION_LEVEL_FORM,
+        onSubmit: (data, dispatch) => {
+            dispatch(editPermissionLevel(data));
+        },
+    })
+)(
+    (props: EditPermissionLevelDialogProps) =>
+        <FormDialog
+            dialogTitle='Edit permission'
+            formFields={PermissionField}
+            submitLabel='Update'
+            {...props}
+        />
+);
+
+interface EditPermissionLevelDataProps {
+    data: Resource;
+}
+
+type EditPermissionLevelDialogProps = EditPermissionLevelDataProps & WithDialogProps<{}> & InjectedFormProps<EditPermissionLevelFormData>;
+
+const PermissionField = (props: EditPermissionLevelDialogProps) =>
+    <Grid container spacing={8}>
+        <Grid item xs={8}>
+            <ResourceLabel uuid={props.data.uuid} />
+        </Grid>
+        <Grid item xs={4} container wrap='nowrap'>
+        <Field
+            name={EDIT_PERMISSION_LEVEL_FIELD_NAME}
+            component={PermissionSelectComponent as any}
+            validate={require} />
+        </Grid>
+    </Grid>;
+
+const PermissionSelectComponent = ({ input }: WrappedFieldProps) =>
+    <PermissionSelect fullWidth disableUnderline {...input} />;
index c402ebb62466a0d0eee195508551848e6a7f2ade..50838f7d5b7610679ba73cb0d9366eb5636eae84 100644 (file)
@@ -7,7 +7,7 @@ import { connect } from 'react-redux';
 
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
-import { ResourceLinkHeadUuid, ResourceLinkTailUuid, ResourceLinkTailEmail, ResourceLinkTailUsername, ResourceLinkName, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkTailUuid, ResourceLinkTailEmail, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -62,7 +62,7 @@ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceLinkName uuid={uuid} />
+        render: uuid => <ResourceLinkTailPermissionLevel uuid={uuid} />
     },
     {
         name: GroupDetailsPanelMembersColumnNames.UUID,
@@ -93,7 +93,7 @@ export const groupDetailsPermissionsPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceLinkName uuid={uuid} />
+        render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
     },
     {
         name: GroupDetailsPanelPermissionsColumnNames.UUID,
index 04f2a2731273bb6b6c3333e9a934e88e384961ae..9bfad5241aa59e36872d30166d06c154aae5bc7a 100644 (file)
@@ -8,7 +8,7 @@ import { Grid, Button, Typography } from "@material-ui/core";
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
-import { ResourceOwner } from 'views-components/data-explorer/renderers';
+import { ResourceUuid } from 'views-components/data-explorer/renderers';
 import { AddIcon } from 'components/icon/icon';
 import { ResourceName } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
@@ -25,7 +25,7 @@ import { navigateToGroupDetails } from 'store/navigation/navigation-action';
 
 export enum GroupsPanelColumnNames {
     GROUP = "Name",
-    OWNER = "Owner",
+    UUID = "UUID",
     MEMBERS = "Members",
 }
 
@@ -39,11 +39,11 @@ export const groupsPanelColumns: DataColumns<string> = [
         render: uuid => <ResourceName uuid={uuid} />
     },
     {
-        name: GroupsPanelColumnNames.OWNER,
+        name: GroupsPanelColumnNames.UUID,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceOwner uuid={uuid} />,
+        render: uuid => <ResourceUuid uuid={uuid} />,
     },
     {
         name: GroupsPanelColumnNames.MEMBERS,
index 9ce93bf2ae6e38214186defb3bc87dc02b6455d8..50194f9e9a832f6c2c51691e2239ba03e92c7125 100644 (file)
@@ -89,6 +89,7 @@ import { GroupDetailsPanel } from 'views/group-details-panel/group-details-panel
 import { RemoveGroupMemberDialog } from 'views-components/groups-dialog/member-remove-dialog';
 import { GroupMemberAttributesDialog } from 'views-components/groups-dialog/member-attributes-dialog';
 import { AddGroupMembersDialog } from 'views-components/dialog-forms/add-group-member-dialog';
+import { EditPermissionLevelDialog } from 'views-components/dialog-forms/edit-permission-level-dialog';
 import { PartialCopyToCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-collection-dialog';
 import { PublicFavoritePanel } from 'views/public-favorites-panel/public-favorites-panel';
 import { LinkAccountPanel } from 'views/link-account-panel/link-account-panel';
@@ -213,6 +214,7 @@ export const WorkbenchPanel =
                 <DetailsPanel />
             </Grid>
             <AddGroupMembersDialog />
+            <EditPermissionLevelDialog />
             <AdvancedTabDialog />
             <AttributesApiClientAuthorizationDialog />
             <AttributesKeepServiceDialog />