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