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