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