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, formatObjectProperties} 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 <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 export const ResourceCreatedAtDate = connect(
668 (state: RootState, props: { uuid: string }) => {
669 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
670 return { date: resource ? resource.createdAt : '' };
671 })((props: { date: string }) => renderDate(props.date));
673 export const ResourceLastModifiedDate = connect(
674 (state: RootState, props: { uuid: string }) => {
675 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
676 return { date: resource ? resource.modifiedAt : '' };
677 })((props: { date: string }) => renderDate(props.date));
679 export const ResourceTrashDate = connect(
680 (state: RootState, props: { uuid: string }) => {
681 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
682 return { date: resource ? resource.trashAt : '' };
683 })((props: { date: string }) => renderDate(props.date));
685 export const ResourceDeleteDate = connect(
686 (state: RootState, props: { uuid: string }) => {
687 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
688 return { date: resource ? resource.deleteAt : '' };
689 })((props: { date: string }) => renderDate(props.date));
691 export const renderFileSize = (fileSize?: number) =>
692 <Typography noWrap style={{ minWidth: '45px' }}>
693 {formatFileSize(fileSize)}
696 export const ResourceFileSize = connect(
697 (state: RootState, props: { uuid: string }) => {
698 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
700 if (resource && resource.kind !== ResourceKind.COLLECTION) {
701 return { fileSize: '' };
704 return { fileSize: resource ? resource.fileSizeTotal : 0 };
705 })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
707 const renderOwner = (owner: string) =>
712 export const ResourceOwner = connect(
713 (state: RootState, props: { uuid: string }) => {
714 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
715 return { owner: resource ? resource.ownerUuid : '' };
716 })((props: { owner: string }) => renderOwner(props.owner));
718 export const ResourceOwnerName = connect(
719 (state: RootState, props: { uuid: string }) => {
720 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
721 const ownerNameState = state.ownerName;
722 const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
723 return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
724 })((props: { owner: string }) => renderOwner(props.owner));
726 export const ResourceUUID = connect(
727 (state: RootState, props: { uuid: string }) => {
728 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
729 console.log('COLLECTION_RESOIRCE', resource)
730 return { uuid: resource ? resource.uuid : '' };
731 })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
733 const renderMetadata = (metadata:any) => {
734 return <>{formatObjectProperties(metadata).map((property, i)=>
735 <Typography key={i} noWrap>{property[0]}: {property[1]}</Typography>
739 export const ResourceMetadata = connect(
740 (state: RootState, props: { uuid: string }) => {
741 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
742 const metadata = resource && Object.keys(resource.properties).length ? resource.properties : {}
743 if(resource && resource.portableDataHash) metadata['Portable Data Hash'] = resource.portableDataHash
744 return { properties: metadata };
745 })((props: { properties: string }) => renderMetadata(props.properties));
747 const renderVersion = (version: number) =>{
748 return <Typography>{version ?? '-'}</Typography>
751 export const ResourceVersion = connect(
752 (state: RootState, props: { uuid: string }) => {
753 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
754 return { version: resource ? resource.version: '' };
755 })((props: { version: number }) => renderVersion(props.version));
757 const renderDescription = (description: string)=>{
758 const truncatedDescription = description ? description.slice(0, 18) + '...' : '-'
759 return <Typography title={description}>{truncatedDescription}</Typography>;
762 const renderFileCount = (fileCount: number) =>{
763 return <Typography>{fileCount ?? '-'}</Typography>
766 export const ResourceFileCount = connect(
767 (state: RootState, props: { uuid: string }) => {
768 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
769 return { fileCount: resource ? resource.fileCount: '' };
770 })((props: { fileCount: number }) => renderFileCount(props.fileCount));
772 export const ResourceDescription = connect(
773 (state: RootState, props: { uuid: string }) => {
774 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
775 //testing---------------
776 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."
777 if (resource && !resource.description && resource.kind === ResourceKind.PROCESS) resource.description = containerRequestDescription
778 //testing---------------
779 return { description: resource ? resource.description : '' };
780 })((props: { description: string }) => renderDescription(props.description));
784 (state: RootState, props: { uuid: string }) => {
785 let userFullname = '';
786 const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
789 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
792 return { uuid: props.uuid, userFullname };
795 const ownerFromResourceId =
797 connect((state: RootState, props: { uuid: string }) => {
798 const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
799 return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
804 const _resourceWithName =
805 withStyles({}, { withTheme: true })
806 ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
807 const { uuid, userFullname, dispatch, theme } = props;
809 if (userFullname === '') {
810 dispatch<any>(loadResource(uuid, false));
811 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
816 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
817 {userFullname} ({uuid})
821 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
823 export const ResourceWithName = userFromID(_resourceWithName);
825 export const UserNameFromID =
827 (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
828 const { uuid, userFullname, dispatch } = props;
830 if (userFullname === '') {
831 dispatch<any>(loadResource(uuid, false));
834 {userFullname ? userFullname : uuid}
838 export const ResponsiblePerson =
841 (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
842 let responsiblePersonName: string = '';
843 let responsiblePersonUUID: string = '';
844 let responsiblePersonProperty: string = '';
846 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
848 const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
850 while (!responsiblePersonProperty && keys[index]) {
851 const key = keys[index];
852 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
853 responsiblePersonProperty = key;
859 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
861 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
862 responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
863 resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
866 if (resource && resource.kind === ResourceKind.USER) {
867 responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
870 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
872 withStyles({}, { withTheme: true }))
873 ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
874 const { uuid, responsiblePersonName, parentRef, theme } = props;
876 if (!uuid && parentRef) {
877 parentRef.style.display = 'none';
879 } else if (parentRef) {
880 parentRef.style.display = 'block';
883 if (!responsiblePersonName) {
884 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
889 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
890 {responsiblePersonName} ({uuid})
894 const renderType = (type: string, subtype: string) =>
896 {resourceLabel(type, subtype)}
899 export const ResourceType = connect(
900 (state: RootState, props: { uuid: string }) => {
901 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
902 return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
903 })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
905 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
906 return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
907 })((props: { resource: GroupContentsResource }) =>
908 (props.resource && props.resource.kind === ResourceKind.COLLECTION)
909 ? <CollectionStatus uuid={props.resource.uuid} />
910 : <ProcessStatus uuid={props.resource.uuid} />
913 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
914 return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
915 })((props: { collection: CollectionResource }) =>
916 (props.collection.uuid !== props.collection.currentVersionUuid)
917 ? <Typography>version {props.collection.version}</Typography>
918 : <Typography>head version</Typography>
921 export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
923 collection: getResource<CollectionResource>(props.uuid)(state.resources),
925 className: props.className,
927 })((props: { collection: CollectionResource, uuid: string, className?: string }) =>
928 <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
931 export const ProcessStatus = compose(
932 connect((state: RootState, props: { uuid: string }) => {
933 return { process: getProcess(props.uuid)(state.resources) };
935 withStyles({}, { withTheme: true }))
936 ((props: { process?: Process, theme: ArvadosTheme }) =>
938 ? <Chip label={getProcessStatus(props.process)}
940 height: props.theme.spacing.unit * 3,
941 width: props.theme.spacing.unit * 12,
942 backgroundColor: getProcessStatusColor(
943 getProcessStatus(props.process), props.theme),
944 color: props.theme.palette.common.white,
945 fontSize: '0.875rem',
946 borderRadius: props.theme.spacing.unit * 0.625,
949 : <Typography>-</Typography>
952 export const ProcessStartDate = connect(
953 (state: RootState, props: { uuid: string }) => {
954 const process = getProcess(props.uuid)(state.resources);
955 return { date: (process && process.container) ? process.container.startedAt : '' };
956 })((props: { date: string }) => renderDate(props.date));
958 export const renderRunTime = (time: number) =>
959 <Typography noWrap style={{ minWidth: '45px' }}>
960 {formatTime(time, true)}
963 interface ContainerRunTimeProps {
967 interface ContainerRunTimeState {
971 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
972 return { process: getProcess(props.uuid)(state.resources) };
973 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
976 constructor(props: ContainerRunTimeProps) {
978 this.state = { runtime: this.getRuntime() };
982 return this.props.process ? getProcessRuntime(this.props.process) : 0;
986 this.setState({ runtime: this.getRuntime() });
989 componentDidMount() {
990 this.timer = setInterval(this.updateRuntime.bind(this), 5000);
993 componentWillUnmount() {
994 clearInterval(this.timer);
998 return renderRunTime(this.state.runtime);