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