fileCount column up Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>
[arvados-workbench2.git] / src / views-components / data-explorer / renderers.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import {
7     Grid,
8     Typography,
9     withStyles,
10     Tooltip,
11     IconButton,
12     Checkbox,
13     Chip
14 } from '@material-ui/core';
15 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
16 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
17 import {
18     FreezeIcon,
19     ProjectIcon,
20     FilterGroupIcon,
21     CollectionIcon,
22     ProcessIcon,
23     DefaultIcon,
24     ShareIcon,
25     CollectionOldVersionIcon,
26     WorkflowIcon,
27     RemoveIcon,
28     RenameIcon,
29     ActiveIcon,
30     SetupIcon,
31     InactiveIcon,
32 } from 'components/icon/icon';
33 import { formatDate, formatFileSize, formatTime, formatObjectProperties} from 'common/formatters';
34 import { resourceLabel } from 'common/labels';
35 import { connect, DispatchProp } from 'react-redux';
36 import { RootState } from 'store/store';
37 import { getResource, filterResources } from 'store/resources/resources';
38 import { GroupContentsResource } from 'services/groups-service/groups-service';
39 import { getProcess, Process, getProcessStatus, getProcessStatusColor, getProcessRuntime } from 'store/processes/process';
40 import { ArvadosTheme } from 'common/custom-theme';
41 import { compose, Dispatch } from 'redux';
42 import { WorkflowResource } from 'models/workflow';
43 import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-panel-view';
44 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
45 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
46 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
47 import { toggleIsAdmin } from 'store/users/users-actions';
48 import { LinkClass, LinkResource } from 'models/link';
49 import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
50 import { withResourceData } from 'views-components/data-explorer/with-resources';
51 import { CollectionResource } from 'models/collection';
52 import { IllegalNamingWarning } from 'components/warning/warning';
53 import { loadResource } from 'store/resources/resources-actions';
54 import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
55 import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
56 import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
57 import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
58 import { PermissionLevel } from 'models/permission';
59 import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
60 import { getUserUuid } from 'common/getuser';
61 import { VirtualMachinesResource } from 'models/virtual-machines';
62 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
63 import { ProjectResource } from 'models/project';
64
65 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
66
67     const navFunc = ("groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo);
68     return <Grid container alignItems="center" wrap="nowrap" spacing={16}>
69         <Grid item>
70             {renderIcon(item)}
71         </Grid>
72         <Grid item>
73             <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
74                 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
75                     ? <IllegalNamingWarning name={item.name} />
76                     : null}
77                 {item.name}
78             </Typography>
79         </Grid>
80         <Grid item>
81             <Typography variant="caption">
82                 <FavoriteStar resourceUuid={item.uuid} />
83                 <PublicFavoriteStar resourceUuid={item.uuid} />
84                 {
85                     item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />
86                 }
87             </Typography>
88         </Grid>
89     </Grid>;
90 };
91
92 const FrozenProject = (props: {item: ProjectResource}) => {
93     const [fullUsername, setFullusername] = React.useState<any>(null);
94     const getFullName = React.useCallback(() => {
95         if (props.item.frozenByUuid) {
96             setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
97         }
98     }, [props.item, setFullusername])
99
100     if (props.item.frozenByUuid) {
101
102         return <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
103             <FreezeIcon style={{ fontSize: "inherit" }}/>
104         </Tooltip>;
105     } else {
106         return null;
107     }
108 }
109
110 export const ResourceName = connect(
111     (state: RootState, props: { uuid: string }) => {
112         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
113         return resource;
114     })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
115
116 const renderIcon = (item: GroupContentsResource) => {
117     switch (item.kind) {
118         case ResourceKind.PROJECT:
119             if (item.groupClass === GroupClass.FILTER) {
120                 return <FilterGroupIcon />;
121             }
122             return <ProjectIcon />;
123         case ResourceKind.COLLECTION:
124             if (item.uuid === item.currentVersionUuid) {
125                 return <CollectionIcon />;
126             }
127             return <CollectionOldVersionIcon />;
128         case ResourceKind.PROCESS:
129             return <ProcessIcon />;
130         case ResourceKind.WORKFLOW:
131             return <WorkflowIcon />;
132         default:
133             return <DefaultIcon />;
134     }
135 };
136
137 const renderDate = (date?: string) => {
138     return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
139 };
140
141 const renderWorkflowName = (item: WorkflowResource) =>
142     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
143         <Grid item>
144             {renderIcon(item)}
145         </Grid>
146         <Grid item>
147             <Typography color="primary" style={{ width: '100px' }}>
148                 {item.name}
149             </Typography>
150         </Grid>
151     </Grid>;
152
153 export const ResourceWorkflowName = connect(
154     (state: RootState, props: { uuid: string }) => {
155         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
156         return resource;
157     })(renderWorkflowName);
158
159 const getPublicUuid = (uuidPrefix: string) => {
160     return `${uuidPrefix}-tpzed-anonymouspublic`;
161 };
162
163 const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
164     const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
165     return (
166         <div>
167             {!isPublic && uuid &&
168                 <Tooltip title="Share">
169                     <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
170                         <ShareIcon />
171                     </IconButton>
172                 </Tooltip>
173             }
174         </div>
175     );
176 };
177
178 export const ResourceShare = connect(
179     (state: RootState, props: { uuid: string }) => {
180         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
181         const uuidPrefix = getUuidPrefix(state);
182         return {
183             uuid: resource ? resource.uuid : '',
184             ownerUuid: resource ? resource.ownerUuid : '',
185             uuidPrefix
186         };
187     })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
188         resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
189
190 // User Resources
191 const renderFirstName = (item: { firstName: string }) => {
192     return <Typography noWrap>{item.firstName}</Typography>;
193 };
194
195 export const ResourceFirstName = connect(
196     (state: RootState, props: { uuid: string }) => {
197         const resource = getResource<UserResource>(props.uuid)(state.resources);
198         return resource || { firstName: '' };
199     })(renderFirstName);
200
201 const renderLastName = (item: { lastName: string }) =>
202     <Typography noWrap>{item.lastName}</Typography>;
203
204 export const ResourceLastName = connect(
205     (state: RootState, props: { uuid: string }) => {
206         const resource = getResource<UserResource>(props.uuid)(state.resources);
207         return resource || { lastName: '' };
208     })(renderLastName);
209
210 const renderFullName = (dispatch: Dispatch, item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
211     const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
212     return link ? <Typography noWrap
213         color="primary"
214         style={{ 'cursor': 'pointer' }}
215         onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
216         {displayName}
217     </Typography> :
218         <Typography noWrap>{displayName}</Typography>;
219 }
220
221 export const UserResourceFullName = connect(
222     (state: RootState, props: { uuid: string, link?: boolean }) => {
223         const resource = getResource<UserResource>(props.uuid)(state.resources);
224         return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
225     })((props: { item: { uuid: string, firstName: string, lastName: string }, link?: boolean } & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
226
227 const renderUuid = (item: { uuid: string }) =>
228     <Typography data-cy="uuid" noWrap>
229         {item.uuid}
230         <CopyToClipboardSnackbar value={item.uuid} />
231     </Typography>;
232
233 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
234     getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
235 ))(renderUuid);
236
237 const renderEmail = (item: { email: string }) =>
238     <Typography noWrap>{item.email}</Typography>;
239
240 export const ResourceEmail = connect(
241     (state: RootState, props: { uuid: string }) => {
242         const resource = getResource<UserResource>(props.uuid)(state.resources);
243         return resource || { email: '' };
244     })(renderEmail);
245
246 enum UserAccountStatus {
247     ACTIVE = 'Active',
248     INACTIVE = 'Inactive',
249     SETUP = 'Setup',
250     UNKNOWN = ''
251 }
252
253 const renderAccountStatus = (props: { status: UserAccountStatus }) =>
254     <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
255         <Grid item>
256             {(() => {
257                 switch (props.status) {
258                     case UserAccountStatus.ACTIVE:
259                         return <ActiveIcon style={{ color: '#4caf50', verticalAlign: "middle" }} />;
260                     case UserAccountStatus.SETUP:
261                         return <SetupIcon style={{ color: '#2196f3', verticalAlign: "middle" }} />;
262                     case UserAccountStatus.INACTIVE:
263                         return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: "middle" }} />;
264                     default:
265                         return <></>;
266                 }
267             })()}
268         </Grid>
269         <Grid item>
270             <Typography noWrap>
271                 {props.status}
272             </Typography>
273         </Grid>
274     </Grid>;
275
276 const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
277     const user = getResource<UserResource>(props.uuid)(state.resources);
278     // Get membership links for all users group
279     const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
280     const permissions = filterResources((resource: LinkResource) =>
281         resource.kind === ResourceKind.LINK &&
282         resource.linkClass === LinkClass.PERMISSION &&
283         resource.headUuid === allUsersGroupUuid &&
284         resource.tailUuid === props.uuid
285     )(state.resources);
286
287     if (user) {
288         return user.isActive ? { status: UserAccountStatus.ACTIVE } : permissions.length > 0 ? { status: UserAccountStatus.SETUP } : { status: UserAccountStatus.INACTIVE };
289     } else {
290         return { status: UserAccountStatus.UNKNOWN };
291     }
292 }
293
294 export const ResourceLinkTailAccountStatus = connect(
295     (state: RootState, props: { uuid: string }) => {
296         const link = getResource<LinkResource>(props.uuid)(state.resources);
297         return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
298     })(renderAccountStatus);
299
300 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
301
302 const renderIsHidden = (props: {
303     memberLinkUuid: string,
304     permissionLinkUuid: string,
305     visible: boolean,
306     canManage: boolean,
307     setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
308 }) => {
309     if (props.memberLinkUuid) {
310         return <Checkbox
311             data-cy="user-visible-checkbox"
312             color="primary"
313             checked={props.visible}
314             disabled={!props.canManage}
315             onClick={(e) => {
316                 e.stopPropagation();
317                 props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
318             }} />;
319     } else {
320         return <Typography />;
321     }
322 }
323
324 export const ResourceLinkTailIsVisible = connect(
325     (state: RootState, props: { uuid: string }) => {
326         const link = getResource<LinkResource>(props.uuid)(state.resources);
327         const member = getResource<Resource>(link?.tailUuid || '')(state.resources);
328         const group = getResource<GroupResource>(link?.headUuid || '')(state.resources);
329         const permissions = filterResources((resource: LinkResource) => {
330             return resource.linkClass === LinkClass.PERMISSION
331                 && resource.headUuid === link?.tailUuid
332                 && resource.tailUuid === group?.uuid
333                 && resource.name === PermissionLevel.CAN_READ;
334         })(state.resources);
335
336         const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : '';
337         const isVisible = link && group && permissions.length > 0;
338         // Consider whether the current user canManage this resurce in addition when it's possible
339         const isBuiltin = isBuiltinGroup(link?.headUuid || '');
340
341         return member?.kind === ResourceKind.USER
342             ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
343             : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
344     }, { setMemberIsHidden }
345 )(renderIsHidden);
346
347 const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
348     <Checkbox
349         color="primary"
350         checked={props.isAdmin}
351         onClick={(e) => {
352             e.stopPropagation();
353             props.toggleIsAdmin(props.uuid);
354         }} />;
355
356 export const ResourceIsAdmin = connect(
357     (state: RootState, props: { uuid: string }) => {
358         const resource = getResource<UserResource>(props.uuid)(state.resources);
359         return resource || { isAdmin: false };
360     }, { toggleIsAdmin }
361 )(renderIsAdmin);
362
363 const renderUsername = (item: { username: string, uuid: string }) =>
364     <Typography noWrap>{item.username || item.uuid}</Typography>;
365
366 export const ResourceUsername = connect(
367     (state: RootState, props: { uuid: string }) => {
368         const resource = getResource<UserResource>(props.uuid)(state.resources);
369         return resource || { username: '', uuid: props.uuid };
370     })(renderUsername);
371
372 // Virtual machine resource
373
374 const renderHostname = (item: { hostname: string }) =>
375     <Typography noWrap>{item.hostname}</Typography>;
376
377 export const VirtualMachineHostname = connect(
378     (state: RootState, props: { uuid: string }) => {
379         const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
380         return resource || { hostname: '' };
381     })(renderHostname);
382
383 const renderVirtualMachineLogin = (login: { user: string }) =>
384     <Typography noWrap>{login.user}</Typography>
385
386 export const VirtualMachineLogin = connect(
387     (state: RootState, props: { linkUuid: string }) => {
388         const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
389         const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
390
391         return { user: user?.username || permission?.tailUuid || '' };
392     })(renderVirtualMachineLogin);
393
394 // Common methods
395 const renderCommonData = (data: string) =>
396     <Typography noWrap>{data}</Typography>;
397
398 const renderCommonDate = (date: string) =>
399     <Typography noWrap>{formatDate(date)}</Typography>;
400
401 export const CommonUuid = withResourceData('uuid', renderCommonData);
402
403 // Api Client Authorizations
404 export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
405
406 export const TokenApiToken = withResourceData('apiToken', renderCommonData);
407
408 export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
409
410 export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
411
412 export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
413
414 export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
415
416 export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
417
418 export const TokenScopes = withResourceData('scopes', renderCommonData);
419
420 export const TokenUserId = withResourceData('userId', renderCommonData);
421
422 const clusterColors = [
423     ['#f44336', '#fff'],
424     ['#2196f3', '#fff'],
425     ['#009688', '#fff'],
426     ['#cddc39', '#fff'],
427     ['#ff9800', '#fff']
428 ];
429
430 export const ResourceCluster = (props: { uuid: string }) => {
431     const CLUSTER_ID_LENGTH = 5;
432     const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
433     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
434     const ci = pos >= CLUSTER_ID_LENGTH ? (((((
435         (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
436         + props.uuid.charCodeAt(2))
437         * props.uuid.charCodeAt(3))
438         + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
439     return <span style={{
440         backgroundColor: clusterColors[ci][0],
441         color: clusterColors[ci][1],
442         padding: "2px 7px",
443         borderRadius: 3
444     }}>{clusterId}</span>;
445 };
446
447 // Links Resources
448 const renderLinkName = (item: { name: string }) =>
449     <Typography noWrap>{item.name || '(none)'}</Typography>;
450
451 export const ResourceLinkName = connect(
452     (state: RootState, props: { uuid: string }) => {
453         const resource = getResource<LinkResource>(props.uuid)(state.resources);
454         return resource || { name: '' };
455     })(renderLinkName);
456
457 const renderLinkClass = (item: { linkClass: string }) =>
458     <Typography noWrap>{item.linkClass}</Typography>;
459
460 export const ResourceLinkClass = connect(
461     (state: RootState, props: { uuid: string }) => {
462         const resource = getResource<LinkResource>(props.uuid)(state.resources);
463         return resource || { linkClass: '' };
464     })(renderLinkClass);
465
466 const getResourceDisplayName = (resource: Resource): string => {
467     if ((resource as UserResource).kind === ResourceKind.USER
468         && typeof (resource as UserResource).firstName !== 'undefined') {
469         // We can be sure the resource is UserResource
470         return getUserDisplayName(resource as UserResource);
471     } else {
472         return (resource as GroupContentsResource).name;
473     }
474 }
475
476 const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
477     var displayName = getResourceDisplayName(item);
478
479     return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
480         {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
481     </Typography>;
482 };
483
484 export const ResourceLinkTail = connect(
485     (state: RootState, props: { uuid: string }) => {
486         const resource = getResource<LinkResource>(props.uuid)(state.resources);
487         const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
488
489         return {
490             item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE }
491         };
492     })((props: { item: Resource } & DispatchProp<any>) =>
493         renderResourceLink(props.dispatch, props.item));
494
495 export const ResourceLinkHead = connect(
496     (state: RootState, props: { uuid: string }) => {
497         const resource = getResource<LinkResource>(props.uuid)(state.resources);
498         const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
499
500         return {
501             item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE }
502         };
503     })((props: { item: Resource } & DispatchProp<any>) =>
504         renderResourceLink(props.dispatch, props.item));
505
506 export const ResourceLinkUuid = connect(
507     (state: RootState, props: { uuid: string }) => {
508         const resource = getResource<LinkResource>(props.uuid)(state.resources);
509         return resource || { uuid: '' };
510     })(renderUuid);
511
512 export const ResourceLinkHeadUuid = connect(
513     (state: RootState, props: { uuid: string }) => {
514         const link = getResource<LinkResource>(props.uuid)(state.resources);
515         const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
516
517         return headResource || { uuid: '' };
518     })(renderUuid);
519
520 export const ResourceLinkTailUuid = connect(
521     (state: RootState, props: { uuid: string }) => {
522         const link = getResource<LinkResource>(props.uuid)(state.resources);
523         const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
524
525         return tailResource || { uuid: '' };
526     })(renderUuid);
527
528 const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
529     if (item.uuid) {
530         return canManage ?
531             <Typography noWrap>
532                 <IconButton data-cy="resource-delete-button" onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
533                     <RemoveIcon />
534                 </IconButton>
535             </Typography> :
536             <Typography noWrap>
537                 <IconButton disabled data-cy="resource-delete-button">
538                     <RemoveIcon />
539                 </IconButton>
540             </Typography>;
541     } else {
542         return <Typography noWrap></Typography>;
543     }
544 }
545
546 export const ResourceLinkDelete = connect(
547     (state: RootState, props: { uuid: string }) => {
548         const link = getResource<LinkResource>(props.uuid)(state.resources);
549         const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
550
551         return {
552             item: link || { uuid: '', kind: ResourceKind.NONE },
553             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
554         };
555     })((props: { item: LinkResource, canManage: boolean } & DispatchProp<any>) =>
556         renderLinkDelete(props.dispatch, props.item, props.canManage));
557
558 export const ResourceLinkTailEmail = connect(
559     (state: RootState, props: { uuid: string }) => {
560         const link = getResource<LinkResource>(props.uuid)(state.resources);
561         const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
562
563         return resource || { email: '' };
564     })(renderEmail);
565
566 export const ResourceLinkTailUsername = connect(
567     (state: RootState, props: { uuid: string }) => {
568         const link = getResource<LinkResource>(props.uuid)(state.resources);
569         const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
570
571         return resource || { username: '' };
572     })(renderUsername);
573
574 const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
575     return <Typography noWrap>
576         {formatPermissionLevel(link.name as PermissionLevel)}
577         {canManage ?
578             <IconButton data-cy="edit-permission-button" onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
579                 <RenameIcon />
580             </IconButton> :
581             ''
582         }
583     </Typography>;
584 }
585
586 export const ResourceLinkHeadPermissionLevel = connect(
587     (state: RootState, props: { uuid: string }) => {
588         const link = getResource<LinkResource>(props.uuid)(state.resources);
589         const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
590
591         return {
592             link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
593             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
594         };
595     })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
596         renderPermissionLevel(props.dispatch, props.link, props.canManage));
597
598 export const ResourceLinkTailPermissionLevel = connect(
599     (state: RootState, props: { uuid: string }) => {
600         const link = getResource<LinkResource>(props.uuid)(state.resources);
601         const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
602
603         return {
604             link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
605             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
606         };
607     })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
608         renderPermissionLevel(props.dispatch, props.link, props.canManage));
609
610 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
611     const headResource = getResource<Resource>(link.headUuid)(state.resources);
612     // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
613     const userUuid = getUserUuid(state);
614
615     if (headResource && headResource.kind === ResourceKind.GROUP) {
616         return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
617     } else {
618         // true for now
619         return true;
620     }
621 }
622
623 // Process Resources
624 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
625     return (
626         <div>
627             {uuid &&
628                 <Tooltip title="Run process">
629                     <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
630                         <ProcessIcon />
631                     </IconButton>
632                 </Tooltip>}
633         </div>
634     );
635 };
636
637 export const ResourceRunProcess = connect(
638     (state: RootState, props: { uuid: string }) => {
639         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
640         return {
641             uuid: resource ? resource.uuid : ''
642         };
643     })((props: { uuid: string } & DispatchProp<any>) =>
644         resourceRunProcess(props.dispatch, props.uuid));
645
646 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
647     if (ownerUuid === getPublicUuid(uuidPrefix)) {
648         return renderStatus(WorkflowStatus.PUBLIC);
649     } else {
650         return renderStatus(WorkflowStatus.PRIVATE);
651     }
652 };
653
654 const renderStatus = (status: string) =>
655     <Typography noWrap style={{ width: '60px' }}>{status}</Typography>;
656
657 export const ResourceWorkflowStatus = connect(
658     (state: RootState, props: { uuid: string }) => {
659         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
660         const uuidPrefix = getUuidPrefix(state);
661         return {
662             ownerUuid: resource ? resource.ownerUuid : '',
663             uuidPrefix
664         };
665     })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
666
667 export const ResourceCreatedAtDate = connect(
668     (state: RootState, props: { uuid: string }) => {
669         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
670         return { date: resource ? resource.createdAt : '' };
671     })((props: { date: string }) => renderDate(props.date));
672     
673 export const ResourceLastModifiedDate = connect(
674     (state: RootState, props: { uuid: string }) => {
675         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
676         return { date: resource ? resource.modifiedAt : '' };
677     })((props: { date: string }) => renderDate(props.date));
678
679 export const ResourceTrashDate = connect(
680     (state: RootState, props: { uuid: string }) => {
681         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
682         return { date: resource ? resource.trashAt : '' };
683     })((props: { date: string }) => renderDate(props.date));
684
685 export const ResourceDeleteDate = connect(
686     (state: RootState, props: { uuid: string }) => {
687         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
688         return { date: resource ? resource.deleteAt : '' };
689     })((props: { date: string }) => renderDate(props.date));
690
691 export const renderFileSize = (fileSize?: number) =>
692     <Typography noWrap style={{ minWidth: '45px' }}>
693         {formatFileSize(fileSize)}
694     </Typography>;
695
696 export const ResourceFileSize = connect(
697     (state: RootState, props: { uuid: string }) => {
698         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
699
700         if (resource && resource.kind !== ResourceKind.COLLECTION) {
701             return { fileSize: '' };
702         }
703
704         return { fileSize: resource ? resource.fileSizeTotal : 0 };
705     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
706
707 const renderOwner = (owner: string) =>
708     <Typography noWrap>
709         {owner}
710     </Typography>;
711
712 export const ResourceOwner = connect(
713     (state: RootState, props: { uuid: string }) => {
714         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
715         return { owner: resource ? resource.ownerUuid : '' };
716     })((props: { owner: string }) => renderOwner(props.owner));
717
718 export const ResourceOwnerName = connect(
719     (state: RootState, props: { uuid: string }) => {
720         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
721         const ownerNameState = state.ownerName;
722         const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
723         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
724     })((props: { owner: string }) => renderOwner(props.owner));
725
726 export const ResourceUUID = connect(
727     (state: RootState, props: { uuid: string }) => {
728         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
729         console.log('COLLECTION_RESOIRCE', resource)
730         return { uuid: resource ? resource.uuid : '' };
731     })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
732
733 const renderMetadata = (metadata:any) => {
734     return <>{formatObjectProperties(metadata).map((property, i)=>
735         <Typography key={i} noWrap>{property[0]}: {property[1]}</Typography>
736     )}</>
737 }
738
739 export const ResourceMetadata = connect(
740     (state: RootState, props: { uuid: string }) => {
741         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
742         const metadata = resource && Object.keys(resource.properties).length ? resource.properties : {}
743         if(resource && resource.portableDataHash) metadata['Portable Data Hash'] = resource.portableDataHash
744         return { properties: metadata };
745     })((props: { properties: string }) => renderMetadata(props.properties));
746
747 const renderVersion = (version: number) =>{
748     return <Typography>{version ?? '-'}</Typography>
749 }
750
751 export const ResourceVersion = connect(
752     (state: RootState, props: { uuid: string }) => {
753         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
754         return { version: resource ? resource.version: '' };
755     })((props: { version: number }) => renderVersion(props.version));
756
757 const renderDescription = (description: string)=>{
758     const truncatedDescription = description ? description.slice(0, 18) + '...' : '-'
759     return <Typography title={description}>{truncatedDescription}</Typography>;
760 }
761
762 const renderFileCount = (fileCount: number) =>{
763     return <Typography>{fileCount ?? '-'}</Typography>
764 }
765
766 export const ResourceFileCount = connect(
767     (state: RootState, props: { uuid: string }) => {
768         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
769         return { fileCount: resource ? resource.fileCount: '' };
770     })((props: { fileCount: number }) => renderFileCount(props.fileCount));
771
772 export const ResourceDescription = connect(
773     (state: RootState, props: { uuid: string }) => {
774         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
775         //testing---------------
776         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."
777         if (resource && !resource.description && resource.kind === ResourceKind.PROCESS) resource.description = containerRequestDescription
778         //testing---------------
779         return { description: resource ? resource.description : '' };
780     })((props: { description: string }) => renderDescription(props.description));
781
782 const userFromID =
783     connect(
784         (state: RootState, props: { uuid: string }) => {
785             let userFullname = '';
786             const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
787
788             if (resource) {
789                 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
790             }
791
792             return { uuid: props.uuid, userFullname };
793         });
794
795 const ownerFromResourceId =
796     compose(
797         connect((state: RootState, props: { uuid: string }) => {
798             const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
799             return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
800         }),
801         userFromID
802     );
803
804 const _resourceWithName =
805     withStyles({}, { withTheme: true })
806         ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
807             const { uuid, userFullname, dispatch, theme } = props;
808
809             if (userFullname === '') {
810                 dispatch<any>(loadResource(uuid, false));
811                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
812                     {uuid}
813                 </Typography>;
814             }
815
816             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
817                 {userFullname} ({uuid})
818             </Typography>;
819         });
820
821 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
822
823 export const ResourceWithName = userFromID(_resourceWithName);
824
825 export const UserNameFromID =
826     compose(userFromID)(
827         (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
828             const { uuid, userFullname, dispatch } = props;
829
830             if (userFullname === '') {
831                 dispatch<any>(loadResource(uuid, false));
832             }
833             return <span>
834                 {userFullname ? userFullname : uuid}
835             </span>;
836         });
837
838 export const ResponsiblePerson =
839     compose(
840         connect(
841             (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
842                 let responsiblePersonName: string = '';
843                 let responsiblePersonUUID: string = '';
844                 let responsiblePersonProperty: string = '';
845
846                 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
847                     let index = 0;
848                     const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
849
850                     while (!responsiblePersonProperty && keys[index]) {
851                         const key = keys[index];
852                         if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
853                             responsiblePersonProperty = key;
854                         }
855                         index++;
856                     }
857                 }
858
859                 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
860
861                 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
862                     responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
863                     resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
864                 }
865
866                 if (resource && resource.kind === ResourceKind.USER) {
867                     responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
868                 }
869
870                 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
871             }),
872         withStyles({}, { withTheme: true }))
873         ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
874             const { uuid, responsiblePersonName, parentRef, theme } = props;
875
876             if (!uuid && parentRef) {
877                 parentRef.style.display = 'none';
878                 return null;
879             } else if (parentRef) {
880                 parentRef.style.display = 'block';
881             }
882
883             if (!responsiblePersonName) {
884                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
885                     {uuid}
886                 </Typography>;
887             }
888
889             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
890                 {responsiblePersonName} ({uuid})
891             </Typography>;
892         });
893
894 const renderType = (type: string, subtype: string) =>
895     <Typography noWrap>
896         {resourceLabel(type, subtype)}
897     </Typography>;
898
899 export const ResourceType = connect(
900     (state: RootState, props: { uuid: string }) => {
901         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
902         return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
903     })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
904
905 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
906     return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
907 })((props: { resource: GroupContentsResource }) =>
908     (props.resource && props.resource.kind === ResourceKind.COLLECTION)
909         ? <CollectionStatus uuid={props.resource.uuid} />
910         : <ProcessStatus uuid={props.resource.uuid} />
911 );
912
913 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
914     return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
915 })((props: { collection: CollectionResource }) =>
916     (props.collection.uuid !== props.collection.currentVersionUuid)
917         ? <Typography>version {props.collection.version}</Typography>
918         : <Typography>head version</Typography>
919 );
920
921 export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
922     return {
923                 collection: getResource<CollectionResource>(props.uuid)(state.resources),
924                 uuid: props.uuid,
925                 className: props.className,
926             };
927 })((props: { collection: CollectionResource, uuid: string, className?: string }) =>
928         <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
929 );
930
931 export const ProcessStatus = compose(
932     connect((state: RootState, props: { uuid: string }) => {
933         return { process: getProcess(props.uuid)(state.resources) };
934     }),
935     withStyles({}, { withTheme: true }))
936     ((props: { process?: Process, theme: ArvadosTheme }) =>
937         props.process
938             ? <Chip label={getProcessStatus(props.process)}
939                 style={{
940                     height: props.theme.spacing.unit * 3,
941                     width: props.theme.spacing.unit * 12,
942                     backgroundColor: getProcessStatusColor(
943                         getProcessStatus(props.process), props.theme),
944                     color: props.theme.palette.common.white,
945                     fontSize: '0.875rem',
946                     borderRadius: props.theme.spacing.unit * 0.625,
947                 }}
948             />
949             : <Typography>-</Typography>
950     );
951
952 export const ProcessStartDate = connect(
953     (state: RootState, props: { uuid: string }) => {
954         const process = getProcess(props.uuid)(state.resources);
955         return { date: (process && process.container) ? process.container.startedAt : '' };
956     })((props: { date: string }) => renderDate(props.date));
957
958 export const renderRunTime = (time: number) =>
959     <Typography noWrap style={{ minWidth: '45px' }}>
960         {formatTime(time, true)}
961     </Typography>;
962
963 interface ContainerRunTimeProps {
964     process: Process;
965 }
966
967 interface ContainerRunTimeState {
968     runtime: number;
969 }
970
971 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
972     return { process: getProcess(props.uuid)(state.resources) };
973 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
974     private timer: any;
975
976     constructor(props: ContainerRunTimeProps) {
977         super(props);
978         this.state = { runtime: this.getRuntime() };
979     }
980
981     getRuntime() {
982         return this.props.process ? getProcessRuntime(this.props.process) : 0;
983     }
984
985     updateRuntime() {
986         this.setState({ runtime: this.getRuntime() });
987     }
988
989     componentDidMount() {
990         this.timer = setInterval(this.updateRuntime.bind(this), 5000);
991     }
992
993     componentWillUnmount() {
994         clearInterval(this.timer);
995     }
996
997     render() {
998         return renderRunTime(this.state.runtime);
999     }
1000 });