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