From 1277b2a092fbd057220ee43d6fc47bffff5933d1 Mon Sep 17 00:00:00 2001 From: Stephen Smith Date: Mon, 8 Nov 2021 10:09:33 -0500 Subject: [PATCH] 18123: Add edit permission level dialog for group members and outgoing permissions. Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- .../group-details-panel-actions.ts | 41 ++++++++- .../data-explorer/renderers.tsx | 90 +++++++++++++++---- .../edit-permission-level-dialog.tsx | 55 ++++++++++++ .../group-details-panel.tsx | 6 +- src/views/groups-panel/groups-panel.tsx | 8 +- src/views/workbench/workbench.tsx | 2 + 6 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 src/views-components/dialog-forms/edit-permission-level-dialog.tsx diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts index 22247a8f..26ba537d 100644 --- a/src/store/group-details-panel/group-details-panel-actions.ts +++ b/src/store/group-details-panel/group-details-panel-actions.ts @@ -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(linkUuid)(getState().resources); + const resource = getResource(resourceUuid)(getState().resources); + + if (link) { + dispatch(reset(EDIT_PERMISSION_LEVEL_FORM)); + dispatch(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(); diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx index 71b82b6f..73ef32b0 100644 --- a/src/views-components/data-explorer/renderers.tsx +++ b/src/views-components/data-explorer/renderers.tsx @@ -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) => - +const renderName = (dispatch: Dispatch, item: GroupContentsResource) => { + + const navFunc = ("groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo); + return {renderIcon(item)} - dispatch(navigateTo(item.uuid))}> + dispatch(navFunc(item.uuid))}> {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? : null} @@ -51,6 +55,7 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => ; +}; 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 dispatch(navigateTo(item.uuid))}> {resourceLabel(item.kind)}: {displayName || item.uuid} ; -} +}; + +const renderResource = (dispatch: Dispatch, item: Resource) => { + var displayName = getResourceDisplayName(item); + + return + {resourceLabel(item.kind)}: {displayName || item.uuid} + ; +}; 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) => - 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) => - 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 + {formatPermissionLevel(link.name as PermissionLevel)} + dispatch(openEditPermissionLevelDialog(link.uuid, resource.uuid))}> + + + ; +} + +export const ResourceLinkHeadPermissionLevel = connect( + (state: RootState, props: { uuid: string }) => { + const link = getResource(props.uuid)(state.resources); + const resource = getResource(link?.headUuid || '')(state.resources); + + return { + link: link || { uuid: '', name: '', kind: ResourceKind.NONE }, + resource: resource || { uuid: '', kind: ResourceKind.NONE } + }; + })((props: { link: LinkResource, resource: Resource } & DispatchProp) => + renderPermissionLevel(props.dispatch, props.link, props.resource)); + +export const ResourceLinkTailPermissionLevel = connect( + (state: RootState, props: { uuid: string }) => { + const link = getResource(props.uuid)(state.resources); + const resource = getResource(link?.tailUuid || '')(state.resources); + + return { + link: link || { uuid: '', name: '', kind: ResourceKind.NONE }, + resource: resource || { uuid: '', kind: ResourceKind.NONE } + }; + })((props: { link: LinkResource, resource: Resource } & DispatchProp) => + 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(props.uuid)(state.resources); + return { + item: resource || { uuid: '', kind: ResourceKind.NONE } + }; + })((props: { item: Resource } & DispatchProp) => + 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 index 00000000..5479a0c6 --- /dev/null +++ b/src/views-components/dialog-forms/edit-permission-level-dialog.tsx @@ -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({ + form: EDIT_PERMISSION_LEVEL_FORM, + onSubmit: (data, dispatch) => { + dispatch(editPermissionLevel(data)); + }, + }) +)( + (props: EditPermissionLevelDialogProps) => + +); + +interface EditPermissionLevelDataProps { + data: Resource; +} + +type EditPermissionLevelDialogProps = EditPermissionLevelDataProps & WithDialogProps<{}> & InjectedFormProps; + +const PermissionField = (props: EditPermissionLevelDialogProps) => + + + + + + + + ; + +const PermissionSelectComponent = ({ input }: WrappedFieldProps) => + ; diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx index c402ebb6..50838f7d 100644 --- a/src/views/group-details-panel/group-details-panel.tsx +++ b/src/views/group-details-panel/group-details-panel.tsx @@ -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 = [ selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => }, { name: GroupDetailsPanelMembersColumnNames.UUID, @@ -93,7 +93,7 @@ export const groupDetailsPermissionsPanelColumns: DataColumns = [ selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => }, { name: GroupDetailsPanelPermissionsColumnNames.UUID, diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx index 04f2a273..9bfad524 100644 --- a/src/views/groups-panel/groups-panel.tsx +++ b/src/views/groups-panel/groups-panel.tsx @@ -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 = [ render: uuid => }, { - name: GroupsPanelColumnNames.OWNER, + name: GroupsPanelColumnNames.UUID, selected: true, configurable: true, filters: createTree(), - render: uuid => , + render: uuid => , }, { name: GroupsPanelColumnNames.MEMBERS, diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 9ce93bf2..50194f9e 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -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 = + -- 2.30.2