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