21128: Merge commit 'adbbc9e3c7a36d39b30f403555ee5889e32adcc0' into 21128-toolbar...
authorLisa Knox <lisaknox83@gmail.com>
Fri, 5 Jan 2024 15:47:49 +0000 (10:47 -0500)
committerLisa Knox <lisaknox83@gmail.com>
Fri, 5 Jan 2024 15:47:49 +0000 (10:47 -0500)
Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>

46 files changed:
1  2 
services/workbench2/.yarn/releases/yarn-3.2.0.cjs
services/workbench2/cypress/integration/collection.spec.js
services/workbench2/cypress/integration/multiselect-toolbar.spec.js
services/workbench2/cypress/integration/process.spec.js
services/workbench2/cypress/integration/project.spec.js
services/workbench2/cypress/integration/workflow.spec.js
services/workbench2/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx
services/workbench2/src/components/data-explorer/data-explorer.tsx
services/workbench2/src/components/data-table/data-table.tsx
services/workbench2/src/components/icon/icon.tsx
services/workbench2/src/components/multiselect-toolbar/MultiselectToolbar.tsx
services/workbench2/src/components/multiselect-toolbar/ms-kind-action-differentiator.ts
services/workbench2/src/components/multiselect-toolbar/ms-toolbar-action-filters.ts
services/workbench2/src/store/collections/collection-info-actions.ts
services/workbench2/src/store/favorites/favorites-actions.ts
services/workbench2/src/store/multiselect/multiselect-actions.tsx
services/workbench2/src/store/multiselect/multiselect-reducer.tsx
services/workbench2/src/store/process-panel/process-panel-actions.ts
services/workbench2/src/store/project-panel/project-panel-middleware-service.ts
services/workbench2/src/store/projects/project-lock-actions.ts
services/workbench2/src/store/public-favorites/public-favorites-actions.ts
services/workbench2/src/store/trash-panel/trash-panel-middleware-service.ts
services/workbench2/src/store/trash/trash-actions.ts
services/workbench2/src/store/workbench/workbench-actions.ts
services/workbench2/src/views-components/context-menu/action-sets/process-resource-action-set.ts
services/workbench2/src/views-components/context-menu/context-menu.tsx
services/workbench2/src/views-components/data-explorer/data-explorer.tsx
services/workbench2/src/views-components/data-explorer/renderers.tsx
services/workbench2/src/views-components/details-panel/details-panel.tsx
services/workbench2/src/views-components/multiselect-toolbar/ms-collection-action-set.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-menu-actions.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-process-action-set.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-project-action-set.ts
services/workbench2/src/views-components/multiselect-toolbar/ms-workflow-action-set.ts
services/workbench2/src/views/all-processes-panel/all-processes-panel.tsx
services/workbench2/src/views/collection-panel/collection-panel.tsx
services/workbench2/src/views/favorite-panel/favorite-panel.tsx
services/workbench2/src/views/process-panel/process-details-attributes.tsx
services/workbench2/src/views/project-panel/project-panel.tsx
services/workbench2/src/views/public-favorites-panel/public-favorites-panel.tsx
services/workbench2/src/views/search-results-panel/search-results-panel.tsx
services/workbench2/src/views/shared-with-me-panel/shared-with-me-panel.tsx
services/workbench2/src/views/subprocess-panel/subprocess-panel-root.tsx
services/workbench2/src/views/subprocess-panel/subprocess-panel.tsx
services/workbench2/src/views/trash-panel/trash-panel.tsx
services/workbench2/src/views/workbench/workbench.tsx

index 0000000000000000000000000000000000000000,ef503f7ef628874af301821f23a1ffc1385e39e0..ef503f7ef628874af301821f23a1ffc1385e39e0
mode 000000,100644..100644
--- /dev/null
index e29930a4b9974b84a54370f7c61b2ee726d27b41,0000000000000000000000000000000000000000..8ec4c59b8781e2b4dc63d04b62bff4f955a815a9
mode 100644,000000..100644
--- /dev/null
@@@ -1,269 -1,0 +1,269 @@@
- export const FreezeIcon = (props: any) => (
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { Badge, SvgIcon, Tooltip } from "@material-ui/core";
 +import Add from "@material-ui/icons/Add";
 +import ArrowBack from "@material-ui/icons/ArrowBack";
 +import ArrowDropDown from "@material-ui/icons/ArrowDropDown";
 +import Build from "@material-ui/icons/Build";
 +import Cached from "@material-ui/icons/Cached";
 +import DescriptionIcon from "@material-ui/icons/Description";
 +import ChevronLeft from "@material-ui/icons/ChevronLeft";
 +import CloudUpload from "@material-ui/icons/CloudUpload";
 +import Code from "@material-ui/icons/Code";
 +import Create from "@material-ui/icons/Create";
 +import ImportContacts from "@material-ui/icons/ImportContacts";
 +import ChevronRight from "@material-ui/icons/ChevronRight";
 +import Close from "@material-ui/icons/Close";
 +import ContentCopy from "@material-ui/icons/FileCopyOutlined";
 +import CreateNewFolder from "@material-ui/icons/CreateNewFolder";
 +import Delete from "@material-ui/icons/Delete";
 +import DeviceHub from "@material-ui/icons/DeviceHub";
 +import Edit from "@material-ui/icons/Edit";
 +import ErrorRoundedIcon from "@material-ui/icons/ErrorRounded";
 +import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
 +import FlipToFront from "@material-ui/icons/FlipToFront";
 +import Folder from "@material-ui/icons/Folder";
 +import FolderShared from "@material-ui/icons/FolderShared";
 +import Pageview from "@material-ui/icons/Pageview";
 +import GetApp from "@material-ui/icons/GetApp";
 +import Help from "@material-ui/icons/Help";
 +import HelpOutline from "@material-ui/icons/HelpOutline";
 +import History from "@material-ui/icons/History";
 +import Inbox from "@material-ui/icons/Inbox";
 +import Memory from "@material-ui/icons/Memory";
 +import MoveToInbox from "@material-ui/icons/MoveToInbox";
 +import Info from "@material-ui/icons/Info";
 +import Input from "@material-ui/icons/Input";
 +import InsertDriveFile from "@material-ui/icons/InsertDriveFile";
 +import LastPage from "@material-ui/icons/LastPage";
 +import LibraryBooks from "@material-ui/icons/LibraryBooks";
 +import ListAlt from "@material-ui/icons/ListAlt";
 +import Menu from "@material-ui/icons/Menu";
 +import MoreVert from "@material-ui/icons/MoreVert";
 +import MoreHoriz from "@material-ui/icons/MoreHoriz";
 +import Mail from "@material-ui/icons/Mail";
 +import Notifications from "@material-ui/icons/Notifications";
 +import OpenInNew from "@material-ui/icons/OpenInNew";
 +import People from "@material-ui/icons/People";
 +import Person from "@material-ui/icons/Person";
 +import PersonAdd from "@material-ui/icons/PersonAdd";
 +import PlayArrow from "@material-ui/icons/PlayArrow";
 +import Public from "@material-ui/icons/Public";
 +import RateReview from "@material-ui/icons/RateReview";
 +import RestoreFromTrash from "@material-ui/icons/History";
 +import Search from "@material-ui/icons/Search";
 +import SettingsApplications from "@material-ui/icons/SettingsApplications";
 +import SettingsEthernet from "@material-ui/icons/SettingsEthernet";
 +import Settings from "@material-ui/icons/Settings";
 +import Star from "@material-ui/icons/Star";
 +import StarBorder from "@material-ui/icons/StarBorder";
 +import Warning from "@material-ui/icons/Warning";
 +import VpnKey from "@material-ui/icons/VpnKey";
 +import LinkOutlined from "@material-ui/icons/LinkOutlined";
 +import RemoveRedEye from "@material-ui/icons/RemoveRedEye";
 +import Computer from "@material-ui/icons/Computer";
 +import WrapText from "@material-ui/icons/WrapText";
 +import TextIncrease from "@material-ui/icons/ZoomIn";
 +import TextDecrease from "@material-ui/icons/ZoomOut";
 +import FullscreenSharp from "@material-ui/icons/FullscreenSharp";
 +import FullscreenExitSharp from "@material-ui/icons/FullscreenExitSharp";
 +import ExitToApp from "@material-ui/icons/ExitToApp";
 +import CheckCircleOutline from "@material-ui/icons/CheckCircleOutline";
 +import RemoveCircleOutline from "@material-ui/icons/RemoveCircleOutline";
 +import NotInterested from "@material-ui/icons/NotInterested";
 +import Image from "@material-ui/icons/Image";
 +import Stop from "@material-ui/icons/Stop";
 +import FileCopy from "@material-ui/icons/FileCopy";
 +
 +// Import FontAwesome icons
 +import { library } from "@fortawesome/fontawesome-svg-core";
 +import { faPencilAlt, faSlash, faUsers, faEllipsisH } from "@fortawesome/free-solid-svg-icons";
 +import { FormatAlignLeft } from "@material-ui/icons";
 +library.add(faPencilAlt, faSlash, faUsers, faEllipsisH);
 +
- export const UnfreezeIcon = (props: any) => (
++export const FreezeIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z" />
 +    </SvgIcon>
 +);
 +
++export const UnfreezeIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M11 5.12L9.29 3.41L10.71 2L12 3.29L13.29 2L14.71 3.41L13 5.12V7.38L15.45 8.82L17.45 7.69L18.07 5.36L20 5.88L19.54 7.65L21.31 8.12L20.79 10.05L18.46 9.43L16.46 10.56V13.26L14.5 11.3V10.56L12.74 9.54L10.73 7.53L11 7.38V5.12M18.46 14.57L16.87 13.67L19.55 16.35L21.3 15.88L20.79 13.95L18.46 14.57M13 16.62V18.88L14.7 20.59L13.29 22L12 20.71L10.71 22L9.29 20.59L11 18.88V16.62L8.55 15.18L6.55 16.31L5.93 18.64L4 18.12L4.47 16.36L2.7 15.89L3.22 13.96L5.55 14.58L7.55 13.45V10.56L5.55 9.43L3.22 10.05L2.7 8.12L4.47 7.65L4 5.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73L14.1 16L13 16.62M12 14.89L12.63 14.5L9.5 11.39V13.44L12 14.89Z" />
 +    </SvgIcon>
 +);
 +
 +export const PendingIcon = (props: any) => (
 +    <span {...props}>
 +        <span className="fas fa-ellipsis-h" />
 +    </span>
 +);
 +
 +export const ReadOnlyIcon = (props: any) => (
 +    <span {...props}>
 +        <div className="fa-layers fa-1x fa-fw">
 +            <span
 +                className="fas fa-slash"
 +                data-fa-mask="fas fa-pencil-alt"
 +                data-fa-transform="down-1.5"
 +            />
 +            <span className="fas fa-slash" />
 +        </div>
 +    </span>
 +);
 +
 +export const GroupsIcon = (props: any) => (
 +    <span {...props}>
 +        <span className="fas fa-users" />
 +    </span>
 +);
 +
 +export const CollectionOldVersionIcon = (props: any) => (
 +    <Tooltip title="Old version">
 +        <Badge badgeContent={<History fontSize="small" />}>
 +            <CollectionIcon {...props} />
 +        </Badge>
 +    </Tooltip>
 +);
 +
 +// https://materialdesignicons.com/icon/image-off
 +export const ImageOffIcon = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M21 17.2L6.8 3H19C20.1 3 21 3.9 21 5V17.2M20.7 22L19.7 21H5C3.9 21 3 20.1 3 19V4.3L2 3.3L3.3 2L22 20.7L20.7 22M16.8 18L12.9 14.1L11 16.5L8.5 13.5L5 18H16.8Z" />
 +    </SvgIcon>
 +);
 +
 +// https://materialdesignicons.com/icon/inbox-arrow-up
 +export const OutputIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M14,14H10V11H8L12,7L16,11H14V14M16,11M5,15V5H19V15H15A3,3 0 0,1 12,18A3,3 0 0,1 9,15H5M19,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3" />
 +    </SvgIcon>
 +);
 +
 +// https://pictogrammers.com/library/mdi/icon/file-move/
 +export const FileMoveIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M14,17H18V14L23,18.5L18,23V20H14V17M13,9H18.5L13,3.5V9M6,2H14L20,8V12.34C19.37,12.12 18.7,12 18,12A6,6 0 0,0 12,18C12,19.54 12.58,20.94 13.53,22H6C4.89,22 4,21.1 4,20V4A2,2 0 0,1 6,2Z" />
 +    </SvgIcon>
 +);
 +
 +// https://pictogrammers.com/library/mdi/icon/checkbox-multiple-outline/
 +export const CheckboxMultipleOutline: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,16H8V4H20V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16M18.53,8.06L17.47,7L12.59,11.88L10.47,9.76L9.41,10.82L12.59,14L18.53,8.06Z" />
 +    </SvgIcon>
 +);
 +
 +// https://pictogrammers.com/library/mdi/icon/checkbox-multiple-blank-outline/
 +export const CheckboxMultipleBlankOutline: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20,16V4H8V16H20M22,16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H20A2,2 0 0,1 22,4V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z" />
 +    </SvgIcon>
 +);
 +
 +//https://pictogrammers.com/library/mdi/icon/console/
 +export const TerminalIcon: IconType = (props: any) => (
 +    <SvgIcon {...props}>
 +        <path d="M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z" />
 +    </SvgIcon>
 +)
 +
 +export type IconType = React.SFC<{ className?: string; style?: object }>;
 +
 +export const AddIcon: IconType = props => <Add {...props} />;
 +export const AddFavoriteIcon: IconType = props => <StarBorder {...props} />;
 +export const AdminMenuIcon: IconType = props => <Build {...props} />;
 +export const AdvancedIcon: IconType = props => <SettingsApplications {...props} />;
 +export const AttributesIcon: IconType = props => <ListAlt {...props} />;
 +export const BackIcon: IconType = props => <ArrowBack {...props} />;
 +export const CustomizeTableIcon: IconType = props => <Menu {...props} />;
 +export const CommandIcon: IconType = props => <LastPage {...props} />;
 +export const CopyIcon: IconType = props => <ContentCopy {...props} />;
 +export const FileCopyIcon: IconType = props => <FileCopy {...props} />;
 +export const CollectionIcon: IconType = props => <LibraryBooks {...props} />;
 +export const CloseIcon: IconType = props => <Close {...props} />;
 +export const CloudUploadIcon: IconType = props => <CloudUpload {...props} />;
 +export const DefaultIcon: IconType = props => <RateReview {...props} />;
 +export const DetailsIcon: IconType = props => <Info {...props} />;
 +export const DirectoryIcon: IconType = props => <Folder {...props} />;
 +export const DownloadIcon: IconType = props => <GetApp {...props} />;
 +export const EditSavedQueryIcon: IconType = props => <Create {...props} />;
 +export const ExpandIcon: IconType = props => <ExpandMoreIcon {...props} />;
 +export const ErrorIcon: IconType = props => (
 +    <ErrorRoundedIcon
 +        style={{ color: "#ff0000" }}
 +        {...props}
 +    />
 +);
 +export const FavoriteIcon: IconType = props => <Star {...props} />;
 +export const FileIcon: IconType = props => <DescriptionIcon {...props} />;
 +export const HelpIcon: IconType = props => <Help {...props} />;
 +export const HelpOutlineIcon: IconType = props => <HelpOutline {...props} />;
 +export const ImportContactsIcon: IconType = props => <ImportContacts {...props} />;
 +export const InfoIcon: IconType = props => <Info {...props} />;
 +export const FileInputIcon: IconType = props => <InsertDriveFile {...props} />;
 +export const KeyIcon: IconType = props => <VpnKey {...props} />;
 +export const LogIcon: IconType = props => <SettingsEthernet {...props} />;
 +export const MailIcon: IconType = props => <Mail {...props} />;
 +export const MaximizeIcon: IconType = props => <FullscreenSharp {...props} />;
 +export const ResourceIcon: IconType = props => <Memory {...props} />;
 +export const UnMaximizeIcon: IconType = props => <FullscreenExitSharp {...props} />;
 +export const MoreVerticalIcon: IconType = props => <MoreVert {...props} />;
 +export const MoreHorizontalIcon: IconType = props => <MoreHoriz {...props} />;
 +export const MoveToIcon: IconType = props => <Input {...props} />;
 +export const NewProjectIcon: IconType = props => <CreateNewFolder {...props} />;
 +export const NotificationIcon: IconType = props => <Notifications {...props} />;
 +export const OpenIcon: IconType = props => <OpenInNew {...props} />;
 +export const InputIcon: IconType = props => <MoveToInbox {...props} />;
 +export const PaginationDownIcon: IconType = props => <ArrowDropDown {...props} />;
 +export const PaginationLeftArrowIcon: IconType = props => <ChevronLeft {...props} />;
 +export const PaginationRightArrowIcon: IconType = props => <ChevronRight {...props} />;
 +export const ProcessIcon: IconType = props => <Settings {...props} />;
 +export const ProjectIcon: IconType = props => <Folder {...props} />;
 +export const FilterGroupIcon: IconType = props => <Pageview {...props} />;
 +export const ProjectsIcon: IconType = props => <Inbox {...props} />;
 +export const ProvenanceGraphIcon: IconType = props => <DeviceHub {...props} />;
 +export const RemoveIcon: IconType = props => <Delete {...props} />;
 +export const RemoveFavoriteIcon: IconType = props => <Star {...props} />;
 +export const PublicFavoriteIcon: IconType = props => <Public {...props} />;
 +export const RenameIcon: IconType = props => <Edit {...props} />;
 +export const RestoreVersionIcon: IconType = props => <FlipToFront {...props} />;
 +export const RestoreFromTrashIcon: IconType = props => <RestoreFromTrash {...props} />;
 +export const ReRunProcessIcon: IconType = props => <Cached {...props} />;
 +export const SearchIcon: IconType = props => <Search {...props} />;
 +export const ShareIcon: IconType = props => <PersonAdd {...props} />;
 +export const ShareMeIcon: IconType = props => <People {...props} />;
 +export const SidePanelRightArrowIcon: IconType = props => <PlayArrow {...props} />;
 +export const TrashIcon: IconType = props => <Delete {...props} />;
 +export const UserPanelIcon: IconType = props => <Person {...props} />;
 +export const UsedByIcon: IconType = props => <Folder {...props} />;
 +export const WorkflowIcon: IconType = props => <Code {...props} />;
 +export const WarningIcon: IconType = props => (
 +    <Warning
 +        style={{ color: "#fbc02d", height: "30px", width: "30px" }}
 +        {...props}
 +    />
 +);
 +export const Link: IconType = props => <LinkOutlined {...props} />;
 +export const FolderSharedIcon: IconType = props => <FolderShared {...props} />;
 +export const CanReadIcon: IconType = props => <RemoveRedEye {...props} />;
 +export const CanWriteIcon: IconType = props => <Edit {...props} />;
 +export const CanManageIcon: IconType = props => <Computer {...props} />;
 +export const AddUserIcon: IconType = props => <PersonAdd {...props} />;
 +export const WordWrapOnIcon: IconType = props => <WrapText {...props} />;
 +export const WordWrapOffIcon: IconType = props => <FormatAlignLeft {...props} />;
 +export const TextIncreaseIcon: IconType = props => <TextIncrease {...props} />;
 +export const TextDecreaseIcon: IconType = props => <TextDecrease {...props} />;
 +export const DeactivateUserIcon: IconType = props => <NotInterested {...props} />;
 +export const LoginAsIcon: IconType = props => <ExitToApp {...props} />;
 +export const ActiveIcon: IconType = props => <CheckCircleOutline {...props} />;
 +export const SetupIcon: IconType = props => <RemoveCircleOutline {...props} />;
 +export const InactiveIcon: IconType = props => <NotInterested {...props} />;
 +export const ImageIcon: IconType = props => <Image {...props} />;
 +export const StartIcon: IconType = props => <PlayArrow {...props} />;
 +export const StopIcon: IconType = props => <Stop {...props} />;
 +export const SelectAllIcon: IconType = props => <CheckboxMultipleOutline {...props} />;
 +export const SelectNoneIcon: IconType = props => <CheckboxMultipleBlankOutline {...props} />;
index 7795e52d1a71be15bfc38c823945b7cb050667a8,0000000000000000000000000000000000000000..b286186aba5eb71a9803fe096e9022c7ecfbcba2
mode 100644,000000..100644
--- /dev/null
@@@ -1,880 -1,0 +1,880 @@@
-                             message: e.message,
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import { Dispatch } from "redux";
 +import { RootState } from "store/store";
 +import { getUserUuid } from "common/getuser";
 +import { loadDetailsPanel } from "store/details-panel/details-panel-action";
 +import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 +import { favoritePanelActions, loadFavoritePanel } from "store/favorite-panel/favorite-panel-action";
 +import { getProjectPanelCurrentUuid, setIsProjectPanelTrashed } from "store/project-panel/project-panel-action";
 +import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
 +import {
 +    activateSidePanelTreeItem,
 +    initSidePanelTree,
 +    loadSidePanelTreeProjects,
 +    SidePanelTreeCategory,
 +    SIDE_PANEL_TREE, 
 +} from "store/side-panel-tree/side-panel-tree-actions";
 +import { updateResources } from "store/resources/resources-actions";
 +import { projectPanelColumns } from "views/project-panel/project-panel";
 +import { favoritePanelColumns } from "views/favorite-panel/favorite-panel";
 +import { matchRootRoute } from "routes/routes";
 +import {
 +    setGroupDetailsBreadcrumbs,
 +    setGroupsBreadcrumbs,
 +    setProcessBreadcrumbs,
 +    setSharedWithMeBreadcrumbs,
 +    setSidePanelBreadcrumbs,
 +    setTrashBreadcrumbs,
 +    setUsersBreadcrumbs,
 +    setMyAccountBreadcrumbs,
 +    setUserProfileBreadcrumbs,
 +    setInstanceTypesBreadcrumbs,
 +    setVirtualMachinesBreadcrumbs,
 +    setVirtualMachinesAdminBreadcrumbs,
 +    setRepositoriesBreadcrumbs,
 +} from "store/breadcrumbs/breadcrumbs-actions";
 +import { navigateTo, navigateToRootProject } from "store/navigation/navigation-action";
 +import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
 +import { ServiceRepository } from "services/services";
 +import { getResource } from "store/resources/resources";
 +import * as projectCreateActions from "store/projects/project-create-actions";
 +import * as projectMoveActions from "store/projects/project-move-actions";
 +import * as projectUpdateActions from "store/projects/project-update-actions";
 +import * as collectionCreateActions from "store/collections/collection-create-actions";
 +import * as collectionCopyActions from "store/collections/collection-copy-actions";
 +import * as collectionMoveActions from "store/collections/collection-move-actions";
 +import * as processesActions from "store/processes/processes-actions";
 +import * as processMoveActions from "store/processes/process-move-actions";
 +import * as processUpdateActions from "store/processes/process-update-actions";
 +import * as processCopyActions from "store/processes/process-copy-actions";
 +import { trashPanelColumns } from "views/trash-panel/trash-panel";
 +import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
 +import { loadProcessPanel } from "store/process-panel/process-panel-actions";
 +import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
 +import { sharedWithMePanelColumns } from "views/shared-with-me-panel/shared-with-me-panel";
 +import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
 +import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
 +import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
 +import { loadLinkAccountPanel, linkAccountPanelActions } from "store/link-account-panel/link-account-panel-actions";
 +import { loadSiteManagerPanel } from "store/auth/auth-action-session";
 +import { workflowPanelColumns } from "views/workflow-panel/workflow-panel-view";
 +import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 +import { getProgressIndicator } from "store/progress-indicator/progress-indicator-reducer";
 +import { extractUuidKind, Resource, ResourceKind } from "models/resource";
 +import { FilterBuilder } from "services/api/filter-builder";
 +import { GroupContentsResource } from "services/groups-service/groups-service";
 +import { MatchCases, ofType, unionize, UnionOf } from "common/unionize";
 +import { loadRunProcessPanel } from "store/run-process-panel/run-process-panel-actions";
 +import { collectionPanelActions, loadCollectionPanel } from "store/collection-panel/collection-panel-action";
 +import { CollectionResource } from "models/collection";
 +import { WorkflowResource } from "models/workflow";
 +import { loadSearchResultsPanel, searchResultsPanelActions } from "store/search-results-panel/search-results-panel-actions";
 +import { searchResultsPanelColumns } from "views/search-results-panel/search-results-panel-view";
 +import { loadVirtualMachinesPanel } from "store/virtual-machines/virtual-machines-actions";
 +import { loadRepositoriesPanel } from "store/repositories/repositories-actions";
 +import { loadKeepServicesPanel } from "store/keep-services/keep-services-actions";
 +import { loadUsersPanel, userBindedActions } from "store/users/users-actions";
 +import * as userProfilePanelActions from "store/user-profile/user-profile-actions";
 +import { linkPanelActions, loadLinkPanel } from "store/link-panel/link-panel-actions";
 +import { linkPanelColumns } from "views/link-panel/link-panel-root";
 +import { userPanelColumns } from "views/user-panel/user-panel";
 +import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from "store/api-client-authorizations/api-client-authorizations-actions";
 +import { apiClientAuthorizationPanelColumns } from "views/api-client-authorization-panel/api-client-authorization-panel-root";
 +import * as groupPanelActions from "store/groups-panel/groups-panel-actions";
 +import { groupsPanelColumns } from "views/groups-panel/groups-panel";
 +import * as groupDetailsPanelActions from "store/group-details-panel/group-details-panel-actions";
 +import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from "views/group-details-panel/group-details-panel";
 +import { DataTableFetchMode } from "components/data-table/data-table";
 +import { loadPublicFavoritePanel, publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
 +import { publicFavoritePanelColumns } from "views/public-favorites-panel/public-favorites-panel";
 +import {
 +    loadCollectionsContentAddressPanel,
 +    collectionsContentAddressActions,
 +} from "store/collections-content-address-panel/collections-content-address-panel-actions";
 +import { collectionContentAddressPanelColumns } from "views/collection-content-address-panel/collection-content-address-panel";
 +import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-actions";
 +import { subprocessPanelColumns } from "views/subprocess-panel/subprocess-panel-root";
 +import { loadAllProcessesPanel, allProcessesPanelActions } from "../all-processes-panel/all-processes-panel-action";
 +import { allProcessesPanelColumns } from "views/all-processes-panel/all-processes-panel";
 +import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
 +import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
 +import { multiselectActions } from "store/multiselect/multiselect-actions";
 +import { treePickerActions } from "store/tree-picker/tree-picker-actions";
 +
 +export const WORKBENCH_LOADING_SCREEN = "workbenchLoadingScreen";
 +
 +export const isWorkbenchLoading = (state: RootState) => {
 +    const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
 +    return progress ? progress.working : false;
 +};
 +
 +export const handleFirstTimeLoad = (action: any) => async (dispatch: Dispatch<any>, getState: () => RootState) => {
 +    try {
 +        await dispatch(action);
 +    } catch (e) {
 +        snackbarActions.OPEN_SNACKBAR({
 +            message: "Error " + e,
 +            hideDuration: 8000,
 +            kind: SnackbarKind.WARNING,
 +        })
 +    } finally {
 +        if (isWorkbenchLoading(getState())) {
 +            dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
 +        }
 +    }
 +};
 +
 +export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
 +    const { auth, router } = getState();
 +    const { user } = auth;
 +    if (user) {
 +        dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
 +        dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
 +        dispatch(
 +            allProcessesPanelActions.SET_COLUMNS({
 +                columns: allProcessesPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            publicFavoritePanelActions.SET_COLUMNS({
 +                columns: publicFavoritePanelColumns,
 +            })
 +        );
 +        dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
 +        dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: sharedWithMePanelColumns }));
 +        dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
 +        dispatch(
 +            searchResultsPanelActions.SET_FETCH_MODE({
 +                fetchMode: DataTableFetchMode.INFINITE,
 +            })
 +        );
 +        dispatch(
 +            searchResultsPanelActions.SET_COLUMNS({
 +                columns: searchResultsPanelColumns,
 +            })
 +        );
 +        dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
 +        dispatch(
 +            groupPanelActions.GroupsPanelActions.SET_COLUMNS({
 +                columns: groupsPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
 +                columns: groupDetailsMembersPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
 +                columns: groupDetailsPermissionsPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
 +                columns: userProfileGroupsColumns,
 +            })
 +        );
 +        dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
 +        dispatch(
 +            apiClientAuthorizationsActions.SET_COLUMNS({
 +                columns: apiClientAuthorizationPanelColumns,
 +            })
 +        );
 +        dispatch(
 +            collectionsContentAddressActions.SET_COLUMNS({
 +                columns: collectionContentAddressPanelColumns,
 +            })
 +        );
 +        dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
 +
 +        if (services.linkAccountService.getAccountToLink()) {
 +            dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
 +        }
 +
 +        dispatch<any>(initSidePanelTree());
 +        if (router.location) {
 +            const match = matchRootRoute(router.location.pathname);
 +            if (match) {
 +                dispatch<any>(navigateToRootProject);
 +            }
 +        }
 +    } else {
 +        dispatch(userIsNotAuthenticated);
 +    }
 +};
 +
 +export const loadFavorites = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.FAVORITES));
 +        dispatch<any>(loadFavoritePanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
 +    });
 +
 +export const loadCollectionContentAddress = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadCollectionsContentAddressPanel());
 +});
 +
 +export const loadTrash = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
 +        dispatch<any>(loadTrashPanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.TRASH));
 +    });
 +
 +export const loadAllProcesses = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES));
 +        dispatch<any>(loadAllProcessesPanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
 +    });
 +
 +export const loadProject = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
 +        const userUuid = getUserUuid(getState());
 +        dispatch(setIsProjectPanelTrashed(false));
 +        if (!userUuid) {
 +            return;
 +        }
 +        try {
 +            dispatch(progressIndicatorActions.START_WORKING(uuid));
 +            if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
 +                // Load another users home projects
 +                dispatch(finishLoadingProject(uuid));
 +            } else if (userUuid !== uuid) {
 +                await dispatch(finishLoadingProject(uuid));
 +                const match = await loadGroupContentsResource({
 +                    uuid,
 +                    userUuid,
 +                    services,
 +                });
 +                match({
 +                    OWNED: async () => {
 +                        await dispatch(activateSidePanelTreeItem(uuid));
 +                        dispatch<any>(setSidePanelBreadcrumbs(uuid));
 +                    },
 +                    SHARED: async () => {
 +                        await dispatch(activateSidePanelTreeItem(uuid));
 +                        dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
 +                    },
 +                    TRASHED: async () => {
 +                        await dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
 +                        dispatch<any>(setTrashBreadcrumbs(uuid));
 +                        dispatch(setIsProjectPanelTrashed(true));
 +                    },
 +                });
 +            } else {
 +                await dispatch(finishLoadingProject(userUuid));
 +                await dispatch(activateSidePanelTreeItem(userUuid));
 +                dispatch<any>(setSidePanelBreadcrumbs(userUuid));
 +            }
 +        } finally {
 +            dispatch(progressIndicatorActions.STOP_WORKING(uuid));
 +        }
 +    });
 +
 +export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) => async (dispatch: Dispatch) => {
 +    const newProject = await dispatch<any>(projectCreateActions.createProject(data));
 +    if (newProject) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Project has been successfully created.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
 +        dispatch<any>(navigateTo(newProject.uuid));
 +    }
 +};
 +
 +export const moveProject =
 +    (data: MoveToFormDialogData, isSecondaryMove = false) =>
 +        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +            const checkedList = getState().multiselect.checkedList;
 +            const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +            //if no items in checkedlist default to normal context menu behavior
 +            if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
 +
 +            const sourceUuid = getResource(data.uuid)(getState().resources)?.ownerUuid;
 +            const destinationUuid = data.ownerUuid;
 +
 +            const projectsToMove: MoveableResource[] = uuidsToMove
 +                .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
 +                .filter(resource => resource.kind === ResourceKind.PROJECT);
 +
 +            for (const project of projectsToMove) {
 +                await moveSingleProject(project);
 +            }
 +
 +            //omly propagate if this call is the original
 +            if (!isSecondaryMove) {
 +                const kindsToMove: Set<string> = selectedToKindSet(checkedList);
 +                kindsToMove.delete(ResourceKind.PROJECT);
 +
 +                kindsToMove.forEach(kind => {
 +                    secondaryMove[kind](data, true)(dispatch, getState, services);
 +                });
 +            }
 +
 +            async function moveSingleProject(project: MoveableResource) {
 +                try {
 +                    const oldProject: MoveToFormDialogData = { name: project.name, uuid: project.uuid, ownerUuid: data.ownerUuid };
 +                    const oldOwnerUuid = oldProject ? oldProject.ownerUuid : "";
 +                    const movedProject = await dispatch<any>(projectMoveActions.moveProject(oldProject));
 +                    if (movedProject) {
 +                        dispatch(
 +                            snackbarActions.OPEN_SNACKBAR({
 +                                message: "Project has been moved",
 +                                hideDuration: 2000,
 +                                kind: SnackbarKind.SUCCESS,
 +                            })
 +                        );
 +                        await dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
 +                    }
 +                } catch (e) {
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
++                            message: !!(project as any).frozenByUuid ? 'Could not move frozen project.' : e.message,
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.ERROR,
 +                        })
 +                    );
 +                }
 +            }
 +            if (sourceUuid) await dispatch<any>(loadSidePanelTreeProjects(sourceUuid));
 +            await dispatch<any>(loadSidePanelTreeProjects(destinationUuid));
 +        };
 +
 +export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
 +    const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
 +    if (updatedProject) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Project has been successfully updated.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
 +        dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
 +    }
 +};
 +
 +export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
 +    const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
 +    if (updatedGroup) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Group has been successfully updated.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
 +        dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
 +    }
 +};
 +
 +export const loadCollection = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
 +        const userUuid = getUserUuid(getState());
 +        try {
 +            dispatch(progressIndicatorActions.START_WORKING(uuid));
 +            if (userUuid) {
 +                const match = await loadGroupContentsResource({
 +                    uuid,
 +                    userUuid,
 +                    services,
 +                });
 +                let collection: CollectionResource | undefined;
 +                let breadcrumbfunc:
 +                    | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
 +                    | undefined;
 +                let sidepanel: string | undefined;
 +                match({
 +                    OWNED: thecollection => {
 +                        collection = thecollection as CollectionResource;
 +                        sidepanel = collection.ownerUuid;
 +                        breadcrumbfunc = setSidePanelBreadcrumbs;
 +                    },
 +                    SHARED: thecollection => {
 +                        collection = thecollection as CollectionResource;
 +                        sidepanel = collection.ownerUuid;
 +                        breadcrumbfunc = setSharedWithMeBreadcrumbs;
 +                    },
 +                    TRASHED: thecollection => {
 +                        collection = thecollection as CollectionResource;
 +                        sidepanel = SidePanelTreeCategory.TRASH;
 +                        breadcrumbfunc = () => setTrashBreadcrumbs("");
 +                    },
 +                });
 +                if (collection && breadcrumbfunc && sidepanel) {
 +                    dispatch(updateResources([collection]));
 +                    await dispatch<any>(finishLoadingProject(collection.ownerUuid));
 +                    dispatch(collectionPanelActions.SET_COLLECTION(collection));
 +                    await dispatch(activateSidePanelTreeItem(sidepanel));
 +                    dispatch(breadcrumbfunc(collection.ownerUuid));
 +                    dispatch(loadCollectionPanel(collection.uuid));
 +                }
 +            }
 +        } finally {
 +            dispatch(progressIndicatorActions.STOP_WORKING(uuid));
 +        }
 +    });
 +
 +export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) => async (dispatch: Dispatch) => {
 +    const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
 +    if (collection) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Collection has been successfully created.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        dispatch<any>(updateResources([collection]));
 +        dispatch<any>(navigateTo(collection.uuid));
 +    }
 +};
 +
 +export const copyCollection = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    const checkedList = getState().multiselect.checkedList;
 +    const uuidsToCopy: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +    //if no items in checkedlist && no items passed in, default to normal context menu behavior
 +    if (!uuidsToCopy.length) uuidsToCopy.push(data.uuid);
 +
 +    const collectionsToCopy: CollectionCopyResource[] = uuidsToCopy
 +        .map(uuid => getResource(uuid)(getState().resources) as CollectionCopyResource)
 +        .filter(resource => resource.kind === ResourceKind.COLLECTION);
 +
 +    for (const collection of collectionsToCopy) {
 +        await copySingleCollection({ ...collection, ownerUuid: data.ownerUuid } as CollectionCopyResource);
 +    }
 +
 +    async function copySingleCollection(copyToProject: CollectionCopyResource) {
 +        const newName = data.fromContextMenu || collectionsToCopy.length === 1 ? data.name : `Copy of: ${copyToProject.name}`;
 +        try {
 +            const collection = await dispatch<any>(
 +                collectionCopyActions.copyCollection({
 +                    ...copyToProject,
 +                    name: newName,
 +                    fromContextMenu: collectionsToCopy.length === 1 ? true : data.fromContextMenu,
 +                })
 +            );
 +            if (copyToProject && collection) {
 +                await dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
 +                dispatch(
 +                    snackbarActions.OPEN_SNACKBAR({
 +                        message: "Collection has been copied.",
 +                        hideDuration: 3000,
 +                        kind: SnackbarKind.SUCCESS,
 +                        link: collection.ownerUuid,
 +                    })
 +                );
 +                dispatch<any>(multiselectActions.deselectOne(copyToProject.uuid));
 +            }
 +        } catch (e) {
 +            dispatch(
 +                snackbarActions.OPEN_SNACKBAR({
 +                    message: e.message,
 +                    hideDuration: 2000,
 +                    kind: SnackbarKind.ERROR,
 +                })
 +            );
 +        }
 +    }
 +    dispatch(projectPanelActions.REQUEST_ITEMS());
 +};
 +
 +export const moveCollection =
 +    (data: MoveToFormDialogData, isSecondaryMove = false) =>
 +        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +            const checkedList = getState().multiselect.checkedList;
 +            const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +            //if no items in checkedlist && no items passed in, default to normal context menu behavior
 +            if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
 +
 +            const collectionsToMove: MoveableResource[] = uuidsToMove
 +                .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
 +                .filter(resource => resource.kind === ResourceKind.COLLECTION);
 +
 +            for (const collection of collectionsToMove) {
 +                await moveSingleCollection(collection);
 +            }
 +
 +            //omly propagate if this call is the original
 +            if (!isSecondaryMove) {
 +                const kindsToMove: Set<string> = selectedToKindSet(checkedList);
 +                kindsToMove.delete(ResourceKind.COLLECTION);
 +
 +                kindsToMove.forEach(kind => {
 +                    secondaryMove[kind](data, true)(dispatch, getState, services);
 +                });
 +            }
 +
 +            async function moveSingleCollection(collection: MoveableResource) {
 +                try {
 +                    const oldCollection: MoveToFormDialogData = { name: collection.name, uuid: collection.uuid, ownerUuid: data.ownerUuid };
 +                    const movedCollection = await dispatch<any>(collectionMoveActions.moveCollection(oldCollection));
 +                    dispatch<any>(updateResources([movedCollection]));
 +                    dispatch<any>(reloadProjectMatchingUuid([movedCollection.ownerUuid]));
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: "Collection has been moved.",
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.SUCCESS,
 +                        })
 +                    );
 +                } catch (e) {
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: e.message,
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.ERROR,
 +                        })
 +                    );
 +                }
 +            }
 +        };
 +
 +export const loadProcess = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
 +        try {
 +            dispatch(progressIndicatorActions.START_WORKING(uuid));
 +            dispatch<any>(loadProcessPanel(uuid));
 +            const process = await dispatch<any>(processesActions.loadProcess(uuid));
 +            if (process) {
 +                await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
 +                await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
 +                dispatch<any>(setProcessBreadcrumbs(uuid));
 +                dispatch<any>(loadDetailsPanel(uuid));
 +            }
 +        } finally {
 +            dispatch(progressIndicatorActions.STOP_WORKING(uuid));
 +        }
 +    });
 +
 +export const loadRegisteredWorkflow = (uuid: string) =>
 +    handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +        const userUuid = getUserUuid(getState());
 +        if (userUuid) {
 +            const match = await loadGroupContentsResource({
 +                uuid,
 +                userUuid,
 +                services,
 +            });
 +            let workflow: WorkflowResource | undefined;
 +            let breadcrumbfunc:
 +                | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
 +                | undefined;
 +            match({
 +                OWNED: async theworkflow => {
 +                    workflow = theworkflow as WorkflowResource;
 +                    breadcrumbfunc = setSidePanelBreadcrumbs;
 +                },
 +                SHARED: async theworkflow => {
 +                    workflow = theworkflow as WorkflowResource;
 +                    breadcrumbfunc = setSharedWithMeBreadcrumbs;
 +                },
 +                TRASHED: () => { },
 +            });
 +            if (workflow && breadcrumbfunc) {
 +                dispatch(updateResources([workflow]));
 +                await dispatch<any>(finishLoadingProject(workflow.ownerUuid));
 +                await dispatch<any>(activateSidePanelTreeItem(workflow.ownerUuid));
 +                dispatch<any>(breadcrumbfunc(workflow.ownerUuid));
 +            }
 +        }
 +    });
 +
 +export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) => async (dispatch: Dispatch) => {
 +    try {
 +        const process = await dispatch<any>(processUpdateActions.updateProcess(data));
 +        if (process) {
 +            dispatch(
 +                snackbarActions.OPEN_SNACKBAR({
 +                    message: "Process has been successfully updated.",
 +                    hideDuration: 2000,
 +                    kind: SnackbarKind.SUCCESS,
 +                })
 +            );
 +            dispatch<any>(updateResources([process]));
 +            dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
 +        }
 +    } catch (e) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: e.message,
 +                hideDuration: 2000,
 +                kind: SnackbarKind.ERROR,
 +            })
 +        );
 +    }
 +};
 +
 +export const moveProcess =
 +    (data: MoveToFormDialogData, isSecondaryMove = false) =>
 +        async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +            const checkedList = getState().multiselect.checkedList;
 +            const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
 +
 +            //if no items in checkedlist && no items passed in, default to normal context menu behavior
 +            if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
 +
 +            const processesToMove: MoveableResource[] = uuidsToMove
 +                .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
 +                .filter(resource => resource.kind === ResourceKind.PROCESS);
 +
 +            for (const process of processesToMove) {
 +                await moveSingleProcess(process);
 +            }
 +
 +            //omly propagate if this call is the original
 +            if (!isSecondaryMove) {
 +                const kindsToMove: Set<string> = selectedToKindSet(checkedList);
 +                kindsToMove.delete(ResourceKind.PROCESS);
 +
 +                kindsToMove.forEach(kind => {
 +                    secondaryMove[kind](data, true)(dispatch, getState, services);
 +                });
 +            }
 +
 +            async function moveSingleProcess(process: MoveableResource) {
 +                try {
 +                    const oldProcess: MoveToFormDialogData = { name: process.name, uuid: process.uuid, ownerUuid: data.ownerUuid };
 +                    const movedProcess = await dispatch<any>(processMoveActions.moveProcess(oldProcess));
 +                    dispatch<any>(updateResources([movedProcess]));
 +                    dispatch<any>(reloadProjectMatchingUuid([movedProcess.ownerUuid]));
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: "Process has been moved.",
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.SUCCESS,
 +                        })
 +                    );
 +                } catch (e) {
 +                    dispatch(
 +                        snackbarActions.OPEN_SNACKBAR({
 +                            message: e.message,
 +                            hideDuration: 2000,
 +                            kind: SnackbarKind.ERROR,
 +                        })
 +                    );
 +                }
 +            }
 +        };
 +
 +export const copyProcess = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    try {
 +        const process = await dispatch<any>(processCopyActions.copyProcess(data));
 +        dispatch<any>(updateResources([process]));
 +        dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: "Process has been copied.",
 +                hideDuration: 2000,
 +                kind: SnackbarKind.SUCCESS,
 +            })
 +        );
 +        dispatch<any>(navigateTo(process.uuid));
 +    } catch (e) {
 +        dispatch(
 +            snackbarActions.OPEN_SNACKBAR({
 +                message: e.message,
 +                hideDuration: 2000,
 +                kind: SnackbarKind.ERROR,
 +            })
 +        );
 +    }
 +};
 +
 +export const resourceIsNotLoaded = (uuid: string) =>
 +    snackbarActions.OPEN_SNACKBAR({
 +        message: `Resource identified by ${uuid} is not loaded.`,
 +        kind: SnackbarKind.ERROR,
 +    });
 +
 +export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
 +    message: "User is not authenticated",
 +    kind: SnackbarKind.ERROR,
 +});
 +
 +export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
 +    message: "Could not load user",
 +    kind: SnackbarKind.ERROR,
 +});
 +
 +export const reloadProjectMatchingUuid =
 +    (matchingUuids: string[]) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +        const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
 +        if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
 +            dispatch<any>(loadProject(currentProjectPanelUuid));
 +        }
 +    };
 +
 +export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
 +    dispatch<any>(loadSharedWithMePanel());
 +    await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
 +    await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
 +});
 +
 +export const loadRunProcess = handleFirstTimeLoad(async (dispatch: Dispatch) => {
 +    await dispatch<any>(loadRunProcessPanel());
 +});
 +
 +export const loadPublicFavorites = () =>
 +    handleFirstTimeLoad((dispatch: Dispatch) => {
 +        dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES));
 +        dispatch<any>(loadPublicFavoritePanel());
 +        dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES));
 +    });
 +
 +export const loadSearchResults = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadSearchResultsPanel());
 +});
 +
 +export const loadLinks = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadLinkPanel());
 +});
 +
 +export const loadVirtualMachines = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadVirtualMachinesPanel());
 +    dispatch(setVirtualMachinesBreadcrumbs());
 +    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHELL_ACCESS));
 +});
 +
 +export const loadVirtualMachinesAdmin = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadVirtualMachinesPanel());
 +    dispatch(setVirtualMachinesAdminBreadcrumbs());
 +    dispatch(treePickerActions.DEACTIVATE_TREE_PICKER_NODE({pickerId: SIDE_PANEL_TREE} ))
 +});
 +
 +export const loadRepositories = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadRepositoriesPanel());
 +    dispatch(setRepositoriesBreadcrumbs());
 +});
 +
 +export const loadSshKeys = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadSshKeysPanel());
 +});
 +
 +export const loadInstanceTypes = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.INSTANCE_TYPES));
 +    dispatch(setInstanceTypesBreadcrumbs());
 +});
 +
 +export const loadSiteManager = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadSiteManagerPanel());
 +});
 +
 +export const loadUserProfile = (userUuid?: string) =>
 +    handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +        if (userUuid) {
 +            dispatch(setUserProfileBreadcrumbs(userUuid));
 +            dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
 +        } else {
 +            dispatch(setMyAccountBreadcrumbs());
 +            dispatch(userProfilePanelActions.loadUserProfilePanel());
 +        }
 +    });
 +
 +export const loadLinkAccount = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +    dispatch(loadLinkAccountPanel());
 +});
 +
 +export const loadKeepServices = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadKeepServicesPanel());
 +});
 +
 +export const loadUsers = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadUsersPanel());
 +    dispatch(setUsersBreadcrumbs());
 +});
 +
 +export const loadApiClientAuthorizations = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
 +    await dispatch(loadApiClientAuthorizationsPanel());
 +});
 +
 +export const loadGroupsPanel = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +    dispatch(setGroupsBreadcrumbs());
 +    dispatch(groupPanelActions.loadGroupsPanel());
 +});
 +
 +export const loadGroupDetailsPanel = (groupUuid: string) =>
 +    handleFirstTimeLoad((dispatch: Dispatch<any>) => {
 +        dispatch(setGroupDetailsBreadcrumbs(groupUuid));
 +        dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
 +    });
 +
 +const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch<any>) => {
 +    const uuid = typeof project === "string" ? project : project.uuid;
 +    dispatch(loadDetailsPanel(uuid));
 +    if (typeof project !== "string") {
 +        dispatch(updateResources([project]));
 +    }
 +};
 +
 +const loadGroupContentsResource = async (params: { uuid: string; userUuid: string; services: ServiceRepository }) => {
 +    const filters = new FilterBuilder().addEqual("uuid", params.uuid).getFilters();
 +    const { items } = await params.services.groupsService.contents(params.userUuid, {
 +        filters,
 +        recursive: true,
 +        includeTrash: true,
 +    });
 +    const resource = items.shift();
 +    let handler: GroupContentsHandler;
 +    if (resource) {
 +        handler =
 +            (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
 +                ? groupContentsHandlers.TRASHED(resource)
 +                : groupContentsHandlers.OWNED(resource);
 +    } else {
 +        const kind = extractUuidKind(params.uuid);
 +        let resource: GroupContentsResource;
 +        if (kind === ResourceKind.COLLECTION) {
 +            resource = await params.services.collectionService.get(params.uuid);
 +        } else if (kind === ResourceKind.PROJECT) {
 +            resource = await params.services.projectService.get(params.uuid);
 +        } else if (kind === ResourceKind.WORKFLOW) {
 +            resource = await params.services.workflowService.get(params.uuid);
 +        } else if (kind === ResourceKind.CONTAINER_REQUEST) {
 +            resource = await params.services.containerRequestService.get(params.uuid);
 +        } else {
 +            throw new Error("loadGroupContentsResource unsupported kind " + kind);
 +        }
 +        handler = groupContentsHandlers.SHARED(resource);
 +    }
 +    return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) => groupContentsHandlers.match(handler, cases);
 +};
 +
 +const groupContentsHandlersRecord = {
 +    TRASHED: ofType<GroupContentsResource>(),
 +    SHARED: ofType<GroupContentsResource>(),
 +    OWNED: ofType<GroupContentsResource>(),
 +};
 +
 +const groupContentsHandlers = unionize(groupContentsHandlersRecord);
 +
 +type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;
 +
 +type CollectionCopyResource = Resource & { name: string; fromContextMenu: boolean };
 +
 +type MoveableResource = Resource & { name: string };
 +
 +type MoveFunc = (
 +    data: MoveToFormDialogData,
 +    isSecondaryMove?: boolean
 +) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>;
 +
 +const secondaryMove: Record<string, MoveFunc> = {
 +    [ResourceKind.PROJECT]: moveProject,
 +    [ResourceKind.PROCESS]: moveProcess,
 +    [ResourceKind.COLLECTION]: moveCollection,
 +};
index 0ebe96ef3acb56f0ea29b3fdbdcdf3d40276b738,0000000000000000000000000000000000000000..56926b513db459dbe818130828761c514ee6fbe9
mode 100644,000000..100644
--- /dev/null
@@@ -1,1134 -1,0 +1,1137 @@@
-                     onClick={() => dispatch<any>(navFunc(item.uuid))}
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from "@material-ui/core";
 +import { FavoriteStar, PublicFavoriteStar } from "../favorite-star/favorite-star";
 +import { Resource, ResourceKind, TrashableResource } from "models/resource";
 +import {
 +    FreezeIcon,
 +    ProjectIcon,
 +    FilterGroupIcon,
 +    CollectionIcon,
 +    ProcessIcon,
 +    DefaultIcon,
 +    ShareIcon,
 +    CollectionOldVersionIcon,
 +    WorkflowIcon,
 +    RemoveIcon,
 +    RenameIcon,
 +    ActiveIcon,
 +    SetupIcon,
 +    InactiveIcon,
 +} from "components/icon/icon";
 +import { formatDate, formatFileSize, formatTime } from "common/formatters";
 +import { resourceLabel } from "common/labels";
 +import { connect, DispatchProp } from "react-redux";
 +import { RootState } from "store/store";
 +import { getResource, filterResources } from "store/resources/resources";
 +import { GroupContentsResource } from "services/groups-service/groups-service";
 +import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from "store/processes/process";
 +import { ArvadosTheme } from "common/custom-theme";
 +import { compose, Dispatch } from "redux";
 +import { WorkflowResource } from "models/workflow";
 +import { ResourceStatus as WorkflowStatus } from "views/workflow-panel/workflow-panel-view";
 +import { getUuidPrefix, openRunProcess } from "store/workflow-panel/workflow-panel-actions";
 +import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
 +import { getUserFullname, getUserDisplayName, User, UserResource } from "models/user";
 +import { toggleIsAdmin } from "store/users/users-actions";
 +import { LinkClass, LinkResource } from "models/link";
 +import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from "store/navigation/navigation-action";
 +import { withResourceData } from "views-components/data-explorer/with-resources";
 +import { CollectionResource } from "models/collection";
 +import { IllegalNamingWarning } from "components/warning/warning";
 +import { loadResource } from "store/resources/resources-actions";
 +import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from "models/group";
 +import { openRemoveGroupMemberDialog } from "store/group-details-panel/group-details-panel-actions";
 +import { setMemberIsHidden } from "store/group-details-panel/group-details-panel-actions";
 +import { formatPermissionLevel } from "views-components/sharing-dialog/permission-select";
 +import { PermissionLevel } from "models/permission";
 +import { openPermissionEditContextMenu } from "store/context-menu/context-menu-actions";
 +import { VirtualMachinesResource } from "models/virtual-machines";
 +import { CopyToClipboardSnackbar } from "components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar";
 +import { ProjectResource } from "models/project";
 +import { ProcessResource } from "models/process";
 +
 +const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 +    const navFunc = "groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
 +    return (
 +        <Grid
 +            container
 +            alignItems="center"
 +            wrap="nowrap"
 +            spacing={16}
 +        >
 +            <Grid item>{renderIcon(item)}</Grid>
 +            <Grid item>
 +                <Typography
 +                    color="primary"
 +                    style={{ width: "auto", cursor: "pointer" }}
++                    onClick={(ev) => {
++                        ev.stopPropagation()
++                        dispatch<any>(navFunc(item.uuid))
++                    }}
 +                >
 +                    {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? <IllegalNamingWarning name={item.name} /> : null}
 +                    {item.name}
 +                </Typography>
 +            </Grid>
 +            <Grid item>
 +                <Typography variant="caption">
 +                    <FavoriteStar resourceUuid={item.uuid} />
 +                    <PublicFavoriteStar resourceUuid={item.uuid} />
 +                    {item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />}
 +                </Typography>
 +            </Grid>
 +        </Grid>
 +    );
 +};
 +
 +const FrozenProject = (props: { item: ProjectResource }) => {
 +    const [fullUsername, setFullusername] = React.useState<any>(null);
 +    const getFullName = React.useCallback(() => {
 +        if (props.item.frozenByUuid) {
 +            setFullusername(<UserNameFromID uuid={props.item.frozenByUuid} />);
 +        }
 +    }, [props.item, setFullusername]);
 +
 +    if (props.item.frozenByUuid) {
 +        return (
 +            <Tooltip
 +                onOpen={getFullName}
 +                enterDelay={500}
 +                title={<span>Project was frozen by {fullUsername}</span>}
 +            >
 +                <FreezeIcon style={{ fontSize: "inherit" }} />
 +            </Tooltip>
 +        );
 +    } else {
 +        return null;
 +    }
 +};
 +
 +export const ResourceName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return resource;
 +})((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
 +
 +const renderIcon = (item: GroupContentsResource) => {
 +    switch (item.kind) {
 +        case ResourceKind.PROJECT:
 +            if (item.groupClass === GroupClass.FILTER) {
 +                return <FilterGroupIcon />;
 +            }
 +            return <ProjectIcon />;
 +        case ResourceKind.COLLECTION:
 +            if (item.uuid === item.currentVersionUuid) {
 +                return <CollectionIcon />;
 +            }
 +            return <CollectionOldVersionIcon />;
 +        case ResourceKind.PROCESS:
 +            return <ProcessIcon />;
 +        case ResourceKind.WORKFLOW:
 +            return <WorkflowIcon />;
 +        default:
 +            return <DefaultIcon />;
 +    }
 +};
 +
 +const renderDate = (date?: string) => {
 +    return (
 +        <Typography
 +            noWrap
 +            style={{ minWidth: "100px" }}
 +        >
 +            {formatDate(date)}
 +        </Typography>
 +    );
 +};
 +
 +const renderWorkflowName = (item: WorkflowResource) => (
 +    <Grid
 +        container
 +        alignItems="center"
 +        wrap="nowrap"
 +        spacing={16}
 +    >
 +        <Grid item>{renderIcon(item)}</Grid>
 +        <Grid item>
 +            <Typography
 +                color="primary"
 +                style={{ width: "100px" }}
 +            >
 +                {item.name}
 +            </Typography>
 +        </Grid>
 +    </Grid>
 +);
 +
 +export const ResourceWorkflowName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    return resource;
 +})(renderWorkflowName);
 +
 +const getPublicUuid = (uuidPrefix: string) => {
 +    return `${uuidPrefix}-tpzed-anonymouspublic`;
 +};
 +
 +const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
 +    const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
 +    return (
 +        <div>
 +            {!isPublic && uuid && (
 +                <Tooltip title="Share">
 +                    <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
 +                        <ShareIcon />
 +                    </IconButton>
 +                </Tooltip>
 +            )}
 +        </div>
 +    );
 +};
 +
 +export const ResourceShare = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    const uuidPrefix = getUuidPrefix(state);
 +    return {
 +        uuid: resource ? resource.uuid : "",
 +        ownerUuid: resource ? resource.ownerUuid : "",
 +        uuidPrefix,
 +    };
 +})((props: { ownerUuid?: string; uuidPrefix: string; uuid?: string } & DispatchProp<any>) =>
 +    resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)
 +);
 +
 +// User Resources
 +const renderFirstName = (item: { firstName: string }) => {
 +    return <Typography noWrap>{item.firstName}</Typography>;
 +};
 +
 +export const ResourceFirstName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { firstName: "" };
 +})(renderFirstName);
 +
 +const renderLastName = (item: { lastName: string }) => <Typography noWrap>{item.lastName}</Typography>;
 +
 +export const ResourceLastName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { lastName: "" };
 +})(renderLastName);
 +
 +const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
 +    const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
 +    return link ? (
 +        <Typography
 +            noWrap
 +            color="primary"
 +            style={{ cursor: "pointer" }}
 +            onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}
 +        >
 +            {displayName}
 +        </Typography>
 +    ) : (
 +        <Typography noWrap>{displayName}</Typography>
 +    );
 +};
 +
 +export const UserResourceFullName = connect((state: RootState, props: { uuid: string; link?: boolean }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return { item: resource || { uuid: "", firstName: "", lastName: "" }, link: props.link };
 +})((props: { item: { uuid: string; firstName: string; lastName: string }; link?: boolean } & DispatchProp<any>) =>
 +    renderFullName(props.dispatch, props.item, props.link)
 +);
 +
 +const renderUuid = (item: { uuid: string }) => (
 +    <Typography
 +        data-cy="uuid"
 +        noWrap
 +    >
 +        {item.uuid}
 +        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
 +    </Typography>
 +);
 +
 +const renderUuidCopyIcon = (item: { uuid: string }) => (
 +    <Typography
 +        data-cy="uuid"
 +        noWrap
 +    >
 +        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
 +    </Typography>
 +);
 +
 +export const ResourceUuid = connect(
 +    (state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: "" }
 +)(renderUuid);
 +
 +const renderEmail = (item: { email: string }) => <Typography noWrap>{item.email}</Typography>;
 +
 +export const ResourceEmail = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { email: "" };
 +})(renderEmail);
 +
 +enum UserAccountStatus {
 +    ACTIVE = "Active",
 +    INACTIVE = "Inactive",
 +    SETUP = "Setup",
 +    UNKNOWN = "",
 +}
 +
 +const renderAccountStatus = (props: { status: UserAccountStatus }) => (
 +    <Grid
 +        container
 +        alignItems="center"
 +        wrap="nowrap"
 +        spacing={8}
 +        data-cy="account-status"
 +    >
 +        <Grid item>
 +            {(() => {
 +                switch (props.status) {
 +                    case UserAccountStatus.ACTIVE:
 +                        return <ActiveIcon style={{ color: "#4caf50", verticalAlign: "middle" }} />;
 +                    case UserAccountStatus.SETUP:
 +                        return <SetupIcon style={{ color: "#2196f3", verticalAlign: "middle" }} />;
 +                    case UserAccountStatus.INACTIVE:
 +                        return <InactiveIcon style={{ color: "#9e9e9e", verticalAlign: "middle" }} />;
 +                    default:
 +                        return <></>;
 +                }
 +            })()}
 +        </Grid>
 +        <Grid item>
 +            <Typography noWrap>{props.status}</Typography>
 +        </Grid>
 +    </Grid>
 +);
 +
 +const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
 +    const user = getResource<UserResource>(props.uuid)(state.resources);
 +    // Get membership links for all users group
 +    const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
 +    const permissions = filterResources(
 +        (resource: LinkResource) =>
 +            resource.kind === ResourceKind.LINK &&
 +            resource.linkClass === LinkClass.PERMISSION &&
 +            resource.headUuid === allUsersGroupUuid &&
 +            resource.tailUuid === props.uuid
 +    )(state.resources);
 +
 +    if (user) {
 +        return user.isActive
 +            ? { status: UserAccountStatus.ACTIVE }
 +            : permissions.length > 0
 +            ? { status: UserAccountStatus.SETUP }
 +            : { status: UserAccountStatus.INACTIVE };
 +    } else {
 +        return { status: UserAccountStatus.UNKNOWN };
 +    }
 +};
 +
 +export const ResourceLinkTailAccountStatus = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, { uuid: link.tailUuid }) : { status: UserAccountStatus.UNKNOWN };
 +})(renderAccountStatus);
 +
 +export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
 +
 +const renderIsHidden = (props: {
 +    memberLinkUuid: string;
 +    permissionLinkUuid: string;
 +    visible: boolean;
 +    canManage: boolean;
 +    setMemberIsHidden: (memberLinkUuid: string, permissionLinkUuid: string, hide: boolean) => void;
 +}) => {
 +    if (props.memberLinkUuid) {
 +        return (
 +            <Checkbox
 +                data-cy="user-visible-checkbox"
 +                color="primary"
 +                checked={props.visible}
 +                disabled={!props.canManage}
 +                onClick={e => {
 +                    e.stopPropagation();
 +                    props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
 +                }}
 +            />
 +        );
 +    } else {
 +        return <Typography />;
 +    }
 +};
 +
 +export const ResourceLinkTailIsVisible = connect(
 +    (state: RootState, props: { uuid: string }) => {
 +        const link = getResource<LinkResource>(props.uuid)(state.resources);
 +        const member = getResource<Resource>(link?.tailUuid || "")(state.resources);
 +        const group = getResource<GroupResource>(link?.headUuid || "")(state.resources);
 +        const permissions = filterResources((resource: LinkResource) => {
 +            return (
 +                resource.linkClass === LinkClass.PERMISSION &&
 +                resource.headUuid === link?.tailUuid &&
 +                resource.tailUuid === group?.uuid &&
 +                resource.name === PermissionLevel.CAN_READ
 +            );
 +        })(state.resources);
 +
 +        const permissionLinkUuid = permissions.length > 0 ? permissions[0].uuid : "";
 +        const isVisible = link && group && permissions.length > 0;
 +        // Consider whether the current user canManage this resurce in addition when it's possible
 +        const isBuiltin = isBuiltinGroup(link?.headUuid || "");
 +
 +        return member?.kind === ResourceKind.USER
 +            ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
 +            : { memberLinkUuid: "", permissionLinkUuid: "", visible: false, canManage: false };
 +    },
 +    { setMemberIsHidden }
 +)(renderIsHidden);
 +
 +const renderIsAdmin = (props: { uuid: string; isAdmin: boolean; toggleIsAdmin: (uuid: string) => void }) => (
 +    <Checkbox
 +        color="primary"
 +        checked={props.isAdmin}
 +        onClick={e => {
 +            e.stopPropagation();
 +            props.toggleIsAdmin(props.uuid);
 +        }}
 +    />
 +);
 +
 +export const ResourceIsAdmin = connect(
 +    (state: RootState, props: { uuid: string }) => {
 +        const resource = getResource<UserResource>(props.uuid)(state.resources);
 +        return resource || { isAdmin: false };
 +    },
 +    { toggleIsAdmin }
 +)(renderIsAdmin);
 +
 +const renderUsername = (item: { username: string; uuid: string }) => <Typography noWrap>{item.username || item.uuid}</Typography>;
 +
 +export const ResourceUsername = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<UserResource>(props.uuid)(state.resources);
 +    return resource || { username: "", uuid: props.uuid };
 +})(renderUsername);
 +
 +// Virtual machine resource
 +
 +const renderHostname = (item: { hostname: string }) => <Typography noWrap>{item.hostname}</Typography>;
 +
 +export const VirtualMachineHostname = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
 +    return resource || { hostname: "" };
 +})(renderHostname);
 +
 +const renderVirtualMachineLogin = (login: { user: string }) => <Typography noWrap>{login.user}</Typography>;
 +
 +export const VirtualMachineLogin = connect((state: RootState, props: { linkUuid: string }) => {
 +    const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
 +    const user = getResource<UserResource>(permission?.tailUuid || "")(state.resources);
 +
 +    return { user: user?.username || permission?.tailUuid || "" };
 +})(renderVirtualMachineLogin);
 +
 +// Common methods
 +const renderCommonData = (data: string) => <Typography noWrap>{data}</Typography>;
 +
 +const renderCommonDate = (date: string) => <Typography noWrap>{formatDate(date)}</Typography>;
 +
 +export const CommonUuid = withResourceData("uuid", renderCommonData);
 +
 +// Api Client Authorizations
 +export const TokenApiClientId = withResourceData("apiClientId", renderCommonData);
 +
 +export const TokenApiToken = withResourceData("apiToken", renderCommonData);
 +
 +export const TokenCreatedByIpAddress = withResourceData("createdByIpAddress", renderCommonDate);
 +
 +export const TokenDefaultOwnerUuid = withResourceData("defaultOwnerUuid", renderCommonData);
 +
 +export const TokenExpiresAt = withResourceData("expiresAt", renderCommonDate);
 +
 +export const TokenLastUsedAt = withResourceData("lastUsedAt", renderCommonDate);
 +
 +export const TokenLastUsedByIpAddress = withResourceData("lastUsedByIpAddress", renderCommonData);
 +
 +export const TokenScopes = withResourceData("scopes", renderCommonData);
 +
 +export const TokenUserId = withResourceData("userId", renderCommonData);
 +
 +const clusterColors = [
 +    ["#f44336", "#fff"],
 +    ["#2196f3", "#fff"],
 +    ["#009688", "#fff"],
 +    ["#cddc39", "#fff"],
 +    ["#ff9800", "#fff"],
 +];
 +
 +export const ResourceCluster = (props: { uuid: string }) => {
 +    const CLUSTER_ID_LENGTH = 5;
 +    const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf("-") : 5;
 +    const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : "";
 +    const ci =
 +        pos >= CLUSTER_ID_LENGTH
 +            ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) +
 +                  props.uuid.charCodeAt(4)) %
 +              clusterColors.length
 +            : 0;
 +    return (
 +        <span
 +            style={{
 +                backgroundColor: clusterColors[ci][0],
 +                color: clusterColors[ci][1],
 +                padding: "2px 7px",
 +                borderRadius: 3,
 +            }}
 +        >
 +            {clusterId}
 +        </span>
 +    );
 +};
 +
 +// Links Resources
 +const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || "-"}</Typography>;
 +
 +export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { name: "" };
 +})(renderLinkName);
 +
 +const renderLinkClass = (item: { linkClass: string }) => <Typography noWrap>{item.linkClass}</Typography>;
 +
 +export const ResourceLinkClass = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { linkClass: "" };
 +})(renderLinkClass);
 +
 +const getResourceDisplayName = (resource: Resource): string => {
 +    if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== "undefined") {
 +        // We can be sure the resource is UserResource
 +        return getUserDisplayName(resource as UserResource);
 +    } else {
 +        return (resource as GroupContentsResource).name;
 +    }
 +};
 +
 +const renderResourceLink = (dispatch: Dispatch, item: Resource ) => {
 +    var displayName = getResourceDisplayName(item);
 +
 +    return (
 +        <Typography
 +            noWrap
 +            color="primary"
 +            style={{ cursor: "pointer" }}
 +            onClick={() => {
 +                item.kind === ResourceKind.GROUP && (item as GroupResource).groupClass === "role"
 +                    ? dispatch<any>(navigateToGroupDetails(item.uuid))
 +                    : item.kind === ResourceKind.USER 
 +                    ? dispatch<any>(navigateToUserProfile(item.uuid))
 +                    : dispatch<any>(navigateTo(item.uuid)); 
 +            }}
 +        >
 +            {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || "" : "")}:{" "}
 +            {displayName || item.uuid}
 +        </Typography>
 +    );
 +};
 +
 +export const ResourceLinkTail = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    const tailResource = getResource<Resource>(resource?.tailUuid || "")(state.resources);
 +
 +    return {
 +        item: tailResource || { uuid: resource?.tailUuid || "", kind: resource?.tailKind || ResourceKind.NONE },
 +    };
 +})((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
 +
 +export const ResourceLinkHead = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    const headResource = getResource<Resource>(resource?.headUuid || "")(state.resources);
 +
 +    return {
 +        item: headResource || { uuid: resource?.headUuid || "", kind: resource?.headKind || ResourceKind.NONE },
 +    };
 +})((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
 +
 +export const ResourceLinkUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { uuid: "" };
 +})(renderUuid);
 +
 +export const ResourceLinkHeadUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const headResource = getResource<Resource>(link?.headUuid || "")(state.resources);
 +
 +    return headResource || { uuid: "" };
 +})(renderUuid);
 +
 +export const ResourceLinkTailUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const tailResource = getResource<Resource>(link?.tailUuid || "")(state.resources);
 +
 +    return tailResource || { uuid: "" };
 +})(renderUuid);
 +
 +const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
 +    if (item.uuid) {
 +        return canManage ? (
 +            <Typography noWrap>
 +                <IconButton
 +                    data-cy="resource-delete-button"
 +                    onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}
 +                >
 +                    <RemoveIcon />
 +                </IconButton>
 +            </Typography>
 +        ) : (
 +            <Typography noWrap>
 +                <IconButton
 +                    disabled
 +                    data-cy="resource-delete-button"
 +                >
 +                    <RemoveIcon />
 +                </IconButton>
 +            </Typography>
 +        );
 +    } else {
 +        return <Typography noWrap></Typography>;
 +    }
 +};
 +
 +export const ResourceLinkDelete = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
 +
 +    return {
 +        item: link || { uuid: "", kind: ResourceKind.NONE },
 +        canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
 +    };
 +})((props: { item: LinkResource; canManage: boolean } & DispatchProp<any>) => renderLinkDelete(props.dispatch, props.item, props.canManage));
 +
 +export const ResourceLinkTailEmail = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
 +
 +    return resource || { email: "" };
 +})(renderEmail);
 +
 +export const ResourceLinkTailUsername = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
 +
 +    return resource || { username: "" };
 +})(renderUsername);
 +
 +const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
 +    return (
 +        <Typography noWrap>
 +            {formatPermissionLevel(link.name as PermissionLevel)}
 +            {canManage ? (
 +                <IconButton
 +                    data-cy="edit-permission-button"
 +                    onClick={event => dispatch<any>(openPermissionEditContextMenu(event, link))}
 +                >
 +                    <RenameIcon />
 +                </IconButton>
 +            ) : (
 +                ""
 +            )}
 +        </Typography>
 +    );
 +};
 +
 +export const ResourceLinkHeadPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
 +
 +    return {
 +        link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
 +        canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
 +    };
 +})((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
 +
 +export const ResourceLinkTailPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
 +    const link = getResource<LinkResource>(props.uuid)(state.resources);
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
 +
 +    return {
 +        link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
 +        canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
 +    };
 +})((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
 +
 +const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
 +    const headResource = getResource<Resource>(link.headUuid)(state.resources);
 +    if (headResource && headResource.kind === ResourceKind.GROUP) {
 +        return (headResource as GroupResource).canManage;
 +    } else {
 +        // true for now
 +        return true;
 +    }
 +};
 +
 +// Process Resources
 +const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
 +    return (
 +        <div>
 +            {uuid && (
 +                <Tooltip title="Run process">
 +                    <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
 +                        <ProcessIcon />
 +                    </IconButton>
 +                </Tooltip>
 +            )}
 +        </div>
 +    );
 +};
 +
 +export const ResourceRunProcess = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    return {
 +        uuid: resource ? resource.uuid : "",
 +    };
 +})((props: { uuid: string } & DispatchProp<any>) => resourceRunProcess(props.dispatch, props.uuid));
 +
 +const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
 +    if (ownerUuid === getPublicUuid(uuidPrefix)) {
 +        return renderStatus(WorkflowStatus.PUBLIC);
 +    } else {
 +        return renderStatus(WorkflowStatus.PRIVATE);
 +    }
 +};
 +
 +const renderStatus = (status: string) => (
 +    <Typography
 +        noWrap
 +        style={{ width: "60px" }}
 +    >
 +        {status}
 +    </Typography>
 +);
 +
 +export const ResourceWorkflowStatus = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
 +    const uuidPrefix = getUuidPrefix(state);
 +    return {
 +        ownerUuid: resource ? resource.ownerUuid : "",
 +        uuidPrefix,
 +    };
 +})((props: { ownerUuid?: string; uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
 +
 +export const ResourceContainerUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { uuid: process?.container?.uuid ? process?.container?.uuid : "" };
 +})((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 +
 +enum ColumnSelection {
 +    OUTPUT_UUID = "outputUuid",
 +    LOG_UUID = "logUuid",
 +}
 +
 +const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, column: string) => {
 +    const selectedColumnUuid = item[column];
 +    return (
 +        <Grid
 +            container
 +            alignItems="center"
 +            wrap="nowrap"
 +        >
 +            <Grid item>
 +                {selectedColumnUuid ? (
 +                    <Typography
 +                        color="primary"
 +                        style={{ width: "auto", cursor: "pointer" }}
 +                        noWrap
 +                        onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}
 +                    >
 +                        {selectedColumnUuid}
 +                    </Typography>
 +                ) : (
 +                    "-"
 +                )}
 +            </Grid>
 +            <Grid item>{selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}</Grid>
 +        </Grid>
 +    );
 +};
 +
 +export const ResourceOutputUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<ProcessResource>(props.uuid)(state.resources);
 +    return resource;
 +})((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.OUTPUT_UUID));
 +
 +export const ResourceLogUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<ProcessResource>(props.uuid)(state.resources);
 +    return resource;
 +})((process: ProcessResource & DispatchProp<any>) => renderUuidLinkWithCopyIcon(process.dispatch, process, ColumnSelection.LOG_UUID));
 +
 +export const ResourceParentProcess = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { parentProcess: process?.containerRequest?.requestingContainerUuid || "" };
 +})((props: { parentProcess: string }) => renderUuid({ uuid: props.parentProcess }));
 +
 +export const ResourceModifiedByUserUuid = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { userUuid: process?.containerRequest?.modifiedByUserUuid || "" };
 +})((props: { userUuid: string }) => renderUuid({ uuid: props.userUuid }));
 +
 +export const ResourceCreatedAtDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.createdAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const ResourceLastModifiedDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.modifiedAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const ResourceTrashDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<TrashableResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.trashAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const ResourceDeleteDate = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<TrashableResource>(props.uuid)(state.resources);
 +    return { date: resource ? resource.deleteAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const renderFileSize = (fileSize?: number) => (
 +    <Typography
 +        noWrap
 +        style={{ minWidth: "45px" }}
 +    >
 +        {formatFileSize(fileSize)}
 +    </Typography>
 +);
 +
 +export const ResourceFileSize = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +
 +    if (resource && resource.kind !== ResourceKind.COLLECTION) {
 +        return { fileSize: "" };
 +    }
 +
 +    return { fileSize: resource ? resource.fileSizeTotal : 0 };
 +})((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 +
 +const renderOwner = (owner: string) => <Typography noWrap>{owner || "-"}</Typography>;
 +
 +export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { owner: resource ? resource.ownerUuid : "" };
 +})((props: { owner: string }) => renderOwner(props.owner));
 +
 +export const ResourceOwnerName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    const ownerNameState = state.ownerName;
 +    const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
 +    return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
 +})((props: { owner: string }) => renderOwner(props.owner));
 +
 +export const ResourceUUID = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { uuid: resource ? resource.uuid : "" };
 +})((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 +
 +const renderVersion = (version: number) => {
 +    return <Typography>{version ?? "-"}</Typography>;
 +};
 +
 +export const ResourceVersion = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { version: resource ? resource.version : "" };
 +})((props: { version: number }) => renderVersion(props.version));
 +
 +const renderPortableDataHash = (portableDataHash: string | null) => (
 +    <Typography noWrap>
 +        {portableDataHash ? (
 +            <>
 +                {portableDataHash}
 +                <CopyToClipboardSnackbar value={portableDataHash} />
 +            </>
 +        ) : (
 +            "-"
 +        )}
 +    </Typography>
 +);
 +
 +export const ResourcePortableDataHash = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { portableDataHash: resource ? resource.portableDataHash : "" };
 +})((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
 +
 +const renderFileCount = (fileCount: number) => {
 +    return <Typography>{fileCount ?? "-"}</Typography>;
 +};
 +
 +export const ResourceFileCount = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<CollectionResource>(props.uuid)(state.resources);
 +    return { fileCount: resource ? resource.fileCount : "" };
 +})((props: { fileCount: number }) => renderFileCount(props.fileCount));
 +
 +const userFromID = connect((state: RootState, props: { uuid: string }) => {
 +    let userFullname = "";
 +    const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
 +
 +    if (resource) {
 +        userFullname = getUserFullname(resource as User) || (resource as GroupContentsResource).name;
 +    }
 +
 +    return { uuid: props.uuid, userFullname };
 +});
 +
 +const ownerFromResourceId = compose(
 +    connect((state: RootState, props: { uuid: string }) => {
 +        const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
 +        return { uuid: childResource ? (childResource as Resource).ownerUuid : "" };
 +    }),
 +    userFromID
 +);
 +
 +const _resourceWithName = withStyles(
 +    {},
 +    { withTheme: true }
 +)((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
 +    const { uuid, userFullname, dispatch, theme } = props;
 +    if (userFullname === "") {
 +        dispatch<any>(loadResource(uuid, false));
 +        return (
 +            <Typography
 +                style={{ color: theme.palette.primary.main }}
 +                inline
 +                noWrap
 +            >
 +                {uuid}
 +            </Typography>
 +        );
 +    }
 +
 +    return (
 +        <Typography
 +            style={{ color: theme.palette.primary.main }}
 +            inline
 +            noWrap
 +        >
 +            {userFullname} ({uuid})
 +        </Typography>
 +    );
 +});
 +
 +const _resourceWithNameLink = withStyles(
 +    {},
 +    { withTheme: true }
 +)((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
 +    const { uuid, userFullname, dispatch, theme } = props;
 +    if (!userFullname) {
 +        dispatch<any>(loadResource(uuid, false));
 +    }
 +
 +    return (
 +        <Typography
 +            style={{ color: theme.palette.primary.main, cursor: 'pointer' }}
 +            inline
 +            noWrap
 +            onClick={() => dispatch<any>(navigateTo(uuid))}
 +        >
 +            {userFullname ? userFullname : uuid}
 +        </Typography>
 +    )
 +});
 +
 +
 +export const ResourceOwnerWithNameLink = ownerFromResourceId(_resourceWithNameLink);
 +
 +export const ResourceOwnerWithName = ownerFromResourceId(_resourceWithName);
 +
 +export const ResourceWithName = userFromID(_resourceWithName);
 +
 +export const UserNameFromID = compose(userFromID)((props: { uuid: string; displayAsText?: string; userFullname: string; dispatch: Dispatch }) => {
 +    const { uuid, userFullname, dispatch } = props;
 +
 +    if (userFullname === "") {
 +        dispatch<any>(loadResource(uuid, false));
 +    }
 +    return <span>{userFullname ? userFullname : uuid}</span>;
 +});
 +
 +export const ResponsiblePerson = compose(
 +    connect((state: RootState, props: { uuid: string; parentRef: HTMLElement | null }) => {
 +        let responsiblePersonName: string = "";
 +        let responsiblePersonUUID: string = "";
 +        let responsiblePersonProperty: string = "";
 +
 +        if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
 +            let index = 0;
 +            const keys = Object.keys(state.auth.config.clusterConfig.Collections.ManagedProperties);
 +
 +            while (!responsiblePersonProperty && keys[index]) {
 +                const key = keys[index];
 +                if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === "original_owner") {
 +                    responsiblePersonProperty = key;
 +                }
 +                index++;
 +            }
 +        }
 +
 +        let resource: Resource | undefined = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
 +
 +        while (resource && resource.kind !== ResourceKind.USER && responsiblePersonProperty) {
 +            responsiblePersonUUID = (resource as CollectionResource).properties[responsiblePersonProperty];
 +            resource = getResource<GroupContentsResource & UserResource>(responsiblePersonUUID)(state.resources);
 +        }
 +
 +        if (resource && resource.kind === ResourceKind.USER) {
 +            responsiblePersonName = getUserFullname(resource as UserResource) || (resource as GroupContentsResource).name;
 +        }
 +
 +        return { uuid: responsiblePersonUUID, responsiblePersonName, parentRef: props.parentRef };
 +    }),
 +    withStyles({}, { withTheme: true })
 +)((props: { uuid: string | null; responsiblePersonName: string; parentRef: HTMLElement | null; theme: ArvadosTheme }) => {
 +    const { uuid, responsiblePersonName, parentRef, theme } = props;
 +
 +    if (!uuid && parentRef) {
 +        parentRef.style.display = "none";
 +        return null;
 +    } else if (parentRef) {
 +        parentRef.style.display = "block";
 +    }
 +
 +    if (!responsiblePersonName) {
 +        return (
 +            <Typography
 +                style={{ color: theme.palette.primary.main }}
 +                inline
 +                noWrap
 +            >
 +                {uuid}
 +            </Typography>
 +        );
 +    }
 +
 +    return (
 +        <Typography
 +            style={{ color: theme.palette.primary.main }}
 +            inline
 +            noWrap
 +        >
 +            {responsiblePersonName} ({uuid})
 +        </Typography>
 +    );
 +});
 +
 +const renderType = (type: string, subtype: string) => <Typography noWrap>{resourceLabel(type, subtype)}</Typography>;
 +
 +export const ResourceType = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { type: resource ? resource.kind : "", subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : "" };
 +})((props: { type: string; subtype: string }) => renderType(props.type, props.subtype));
 +
 +export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
 +    return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
 +})((props: { resource: GroupContentsResource }) =>
 +    props.resource && props.resource.kind === ResourceKind.COLLECTION ? (
 +        <CollectionStatus uuid={props.resource.uuid} />
 +    ) : (
 +        <ProcessStatus uuid={props.resource.uuid} />
 +    )
 +);
 +
 +export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
 +    return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
 +})((props: { collection: CollectionResource }) =>
 +    props.collection.uuid !== props.collection.currentVersionUuid ? (
 +        <Typography>version {props.collection.version}</Typography>
 +    ) : (
 +        <Typography>head version</Typography>
 +    )
 +);
 +
 +export const CollectionName = connect((state: RootState, props: { uuid: string; className?: string }) => {
 +    return {
 +        collection: getResource<CollectionResource>(props.uuid)(state.resources),
 +        uuid: props.uuid,
 +        className: props.className,
 +    };
 +})((props: { collection: CollectionResource; uuid: string; className?: string }) => (
 +    <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
 +));
 +
 +export const ProcessStatus = compose(
 +    connect((state: RootState, props: { uuid: string }) => {
 +        return { process: getProcess(props.uuid)(state.resources) };
 +    }),
 +    withStyles({}, { withTheme: true })
 +)((props: { process?: Process; theme: ArvadosTheme }) =>
 +    props.process ? (
 +        <Chip
 +            label={getProcessStatus(props.process)}
 +            style={{
 +                height: props.theme.spacing.unit * 3,
 +                width: props.theme.spacing.unit * 12,
 +                ...getProcessStatusStyles(getProcessStatus(props.process), props.theme),
 +                fontSize: "0.875rem",
 +                borderRadius: props.theme.spacing.unit * 0.625,
 +            }}
 +        />
 +    ) : (
 +        <Typography>-</Typography>
 +    )
 +);
 +
 +export const ProcessStartDate = connect((state: RootState, props: { uuid: string }) => {
 +    const process = getProcess(props.uuid)(state.resources);
 +    return { date: process && process.container ? process.container.startedAt : "" };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const renderRunTime = (time: number) => (
 +    <Typography
 +        noWrap
 +        style={{ minWidth: "45px" }}
 +    >
 +        {formatTime(time, true)}
 +    </Typography>
 +);
 +
 +interface ContainerRunTimeProps {
 +    process: Process;
 +}
 +
 +interface ContainerRunTimeState {
 +    runtime: number;
 +}
 +
 +export const ContainerRunTime = connect((state: RootState, props: { uuid: string }) => {
 +    return { process: getProcess(props.uuid)(state.resources) };
 +})(
 +    class extends React.Component<ContainerRunTimeProps, ContainerRunTimeState> {
 +        private timer: any;
 +
 +        constructor(props: ContainerRunTimeProps) {
 +            super(props);
 +            this.state = { runtime: this.getRuntime() };
 +        }
 +
 +        getRuntime() {
 +            return this.props.process ? getProcessRuntime(this.props.process) : 0;
 +        }
 +
 +        updateRuntime() {
 +            this.setState({ runtime: this.getRuntime() });
 +        }
 +
 +        componentDidMount() {
 +            this.timer = setInterval(this.updateRuntime.bind(this), 5000);
 +        }
 +
 +        componentWillUnmount() {
 +            clearInterval(this.timer);
 +        }
 +
 +        render() {
 +            return this.props.process ? renderRunTime(this.state.runtime) : <Typography>-</Typography>;
 +        }
 +    }
 +);
index 0000000000000000000000000000000000000000,91e96d9bfbe002304616782d120f8239850150a4..91e96d9bfbe002304616782d120f8239850150a4
mode 000000,100644..100644
--- /dev/null
index 0000000000000000000000000000000000000000,ab819df22550b3379743bf599a0c640ecfae9115..ab819df22550b3379743bf599a0c640ecfae9115
mode 000000,100644..100644
--- /dev/null
index 4e5c038386f9ce400f0943e9ae80701af29513b4,0000000000000000000000000000000000000000..5c666acd1b6f6e14215e92d2691a52852efde49d
mode 100644,000000..100644
--- /dev/null
@@@ -1,199 -1,0 +1,199 @@@
-     navigateToOutput: (uuid: string) => void;
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { Grid, StyleRulesCallback, withStyles } from "@material-ui/core";
 +import { Dispatch } from 'redux';
 +import { formatCost, formatDate } from "common/formatters";
 +import { resourceLabel } from "common/labels";
 +import { DetailsAttribute } from "components/details-attribute/details-attribute";
 +import { ResourceKind } from "models/resource";
 +import { CollectionName, ContainerRunTime, ResourceWithName } from "views-components/data-explorer/renderers";
 +import { getProcess, getProcessStatus } from "store/processes/process";
 +import { RootState } from "store/store";
 +import { connect } from "react-redux";
 +import { ProcessResource, MOUNT_PATH_CWL_WORKFLOW } from "models/process";
 +import { ContainerResource } from "models/container";
 +import { navigateToOutput, openWorkflow } from "store/process-panel/process-panel-actions";
 +import { ArvadosTheme } from "common/custom-theme";
 +import { ProcessRuntimeStatus } from "views-components/process-runtime-status/process-runtime-status";
 +import { getPropertyChip } from "views-components/resource-properties-form/property-chip";
 +import { ContainerRequestResource } from "models/container-request";
 +import { filterResources } from "store/resources/resources";
 +import { JSONMount } from 'models/mount-types';
 +import { getCollectionUrl } from 'models/collection';
 +
 +type CssRules = 'link' | 'propertyTag';
 +
 +const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 +    link: {
 +        fontSize: '0.875rem',
 +        color: theme.palette.primary.main,
 +        '&:hover': {
 +            cursor: 'pointer'
 +        }
 +    },
 +    propertyTag: {
 +        marginRight: theme.spacing.unit / 2,
 +        marginBottom: theme.spacing.unit / 2
 +    },
 +});
 +
 +const mapStateToProps = (state: RootState, props: { request: ProcessResource }) => {
 +    const process = getProcess(props.request.uuid)(state.resources);
 +
 +    let workflowCollection = "";
 +    let workflowPath = "";
 +    if (process?.containerRequest?.mounts && process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
 +        const wf = process.containerRequest.mounts[MOUNT_PATH_CWL_WORKFLOW] as JSONMount;
 +
 +        if (wf.content["$graph"] &&
 +            wf.content["$graph"].length > 0 &&
 +            wf.content["$graph"][0] &&
 +            wf.content["$graph"][0]["steps"] &&
 +            wf.content["$graph"][0]["steps"][0]) {
 +
 +            const REGEX = /keep:([0-9a-f]{32}\+\d+)\/(.*)/;
 +            const pdh = wf.content["$graph"][0]["steps"][0].run.match(REGEX);
 +            if (pdh) {
 +                workflowCollection = pdh[1];
 +                workflowPath = pdh[2];
 +            }
 +        }
 +    }
 +
 +    return {
 +        container: process?.container,
 +        workflowCollection,
 +        workflowPath,
 +        subprocesses: filterResources((resource: ContainerRequestResource) =>
 +            resource.kind === ResourceKind.CONTAINER_REQUEST &&
 +            resource.requestingContainerUuid === process?.containerRequest.containerUuid
 +        )(state.resources),
 +    };
 +};
 +
 +interface ProcessDetailsAttributesActionProps {
-     navigateToOutput: (uuid) => dispatch<any>(navigateToOutput(uuid)),
++    navigateToOutput: (resource: ContainerRequestResource) => void;
 +    openWorkflow: (uuid: string) => void;
 +}
 +
 +const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionProps => ({
-                     {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest.outputUuid!)}>
++    navigateToOutput: (resource) => dispatch<any>(navigateToOutput(resource)),
 +    openWorkflow: (uuid) => dispatch<any>(openWorkflow(uuid)),
 +});
 +
 +export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
 +    connect(mapStateToProps, mapDispatchToProps)(
 +        (props: {
 +            request: ProcessResource, container?: ContainerResource, subprocesses: ContainerRequestResource[],
 +            workflowCollection, workflowPath,
 +            twoCol?: boolean, hideProcessPanelRedundantFields?: boolean, classes: Record<CssRules, string>
 +        } & ProcessDetailsAttributesActionProps) => {
 +            const containerRequest = props.request;
 +            const container = props.container;
 +            const subprocesses = props.subprocesses;
 +            const classes = props.classes;
 +            const mdSize = props.twoCol ? 6 : 12;
 +            const workflowCollection = props.workflowCollection;
 +            const workflowPath = props.workflowPath;
 +            const filteredPropertyKeys = Object.keys(containerRequest.properties)
 +                .filter(k => (typeof containerRequest.properties[k] !== 'object'));
 +            const hasTotalCost = containerRequest && containerRequest.cumulativeCost > 0;
 +            const totalCostNotReady = container && container.cost > 0 && container.state === "Running" && containerRequest && containerRequest.cumulativeCost === 0 && subprocesses.length > 0;
 +            return <Grid container>
 +                <Grid item xs={12}>
 +                    <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} containerCount={containerRequest.containerCount} />
 +                </Grid>
 +                {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
 +                </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Container request UUID' linkToUuid={containerRequest.uuid} value={containerRequest.uuid} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Docker image locator'
 +                        linkToUuid={containerRequest.containerImage} value={containerRequest.containerImage} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute
 +                        label='Owner' linkToUuid={containerRequest.ownerUuid}
 +                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Container UUID' value={containerRequest.containerUuid} />
 +                </Grid>
 +                {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Status' value={getProcessStatus({ containerRequest, container })} />
 +                </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Created at' value={formatDate(containerRequest.createdAt)} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Started at' value={container ? formatDate(container.startedAt) : "(none)"} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Finished at' value={container ? formatDate(container.finishedAt) : "(none)"} />
 +                </Grid>
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Container run time'>
 +                        <ContainerRunTime uuid={containerRequest.uuid} />
 +                    </DetailsAttribute>
 +                </Grid>
 +                {(containerRequest && containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-modifiedby-user">
 +                    <DetailsAttribute
 +                        label='Submitted by' linkToUuid={containerRequest.modifiedByUserUuid}
 +                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
 +                </Grid>}
 +                {(container && container.runtimeUserUuid && container.runtimeUserUuid !== containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-runtime-user">
 +                    <DetailsAttribute
 +                        label='Run as' linkToUuid={container.runtimeUserUuid}
 +                        uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
 +                </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Requesting container UUID' value={containerRequest.requestingContainerUuid || "(none)"} />
 +                </Grid>
 +                <Grid item xs={6}>
 +                    <DetailsAttribute label='Output collection' />
++                    {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest!)}>
 +                        <CollectionName className={classes.link} uuid={containerRequest.outputUuid} />
 +                    </span>}
 +                </Grid>
 +                {container && <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Cost' value={
 +                        `${hasTotalCost ? formatCost(containerRequest.cumulativeCost) + ' total, ' : (totalCostNotReady ? 'total pending completion, ' : '')}${container.cost > 0 ? formatCost(container.cost) : 'not available'} for this container`
 +                    } />
 +
 +                    {container && workflowCollection && <Grid item xs={12} md={mdSize}>
 +                        <DetailsAttribute label='Workflow code' link={getCollectionUrl(workflowCollection)} value={workflowPath} />
 +                    </Grid>}
 +                </Grid>}
 +                {containerRequest.properties.template_uuid &&
 +                    <Grid item xs={12} md={mdSize}>
 +                        <span onClick={() => props.openWorkflow(containerRequest.properties.template_uuid)}>
 +                            <DetailsAttribute classValue={classes.link}
 +                                label='Workflow' value={containerRequest.properties.workflowName} />
 +                        </span>
 +                    </Grid>}
 +                <Grid item xs={12} md={mdSize}>
 +                    <DetailsAttribute label='Priority' value={containerRequest.priority} />
 +                </Grid>
 +                {/*
 +                      NOTE: The property list should be kept at the bottom, because it spans
 +                      the entire available width, without regards of the twoCol prop.
 +                      */}
 +                <Grid item xs={12} md={12}>
 +                    <DetailsAttribute label='Properties' />
 +                    {filteredPropertyKeys.length > 0
 +                        ? filteredPropertyKeys.map(k =>
 +                            Array.isArray(containerRequest.properties[k])
 +                                ? containerRequest.properties[k].map((v: string) =>
 +                                    getPropertyChip(k, v, undefined, classes.propertyTag))
 +                                : getPropertyChip(k, containerRequest.properties[k], undefined, classes.propertyTag))
 +                        : <div>No properties</div>}
 +                </Grid>
 +            </Grid>;
 +        }
 +    )
 +);
index 05ea215dd9a2de0716b17a2778eec722ac789c18,0000000000000000000000000000000000000000..b094b769cb0bc93a979b7cf03eee300753f8dcd0
mode 100644,000000..100644
--- /dev/null
@@@ -1,445 -1,0 +1,445 @@@
-     window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
 +// Copyright (C) The Arvados Authors. All rights reserved.
 +//
 +// SPDX-License-Identifier: AGPL-3.0
 +
 +import React from "react";
 +import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
 +import { Route, Switch } from "react-router";
 +import { ProjectPanel } from "views/project-panel/project-panel";
 +import { DetailsPanel } from "views-components/details-panel/details-panel";
 +import { ArvadosTheme } from "common/custom-theme";
 +import { ContextMenu } from "views-components/context-menu/context-menu";
 +import { FavoritePanel } from "../favorite-panel/favorite-panel";
 +import { TokenDialog } from "views-components/token-dialog/token-dialog";
 +import { RichTextEditorDialog } from "views-components/rich-text-editor-dialog/rich-text-editor-dialog";
 +import { Snackbar } from "views-components/snackbar/snackbar";
 +import { CollectionPanel } from "../collection-panel/collection-panel";
 +import { RenameFileDialog } from "views-components/rename-file-dialog/rename-file-dialog";
 +import { FileRemoveDialog } from "views-components/file-remove-dialog/file-remove-dialog";
 +import { MultipleFilesRemoveDialog } from "views-components/file-remove-dialog/multiple-files-remove-dialog";
 +import { Routes } from "routes/routes";
 +import { SidePanel } from "views-components/side-panel/side-panel";
 +import { ProcessPanel } from "views/process-panel/process-panel";
 +import { ChangeWorkflowDialog } from "views-components/run-process-dialog/change-workflow-dialog";
 +import { CreateProjectDialog } from "views-components/dialog-forms/create-project-dialog";
 +import { CreateCollectionDialog } from "views-components/dialog-forms/create-collection-dialog";
 +import { CopyCollectionDialog, CopyMultiCollectionDialog } from "views-components/dialog-forms/copy-collection-dialog";
 +import { CopyProcessDialog } from "views-components/dialog-forms/copy-process-dialog";
 +import { UpdateCollectionDialog } from "views-components/dialog-forms/update-collection-dialog";
 +import { UpdateProcessDialog } from "views-components/dialog-forms/update-process-dialog";
 +import { UpdateProjectDialog } from "views-components/dialog-forms/update-project-dialog";
 +import { MoveProcessDialog } from "views-components/dialog-forms/move-process-dialog";
 +import { MoveProjectDialog } from "views-components/dialog-forms/move-project-dialog";
 +import { MoveCollectionDialog } from "views-components/dialog-forms/move-collection-dialog";
 +import { FilesUploadCollectionDialog } from "views-components/dialog-forms/files-upload-collection-dialog";
 +import { PartialCopyToNewCollectionDialog } from "views-components/dialog-forms/partial-copy-to-new-collection-dialog";
 +import { PartialCopyToExistingCollectionDialog } from "views-components/dialog-forms/partial-copy-to-existing-collection-dialog";
 +import { PartialCopyToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-copy-to-separate-collections-dialog";
 +import { PartialMoveToNewCollectionDialog } from "views-components/dialog-forms/partial-move-to-new-collection-dialog";
 +import { PartialMoveToExistingCollectionDialog } from "views-components/dialog-forms/partial-move-to-existing-collection-dialog";
 +import { PartialMoveToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-move-to-separate-collections-dialog";
 +import { RemoveProcessDialog } from "views-components/process-remove-dialog/process-remove-dialog";
 +import { MainContentBar } from "views-components/main-content-bar/main-content-bar";
 +import { Grid } from "@material-ui/core";
 +import { TrashPanel } from "views/trash-panel/trash-panel";
 +import { SharedWithMePanel } from "views/shared-with-me-panel/shared-with-me-panel";
 +import { RunProcessPanel } from "views/run-process-panel/run-process-panel";
 +import SplitterLayout from "react-splitter-layout";
 +import { WorkflowPanel } from "views/workflow-panel/workflow-panel";
 +import { RegisteredWorkflowPanel } from "views/workflow-panel/registered-workflow-panel";
 +import { SearchResultsPanel } from "views/search-results-panel/search-results-panel";
 +import { SshKeyPanel } from "views/ssh-key-panel/ssh-key-panel";
 +import { SshKeyAdminPanel } from "views/ssh-key-panel/ssh-key-admin-panel";
 +import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
 +import { UserProfilePanel } from "views/user-profile-panel/user-profile-panel";
 +import { SharingDialog } from "views-components/sharing-dialog/sharing-dialog";
 +import { NotFoundDialog } from "views-components/not-found-dialog/not-found-dialog";
 +import { AdvancedTabDialog } from "views-components/advanced-tab-dialog/advanced-tab-dialog";
 +import { ProcessInputDialog } from "views-components/process-input-dialog/process-input-dialog";
 +import { VirtualMachineUserPanel } from "views/virtual-machine-panel/virtual-machine-user-panel";
 +import { VirtualMachineAdminPanel } from "views/virtual-machine-panel/virtual-machine-admin-panel";
 +import { RepositoriesPanel } from "views/repositories-panel/repositories-panel";
 +import { KeepServicePanel } from "views/keep-service-panel/keep-service-panel";
 +import { ApiClientAuthorizationPanel } from "views/api-client-authorization-panel/api-client-authorization-panel";
 +import { LinkPanel } from "views/link-panel/link-panel";
 +import { RepositoriesSampleGitDialog } from "views-components/repositories-sample-git-dialog/repositories-sample-git-dialog";
 +import { RepositoryAttributesDialog } from "views-components/repository-attributes-dialog/repository-attributes-dialog";
 +import { CreateRepositoryDialog } from "views-components/dialog-forms/create-repository-dialog";
 +import { RemoveRepositoryDialog } from "views-components/repository-remove-dialog/repository-remove-dialog";
 +import { CreateSshKeyDialog } from "views-components/dialog-forms/create-ssh-key-dialog";
 +import { PublicKeyDialog } from "views-components/ssh-keys-dialog/public-key-dialog";
 +import { RemoveApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/remove-dialog";
 +import { RemoveKeepServiceDialog } from "views-components/keep-services-dialog/remove-dialog";
 +import { RemoveLinkDialog } from "views-components/links-dialog/remove-dialog";
 +import { RemoveSshKeyDialog } from "views-components/ssh-keys-dialog/remove-dialog";
 +import { VirtualMachineAttributesDialog } from "views-components/virtual-machines-dialog/attributes-dialog";
 +import { RemoveVirtualMachineDialog } from "views-components/virtual-machines-dialog/remove-dialog";
 +import { RemoveVirtualMachineLoginDialog } from "views-components/virtual-machines-dialog/remove-login-dialog";
 +import { VirtualMachineAddLoginDialog } from "views-components/virtual-machines-dialog/add-login-dialog";
 +import { AttributesApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/attributes-dialog";
 +import { AttributesKeepServiceDialog } from "views-components/keep-services-dialog/attributes-dialog";
 +import { AttributesLinkDialog } from "views-components/links-dialog/attributes-dialog";
 +import { AttributesSshKeyDialog } from "views-components/ssh-keys-dialog/attributes-dialog";
 +import { UserPanel } from "views/user-panel/user-panel";
 +import { UserAttributesDialog } from "views-components/user-dialog/attributes-dialog";
 +import { CreateUserDialog } from "views-components/dialog-forms/create-user-dialog";
 +import { HelpApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/help-dialog";
 +import { DeactivateDialog } from "views-components/user-dialog/deactivate-dialog";
 +import { ActivateDialog } from "views-components/user-dialog/activate-dialog";
 +import { SetupDialog } from "views-components/user-dialog/setup-dialog";
 +import { GroupsPanel } from "views/groups-panel/groups-panel";
 +import { RemoveGroupDialog } from "views-components/groups-dialog/remove-dialog";
 +import { GroupAttributesDialog } from "views-components/groups-dialog/attributes-dialog";
 +import { GroupDetailsPanel } from "views/group-details-panel/group-details-panel";
 +import { RemoveGroupMemberDialog } from "views-components/groups-dialog/member-remove-dialog";
 +import { GroupMemberAttributesDialog } from "views-components/groups-dialog/member-attributes-dialog";
 +import { PublicFavoritePanel } from "views/public-favorites-panel/public-favorites-panel";
 +import { LinkAccountPanel } from "views/link-account-panel/link-account-panel";
 +import { FedLogin } from "./fed-login";
 +import { CollectionsContentAddressPanel } from "views/collection-content-address-panel/collection-content-address-panel";
 +import { AllProcessesPanel } from "../all-processes-panel/all-processes-panel";
 +import { NotFoundPanel } from "../not-found-panel/not-found-panel";
 +import { AutoLogout } from "views-components/auto-logout/auto-logout";
 +import { RestoreCollectionVersionDialog } from "views-components/collections-dialog/restore-version-dialog";
 +import { WebDavS3InfoDialog } from "views-components/webdav-s3-dialog/webdav-s3-dialog";
 +import { pluginConfig } from "plugins";
 +import { ElementListReducer } from "common/plugintypes";
 +import { COLLAPSE_ICON_SIZE } from "views-components/side-panel-toggle/side-panel-toggle";
 +import { Banner } from "views-components/baner/banner";
 +import { InstanceTypesPanel } from "views/instance-types-panel/instance-types-panel";
 +
 +type CssRules = "root" | "container" | "splitter" | "asidePanel" | "contentWrapper" | "content";
 +
 +const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 +    root: {
 +        paddingTop: theme.spacing.unit * 7,
 +        background: theme.palette.background.default,
 +    },
 +    container: {
 +        position: "relative",
 +    },
 +    splitter: {
 +        "& > .layout-splitter": {
 +            width: "3px",
 +        },
 +        "& > .layout-splitter-disabled": {
 +            pointerEvents: "none",
 +            cursor: "pointer",
 +        },
 +    },
 +    asidePanel: {
 +        paddingTop: theme.spacing.unit,
 +        height: "100%",
 +    },
 +    contentWrapper: {
 +        paddingTop: theme.spacing.unit,
 +        minWidth: 0,
 +    },
 +    content: {
 +        minWidth: 0,
 +        paddingLeft: theme.spacing.unit * 3,
 +        paddingRight: theme.spacing.unit * 3,
 +        // Reserve vertical space for app bar + MainContentBar
 +        minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`,
 +        display: "flex",
 +    },
 +});
 +
 +interface WorkbenchDataProps {
 +    isUserActive: boolean;
 +    isNotLinking: boolean;
 +    sessionIdleTimeout: number;
 +    sidePanelIsCollapsed: boolean;
 +}
 +
 +type WorkbenchPanelProps = WithStyles<CssRules> & WorkbenchDataProps;
 +
 +const defaultSplitterSize = 90;
 +
 +const getSplitterInitialSize = () => {
 +    const splitterSize = localStorage.getItem("splitterSize");
 +    return splitterSize ? Number(splitterSize) : defaultSplitterSize;
 +};
 +
 +const saveSplitterSize = (size: number) => localStorage.setItem("splitterSize", size.toString());
 +
 +let routes = (
 +    <>
 +        <Route
 +            path={Routes.PROJECTS}
 +            component={ProjectPanel}
 +        />
 +        <Route
 +            path={Routes.COLLECTIONS}
 +            component={CollectionPanel}
 +        />
 +        <Route
 +            path={Routes.FAVORITES}
 +            component={FavoritePanel}
 +        />
 +        <Route
 +            path={Routes.ALL_PROCESSES}
 +            component={AllProcessesPanel}
 +        />
 +        <Route
 +            path={Routes.PROCESSES}
 +            component={ProcessPanel}
 +        />
 +        <Route
 +            path={Routes.TRASH}
 +            component={TrashPanel}
 +        />
 +        <Route
 +            path={Routes.SHARED_WITH_ME}
 +            component={SharedWithMePanel}
 +        />
 +        <Route
 +            path={Routes.RUN_PROCESS}
 +            component={RunProcessPanel}
 +        />
 +        <Route
 +            path={Routes.REGISTEREDWORKFLOW}
 +            component={RegisteredWorkflowPanel}
 +        />
 +        <Route
 +            path={Routes.WORKFLOWS}
 +            component={WorkflowPanel}
 +        />
 +        <Route
 +            path={Routes.SEARCH_RESULTS}
 +            component={SearchResultsPanel}
 +        />
 +        <Route
 +            path={Routes.VIRTUAL_MACHINES_USER}
 +            component={VirtualMachineUserPanel}
 +        />
 +        <Route
 +            path={Routes.VIRTUAL_MACHINES_ADMIN}
 +            component={VirtualMachineAdminPanel}
 +        />
 +        <Route
 +            path={Routes.REPOSITORIES}
 +            component={RepositoriesPanel}
 +        />
 +        <Route
 +            path={Routes.SSH_KEYS_USER}
 +            component={SshKeyPanel}
 +        />
 +        <Route
 +            path={Routes.SSH_KEYS_ADMIN}
 +            component={SshKeyAdminPanel}
 +        />
 +        <Route
 +            path={Routes.INSTANCE_TYPES}
 +            component={InstanceTypesPanel}
 +        />
 +        <Route
 +            path={Routes.SITE_MANAGER}
 +            component={SiteManagerPanel}
 +        />
 +        <Route
 +            path={Routes.KEEP_SERVICES}
 +            component={KeepServicePanel}
 +        />
 +        <Route
 +            path={Routes.USERS}
 +            component={UserPanel}
 +        />
 +        <Route
 +            path={Routes.API_CLIENT_AUTHORIZATIONS}
 +            component={ApiClientAuthorizationPanel}
 +        />
 +        <Route
 +            path={Routes.MY_ACCOUNT}
 +            component={UserProfilePanel}
 +        />
 +        <Route
 +            path={Routes.USER_PROFILE}
 +            component={UserProfilePanel}
 +        />
 +        <Route
 +            path={Routes.GROUPS}
 +            component={GroupsPanel}
 +        />
 +        <Route
 +            path={Routes.GROUP_DETAILS}
 +            component={GroupDetailsPanel}
 +        />
 +        <Route
 +            path={Routes.LINKS}
 +            component={LinkPanel}
 +        />
 +        <Route
 +            path={Routes.PUBLIC_FAVORITES}
 +            component={PublicFavoritePanel}
 +        />
 +        <Route
 +            path={Routes.LINK_ACCOUNT}
 +            component={LinkAccountPanel}
 +        />
 +        <Route
 +            path={Routes.COLLECTIONS_CONTENT_ADDRESS}
 +            component={CollectionsContentAddressPanel}
 +        />
 +    </>
 +);
 +
 +const reduceRoutesFn: (a: React.ReactElement[], b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
 +
 +routes = React.createElement(
 +    React.Fragment,
 +    null,
 +    pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children))
 +);
 +
 +const applyCollapsedState = isCollapsed => {
 +    const rightPanel: Element = document.getElementsByClassName("layout-pane")[1];
 +    const totalWidth: number = document.getElementsByClassName("splitter-layout")[0]?.clientWidth;
 +    const rightPanelExpandedWidth = (totalWidth - COLLAPSE_ICON_SIZE) / (totalWidth / 100);
 +    if (rightPanel) {
 +        rightPanel.setAttribute("style", `width: ${isCollapsed ? `calc(${rightPanelExpandedWidth}% - 1rem)` : `${getSplitterInitialSize()}%`}`);
 +    }
 +    const splitter = document.getElementsByClassName("layout-splitter")[0];
 +    isCollapsed ? splitter?.classList.add("layout-splitter-disabled") : splitter?.classList.remove("layout-splitter-disabled");
 +};
 +
 +export const WorkbenchPanel = withStyles(styles)((props: WorkbenchPanelProps) => {
 +    //panel size will not scale automatically on window resize, so we do it manually
++    if (props && props.sidePanelIsCollapsed) window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
 +    applyCollapsedState(props.sidePanelIsCollapsed);
 +
 +    return (
 +        <Grid
 +            container
 +            item
 +            xs
 +            className={props.classes.root}
 +        >
 +            {props.sessionIdleTimeout > 0 && <AutoLogout />}
 +            <Grid
 +                container
 +                item
 +                xs
 +                className={props.classes.container}
 +            >
 +                <SplitterLayout
 +                    customClassName={props.classes.splitter}
 +                    percentage={true}
 +                    primaryIndex={0}
 +                    primaryMinSize={10}
 +                    secondaryInitialSize={getSplitterInitialSize()}
 +                    secondaryMinSize={40}
 +                    onSecondaryPaneSizeChange={saveSplitterSize}
 +                >
 +                    {props.isUserActive && props.isNotLinking && (
 +                        <Grid
 +                            container
 +                            item
 +                            xs
 +                            component="aside"
 +                            direction="column"
 +                            className={props.classes.asidePanel}
 +                        >
 +                            <SidePanel />
 +                        </Grid>
 +                    )}
 +                    <Grid
 +                        container
 +                        item
 +                        xs
 +                        component="main"
 +                        direction="column"
 +                        className={props.classes.contentWrapper}
 +                    >
 +                        <Grid
 +                            item
 +                            xs
 +                        >
 +                            {props.isNotLinking && <MainContentBar />}
 +                        </Grid>
 +                        <Grid
 +                            item
 +                            xs
 +                            className={props.classes.content}
 +                        >
 +                            <Switch>
 +                                {routes.props.children}
 +                                <Route
 +                                    path={Routes.NO_MATCH}
 +                                    component={NotFoundPanel}
 +                                />
 +                            </Switch>
 +                        </Grid>
 +                    </Grid>
 +                </SplitterLayout>
 +            </Grid>
 +            <Grid item>
 +                <DetailsPanel />
 +            </Grid>
 +            <AdvancedTabDialog />
 +            <AttributesApiClientAuthorizationDialog />
 +            <AttributesKeepServiceDialog />
 +            <AttributesLinkDialog />
 +            <AttributesSshKeyDialog />
 +            <ChangeWorkflowDialog />
 +            <ContextMenu />
 +            <CopyCollectionDialog />
 +            <CopyMultiCollectionDialog />
 +            <CopyProcessDialog />
 +            <CreateCollectionDialog />
 +            <CreateProjectDialog />
 +            <CreateRepositoryDialog />
 +            <CreateSshKeyDialog />
 +            <CreateUserDialog />
 +            <TokenDialog />
 +            <FileRemoveDialog />
 +            <FilesUploadCollectionDialog />
 +            <GroupAttributesDialog />
 +            <GroupMemberAttributesDialog />
 +            <HelpApiClientAuthorizationDialog />
 +            <MoveCollectionDialog />
 +            <MoveProcessDialog />
 +            <MoveProjectDialog />
 +            <MultipleFilesRemoveDialog />
 +            <PublicKeyDialog />
 +            <PartialCopyToNewCollectionDialog />
 +            <PartialCopyToExistingCollectionDialog />
 +            <PartialCopyToSeparateCollectionsDialog />
 +            <PartialMoveToNewCollectionDialog />
 +            <PartialMoveToExistingCollectionDialog />
 +            <PartialMoveToSeparateCollectionsDialog />
 +            <ProcessInputDialog />
 +            <RestoreCollectionVersionDialog />
 +            <RemoveApiClientAuthorizationDialog />
 +            <RemoveGroupDialog />
 +            <RemoveGroupMemberDialog />
 +            <RemoveKeepServiceDialog />
 +            <RemoveLinkDialog />
 +            <RemoveProcessDialog />
 +            <RemoveRepositoryDialog />
 +            <RemoveSshKeyDialog />
 +            <RemoveVirtualMachineDialog />
 +            <RemoveVirtualMachineLoginDialog />
 +            <VirtualMachineAddLoginDialog />
 +            <RenameFileDialog />
 +            <RepositoryAttributesDialog />
 +            <RepositoriesSampleGitDialog />
 +            <RichTextEditorDialog />
 +            <SharingDialog />
 +            <NotFoundDialog />
 +            <Snackbar />
 +            <UpdateCollectionDialog />
 +            <UpdateProcessDialog />
 +            <UpdateProjectDialog />
 +            <UserAttributesDialog />
 +            <DeactivateDialog />
 +            <ActivateDialog />
 +            <SetupDialog />
 +            <VirtualMachineAttributesDialog />
 +            <FedLogin />
 +            <WebDavS3InfoDialog />
 +            <Banner />
 +            {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
 +        </Grid>
 +    );
 +});