1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
6 import React from 'react';
7 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from '@material-ui/core';
8 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
9 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
11 import React from "react";
12 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from "@material-ui/core";
13 import { FavoriteStar, PublicFavoriteStar } from "../favorite-star/favorite-star";
14 import { Resource, ResourceKind, TrashableResource } from "models/resource";
24 CollectionOldVersionIcon,
32 } from 'components/icon/icon';
33 import { formatDate, formatFileSize, formatTime } from 'common/formatters';
34 import { resourceLabel } from 'common/labels';
35 import { connect, DispatchProp } from 'react-redux';
36 import { RootState } from 'store/store';
37 import { getResource, filterResources } from 'store/resources/resources';
38 import { GroupContentsResource } from 'services/groups-service/groups-service';
39 import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from 'store/processes/process';
40 import { ArvadosTheme } from 'common/custom-theme';
41 import { compose, Dispatch } from 'redux';
42 import { WorkflowResource } from 'models/workflow';
43 import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-panel-view';
44 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
45 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
46 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
47 import { toggleIsAdmin } from 'store/users/users-actions';
48 import { LinkClass, LinkResource } from 'models/link';
49 import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
50 import { withResourceData } from 'views-components/data-explorer/with-resources';
51 import { CollectionResource } from 'models/collection';
52 import { IllegalNamingWarning } from 'components/warning/warning';
53 import { loadResource } from 'store/resources/resources-actions';
54 import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
55 import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
56 import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
57 import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
58 import { PermissionLevel } from 'models/permission';
59 import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
60 import { getUserUuid } from 'common/getuser';
61 import { VirtualMachinesResource } from 'models/virtual-machines';
62 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
63 import { ProjectResource } from 'models/project';
64 import { ProcessResource } from 'models/process';
66 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
67 const navFunc = 'groupClass' in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
69 <Grid container alignItems='center' wrap='nowrap' spacing={16}>
70 <Grid item>{renderIcon(item)}</Grid>
72 <Typography color='primary' style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
74 } from "components/icon/icon";
75 import { formatDate, formatFileSize, formatTime } from "common/formatters";
76 import { resourceLabel } from "common/labels";
77 import { connect, DispatchProp } from "react-redux";
78 import { RootState } from "store/store";
79 import { getResource, filterResources } from "store/resources/resources";
80 import { GroupContentsResource } from "services/groups-service/groups-service";
81 import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from "store/processes/process";
82 import { ArvadosTheme } from "common/custom-theme";
83 import { compose, Dispatch } from "redux";
84 import { WorkflowResource } from "models/workflow";
85 import { ResourceStatus as WorkflowStatus } from "views/workflow-panel/workflow-panel-view";
86 import { getUuidPrefix, openRunProcess } from "store/workflow-panel/workflow-panel-actions";
87 import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
88 import { getUserFullname, getUserDisplayName, User, UserResource } from "models/user";
89 import { toggleIsAdmin } from "store/users/users-actions";
90 import { LinkClass, LinkResource } from "models/link";
91 import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from "store/navigation/navigation-action";
92 import { withResourceData } from "views-components/data-explorer/with-resources";
93 import { CollectionResource } from "models/collection";
94 import { IllegalNamingWarning } from "components/warning/warning";
95 import { loadResource } from "store/resources/resources-actions";
96 import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from "models/group";
97 import { openRemoveGroupMemberDialog } from "store/group-details-panel/group-details-panel-actions";
98 import { setMemberIsHidden } from "store/group-details-panel/group-details-panel-actions";
99 import { formatPermissionLevel } from "views-components/sharing-dialog/permission-select";
100 import { PermissionLevel } from "models/permission";
101 import { openPermissionEditContextMenu } from "store/context-menu/context-menu-actions";
102 import { VirtualMachinesResource } from "models/virtual-machines";
103 import { CopyToClipboardSnackbar } from "components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar";
104 import { ProjectResource } from "models/project";
105 import { ProcessResource } from "models/process";
107 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
108 const navFunc = "groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
116 <Grid item>{renderIcon(item)}</Grid>
120 style={{ width: "auto", cursor: "pointer" }}
121 onClick={() => dispatch<any>(navFunc(item.uuid))}
124 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? <IllegalNamingWarning name={item.name} /> : null}
130 <Typography variant='caption'>
132 <Typography variant="caption">
134 <FavoriteStar resourceUuid={item.uuid} />
135 <PublicFavoriteStar resourceUuid={item.uuid} />
136 {item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />}
143 const FrozenProject = (props: { item: ProjectResource }) => {
144 const [fullUsername, setFullusername] = React.useState<any>(null);
145 const getFullName = React.useCallback(() => {
146 if (props.item.frozenByUuid) {
147 setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
149 }, [props.item, setFullusername]);
151 if (props.item.frozenByUuid) {
154 <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
155 <FreezeIcon style={{ fontSize: 'inherit' }} />
160 title={<span>Project was frozen by {fullUsername}</span>}
162 <FreezeIcon style={{ fontSize: "inherit" }} />
171 export const ResourceName = connect((state: RootState, props: { uuid: string }) => {
172 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
174 })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
176 const renderIcon = (item: GroupContentsResource) => {
178 case ResourceKind.PROJECT:
179 if (item.groupClass === GroupClass.FILTER) {
180 return <FilterGroupIcon />;
182 return <ProjectIcon />;
183 case ResourceKind.COLLECTION:
184 if (item.uuid === item.currentVersionUuid) {
185 return <CollectionIcon />;
187 return <CollectionOldVersionIcon />;
188 case ResourceKind.PROCESS:
189 return <ProcessIcon />;
190 case ResourceKind.WORKFLOW:
191 return <WorkflowIcon />;
193 return <DefaultIcon />;
197 const renderDate = (date?: string) => {
200 <Typography noWrap style={{ minWidth: '100px' }}>
204 style={{ minWidth: "100px" }}
212 const renderWorkflowName = (item: WorkflowResource) => (
214 <Grid container alignItems='center' wrap='nowrap' spacing={16}>
215 <Grid item>{renderIcon(item)}</Grid>
217 <Typography color='primary' style={{ width: '100px' }}>
225 <Grid item>{renderIcon(item)}</Grid>
229 style={{ width: "100px" }}
238 export const ResourceWorkflowName = connect((state: RootState, props: { uuid: string }) => {
239 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
241 })(renderWorkflowName);
243 const getPublicUuid = (uuidPrefix: string) => {
244 return `${uuidPrefix}-tpzed-anonymouspublic`;
247 const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
248 const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
251 {!isPublic && uuid && (
253 <Tooltip title='Share'>
255 <Tooltip title="Share">
257 <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
266 export const ResourceShare = connect((state: RootState, props: { uuid: string }) => {
267 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
268 const uuidPrefix = getUuidPrefix(state);
271 uuid: resource ? resource.uuid : '',
272 ownerUuid: resource ? resource.ownerUuid : '',
274 uuid: resource ? resource.uuid : "",
275 ownerUuid: resource ? resource.ownerUuid : "",
279 })((props: { ownerUuid?: string; uuidPrefix: string; uuid?: string } & DispatchProp<any>) =>
280 resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)
284 const renderFirstName = (item: { firstName: string }) => {
285 return <Typography noWrap>{item.firstName}</Typography>;
288 export const ResourceFirstName = connect((state: RootState, props: { uuid: string }) => {
289 const resource = getResource<UserResource>(props.uuid)(state.resources);
291 return resource || { firstName: '' };
293 return resource || { firstName: "" };
297 const renderLastName = (item: { lastName: string }) => <Typography noWrap>{item.lastName}</Typography>;
299 export const ResourceLastName = connect((state: RootState, props: { uuid: string }) => {
300 const resource = getResource<UserResource>(props.uuid)(state.resources);
302 return resource || { lastName: '' };
305 const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
306 const displayName = (item.firstName + ' ' + item.lastName).trim() || item.uuid;
308 <Typography noWrap color='primary' style={{ cursor: 'pointer' }} onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
310 return resource || { lastName: "" };
313 const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
314 const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
319 style={{ cursor: "pointer" }}
320 onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}
326 <Typography noWrap>{displayName}</Typography>
330 export const UserResourceFullName = connect((state: RootState, props: { uuid: string; link?: boolean }) => {
331 const resource = getResource<UserResource>(props.uuid)(state.resources);
333 return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
335 return { item: resource || { uuid: "", firstName: "", lastName: "" }, link: props.link };
337 })((props: { item: { uuid: string; firstName: string; lastName: string }; link?: boolean } & DispatchProp<any>) =>
338 renderFullName(props.dispatch, props.item, props.link)
341 const renderUuid = (item: { uuid: string }) => (
343 <Typography data-cy='uuid' noWrap>
345 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
352 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
357 const renderUuidCopyIcon = (item: { uuid: string }) => (
359 <Typography data-cy='uuid' noWrap>
360 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
364 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' })(renderUuid);
370 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
374 export const ResourceUuid = connect(
375 (state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: "" }
379 const renderEmail = (item: { email: string }) => <Typography noWrap>{item.email}</Typography>;
381 export const ResourceEmail = connect((state: RootState, props: { uuid: string }) => {
382 const resource = getResource<UserResource>(props.uuid)(state.resources);
384 return resource || { email: '' };
387 enum UserAccountStatus {
389 INACTIVE = 'Inactive',
394 const renderAccountStatus = (props: { status: UserAccountStatus }) => (
395 <Grid container alignItems='center' wrap='nowrap' spacing={8} data-cy='account-status'>
397 return resource || { email: "" };
400 enum UserAccountStatus {
402 INACTIVE = "Inactive",
407 const renderAccountStatus = (props: { status: UserAccountStatus }) => (
413 data-cy="account-status"
418 switch (props.status) {
419 case UserAccountStatus.ACTIVE:
421 return <ActiveIcon style={{ color: '#4caf50', verticalAlign: 'middle' }} />;
422 case UserAccountStatus.SETUP:
423 return <SetupIcon style={{ color: '#2196f3', verticalAlign: 'middle' }} />;
424 case UserAccountStatus.INACTIVE:
425 return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: 'middle' }} />;
427 return <ActiveIcon style={{ color: "#4caf50", verticalAlign: "middle" }} />;
428 case UserAccountStatus.SETUP:
429 return <SetupIcon style={{ color: "#2196f3", verticalAlign: "middle" }} />;
430 case UserAccountStatus.INACTIVE:
431 return <InactiveIcon style={{ color: "#9e9e9e", verticalAlign: "middle" }} />;
439 <Typography noWrap>{props.status}</Typography>
444 const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
445 const user = getResource<UserResource>(props.uuid)(state.resources);
446 // Get membership links for all users group
447 const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
448 const permissions = filterResources(
449 (resource: LinkResource) =>
450 resource.kind === ResourceKind.LINK &&
451 resource.linkClass === LinkClass.PERMISSION &&
452 resource.headUuid === allUsersGroupUuid &&
453 resource.tailUuid === props.uuid
458 ? { status: UserAccountStatus.ACTIVE }
459 : permissions.length > 0
460 ? { status: UserAccountStatus.SETUP }
461 : { status: UserAccountStatus.INACTIVE };
463 return { status: UserAccountStatus.UNKNOWN };
467 export const ResourceLinkTailAccountStatus = connect((state: RootState, props: { uuid: string }) => {
468 const link = getResource<LinkResource>(props.uuid)(state.resources);
469 return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
470 })(renderAccountStatus);
472 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
474 const renderIsHidden = (props: {
475 memberLinkUuid: string;
476 permissionLinkUuid: string;
479 setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void;
481 if (props.memberLinkUuid) {
485 data-cy='user-visible-checkbox'
487 checked={props.visible}
488 disabled={!props.canManage}
491 data-cy="user-visible-checkbox"
493 checked={props.visible}
494 disabled={!props.canManage}
498 props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
503 return <Typography />;
507 export const ResourceLinkTailIsVisible = connect(
508 (state: RootState, props: { uuid: string }) => {
509 const link = getResource<LinkResource>(props.uuid)(state.resources);
510 const member = getResource<Resource>(link?.tailUuid || "")(state.resources);
511 const group = getResource<GroupResource>(link?.headUuid || "")(state.resources);
512 const permissions = filterResources((resource: LinkResource) => {
514 resource.linkClass === LinkClass.PERMISSION &&
515 resource.headUuid === link?.tailUuid &&
516 resource.tailUuid === group?.uuid &&
517 resource.name === PermissionLevel.CAN_READ
521 const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : "";
522 const isVisible = link && group && permissions.length > 0;
523 // Consider whether the current user canManage this resurce in addition when it's possible
524 const isBuiltin = isBuiltinGroup(link?.headUuid || "");
526 return member?.kind === ResourceKind.USER
527 ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
529 : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
531 : { memberLinkUuid: "", permissionLinkUuid: "", visible: false, canManage: false };
534 { setMemberIsHidden }
537 const renderIsAdmin = (props: { uuid: string; isAdmin: boolean; toggleIsAdmin: (uuid: string) => void }) => (
540 checked={props.isAdmin}
543 props.toggleIsAdmin(props.uuid);
548 export const ResourceIsAdmin = connect(
549 (state: RootState, props: { uuid: string }) => {
550 const resource = getResource<UserResource>(props.uuid)(state.resources);
551 return resource || { isAdmin: false };
556 const renderUsername = (item: { username: string; uuid: string }) => <Typography noWrap>{item.username || item.uuid}</Typography>;
558 export const ResourceUsername = connect((state: RootState, props: { uuid: string }) => {
559 const resource = getResource<UserResource>(props.uuid)(state.resources);
561 return resource || { username: '', uuid: props.uuid };
563 return resource || { username: "", uuid: props.uuid };
567 // Virtual machine resource
569 const renderHostname = (item: { hostname: string }) => <Typography noWrap>{item.hostname}</Typography>;
571 export const VirtualMachineHostname = connect((state: RootState, props: { uuid: string }) => {
572 const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
574 return resource || { hostname: '' };
576 return resource || { hostname: "" };
580 const renderVirtualMachineLogin = (login: { user: string }) => <Typography noWrap>{login.user}</Typography>;
582 export const VirtualMachineLogin = connect((state: RootState, props: { linkUuid: string }) => {
583 const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
585 const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
587 return { user: user?.username || permission?.tailUuid || '' };
589 const user = getResource<UserResource>(permission?.tailUuid || "")(state.resources);
591 return { user: user?.username || permission?.tailUuid || "" };
593 })(renderVirtualMachineLogin);
596 const renderCommonData = (data: string) => <Typography noWrap>{data}</Typography>;
598 const renderCommonDate = (date: string) => <Typography noWrap>{formatDate(date)}</Typography>;
600 export const CommonUuid = withResourceData("uuid", renderCommonData);
602 // Api Client Authorizations
603 export const TokenApiClientId = withResourceData("apiClientId", renderCommonData);
605 export const TokenApiToken = withResourceData("apiToken", renderCommonData);
607 export const TokenCreatedByIpAddress = withResourceData("createdByIpAddress", renderCommonDate);
609 export const TokenDefaultOwnerUuid = withResourceData("defaultOwnerUuid", renderCommonData);
611 export const TokenExpiresAt = withResourceData("expiresAt", renderCommonDate);
613 export const TokenLastUsedAt = withResourceData("lastUsedAt", renderCommonDate);
615 export const TokenLastUsedByIpAddress = withResourceData("lastUsedByIpAddress", renderCommonData);
617 export const TokenScopes = withResourceData("scopes", renderCommonData);
619 export const TokenUserId = withResourceData("userId", renderCommonData);
621 const clusterColors = [
637 export const ResourceCluster = (props: { uuid: string }) => {
638 const CLUSTER_ID_LENGTH = 5;
640 const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
641 const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
643 pos >= CLUSTER_ID_LENGTH
644 ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) + props.uuid.charCodeAt(4)) %
646 const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf("-") : 5;
647 const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : "";
649 pos >= CLUSTER_ID_LENGTH
650 ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) +
651 props.uuid.charCodeAt(4)) %
658 backgroundColor: clusterColors[ci][0],
659 color: clusterColors[ci][1],
675 const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || '-'}</Typography>;
677 export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
678 const resource = getResource<LinkResource>(props.uuid)(state.resources);
679 return resource || { name: '' };
681 const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || "-"}</Typography>;
683 export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
684 const resource = getResource<LinkResource>(props.uuid)(state.resources);
685 return resource || { name: "" };
689 const renderLinkClass = (item: { linkClass: string }) => <Typography noWrap>{item.linkClass}</Typography>;
691 export const ResourceLinkClass = connect((state: RootState, props: { uuid: string }) => {
692 const resource = getResource<LinkResource>(props.uuid)(state.resources);
694 return resource || { linkClass: '' };
697 const getResourceDisplayName = (resource: Resource): string => {
698 if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== 'undefined') {
700 return resource || { linkClass: "" };
703 const getResourceDisplayName = (resource: Resource): string => {
704 if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== "undefined") {
706 // We can be sure the resource is UserResource
707 return getUserDisplayName(resource as UserResource);
709 return (resource as GroupContentsResource).name;
713 const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
714 var displayName = getResourceDisplayName(item);
718 <Typography noWrap color='primary' style={{ cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
719 {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
724 style={{ cursor: "pointer" }}
727 item.kind === ResourceKind.GROUP && (item as GroupResource).groupClass === "role"
728 ? dispatch<any>(navigateToGroupDetails(item.uuid))
729 : dispatch<any>(navigateTo(item.uuid));
732 {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || "" : "")}:{" "}
733 {displayName || item.uuid}
739 export const ResourceLinkTail = connect((state: RootState, props: { uuid: string }) => {
740 const resource = getResource<LinkResource>(props.uuid)(state.resources);
742 const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
745 item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE },
747 const tailResource = getResource<Resource>(resource?.tailUuid || "")(state.resources);
750 item: tailResource || { uuid: resource?.tailUuid || "", kind: resource?.tailKind || ResourceKind.NONE },
753 })((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
755 export const ResourceLinkHead = connect((state: RootState, props: { uuid: string }) => {
756 const resource = getResource<LinkResource>(props.uuid)(state.resources);
758 const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
761 item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE },
763 const headResource = getResource<Resource>(resource?.headUuid || "")(state.resources);
766 item: headResource || { uuid: resource?.headUuid || "", kind: resource?.headKind || ResourceKind.NONE },
769 })((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
771 export const ResourceLinkUuid = connect((state: RootState, props: { uuid: string }) => {
772 const resource = getResource<LinkResource>(props.uuid)(state.resources);
774 return resource || { uuid: '' };
776 return resource || { uuid: "" };
780 export const ResourceLinkHeadUuid = connect((state: RootState, props: { uuid: string }) => {
781 const link = getResource<LinkResource>(props.uuid)(state.resources);
783 const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
785 return headResource || { uuid: '' };
787 const headResource = getResource<Resource>(link?.headUuid || "")(state.resources);
789 return headResource || { uuid: "" };
793 export const ResourceLinkTailUuid = connect((state: RootState, props: { uuid: string }) => {
794 const link = getResource<LinkResource>(props.uuid)(state.resources);
796 const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
798 return tailResource || { uuid: '' };
800 const tailResource = getResource<Resource>(link?.tailUuid || "")(state.resources);
802 return tailResource || { uuid: "" };
806 const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
811 <IconButton data-cy='resource-delete-button' onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
814 data-cy="resource-delete-button"
815 onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}
824 <IconButton disabled data-cy='resource-delete-button'>
828 data-cy="resource-delete-button"
836 return <Typography noWrap></Typography>;
840 export const ResourceLinkDelete = connect((state: RootState, props: { uuid: string }) => {
841 const link = getResource<LinkResource>(props.uuid)(state.resources);
843 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
846 item: link || { uuid: '', kind: ResourceKind.NONE },
848 const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
851 item: link || { uuid: "", kind: ResourceKind.NONE },
853 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
855 })((props: { item: LinkResource; canManage: boolean } & DispatchProp<any>) => renderLinkDelete(props.dispatch, props.item, props.canManage));
857 export const ResourceLinkTailEmail = connect((state: RootState, props: { uuid: string }) => {
858 const link = getResource<LinkResource>(props.uuid)(state.resources);
860 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
862 return resource || { email: '' };
864 const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
866 return resource || { email: "" };
870 export const ResourceLinkTailUsername = connect((state: RootState, props: { uuid: string }) => {
871 const link = getResource<LinkResource>(props.uuid)(state.resources);
873 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
875 return resource || { username: '' };
877 const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
879 return resource || { username: "" };
883 const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
886 {formatPermissionLevel(link.name as PermissionLevel)}
889 <IconButton data-cy='edit-permission-button' onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
896 data-cy="edit-permission-button"
897 onClick={event => dispatch<any>(openPermissionEditContextMenu(event, link))}
909 export const ResourceLinkHeadPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
910 const link = getResource<LinkResource>(props.uuid)(state.resources);
912 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
915 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
917 const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
920 link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
922 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
924 })((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
926 export const ResourceLinkTailPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
927 const link = getResource<LinkResource>(props.uuid)(state.resources);
929 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
932 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
934 const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
937 link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
939 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
941 })((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
943 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
944 const headResource = getResource<Resource>(link.headUuid)(state.resources);
945 if (headResource && headResource.kind === ResourceKind.GROUP) {
946 return (headResource as GroupResource).canManage;
954 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
959 <Tooltip title='Run process'>
961 <Tooltip title="Run process">
963 <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
972 export const ResourceRunProcess = connect((state: RootState, props: { uuid: string }) => {
973 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
976 uuid: resource ? resource.uuid : '',
978 uuid: resource ? resource.uuid : "",
981 })((props: { uuid: string } & DispatchProp<any>) => resourceRunProcess(props.dispatch, props.uuid));
983 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
984 if (ownerUuid === getPublicUuid(uuidPrefix)) {
985 return renderStatus(WorkflowStatus.PUBLIC);
987 return renderStatus(WorkflowStatus.PRIVATE);
991 const renderStatus = (status: string) => (
993 <Typography noWrap style={{ width: '60px' }}>
997 style={{ width: "60px" }}
1004 export const ResourceWorkflowStatus = connect((state: RootState, props: { uuid: string }) => {
1005 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
1006 const uuidPrefix = getUuidPrefix(state);
1009 ownerUuid: resource ? resource.ownerUuid : '',
1011 ownerUuid: resource ? resource.ownerUuid : "",
1015 })((props: { ownerUuid?: string; uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
1017 export const ResourceContainerUuid = connect((state: RootState, props: { uuid: string }) => {
1018 const process = getProcess(props.uuid)(state.resources);
1020 return { uuid: process?.container?.uuid ? process?.container?.uuid : '' };
1021 })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
1023 enum ColumnSelection {
1024 OUTPUT_UUID = 'outputUuid',
1025 LOG_UUID = 'logUuid',
1027 return { uuid: process?.container?.uuid ? process?.container?.uuid : "" };
1028 })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
1030 enum ColumnSelection {
1031 OUTPUT_UUID = "outputUuid",
1032 LOG_UUID = "logUuid",
1036 const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, column: string) => {
1037 const selectedColumnUuid = item[column];
1040 <Grid container alignItems='center' wrap='nowrap'>
1042 {selectedColumnUuid ? (
1043 <Typography color='primary' style={{ width: 'auto', cursor: 'pointer' }} noWrap onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}>
1044 {selectedColumnUuid}
1055 {selectedColumnUuid ? (
1058 style={{ width: "auto", cursor: "pointer" }}
1060 onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}
1062 {selectedColumnUuid}
1069 <Grid item>{selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}</Grid>
1074 export const ResourceOutputUuid = connect((state: RootState, props: { uuid: string }) => {
1075 const resource = getResource<ProcessResource>(props.uuid)(state.resources);
1077 })((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.OUTPUT_UUID));
1079 export const ResourceLogUuid = connect((state: RootState, props: { uuid: string }) => {
1080 const resource = getResource<ProcessResource>(props.uuid)(state.resources);
1082 })((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.LOG_UUID));
1084 export const ResourceParentProcess = connect((state: RootState, props: { uuid: string }) => {
1085 const process = getProcess(props.uuid)(state.resources);
1087 return { parentProcess: process?.containerRequest?.requestingContainerUuid || '' };
1089 return { parentProcess: process?.containerRequest?.requestingContainerUuid || "" };
1091 })((props: { parentProcess: string }) => renderUuid({ uuid: props.parentProcess }));
1093 export const ResourceModifiedByUserUuid = connect((state: RootState, props: { uuid: string }) => {
1094 const process = getProcess(props.uuid)(state.resources);
1096 return { userUuid: process?.containerRequest?.modifiedByUserUuid || '' };
1098 return { userUuid: process?.containerRequest?.modifiedByUserUuid || "" };
1100 })((props: { userUuid: string }) => renderUuid({ uuid: props.userUuid }));
1102 export const ResourceCreatedAtDate = connect((state: RootState, props: { uuid: string }) => {
1103 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
1105 return { date: resource ? resource.createdAt : '' };
1107 return { date: resource ? resource.createdAt : "" };
1109 })((props: { date: string }) => renderDate(props.date));
1111 export const ResourceLastModifiedDate = connect((state: RootState, props: { uuid: string }) => {
1112 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
1114 return { date: resource ? resource.modifiedAt : '' };
1116 return { date: resource ? resource.modifiedAt : "" };
1118 })((props: { date: string }) => renderDate(props.date));
1120 export const ResourceTrashDate = connect((state: RootState, props: { uuid: string }) => {
1121 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
1123 return { date: resource ? resource.trashAt : '' };
1125 return { date: resource ? resource.trashAt : "" };
1127 })((props: { date: string }) => renderDate(props.date));
1129 export const ResourceDeleteDate = connect((state: RootState, props: { uuid: string }) => {
1130 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
1132 return { date: resource ? resource.deleteAt : '' };
1133 })((props: { date: string }) => renderDate(props.date));
1135 export const renderFileSize = (fileSize?: number) => (
1136 <Typography noWrap style={{ minWidth: '45px' }}>
1138 return { date: resource ? resource.deleteAt : "" };
1139 })((props: { date: string }) => renderDate(props.date));
1141 export const renderFileSize = (fileSize?: number) => (
1144 style={{ minWidth: "45px" }}
1147 {formatFileSize(fileSize)}
1151 export const ResourceFileSize = connect((state: RootState, props: { uuid: string }) => {
1152 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
1154 if (resource && resource.kind !== ResourceKind.COLLECTION) {
1156 return { fileSize: '' };
1158 return { fileSize: "" };
1162 return { fileSize: resource ? resource.fileSizeTotal : 0 };
1163 })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
1166 const renderOwner = (owner: string) => <Typography noWrap>{owner || '-'}</Typography>;
1168 export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
1169 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
1170 return { owner: resource ? resource.ownerUuid : '' };
1172 const renderOwner = (owner: string) => <Typography noWrap>{owner || "-"}</Typography>;
1174 export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
1175 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
1176 return { owner: resource ? resource.ownerUuid : "" };
1178 })((props: { owner: string }) => renderOwner(props.owner));
1180 export const ResourceOwnerName = connect((state: RootState, props: { uuid: string }) => {
1181 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
1182 const ownerNameState = state.ownerName;
1184 const ownerName = ownerNameState.find((it) => it.uuid === resource!.ownerUuid);
1186 const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
1188 return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
1189 })((props: { owner: string }) => renderOwner(props.owner));
1191 export const ResourceUUID = connect((state: RootState, props: { uuid: string }) => {
1192 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
1194 return { uuid: resource ? resource.uuid : '' };
1195 })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
1197 const renderVersion = (version: number) => {
1198 return <Typography>{version ?? '-'}</Typography>;
1200 return { uuid: resource ? resource.uuid : "" };
1201 })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
1203 const renderVersion = (version: number) => {
1204 return <Typography>{version ?? "-"}</Typography>;
1208 export const ResourceVersion = connect((state: RootState, props: { uuid: string }) => {
1209 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
1211 return { version: resource ? resource.version : '' };
1213 return { version: resource ? resource.version : "" };
1215 })((props: { version: number }) => renderVersion(props.version));
1217 const renderPortableDataHash = (portableDataHash: string | null) => (
1219 {portableDataHash ? (
1222 <CopyToClipboardSnackbar value={portableDataHash} />
1234 export const ResourcePortableDataHash = connect((state: RootState, props: { uuid: string }) => {
1235 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
1237 return { portableDataHash: resource ? resource.portableDataHash : '' };
1238 })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
1240 const renderFileCount = (fileCount: number) => {
1241 return <Typography>{fileCount ?? '-'}</Typography>;
1243 return { portableDataHash: resource ? resource.portableDataHash : "" };
1244 })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
1246 const renderFileCount = (fileCount: number) => {
1247 return <Typography>{fileCount ?? "-"}</Typography>;
1251 export const ResourceFileCount = connect((state: RootState, props: { uuid: string }) => {
1252 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
1254 return { fileCount: resource ? resource.fileCount : '' };
1255 })((props: { fileCount: number }) => renderFileCount(props.fileCount));
1257 const userFromID = connect((state: RootState, props: { uuid: string }) => {
1258 let userFullname = '';
1260 return { fileCount: resource ? resource.fileCount : "" };
1261 })((props: { fileCount: number }) => renderFileCount(props.fileCount));
1263 const userFromID = connect((state: RootState, props: { uuid: string }) => {
1264 let userFullname = "";
1266 const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
1269 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
1272 return { uuid: props.uuid, userFullname };
1275 const ownerFromResourceId = compose(
1276 connect((state: RootState, props: { uuid: string }) => {
1277 const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
1279 return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
1281 return { uuid: childResource ? (childResource as Resource).ownerUuid : "" };
1287 const _resourceWithName = withStyles(
1290 )((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
1291 const { uuid, userFullname, dispatch, theme } = props;
1293 if (userFullname === '') {
1294 dispatch<any>(loadResource(uuid, false));
1296 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
1298 if (userFullname === "") {
1299 dispatch<any>(loadResource(uuid, false));
1302 style={{ color: theme.palette.primary.main }}
1314 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
1317 style={{ color: theme.palette.primary.main }}
1322 {userFullname} ({uuid})
1327 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
1329 export const ResourceWithName = userFromID(_resourceWithName);
1331 export const UserNameFromID = compose(userFromID)((props: { uuid: string; displayAsText?: string; userFullname: string; dispatch: Dispatch }) => {
1332 const { uuid, userFullname, dispatch } = props;
1335 if (userFullname === '') {
1337 if (userFullname === "") {
1339 dispatch<any>(loadResource(uuid, false));
1341 return <span>{userFullname ? userFullname : uuid}</span>;
1344 export const ResponsiblePerson = compose(
1345 connect((state: RootState, props: { uuid: string; parentRef: HTMLElement | null }) => {
1347 let responsiblePersonName: string = '';
1348 let responsiblePersonUUID: string = '';
1349 let responsiblePersonProperty: string = '';
1351 let responsiblePersonName: string = "";
1352 let responsiblePersonUUID: string = "";
1353 let responsiblePersonProperty: string = "";
1356 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
1358 const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
1360 while (!responsiblePersonProperty && keys[index]) {
1361 const key = keys[index];
1363 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
1365 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === "original_owner") {
1367 responsiblePersonProperty = key;
1373 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
1375 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
1376 responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
1377 resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
1380 if (resource && resource.kind === ResourceKind.USER) {
1381 responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
1384 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
1386 withStyles({}, { withTheme: true })
1387 )((props: { uuid: string | null; responsiblePersonName: string; parentRef: HTMLElement | null; theme: ArvadosTheme }) => {
1388 const { uuid, responsiblePersonName, parentRef, theme } = props;
1390 if (!uuid && parentRef) {
1392 parentRef.style.display = 'none';
1394 } else if (parentRef) {
1395 parentRef.style.display = 'block';
1397 parentRef.style.display = "none";
1399 } else if (parentRef) {
1400 parentRef.style.display = "block";
1404 if (!responsiblePersonName) {
1407 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
1410 style={{ color: theme.palette.primary.main }}
1422 <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
1425 style={{ color: theme.palette.primary.main }}
1430 {responsiblePersonName} ({uuid})
1435 const renderType = (type: string, subtype: string) => <Typography noWrap>{resourceLabel(type, subtype)}</Typography>;
1437 export const ResourceType = connect((state: RootState, props: { uuid: string }) => {
1438 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
1440 return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
1442 return { type: resource ? resource.kind : "", subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : "" };
1444 })((props: { type: string; subtype: string }) => renderType(props.type, props.subtype));
1446 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
1447 return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
1448 })((props: { resource: GroupContentsResource }) =>
1450 props.resource && props.resource.kind === ResourceKind.COLLECTION ? <CollectionStatus uuid={props.resource.uuid} /> : <ProcessStatus uuid={props.resource.uuid} />
1452 props.resource && props.resource.kind === ResourceKind.COLLECTION ? (
1453 <CollectionStatus uuid={props.resource.uuid} />
1455 <ProcessStatus uuid={props.resource.uuid} />
1460 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
1461 return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
1462 })((props: { collection: CollectionResource }) =>
1464 props.collection.uuid !== props.collection.currentVersionUuid ? <Typography>version {props.collection.version}</Typography> : <Typography>head version</Typography>
1466 props.collection.uuid !== props.collection.currentVersionUuid ? (
1467 <Typography>version {props.collection.version}</Typography>
1469 <Typography>head version</Typography>
1474 export const CollectionName = connect((state: RootState, props: { uuid: string; className?: string }) => {
1476 collection: getResource<CollectionResource>(props.uuid)(state.resources),
1478 className: props.className,
1480 })((props: { collection: CollectionResource; uuid: string; className?: string }) => (
1481 <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
1484 export const ProcessStatus = compose(
1485 connect((state: RootState, props: { uuid: string }) => {
1486 return { process: getProcess(props.uuid)(state.resources) };
1488 withStyles({}, { withTheme: true })
1489 )((props: { process?: Process; theme: ArvadosTheme }) =>
1492 label={getProcessStatus(props.process)}
1494 height: props.theme.spacing.unit * 3,
1495 width: props.theme.spacing.unit * 12,
1496 ...getProcessStatusStyles(getProcessStatus(props.process), props.theme),
1498 fontSize: '0.875rem',
1500 fontSize: "0.875rem",
1502 borderRadius: props.theme.spacing.unit * 0.625,
1506 <Typography>-</Typography>
1510 export const ProcessStartDate = connect((state: RootState, props: { uuid: string }) => {
1511 const process = getProcess(props.uuid)(state.resources);
1513 return { date: process && process.container ? process.container.startedAt : '' };
1514 })((props: { date: string }) => renderDate(props.date));
1516 export const renderRunTime = (time: number) => (
1517 <Typography noWrap style={{ minWidth: '45px' }}>
1519 return { date: process && process.container ? process.container.startedAt : "" };
1520 })((props: { date: string }) => renderDate(props.date));
1522 export const renderRunTime = (time: number) => (
1525 style={{ minWidth: "45px" }}
1528 {formatTime(time, true)}
1532 interface ContainerRunTimeProps {
1536 interface ContainerRunTimeState {
1540 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
1541 return { process: getProcess(props.uuid)(state.resources) };
1543 class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
1546 constructor(props: ContainerRunTimeProps) {
1548 this.state = { runtime: this.getRuntime() };
1552 return this.props.process ? getProcessRuntime(this.props.process) : 0;
1556 this.setState({ runtime: this.getRuntime() });
1559 componentDidMount() {
1560 this.timer = setInterval(this.updateRuntime.bind(this), 5000);
1563 componentWillUnmount() {
1564 clearInterval(this.timer);
1568 return this.props.process ? renderRunTime(this.state.runtime) : <Typography>-</Typography>;