process parent uuid column up, minor fixes Arvados-DCO-1.1-Signed-off-by: Lisa Knox...
[arvados.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 } 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         {(item.uuid && <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 const renderProcessState = (processState: string) => <Typography>{processState || '-'}</Typography>
668
669 export const ResourceProcessState = connect(
670     (state: RootState, props: { uuid: string }) => {
671         const process = getProcess(props.uuid)(state.resources)
672         return { state: process?.container?.state ? process?.container?.state : '' };
673     })((props: { state: string }) => renderProcessState(props.state));
674
675 export const ResourceParentProcess = connect(
676     (state: RootState, props: { uuid: string }) => {
677         const process = getProcess(props.uuid)(state.resources)
678         const parentProcessUuid = process?.containerRequest?.requestingContainerUuid
679         return { parentProcess: parentProcessUuid || '' };
680     })((props: { parentProcess: string }) => renderUuid({uuid: props.parentProcess}));
681
682 export const ResourceCreatedAtDate = connect(
683     (state: RootState, props: { uuid: string }) => {
684         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
685         return { date: resource ? resource.createdAt : '' };
686     })((props: { date: string }) => renderDate(props.date));
687     
688 export const ResourceLastModifiedDate = connect(
689     (state: RootState, props: { uuid: string }) => {
690         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
691         return { date: resource ? resource.modifiedAt : '' };
692     })((props: { date: string }) => renderDate(props.date));
693
694 export const ResourceTrashDate = connect(
695     (state: RootState, props: { uuid: string }) => {
696         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
697         return { date: resource ? resource.trashAt : '' };
698     })((props: { date: string }) => renderDate(props.date));
699
700 export const ResourceDeleteDate = connect(
701     (state: RootState, props: { uuid: string }) => {
702         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
703         return { date: resource ? resource.deleteAt : '' };
704     })((props: { date: string }) => renderDate(props.date));
705
706 export const renderFileSize = (fileSize?: number) =>
707     <Typography noWrap style={{ minWidth: '45px' }}>
708         {formatFileSize(fileSize)}
709     </Typography>;
710
711 export const ResourceFileSize = connect(
712     (state: RootState, props: { uuid: string }) => {
713         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
714
715         if (resource && resource.kind !== ResourceKind.COLLECTION) {
716             return { fileSize: '' };
717         }
718
719         return { fileSize: resource ? resource.fileSizeTotal : 0 };
720     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
721
722 const renderOwner = (owner: string) =>
723     <Typography noWrap>
724         {owner || '-'}
725     </Typography>;
726
727 export const ResourceOwner = connect(
728     (state: RootState, props: { uuid: string }) => {
729         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
730         return { owner: resource ? resource.ownerUuid : '' };
731     })((props: { owner: string }) => renderOwner(props.owner));
732
733 export const ResourceOwnerName = connect(
734     (state: RootState, props: { uuid: string }) => {
735         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
736         const ownerNameState = state.ownerName;
737         const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
738         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
739     })((props: { owner: string }) => renderOwner(props.owner));
740
741 export const ResourceUUID = connect(
742     (state: RootState, props: { uuid: string }) => {
743         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
744         return { uuid: resource ? resource.uuid : '' };
745     })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
746     
747 const renderPortableDataHash = (portableDataHash:string | null) => 
748     <Typography noWrap>
749         {portableDataHash ? <>{portableDataHash}
750         <CopyToClipboardSnackbar value={portableDataHash} /></> : '-' }
751     </Typography>
752     
753 export const ResourcePortableDataHash = connect(
754     (state: RootState, props: { uuid: string }) => {
755         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
756         return { portableDataHash: resource ? resource.portableDataHash : '' };    
757     })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
758
759 const renderVersion = (version: number) =>{
760     return <Typography>{version ?? '-'}</Typography>
761 }
762
763 export const ResourceVersion = connect(
764     (state: RootState, props: { uuid: string }) => {
765         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
766         return { version: resource ? resource.version: '' };
767     })((props: { version: number }) => renderVersion(props.version));
768
769 const renderDescription = (description: string)=>{
770     const truncatedDescription = description ? description.slice(0, 18) + '...' : '-'
771     return <Typography title={description}>{truncatedDescription}</Typography>;
772 }
773
774 const renderFileCount = (fileCount: number) =>{
775     return <Typography>{fileCount ?? '-'}</Typography>
776 }
777
778 export const ResourceFileCount = connect(
779     (state: RootState, props: { uuid: string }) => {
780         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
781         return { fileCount: resource ? resource.fileCount: '' };
782     })((props: { fileCount: number }) => renderFileCount(props.fileCount));
783
784 export const ResourceDescription = connect(
785     (state: RootState, props: { uuid: string }) => {
786         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
787         //testing---------------
788         // 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."
789         // if (resource && !resource.description && resource.kind === ResourceKind.PROCESS) resource.description = containerRequestDescription
790         //testing---------------
791         return { description: resource ? resource.description : '' };
792     })((props: { description: string }) => renderDescription(props.description));
793
794 const userFromID =
795     connect(
796         (state: RootState, props: { uuid: string }) => {
797             let userFullname = '';
798             const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
799
800             if (resource) {
801                 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
802             }
803
804             return { uuid: props.uuid, userFullname };
805         });
806
807 const ownerFromResourceId =
808     compose(
809         connect((state: RootState, props: { uuid: string }) => {
810             const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
811             return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
812         }),
813         userFromID
814     );
815
816 const _resourceWithName =
817     withStyles({}, { withTheme: true })
818         ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
819             const { uuid, userFullname, dispatch, theme } = props;
820
821             if (userFullname === '') {
822                 dispatch<any>(loadResource(uuid, false));
823                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
824                     {uuid}
825                 </Typography>;
826             }
827
828             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
829                 {userFullname} ({uuid})
830             </Typography>;
831         });
832
833 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
834
835 export const ResourceWithName = userFromID(_resourceWithName);
836
837 export const UserNameFromID =
838     compose(userFromID)(
839         (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
840             const { uuid, userFullname, dispatch } = props;
841
842             if (userFullname === '') {
843                 dispatch<any>(loadResource(uuid, false));
844             }
845             return <span>
846                 {userFullname ? userFullname : uuid}
847             </span>;
848         });
849
850 export const ResponsiblePerson =
851     compose(
852         connect(
853             (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
854                 let responsiblePersonName: string = '';
855                 let responsiblePersonUUID: string = '';
856                 let responsiblePersonProperty: string = '';
857
858                 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
859                     let index = 0;
860                     const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
861
862                     while (!responsiblePersonProperty && keys[index]) {
863                         const key = keys[index];
864                         if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
865                             responsiblePersonProperty = key;
866                         }
867                         index++;
868                     }
869                 }
870
871                 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
872
873                 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
874                     responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
875                     resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
876                 }
877
878                 if (resource && resource.kind === ResourceKind.USER) {
879                     responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
880                 }
881
882                 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
883             }),
884         withStyles({}, { withTheme: true }))
885         ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
886             const { uuid, responsiblePersonName, parentRef, theme } = props;
887
888             if (!uuid && parentRef) {
889                 parentRef.style.display = 'none';
890                 return null;
891             } else if (parentRef) {
892                 parentRef.style.display = 'block';
893             }
894
895             if (!responsiblePersonName) {
896                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
897                     {uuid}
898                 </Typography>;
899             }
900
901             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
902                 {responsiblePersonName} ({uuid})
903             </Typography>;
904         });
905
906 const renderType = (type: string, subtype: string) =>
907     <Typography noWrap>
908         {resourceLabel(type, subtype)}
909     </Typography>;
910
911 export const ResourceType = connect(
912     (state: RootState, props: { uuid: string }) => {
913         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
914         return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
915     })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
916
917 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
918     return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
919 })((props: { resource: GroupContentsResource }) =>
920     (props.resource && props.resource.kind === ResourceKind.COLLECTION)
921         ? <CollectionStatus uuid={props.resource.uuid} />
922         : <ProcessStatus uuid={props.resource.uuid} />
923 );
924
925 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
926     return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
927 })((props: { collection: CollectionResource }) =>
928     (props.collection.uuid !== props.collection.currentVersionUuid)
929         ? <Typography>version {props.collection.version}</Typography>
930         : <Typography>head version</Typography>
931 );
932
933 export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
934     return {
935                 collection: getResource<CollectionResource>(props.uuid)(state.resources),
936                 uuid: props.uuid,
937                 className: props.className,
938             };
939 })((props: { collection: CollectionResource, uuid: string, className?: string }) =>
940         <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
941 );
942
943 export const ProcessStatus = compose(
944     connect((state: RootState, props: { uuid: string }) => {
945         return { process: getProcess(props.uuid)(state.resources) };
946     }),
947     withStyles({}, { withTheme: true }))
948     ((props: { process?: Process, theme: ArvadosTheme }) =>
949         props.process
950             ? <Chip label={getProcessStatus(props.process)}
951                 style={{
952                     height: props.theme.spacing.unit * 3,
953                     width: props.theme.spacing.unit * 12,
954                     backgroundColor: getProcessStatusColor(
955                         getProcessStatus(props.process), props.theme),
956                     color: props.theme.palette.common.white,
957                     fontSize: '0.875rem',
958                     borderRadius: props.theme.spacing.unit * 0.625,
959                 }}
960             />
961             : <Typography>-</Typography>
962     );
963
964 export const ProcessStartDate = connect(
965     (state: RootState, props: { uuid: string }) => {
966         const process = getProcess(props.uuid)(state.resources);
967         return { date: (process && process.container) ? process.container.startedAt : '' };
968     })((props: { date: string }) => renderDate(props.date));
969
970 export const renderRunTime = (time: number) =>
971     <Typography noWrap style={{ minWidth: '45px' }}>
972         {formatTime(time, true)}
973     </Typography>;
974
975 interface ContainerRunTimeProps {
976     process: Process;
977 }
978
979 interface ContainerRunTimeState {
980     runtime: number;
981 }
982
983 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
984     return { process: getProcess(props.uuid)(state.resources) };
985 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
986     private timer: any;
987
988     constructor(props: ContainerRunTimeProps) {
989         super(props);
990         this.state = { runtime: this.getRuntime() };
991     }
992
993     getRuntime() {
994         return this.props.process ? getProcessRuntime(this.props.process) : 0;
995     }
996
997     updateRuntime() {
998         this.setState({ runtime: this.getRuntime() });
999     }
1000
1001     componentDidMount() {
1002         this.timer = setInterval(this.updateRuntime.bind(this), 5000);
1003     }
1004
1005     componentWillUnmount() {
1006         clearInterval(this.timer);
1007     }
1008
1009     render() {
1010         return renderRunTime(this.state.runtime);
1011     }
1012 });