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