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