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