[ARVADOS-WORKBENCH2] created: 2.3.0-39-g207429c4

Git user git at public.arvados.org
Fri Nov 19 19:52:35 UTC 2021


        at  207429c4a8863bce7e53082179e3e3b78c67b073 (commit)


commit 207429c4a8863bce7e53082179e3e3b78c67b073
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 16 21:10:28 2021 -0500

    18123: Hide add member button unless the user can_manage the group.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index 7b06a693..bade28cb 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -11,13 +11,15 @@ import { ResourceLinkHeadUuid, ResourceLinkTailUuid, ResourceLinkTailEmail, Reso
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
-import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions';
+import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog, getCurrentGroupDetailsPanelUuid } from 'store/group-details-panel/group-details-panel-actions';
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
 import { PermissionResource } from 'models/permission';
 import { Grid, Button, Tabs, Tab, Paper } from '@material-ui/core';
 import { AddIcon } from 'components/icon/icon';
+import { getUserUuid } from 'common/getuser';
+import { GroupResource } from 'models/group';
 
 export enum GroupDetailsPanelMembersColumnNames {
     FULL_NAME = "Name",
@@ -120,8 +122,13 @@ export const groupDetailsPermissionsPanelColumns: DataColumns<string> = [
 ];
 
 const mapStateToProps = (state: RootState) => {
+    const groupUuid = getCurrentGroupDetailsPanelUuid(state.properties);
+    const group = getResource<GroupResource>(groupUuid || '')(state.resources);
+    const userUuid = getUserUuid(state);
+
     return {
-        resources: state.resources
+        resources: state.resources,
+        groupCanManage: userUuid ? group?.writableBy?.includes(userUuid) : false,
     };
 };
 
@@ -134,6 +141,7 @@ export interface GroupDetailsPanelProps {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
     onAddUser: () => void;
     resources: ResourcesState;
+    groupCanManage: boolean;
 }
 
 export const GroupDetailsPanel = connect(
@@ -166,14 +174,15 @@ export const GroupDetailsPanel = connect(
                           hideColumnSelector
                           hideSearchInput
                           actions={
-                              <Grid container justify='flex-end'>
-                                  <Button
+                                this.props.groupCanManage &&
+                                <Grid container justify='flex-end'>
+                                    <Button
                                       variant="contained"
                                       color="primary"
                                       onClick={this.props.onAddUser}>
                                       <AddIcon /> Add user
-                              </Button>
-                              </Grid>
+                                    </Button>
+                                </Grid>
                           }
                           paperProps={{
                               elevation: 0,

commit 134cf300692c9f09f1a79d02295e1d6b7242f32d
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Nov 11 11:23:28 2021 -0500

    18123: Add isActive checkbox to group member list for user members
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 73ef32b0..aa200942 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -185,16 +185,30 @@ export const ResourceEmail = connect(
         return resource || { email: '' };
     })(renderEmail);
 
-const renderIsActive = (props: { uuid: string, isActive: boolean, toggleIsActive: (uuid: string) => void }) =>
-    <Checkbox
-        color="primary"
-        checked={props.isActive}
-        onClick={() => props.toggleIsActive(props.uuid)} />;
+const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boolean, toggleIsActive: (uuid: string) => void }) => {
+    if (props.kind === ResourceKind.USER) {
+        return <Checkbox
+            color="primary"
+            checked={props.isActive}
+            onClick={() => props.toggleIsActive(props.uuid)} />;
+    } else {
+        return <Typography />;
+    }
+}
 
 export const ResourceIsActive = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { isActive: false };
+        return resource || { isActive: false, kind: ResourceKind.NONE };
+    }, { toggleIsActive }
+)(renderIsActive);
+
+export const ResourceLinkTailIsActive = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const tailResource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
+
+        return tailResource || { isActive: false, kind: ResourceKind.NONE };
     }, { toggleIsActive }
 )(renderIsActive);
 
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index b4a8c6d3..7b06a693 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, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkTailUuid, ResourceLinkTailEmail, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailIsActive } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -21,10 +21,11 @@ import { AddIcon } from 'components/icon/icon';
 
 export enum GroupDetailsPanelMembersColumnNames {
     FULL_NAME = "Name",
-    UUID = "UUID",
-    EMAIL = "Email",
     USERNAME = "Username",
+    EMAIL = "Email",
+    ACTIVE = "User Active",
     PERMISSION = "Permission",
+    UUID = "UUID",
     REMOVE = "Remove",
 }
 
@@ -57,6 +58,13 @@ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
         filters: createTree(),
         render: uuid => <ResourceLinkTailEmail uuid={uuid} />
     },
+    {
+        name: GroupDetailsPanelMembersColumnNames.ACTIVE,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkTailIsActive uuid={uuid} />
+    },
     {
         name: GroupDetailsPanelMembersColumnNames.PERMISSION,
         selected: true,

commit 844241adac4afa32679f07874e5c659896399fdc
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Nov 10 22:55:05 2021 -0500

    18123: stopSubmit on failure in edit group dialog
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index e961b347..6d17db19 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -85,6 +85,7 @@ export const updateGroup = (project: ProjectUpdateFormDialogData) =>
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
             return updatedGroup;
         } catch (e) {
+            dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME));
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
                 dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Group with the same name already exists.' } as FormErrors));

commit 022c93cff94f9f253e1df177ad75dde0dde2597f
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Nov 10 20:34:19 2021 -0500

    18123: Add group edit dialog actions to re-use edit project dialog.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index 1c4a0732..e961b347 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from 'redux';
-import { reset, startSubmit, stopSubmit, FormErrors } from 'redux-form';
+import { reset, startSubmit, stopSubmit, FormErrors, initialize } from 'redux-form';
 import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
 import { dialogActions } from 'store/dialog/dialog-actions';
 import { Participant } from 'views-components/sharing-dialog/participant-select';
@@ -16,6 +16,7 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { PermissionLevel } from 'models/permission';
 import { PermissionService } from 'services/permission-service/permission-service';
 import { FilterBuilder } from 'services/api/filter-builder';
+import { ProjectUpdateFormDialogData, PROJECT_UPDATE_FORM_NAME } from 'store/projects/project-update-actions';
 
 export const GROUPS_PANEL_ID = "groupsPanel";
 
@@ -66,6 +67,32 @@ export const openRemoveGroupDialog = (uuid: string) =>
         }));
     };
 
+// Group edit dialog uses project update dialog with sourcePanel set to reload the appropriate parts
+export const openGroupUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
+        dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {sourcePanel: GroupClass.ROLE} }));
+    };
+
+export const updateGroup = (project: ProjectUpdateFormDialogData) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const uuid = project.uuid || '';
+        dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
+        try {
+            const updatedGroup = await services.groupsService.update(uuid, { name: project.name, description: project.description });
+            dispatch(GroupsPanelActions.REQUEST_ITEMS());
+            dispatch(reset(PROJECT_UPDATE_FORM_NAME));
+            dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
+            return updatedGroup;
+        } catch (e) {
+            const error = getCommonResourceServiceError(e);
+            if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
+                dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Group with the same name already exists.' } as FormErrors));
+            }
+            return ;
+        }
+    };
+
 export interface CreateGroupFormData {
     [CREATE_GROUP_NAME_FIELD_NAME]: string;
     [CREATE_GROUP_USERS_FIELD_NAME]?: Participant[];
diff --git a/src/store/projects/project-update-actions.ts b/src/store/projects/project-update-actions.ts
index 35100eb6..45065b62 100644
--- a/src/store/projects/project-update-actions.ts
+++ b/src/store/projects/project-update-actions.ts
@@ -3,12 +3,13 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from "redux";
-import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form';
+import { FormErrors, initialize, reset, startSubmit, stopSubmit } from 'redux-form';
 import { RootState } from "store/store";
 import { dialogActions } from "store/dialog/dialog-actions";
 import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
 import { ServiceRepository } from "services/services";
 import { projectPanelActions } from 'store/project-panel/project-panel-action';
+import { GroupClass } from "models/group";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -21,7 +22,7 @@ export const PROJECT_UPDATE_FORM_NAME = 'projectUpdateFormName';
 export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
-        dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {} }));
+        dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_UPDATE_FORM_NAME, data: {sourcePanel: GroupClass.PROJECT} }));
     };
 
 export const updateProject = (project: ProjectUpdateFormDialogData) =>
@@ -31,6 +32,7 @@ export const updateProject = (project: ProjectUpdateFormDialogData) =>
         try {
             const updatedProject = await services.projectService.update(uuid, { name: project.name, description: project.description });
             dispatch(projectPanelActions.REQUEST_ITEMS());
+            dispatch(reset(PROJECT_UPDATE_FORM_NAME));
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
             return updatedProject;
         } catch (e) {
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 9c89d199..527d9d74 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -272,6 +272,20 @@ export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialog
         }
     };
 
+export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
+    async (dispatch: Dispatch) => {
+        const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
+        if (updatedGroup) {
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: "Group has been successfully updated.",
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS
+            }));
+            await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
+            dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
+        }
+    };
+
 export const loadCollection = (uuid: string) =>
     handleFirstTimeLoad(
         async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
diff --git a/src/views-components/context-menu/action-sets/group-action-set.ts b/src/views-components/context-menu/action-sets/group-action-set.ts
index f2c9b92f..874a601b 100644
--- a/src/views-components/context-menu/action-sets/group-action-set.ts
+++ b/src/views-components/context-menu/action-sets/group-action-set.ts
@@ -5,14 +5,13 @@
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
 import { RenameIcon, AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { openGroupAttributes, openRemoveGroupDialog } from "store/groups-panel/groups-panel-actions";
-import { openProjectUpdateDialog } from "store/projects/project-update-actions";
+import { openGroupAttributes, openRemoveGroupDialog, openGroupUpdateDialog } from "store/groups-panel/groups-panel-actions";
 
 export const groupActionSet: ContextMenuActionSet = [[{
     name: "Rename",
     icon: RenameIcon,
     execute: (dispatch, resource) => {
-        dispatch<any>(openProjectUpdateDialog(resource));
+        dispatch<any>(openGroupUpdateDialog(resource));
     }
 }, {
     name: "Attributes",
diff --git a/src/views-components/dialog-forms/update-project-dialog.ts b/src/views-components/dialog-forms/update-project-dialog.ts
index dca51b96..119e9256 100644
--- a/src/views-components/dialog-forms/update-project-dialog.ts
+++ b/src/views-components/dialog-forms/update-project-dialog.ts
@@ -7,14 +7,25 @@ import { reduxForm } from 'redux-form';
 import { withDialog } from "store/dialog/with-dialog";
 import { DialogProjectUpdate } from 'views-components/dialog-update/dialog-project-update';
 import { PROJECT_UPDATE_FORM_NAME, ProjectUpdateFormDialogData } from 'store/projects/project-update-actions';
-import { updateProject } from 'store/workbench/workbench-actions';
+import { updateProject, updateGroup } from 'store/workbench/workbench-actions';
+import { GroupClass } from "models/group";
 
 export const UpdateProjectDialog = compose(
     withDialog(PROJECT_UPDATE_FORM_NAME),
     reduxForm<ProjectUpdateFormDialogData>({
         form: PROJECT_UPDATE_FORM_NAME,
-        onSubmit: (data, dispatch) => {
-            dispatch(updateProject(data));
+        onSubmit: (data, dispatch, props) => {
+            console.log(props);
+            switch (props.data.sourcePanel) {
+                case GroupClass.PROJECT:
+                    dispatch(updateProject(data));
+                    break;
+                case GroupClass.ROLE:
+                    dispatch(updateGroup(data));
+                    break;
+                default:
+                    break;
+            }
         }
     })
-)(DialogProjectUpdate);
\ No newline at end of file
+)(DialogProjectUpdate);
diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx
index a9f59f68..4f25f6e5 100644
--- a/src/views/groups-panel/groups-panel.tsx
+++ b/src/views/groups-panel/groups-panel.tsx
@@ -101,8 +101,9 @@ export const GroupsPanel = connect(
             const resource = getResource<GroupResource>(resourceUuid)(this.props.resources);
             if (resource) {
                 this.props.onContextMenu(event, {
-                    name: '',
+                    name: resource.name,
                     uuid: resource.uuid,
+                    description: resource.description,
                     ownerUuid: resource.ownerUuid,
                     kind: resource.kind,
                     menuKind: ContextMenuKind.GROUPS

commit e02fcdadca54f1dc970734f7c5ce0bc6407e10c6
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Nov 10 11:01:12 2021 -0500

    18123: Use project update dialog for renaming group
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index 9c9f15cf..1c4a0732 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from 'redux';
-import { reset, initialize, startSubmit, stopSubmit, FormErrors } from 'redux-form';
+import { reset, startSubmit, stopSubmit, FormErrors } from 'redux-form';
 import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
 import { dialogActions } from 'store/dialog/dialog-actions';
 import { Participant } from 'views-components/sharing-dialog/participant-select';
@@ -25,12 +25,6 @@ export const CREATE_GROUP_FORM = "createGroupForm";
 export const CREATE_GROUP_NAME_FIELD_NAME = 'name';
 export const CREATE_GROUP_USERS_FIELD_NAME = 'users';
 
-// Rename group dialog
-export const RENAME_GROUP_DIALOG = "renameGroupDialog";
-export const RENAME_GROUP_FORM = "renameGroupForm";
-export const RENAME_GROUP_UUID_FIELD_NAME = 'uuid';
-export const RENAME_GROUP_NAME_FIELD_NAME = 'name';
-
 export const GROUP_ATTRIBUTES_DIALOG = 'groupAttributesDialog';
 export const GROUP_REMOVE_DIALOG = 'groupRemoveDialog';
 
@@ -72,33 +66,6 @@ export const openRemoveGroupDialog = (uuid: string) =>
         }));
     };
 
-export interface RenameGroupFormData {
-    [RENAME_GROUP_UUID_FIELD_NAME]: string;
-    [RENAME_GROUP_NAME_FIELD_NAME]: string;
-}
-
-export const openRenameGroupDialog = (uuid: string) =>
-    (dispatch: Dispatch, getState: () => RootState) => {
-        const group = getResource<GroupResource>(uuid)(getState().resources);
-
-        if (group) {
-            const formData: RenameGroupFormData = {[RENAME_GROUP_UUID_FIELD_NAME]: group.uuid, [RENAME_GROUP_NAME_FIELD_NAME]: group.name};
-            console.log("Initialize form: ", formData);
-            dispatch(reset(RENAME_GROUP_FORM));
-            dispatch<any>(initialize(RENAME_GROUP_FORM, formData));
-            dispatch(dialogActions.OPEN_DIALOG({ id: RENAME_GROUP_DIALOG, data: group }));
-        }
-    };
-
-
-export const renameGroup = (data: RenameGroupFormData) =>
-    async (dispatch: Dispatch, getState: () => RootState, { groupsService }: ServiceRepository) => {
-        console.log("RenameGroupFormData", data);
-        await groupsService.update(data[RENAME_GROUP_UUID_FIELD_NAME], { name: data[RENAME_GROUP_NAME_FIELD_NAME] });
-        dispatch(dialogActions.CLOSE_DIALOG({ id: RENAME_GROUP_DIALOG }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Renamed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-    };
-
 export interface CreateGroupFormData {
     [CREATE_GROUP_NAME_FIELD_NAME]: string;
     [CREATE_GROUP_USERS_FIELD_NAME]?: Participant[];
diff --git a/src/views-components/context-menu/action-sets/group-action-set.ts b/src/views-components/context-menu/action-sets/group-action-set.ts
index 3fbec27e..f2c9b92f 100644
--- a/src/views-components/context-menu/action-sets/group-action-set.ts
+++ b/src/views-components/context-menu/action-sets/group-action-set.ts
@@ -5,13 +5,14 @@
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
 import { RenameIcon, AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { openRenameGroupDialog, openGroupAttributes, openRemoveGroupDialog } from "store/groups-panel/groups-panel-actions";
+import { openGroupAttributes, openRemoveGroupDialog } from "store/groups-panel/groups-panel-actions";
+import { openProjectUpdateDialog } from "store/projects/project-update-actions";
 
 export const groupActionSet: ContextMenuActionSet = [[{
     name: "Rename",
     icon: RenameIcon,
-    execute: (dispatch, { uuid }) => {
-        dispatch<any>(openRenameGroupDialog(uuid));
+    execute: (dispatch, resource) => {
+        dispatch<any>(openProjectUpdateDialog(resource));
     }
 }, {
     name: "Attributes",
diff --git a/src/views-components/dialog-forms/rename-group-dialog.tsx b/src/views-components/dialog-forms/rename-group-dialog.tsx
deleted file mode 100644
index 72f09d78..00000000
--- a/src/views-components/dialog-forms/rename-group-dialog.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { compose, Dispatch } from 'redux';
-import { reduxForm, InjectedFormProps, Field } from 'redux-form';
-import { withDialog, WithDialogProps } from 'store/dialog/with-dialog';
-import { FormDialog } from 'components/form-dialog/form-dialog';
-import { DialogContentText } from '@material-ui/core';
-import { TextField } from 'components/text-field/text-field';
-import { GroupResource } from 'models/group';
-import { RENAME_GROUP_DIALOG, RENAME_GROUP_NAME_FIELD_NAME, RenameGroupFormData, renameGroup } from 'store/groups-panel/groups-panel-actions';
-// import { WarningCollection } from 'components/warning-collection/warning-collection';
-import { RENAME_FILE_VALIDATION } from 'validators/validators';
-
-export const RenameGroupDialog = compose(
-    withDialog(RENAME_GROUP_DIALOG),
-    reduxForm<RenameGroupFormData>({
-        form: RENAME_GROUP_DIALOG,
-        // touchOnChange: true,
-        onSubmit: (data: RenameGroupFormData, dispatch: Dispatch) => {
-            console.log(data);
-            // dispatch<any>(renameGroup(data));
-        }
-    })
-)((props: RenameGroupDialogProps) =>
-    <FormDialog
-        dialogTitle='Rename'
-        formFields={RenameGroupFormFields}
-        submitLabel='Ok'
-        {...props}
-    />);
-
-interface RenameGroupDataProps {
-    data: GroupResource;
-}
-
-type RenameGroupDialogProps = RenameGroupDataProps & WithDialogProps<{}> & InjectedFormProps<RenameGroupFormData>;
-
-const RenameGroupFormFields = (props: RenameGroupDialogProps) => {
-    // console.log(props);
-    return <>
-        <DialogContentText>
-            {`Please enter a new name for ${props.data.name}`}
-        </DialogContentText>
-        <Field
-            name={RENAME_GROUP_NAME_FIELD_NAME}
-            component={TextField as any}
-            autoFocus={true}
-            validate={RENAME_FILE_VALIDATION}
-        />
-        {/* <WarningCollection text="Renaming a file will change the collection's content address." /> */}
-    </>;
-}
diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx
index d99b98ff..a9f59f68 100644
--- a/src/views/groups-panel/groups-panel.tsx
+++ b/src/views/groups-panel/groups-panel.tsx
@@ -21,7 +21,6 @@ import { RootState } from 'store/store';
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourceKind } from 'models/resource';
 import { LinkClass, LinkResource } from 'models/link';
-import { navigateToGroupDetails } from 'store/navigation/navigation-action';
 
 export enum GroupsPanelColumnNames {
     GROUP = "Name",
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 8b687632..50194f9e 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -90,7 +90,6 @@ import { RemoveGroupMemberDialog } from 'views-components/groups-dialog/member-r
 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 { RenameGroupDialog } from 'views-components/dialog-forms/rename-group-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';
@@ -215,7 +214,6 @@ export const WorkbenchPanel =
                 <DetailsPanel />
             </Grid>
             <AddGroupMembersDialog />
-            <RenameGroupDialog />
             <EditPermissionLevelDialog />
             <AdvancedTabDialog />
             <AttributesApiClientAuthorizationDialog />

commit de3aa7cd755ae48855c0a8031a67d66237755fe0
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 9 15:40:06 2021 -0500

    18123: Add group rename dialog
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/dialog-forms/rename-group-dialog.tsx b/src/views-components/dialog-forms/rename-group-dialog.tsx
new file mode 100644
index 00000000..72f09d78
--- /dev/null
+++ b/src/views-components/dialog-forms/rename-group-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, Dispatch } from 'redux';
+import { reduxForm, InjectedFormProps, Field } from 'redux-form';
+import { withDialog, WithDialogProps } from 'store/dialog/with-dialog';
+import { FormDialog } from 'components/form-dialog/form-dialog';
+import { DialogContentText } from '@material-ui/core';
+import { TextField } from 'components/text-field/text-field';
+import { GroupResource } from 'models/group';
+import { RENAME_GROUP_DIALOG, RENAME_GROUP_NAME_FIELD_NAME, RenameGroupFormData, renameGroup } from 'store/groups-panel/groups-panel-actions';
+// import { WarningCollection } from 'components/warning-collection/warning-collection';
+import { RENAME_FILE_VALIDATION } from 'validators/validators';
+
+export const RenameGroupDialog = compose(
+    withDialog(RENAME_GROUP_DIALOG),
+    reduxForm<RenameGroupFormData>({
+        form: RENAME_GROUP_DIALOG,
+        // touchOnChange: true,
+        onSubmit: (data: RenameGroupFormData, dispatch: Dispatch) => {
+            console.log(data);
+            // dispatch<any>(renameGroup(data));
+        }
+    })
+)((props: RenameGroupDialogProps) =>
+    <FormDialog
+        dialogTitle='Rename'
+        formFields={RenameGroupFormFields}
+        submitLabel='Ok'
+        {...props}
+    />);
+
+interface RenameGroupDataProps {
+    data: GroupResource;
+}
+
+type RenameGroupDialogProps = RenameGroupDataProps & WithDialogProps<{}> & InjectedFormProps<RenameGroupFormData>;
+
+const RenameGroupFormFields = (props: RenameGroupDialogProps) => {
+    // console.log(props);
+    return <>
+        <DialogContentText>
+            {`Please enter a new name for ${props.data.name}`}
+        </DialogContentText>
+        <Field
+            name={RENAME_GROUP_NAME_FIELD_NAME}
+            component={TextField as any}
+            autoFocus={true}
+            validate={RENAME_FILE_VALIDATION}
+        />
+        {/* <WarningCollection text="Renaming a file will change the collection's content address." /> */}
+    </>;
+}

commit 12d2c589092bc5cead8ded7ea2148949969bc477
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 9 14:38:46 2021 -0500

    18123: Partial group rename dialog
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index 099d046d..9c9f15cf 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Dispatch } from 'redux';
-import { reset, startSubmit, stopSubmit, FormErrors } from 'redux-form';
+import { reset, initialize, startSubmit, stopSubmit, FormErrors } from 'redux-form';
 import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
 import { dialogActions } from 'store/dialog/dialog-actions';
 import { Participant } from 'views-components/sharing-dialog/participant-select';
@@ -18,10 +18,19 @@ import { PermissionService } from 'services/permission-service/permission-servic
 import { FilterBuilder } from 'services/api/filter-builder';
 
 export const GROUPS_PANEL_ID = "groupsPanel";
+
+// Create group dialog
 export const CREATE_GROUP_DIALOG = "createGroupDialog";
 export const CREATE_GROUP_FORM = "createGroupForm";
 export const CREATE_GROUP_NAME_FIELD_NAME = 'name';
 export const CREATE_GROUP_USERS_FIELD_NAME = 'users';
+
+// Rename group dialog
+export const RENAME_GROUP_DIALOG = "renameGroupDialog";
+export const RENAME_GROUP_FORM = "renameGroupForm";
+export const RENAME_GROUP_UUID_FIELD_NAME = 'uuid';
+export const RENAME_GROUP_NAME_FIELD_NAME = 'name';
+
 export const GROUP_ATTRIBUTES_DIALOG = 'groupAttributesDialog';
 export const GROUP_REMOVE_DIALOG = 'groupRemoveDialog';
 
@@ -63,6 +72,33 @@ export const openRemoveGroupDialog = (uuid: string) =>
         }));
     };
 
+export interface RenameGroupFormData {
+    [RENAME_GROUP_UUID_FIELD_NAME]: string;
+    [RENAME_GROUP_NAME_FIELD_NAME]: string;
+}
+
+export const openRenameGroupDialog = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const group = getResource<GroupResource>(uuid)(getState().resources);
+
+        if (group) {
+            const formData: RenameGroupFormData = {[RENAME_GROUP_UUID_FIELD_NAME]: group.uuid, [RENAME_GROUP_NAME_FIELD_NAME]: group.name};
+            console.log("Initialize form: ", formData);
+            dispatch(reset(RENAME_GROUP_FORM));
+            dispatch<any>(initialize(RENAME_GROUP_FORM, formData));
+            dispatch(dialogActions.OPEN_DIALOG({ id: RENAME_GROUP_DIALOG, data: group }));
+        }
+    };
+
+
+export const renameGroup = (data: RenameGroupFormData) =>
+    async (dispatch: Dispatch, getState: () => RootState, { groupsService }: ServiceRepository) => {
+        console.log("RenameGroupFormData", data);
+        await groupsService.update(data[RENAME_GROUP_UUID_FIELD_NAME], { name: data[RENAME_GROUP_NAME_FIELD_NAME] });
+        dispatch(dialogActions.CLOSE_DIALOG({ id: RENAME_GROUP_DIALOG }));
+        dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Renamed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+    };
+
 export interface CreateGroupFormData {
     [CREATE_GROUP_NAME_FIELD_NAME]: string;
     [CREATE_GROUP_USERS_FIELD_NAME]?: Participant[];
diff --git a/src/views-components/context-menu/action-sets/group-action-set.ts b/src/views-components/context-menu/action-sets/group-action-set.ts
index ad38cbeb..3fbec27e 100644
--- a/src/views-components/context-menu/action-sets/group-action-set.ts
+++ b/src/views-components/context-menu/action-sets/group-action-set.ts
@@ -3,11 +3,17 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
+import { RenameIcon, AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { openGroupAttributes, openRemoveGroupDialog } from "store/groups-panel/groups-panel-actions";
+import { openRenameGroupDialog, openGroupAttributes, openRemoveGroupDialog } from "store/groups-panel/groups-panel-actions";
 
 export const groupActionSet: ContextMenuActionSet = [[{
+    name: "Rename",
+    icon: RenameIcon,
+    execute: (dispatch, { uuid }) => {
+        dispatch<any>(openRenameGroupDialog(uuid));
+    }
+}, {
     name: "Attributes",
     icon: AttributesIcon,
     execute: (dispatch, { uuid }) => {
@@ -25,4 +31,4 @@ export const groupActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openRemoveGroupDialog(uuid));
     }
-}]];
\ No newline at end of file
+}]];
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 50194f9e..8b687632 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -90,6 +90,7 @@ import { RemoveGroupMemberDialog } from 'views-components/groups-dialog/member-r
 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 { RenameGroupDialog } from 'views-components/dialog-forms/rename-group-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';
@@ -214,6 +215,7 @@ export const WorkbenchPanel =
                 <DetailsPanel />
             </Grid>
             <AddGroupMembersDialog />
+            <RenameGroupDialog />
             <EditPermissionLevelDialog />
             <AdvancedTabDialog />
             <AttributesApiClientAuthorizationDialog />

commit 5d0a0226a4ea4bb98f35d4ce76698f7a6606bfb4
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 9 14:37:51 2021 -0500

    18123: Fix extraneous props.dispatch being passed to Typography warning
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx
index 98dc78ee..d99b98ff 100644
--- a/src/views/groups-panel/groups-panel.tsx
+++ b/src/views/groups-panel/groups-panel.tsx
@@ -127,4 +127,4 @@ const GroupMembersCount = connect(
         };
 
     }
-)(Typography);
+)((props: {children: number}) => (<Typography children={props.children} />));

commit 1dcbe2d0ec3fdc613edca3a490d0adfce023a803
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Nov 9 14:29:50 2021 -0500

    18123: Remove unnecessary cick handlers
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index 50838f7d..b4a8c6d3 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -153,8 +153,8 @@ export const GroupDetailsPanel = connect(
                           id={GROUP_DETAILS_MEMBERS_PANEL_ID}
                           onRowClick={noop}
                           onRowDoubleClick={noop}
-                          onContextMenu={this.handleContextMenu}
-                          contextMenuColumn={true}
+                          onContextMenu={noop}
+                          contextMenuColumn={false}
                           hideColumnSelector
                           hideSearchInput
                           actions={
@@ -176,8 +176,8 @@ export const GroupDetailsPanel = connect(
                           id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
                           onRowClick={noop}
                           onRowDoubleClick={noop}
-                          onContextMenu={this.handleContextMenu}
-                          contextMenuColumn={true}
+                          onContextMenu={noop}
+                          contextMenuColumn={false}
                           hideColumnSelector
                           hideSearchInput
                           paperProps={{
diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx
index 9bfad524..98dc78ee 100644
--- a/src/views/groups-panel/groups-panel.tsx
+++ b/src/views/groups-panel/groups-panel.tsx
@@ -62,15 +62,12 @@ const mapStateToProps = (state: RootState) => {
 
 const mapDispatchToProps = {
     onContextMenu: openContextMenu,
-    onRowDoubleClick: (uuid: string) =>
-        navigateToGroupDetails(uuid),
     onNewGroup: openCreateGroupDialog,
 };
 
 export interface GroupsPanelProps {
     onNewGroup: () => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
-    onRowDoubleClick: (item: string) => void;
     resources: ResourcesState;
 }
 
@@ -84,7 +81,7 @@ export const GroupsPanel = connect(
                 <DataExplorer
                     id={GROUPS_PANEL_ID}
                     onRowClick={noop}
-                    onRowDoubleClick={this.props.onRowDoubleClick}
+                    onRowDoubleClick={noop}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
                     hideColumnSelector

commit 1277b2a092fbd057220ee43d6fc47bffff5933d1
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Nov 8 10:09:33 2021 -0500

    18123: Add edit permission level dialog for group members and outgoing permissions.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

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<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();
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) =>
-    <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
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<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} />;
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<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,
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<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,
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 =
                 <DetailsPanel />
             </Grid>
             <AddGroupMembersDialog />
+            <EditPermissionLevelDialog />
             <AdvancedTabDialog />
             <AttributesApiClientAuthorizationDialog />
             <AttributesKeepServiceDialog />

commit b565525602ad5203d313abe6b898f1885e344abc
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Nov 4 10:17:05 2021 -0400

    18123: Filter groups list to type role instead of excluding all other types.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/groups-panel/groups-panel-middleware-service.ts b/src/store/groups-panel/groups-panel-middleware-service.ts
index c157e9ab..28415506 100644
--- a/src/store/groups-panel/groups-panel-middleware-service.ts
+++ b/src/store/groups-panel/groups-panel-middleware-service.ts
@@ -36,7 +36,7 @@ export class GroupsPanelMiddlewareService extends DataExplorerMiddlewareService
                     order.addOrder(direction, 'name');
                 }
                 const filters = new FilterBuilder()
-                    .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
+                    .addEqual('group_class', GroupClass.ROLE)
                     .addILike('name', dataExplorer.searchValue)
                     .getFilters();
                 const response = await this.services.groupsService

commit d6c180028671059f19912a11887b804e9d63d608
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Nov 3 20:57:53 2021 -0400

    18123: Allow hiding search box while actions are present for group details.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index d272e870..f3cccfce 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -98,14 +98,14 @@ export const DataExplorer = withStyles(styles)(
             } = this.props;
             return <Paper className={classes.root} {...paperProps} key={paperKey}>
                 {title && <div className={classes.title}>{title}</div>}
-                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
+                {(!hideColumnSelector || !hideSearchInput || !!actions) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
-                        <div className={classes.searchBox}>
+                        {!hideSearchInput && <div className={classes.searchBox}>
                             {!hideSearchInput && <SearchInput
                                 label={searchLabel}
                                 value={searchValue}
                                 onSearch={onSearch} />}
-                        </div>
+                        </div>}
                         {actions}
                         {!hideColumnSelector && <ColumnSelector
                             columns={columns}
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index f292b97b..c402ebb6 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -180,16 +180,6 @@ export const GroupDetailsPanel = connect(
                           contextMenuColumn={true}
                           hideColumnSelector
                           hideSearchInput
-                          actions={
-                              <Grid container justify='flex-end'>
-                                  <Button
-                                      variant="contained"
-                                      color="primary"
-                                      onClick={this.props.onAddUser}>
-                                      <AddIcon /> Add user
-                              </Button>
-                              </Grid>
-                          }
                           paperProps={{
                               elevation: 0,
                           }} />

commit 703a2e1813ed1ff80d2ccd3214233240802b4754
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Nov 3 20:39:23 2021 -0400

    18123: Fix directionality of link tail renderer.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 77b5b694..71b82b6f 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -309,7 +309,7 @@ export const ResourceLinkTail = connect(
         const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
 
         return {
-            item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.headKind || ResourceKind.NONE }
+            item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE }
         };
     })((props: { item: Resource } & DispatchProp<any>) =>
         renderLink(props.dispatch, props.item));

commit 8d40870a23aa60855ed4b1a43a0186b7d50c0d7d
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Nov 3 20:35:01 2021 -0400

    18123: Add role group class to model to fix creating groups.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/models/group.ts b/src/models/group.ts
index 365e9cce..7d144a58 100644
--- a/src/models/group.ts
+++ b/src/models/group.ts
@@ -17,4 +17,5 @@ export interface GroupResource extends TrashableResource {
 export enum GroupClass {
     PROJECT = 'project',
     FILTER  = 'filter',
+    ROLE  = 'role',
 }
diff --git a/src/models/project.ts b/src/models/project.ts
index 86ac04f6..b47b426f 100644
--- a/src/models/project.ts
+++ b/src/models/project.ts
@@ -5,7 +5,7 @@
 import { GroupClass, GroupResource } from "./group";
 
 export interface ProjectResource extends GroupResource {
-    groupClass: GroupClass.PROJECT | GroupClass.FILTER;
+    groupClass: GroupClass.PROJECT | GroupClass.FILTER | GroupClass.ROLE;
 }
 
 export const getProjectUrl = (uuid: string) => {
diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index dcf81f2d..099d046d 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -10,7 +10,7 @@ import { Participant } from 'views-components/sharing-dialog/participant-select'
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { getResource } from 'store/resources/resources';
-import { GroupResource } from 'models/group';
+import { GroupResource, GroupClass } from 'models/group';
 import { getCommonResourceServiceError, CommonResourceServiceError } from 'services/common-service/common-resource-service';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { PermissionLevel } from 'models/permission';
@@ -72,7 +72,7 @@ export const createGroup = ({ name, users = [] }: CreateGroupFormData) =>
     async (dispatch: Dispatch, _: {}, { groupsService, permissionService }: ServiceRepository) => {
         dispatch(startSubmit(CREATE_GROUP_FORM));
         try {
-            const newGroup = await groupsService.create({ name });
+            const newGroup = await groupsService.create({ name, groupClass: GroupClass.ROLE });
             for (const user of users) {
                 await addGroupMember({
                     user,

commit 798064c6616d98f8b6b3dfe562458bffe662f4ad
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Oct 25 10:26:50 2021 -0400

    18123: Add group data table renderers
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 05ae4ce2..77b5b694 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 } from 'components/icon/icon';
+import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon } from 'components/icon/icon';
 import { formatDate, formatFileSize, formatTime } from 'common/formatters';
 import { resourceLabel } from 'common/labels';
 import { connect, DispatchProp } from 'react-redux';
@@ -29,6 +29,7 @@ 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';
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -286,20 +287,6 @@ export const ResourceLinkClass = connect(
         return resource || { linkClass: '' };
     })(renderLinkClass);
 
-// const renderLinkTail = (dispatch: Dispatch, item: { uuid: string, tailUuid: string, tailKind: string }) => {
-//     const currentLabel = resourceLabel(item.tailKind);
-//     const isUnknow = currentLabel === "Unknown";
-//     return (<div>
-//         {!isUnknow ? (
-//             renderLink(dispatch, item.tailUuid, "name", currentLabel)
-//         ) : (
-//                 <Typography noWrap color="default">
-//                     {item.tailUuid}
-//                 </Typography>
-//             )}
-//     </div>);
-// };
-
 const renderLink = (dispatch: Dispatch, item: Resource) => {
     var displayName = '';
 
@@ -344,6 +331,59 @@ export const ResourceLinkUuid = connect(
         return resource || { uuid: '' };
     })(renderUuid);
 
+export const ResourceLinkHeadUuid = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
+
+        return headResource || { uuid: '' };
+    })(renderUuid);
+
+export const ResourceLinkTailUuid = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
+
+        return tailResource || { uuid: '' };
+    })(renderUuid);
+
+const renderLinkDelete = (dispatch: Dispatch, item: LinkResource) => {
+    if (item.uuid) {
+        return <Typography noWrap>
+            <IconButton onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
+                <RemoveIcon />
+            </IconButton>
+        </Typography>;
+    } else {
+      return <Typography noWrap></Typography>;
+    }
+}
+
+export const ResourceLinkDelete = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        return {
+            item: link || { uuid: '', kind: ResourceKind.NONE }
+        };
+    })((props: { item: LinkResource } & DispatchProp<any>) =>
+      renderLinkDelete(props.dispatch, props.item));
+
+export const ResourceLinkTailEmail = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
+
+        return resource || { email: '' };
+    })(renderEmail);
+
+export const ResourceLinkTailUsername = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
+
+        return resource || { username: '' };
+    })(renderUsername);
+
 // Process Resources
 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
     return (
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index 5b1a884e..f292b97b 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 { ResourceUuid, ResourceEmail, ResourceUsername, ResourceLinkName, ResourceLinkHead, ResourceLinkTail } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkTailUuid, ResourceLinkTailEmail, ResourceLinkTailUsername, ResourceLinkName, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -24,12 +24,15 @@ export enum GroupDetailsPanelMembersColumnNames {
     UUID = "UUID",
     EMAIL = "Email",
     USERNAME = "Username",
+    PERMISSION = "Permission",
+    REMOVE = "Remove",
 }
 
 export enum GroupDetailsPanelPermissionsColumnNames {
-    HEAD = "Head",
     NAME = "Name",
+    PERMISSION = "Permission",
     UUID = "UUID",
+    REMOVE = "Remove",
 }
 
 export const groupDetailsMembersPanelColumns: DataColumns<string> = [
@@ -45,34 +48,48 @@ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceUsername uuid={uuid} />
+        render: uuid => <ResourceLinkTailUsername uuid={uuid} />
+    },
+    {
+        name: GroupDetailsPanelMembersColumnNames.EMAIL,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkTailEmail uuid={uuid} />
+    },
+    {
+        name: GroupDetailsPanelMembersColumnNames.PERMISSION,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkName uuid={uuid} />
     },
     {
         name: GroupDetailsPanelMembersColumnNames.UUID,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceUuid uuid={uuid} />
+        render: uuid => <ResourceLinkTailUuid uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelMembersColumnNames.EMAIL,
+        name: GroupDetailsPanelMembersColumnNames.REMOVE,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceEmail uuid={uuid} />
+        render: uuid => <ResourceLinkDelete uuid={uuid} />
     },
 ];
 
 export const groupDetailsPermissionsPanelColumns: DataColumns<string> = [
     {
-        name: GroupDetailsPanelPermissionsColumnNames.HEAD,
+        name: GroupDetailsPanelPermissionsColumnNames.NAME,
         selected: true,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceLinkHead uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelPermissionsColumnNames.NAME,
+        name: GroupDetailsPanelPermissionsColumnNames.PERMISSION,
         selected: true,
         configurable: true,
         filters: createTree(),
@@ -83,7 +100,14 @@ export const groupDetailsPermissionsPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceUuid uuid={uuid} />
+        render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
+    },
+    {
+        name: GroupDetailsPanelPermissionsColumnNames.REMOVE,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkDelete uuid={uuid} />
     },
 ];
 

commit 0399c993a117c3489f1fe0160d6a554a56b8bbab
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Oct 25 10:24:43 2021 -0400

    18123: Fix directionality of queried links for group member count
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/groups-panel/groups-panel-middleware-service.ts b/src/store/groups-panel/groups-panel-middleware-service.ts
index 5fb4718d..c157e9ab 100644
--- a/src/store/groups-panel/groups-panel-middleware-service.ts
+++ b/src/store/groups-panel/groups-panel-middleware-service.ts
@@ -52,7 +52,7 @@ export class GroupsPanelMiddlewareService extends DataExplorerMiddlewareService
                 }));
                 const permissions = await this.services.permissionService.list({
                     filters: new FilterBuilder()
-                        .addIn('tail_uuid', response.items.map(item => item.uuid))
+                        .addIn('head_uuid', response.items.map(item => item.uuid))
                         .getFilters()
                 });
                 api.dispatch(updateResources(permissions.items));
@@ -74,4 +74,3 @@ const couldNotFetchFavoritesContents = () =>
         message: 'Could not fetch groups.',
         kind: SnackbarKind.ERROR
     });
-
diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx
index 4d15118c..04f2a273 100644
--- a/src/views/groups-panel/groups-panel.tsx
+++ b/src/views/groups-panel/groups-panel.tsx
@@ -122,7 +122,7 @@ const GroupMembersCount = connect(
         const permissions = filterResources((resource: LinkResource) =>
             resource.kind === ResourceKind.LINK &&
             resource.linkClass === LinkClass.PERMISSION &&
-            resource.tailUuid === props.uuid
+            resource.headUuid === props.uuid
         )(state.resources);
 
         return {

commit acceec90afa2a2b5007ab75795c911ac75446bc4
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Oct 25 10:07:37 2021 -0400

    18123: Filter uuids by kind before querying API
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

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 b9ac0d79..22247a8f 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -19,8 +19,8 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 
 export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
 export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
-export const ADD_GROUP_MEMBERS_DIALOG = 'addGrupMembers';
-export const ADD_GROUP_MEMBERS_FORM = 'addGrupMembers';
+export const ADD_GROUP_MEMBERS_DIALOG = 'addGroupMembers';
+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';
diff --git a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
index 05a12d22..36e3c735 100644
--- a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
@@ -12,6 +12,7 @@ import { FilterBuilder } from 'services/api/filter-builder';
 import { updateResources } from 'store/resources/resources-actions';
 import { getCurrentGroupDetailsPanelUuid, GroupMembersPanelActions } from 'store/group-details-panel/group-details-panel-actions';
 import { LinkClass } from 'models/link';
+import { ResourceKind } from 'models/resource';
 
 export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddlewareService {
 
@@ -41,7 +42,9 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
 
                 const usersIn = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
+                        .addIn('uuid', permissionsIn.items
+                            .filter((item) => item.tailKind === ResourceKind.USER)
+                            .map(item => item.tailUuid))
                         .getFilters(),
                     count: "none"
                 });
@@ -49,7 +52,9 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
 
                 const projectsIn = await this.services.projectService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
+                        .addIn('uuid', permissionsIn.items
+                            .filter((item) => item.tailKind === ResourceKind.PROJECT)
+                            .map(item => item.tailUuid))
                         .getFilters(),
                     count: "none"
                 });
diff --git a/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
index 7ce2d380..9e41409d 100644
--- a/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
@@ -12,6 +12,7 @@ import { FilterBuilder } from 'services/api/filter-builder';
 import { updateResources } from 'store/resources/resources-actions';
 import { getCurrentGroupDetailsPanelUuid, GroupPermissionsPanelActions } from 'store/group-details-panel/group-details-panel-actions';
 import { LinkClass } from 'models/link';
+import { ResourceKind } from 'models/resource';
 
 export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerMiddlewareService {
 
@@ -41,7 +42,9 @@ export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerM
 
                 const usersOut = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsOut.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsOut.items
+                            .filter((item) => item.headKind === ResourceKind.USER)
+                            .map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
@@ -49,7 +52,9 @@ export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerM
 
                 const collectionsOut = await this.services.collectionService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsOut.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsOut.items
+                            .filter((item) => item.headKind === ResourceKind.COLLECTION)
+                            .map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
@@ -57,7 +62,9 @@ export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerM
 
                 const projectsOut = await this.services.projectService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsOut.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsOut.items
+                            .filter((item) => item.headKind === ResourceKind.PROJECT)
+                            .map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });

commit 418b900ceaaace5aa2e844959dee9c41d35fbe2d
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Oct 20 20:55:07 2021 -0400

    18123: Fix add and remove group member.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

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 e71f75f3..b9ac0d79 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -16,7 +16,6 @@ import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
 import { PermissionResource } from 'models/permission';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { UserResource, getUserDisplayName } from 'models/user';
 
 export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
 export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
@@ -108,20 +107,11 @@ export const removeGroupMember = (uuid: string) =>
         const groupUuid = getCurrentGroupDetailsPanelUuid(getState().properties);
 
         if (groupUuid) {
-
-            const group = getResource<GroupResource>(groupUuid)(getState().resources);
-            const user = getResource<UserResource>(groupUuid)(getState().resources);
-
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
 
             await deleteGroupMember({
-                user: {
+                link: {
                     uuid,
-                    name: user ? getUserDisplayName(user) : uuid,
-                },
-                group: {
-                    uuid: groupUuid,
-                    name: group ? group.name : groupUuid,
                 },
                 permissionService,
                 dispatch,
diff --git a/src/store/groups-panel/groups-panel-actions.ts b/src/store/groups-panel/groups-panel-actions.ts
index 0d92e946..dcf81f2d 100644
--- a/src/store/groups-panel/groups-panel-actions.ts
+++ b/src/store/groups-panel/groups-panel-actions.ts
@@ -113,8 +113,8 @@ interface AddGroupMemberArgs {
  */
 export const addGroupMember = async ({ user, group, ...args }: AddGroupMemberArgs) => {
     await createPermission({
-        head: { ...user },
-        tail: { ...group },
+        head: { ...group },
+        tail: { ...user },
         permissionLevel: PermissionLevel.CAN_READ,
         ...args,
     });
@@ -144,33 +144,29 @@ const createPermission = async ({ head, tail, permissionLevel, dispatch, permiss
 };
 
 interface DeleteGroupMemberArgs {
-    user: { uuid: string, name: string };
-    group: { uuid: string, name: string };
+    link: { uuid: string };
     dispatch: Dispatch;
     permissionService: PermissionService;
 }
 
-export const deleteGroupMember = async ({ user, group, ...args }: DeleteGroupMemberArgs) => {
+export const deleteGroupMember = async ({ link, ...args }: DeleteGroupMemberArgs) => {
     await deletePermission({
-        tail: group,
-        head: user,
+        uuid: link.uuid,
         ...args,
     });
 };
 
 interface DeletePermissionLinkArgs {
-    head: { uuid: string, name: string };
-    tail: { uuid: string, name: string };
+    uuid: string;
     dispatch: Dispatch;
     permissionService: PermissionService;
 }
 
-export const deletePermission = async ({ head, tail, dispatch, permissionService }: DeletePermissionLinkArgs) => {
+export const deletePermission = async ({ uuid, dispatch, permissionService }: DeletePermissionLinkArgs) => {
     try {
         const permissionsResponse = await permissionService.list({
             filters: new FilterBuilder()
-                .addEqual('tail_uuid', tail.uuid)
-                .addEqual('head_uuid', head.uuid)
+                .addEqual('uuid', uuid)
                 .getFilters()
         });
         const [permission] = permissionsResponse.items;
@@ -181,8 +177,8 @@ export const deletePermission = async ({ head, tail, dispatch, permissionService
         }
     } catch (e) {
         dispatch(snackbarActions.OPEN_SNACKBAR({
-            message: `Could not delete ${tail.name} -> ${head.name} relation`,
+            message: `Could not delete ${uuid} permission`,
             kind: SnackbarKind.ERROR,
         }));
     }
-};
\ No newline at end of file
+};

commit 2b6003f9be46bd178159a116886b02192e2ebfaa
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Oct 13 19:59:49 2021 -0400

    18123: Add projects to group member list, make variables and error messages clearer.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
index 23f0bbfd..05a12d22 100644
--- a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
@@ -26,26 +26,34 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
             api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
         } else {
             try {
-                const permissions = await this.services.permissionService.list({
+                const permissionsIn = await this.services.permissionService.list({
                     filters: new FilterBuilder()
                         .addEqual('head_uuid', groupUuid)
                         .addEqual('link_class', LinkClass.PERMISSION)
                         .getFilters()
                 });
-                api.dispatch(updateResources(permissions.items));
+                api.dispatch(updateResources(permissionsIn.items));
 
-                const users = await this.services.userService.list({
+                api.dispatch(GroupMembersPanelActions.SET_ITEMS({
+                    ...listResultsToDataExplorerItemsMeta(permissionsIn),
+                    items: permissionsIn.items.map(item => item.uuid),
+                }));
+
+                const usersIn = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissions.items.map(item => item.tailUuid))
+                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
                         .getFilters(),
                     count: "none"
                 });
-                api.dispatch(updateResources(users.items));
+                api.dispatch(updateResources(usersIn.items));
 
-                api.dispatch(GroupMembersPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissions),
-                    items: permissions.items.map(item => item.uuid),
-                }));
+                const projectsIn = await this.services.projectService.list({
+                    filters: new FilterBuilder()
+                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
+                        .getFilters(),
+                    count: "none"
+                });
+                api.dispatch(updateResources(projectsIn.items));
             } catch (e) {
                 api.dispatch(couldNotFetchGroupDetailsContents());
             }
@@ -55,12 +63,12 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
 
 const groupsDetailsPanelDataExplorerIsNotSet = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Group details panel is not ready.',
+        message: 'Group members panel is not ready.',
         kind: SnackbarKind.ERROR
     });
 
 const couldNotFetchGroupDetailsContents = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch group details.',
+        message: 'Could not fetch group members.',
         kind: SnackbarKind.ERROR
     });
diff --git a/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
index 91bff1e9..7ce2d380 100644
--- a/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
@@ -26,42 +26,42 @@ export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerM
             api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
         } else {
             try {
-                const permissions = await this.services.permissionService.list({
+                const permissionsOut = await this.services.permissionService.list({
                     filters: new FilterBuilder()
                         .addEqual('tail_uuid', groupUuid)
                         .addEqual('link_class', LinkClass.PERMISSION)
                         .getFilters()
                 });
-                api.dispatch(updateResources(permissions.items));
+                api.dispatch(updateResources(permissionsOut.items));
 
-                const users = await this.services.userService.list({
+                api.dispatch(GroupPermissionsPanelActions.SET_ITEMS({
+                    ...listResultsToDataExplorerItemsMeta(permissionsOut),
+                    items: permissionsOut.items.map(item => item.uuid),
+                }));
+
+                const usersOut = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissions.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsOut.items.map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
-                api.dispatch(updateResources(users.items));
+                api.dispatch(updateResources(usersOut.items));
 
-                const collections = await this.services.collectionService.list({
+                const collectionsOut = await this.services.collectionService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissions.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsOut.items.map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
-                api.dispatch(updateResources(collections.items));
+                api.dispatch(updateResources(collectionsOut.items));
 
-                const projects = await this.services.projectService.list({
+                const projectsOut = await this.services.projectService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissions.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsOut.items.map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
-                api.dispatch(updateResources(projects.items));
-
-                api.dispatch(GroupPermissionsPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissions),
-                    items: permissions.items.map(item => item.uuid),
-                }));
+                api.dispatch(updateResources(projectsOut.items));
             } catch (e) {
                 api.dispatch(couldNotFetchGroupDetailsContents());
             }

commit 8a6c5e3b1ac52cdd6e7e07f8349da92b31216c76
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Oct 13 17:07:27 2021 -0400

    18123: List all permission links in group members list & cleanup.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
index e295579a..23f0bbfd 100644
--- a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
@@ -44,7 +44,7 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
 
                 api.dispatch(GroupMembersPanelActions.SET_ITEMS({
                     ...listResultsToDataExplorerItemsMeta(permissions),
-                    items: users.items.map(item => item.uuid),
+                    items: permissions.items.map(item => item.uuid),
                 }));
             } catch (e) {
                 api.dispatch(couldNotFetchGroupDetailsContents());
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 99ca02d4..05ae4ce2 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -303,7 +303,7 @@ export const ResourceLinkClass = connect(
 const renderLink = (dispatch: Dispatch, item: Resource) => {
     var displayName = '';
 
-    if ((item as UserResource).kind == ResourceKind.USER
+    if ((item as UserResource).kind === ResourceKind.USER
           && typeof (item as UserResource).firstName !== 'undefined') {
         // We can be sure the resource is UserResource
         displayName = getUserDisplayName(item as UserResource);
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index 3baff579..5b1a884e 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 { ResourceFullName, ResourceUuid, ResourceEmail, ResourceUsername, ResourceLinkName, ResourceLinkHead, ResourceName } from 'views-components/data-explorer/renderers';
+import { ResourceUuid, ResourceEmail, ResourceUsername, ResourceLinkName, ResourceLinkHead, ResourceLinkTail } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -38,7 +38,7 @@ export const groupDetailsMembersPanelColumns: DataColumns<string> = [
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceFullName uuid={uuid} />
+        render: uuid => <ResourceLinkTail uuid={uuid} />
     },
     {
         name: GroupDetailsPanelMembersColumnNames.USERNAME,

commit 55b1d7f984ac36dab63edc89e3b132b61fedbbfa
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Oct 13 13:56:10 2021 -0400

    18123: Improve checking of group permission head type for permissions list
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 6766d4c0..99ca02d4 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -301,11 +301,18 @@ export const ResourceLinkClass = connect(
 // };
 
 const renderLink = (dispatch: Dispatch, item: Resource) => {
-    const name = (item as LinkResource).name;
-    const fullName = getUserDisplayName(item as UserResource);
+    var displayName = '';
+
+    if ((item as UserResource).kind == ResourceKind.USER
+          && typeof (item as UserResource).firstName !== 'undefined') {
+        // We can be sure the resource is UserResource
+        displayName = getUserDisplayName(item as UserResource);
+    } else {
+        displayName = (item as GroupContentsResource).name;
+    }
 
     return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
-        {resourceLabel(item.kind)}: {name || fullName || item.uuid}
+        {resourceLabel(item.kind)}: {displayName || item.uuid}
     </Typography>;
 }
 

commit cfa8c66b29ba7eb4cab946ab8bb2f58ca93bc80a
Author: Stephen Smith <stephen at curii.com>
Date:   Wed Oct 13 08:34:24 2021 -0400

    18123: Add group details permissions columns and tweaked renderers
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

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 5a190a5f..e71f75f3 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -18,26 +18,26 @@ import { PermissionResource } from 'models/permission';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { UserResource, getUserDisplayName } from 'models/user';
 
-export const GROUP_DETAILS_PANEL_ID = 'groupMembersPanel';
-export const GROUP_PERMISSIONS_PANEL_ID = 'groupPermissionsPanel';
+export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
+export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
 export const ADD_GROUP_MEMBERS_DIALOG = 'addGrupMembers';
 export const ADD_GROUP_MEMBERS_FORM = 'addGrupMembers';
 export const ADD_GROUP_MEMBERS_USERS_FIELD_NAME = 'users';
 export const MEMBER_ATTRIBUTES_DIALOG = 'memberAttributesDialog';
 export const MEMBER_REMOVE_DIALOG = 'memberRemoveDialog';
 
-export const GroupMembersPanelActions = bindDataExplorerActions(GROUP_DETAILS_PANEL_ID);
-export const GroupPermissionsPanelActions = bindDataExplorerActions(GROUP_PERMISSIONS_PANEL_ID);
+export const GroupMembersPanelActions = bindDataExplorerActions(GROUP_DETAILS_MEMBERS_PANEL_ID);
+export const GroupPermissionsPanelActions = bindDataExplorerActions(GROUP_DETAILS_PERMISSIONS_PANEL_ID);
 
 export const loadGroupDetailsPanel = (groupUuid: string) =>
     (dispatch: Dispatch) => {
-        dispatch(propertiesActions.SET_PROPERTY({ key: GROUP_DETAILS_PANEL_ID, value: groupUuid }));
+        dispatch(propertiesActions.SET_PROPERTY({ key: GROUP_DETAILS_MEMBERS_PANEL_ID, value: groupUuid }));
         dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
-        dispatch(propertiesActions.SET_PROPERTY({ key: GROUP_PERMISSIONS_PANEL_ID, value: groupUuid }));
+        dispatch(propertiesActions.SET_PROPERTY({ key: GROUP_DETAILS_PERMISSIONS_PANEL_ID, value: groupUuid }));
         dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
     };
 
-export const getCurrentGroupDetailsPanelUuid = getProperty<string>(GROUP_DETAILS_PANEL_ID);
+export const getCurrentGroupDetailsPanelUuid = getProperty<string>(GROUP_DETAILS_MEMBERS_PANEL_ID);
 
 export interface AddGroupMembersFormData {
     [ADD_GROUP_MEMBERS_USERS_FIELD_NAME]: Participant[];
diff --git a/src/store/group-details-panel/group-details-panel-middleware-service.ts b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
similarity index 64%
copy from src/store/group-details-panel/group-details-panel-middleware-service.ts
copy to src/store/group-details-panel/group-details-panel-members-middleware-service.ts
index 67f72391..e295579a 100644
--- a/src/store/group-details-panel/group-details-panel-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
@@ -10,10 +10,10 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { getDataExplorer } from "store/data-explorer/data-explorer-reducer";
 import { FilterBuilder } from 'services/api/filter-builder';
 import { updateResources } from 'store/resources/resources-actions';
-import { getCurrentGroupDetailsPanelUuid, GroupMembersPanelActions, GroupPermissionsPanelActions } from 'store/group-details-panel/group-details-panel-actions';
+import { getCurrentGroupDetailsPanelUuid, GroupMembersPanelActions } from 'store/group-details-panel/group-details-panel-actions';
 import { LinkClass } from 'models/link';
 
-export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareService {
+export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddlewareService {
 
     constructor(private services: ServiceRepository, id: string) {
         super(id);
@@ -26,41 +26,26 @@ export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareSe
             api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
         } else {
             try {
-                const permissionsIn = await this.services.permissionService.list({
+                const permissions = await this.services.permissionService.list({
                     filters: new FilterBuilder()
                         .addEqual('head_uuid', groupUuid)
                         .addEqual('link_class', LinkClass.PERMISSION)
                         .getFilters()
                 });
-                api.dispatch(updateResources(permissionsIn.items));
-                const permissionsOut = await this.services.permissionService.list({
-                    filters: new FilterBuilder()
-                        .addEqual('tail_uuid', groupUuid)
-                        .addEqual('link_class', LinkClass.PERMISSION)
-                        .getFilters()
-                });
-                api.dispatch(updateResources(permissionsOut.items));
+                api.dispatch(updateResources(permissions.items));
+
                 const users = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
-                        .getFilters(),
-                    count: "none"
-                });
-                const usersOut = await this.services.userService.list({
-                    filters: new FilterBuilder()
-                        .addIn('uuid', permissionsOut.items.map(item => item.tailUuid))
+                        .addIn('uuid', permissions.items.map(item => item.tailUuid))
                         .getFilters(),
                     count: "none"
                 });
+                api.dispatch(updateResources(users.items));
+
                 api.dispatch(GroupMembersPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissionsIn),
+                    ...listResultsToDataExplorerItemsMeta(permissions),
                     items: users.items.map(item => item.uuid),
                 }));
-                api.dispatch(GroupPermissionsPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissionsOut),
-                    items: usersOut.items.map(item => item.uuid),
-                }));
-                api.dispatch(updateResources(users.items));
             } catch (e) {
                 api.dispatch(couldNotFetchGroupDetailsContents());
             }
diff --git a/src/store/group-details-panel/group-details-panel-middleware-service.ts b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
similarity index 64%
rename from src/store/group-details-panel/group-details-panel-middleware-service.ts
rename to src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
index 67f72391..91bff1e9 100644
--- a/src/store/group-details-panel/group-details-panel-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-permissions-middleware-service.ts
@@ -10,10 +10,10 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { getDataExplorer } from "store/data-explorer/data-explorer-reducer";
 import { FilterBuilder } from 'services/api/filter-builder';
 import { updateResources } from 'store/resources/resources-actions';
-import { getCurrentGroupDetailsPanelUuid, GroupMembersPanelActions, GroupPermissionsPanelActions } from 'store/group-details-panel/group-details-panel-actions';
+import { getCurrentGroupDetailsPanelUuid, GroupPermissionsPanelActions } from 'store/group-details-panel/group-details-panel-actions';
 import { LinkClass } from 'models/link';
 
-export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareService {
+export class GroupDetailsPanelPermissionsMiddlewareService extends DataExplorerMiddlewareService {
 
     constructor(private services: ServiceRepository, id: string) {
         super(id);
@@ -26,41 +26,42 @@ export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareSe
             api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
         } else {
             try {
-                const permissionsIn = await this.services.permissionService.list({
-                    filters: new FilterBuilder()
-                        .addEqual('head_uuid', groupUuid)
-                        .addEqual('link_class', LinkClass.PERMISSION)
-                        .getFilters()
-                });
-                api.dispatch(updateResources(permissionsIn.items));
-                const permissionsOut = await this.services.permissionService.list({
+                const permissions = await this.services.permissionService.list({
                     filters: new FilterBuilder()
                         .addEqual('tail_uuid', groupUuid)
                         .addEqual('link_class', LinkClass.PERMISSION)
                         .getFilters()
                 });
-                api.dispatch(updateResources(permissionsOut.items));
+                api.dispatch(updateResources(permissions.items));
+
                 const users = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
+                        .addIn('uuid', permissions.items.map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
-                const usersOut = await this.services.userService.list({
+                api.dispatch(updateResources(users.items));
+
+                const collections = await this.services.collectionService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissionsOut.items.map(item => item.tailUuid))
+                        .addIn('uuid', permissions.items.map(item => item.headUuid))
                         .getFilters(),
                     count: "none"
                 });
-                api.dispatch(GroupMembersPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissionsIn),
-                    items: users.items.map(item => item.uuid),
-                }));
+                api.dispatch(updateResources(collections.items));
+
+                const projects = await this.services.projectService.list({
+                    filters: new FilterBuilder()
+                        .addIn('uuid', permissions.items.map(item => item.headUuid))
+                        .getFilters(),
+                    count: "none"
+                });
+                api.dispatch(updateResources(projects.items));
+
                 api.dispatch(GroupPermissionsPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissionsOut),
-                    items: usersOut.items.map(item => item.uuid),
+                    ...listResultsToDataExplorerItemsMeta(permissions),
+                    items: permissions.items.map(item => item.uuid),
                 }));
-                api.dispatch(updateResources(users.items));
             } catch (e) {
                 api.dispatch(couldNotFetchGroupDetailsContents());
             }
@@ -70,12 +71,12 @@ export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareSe
 
 const groupsDetailsPanelDataExplorerIsNotSet = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Group details panel is not ready.',
+        message: 'Group permissions panel is not ready.',
         kind: SnackbarKind.ERROR
     });
 
 const couldNotFetchGroupDetailsContents = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch group details.',
+        message: 'Could not fetch group permissions.',
         kind: SnackbarKind.ERROR
     });
diff --git a/src/store/store.ts b/src/store/store.ts
index 59a0cb12..688c8a05 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -51,8 +51,9 @@ import { UserMiddlewareService } from 'store/users/user-panel-middleware-service
 import { USERS_PANEL_ID } from 'store/users/users-actions';
 import { GroupsPanelMiddlewareService } from 'store/groups-panel/groups-panel-middleware-service';
 import { GROUPS_PANEL_ID } from 'store/groups-panel/groups-panel-actions';
-import { GroupDetailsPanelMiddlewareService } from 'store/group-details-panel/group-details-panel-middleware-service';
-import { GROUP_DETAILS_PANEL_ID } from 'store/group-details-panel/group-details-panel-actions';
+import { GroupDetailsPanelMembersMiddlewareService } from 'store/group-details-panel/group-details-panel-members-middleware-service';
+import { GroupDetailsPanelPermissionsMiddlewareService } from 'store/group-details-panel/group-details-panel-permissions-middleware-service';
+import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID } from 'store/group-details-panel/group-details-panel-actions';
 import { LINK_PANEL_ID } from 'store/link-panel/link-panel-actions';
 import { LinkMiddlewareService } from 'store/link-panel/link-panel-middleware-service';
 import { API_CLIENT_AUTHORIZATION_PANEL_ID } from 'store/api-client-authorizations/api-client-authorizations-actions';
@@ -116,8 +117,11 @@ export function configureStore(history: History, services: ServiceRepository, co
     const groupsPanelMiddleware = dataExplorerMiddleware(
         new GroupsPanelMiddlewareService(services, GROUPS_PANEL_ID)
     );
-    const groupDetailsPanelMiddleware = dataExplorerMiddleware(
-        new GroupDetailsPanelMiddlewareService(services, GROUP_DETAILS_PANEL_ID)
+    const groupDetailsPanelMembersMiddleware = dataExplorerMiddleware(
+        new GroupDetailsPanelMembersMiddlewareService(services, GROUP_DETAILS_MEMBERS_PANEL_ID)
+    );
+    const groupDetailsPanelPermissionsMiddleware = dataExplorerMiddleware(
+        new GroupDetailsPanelPermissionsMiddlewareService(services, GROUP_DETAILS_PERMISSIONS_PANEL_ID)
     );
     const linkPanelMiddleware = dataExplorerMiddleware(
         new LinkMiddlewareService(services, LINK_PANEL_ID)
@@ -157,7 +161,8 @@ export function configureStore(history: History, services: ServiceRepository, co
         workflowPanelMiddleware,
         userPanelMiddleware,
         groupsPanelMiddleware,
-        groupDetailsPanelMiddleware,
+        groupDetailsPanelMembersMiddleware,
+        groupDetailsPanelPermissionsMiddleware,
         linkPanelMiddleware,
         apiClientAuthorizationMiddlewareService,
         publicFavoritesMiddleware,
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 1ee0a9f8..9c89d199 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -88,7 +88,7 @@ import { apiClientAuthorizationPanelColumns } from 'views/api-client-authorizati
 import * as groupPanelActions from 'store/groups-panel/groups-panel-actions';
 import { groupsPanelColumns } from 'views/groups-panel/groups-panel';
 import * as groupDetailsPanelActions from 'store/group-details-panel/group-details-panel-actions';
-import { groupDetailsPanelColumns } from 'views/group-details-panel/group-details-panel';
+import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from 'views/group-details-panel/group-details-panel';
 import { DataTableFetchMode } from "components/data-table/data-table";
 import { loadPublicFavoritePanel, publicFavoritePanelActions } from 'store/public-favorites-panel/public-favorites-action';
 import { publicFavoritePanelColumns } from 'views/public-favorites-panel/public-favorites-panel';
@@ -136,8 +136,8 @@ export const loadWorkbench = () =>
             dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
             dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
             dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
-            dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsPanelColumns }));
-            dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPanelColumns }));
+            dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsMembersPanelColumns }));
+            dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPermissionsPanelColumns }));
             dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index ef8f70d4..6766d4c0 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -20,7 +20,7 @@ import { WorkflowResource } from 'models/workflow';
 import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-panel-view';
 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
-import { getUserFullname, User, UserResource } from 'models/user';
+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';
@@ -286,45 +286,50 @@ export const ResourceLinkClass = connect(
         return resource || { linkClass: '' };
     })(renderLinkClass);
 
-const renderLinkTail = (dispatch: Dispatch, item: { uuid: string, tailUuid: string, tailKind: string }) => {
-    const currentLabel = resourceLabel(item.tailKind);
-    const isUnknow = currentLabel === "Unknown";
-    return (<div>
-        {!isUnknow ? (
-            renderLink(dispatch, item.tailUuid, currentLabel)
-        ) : (
-                <Typography noWrap color="default">
-                    {item.tailUuid}
-                </Typography>
-            )}
-    </div>);
-};
-
-const renderLink = (dispatch: Dispatch, uuid: string, label: string) =>
-    <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(uuid))}>
-        {label}: {uuid}
+// const renderLinkTail = (dispatch: Dispatch, item: { uuid: string, tailUuid: string, tailKind: string }) => {
+//     const currentLabel = resourceLabel(item.tailKind);
+//     const isUnknow = currentLabel === "Unknown";
+//     return (<div>
+//         {!isUnknow ? (
+//             renderLink(dispatch, item.tailUuid, "name", currentLabel)
+//         ) : (
+//                 <Typography noWrap color="default">
+//                     {item.tailUuid}
+//                 </Typography>
+//             )}
+//     </div>);
+// };
+
+const renderLink = (dispatch: Dispatch, item: Resource) => {
+    const name = (item as LinkResource).name;
+    const fullName = getUserDisplayName(item as UserResource);
+
+    return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
+        {resourceLabel(item.kind)}: {name || fullName || item.uuid}
     </Typography>;
+}
 
 export const ResourceLinkTail = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<LinkResource>(props.uuid)(state.resources);
+        const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
+
         return {
-            item: resource || { uuid: '', tailUuid: '', tailKind: ResourceKind.NONE }
+            item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.headKind || ResourceKind.NONE }
         };
-    })((props: { item: any } & DispatchProp<any>) =>
-        renderLinkTail(props.dispatch, props.item));
-
-const renderLinkHead = (dispatch: Dispatch, item: { uuid: string, headUuid: string, headKind: ResourceKind }) =>
-    renderLink(dispatch, item.headUuid, resourceLabel(item.headKind));
+    })((props: { item: Resource } & DispatchProp<any>) =>
+        renderLink(props.dispatch, props.item));
 
 export const ResourceLinkHead = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<LinkResource>(props.uuid)(state.resources);
+        const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
+
         return {
-            item: resource || { uuid: '', headUuid: '', headKind: ResourceKind.NONE }
+            item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE }
         };
-    })((props: { item: any } & DispatchProp<any>) =>
-        renderLinkHead(props.dispatch, props.item));
+    })((props: { item: Resource } & DispatchProp<any>) =>
+        renderLink(props.dispatch, props.item));
 
 export const ResourceLinkUuid = connect(
     (state: RootState, props: { uuid: string }) => {
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index 3885ec18..3baff579 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -7,11 +7,11 @@ import { connect } from 'react-redux';
 
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { DataColumns } from 'components/data-table/data-table';
-import { ResourceFullName, ResourceUuid, ResourceEmail, ResourceUsername } from 'views-components/data-explorer/renderers';
+import { ResourceFullName, ResourceUuid, ResourceEmail, ResourceUsername, ResourceLinkName, ResourceLinkHead, ResourceName } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
-import { GROUP_DETAILS_PANEL_ID, GROUP_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions';
+import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions';
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
@@ -19,37 +19,43 @@ import { PermissionResource } from 'models/permission';
 import { Grid, Button, Tabs, Tab, Paper } from '@material-ui/core';
 import { AddIcon } from 'components/icon/icon';
 
-export enum GroupDetailsPanelColumnNames {
+export enum GroupDetailsPanelMembersColumnNames {
     FULL_NAME = "Name",
     UUID = "UUID",
     EMAIL = "Email",
     USERNAME = "Username",
 }
 
-export const groupDetailsPanelColumns: DataColumns<string> = [
+export enum GroupDetailsPanelPermissionsColumnNames {
+    HEAD = "Head",
+    NAME = "Name",
+    UUID = "UUID",
+}
+
+export const groupDetailsMembersPanelColumns: DataColumns<string> = [
     {
-        name: GroupDetailsPanelColumnNames.FULL_NAME,
+        name: GroupDetailsPanelMembersColumnNames.FULL_NAME,
         selected: true,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceFullName uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelColumnNames.USERNAME,
+        name: GroupDetailsPanelMembersColumnNames.USERNAME,
         selected: true,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceUsername uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelColumnNames.UUID,
+        name: GroupDetailsPanelMembersColumnNames.UUID,
         selected: true,
         configurable: true,
         filters: createTree(),
         render: uuid => <ResourceUuid uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelColumnNames.EMAIL,
+        name: GroupDetailsPanelMembersColumnNames.EMAIL,
         selected: true,
         configurable: true,
         filters: createTree(),
@@ -57,6 +63,30 @@ export const groupDetailsPanelColumns: DataColumns<string> = [
     },
 ];
 
+export const groupDetailsPermissionsPanelColumns: DataColumns<string> = [
+    {
+        name: GroupDetailsPanelPermissionsColumnNames.HEAD,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkHead uuid={uuid} />
+    },
+    {
+        name: GroupDetailsPanelPermissionsColumnNames.NAME,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceLinkName uuid={uuid} />
+    },
+    {
+        name: GroupDetailsPanelPermissionsColumnNames.UUID,
+        selected: true,
+        configurable: true,
+        filters: createTree(),
+        render: uuid => <ResourceUuid uuid={uuid} />
+    },
+];
+
 const mapStateToProps = (state: RootState) => {
     return {
         resources: state.resources
@@ -90,13 +120,13 @@ export const GroupDetailsPanel = connect(
             const { value } = this.state;
             return (
                 <Paper>
-                  <Tabs value={value} onChange={this.handleChange} fullWidth>
+                  <Tabs value={value} onChange={this.handleChange} variant="fullWidth">
                       <Tab label="MEMBERS" />
                       <Tab label="PERMISSIONS" />
                   </Tabs>
                   {value === 0 &&
                       <DataExplorer
-                          id={GROUP_DETAILS_PANEL_ID}
+                          id={GROUP_DETAILS_MEMBERS_PANEL_ID}
                           onRowClick={noop}
                           onRowDoubleClick={noop}
                           onContextMenu={this.handleContextMenu}
@@ -119,7 +149,7 @@ export const GroupDetailsPanel = connect(
                   }
                   {value === 1 &&
                       <DataExplorer
-                          id={GROUP_PERMISSIONS_PANEL_ID}
+                          id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
                           onRowClick={noop}
                           onRowDoubleClick={noop}
                           onContextMenu={this.handleContextMenu}

commit 237776fd3412409bf0a9c9f0ac538f82d4b4a8d4
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 7 00:19:11 2021 -0400

    18123: Add permissions data explorer and tab in groups edit
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

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 01e6c151..5a190a5f 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -18,19 +18,23 @@ import { PermissionResource } from 'models/permission';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { UserResource, getUserDisplayName } from 'models/user';
 
-export const GROUP_DETAILS_PANEL_ID = 'groupDetailsPanel';
+export const GROUP_DETAILS_PANEL_ID = 'groupMembersPanel';
+export const GROUP_PERMISSIONS_PANEL_ID = 'groupPermissionsPanel';
 export const ADD_GROUP_MEMBERS_DIALOG = 'addGrupMembers';
 export const ADD_GROUP_MEMBERS_FORM = 'addGrupMembers';
 export const ADD_GROUP_MEMBERS_USERS_FIELD_NAME = 'users';
 export const MEMBER_ATTRIBUTES_DIALOG = 'memberAttributesDialog';
 export const MEMBER_REMOVE_DIALOG = 'memberRemoveDialog';
 
-export const GroupDetailsPanelActions = bindDataExplorerActions(GROUP_DETAILS_PANEL_ID);
+export const GroupMembersPanelActions = bindDataExplorerActions(GROUP_DETAILS_PANEL_ID);
+export const GroupPermissionsPanelActions = bindDataExplorerActions(GROUP_PERMISSIONS_PANEL_ID);
 
 export const loadGroupDetailsPanel = (groupUuid: string) =>
     (dispatch: Dispatch) => {
         dispatch(propertiesActions.SET_PROPERTY({ key: GROUP_DETAILS_PANEL_ID, value: groupUuid }));
-        dispatch(GroupDetailsPanelActions.REQUEST_ITEMS());
+        dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
+        dispatch(propertiesActions.SET_PROPERTY({ key: GROUP_PERMISSIONS_PANEL_ID, value: groupUuid }));
+        dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
     };
 
 export const getCurrentGroupDetailsPanelUuid = getProperty<string>(GROUP_DETAILS_PANEL_ID);
@@ -72,7 +76,7 @@ export const addGroupMembers = ({ users }: AddGroupMembersFormData) =>
             }
 
             dispatch(dialogActions.CLOSE_DIALOG({ id: ADD_GROUP_MEMBERS_FORM }));
-            dispatch(GroupDetailsPanelActions.REQUEST_ITEMS());
+            dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
 
         }
     };
@@ -124,7 +128,7 @@ export const removeGroupMember = (uuid: string) =>
             });
 
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
-            dispatch(GroupDetailsPanelActions.REQUEST_ITEMS());
+            dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
 
         }
 
diff --git a/src/store/group-details-panel/group-details-panel-middleware-service.ts b/src/store/group-details-panel/group-details-panel-middleware-service.ts
index 834b4c21..67f72391 100644
--- a/src/store/group-details-panel/group-details-panel-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-middleware-service.ts
@@ -10,7 +10,7 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { getDataExplorer } from "store/data-explorer/data-explorer-reducer";
 import { FilterBuilder } from 'services/api/filter-builder';
 import { updateResources } from 'store/resources/resources-actions';
-import { getCurrentGroupDetailsPanelUuid, GroupDetailsPanelActions } from 'store/group-details-panel/group-details-panel-actions';
+import { getCurrentGroupDetailsPanelUuid, GroupMembersPanelActions, GroupPermissionsPanelActions } from 'store/group-details-panel/group-details-panel-actions';
 import { LinkClass } from 'models/link';
 
 export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareService {
@@ -26,23 +26,40 @@ export class GroupDetailsPanelMiddlewareService extends DataExplorerMiddlewareSe
             api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
         } else {
             try {
-                const permissions = await this.services.permissionService.list({
+                const permissionsIn = await this.services.permissionService.list({
+                    filters: new FilterBuilder()
+                        .addEqual('head_uuid', groupUuid)
+                        .addEqual('link_class', LinkClass.PERMISSION)
+                        .getFilters()
+                });
+                api.dispatch(updateResources(permissionsIn.items));
+                const permissionsOut = await this.services.permissionService.list({
                     filters: new FilterBuilder()
                         .addEqual('tail_uuid', groupUuid)
                         .addEqual('link_class', LinkClass.PERMISSION)
                         .getFilters()
                 });
-                api.dispatch(updateResources(permissions.items));
+                api.dispatch(updateResources(permissionsOut.items));
                 const users = await this.services.userService.list({
                     filters: new FilterBuilder()
-                        .addIn('uuid', permissions.items.map(item => item.headUuid))
+                        .addIn('uuid', permissionsIn.items.map(item => item.tailUuid))
                         .getFilters(),
                     count: "none"
                 });
-                api.dispatch(GroupDetailsPanelActions.SET_ITEMS({
-                    ...listResultsToDataExplorerItemsMeta(permissions),
+                const usersOut = await this.services.userService.list({
+                    filters: new FilterBuilder()
+                        .addIn('uuid', permissionsOut.items.map(item => item.tailUuid))
+                        .getFilters(),
+                    count: "none"
+                });
+                api.dispatch(GroupMembersPanelActions.SET_ITEMS({
+                    ...listResultsToDataExplorerItemsMeta(permissionsIn),
                     items: users.items.map(item => item.uuid),
                 }));
+                api.dispatch(GroupPermissionsPanelActions.SET_ITEMS({
+                    ...listResultsToDataExplorerItemsMeta(permissionsOut),
+                    items: usersOut.items.map(item => item.uuid),
+                }));
                 api.dispatch(updateResources(users.items));
             } catch (e) {
                 api.dispatch(couldNotFetchGroupDetailsContents());
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 6ea30855..1ee0a9f8 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -136,7 +136,8 @@ export const loadWorkbench = () =>
             dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
             dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
             dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
-            dispatch(groupDetailsPanelActions.GroupDetailsPanelActions.SET_COLUMNS({ columns: groupDetailsPanelColumns }));
+            dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsPanelColumns }));
+            dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPanelColumns }));
             dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index faf7b3ae..3885ec18 100644
--- a/src/views/group-details-panel/group-details-panel.tsx
+++ b/src/views/group-details-panel/group-details-panel.tsx
@@ -11,7 +11,7 @@ import { ResourceFullName, ResourceUuid, ResourceEmail, ResourceUsername } from
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
-import { GROUP_DETAILS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions';
+import { GROUP_DETAILS_PANEL_ID, GROUP_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions';
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
@@ -78,27 +78,69 @@ export const GroupDetailsPanel = connect(
     mapStateToProps, mapDispatchToProps
 )(
     class GroupDetailsPanel extends React.Component<GroupDetailsPanelProps> {
+        state = {
+          value: 0,
+        };
+
+        componentDidMount() {
+            this.setState({ value: 0 });
+        }
 
         render() {
+            const { value } = this.state;
             return (
-                <DataExplorer
-                    id={GROUP_DETAILS_PANEL_ID}
-                    onRowClick={noop}
-                    onRowDoubleClick={noop}
-                    onContextMenu={this.handleContextMenu}
-                    contextMenuColumn={true}
-                    hideColumnSelector
-                    hideSearchInput
-                    actions={
-                        <Grid container justify='flex-end'>
-                            <Button
-                                variant="contained"
-                                color="primary"
-                                onClick={this.props.onAddUser}>
-                                <AddIcon /> Add user
-                        </Button>
-                        </Grid>
-                    } />
+                <Paper>
+                  <Tabs value={value} onChange={this.handleChange} fullWidth>
+                      <Tab label="MEMBERS" />
+                      <Tab label="PERMISSIONS" />
+                  </Tabs>
+                  {value === 0 &&
+                      <DataExplorer
+                          id={GROUP_DETAILS_PANEL_ID}
+                          onRowClick={noop}
+                          onRowDoubleClick={noop}
+                          onContextMenu={this.handleContextMenu}
+                          contextMenuColumn={true}
+                          hideColumnSelector
+                          hideSearchInput
+                          actions={
+                              <Grid container justify='flex-end'>
+                                  <Button
+                                      variant="contained"
+                                      color="primary"
+                                      onClick={this.props.onAddUser}>
+                                      <AddIcon /> Add user
+                              </Button>
+                              </Grid>
+                          }
+                          paperProps={{
+                              elevation: 0,
+                          }} />
+                  }
+                  {value === 1 &&
+                      <DataExplorer
+                          id={GROUP_PERMISSIONS_PANEL_ID}
+                          onRowClick={noop}
+                          onRowDoubleClick={noop}
+                          onContextMenu={this.handleContextMenu}
+                          contextMenuColumn={true}
+                          hideColumnSelector
+                          hideSearchInput
+                          actions={
+                              <Grid container justify='flex-end'>
+                                  <Button
+                                      variant="contained"
+                                      color="primary"
+                                      onClick={this.props.onAddUser}>
+                                      <AddIcon /> Add user
+                              </Button>
+                              </Grid>
+                          }
+                          paperProps={{
+                              elevation: 0,
+                          }} />
+                  }
+                </Paper>
             );
         }
 
@@ -114,5 +156,8 @@ export const GroupDetailsPanel = connect(
                 });
             }
         }
-    });
 
+        handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+            this.setState({ value });
+        }
+    });

commit de544885631caf7d6e6c1d9ae43763b4d612dfca
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 7 00:17:53 2021 -0400

    18123: Display full name of group members in group edit
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index 3965e69d..ef8f70d4 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -131,11 +131,11 @@ export const ResourceShare = connect(
     })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
         resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
 
+// User Resources
 const renderFirstName = (item: { firstName: string }) => {
     return <Typography noWrap>{item.firstName}</Typography>;
 };
 
-// User Resources
 export const ResourceFirstName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
@@ -151,6 +151,16 @@ export const ResourceLastName = connect(
         return resource || { lastName: '' };
     })(renderLastName);
 
+const renderFullName = (item: { firstName: string, lastName: string }) =>
+    <Typography noWrap>{(item.firstName + " " + item.lastName).trim()}</Typography>;
+
+export const ResourceFullName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { firstName: '', lastName: '' };
+    })(renderFullName);
+
+
 const renderUuid = (item: { uuid: string }) =>
     <Typography noWrap>{item.uuid}</Typography>;
 
diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx
index d0f79736..faf7b3ae 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 { ResourceUuid, ResourceFirstName, ResourceLastName, ResourceEmail, ResourceUsername } from 'views-components/data-explorer/renderers';
+import { ResourceFullName, ResourceUuid, ResourceEmail, ResourceUsername } from 'views-components/data-explorer/renderers';
 import { createTree } from 'models/tree';
 import { noop } from 'lodash/fp';
 import { RootState } from 'store/store';
@@ -16,12 +16,11 @@ import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { ContextMenuKind } from 'views-components/context-menu/context-menu';
 import { PermissionResource } from 'models/permission';
-import { Grid, Button } from '@material-ui/core';
+import { Grid, Button, Tabs, Tab, Paper } from '@material-ui/core';
 import { AddIcon } from 'components/icon/icon';
 
 export enum GroupDetailsPanelColumnNames {
-    FIRST_NAME = "First name",
-    LAST_NAME = "Last name",
+    FULL_NAME = "Name",
     UUID = "UUID",
     EMAIL = "Email",
     USERNAME = "Username",
@@ -29,18 +28,18 @@ export enum GroupDetailsPanelColumnNames {
 
 export const groupDetailsPanelColumns: DataColumns<string> = [
     {
-        name: GroupDetailsPanelColumnNames.FIRST_NAME,
+        name: GroupDetailsPanelColumnNames.FULL_NAME,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceFirstName uuid={uuid} />
+        render: uuid => <ResourceFullName uuid={uuid} />
     },
     {
-        name: GroupDetailsPanelColumnNames.LAST_NAME,
+        name: GroupDetailsPanelColumnNames.USERNAME,
         selected: true,
         configurable: true,
         filters: createTree(),
-        render: uuid => <ResourceLastName uuid={uuid} />
+        render: uuid => <ResourceUsername uuid={uuid} />
     },
     {
         name: GroupDetailsPanelColumnNames.UUID,
@@ -56,13 +55,6 @@ export const groupDetailsPanelColumns: DataColumns<string> = [
         filters: createTree(),
         render: uuid => <ResourceEmail uuid={uuid} />
     },
-    {
-        name: GroupDetailsPanelColumnNames.USERNAME,
-        selected: true,
-        configurable: true,
-        filters: createTree(),
-        render: uuid => <ResourceUsername uuid={uuid} />
-    },
 ];
 
 const mapStateToProps = (state: RootState) => {

commit b2485c13b72d66f5e207814a368b72334970afca
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Oct 7 00:15:48 2021 -0400

    18123: Add Groups page to sidebar
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 97082e5a..19cc36ae 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -9,7 +9,6 @@ import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-action
 import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl } from 'routes/routes';
 import { RootState } from 'store/store';
 import { ServiceRepository } from 'services/services';
-import { GROUPS_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
 import { pluginConfig } from 'plugins';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 
@@ -64,7 +63,7 @@ export const navigateTo = (uuid: string) =>
             case SidePanelTreeCategory.TRASH:
                 dispatch(navigateToTrash);
                 return;
-            case GROUPS_PANEL_LABEL:
+            case SidePanelTreeCategory.GROUPS:
                 dispatch(navigateToGroups);
                 return;
             case SidePanelTreeCategory.ALL_PROCESSES:
diff --git a/src/store/side-panel-tree/side-panel-tree-actions.ts b/src/store/side-panel-tree/side-panel-tree-actions.ts
index 66521f35..895fe79c 100644
--- a/src/store/side-panel-tree/side-panel-tree-actions.ts
+++ b/src/store/side-panel-tree/side-panel-tree-actions.ts
@@ -26,7 +26,8 @@ export enum SidePanelTreeCategory {
     WORKFLOWS = 'Workflows',
     FAVORITES = 'My Favorites',
     TRASH = 'Trash',
-    ALL_PROCESSES = 'All Processes'
+    ALL_PROCESSES = 'All Processes',
+    GROUPS = 'Groups',
 }
 
 export const SIDE_PANEL_TREE = 'sidePanelTree';
@@ -52,6 +53,7 @@ let SIDE_PANEL_CATEGORIES: string[] = [
     SidePanelTreeCategory.PUBLIC_FAVORITES,
     SidePanelTreeCategory.FAVORITES,
     SidePanelTreeCategory.WORKFLOWS,
+    SidePanelTreeCategory.GROUPS,
     SidePanelTreeCategory.ALL_PROCESSES,
     SidePanelTreeCategory.TRASH
 ];

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list