1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
14 } from '@material-ui/core';
15 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
16 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
25 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, getProcessStatusColor, getProcessRuntime } from 'store/processes/process';
40 import { ArvadosTheme } from 'common/custom-theme';
41 import { compose, Dispatch } from 'redux';
42 import { WorkflowResource } from 'models/workflow';
43 import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-panel-view';
44 import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
45 import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
46 import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
47 import { toggleIsAdmin } from 'store/users/users-actions';
48 import { LinkClass, LinkResource } from 'models/link';
49 import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
50 import { withResourceData } from 'views-components/data-explorer/with-resources';
51 import { CollectionResource } from 'models/collection';
52 import { IllegalNamingWarning } from 'components/warning/warning';
53 import { loadResource } from 'store/resources/resources-actions';
54 import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
55 import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
56 import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
57 import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
58 import { PermissionLevel } from 'models/permission';
59 import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
60 import { getUserUuid } from 'common/getuser';
61 import { VirtualMachinesResource } from 'models/virtual-machines';
62 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
63 import { ProjectResource } from 'models/project';
65 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
67 const navFunc = ("groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo);
68 return <Grid container alignItems="center" wrap="nowrap" spacing={16}>
73 <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
74 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
75 ? <IllegalNamingWarning name={item.name} />
81 <Typography variant="caption">
82 <FavoriteStar resourceUuid={item.uuid} />
83 <PublicFavoriteStar resourceUuid={item.uuid} />
85 item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />
92 const FrozenProject = (props: {item: ProjectResource}) => {
93 const [fullUsername, setFullusername] = React.useState<any>(null);
94 const getFullName = React.useCallback(() => {
95 if (props.item.frozenByUuid) {
96 setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
98 }, [props.item, setFullusername])
100 if (props.item.frozenByUuid) {
102 return <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
103 <FreezeIcon style={{ fontSize: "inherit" }}/>
110 export const ResourceName = connect(
111 (state: RootState, props: { uuid: string }) => {
112 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
114 })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
116 const renderIcon = (item: GroupContentsResource) => {
118 case ResourceKind.PROJECT:
119 if (item.groupClass === GroupClass.FILTER) {
120 return <FilterGroupIcon />;
122 return <ProjectIcon />;
123 case ResourceKind.COLLECTION:
124 if (item.uuid === item.currentVersionUuid) {
125 return <CollectionIcon />;
127 return <CollectionOldVersionIcon />;
128 case ResourceKind.PROCESS:
129 return <ProcessIcon />;
130 case ResourceKind.WORKFLOW:
131 return <WorkflowIcon />;
133 return <DefaultIcon />;
137 const renderDate = (date?: string) => {
138 return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
141 const renderWorkflowName = (item: WorkflowResource) =>
142 <Grid container alignItems="center" wrap="nowrap" spacing={16}>
147 <Typography color="primary" style={{ width: '100px' }}>
153 export const ResourceWorkflowName = connect(
154 (state: RootState, props: { uuid: string }) => {
155 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
157 })(renderWorkflowName);
159 const getPublicUuid = (uuidPrefix: string) => {
160 return `${uuidPrefix}-tpzed-anonymouspublic`;
163 const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
164 const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
167 {!isPublic && uuid &&
168 <Tooltip title="Share">
169 <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
178 export const ResourceShare = connect(
179 (state: RootState, props: { uuid: string }) => {
180 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
181 const uuidPrefix = getUuidPrefix(state);
183 uuid: resource ? resource.uuid : '',
184 ownerUuid: resource ? resource.ownerUuid : '',
187 })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
188 resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
191 const renderFirstName = (item: { firstName: string }) => {
192 return <Typography noWrap>{item.firstName}</Typography>;
195 export const ResourceFirstName = connect(
196 (state: RootState, props: { uuid: string }) => {
197 const resource = getResource<UserResource>(props.uuid)(state.resources);
198 return resource || { firstName: '' };
201 const renderLastName = (item: { lastName: string }) =>
202 <Typography noWrap>{item.lastName}</Typography>;
204 export const ResourceLastName = connect(
205 (state: RootState, props: { uuid: string }) => {
206 const resource = getResource<UserResource>(props.uuid)(state.resources);
207 return resource || { lastName: '' };
210 const renderFullName = (dispatch: Dispatch, item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
211 const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
212 return link ? <Typography noWrap
214 style={{ 'cursor': 'pointer' }}
215 onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
218 <Typography noWrap>{displayName}</Typography>;
221 export const UserResourceFullName = connect(
222 (state: RootState, props: { uuid: string, link?: boolean }) => {
223 const resource = getResource<UserResource>(props.uuid)(state.resources);
224 return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
225 })((props: { item: { uuid: string, firstName: string, lastName: string }, link?: boolean } & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
227 const renderUuid = (item: { uuid: string }) =>
228 <Typography data-cy="uuid" noWrap>
230 {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-' }
233 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
234 getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
237 const renderEmail = (item: { email: string }) =>
238 <Typography noWrap>{item.email}</Typography>;
240 export const ResourceEmail = connect(
241 (state: RootState, props: { uuid: string }) => {
242 const resource = getResource<UserResource>(props.uuid)(state.resources);
243 return resource || { email: '' };
246 enum UserAccountStatus {
248 INACTIVE = 'Inactive',
253 const renderAccountStatus = (props: { status: UserAccountStatus }) =>
254 <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
257 switch (props.status) {
258 case UserAccountStatus.ACTIVE:
259 return <ActiveIcon style={{ color: '#4caf50', verticalAlign: "middle" }} />;
260 case UserAccountStatus.SETUP:
261 return <SetupIcon style={{ color: '#2196f3', verticalAlign: "middle" }} />;
262 case UserAccountStatus.INACTIVE:
263 return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: "middle" }} />;
276 const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
277 const user = getResource<UserResource>(props.uuid)(state.resources);
278 // Get membership links for all users group
279 const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
280 const permissions = filterResources((resource: LinkResource) =>
281 resource.kind === ResourceKind.LINK &&
282 resource.linkClass === LinkClass.PERMISSION &&
283 resource.headUuid === allUsersGroupUuid &&
284 resource.tailUuid === props.uuid
288 return user.isActive ? { status: UserAccountStatus.ACTIVE } : permissions.length > 0 ? { status: UserAccountStatus.SETUP } : { status: UserAccountStatus.INACTIVE };
290 return { status: UserAccountStatus.UNKNOWN };
294 export const ResourceLinkTailAccountStatus = connect(
295 (state: RootState, props: { uuid: string }) => {
296 const link = getResource<LinkResource>(props.uuid)(state.resources);
297 return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
298 })(renderAccountStatus);
300 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
302 const renderIsHidden = (props: {
303 memberLinkUuid: string,
304 permissionLinkUuid: string,
307 setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
309 if (props.memberLinkUuid) {
311 data-cy="user-visible-checkbox"
313 checked={props.visible}
314 disabled={!props.canManage}
317 props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
320 return <Typography />;
324 export const ResourceLinkTailIsVisible = connect(
325 (state: RootState, props: { uuid: string }) => {
326 const link = getResource<LinkResource>(props.uuid)(state.resources);
327 const member = getResource<Resource>(link?.tailUuid || '')(state.resources);
328 const group = getResource<GroupResource>(link?.headUuid || '')(state.resources);
329 const permissions = filterResources((resource: LinkResource) => {
330 return resource.linkClass === LinkClass.PERMISSION
331 && resource.headUuid === link?.tailUuid
332 && resource.tailUuid === group?.uuid
333 && resource.name === PermissionLevel.CAN_READ;
336 const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : '';
337 const isVisible = link && group && permissions.length > 0;
338 // Consider whether the current user canManage this resurce in addition when it's possible
339 const isBuiltin = isBuiltinGroup(link?.headUuid || '');
341 return member?.kind === ResourceKind.USER
342 ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
343 : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
344 }, { setMemberIsHidden }
347 const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
350 checked={props.isAdmin}
353 props.toggleIsAdmin(props.uuid);
356 export const ResourceIsAdmin = connect(
357 (state: RootState, props: { uuid: string }) => {
358 const resource = getResource<UserResource>(props.uuid)(state.resources);
359 return resource || { isAdmin: false };
363 const renderUsername = (item: { username: string, uuid: string }) =>
364 <Typography noWrap>{item.username || item.uuid}</Typography>;
366 export const ResourceUsername = connect(
367 (state: RootState, props: { uuid: string }) => {
368 const resource = getResource<UserResource>(props.uuid)(state.resources);
369 return resource || { username: '', uuid: props.uuid };
372 // Virtual machine resource
374 const renderHostname = (item: { hostname: string }) =>
375 <Typography noWrap>{item.hostname}</Typography>;
377 export const VirtualMachineHostname = connect(
378 (state: RootState, props: { uuid: string }) => {
379 const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
380 return resource || { hostname: '' };
383 const renderVirtualMachineLogin = (login: { user: string }) =>
384 <Typography noWrap>{login.user}</Typography>
386 export const VirtualMachineLogin = connect(
387 (state: RootState, props: { linkUuid: string }) => {
388 const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
389 const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
391 return { user: user?.username || permission?.tailUuid || '' };
392 })(renderVirtualMachineLogin);
395 const renderCommonData = (data: string) =>
396 <Typography noWrap>{data}</Typography>;
398 const renderCommonDate = (date: string) =>
399 <Typography noWrap>{formatDate(date)}</Typography>;
401 export const CommonUuid = withResourceData('uuid', renderCommonData);
403 // Api Client Authorizations
404 export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
406 export const TokenApiToken = withResourceData('apiToken', renderCommonData);
408 export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
410 export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
412 export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
414 export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
416 export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
418 export const TokenScopes = withResourceData('scopes', renderCommonData);
420 export const TokenUserId = withResourceData('userId', renderCommonData);
422 const clusterColors = [
430 export const ResourceCluster = (props: { uuid: string }) => {
431 const CLUSTER_ID_LENGTH = 5;
432 const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
433 const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
434 const ci = pos >= CLUSTER_ID_LENGTH ? (((((
435 (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
436 + props.uuid.charCodeAt(2))
437 * props.uuid.charCodeAt(3))
438 + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
439 return <span style={{
440 backgroundColor: clusterColors[ci][0],
441 color: clusterColors[ci][1],
444 }}>{clusterId}</span>;
448 const renderLinkName = (item: { name: string }) =>
449 <Typography noWrap>{item.name || '(none)'}</Typography>;
451 export const ResourceLinkName = connect(
452 (state: RootState, props: { uuid: string }) => {
453 const resource = getResource<LinkResource>(props.uuid)(state.resources);
454 return resource || { name: '' };
457 const renderLinkClass = (item: { linkClass: string }) =>
458 <Typography noWrap>{item.linkClass}</Typography>;
460 export const ResourceLinkClass = connect(
461 (state: RootState, props: { uuid: string }) => {
462 const resource = getResource<LinkResource>(props.uuid)(state.resources);
463 return resource || { linkClass: '' };
466 const getResourceDisplayName = (resource: Resource): string => {
467 if ((resource as UserResource).kind === ResourceKind.USER
468 && typeof (resource as UserResource).firstName !== 'undefined') {
469 // We can be sure the resource is UserResource
470 return getUserDisplayName(resource as UserResource);
472 return (resource as GroupContentsResource).name;
476 const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
477 var displayName = getResourceDisplayName(item);
479 return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
480 {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
484 export const ResourceLinkTail = connect(
485 (state: RootState, props: { uuid: string }) => {
486 const resource = getResource<LinkResource>(props.uuid)(state.resources);
487 const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
490 item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE }
492 })((props: { item: Resource } & DispatchProp<any>) =>
493 renderResourceLink(props.dispatch, props.item));
495 export const ResourceLinkHead = connect(
496 (state: RootState, props: { uuid: string }) => {
497 const resource = getResource<LinkResource>(props.uuid)(state.resources);
498 const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
501 item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE }
503 })((props: { item: Resource } & DispatchProp<any>) =>
504 renderResourceLink(props.dispatch, props.item));
506 export const ResourceLinkUuid = connect(
507 (state: RootState, props: { uuid: string }) => {
508 const resource = getResource<LinkResource>(props.uuid)(state.resources);
509 return resource || { uuid: '' };
512 export const ResourceLinkHeadUuid = connect(
513 (state: RootState, props: { uuid: string }) => {
514 const link = getResource<LinkResource>(props.uuid)(state.resources);
515 const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
517 return headResource || { uuid: '' };
520 export const ResourceLinkTailUuid = connect(
521 (state: RootState, props: { uuid: string }) => {
522 const link = getResource<LinkResource>(props.uuid)(state.resources);
523 const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
525 return tailResource || { uuid: '' };
528 const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
532 <IconButton data-cy="resource-delete-button" onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
537 <IconButton disabled data-cy="resource-delete-button">
542 return <Typography noWrap></Typography>;
546 export const ResourceLinkDelete = connect(
547 (state: RootState, props: { uuid: string }) => {
548 const link = getResource<LinkResource>(props.uuid)(state.resources);
549 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
552 item: link || { uuid: '', kind: ResourceKind.NONE },
553 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
555 })((props: { item: LinkResource, canManage: boolean } & DispatchProp<any>) =>
556 renderLinkDelete(props.dispatch, props.item, props.canManage));
558 export const ResourceLinkTailEmail = connect(
559 (state: RootState, props: { uuid: string }) => {
560 const link = getResource<LinkResource>(props.uuid)(state.resources);
561 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
563 return resource || { email: '' };
566 export const ResourceLinkTailUsername = connect(
567 (state: RootState, props: { uuid: string }) => {
568 const link = getResource<LinkResource>(props.uuid)(state.resources);
569 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
571 return resource || { username: '' };
574 const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
575 return <Typography noWrap>
576 {formatPermissionLevel(link.name as PermissionLevel)}
578 <IconButton data-cy="edit-permission-button" onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
586 export const ResourceLinkHeadPermissionLevel = connect(
587 (state: RootState, props: { uuid: string }) => {
588 const link = getResource<LinkResource>(props.uuid)(state.resources);
589 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
592 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
593 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
595 })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
596 renderPermissionLevel(props.dispatch, props.link, props.canManage));
598 export const ResourceLinkTailPermissionLevel = connect(
599 (state: RootState, props: { uuid: string }) => {
600 const link = getResource<LinkResource>(props.uuid)(state.resources);
601 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
604 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
605 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
607 })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
608 renderPermissionLevel(props.dispatch, props.link, props.canManage));
610 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
611 const headResource = getResource<Resource>(link.headUuid)(state.resources);
612 // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
613 const userUuid = getUserUuid(state);
615 if (headResource && headResource.kind === ResourceKind.GROUP) {
616 return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
624 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
628 <Tooltip title="Run process">
629 <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
637 export const ResourceRunProcess = connect(
638 (state: RootState, props: { uuid: string }) => {
639 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
641 uuid: resource ? resource.uuid : ''
643 })((props: { uuid: string } & DispatchProp<any>) =>
644 resourceRunProcess(props.dispatch, props.uuid));
646 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
647 if (ownerUuid === getPublicUuid(uuidPrefix)) {
648 return renderStatus(WorkflowStatus.PUBLIC);
650 return renderStatus(WorkflowStatus.PRIVATE);
654 const renderStatus = (status: string) =>
655 <Typography noWrap style={{ width: '60px' }}>{status}</Typography>;
657 export const ResourceWorkflowStatus = connect(
658 (state: RootState, props: { uuid: string }) => {
659 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
660 const uuidPrefix = getUuidPrefix(state);
662 ownerUuid: resource ? resource.ownerUuid : '',
665 })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
667 const renderProcessState = (processState: string) => <Typography>{processState || '-'}</Typography>
669 export const ResourceProcessState = connect(
670 (state: RootState, props: { uuid: string }) => {
671 const process = getProcess(props.uuid)(state.resources)
672 return { state: process?.container?.state ? process?.container?.state : '' };
673 })((props: { state: string }) => renderProcessState(props.state));
675 export const ResourceParentProcess = connect(
676 (state: RootState, props: { uuid: string }) => {
677 const process = getProcess(props.uuid)(state.resources)
678 const parentProcessUuid = process?.containerRequest?.requestingContainerUuid
679 return { parentProcess: parentProcessUuid || '' };
680 })((props: { parentProcess: string }) => renderUuid({uuid: props.parentProcess}));
682 export const ResourceCreatedAtDate = connect(
683 (state: RootState, props: { uuid: string }) => {
684 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
685 return { date: resource ? resource.createdAt : '' };
686 })((props: { date: string }) => renderDate(props.date));
688 export const ResourceLastModifiedDate = connect(
689 (state: RootState, props: { uuid: string }) => {
690 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
691 return { date: resource ? resource.modifiedAt : '' };
692 })((props: { date: string }) => renderDate(props.date));
694 export const ResourceTrashDate = connect(
695 (state: RootState, props: { uuid: string }) => {
696 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
697 return { date: resource ? resource.trashAt : '' };
698 })((props: { date: string }) => renderDate(props.date));
700 export const ResourceDeleteDate = connect(
701 (state: RootState, props: { uuid: string }) => {
702 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
703 return { date: resource ? resource.deleteAt : '' };
704 })((props: { date: string }) => renderDate(props.date));
706 export const renderFileSize = (fileSize?: number) =>
707 <Typography noWrap style={{ minWidth: '45px' }}>
708 {formatFileSize(fileSize)}
711 export const ResourceFileSize = connect(
712 (state: RootState, props: { uuid: string }) => {
713 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
715 if (resource && resource.kind !== ResourceKind.COLLECTION) {
716 return { fileSize: '' };
719 return { fileSize: resource ? resource.fileSizeTotal : 0 };
720 })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
722 const renderOwner = (owner: string) =>
727 export const ResourceOwner = connect(
728 (state: RootState, props: { uuid: string }) => {
729 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
730 return { owner: resource ? resource.ownerUuid : '' };
731 })((props: { owner: string }) => renderOwner(props.owner));
733 export const ResourceOwnerName = connect(
734 (state: RootState, props: { uuid: string }) => {
735 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
736 const ownerNameState = state.ownerName;
737 const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
738 return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
739 })((props: { owner: string }) => renderOwner(props.owner));
741 export const ResourceUUID = connect(
742 (state: RootState, props: { uuid: string }) => {
743 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
744 return { uuid: resource ? resource.uuid : '' };
745 })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
747 const renderPortableDataHash = (portableDataHash:string | null) =>
749 {portableDataHash ? <>{portableDataHash}
750 <CopyToClipboardSnackbar value={portableDataHash} /></> : '-' }
753 export const ResourcePortableDataHash = connect(
754 (state: RootState, props: { uuid: string }) => {
755 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
756 return { portableDataHash: resource ? resource.portableDataHash : '' };
757 })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
759 const renderVersion = (version: number) =>{
760 return <Typography>{version ?? '-'}</Typography>
763 export const ResourceVersion = connect(
764 (state: RootState, props: { uuid: string }) => {
765 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
766 return { version: resource ? resource.version: '' };
767 })((props: { version: number }) => renderVersion(props.version));
769 const renderDescription = (description: string)=>{
770 const truncatedDescription = description ? description.slice(0, 18) + '...' : '-'
771 return <Typography title={description}>{truncatedDescription}</Typography>;
774 const renderFileCount = (fileCount: number) =>{
775 return <Typography>{fileCount ?? '-'}</Typography>
778 export const ResourceFileCount = connect(
779 (state: RootState, props: { uuid: string }) => {
780 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
781 return { fileCount: resource ? resource.fileCount: '' };
782 })((props: { fileCount: number }) => renderFileCount(props.fileCount));
784 export const ResourceDescription = connect(
785 (state: RootState, props: { uuid: string }) => {
786 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
787 //testing---------------
788 // const containerRequestDescription = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
789 // if (resource && !resource.description && resource.kind === ResourceKind.PROCESS) resource.description = containerRequestDescription
790 //testing---------------
791 return { description: resource ? resource.description : '' };
792 })((props: { description: string }) => renderDescription(props.description));
796 (state: RootState, props: { uuid: string }) => {
797 let userFullname = '';
798 const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
801 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
804 return { uuid: props.uuid, userFullname };
807 const ownerFromResourceId =
809 connect((state: RootState, props: { uuid: string }) => {
810 const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
811 return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
816 const _resourceWithName =
817 withStyles({}, { withTheme: true })
818 ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
819 const { uuid, userFullname, dispatch, theme } = props;
821 if (userFullname === '') {
822 dispatch<any>(loadResource(uuid, false));
823 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
828 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
829 {userFullname} ({uuid})
833 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
835 export const ResourceWithName = userFromID(_resourceWithName);
837 export const UserNameFromID =
839 (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
840 const { uuid, userFullname, dispatch } = props;
842 if (userFullname === '') {
843 dispatch<any>(loadResource(uuid, false));
846 {userFullname ? userFullname : uuid}
850 export const ResponsiblePerson =
853 (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
854 let responsiblePersonName: string = '';
855 let responsiblePersonUUID: string = '';
856 let responsiblePersonProperty: string = '';
858 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
860 const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
862 while (!responsiblePersonProperty && keys[index]) {
863 const key = keys[index];
864 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
865 responsiblePersonProperty = key;
871 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
873 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
874 responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
875 resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
878 if (resource && resource.kind === ResourceKind.USER) {
879 responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
882 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
884 withStyles({}, { withTheme: true }))
885 ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
886 const { uuid, responsiblePersonName, parentRef, theme } = props;
888 if (!uuid && parentRef) {
889 parentRef.style.display = 'none';
891 } else if (parentRef) {
892 parentRef.style.display = 'block';
895 if (!responsiblePersonName) {
896 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
901 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
902 {responsiblePersonName} ({uuid})
906 const renderType = (type: string, subtype: string) =>
908 {resourceLabel(type, subtype)}
911 export const ResourceType = connect(
912 (state: RootState, props: { uuid: string }) => {
913 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
914 return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
915 })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
917 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
918 return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
919 })((props: { resource: GroupContentsResource }) =>
920 (props.resource && props.resource.kind === ResourceKind.COLLECTION)
921 ? <CollectionStatus uuid={props.resource.uuid} />
922 : <ProcessStatus uuid={props.resource.uuid} />
925 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
926 return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
927 })((props: { collection: CollectionResource }) =>
928 (props.collection.uuid !== props.collection.currentVersionUuid)
929 ? <Typography>version {props.collection.version}</Typography>
930 : <Typography>head version</Typography>
933 export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
935 collection: getResource<CollectionResource>(props.uuid)(state.resources),
937 className: props.className,
939 })((props: { collection: CollectionResource, uuid: string, className?: string }) =>
940 <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
943 export const ProcessStatus = compose(
944 connect((state: RootState, props: { uuid: string }) => {
945 return { process: getProcess(props.uuid)(state.resources) };
947 withStyles({}, { withTheme: true }))
948 ((props: { process?: Process, theme: ArvadosTheme }) =>
950 ? <Chip label={getProcessStatus(props.process)}
952 height: props.theme.spacing.unit * 3,
953 width: props.theme.spacing.unit * 12,
954 backgroundColor: getProcessStatusColor(
955 getProcessStatus(props.process), props.theme),
956 color: props.theme.palette.common.white,
957 fontSize: '0.875rem',
958 borderRadius: props.theme.spacing.unit * 0.625,
961 : <Typography>-</Typography>
964 export const ProcessStartDate = connect(
965 (state: RootState, props: { uuid: string }) => {
966 const process = getProcess(props.uuid)(state.resources);
967 return { date: (process && process.container) ? process.container.startedAt : '' };
968 })((props: { date: string }) => renderDate(props.date));
970 export const renderRunTime = (time: number) =>
971 <Typography noWrap style={{ minWidth: '45px' }}>
972 {formatTime(time, true)}
975 interface ContainerRunTimeProps {
979 interface ContainerRunTimeState {
983 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
984 return { process: getProcess(props.uuid)(state.resources) };
985 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
988 constructor(props: ContainerRunTimeProps) {
990 this.state = { runtime: this.getRuntime() };
994 return this.props.process ? getProcessRuntime(this.props.process) : 0;
998 this.setState({ runtime: this.getRuntime() });
1001 componentDidMount() {
1002 this.timer = setInterval(this.updateRuntime.bind(this), 5000);
1005 componentWillUnmount() {
1006 clearInterval(this.timer);
1010 return renderRunTime(this.state.runtime);