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