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