18559: Re-use account status renderer code for group details panel
[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 { 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 enum UserAccountStatus {
215     ACTIVE = 'Active',
216     INACTIVE = 'Inactive',
217     SETUP = 'Setup',
218     UNKNOWN = ''
219 }
220
221 const renderAccountStatus = (props: {status: UserAccountStatus}) =>
222     <Grid container alignItems="center" wrap="nowrap" spacing={8}>
223         <Grid item>
224             {(() => {
225                 switch(props.status) {
226                     case UserAccountStatus.ACTIVE:
227                         return <ActiveIcon style={{color: '#4caf50'}} />;
228                     case UserAccountStatus.SETUP:
229                         return <SetupIcon style={{color: '#2196f3'}} />;
230                     case UserAccountStatus.INACTIVE:
231                         return <InactiveIcon style={{color: '#9e9e9e'}} />;
232                     default:
233                         return <></>;
234                 }
235             })()}
236         </Grid>
237         <Grid item>
238             <Typography noWrap>
239                 {props.status}
240             </Typography>
241         </Grid>
242     </Grid>;
243
244 const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
245     const user = getResource<UserResource>(props.uuid)(state.resources);
246     // Get membership links for all users group
247     const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
248     const permissions = filterResources((resource: LinkResource) =>
249         resource.kind === ResourceKind.LINK &&
250         resource.linkClass === LinkClass.PERMISSION &&
251         resource.headUuid === allUsersGroupUuid &&
252         resource.tailUuid === props.uuid
253     )(state.resources);
254
255     if (user) {
256         return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
257     } else {
258         return {status: UserAccountStatus.UNKNOWN};
259     }
260 }
261
262 export const ResourceLinkTailAccountStatus = connect(
263     (state: RootState, props: { uuid: string }) => {
264         const link = getResource<LinkResource>(props.uuid)(state.resources);
265         return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, {uuid: link.tailUuid}) : {status: UserAccountStatus.UNKNOWN};
266     })(renderAccountStatus);
267
268 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
269
270 const renderIsHidden = (props: {
271                             memberLinkUuid: string,
272                             permissionLinkUuid: string,
273                             visible: boolean,
274                             canManage: boolean,
275                             setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
276                         }) => {
277     if (props.memberLinkUuid) {
278         return <Checkbox
279                 data-cy="user-visible-checkbox"
280                 color="primary"
281                 checked={props.visible}
282                 disabled={!props.canManage}
283                 onClick={(e) => {
284                     e.stopPropagation();
285                     props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
286                 }} />;
287     } else {
288         return <Typography />;
289     }
290 }
291
292 export const ResourceLinkTailIsVisible = connect(
293     (state: RootState, props: { uuid: string }) => {
294         const link = getResource<LinkResource>(props.uuid)(state.resources);
295         const member = getResource<Resource>(link?.tailUuid || '')(state.resources);
296         const group = getResource<GroupResource>(link?.headUuid || '')(state.resources);
297         const permissions = filterResources((resource: LinkResource) => {
298             return resource.linkClass === LinkClass.PERMISSION
299                 && resource.headUuid === link?.tailUuid
300                 && resource.tailUuid === group?.uuid
301                 && resource.name === PermissionLevel.CAN_READ;
302         })(state.resources);
303
304         const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : '';
305         const isVisible = link && group && permissions.length > 0;
306         // Consider whether the current user canManage this resurce in addition when it's possible
307         const isBuiltin = isBuiltinGroup(link?.headUuid || '');
308
309         return member?.kind === ResourceKind.USER
310             ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
311             : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
312     }, { setMemberIsHidden }
313 )(renderIsHidden);
314
315 const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
316     <Checkbox
317         color="primary"
318         checked={props.isAdmin}
319         onClick={(e) => {
320             e.stopPropagation();
321             props.toggleIsAdmin(props.uuid);
322         }} />;
323
324 export const ResourceIsAdmin = connect(
325     (state: RootState, props: { uuid: string }) => {
326         const resource = getResource<UserResource>(props.uuid)(state.resources);
327         return resource || { isAdmin: false };
328     }, { toggleIsAdmin }
329 )(renderIsAdmin);
330
331 const renderUsername = (item: { username: string, uuid: string }) =>
332     <Typography noWrap>{item.username || item.uuid}</Typography>;
333
334 export const ResourceUsername = connect(
335     (state: RootState, props: { uuid: string }) => {
336         const resource = getResource<UserResource>(props.uuid)(state.resources);
337         return resource || { username: '', uuid: props.uuid };
338     })(renderUsername);
339
340 // Virtual machine resource
341
342 const renderHostname = (item: { hostname: string }) =>
343     <Typography noWrap>{item.hostname}</Typography>;
344
345 export const VirtualMachineHostname = connect(
346     (state: RootState, props: { uuid: string }) => {
347         const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
348         return resource || { hostname: '' };
349     })(renderHostname);
350
351 const renderVirtualMachineLogin = (login: {user: string}) =>
352     <Typography noWrap>{login.user}</Typography>
353
354 export const VirtualMachineLogin = connect(
355     (state: RootState, props: { linkUuid: string }) => {
356         const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
357         const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
358
359         return {user: user?.username || permission?.tailUuid || ''};
360     })(renderVirtualMachineLogin);
361
362 // Common methods
363 const renderCommonData = (data: string) =>
364     <Typography noWrap>{data}</Typography>;
365
366 const renderCommonDate = (date: string) =>
367     <Typography noWrap>{formatDate(date)}</Typography>;
368
369 export const CommonUuid = withResourceData('uuid', renderCommonData);
370
371 // Api Client Authorizations
372 export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
373
374 export const TokenApiToken = withResourceData('apiToken', renderCommonData);
375
376 export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
377
378 export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
379
380 export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
381
382 export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
383
384 export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
385
386 export const TokenScopes = withResourceData('scopes', renderCommonData);
387
388 export const TokenUserId = withResourceData('userId', renderCommonData);
389
390 const clusterColors = [
391     ['#f44336', '#fff'],
392     ['#2196f3', '#fff'],
393     ['#009688', '#fff'],
394     ['#cddc39', '#fff'],
395     ['#ff9800', '#fff']
396 ];
397
398 export const ResourceCluster = (props: { uuid: string }) => {
399     const CLUSTER_ID_LENGTH = 5;
400     const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
401     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
402     const ci = pos >= CLUSTER_ID_LENGTH ? (((((
403         (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
404         + props.uuid.charCodeAt(2))
405         * props.uuid.charCodeAt(3))
406         + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
407     return <span style={{
408         backgroundColor: clusterColors[ci][0],
409         color: clusterColors[ci][1],
410         padding: "2px 7px",
411         borderRadius: 3
412     }}>{clusterId}</span>;
413 };
414
415 // Links Resources
416 const renderLinkName = (item: { name: string }) =>
417     <Typography noWrap>{item.name || '(none)'}</Typography>;
418
419 export const ResourceLinkName = connect(
420     (state: RootState, props: { uuid: string }) => {
421         const resource = getResource<LinkResource>(props.uuid)(state.resources);
422         return resource || { name: '' };
423     })(renderLinkName);
424
425 const renderLinkClass = (item: { linkClass: string }) =>
426     <Typography noWrap>{item.linkClass}</Typography>;
427
428 export const ResourceLinkClass = connect(
429     (state: RootState, props: { uuid: string }) => {
430         const resource = getResource<LinkResource>(props.uuid)(state.resources);
431         return resource || { linkClass: '' };
432     })(renderLinkClass);
433
434 const getResourceDisplayName = (resource: Resource): string => {
435     if ((resource as UserResource).kind === ResourceKind.USER
436           && typeof (resource as UserResource).firstName !== 'undefined') {
437         // We can be sure the resource is UserResource
438         return getUserDisplayName(resource as UserResource);
439     } else {
440         return (resource as GroupContentsResource).name;
441     }
442 }
443
444 const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
445     var displayName = getResourceDisplayName(item);
446
447     return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
448         {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
449     </Typography>;
450 };
451
452 export const ResourceLinkTail = connect(
453     (state: RootState, props: { uuid: string }) => {
454         const resource = getResource<LinkResource>(props.uuid)(state.resources);
455         const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
456
457         return {
458             item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE }
459         };
460     })((props: { item: Resource } & DispatchProp<any>) =>
461         renderResourceLink(props.dispatch, props.item));
462
463 export const ResourceLinkHead = connect(
464     (state: RootState, props: { uuid: string }) => {
465         const resource = getResource<LinkResource>(props.uuid)(state.resources);
466         const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
467
468         return {
469             item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE }
470         };
471     })((props: { item: Resource } & DispatchProp<any>) =>
472         renderResourceLink(props.dispatch, props.item));
473
474 export const ResourceLinkUuid = connect(
475     (state: RootState, props: { uuid: string }) => {
476         const resource = getResource<LinkResource>(props.uuid)(state.resources);
477         return resource || { uuid: '' };
478     })(renderUuid);
479
480 export const ResourceLinkHeadUuid = connect(
481     (state: RootState, props: { uuid: string }) => {
482         const link = getResource<LinkResource>(props.uuid)(state.resources);
483         const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
484
485         return headResource || { uuid: '' };
486     })(renderUuid);
487
488 export const ResourceLinkTailUuid = connect(
489     (state: RootState, props: { uuid: string }) => {
490         const link = getResource<LinkResource>(props.uuid)(state.resources);
491         const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
492
493         return tailResource || { uuid: '' };
494     })(renderUuid);
495
496 const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
497     if (item.uuid) {
498         return canManage ?
499             <Typography noWrap>
500                 <IconButton data-cy="resource-delete-button" onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
501                     <RemoveIcon />
502                 </IconButton>
503             </Typography> :
504             <Typography noWrap>
505                 <IconButton disabled data-cy="resource-delete-button">
506                     <RemoveIcon />
507                 </IconButton>
508             </Typography>;
509     } else {
510       return <Typography noWrap></Typography>;
511     }
512 }
513
514 export const ResourceLinkDelete = connect(
515     (state: RootState, props: { uuid: string }) => {
516         const link = getResource<LinkResource>(props.uuid)(state.resources);
517         const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
518
519         return {
520             item: link || { uuid: '', kind: ResourceKind.NONE },
521             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
522         };
523     })((props: { item: LinkResource, canManage: boolean } & DispatchProp<any>) =>
524       renderLinkDelete(props.dispatch, props.item, props.canManage));
525
526 export const ResourceLinkTailEmail = connect(
527     (state: RootState, props: { uuid: string }) => {
528         const link = getResource<LinkResource>(props.uuid)(state.resources);
529         const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
530
531         return resource || { email: '' };
532     })(renderEmail);
533
534 export const ResourceLinkTailUsername = connect(
535     (state: RootState, props: { uuid: string }) => {
536         const link = getResource<LinkResource>(props.uuid)(state.resources);
537         const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
538
539         return resource || { username: '' };
540     })(renderUsername);
541
542 const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
543     return <Typography noWrap>
544         {formatPermissionLevel(link.name as PermissionLevel)}
545         {canManage ?
546             <IconButton data-cy="edit-permission-button" onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
547                 <RenameIcon />
548             </IconButton> :
549             ''
550         }
551     </Typography>;
552 }
553
554 export const ResourceLinkHeadPermissionLevel = connect(
555     (state: RootState, props: { uuid: string }) => {
556         const link = getResource<LinkResource>(props.uuid)(state.resources);
557         const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
558
559         return {
560             link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
561             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
562         };
563     })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
564         renderPermissionLevel(props.dispatch, props.link, props.canManage));
565
566 export const ResourceLinkTailPermissionLevel = connect(
567     (state: RootState, props: { uuid: string }) => {
568         const link = getResource<LinkResource>(props.uuid)(state.resources);
569         const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
570
571         return {
572             link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
573             canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
574         };
575     })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
576         renderPermissionLevel(props.dispatch, props.link, props.canManage));
577
578 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
579     const headResource = getResource<Resource>(link.headUuid)(state.resources);
580     // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
581     const userUuid = getUserUuid(state);
582
583     if (headResource && headResource.kind === ResourceKind.GROUP) {
584         return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
585     } else {
586         // true for now
587         return true;
588     }
589 }
590
591 // Process Resources
592 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
593     return (
594         <div>
595             {uuid &&
596                 <Tooltip title="Run process">
597                     <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
598                         <ProcessIcon />
599                     </IconButton>
600                 </Tooltip>}
601         </div>
602     );
603 };
604
605 export const ResourceRunProcess = connect(
606     (state: RootState, props: { uuid: string }) => {
607         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
608         return {
609             uuid: resource ? resource.uuid : ''
610         };
611     })((props: { uuid: string } & DispatchProp<any>) =>
612         resourceRunProcess(props.dispatch, props.uuid));
613
614 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
615     if (ownerUuid === getPublicUuid(uuidPrefix)) {
616         return renderStatus(WorkflowStatus.PUBLIC);
617     } else {
618         return renderStatus(WorkflowStatus.PRIVATE);
619     }
620 };
621
622 const renderStatus = (status: string) =>
623     <Typography noWrap style={{ width: '60px' }}>{status}</Typography>;
624
625 export const ResourceWorkflowStatus = connect(
626     (state: RootState, props: { uuid: string }) => {
627         const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
628         const uuidPrefix = getUuidPrefix(state);
629         return {
630             ownerUuid: resource ? resource.ownerUuid : '',
631             uuidPrefix
632         };
633     })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
634
635 export const ResourceLastModifiedDate = connect(
636     (state: RootState, props: { uuid: string }) => {
637         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
638         return { date: resource ? resource.modifiedAt : '' };
639     })((props: { date: string }) => renderDate(props.date));
640
641 export const ResourceCreatedAtDate = connect(
642     (state: RootState, props: { uuid: string }) => {
643         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
644         return { date: resource ? resource.createdAt : '' };
645     })((props: { date: string }) => renderDate(props.date));
646
647 export const ResourceTrashDate = connect(
648     (state: RootState, props: { uuid: string }) => {
649         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
650         return { date: resource ? resource.trashAt : '' };
651     })((props: { date: string }) => renderDate(props.date));
652
653 export const ResourceDeleteDate = connect(
654     (state: RootState, props: { uuid: string }) => {
655         const resource = getResource<TrashableResource>(props.uuid)(state.resources);
656         return { date: resource ? resource.deleteAt : '' };
657     })((props: { date: string }) => renderDate(props.date));
658
659 export const renderFileSize = (fileSize?: number) =>
660     <Typography noWrap style={{ minWidth: '45px' }}>
661         {formatFileSize(fileSize)}
662     </Typography>;
663
664 export const ResourceFileSize = connect(
665     (state: RootState, props: { uuid: string }) => {
666         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
667
668         if (resource && resource.kind !== ResourceKind.COLLECTION) {
669             return { fileSize: '' };
670         }
671
672         return { fileSize: resource ? resource.fileSizeTotal : 0 };
673     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
674
675 const renderOwner = (owner: string) =>
676     <Typography noWrap>
677         {owner}
678     </Typography>;
679
680 export const ResourceOwner = connect(
681     (state: RootState, props: { uuid: string }) => {
682         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
683         return { owner: resource ? resource.ownerUuid : '' };
684     })((props: { owner: string }) => renderOwner(props.owner));
685
686 export const ResourceOwnerName = connect(
687     (state: RootState, props: { uuid: string }) => {
688         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
689         const ownerNameState = state.ownerName;
690         const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
691         return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
692     })((props: { owner: string }) => renderOwner(props.owner));
693
694 const userFromID =
695     connect(
696         (state: RootState, props: { uuid: string }) => {
697             let userFullname = '';
698             const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
699
700             if (resource) {
701                 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
702             }
703
704             return { uuid: props.uuid, userFullname };
705         });
706
707 export const ResourceOwnerWithName =
708     compose(
709         userFromID,
710         withStyles({}, { withTheme: true }))
711         ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
712             const { uuid, userFullname, dispatch, theme } = props;
713
714             if (userFullname === '') {
715                 dispatch<any>(loadResource(uuid, false));
716                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
717                     {uuid}
718                 </Typography>;
719             }
720
721             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
722                 {userFullname} ({uuid})
723             </Typography>;
724         });
725
726 export const UserNameFromID =
727     compose(userFromID)(
728         (props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
729             const { uuid, userFullname, dispatch } = props;
730
731             if (userFullname === '') {
732                 dispatch<any>(loadResource(uuid, false));
733             }
734             return <span>
735                 {userFullname ? userFullname : uuid}
736             </span>;
737         });
738
739 export const ResponsiblePerson =
740     compose(
741         connect(
742             (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
743                 let responsiblePersonName: string = '';
744                 let responsiblePersonUUID: string = '';
745                 let responsiblePersonProperty: string = '';
746
747                 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
748                     let index = 0;
749                     const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
750
751                     while (!responsiblePersonProperty && keys[index]) {
752                         const key = keys[index];
753                         if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
754                             responsiblePersonProperty = key;
755                         }
756                         index++;
757                     }
758                 }
759
760                 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
761
762                 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
763                     responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
764                     resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
765                 }
766
767                 if (resource && resource.kind === ResourceKind.USER) {
768                     responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
769                 }
770
771                 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
772             }),
773         withStyles({}, { withTheme: true }))
774         ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
775             const { uuid, responsiblePersonName, parentRef, theme } = props;
776
777             if (!uuid && parentRef) {
778                 parentRef.style.display = 'none';
779                 return null;
780             } else if (parentRef) {
781                 parentRef.style.display = 'block';
782             }
783
784             if (!responsiblePersonName) {
785                 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
786                     {uuid}
787                 </Typography>;
788             }
789
790             return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
791                 {responsiblePersonName} ({uuid})
792                 </Typography>;
793         });
794
795 const renderType = (type: string, subtype: string) =>
796     <Typography noWrap>
797         {resourceLabel(type, subtype)}
798     </Typography>;
799
800 export const ResourceType = connect(
801     (state: RootState, props: { uuid: string }) => {
802         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
803         return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
804     })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
805
806 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
807     return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
808 })((props: { resource: GroupContentsResource }) =>
809     (props.resource && props.resource.kind === ResourceKind.COLLECTION)
810         ? <CollectionStatus uuid={props.resource.uuid} />
811         : <ProcessStatus uuid={props.resource.uuid} />
812 );
813
814 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
815     return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
816 })((props: { collection: CollectionResource }) =>
817     (props.collection.uuid !== props.collection.currentVersionUuid)
818         ? <Typography>version {props.collection.version}</Typography>
819         : <Typography>head version</Typography>
820 );
821
822 export const ProcessStatus = compose(
823     connect((state: RootState, props: { uuid: string }) => {
824         return { process: getProcess(props.uuid)(state.resources) };
825     }),
826     withStyles({}, { withTheme: true }))
827     ((props: { process?: Process, theme: ArvadosTheme }) => {
828         const status = props.process ? getProcessStatus(props.process) : "-";
829         return <Typography
830             noWrap
831             style={{ color: getProcessStatusColor(status, props.theme) }} >
832             {status}
833         </Typography>;
834     });
835
836 export const ProcessStartDate = connect(
837     (state: RootState, props: { uuid: string }) => {
838         const process = getProcess(props.uuid)(state.resources);
839         return { date: (process && process.container) ? process.container.startedAt : '' };
840     })((props: { date: string }) => renderDate(props.date));
841
842 export const renderRunTime = (time: number) =>
843     <Typography noWrap style={{ minWidth: '45px' }}>
844         {formatTime(time, true)}
845     </Typography>;
846
847 interface ContainerRunTimeProps {
848     process: Process;
849 }
850
851 interface ContainerRunTimeState {
852     runtime: number;
853 }
854
855 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
856     return { process: getProcess(props.uuid)(state.resources) };
857 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
858     private timer: any;
859
860     constructor(props: ContainerRunTimeProps) {
861         super(props);
862         this.state = { runtime: this.getRuntime() };
863     }
864
865     getRuntime() {
866         return this.props.process ? getProcessRuntime(this.props.process) : 0;
867     }
868
869     updateRuntime() {
870         this.setState({ runtime: this.getRuntime() });
871     }
872
873     componentDidMount() {
874         this.timer = setInterval(this.updateRuntime.bind(this), 5000);
875     }
876
877     componentWillUnmount() {
878         clearInterval(this.timer);
879     }
880
881     render() {
882         return renderRunTime(this.state.runtime);
883     }
884 });