Merge branch 'main' into 18692-frozen-projects-workbench-support
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Thu, 23 Jun 2022 12:36:25 +0000 (14:36 +0200)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Thu, 23 Jun 2022 12:36:25 +0000 (14:36 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

16 files changed:
cypress/integration/project.spec.js
src/common/frozen-resources.ts [new file with mode: 0644]
src/components/breadcrumbs/breadcrumbs.tsx
src/components/icon/icon.tsx
src/index.tsx
src/models/project.ts
src/store/breadcrumbs/breadcrumbs-middleware.ts [new file with mode: 0644]
src/store/context-menu/context-menu-actions.ts
src/store/projects/project-lock-actions.ts [new file with mode: 0644]
src/store/store.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/actions/lock-action.tsx [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/renderers.tsx
src/views-components/side-panel-button/side-panel-button.tsx
src/views/project-panel/project-panel.tsx

index c4983e3e16bc5f135a0b6e4a29f24c3dad79bf58..0ad8da5622747231283946072b7b315418ed2972 100644 (file)
@@ -278,4 +278,101 @@ describe('Project tests', function() {
                 });
         });
     });
+
+    describe('Frozen projects', () => {
+        beforeEach(() => {  
+            cy.createGroup(activeUser.token, {
+                name: `Main project ${Math.floor(Math.random() * 999999)}`,
+                group_class: 'project',
+            }).as('mainProject');
+    
+            cy.createGroup(adminUser.token, {
+                name: `Admin project ${Math.floor(Math.random() * 999999)}`,
+                group_class: 'project',
+            }).as('adminProject').then((mainProject) => {
+                cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, 'can_write');
+            });
+
+            cy.get('@mainProject').then((mainProject) => {
+                cy.createGroup(adminUser.token, {
+                    name : `Sub project ${Math.floor(Math.random() * 999999)}`,
+                    group_class: 'project',
+                    owner_uuid: mainProject.uuid,
+                }).as('subProject');
+
+                cy.createCollection(adminUser.token, {
+                    name: `Main collection ${Math.floor(Math.random() * 999999)}`,
+                    owner_uuid: mainProject.uuid,
+                    manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+                }).as('mainCollection');        
+            });
+        });
+
+        it('should be able to froze own project', () => {
+            cy.getAll('@mainProject').then(([mainProject]) => {
+                cy.loginAs(activeUser);
+
+                cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+                cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Freeze').should('not.exist');
+            });
+        });
+
+        it('should not be able to modify items within the frozen project', () => {
+            cy.getAll('@mainProject', '@mainCollection').then(([mainProject, mainCollection]) => {
+                cy.loginAs(activeUser);
+
+                cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+                cy.get('[data-cy=project-panel]').contains(mainProject.name).click();
+
+                cy.get('[data-cy=project-panel]').contains(mainCollection.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Move to trash').should('not.exist');
+            });
+        });
+
+        it('should not be able to froze not owned project', () => {
+            cy.getAll('@adminProject').then(([adminProject]) => {
+                cy.loginAs(activeUser);
+
+                cy.get('[data-cy=side-panel-tree]').contains('Shared with me').click();
+
+                cy.get('main').contains(adminProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+                cy.get('main').contains(adminProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
+            });
+        });
+
+        it('should be able to unfroze project if user is an admin', () => {
+            cy.getAll('@adminProject').then(([adminProject]) => {
+                cy.loginAs(adminUser);
+
+                cy.get('main').contains(adminProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Freeze').click();
+
+                cy.wait(1000);
+
+                cy.get('main').contains(adminProject.name).rightclick();
+
+                cy.get('[data-cy=context-menu]').contains('Unfreeze').click();
+
+                cy.get('main').contains(adminProject.name).rightclick();
+                
+                cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
+            });
+        });
+    });
 });
+
diff --git a/src/common/frozen-resources.ts b/src/common/frozen-resources.ts
new file mode 100644 (file)
index 0000000..10e1815
--- /dev/null
@@ -0,0 +1,19 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProjectResource } from "models/project";
+import { getResource } from "store/resources/resources";
+
+export const resourceIsFrozen = (resource: any, resources): boolean => {
+    let isFrozen: boolean = !!resource.frozenByUuid;
+    let ownerUuid: string | undefined = resource?.ownerUuid;
+
+    while(!isFrozen && !!ownerUuid) {
+        const parentResource: ProjectResource | undefined = getResource<ProjectResource>(ownerUuid)(resources);
+        isFrozen = !!parentResource?.frozenByUuid;
+        ownerUuid = parentResource?.ownerUuid;
+    }
+
+    return isFrozen;
+}
\ No newline at end of file
index 3d668856ecd5d1c20e4faebc3b51d54466045277..c081c314b607d8cb36916629981b682f1acb601b 100644 (file)
@@ -7,12 +7,13 @@ import { Button, Grid, StyleRulesCallback, WithStyles, Typography, Tooltip } fro
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
 import { withStyles } from '@material-ui/core';
 import { IllegalNamingWarning } from '../warning/warning';
-import { IconType } from 'components/icon/icon';
+import { IconType, LockIcon } from 'components/icon/icon';
 import grey from '@material-ui/core/colors/grey';
 
 export interface Breadcrumb {
     label: string;
     icon?: IconType;
+    isFrozen?: boolean;
 }
 
 type CssRules = "item" | "currentItem" | "label" | "icon";
@@ -63,6 +64,9 @@ export const Breadcrumbs = withStyles(styles)(
                             onClick={() => onClick(item)}
                             onContextMenu={event => onContextMenu(event, item)}>
                             <Icon className={classes.icon} />
+                            {
+                                item.isFrozen ? <LockIcon className={classes.icon} /> : null
+                            }
                             <Typography
                                 noWrap
                                 color="inherit"
index 19b4beea1eb66274f45c2fc8ba1f1879a3194333..178c4cbe29676e65b01cf53c280b276908a81e5b 100644 (file)
@@ -73,6 +73,8 @@ 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 Lock from '@material-ui/icons/Lock'
+import LockOpen from '@material-ui/icons/LockOpen'
 
 // Import FontAwesome icons
 import { library } from '@fortawesome/fontawesome-svg-core';
@@ -153,6 +155,8 @@ export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...pro
 export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
 export const ProcessIcon: IconType = (props) => <BubbleChart {...props} />;
 export const ProjectIcon: IconType = (props) => <Folder {...props} />;
+export const LockIcon: IconType = (props) => <Lock {...props} />;
+export const UnlockIcon: IconType = (props) => <LockOpen {...props} />;
 export const FilterGroupIcon: IconType = (props) => <Pageview {...props} />;
 export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
 export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
index 03840d49ad232fe22d22a14c3762757690523894..066b520c9340be89f2da31a5cb204ca42d730a32 100644 (file)
@@ -22,7 +22,7 @@ import { fetchConfig } from 'common/config';
 import servicesProvider from 'common/service-provider';
 import { addMenuActionSet, ContextMenuKind } from 'views-components/context-menu/context-menu';
 import { rootProjectActionSet } from "views-components/context-menu/action-sets/root-project-action-set";
-import { filterGroupActionSet, projectActionSet, readOnlyProjectActionSet } from "views-components/context-menu/action-sets/project-action-set";
+import { filterGroupActionSet, frozenActionSet, projectActionSet, readOnlyProjectActionSet } from "views-components/context-menu/action-sets/project-action-set";
 import { resourceActionSet } from 'views-components/context-menu/action-sets/resource-action-set';
 import { favoriteActionSet } from "views-components/context-menu/action-sets/favorite-action-set";
 import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from 'views-components/context-menu/action-sets/collection-files-action-set';
@@ -101,6 +101,12 @@ addMenuActionSet(ContextMenuKind.GROUP_MEMBER, groupMemberActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
+addMenuActionSet(ContextMenuKind.FROZEN_PROJECT, [
+    [
+        ...frozenActionSet.reduce((prev, next) => prev.concat(next), []),
+        ...readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []),
+    ]
+]);
 addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
 addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
 addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
index b47b426f274989db009552aab3ecbd9e8c1bfa1e..b490864d8ff539b8e6be82e3c2634a2ee2506a80 100644 (file)
@@ -5,6 +5,7 @@
 import { GroupClass, GroupResource } from "./group";
 
 export interface ProjectResource extends GroupResource {
+    frozenByUuid: null|string;
     groupClass: GroupClass.PROJECT | GroupClass.FILTER | GroupClass.ROLE;
 }
 
diff --git a/src/store/breadcrumbs/breadcrumbs-middleware.ts b/src/store/breadcrumbs/breadcrumbs-middleware.ts
new file mode 100644 (file)
index 0000000..df8dd00
--- /dev/null
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { propertiesActions } from "store/properties/properties-actions";
+import { BREADCRUMBS } from "./breadcrumbs-actions";
+
+export const breadcrumbsMiddleware = store => next => action => {
+    propertiesActions.match(action, {
+        SET_PROPERTY: () => {
+
+            if (action.payload.key === BREADCRUMBS && Array.isArray(action.payload.value)) {
+                action.payload.value = action.payload
+                    .value.map((value)=> ({ ...value, isFrozen: !!store.getState().resources[value.uuid]?.frozenByUuid }));
+            }
+
+            next(action);
+        },
+        default: () => next(action)
+    });
+};
index 3e239feeaa8bb9cb27867db910b6222398e48495..3fb4d25d70af0e9d51844808696cc168dbdce99d 100644 (file)
@@ -21,6 +21,7 @@ import { CollectionResource } from 'models/collection';
 import { GroupClass, GroupResource } from 'models/group';
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { LinkResource } from 'models/link';
+import { resourceIsFrozen } from 'common/frozen-resources';
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -40,6 +41,8 @@ export type ContextMenuResource = {
     isEditable?: boolean;
     outputUuid?: string;
     workflowUuid?: string;
+    isAdmin?: boolean;
+    isFrozen?: boolean;
     storageClassesDesired?: string[];
     properties?: { [key: string]: string | string[] };
 };
@@ -224,10 +227,15 @@ export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
         const kind = extractUuidKind(uuid);
         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
+        const isFrozen = resourceIsFrozen(resource, getState().resources);
+        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly && !isFrozen;
 
-        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly;
         switch (kind) {
             case ResourceKind.PROJECT:
+                if (resource && !!(resource as any).frozenByUuid) {
+                    return ContextMenuKind.FROZEN_PROJECT;
+                }
+
                 return (isAdminUser && !readonly)
                     ? (resource && resource.groupClass !== GroupClass.FILTER)
                         ? ContextMenuKind.PROJECT_ADMIN
diff --git a/src/store/projects/project-lock-actions.ts b/src/store/projects/project-lock-actions.ts
new file mode 100644 (file)
index 0000000..f135522
--- /dev/null
@@ -0,0 +1,31 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { ServiceRepository } from "services/services";
+import { projectPanelActions } from "store/project-panel/project-panel-action";
+import { RootState } from "store/store";
+
+export const lockProject = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUUID = getState().auth.user!.uuid;
+
+        const updatedProject = await services.projectService.update(uuid, {
+            frozenByUuid: userUUID
+        });
+
+        dispatch(projectPanelActions.REQUEST_ITEMS());
+        return updatedProject;
+    };
+
+export const unlockProject = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+        const updatedProject = await services.projectService.update(uuid, {
+            frozenByUuid: null
+        });
+
+        dispatch(projectPanelActions.REQUEST_ITEMS());
+        return updatedProject;
+    };
\ No newline at end of file
index 94f110a09563ab17537b44445b964f650fef2ce5..7bc28ff4b5de6adb0492a0cda1cc63d5a0a7f206 100644 (file)
@@ -73,6 +73,7 @@ import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-pane
 import { Config } from 'common/config';
 import { pluginConfig } from 'plugins';
 import { MiddlewareListReducer } from 'common/plugintypes';
+import { breadcrumbsMiddleware } from './breadcrumbs/breadcrumbs-middleware';
 
 declare global {
     interface Window {
@@ -174,6 +175,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         publicFavoritesMiddleware,
         collectionsContentAddress,
         subprocessMiddleware,
+        breadcrumbsMiddleware,
     ];
 
     const reduceMiddlewaresFn: (a: Middleware[],
index a079bf4ff3c314b157d9f9bb8977a07a9c8300ee..77abb128a0f22e547680defd1cb4c9a21e64c599 100644 (file)
@@ -18,6 +18,8 @@ import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
 import { openWebDavS3InfoDialog } from "store/collections/collection-info-actions";
+import { ToggleLockAction } from "../actions/lock-action";
+import { lockProject, unlockProject } from "store/projects/project-lock-actions";
 
 export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     {
@@ -100,6 +102,23 @@ export const filterGroupActionSet: ContextMenuActionSet = [
     ]
 ];
 
+export const frozenActionSet: ContextMenuActionSet = [
+    [
+        {
+            component: ToggleLockAction,
+            name: 'ToggleLockAction',
+            execute: (dispatch, resource) => {
+                if (resource.isFrozen) {
+                    dispatch<any>(unlockProject(resource.uuid));
+                } else {
+                    dispatch<any>(lockProject(resource.uuid));
+                }
+
+            }
+        }
+    ]
+];
+
 export const projectActionSet: ContextMenuActionSet = [
     [
         ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
@@ -110,5 +129,6 @@ export const projectActionSet: ContextMenuActionSet = [
                 dispatch<any>(openProjectCreateDialog(resource.uuid));
             }
         },
+        ...frozenActionSet.reduce((prev, next) => prev.concat(next), []),
     ]
 ];
diff --git a/src/views-components/context-menu/actions/lock-action.tsx b/src/views-components/context-menu/actions/lock-action.tsx
new file mode 100644 (file)
index 0000000..9e66763
--- /dev/null
@@ -0,0 +1,34 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
+import { LockIcon, UnlockIcon } from "components/icon/icon";
+import { connect } from "react-redux";
+import { RootState } from "store/store";
+import { ProjectResource } from "models/project";
+import { withRouter, RouteComponentProps } from "react-router";
+
+const mapStateToProps = (state: RootState, props: { onClick: () => {} }) => ({
+    isAdmin: state.auth.user!.isAdmin,
+    isLocked: !!(state.resources[state.contextMenu.resource!.uuid] as ProjectResource).frozenByUuid,
+    onClick: props.onClick
+});
+
+export const ToggleLockAction = withRouter(connect(mapStateToProps)((props: { isLocked: boolean, isAdmin: boolean, onClick: () => void } & RouteComponentProps) =>
+    props.isLocked && !props.isAdmin ? null :
+        < ListItem
+            button
+            onClick={props.onClick} >
+            <ListItemIcon>
+                {props.isLocked
+                    ? <UnlockIcon />
+                    : <LockIcon />}
+            </ListItemIcon>
+            <ListItemText style={{ textDecoration: 'none' }}>
+                {props.isLocked
+                    ? <>Unfreeze project</>
+                    : <>Freeze project</>}
+            </ListItemText>
+        </ListItem >));
index 4766259a921dfc0728040dbe111dca3f7032a7b9..b66a9a844289d9da8d7d073326a2e1d2ed117f5f 100644 (file)
@@ -79,6 +79,7 @@ export enum ContextMenuKind {
     PROJECT = "Project",
     FILTER_GROUP = "FilterGroup",
     READONLY_PROJECT = 'ReadOnlyProject',
+    FROZEN_PROJECT = 'FrozenProject',
     PROJECT_ADMIN = "ProjectAdmin",
     FILTER_GROUP_ADMIN = "FilterGroupAdmin",
     RESOURCE = "Resource",
index 7822bdc6b4cd2411aaa37fc78a6a626200db35cf..c955ebec8faaa0f3db964e63dae08ef39251cbec 100644 (file)
@@ -15,6 +15,7 @@ import {
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { Resource, ResourceKind, TrashableResource } from 'models/resource';
 import {
+    LockIcon,
     ProjectIcon,
     FilterGroupIcon,
     CollectionIcon,
@@ -59,6 +60,7 @@ import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-a
 import { getUserUuid } from 'common/getuser';
 import { VirtualMachinesResource } from 'models/virtual-machines';
 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
+import { ProjectResource } from 'models/project';
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 
@@ -79,11 +81,32 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
             <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>}>
+            <LockIcon style={{ fontSize: "inherit" }}/>
+        </Tooltip>;
+    } else {
+        return null;
+    }
+}
+
 export const ResourceName = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
@@ -745,7 +768,7 @@ export const ResourceWithName = userFromID(_resourceWithName);
 
 export const UserNameFromID =
     compose(userFromID)(
-        (props: { uuid: string, userFullname: string, dispatch: Dispatch }) => {
+        (props: { uuid: string, displayAsText?: string, userFullname: string, dispatch: Dispatch }) => {
             const { uuid, userFullname, dispatch } = props;
 
             if (userFullname === '') {
index c813efb0a373f4080fe177b1234ce0222f5f5a1d..7874441588d51344247950f50d043de1c81e88fb 100644 (file)
@@ -21,6 +21,7 @@ import { extractUuidKind, ResourceKind } from 'models/resource';
 import { pluginConfig } from 'plugins';
 import { ElementListReducer } from 'common/plugintypes';
 import { Location } from 'history';
+import { ProjectResource } from 'models/project';
 
 type CssRules = 'button' | 'menuItem' | 'icon';
 
@@ -87,9 +88,10 @@ export const SidePanelButton = withStyles(styles)(
                 if (currentItemId === currentUserUUID) {
                     enabled = true;
                 } else if (matchProjectRoute(location ? location.pathname : '')) {
-                    const currentProject = getResource<GroupResource>(currentItemId)(resources);
-                    if (currentProject &&
+                    const currentProject = getResource<ProjectResource>(currentItemId)(resources);
+                    if (currentProject && currentProject.writableBy &&
                         currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
+                        !currentProject.frozenByUuid &&
                         !isProjectTrashed(currentProject, resources) &&
                         currentProject.groupClass !== GroupClass.FILTER) {
                         enabled = true;
index ccb40d53ba958b35645c14fb0af4a874e72f1b88..d9d14ae31c14252330be10990f8fb1eb7ad04e08 100644 (file)
@@ -45,6 +45,7 @@ import {
 import { GroupContentsResource } from 'services/groups-service/groups-service';
 import { GroupClass, GroupResource } from 'models/group';
 import { CollectionResource } from 'models/collection';
+import { resourceIsFrozen } from 'common/frozen-resources';
 
 type CssRules = 'root' | "button";
 
@@ -165,7 +166,7 @@ export const ProjectPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const { resources } = this.props;
+                const { resources, isAdmin } = this.props;
                 const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
                 // When viewing the contents of a filter group, all contents should be treated as read only.
                 let readonly = false;
@@ -183,6 +184,8 @@ export const ProjectPanel = withStyles(styles)(
                         isTrashed: ('isTrashed' in resource) ? resource.isTrashed : false,
                         kind: resource.kind,
                         menuKind,
+                        isAdmin,
+                        isFrozen: resourceIsFrozen(resource, resources),
                         description: resource.description,
                         storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
                         properties: ('properties' in resource) ? resource.properties : {},