standardized nullish grid cells to display hyphens i nnstead of nothing or (none)
[arvados-workbench2.git] / src / views-components / data-explorer / renderers.tsx
index 6cf29faecf444683540de031966273298fc19314..9c24a677bbe7c67e7b16a48bd1cf417569744ba1 100644 (file)
@@ -2,40 +2,76 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
-import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
+import React from 'react';
+import {
+    Grid,
+    Typography,
+    withStyles,
+    Tooltip,
+    IconButton,
+    Checkbox,
+    Chip
+} from '@material-ui/core';
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
-import { ResourceKind, TrashableResource } from '~/models/resource';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
-import { formatDate, formatFileSize, formatTime } from '~/common/formatters';
-import { resourceLabel } from '~/common/labels';
+import { Resource, ResourceKind, TrashableResource } from 'models/resource';
+import {
+    FreezeIcon,
+    ProjectIcon,
+    FilterGroupIcon,
+    CollectionIcon,
+    ProcessIcon,
+    DefaultIcon,
+    ShareIcon,
+    CollectionOldVersionIcon,
+    WorkflowIcon,
+    RemoveIcon,
+    RenameIcon,
+    ActiveIcon,
+    SetupIcon,
+    InactiveIcon,
+} from 'components/icon/icon';
+import { formatDate, formatFileSize, formatTime } from 'common/formatters';
+import { resourceLabel } from 'common/labels';
 import { connect, DispatchProp } from 'react-redux';
-import { RootState } from '~/store/store';
-import { getResource } from '~/store/resources/resources';
-import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { getProcess, Process, getProcessStatus, getProcessStatusColor, getProcessRuntime } from '~/store/processes/process';
-import { ArvadosTheme } from '~/common/custom-theme';
+import { RootState } from 'store/store';
+import { getResource, filterResources } from 'store/resources/resources';
+import { GroupContentsResource } from 'services/groups-service/groups-service';
+import { getProcess, Process, getProcessStatus, getProcessStatusColor, getProcessRuntime } from 'store/processes/process';
+import { ArvadosTheme } from 'common/custom-theme';
 import { compose, Dispatch } from 'redux';
-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 { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
-import { LinkResource } from '~/models/link';
-import { navigateTo } 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';
-
-const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
-    <Grid container alignItems="center" wrap="nowrap" spacing={16}>
+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, getUserDisplayName, User, UserResource } from 'models/user';
+import { toggleIsAdmin } from 'store/users/users-actions';
+import { LinkClass, LinkResource } from 'models/link';
+import { navigateTo, navigateToGroupDetails, navigateToUserProfile } 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 { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
+import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
+import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
+import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
+import { PermissionLevel } from 'models/permission';
+import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
+import { getUserUuid } from 'common/getuser';
+import { VirtualMachinesResource } from 'models/virtual-machines';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
+import { ProjectResource } from 'models/project';
+import {ContainerRequestResource} from 'models/container-request'
+
+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}
@@ -46,9 +82,31 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
             <Typography variant="caption">
                 <FavoriteStar resourceUuid={item.uuid} />
                 <PublicFavoriteStar resourceUuid={item.uuid} />
+                {
+                    item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />
+                }
             </Typography>
         </Grid>
     </Grid>;
+};
+
+const FrozenProject = (props: {item: ProjectResource}) => {
+    const [fullUsername, setFullusername] = React.useState<any>(null);
+    const getFullName = React.useCallback(() => {
+        if (props.item.frozenByUuid) {
+            setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
+        }
+    }, [props.item, setFullusername])
+
+    if (props.item.frozenByUuid) {
+
+        return <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
+            <FreezeIcon style={{ fontSize: "inherit" }}/>
+        </Tooltip>;
+    } else {
+        return null;
+    }
+}
 
 export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -59,6 +117,9 @@ export const ResourceName = connect(
 const renderIcon = (item: GroupContentsResource) => {
     switch (item.kind) {
         case ResourceKind.PROJECT:
+            if (item.groupClass === GroupClass.FILTER) {
+                return <FilterGroupIcon />;
+            }
             return <ProjectIcon />;
         case ResourceKind.COLLECTION:
             if (item.uuid === item.currentVersionUuid) {
@@ -127,11 +188,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);
@@ -147,14 +208,32 @@ export const ResourceLastName = connect(
         return resource || { lastName: '' };
     })(renderLastName);
 
-const renderUuid = (item: { uuid: string }) =>
-    <Typography noWrap>{item.uuid}</Typography>;
+const renderFullName = (dispatch: Dispatch, item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
+    const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
+    return link ? <Typography noWrap
+        color="primary"
+        style={{ 'cursor': 'pointer' }}
+        onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
+        {displayName}
+    </Typography> :
+        <Typography noWrap>{displayName}</Typography>;
+}
 
-export const ResourceUuid = connect(
-    (state: RootState, props: { uuid: string }) => {
+export const UserResourceFullName = connect(
+    (state: RootState, props: { uuid: string, link?: boolean }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { uuid: '' };
-    })(renderUuid);
+        return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
+    })((props: { item: { uuid: string, firstName: string, lastName: string }, link?: boolean } & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
+
+const renderUuid = (item: { uuid: string }) =>
+    <Typography data-cy="uuid" noWrap>
+        {item.uuid}
+        <CopyToClipboardSnackbar value={item.uuid} />
+    </Typography>;
+
+export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
+    getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
+))(renderUuid);
 
 const renderEmail = (item: { email: string }) =>
     <Typography noWrap>{item.email}</Typography>;
@@ -165,24 +244,115 @@ 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)} />;
+enum UserAccountStatus {
+    ACTIVE = 'Active',
+    INACTIVE = 'Inactive',
+    SETUP = 'Setup',
+    UNKNOWN = ''
+}
+
+const renderAccountStatus = (props: { status: UserAccountStatus }) =>
+    <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
+        <Grid item>
+            {(() => {
+                switch (props.status) {
+                    case UserAccountStatus.ACTIVE:
+                        return <ActiveIcon style={{ color: '#4caf50', verticalAlign: "middle" }} />;
+                    case UserAccountStatus.SETUP:
+                        return <SetupIcon style={{ color: '#2196f3', verticalAlign: "middle" }} />;
+                    case UserAccountStatus.INACTIVE:
+                        return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: "middle" }} />;
+                    default:
+                        return <></>;
+                }
+            })()}
+        </Grid>
+        <Grid item>
+            <Typography noWrap>
+                {props.status}
+            </Typography>
+        </Grid>
+    </Grid>;
 
-export const ResourceIsActive = connect(
+const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
+    const user = getResource<UserResource>(props.uuid)(state.resources);
+    // Get membership links for all users group
+    const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+    const permissions = filterResources((resource: LinkResource) =>
+        resource.kind === ResourceKind.LINK &&
+        resource.linkClass === LinkClass.PERMISSION &&
+        resource.headUuid === allUsersGroupUuid &&
+        resource.tailUuid === props.uuid
+    )(state.resources);
+
+    if (user) {
+        return user.isActive ? { status: UserAccountStatus.ACTIVE } : permissions.length > 0 ? { status: UserAccountStatus.SETUP } : { status: UserAccountStatus.INACTIVE };
+    } else {
+        return { status: UserAccountStatus.UNKNOWN };
+    }
+}
+
+export const ResourceLinkTailAccountStatus = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { isActive: false };
-    }, { toggleIsActive }
-)(renderIsActive);
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
+    })(renderAccountStatus);
+
+export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
+
+const renderIsHidden = (props: {
+    memberLinkUuid: string,
+    permissionLinkUuid: string,
+    visible: boolean,
+    canManage: boolean,
+    setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
+}) => {
+    if (props.memberLinkUuid) {
+        return <Checkbox
+            data-cy="user-visible-checkbox"
+            color="primary"
+            checked={props.visible}
+            disabled={!props.canManage}
+            onClick={(e) => {
+                e.stopPropagation();
+                props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
+            }} />;
+    } else {
+        return <Typography />;
+    }
+}
+
+export const ResourceLinkTailIsVisible = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const member = getResource<Resource>(link?.tailUuid || '')(state.resources);
+        const group = getResource<GroupResource>(link?.headUuid || '')(state.resources);
+        const permissions = filterResources((resource: LinkResource) => {
+            return resource.linkClass === LinkClass.PERMISSION
+                && resource.headUuid === link?.tailUuid
+                && resource.tailUuid === group?.uuid
+                && resource.name === PermissionLevel.CAN_READ;
+        })(state.resources);
+
+        const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : '';
+        const isVisible = link && group && permissions.length > 0;
+        // Consider whether the current user canManage this resurce in addition when it's possible
+        const isBuiltin = isBuiltinGroup(link?.headUuid || '');
+
+        return member?.kind === ResourceKind.USER
+            ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
+            : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
+    }, { setMemberIsHidden }
+)(renderIsHidden);
 
 const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
     <Checkbox
         color="primary"
         checked={props.isAdmin}
-        onClick={() => props.toggleIsAdmin(props.uuid)} />;
+        onClick={(e) => {
+            e.stopPropagation();
+            props.toggleIsAdmin(props.uuid);
+        }} />;
 
 export const ResourceIsAdmin = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -191,15 +361,37 @@ export const ResourceIsAdmin = connect(
     }, { toggleIsAdmin }
 )(renderIsAdmin);
 
-const renderUsername = (item: { username: string }) =>
-    <Typography noWrap>{item.username}</Typography>;
+const renderUsername = (item: { username: string, uuid: string }) =>
+    <Typography noWrap>{item.username || item.uuid}</Typography>;
 
 export const ResourceUsername = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<UserResource>(props.uuid)(state.resources);
-        return resource || { username: '' };
+        return resource || { username: '', uuid: props.uuid };
     })(renderUsername);
 
+// Virtual machine resource
+
+const renderHostname = (item: { hostname: string }) =>
+    <Typography noWrap>{item.hostname}</Typography>;
+
+export const VirtualMachineHostname = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
+        return resource || { hostname: '' };
+    })(renderHostname);
+
+const renderVirtualMachineLogin = (login: { user: string }) =>
+    <Typography noWrap>{login.user}</Typography>
+
+export const VirtualMachineLogin = connect(
+    (state: RootState, props: { linkUuid: string }) => {
+        const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
+        const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
+
+        return { user: user?.username || permission?.tailUuid || '' };
+    })(renderVirtualMachineLogin);
+
 // Common methods
 const renderCommonData = (data: string) =>
     <Typography noWrap>{data}</Typography>;
@@ -228,11 +420,6 @@ export const TokenScopes = withResourceData('scopes', renderCommonData);
 
 export const TokenUserId = withResourceData('userId', renderCommonData);
 
-// Compute Node Resources
-const renderNodeInfo = (data: string) => {
-    return <Typography>{JSON.stringify(data, null, 4)}</Typography>;
-};
-
 const clusterColors = [
     ['#f44336', '#fff'],
     ['#2196f3', '#fff'],
@@ -244,7 +431,7 @@ const clusterColors = [
 export const ResourceCluster = (props: { uuid: string }) => {
     const CLUSTER_ID_LENGTH = 5;
     const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
-    const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
+    const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
     const ci = pos >= CLUSTER_ID_LENGTH ? (((((
         (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
         + props.uuid.charCodeAt(2))
@@ -258,20 +445,6 @@ export const ResourceCluster = (props: { uuid: string }) => {
     }}>{clusterId}</span>;
 };
 
-export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
-
-export const ComputeNodeDomain = withResourceData('domain', renderCommonData);
-
-export const ComputeNodeFirstPingAt = withResourceData('firstPingAt', renderCommonDate);
-
-export const ComputeNodeHostname = withResourceData('hostname', renderCommonData);
-
-export const ComputeNodeIpAddress = withResourceData('ipAddress', renderCommonData);
-
-export const ComputeNodeJobUuid = withResourceData('jobUuid', renderCommonData);
-
-export const ComputeNodeLastPingAt = withResourceData('lastPingAt', renderCommonDate);
-
 // Links Resources
 const renderLinkName = (item: { name: string }) =>
     <Typography noWrap>{item.name || '(none)'}</Typography>;
@@ -291,45 +464,45 @@ 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 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
+        return getUserDisplayName(resource as UserResource);
+    } else {
+        return (resource as GroupContentsResource).name;
+    }
+}
 
-const renderLink = (dispatch: Dispatch, uuid: string, label: string) =>
-    <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(uuid))}>
-        {label}: {uuid}
+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, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || 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?.tailKind || 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>) =>
+        renderResourceLink(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>) =>
+        renderResourceLink(props.dispatch, props.item));
 
 export const ResourceLinkUuid = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -337,6 +510,117 @@ 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, canManage: boolean) => {
+    if (item.uuid) {
+        return canManage ?
+            <Typography noWrap>
+                <IconButton data-cy="resource-delete-button" onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
+                    <RemoveIcon />
+                </IconButton>
+            </Typography> :
+            <Typography noWrap>
+                <IconButton disabled data-cy="resource-delete-button">
+                    <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);
+        const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
+
+        return {
+            item: link || { uuid: '', kind: ResourceKind.NONE },
+            canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
+        };
+    })((props: { item: LinkResource, canManage: boolean } & DispatchProp<any>) =>
+        renderLinkDelete(props.dispatch, props.item, props.canManage));
+
+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);
+
+const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
+    return <Typography noWrap>
+        {formatPermissionLevel(link.name as PermissionLevel)}
+        {canManage ?
+            <IconButton data-cy="edit-permission-button" onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
+                <RenameIcon />
+            </IconButton> :
+            ''
+        }
+    </Typography>;
+}
+
+export const ResourceLinkHeadPermissionLevel = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
+
+        return {
+            link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
+            canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
+        };
+    })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
+        renderPermissionLevel(props.dispatch, props.link, props.canManage));
+
+export const ResourceLinkTailPermissionLevel = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const link = getResource<LinkResource>(props.uuid)(state.resources);
+        const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
+
+        return {
+            link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
+            canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
+        };
+    })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
+        renderPermissionLevel(props.dispatch, props.link, props.canManage));
+
+const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
+    const headResource = getResource<Resource>(link.headUuid)(state.resources);
+    // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
+    const userUuid = getUserUuid(state);
+
+    if (headResource && headResource.kind === ResourceKind.GROUP) {
+        return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
+    } else {
+        // true for now
+        return true;
+    }
+}
+
 // Process Resources
 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
     return (
@@ -381,17 +665,25 @@ export const ResourceWorkflowStatus = connect(
         };
     })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
 
-export const ResourceLastModifiedDate = connect(
+const renderProcessState = (processState: string) => <Typography>{processState || '-'}</Typography>
+
+export const ResourceProcessState = connect(
     (state: RootState, props: { uuid: string }) => {
-        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
-        return { date: resource ? resource.modifiedAt : '' };
-    })((props: { date: string }) => renderDate(props.date));
+        const resource = getResource<ContainerRequestResource>(props.uuid)(state.resources);
+        return { state: resource?.state ? resource.state: '' };
+    })((props: { state: string }) => renderProcessState(props.state));
 
 export const ResourceCreatedAtDate = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return { date: resource ? resource.createdAt : '' };
     })((props: { date: string }) => renderDate(props.date));
+    
+export const ResourceLastModifiedDate = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+        return { date: resource ? resource.modifiedAt : '' };
+    })((props: { date: string }) => renderDate(props.date));
 
 export const ResourceTrashDate = connect(
     (state: RootState, props: { uuid: string }) => {
@@ -413,12 +705,17 @@ export const renderFileSize = (fileSize?: number) =>
 export const ResourceFileSize = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+
+        if (resource && resource.kind !== ResourceKind.COLLECTION) {
+            return { fileSize: '' };
+        }
+
         return { fileSize: resource ? resource.fileSizeTotal : 0 };
     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 
 const renderOwner = (owner: string) =>
     <Typography noWrap>
-        {owner}
+        {owner || '-'}
     </Typography>;
 
 export const ResourceOwner = connect(
@@ -435,45 +732,182 @@ export const ResourceOwnerName = connect(
         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
     })((props: { owner: string }) => renderOwner(props.owner));
 
-export const ResourceOwnerWithName =
+export const ResourceUUID = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { uuid: resource ? resource.uuid : '' };
+    })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
+    
+const renderPortableDataHash = (portableDataHash:string | null) => 
+    <Typography noWrap>
+        {portableDataHash ? <>{portableDataHash}
+        <CopyToClipboardSnackbar value={portableDataHash} /></> : '-' }
+    </Typography>
+    
+export const ResourcePortableDataHash = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        // console.log('COLLECTION_RESOIRCE', resource)
+        return { portableDataHash: resource ? resource.portableDataHash : '' };    
+    })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
+
+const renderVersion = (version: number) =>{
+    return <Typography>{version ?? '-'}</Typography>
+}
+
+export const ResourceVersion = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { version: resource ? resource.version: '' };
+    })((props: { version: number }) => renderVersion(props.version));
+
+const renderDescription = (description: string)=>{
+    const truncatedDescription = description ? description.slice(0, 18) + '...' : '-'
+    return <Typography title={description}>{truncatedDescription}</Typography>;
+}
+
+const renderFileCount = (fileCount: number) =>{
+    return <Typography>{fileCount ?? '-'}</Typography>
+}
+
+export const ResourceFileCount = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<CollectionResource>(props.uuid)(state.resources);
+        return { fileCount: resource ? resource.fileCount: '' };
+    })((props: { fileCount: number }) => renderFileCount(props.fileCount));
+
+export const ResourceDescription = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+        //testing---------------
+        const containerRequestDescription = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
+        if (resource && !resource.description && resource.kind === ResourceKind.PROCESS) resource.description = containerRequestDescription
+        //testing---------------
+        return { description: resource ? resource.description : '' };
+    })((props: { description: string }) => renderDescription(props.description));
+
+const userFromID =
+    connect(
+        (state: RootState, props: { uuid: string }) => {
+            let userFullname = '';
+            const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+
+            if (resource) {
+                userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
+            }
+
+            return { uuid: props.uuid, userFullname };
+        });
+
+const ownerFromResourceId =
+    compose(
+        connect((state: RootState, props: { uuid: string }) => {
+            const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+            return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
+        }),
+        userFromID
+    );
+
+const _resourceWithName =
+    withStyles({}, { withTheme: true })
+        ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
+            const { uuid, userFullname, dispatch, theme } = props;
+
+            if (userFullname === '') {
+                dispatch<any>(loadResource(uuid, false));
+                return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
+                    {uuid}
+                </Typography>;
+            }
+
+            return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
+                {userFullname} ({uuid})
+            </Typography>;
+        });
+
+export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
+
+export const ResourceWithName = userFromID(_resourceWithName);
+
+export const UserNameFromID =
+    compose(userFromID)(
+        (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
+            const { uuid, userFullname, dispatch } = props;
+
+            if (userFullname === '') {
+                dispatch<any>(loadResource(uuid, false));
+            }
+            return <span>
+                {userFullname ? userFullname : uuid}
+            </span>;
+        });
+
+export const ResponsiblePerson =
     compose(
         connect(
-            (state: RootState, props: { uuid: string }) => {
-                let ownerName = '';
-                const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+            (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
+                let responsiblePersonName: string = '';
+                let responsiblePersonUUID: string = '';
+                let responsiblePersonProperty: string = '';
+
+                if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
+                    let index = 0;
+                    const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
+
+                    while (!responsiblePersonProperty && keys[index]) {
+                        const key = keys[index];
+                        if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
+                            responsiblePersonProperty = key;
+                        }
+                        index++;
+                    }
+                }
+
+                let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
+
+                while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
+                    responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
+                    resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
+                }
 
-                if (resource) {
-                    ownerName = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
+                if (resource && resource.kind === ResourceKind.USER) {
+                    responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
                 }
 
-                return { uuid: props.uuid, ownerName };
+                return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
             }),
         withStyles({}, { withTheme: true }))
-        ((props: { uuid: string, ownerName: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
-            const { uuid, ownerName, dispatch, theme } = props;
+        ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
+            const { uuid, responsiblePersonName, parentRef, theme } = props;
+
+            if (!uuid && parentRef) {
+                parentRef.style.display = 'none';
+                return null;
+            } else if (parentRef) {
+                parentRef.style.display = 'block';
+            }
 
-            if (ownerName === '') {
-                dispatch<any>(loadResource(uuid, false));
+            if (!responsiblePersonName) {
                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
                     {uuid}
                 </Typography>;
             }
 
             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
-                {ownerName} ({uuid})
+                {responsiblePersonName} ({uuid})
             </Typography>;
         });
 
-const renderType = (type: string) =>
+const renderType = (type: string, subtype: string) =>
     <Typography noWrap>
-        {resourceLabel(type)}
+        {resourceLabel(type, subtype)}
     </Typography>;
 
 export const ResourceType = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
-        return { type: resource ? resource.kind : '' };
-    })((props: { type: string }) => renderType(props.type));
+        return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
+    })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
 
 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
     return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
@@ -491,19 +925,36 @@ export const CollectionStatus = connect((state: RootState, props: { uuid: string
         : <Typography>head version</Typography>
 );
 
+export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
+    return {
+                collection: getResource<CollectionResource>(props.uuid)(state.resources),
+                uuid: props.uuid,
+                className: props.className,
+            };
+})((props: { collection: CollectionResource, uuid: string, className?: string }) =>
+        <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
+);
+
 export const ProcessStatus = compose(
     connect((state: RootState, props: { uuid: string }) => {
         return { process: getProcess(props.uuid)(state.resources) };
     }),
     withStyles({}, { withTheme: true }))
-    ((props: { process?: Process, theme: ArvadosTheme }) => {
-        const status = props.process ? getProcessStatus(props.process) : "-";
-        return <Typography
-            noWrap
-            style={{ color: getProcessStatusColor(status, props.theme) }} >
-            {status}
-        </Typography>;
-    });
+    ((props: { process?: Process, theme: ArvadosTheme }) =>
+        props.process
+            ? <Chip label={getProcessStatus(props.process)}
+                style={{
+                    height: props.theme.spacing.unit * 3,
+                    width: props.theme.spacing.unit * 12,
+                    backgroundColor: getProcessStatusColor(
+                        getProcessStatus(props.process), props.theme),
+                    color: props.theme.palette.common.white,
+                    fontSize: '0.875rem',
+                    borderRadius: props.theme.spacing.unit * 0.625,
+                }}
+            />
+            : <Typography>-</Typography>
+    );
 
 export const ProcessStartDate = connect(
     (state: RootState, props: { uuid: string }) => {