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';
64 import {ContainerRequestResource} from 'models/container-request'
66 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
68 const navFunc = ("groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo);
69 return <Grid container alignItems="center" wrap="nowrap" spacing={16}>
74 <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
75 {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION
76 ? <IllegalNamingWarning name={item.name} />
82 <Typography variant="caption">
83 <FavoriteStar resourceUuid={item.uuid} />
84 <PublicFavoriteStar resourceUuid={item.uuid} />
86 item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />
93 const FrozenProject = (props: {item: ProjectResource}) => {
94 const [fullUsername, setFullusername] = React.useState<any>(null);
95 const getFullName = React.useCallback(() => {
96 if (props.item.frozenByUuid) {
97 setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
99 }, [props.item, setFullusername])
101 if (props.item.frozenByUuid) {
103 return <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
104 <FreezeIcon style={{ fontSize: "inherit" }}/>
111 export const ResourceName = connect(
112 (state: RootState, props: { uuid: string }) => {
113 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
115 })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
117 const renderIcon = (item: GroupContentsResource) => {
119 case ResourceKind.PROJECT:
120 if (item.groupClass === GroupClass.FILTER) {
121 return <FilterGroupIcon />;
123 return <ProjectIcon />;
124 case ResourceKind.COLLECTION:
125 if (item.uuid === item.currentVersionUuid) {
126 return <CollectionIcon />;
128 return <CollectionOldVersionIcon />;
129 case ResourceKind.PROCESS:
130 return <ProcessIcon />;
131 case ResourceKind.WORKFLOW:
132 return <WorkflowIcon />;
134 return <DefaultIcon />;
138 const renderDate = (date?: string) => {
139 return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
142 const renderWorkflowName = (item: WorkflowResource) =>
143 <Grid container alignItems="center" wrap="nowrap" spacing={16}>
148 <Typography color="primary" style={{ width: '100px' }}>
154 export const ResourceWorkflowName = connect(
155 (state: RootState, props: { uuid: string }) => {
156 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
158 })(renderWorkflowName);
160 const getPublicUuid = (uuidPrefix: string) => {
161 return `${uuidPrefix}-tpzed-anonymouspublic`;
164 const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
165 const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
168 {!isPublic && uuid &&
169 <Tooltip title="Share">
170 <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
179 export const ResourceShare = connect(
180 (state: RootState, props: { uuid: string }) => {
181 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
182 const uuidPrefix = getUuidPrefix(state);
184 uuid: resource ? resource.uuid : '',
185 ownerUuid: resource ? resource.ownerUuid : '',
188 })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
189 resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
192 const renderFirstName = (item: { firstName: string }) => {
193 return <Typography noWrap>{item.firstName}</Typography>;
196 export const ResourceFirstName = connect(
197 (state: RootState, props: { uuid: string }) => {
198 const resource = getResource<UserResource>(props.uuid)(state.resources);
199 return resource || { firstName: '' };
202 const renderLastName = (item: { lastName: string }) =>
203 <Typography noWrap>{item.lastName}</Typography>;
205 export const ResourceLastName = connect(
206 (state: RootState, props: { uuid: string }) => {
207 const resource = getResource<UserResource>(props.uuid)(state.resources);
208 return resource || { lastName: '' };
211 const renderFullName = (dispatch: Dispatch, item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
212 const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
213 return link ? <Typography noWrap
215 style={{ 'cursor': 'pointer' }}
216 onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
219 <Typography noWrap>{displayName}</Typography>;
222 export const UserResourceFullName = connect(
223 (state: RootState, props: { uuid: string, link?: boolean }) => {
224 const resource = getResource<UserResource>(props.uuid)(state.resources);
225 return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
226 })((props: { item: { uuid: string, firstName: string, lastName: string }, link?: boolean } & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
228 const renderUuid = (item: { uuid: string }) =>
229 <Typography data-cy="uuid" noWrap>
231 <CopyToClipboardSnackbar value={item.uuid} />
234 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
235 getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
238 const renderEmail = (item: { email: string }) =>
239 <Typography noWrap>{item.email}</Typography>;
241 export const ResourceEmail = connect(
242 (state: RootState, props: { uuid: string }) => {
243 const resource = getResource<UserResource>(props.uuid)(state.resources);
244 return resource || { email: '' };
247 enum UserAccountStatus {
249 INACTIVE = 'Inactive',
254 const renderAccountStatus = (props: { status: UserAccountStatus }) =>
255 <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
258 switch (props.status) {
259 case UserAccountStatus.ACTIVE:
260 return <ActiveIcon style={{ color: '#4caf50', verticalAlign: "middle" }} />;
261 case UserAccountStatus.SETUP:
262 return <SetupIcon style={{ color: '#2196f3', verticalAlign: "middle" }} />;
263 case UserAccountStatus.INACTIVE:
264 return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: "middle" }} />;
277 const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
278 const user = getResource<UserResource>(props.uuid)(state.resources);
279 // Get membership links for all users group
280 const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
281 const permissions = filterResources((resource: LinkResource) =>
282 resource.kind === ResourceKind.LINK &&
283 resource.linkClass === LinkClass.PERMISSION &&
284 resource.headUuid === allUsersGroupUuid &&
285 resource.tailUuid === props.uuid
289 return user.isActive ? { status: UserAccountStatus.ACTIVE } : permissions.length > 0 ? { status: UserAccountStatus.SETUP } : { status: UserAccountStatus.INACTIVE };
291 return { status: UserAccountStatus.UNKNOWN };
295 export const ResourceLinkTailAccountStatus = connect(
296 (state: RootState, props: { uuid: string }) => {
297 const link = getResource<LinkResource>(props.uuid)(state.resources);
298 return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
299 })(renderAccountStatus);
301 export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
303 const renderIsHidden = (props: {
304 memberLinkUuid: string,
305 permissionLinkUuid: string,
308 setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void
310 if (props.memberLinkUuid) {
312 data-cy="user-visible-checkbox"
314 checked={props.visible}
315 disabled={!props.canManage}
318 props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
321 return <Typography />;
325 export const ResourceLinkTailIsVisible = connect(
326 (state: RootState, props: { uuid: string }) => {
327 const link = getResource<LinkResource>(props.uuid)(state.resources);
328 const member = getResource<Resource>(link?.tailUuid || '')(state.resources);
329 const group = getResource<GroupResource>(link?.headUuid || '')(state.resources);
330 const permissions = filterResources((resource: LinkResource) => {
331 return resource.linkClass === LinkClass.PERMISSION
332 && resource.headUuid === link?.tailUuid
333 && resource.tailUuid === group?.uuid
334 && resource.name === PermissionLevel.CAN_READ;
337 const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : '';
338 const isVisible = link && group && permissions.length > 0;
339 // Consider whether the current user canManage this resurce in addition when it's possible
340 const isBuiltin = isBuiltinGroup(link?.headUuid || '');
342 return member?.kind === ResourceKind.USER
343 ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
344 : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
345 }, { setMemberIsHidden }
348 const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
351 checked={props.isAdmin}
354 props.toggleIsAdmin(props.uuid);
357 export const ResourceIsAdmin = connect(
358 (state: RootState, props: { uuid: string }) => {
359 const resource = getResource<UserResource>(props.uuid)(state.resources);
360 return resource || { isAdmin: false };
364 const renderUsername = (item: { username: string, uuid: string }) =>
365 <Typography noWrap>{item.username || item.uuid}</Typography>;
367 export const ResourceUsername = connect(
368 (state: RootState, props: { uuid: string }) => {
369 const resource = getResource<UserResource>(props.uuid)(state.resources);
370 return resource || { username: '', uuid: props.uuid };
373 // Virtual machine resource
375 const renderHostname = (item: { hostname: string }) =>
376 <Typography noWrap>{item.hostname}</Typography>;
378 export const VirtualMachineHostname = connect(
379 (state: RootState, props: { uuid: string }) => {
380 const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
381 return resource || { hostname: '' };
384 const renderVirtualMachineLogin = (login: { user: string }) =>
385 <Typography noWrap>{login.user}</Typography>
387 export const VirtualMachineLogin = connect(
388 (state: RootState, props: { linkUuid: string }) => {
389 const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
390 const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
392 return { user: user?.username || permission?.tailUuid || '' };
393 })(renderVirtualMachineLogin);
396 const renderCommonData = (data: string) =>
397 <Typography noWrap>{data}</Typography>;
399 const renderCommonDate = (date: string) =>
400 <Typography noWrap>{formatDate(date)}</Typography>;
402 export const CommonUuid = withResourceData('uuid', renderCommonData);
404 // Api Client Authorizations
405 export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
407 export const TokenApiToken = withResourceData('apiToken', renderCommonData);
409 export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
411 export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
413 export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
415 export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
417 export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
419 export const TokenScopes = withResourceData('scopes', renderCommonData);
421 export const TokenUserId = withResourceData('userId', renderCommonData);
423 const clusterColors = [
431 export const ResourceCluster = (props: { uuid: string }) => {
432 const CLUSTER_ID_LENGTH = 5;
433 const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
434 const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
435 const ci = pos >= CLUSTER_ID_LENGTH ? (((((
436 (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
437 + props.uuid.charCodeAt(2))
438 * props.uuid.charCodeAt(3))
439 + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
440 return <span style={{
441 backgroundColor: clusterColors[ci][0],
442 color: clusterColors[ci][1],
445 }}>{clusterId}</span>;
449 const renderLinkName = (item: { name: string }) =>
450 <Typography noWrap>{item.name || '(none)'}</Typography>;
452 export const ResourceLinkName = connect(
453 (state: RootState, props: { uuid: string }) => {
454 const resource = getResource<LinkResource>(props.uuid)(state.resources);
455 return resource || { name: '' };
458 const renderLinkClass = (item: { linkClass: string }) =>
459 <Typography noWrap>{item.linkClass}</Typography>;
461 export const ResourceLinkClass = connect(
462 (state: RootState, props: { uuid: string }) => {
463 const resource = getResource<LinkResource>(props.uuid)(state.resources);
464 return resource || { linkClass: '' };
467 const getResourceDisplayName = (resource: Resource): string => {
468 if ((resource as UserResource).kind === ResourceKind.USER
469 && typeof (resource as UserResource).firstName !== 'undefined') {
470 // We can be sure the resource is UserResource
471 return getUserDisplayName(resource as UserResource);
473 return (resource as GroupContentsResource).name;
477 const renderResourceLink = (dispatch: Dispatch, item: Resource) => {
478 var displayName = getResourceDisplayName(item);
480 return <Typography noWrap color="primary" style={{ 'cursor': 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
481 {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
485 export const ResourceLinkTail = connect(
486 (state: RootState, props: { uuid: string }) => {
487 const resource = getResource<LinkResource>(props.uuid)(state.resources);
488 const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
491 item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE }
493 })((props: { item: Resource } & DispatchProp<any>) =>
494 renderResourceLink(props.dispatch, props.item));
496 export const ResourceLinkHead = connect(
497 (state: RootState, props: { uuid: string }) => {
498 const resource = getResource<LinkResource>(props.uuid)(state.resources);
499 const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
502 item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE }
504 })((props: { item: Resource } & DispatchProp<any>) =>
505 renderResourceLink(props.dispatch, props.item));
507 export const ResourceLinkUuid = connect(
508 (state: RootState, props: { uuid: string }) => {
509 const resource = getResource<LinkResource>(props.uuid)(state.resources);
510 return resource || { uuid: '' };
513 export const ResourceLinkHeadUuid = connect(
514 (state: RootState, props: { uuid: string }) => {
515 const link = getResource<LinkResource>(props.uuid)(state.resources);
516 const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
518 return headResource || { uuid: '' };
521 export const ResourceLinkTailUuid = connect(
522 (state: RootState, props: { uuid: string }) => {
523 const link = getResource<LinkResource>(props.uuid)(state.resources);
524 const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
526 return tailResource || { uuid: '' };
529 const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
533 <IconButton data-cy="resource-delete-button" onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
538 <IconButton disabled data-cy="resource-delete-button">
543 return <Typography noWrap></Typography>;
547 export const ResourceLinkDelete = connect(
548 (state: RootState, props: { uuid: string }) => {
549 const link = getResource<LinkResource>(props.uuid)(state.resources);
550 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
553 item: link || { uuid: '', kind: ResourceKind.NONE },
554 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
556 })((props: { item: LinkResource, canManage: boolean } & DispatchProp<any>) =>
557 renderLinkDelete(props.dispatch, props.item, props.canManage));
559 export const ResourceLinkTailEmail = connect(
560 (state: RootState, props: { uuid: string }) => {
561 const link = getResource<LinkResource>(props.uuid)(state.resources);
562 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
564 return resource || { email: '' };
567 export const ResourceLinkTailUsername = connect(
568 (state: RootState, props: { uuid: string }) => {
569 const link = getResource<LinkResource>(props.uuid)(state.resources);
570 const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
572 return resource || { username: '' };
575 const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
576 return <Typography noWrap>
577 {formatPermissionLevel(link.name as PermissionLevel)}
579 <IconButton data-cy="edit-permission-button" onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
587 export const ResourceLinkHeadPermissionLevel = connect(
588 (state: RootState, props: { uuid: string }) => {
589 const link = getResource<LinkResource>(props.uuid)(state.resources);
590 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
593 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
594 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
596 })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
597 renderPermissionLevel(props.dispatch, props.link, props.canManage));
599 export const ResourceLinkTailPermissionLevel = connect(
600 (state: RootState, props: { uuid: string }) => {
601 const link = getResource<LinkResource>(props.uuid)(state.resources);
602 const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
605 link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
606 canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
608 })((props: { link: LinkResource, canManage: boolean } & DispatchProp<any>) =>
609 renderPermissionLevel(props.dispatch, props.link, props.canManage));
611 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
612 const headResource = getResource<Resource>(link.headUuid)(state.resources);
613 // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
614 const userUuid = getUserUuid(state);
616 if (headResource && headResource.kind === ResourceKind.GROUP) {
617 return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
625 const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
629 <Tooltip title="Run process">
630 <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
638 export const ResourceRunProcess = connect(
639 (state: RootState, props: { uuid: string }) => {
640 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
642 uuid: resource ? resource.uuid : ''
644 })((props: { uuid: string } & DispatchProp<any>) =>
645 resourceRunProcess(props.dispatch, props.uuid));
647 const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
648 if (ownerUuid === getPublicUuid(uuidPrefix)) {
649 return renderStatus(WorkflowStatus.PUBLIC);
651 return renderStatus(WorkflowStatus.PRIVATE);
655 const renderStatus = (status: string) =>
656 <Typography noWrap style={{ width: '60px' }}>{status}</Typography>;
658 export const ResourceWorkflowStatus = connect(
659 (state: RootState, props: { uuid: string }) => {
660 const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
661 const uuidPrefix = getUuidPrefix(state);
663 ownerUuid: resource ? resource.ownerUuid : '',
666 })((props: { ownerUuid?: string, uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
668 const renderProcessState = (processState: string) => <Typography>{processState}</Typography>
670 export const ResourceProcessState = connect(
671 (state: RootState, props: { uuid: string }) => {
672 const resource = getResource<ContainerRequestResource>(props.uuid)(state.resources);
673 return { state: resource?.state ? resource.state: '' };
674 })((props: { state: string }) => renderProcessState(props.state));
676 export const ResourceCreatedAtDate = connect(
677 (state: RootState, props: { uuid: string }) => {
678 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
679 return { date: resource ? resource.createdAt : '' };
680 })((props: { date: string }) => renderDate(props.date));
682 export const ResourceLastModifiedDate = connect(
683 (state: RootState, props: { uuid: string }) => {
684 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
685 return { date: resource ? resource.modifiedAt : '' };
686 })((props: { date: string }) => renderDate(props.date));
688 export const ResourceTrashDate = connect(
689 (state: RootState, props: { uuid: string }) => {
690 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
691 return { date: resource ? resource.trashAt : '' };
692 })((props: { date: string }) => renderDate(props.date));
694 export const ResourceDeleteDate = connect(
695 (state: RootState, props: { uuid: string }) => {
696 const resource = getResource<TrashableResource>(props.uuid)(state.resources);
697 return { date: resource ? resource.deleteAt : '' };
698 })((props: { date: string }) => renderDate(props.date));
700 export const renderFileSize = (fileSize?: number) =>
701 <Typography noWrap style={{ minWidth: '45px' }}>
702 {formatFileSize(fileSize)}
705 export const ResourceFileSize = connect(
706 (state: RootState, props: { uuid: string }) => {
707 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
709 if (resource && resource.kind !== ResourceKind.COLLECTION) {
710 return { fileSize: '' };
713 return { fileSize: resource ? resource.fileSizeTotal : 0 };
714 })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
716 const renderOwner = (owner: string) =>
721 export const ResourceOwner = connect(
722 (state: RootState, props: { uuid: string }) => {
723 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
724 return { owner: resource ? resource.ownerUuid : '' };
725 })((props: { owner: string }) => renderOwner(props.owner));
727 export const ResourceOwnerName = connect(
728 (state: RootState, props: { uuid: string }) => {
729 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
730 const ownerNameState = state.ownerName;
731 const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
732 return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
733 })((props: { owner: string }) => renderOwner(props.owner));
735 export const ResourceUUID = connect(
736 (state: RootState, props: { uuid: string }) => {
737 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
738 return { uuid: resource ? resource.uuid : '' };
739 })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
741 const renderPortableDataHash = (portableDataHash:string | null) =>
743 {portableDataHash ? <>{portableDataHash}
744 <CopyToClipboardSnackbar value={portableDataHash} /></> : '-' }
747 export const ResourcePortableDataHash = connect(
748 (state: RootState, props: { uuid: string }) => {
749 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
750 // console.log('COLLECTION_RESOIRCE', resource)
751 return { portableDataHash: resource ? resource.portableDataHash : '' };
752 })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
754 const renderVersion = (version: number) =>{
755 return <Typography>{version ?? '-'}</Typography>
758 export const ResourceVersion = connect(
759 (state: RootState, props: { uuid: string }) => {
760 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
761 return { version: resource ? resource.version: '' };
762 })((props: { version: number }) => renderVersion(props.version));
764 const renderDescription = (description: string)=>{
765 const truncatedDescription = description ? description.slice(0, 18) + '...' : '-'
766 return <Typography title={description}>{truncatedDescription}</Typography>;
769 const renderFileCount = (fileCount: number) =>{
770 return <Typography>{fileCount ?? '-'}</Typography>
773 export const ResourceFileCount = connect(
774 (state: RootState, props: { uuid: string }) => {
775 const resource = getResource<CollectionResource>(props.uuid)(state.resources);
776 return { fileCount: resource ? resource.fileCount: '' };
777 })((props: { fileCount: number }) => renderFileCount(props.fileCount));
779 export const ResourceDescription = connect(
780 (state: RootState, props: { uuid: string }) => {
781 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
782 //testing---------------
783 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."
784 if (resource && !resource.description && resource.kind === ResourceKind.PROCESS) resource.description = containerRequestDescription
785 //testing---------------
786 return { description: resource ? resource.description : '' };
787 })((props: { description: string }) => renderDescription(props.description));
791 (state: RootState, props: { uuid: string }) => {
792 let userFullname = '';
793 const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
796 userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
799 return { uuid: props.uuid, userFullname };
802 const ownerFromResourceId =
804 connect((state: RootState, props: { uuid: string }) => {
805 const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
806 return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
811 const _resourceWithName =
812 withStyles({}, { withTheme: true })
813 ((props: { uuid: string, userFullname: string, dispatch: Dispatch, theme: ArvadosTheme }) => {
814 const { uuid, userFullname, dispatch, theme } = props;
816 if (userFullname === '') {
817 dispatch<any>(loadResource(uuid, false));
818 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
823 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
824 {userFullname} ({uuid})
828 export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
830 export const ResourceWithName = userFromID(_resourceWithName);
832 export const UserNameFromID =
834 (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
835 const { uuid, userFullname, dispatch } = props;
837 if (userFullname === '') {
838 dispatch<any>(loadResource(uuid, false));
841 {userFullname ? userFullname : uuid}
845 export const ResponsiblePerson =
848 (state: RootState, props: { uuid: string, parentRef: HTMLElement | null }) => {
849 let responsiblePersonName: string = '';
850 let responsiblePersonUUID: string = '';
851 let responsiblePersonProperty: string = '';
853 if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
855 const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
857 while (!responsiblePersonProperty && keys[index]) {
858 const key = keys[index];
859 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
860 responsiblePersonProperty = key;
866 let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
868 while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
869 responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
870 resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
873 if (resource && resource.kind === ResourceKind.USER) {
874 responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
877 return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
879 withStyles({}, { withTheme: true }))
880 ((props: { uuid: string | null, responsiblePersonName: string, parentRef: HTMLElement | null, theme: ArvadosTheme }) => {
881 const { uuid, responsiblePersonName, parentRef, theme } = props;
883 if (!uuid && parentRef) {
884 parentRef.style.display = 'none';
886 } else if (parentRef) {
887 parentRef.style.display = 'block';
890 if (!responsiblePersonName) {
891 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
896 return <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
897 {responsiblePersonName} ({uuid})
901 const renderType = (type: string, subtype: string) =>
903 {resourceLabel(type, subtype)}
906 export const ResourceType = connect(
907 (state: RootState, props: { uuid: string }) => {
908 const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
909 return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
910 })((props: { type: string, subtype: string }) => renderType(props.type, props.subtype));
912 export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
913 return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
914 })((props: { resource: GroupContentsResource }) =>
915 (props.resource && props.resource.kind === ResourceKind.COLLECTION)
916 ? <CollectionStatus uuid={props.resource.uuid} />
917 : <ProcessStatus uuid={props.resource.uuid} />
920 export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
921 return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
922 })((props: { collection: CollectionResource }) =>
923 (props.collection.uuid !== props.collection.currentVersionUuid)
924 ? <Typography>version {props.collection.version}</Typography>
925 : <Typography>head version</Typography>
928 export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
930 collection: getResource<CollectionResource>(props.uuid)(state.resources),
932 className: props.className,
934 })((props: { collection: CollectionResource, uuid: string, className?: string }) =>
935 <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
938 export const ProcessStatus = compose(
939 connect((state: RootState, props: { uuid: string }) => {
940 return { process: getProcess(props.uuid)(state.resources) };
942 withStyles({}, { withTheme: true }))
943 ((props: { process?: Process, theme: ArvadosTheme }) =>
945 ? <Chip label={getProcessStatus(props.process)}
947 height: props.theme.spacing.unit * 3,
948 width: props.theme.spacing.unit * 12,
949 backgroundColor: getProcessStatusColor(
950 getProcessStatus(props.process), props.theme),
951 color: props.theme.palette.common.white,
952 fontSize: '0.875rem',
953 borderRadius: props.theme.spacing.unit * 0.625,
956 : <Typography>-</Typography>
959 export const ProcessStartDate = connect(
960 (state: RootState, props: { uuid: string }) => {
961 const process = getProcess(props.uuid)(state.resources);
962 return { date: (process && process.container) ? process.container.startedAt : '' };
963 })((props: { date: string }) => renderDate(props.date));
965 export const renderRunTime = (time: number) =>
966 <Typography noWrap style={{ minWidth: '45px' }}>
967 {formatTime(time, true)}
970 interface ContainerRunTimeProps {
974 interface ContainerRunTimeState {
978 export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
979 return { process: getProcess(props.uuid)(state.resources) };
980 })(class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
983 constructor(props: ContainerRunTimeProps) {
985 this.state = { runtime: this.getRuntime() };
989 return this.props.process ? getProcessRuntime(this.props.process) : 0;
993 this.setState({ runtime: this.getRuntime() });
996 componentDidMount() {
997 this.timer = setInterval(this.updateRuntime.bind(this), 5000);
1000 componentWillUnmount() {
1001 clearInterval(this.timer);
1005 return renderRunTime(this.state.runtime);