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