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