17119: further changes after review feedback.
authorWard Vandewege <ward@curii.com>
Tue, 23 Mar 2021 16:40:56 +0000 (12:40 -0400)
committerWard Vandewege <ward@curii.com>
Tue, 23 Mar 2021 17:26:38 +0000 (13:26 -0400)
Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

13 files changed:
src/common/labels.ts
src/index.tsx
src/store/context-menu/context-menu-actions.test.ts
src/store/context-menu/context-menu-actions.ts
src/store/resource-type-filters/resource-type-filters.test.ts
src/store/resource-type-filters/resource-type-filters.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/views-components/context-menu/action-sets/project-action-set.test.ts
src/views-components/context-menu/action-sets/project-action-set.ts
src/views-components/context-menu/action-sets/project-admin-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/renderers.tsx
src/views/project-panel/project-panel.tsx

index c3c4fcd02733ac465bb2a4b27f63503a14bac9ee..cfc2c52c799ce96b2ef03647c8e0ba3d41d20613 100644 (file)
@@ -4,11 +4,14 @@
 
 import { ResourceKind } from "~/models/resource";
 
-export const resourceLabel = (type: string) => {
+export const resourceLabel = (type: string, subtype = '') => {
     switch (type) {
         case ResourceKind.COLLECTION:
             return "Data collection";
         case ResourceKind.PROJECT:
+            if (subtype === "filter") {
+                return "Filter group";
+            }
             return "Project";
         case ResourceKind.PROCESS:
             return "Process";
index 31ae85645f00763d75546905d7d1945765d93bc5..522d8dc1ee55676b777f09eaa3902ebecf53124d 100644 (file)
@@ -21,7 +21,7 @@ import { CustomTheme } from '~/common/custom-theme';
 import { fetchConfig } from '~/common/config';
 import { addMenuActionSet, ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { rootProjectActionSet } from "~/views-components/context-menu/action-sets/root-project-action-set";
-import { projectActionSet, readOnlyProjectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
+import { filterGroupActionSet, 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';
@@ -58,7 +58,7 @@ import { groupMemberActionSet } from '~/views-components/context-menu/action-set
 import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set';
 import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions';
 import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
-import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
+import { filterGroupAdminActionSet, projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
 import { storeRedirects } from './common/redirect-to';
@@ -68,6 +68,7 @@ console.log(`Starting arvados [${getBuildInfo()}]`);
 addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
 addMenuActionSet(ContextMenuKind.READONLY_PROJECT, readOnlyProjectActionSet);
+addMenuActionSet(ContextMenuKind.FILTER_GROUP, filterGroupActionSet);
 addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
 addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
@@ -96,6 +97,7 @@ addMenuActionSet(ContextMenuKind.GROUP_MEMBER, groupMemberActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
+addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
 
 storeRedirects();
 
index 7f6326f7f7b78ceed3257a51e0ce378810dab01b..179e3a3cff861a2e6df273d315b35d3add2ff5fc 100644 (file)
@@ -24,7 +24,7 @@ describe('context-menu-actions', () => {
 
         it('should return the correct menu kind', () => {
             const cases = [
-                // resourceUuid, isAdminUser, isEditable, isTrashed, inFilterGroup, expected
+                // resourceUuid, isAdminUser, isEditable, isTrashed, readonly, expected
                 [headCollectionUuid, false, true, true, false, ContextMenuKind.TRASHED_COLLECTION],
                 [headCollectionUuid, false, true, false, false, ContextMenuKind.COLLECTION],
                 [headCollectionUuid, false, true, false, true, ContextMenuKind.READONLY_COLLECTION],
@@ -87,10 +87,10 @@ describe('context-menu-actions', () => {
                 [containerRequestUuid, true, false, false, true, ContextMenuKind.READONLY_PROCESS_RESOURCE],
             ]
 
-            cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, inFilterGroup, expected]) => {
+            cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, readonly, expected]) => {
                 const initialState = {
                     properties: {
-                        [PROJECT_PANEL_CURRENT_UUID]: inFilterGroup ? filterGroupUuid : projectUuid,
+                        [PROJECT_PANEL_CURRENT_UUID]: projectUuid,
                     },
                     resources: {
                         [headCollectionUuid]: {
@@ -133,20 +133,15 @@ describe('context-menu-actions', () => {
                             isAdmin: isAdminUser,
                         },
                     },
-                    router: {
-                        location: {
-                            pathname: inFilterGroup ? "/projects/" + filterGroupUuid : "",
-                        },
-                    },
                 };
                 const store = mockStore(initialState);
 
                 let menuKind: any;
                 try {
-                    menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string))
+                    menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string, readonly as boolean))
                     expect(menuKind).toBe(expected);
                 } catch (err) {
-                    throw new Error(`menuKind for resource ${JSON.stringify(initialState.resources[resourceUuid as string])} expected to be ${expected} but got ${menuKind}.`);
+                    throw new Error(`menuKind for resource ${JSON.stringify(initialState.resources[resourceUuid as string])} readonly: ${readonly} expected to be ${expected} but got ${menuKind}.`);
                 }
             });
         });
index f8049a5c532b84ba9f092a0c7a7e0549257c153c..83335f83c5aa938d2716f05705695ed7eabce358 100644 (file)
@@ -20,9 +20,6 @@ import { ProcessResource } from '~/models/process';
 import { CollectionResource } from '~/models/collection';
 import { GroupClass, GroupResource } from '~/models/group';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
-import { getProjectPanelCurrentUuid } from '~/store/project-panel/project-panel-action';
-import { matchProjectRoute } from '~/routes/routes';
-import { RouterState } from "react-router-redux";
 
 export const contextMenuActions = unionize({
     OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
@@ -204,38 +201,23 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
         }
     };
 
-export const isProjectRoute = (router: RouterState) => {
-    const pathname = router.location ? router.location.pathname : '';
-    const matchProject = matchProjectRoute(pathname);
-    return Boolean(matchProject);
-};
-
-export const resourceUuidToContextMenuKind = (uuid: string) =>
+export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
     (dispatch: Dispatch, getState: () => RootState) => {
         const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
         const kind = extractUuidKind(uuid);
         const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
 
-        // When viewing the contents of a filter group, all contents should be treated as read only.
-        let inFilterGroup = false;
-        const { router } = getState();
-        if (isProjectRoute(router)) {
-            const projectUuid = getProjectPanelCurrentUuid(getState());
-            if (projectUuid !== undefined) {
-                const project = getResource<GroupResource>(projectUuid)(getState().resources);
-                if (project && project.groupClass === GroupClass.FILTER) {
-                    inFilterGroup = true;
-                }
-            }
-        }
-
-        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !inFilterGroup;
+        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly;
         switch (kind) {
             case ResourceKind.PROJECT:
-                return (isAdminUser && !inFilterGroup)
-                    ? ContextMenuKind.PROJECT_ADMIN
+                return (isAdminUser && !readonly)
+                    ? (resource && resource.groupClass !== GroupClass.FILTER)
+                        ? ContextMenuKind.PROJECT_ADMIN
+                        : ContextMenuKind.FILTER_GROUP_ADMIN
                     : isEditable
-                        ? ContextMenuKind.PROJECT
+                        ? (resource && resource.groupClass !== GroupClass.FILTER)
+                            ? ContextMenuKind.PROJECT
+                            : ContextMenuKind.FILTER_GROUP
                         : ContextMenuKind.READONLY_PROJECT;
             case ResourceKind.COLLECTION:
                 const c = getResource<CollectionResource>(uuid)(getState().resources);
@@ -246,15 +228,15 @@ export const resourceUuidToContextMenuKind = (uuid: string) =>
                     ? ContextMenuKind.OLD_VERSION_COLLECTION
                     : (isTrashed && isEditable)
                         ? ContextMenuKind.TRASHED_COLLECTION
-                        : (isAdminUser && !inFilterGroup)
+                        : (isAdminUser && !readonly)
                             ? ContextMenuKind.COLLECTION_ADMIN
                             : isEditable
                                 ? ContextMenuKind.COLLECTION
                                 : ContextMenuKind.READONLY_COLLECTION;
             case ResourceKind.PROCESS:
-                return (isAdminUser && !inFilterGroup)
+                return (isAdminUser && !readonly)
                     ? ContextMenuKind.PROCESS_ADMIN
-                    : inFilterGroup
+                    : readonly
                         ? ContextMenuKind.READONLY_PROCESS_RESOURCE
                         : ContextMenuKind.PROCESS_RESOURCE;
             case ResourceKind.USER:
index 2f4d3cad524fd3c04f99d147b28ffc4be31d7070..95d0349f11c3a37987972f23a295004f8a676e38 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter } from './resource-type-filters';
+import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter, GroupTypeFilter } from './resource-type-filters';
 import { ResourceKind } from '~/models/resource';
 import { deselectNode } from '~/models/tree';
 import { pipe } from 'lodash/fp';
@@ -73,4 +73,43 @@ describe("serializeResourceTypeFilters", () => {
         expect(serializedFilters)
             .toEqual(`["uuid","is_a",["${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","!=",null]`);
     });
+
+    it("should serialize all project types", () => {
+        const filters = pipe(
+            () => getInitialResourceTypeFilters(),
+            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.COLLECTION),
+        )();
+
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.GROUP}"]]`);
+    });
+
+    it("should serialize filter groups", () => {
+        const filters = pipe(
+            () => getInitialResourceTypeFilters(),
+            deselectNode(GroupTypeFilter.PROJECT)
+            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.COLLECTION),
+        )();
+
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.GROUP}"]],["groups.group_class","=","filter"]`);
+    });
+
+    it("should serialize projects (normal)", () => {
+        const filters = pipe(
+            () => getInitialResourceTypeFilters(),
+            deselectNode(GroupTypeFilter.FILTER_GROUP)
+            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.COLLECTION),
+        )();
+
+        const serializedFilters = serializeResourceTypeFilters(filters);
+        expect(serializedFilters)
+            .toEqual(`["uuid","is_a",["${ResourceKind.GROUP}"]],["groups.group_class","=","project"]`);
+    });
+
 });
index ef1198bc65d862554b4b655fa617a902ca9e6584..26db4e9e1c9ed1027adec8c03eb9735feecb42ad 100644 (file)
@@ -25,7 +25,12 @@ export enum ProcessStatusFilter {
 export enum ObjectTypeFilter {
     PROJECT = 'Project',
     PROCESS = 'Process',
-    COLLECTION = 'Data Collection',
+    COLLECTION = 'Data collection',
+}
+
+export enum GroupTypeFilter {
+    PROJECT = 'Project (normal)',
+    FILTER_GROUP = 'Filter group',
 }
 
 export enum CollectionTypeFilter {
@@ -62,7 +67,11 @@ export const getSimpleObjectTypeFilters = pipe(
 // causing compile issues.
 export const getInitialResourceTypeFilters = pipe(
     (): DataTableFilters => createTree<DataTableFilterItem>(),
-    initFilter(ObjectTypeFilter.PROJECT),
+    pipe(
+        initFilter(ObjectTypeFilter.PROJECT),
+        initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT),
+        initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT),
+    ),
     pipe(
         initFilter(ObjectTypeFilter.PROCESS),
         initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.PROCESS),
@@ -124,10 +133,14 @@ const objectTypeToResourceKind = (type: ObjectTypeFilter) => {
 };
 
 const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => {
+    const groupFilters = getMatchingFilters(values(GroupTypeFilter), selectedFilters);
     const collectionFilters = getMatchingFilters(values(CollectionTypeFilter), selectedFilters);
     const processFilters = getMatchingFilters(values(ProcessTypeFilter), selectedFilters);
     const typeFilters = pipe(
         () => new Set(getMatchingFilters(values(ObjectTypeFilter), selectedFilters)),
+        set => groupFilters.length > 0
+            ? set.add(ObjectTypeFilter.PROJECT)
+            : set,
         set => collectionFilters.length > 0
             ? set.add(ObjectTypeFilter.COLLECTION)
             : set,
@@ -182,6 +195,30 @@ const buildCollectionTypeFilters = ({ fb, filters }: { fb: FilterBuilder, filter
     }
 };
 
+const serializeGroupTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
+    () => getMatchingFilters(values(GroupTypeFilter), selectedFilters),
+    filters => filters,
+    mappedFilters => ({
+        fb: buildGroupTypeFilters({ fb, filters: mappedFilters, use_prefix: true }),
+        selectedFilters
+    })
+)();
+
+const GROUP_TYPES = values(GroupTypeFilter);
+
+const buildGroupTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilder, filters: string[], use_prefix: boolean }) => {
+    switch (true) {
+        case filters.length === 0 || filters.length === GROUP_TYPES.length:
+            return fb;
+        case includes(GroupTypeFilter.PROJECT, filters):
+            return fb.addEqual('groups.group_class', 'project');
+        case includes(GroupTypeFilter.FILTER_GROUP, filters):
+            return fb.addEqual('groups.group_class', 'filter');
+        default:
+            return fb;
+    }
+};
+
 const serializeProcessTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof createFiltersBuilder>) => pipe(
     () => getMatchingFilters(values(ProcessTypeFilter), selectedFilters),
     filters => filters,
@@ -210,6 +247,7 @@ const buildProcessTypeFilters = ({ fb, filters, use_prefix }: { fb: FilterBuilde
 export const serializeResourceTypeFilters = pipe(
     createFiltersBuilder,
     serializeObjectTypeFilters,
+    serializeGroupTypeFilters,
     serializeCollectionTypeFilters,
     serializeProcessTypeFilters,
     ({ fb }) => fb.getFilters(),
@@ -260,4 +298,4 @@ export const buildProcessStatusFilters = ( fb:FilterBuilder, activeStatusFilter:
         }
     }
     return fb;
-};
\ No newline at end of file
+};
index ff506103db6ce3ecf23e4e3d0fadbd26d8d5385b..05d619270fd2a6a5f1b732172ab4cb00ef5f2f44 100644 (file)
@@ -112,7 +112,7 @@ const loadSharedRoot = async (dispatch: Dispatch, getState: () => RootState, ser
     const params = {
         filters: `[${new FilterBuilder()
             .addIsA('uuid', ResourceKind.PROJECT)
-            .addEqual('group_class', GroupClass.PROJECT)
+            .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
             .addDistinct('uuid', getState().auth.config.uuidPrefix + '-j7d0g-publicfavorites')
             .getFilters()}]`,
         order: new OrderBuilder<ProjectResource>()
index fd328221a8a9ffca94e818b26f70cbdc8dc6d56a..1932194ca4b0c694a7dff83b964d76753ef7ae55 100644 (file)
@@ -2,11 +2,12 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { projectActionSet, readOnlyProjectActionSet } from "./project-action-set";
+import { filterGroupActionSet, projectActionSet, readOnlyProjectActionSet } from "./project-action-set";
 
 describe('project-action-set', () => {
     const flattProjectActionSet = projectActionSet.reduce((prev, next) => prev.concat(next), []);
     const flattReadOnlyProjectActionSet = readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []);
+    const flattFilterGroupActionSet = filterGroupActionSet.reduce((prev, next) => prev.concat(next), []);
 
     describe('projectActionSet', () => {
         it('should not be empty', () => {
@@ -33,4 +34,17 @@ describe('project-action-set', () => {
                 .not.toEqual(expect.arrayContaining(flattProjectActionSet));
         })
     });
-});
\ No newline at end of file
+
+    describe('filterGroupActionSet', () => {
+        it('should not be empty', () => {
+            // then
+            expect(flattFilterGroupActionSet.length).toBeGreaterThan(0);
+        });
+
+        it('should not contain projectActionSet items', () => {
+            // then
+            expect(flattFilterGroupActionSet)
+                .not.toEqual(expect.arrayContaining(flattProjectActionSet));
+        })
+    });
+});
index 57ba0ea3f1fcbcf98c9a763878ff6655a8a77a57..800f57d9f5ff13874d07b918c491a1ece4250c38 100644 (file)
@@ -66,16 +66,9 @@ export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     },
 ]];
 
-export const projectActionSet: ContextMenuActionSet = [
+export const filterGroupActionSet: ContextMenuActionSet = [
     [
         ...readOnlyProjectActionSet.reduce((prev, next) => prev.concat(next), []),
-        {
-            icon: NewProjectIcon,
-            name: "New project",
-            execute: (dispatch, resource) => {
-                dispatch<any>(openProjectCreateDialog(resource.uuid));
-            }
-        },
         {
             icon: RenameIcon,
             name: "Edit project",
@@ -106,3 +99,16 @@ export const projectActionSet: ContextMenuActionSet = [
         },
     ]
 ];
+
+export const projectActionSet: ContextMenuActionSet = [
+    [
+        ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
+        {
+            icon: NewProjectIcon,
+            name: "New project",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openProjectCreateDialog(resource.uuid));
+            }
+        },
+    ]
+];
index a3a8ce79e9ffd897c91dcb3751660abff71d4d54..982a78832740f942e89b377a879558b28ff096ac 100644 (file)
@@ -7,7 +7,7 @@ import { TogglePublicFavoriteAction } from "~/views-components/context-menu/acti
 import { togglePublicFavorite } from "~/store/public-favorites/public-favorites-actions";
 import { publicFavoritePanelActions } from "~/store/public-favorites-panel/public-favorites-action";
 
-import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
+import { projectActionSet, filterGroupActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
 
 export const projectAdminActionSet: ContextMenuActionSet = [[
     ...projectActionSet.reduce((prev, next) => prev.concat(next), []),
@@ -21,3 +21,16 @@ export const projectAdminActionSet: ContextMenuActionSet = [[
         }
     }
 ]];
+
+export const filterGroupAdminActionSet: ContextMenuActionSet = [[
+    ...filterGroupActionSet.reduce((prev, next) => prev.concat(next), []),
+    {
+        component: TogglePublicFavoriteAction,
+        name: 'TogglePublicFavoriteAction',
+        execute: (dispatch, resource) => {
+            dispatch<any>(togglePublicFavorite(resource)).then(() => {
+                dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+            });
+        }
+    }
+]];
index 7f2a29f8d1added32cf1a6437d183952a797b838..ee87d71a37d84da65a8f0594cf1cfe3130a97fc3 100644 (file)
@@ -67,8 +67,10 @@ export enum ContextMenuKind {
     API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
     ROOT_PROJECT = "RootProject",
     PROJECT = "Project",
+    FILTER_GROUP = "FilterGroup",
     READONLY_PROJECT = 'ReadOnlyProject',
     PROJECT_ADMIN = "ProjectAdmin",
+    FILTER_GROUP_ADMIN = "FilterGroupAdmin",
     RESOURCE = "Resource",
     FAVORITE = "Favorite",
     TRASH = "Trash",
index 28a6f253138f819dfb39274afe7e7818d43f5edc..93abb15e237ddc73424271f26762cbab3d6471bb 100644 (file)
@@ -468,16 +468,16 @@ export const ResourceOwnerWithName =
             </Typography>;
         });
 
-const renderType = (type: string) =>
+const renderType = (type: string, subtype: string) =>
     <Typography noWrap>
-        {resourceLabel(type)}
+        {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 : '' };
-    })((props: { type: string }) => renderType(props.type));
+        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) };
index 47dbd9b062b665f0c92f08a2d0079bb443833a0f..35a7f9c16068e87296db695cf0b63f0e0fbae722 100644 (file)
@@ -44,6 +44,7 @@ import {
     getInitialProcessStatusFilters
 } from '~/store/resource-type-filters/resource-type-filters';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
+import { GroupClass, GroupResource } from '~/models/group';
 
 type CssRules = 'root' | "button";
 
@@ -167,7 +168,14 @@ export const ProjectPanel = withStyles(styles)(
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
                 const { resources } = this.props;
                 const resource = getResource<GroupContentsResource>(resourceUuid)(resources);
-                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
+                // When viewing the contents of a filter group, all contents should be treated as read only.
+                let readonly = false;
+                const project = getResource<GroupResource>(this.props.currentItemId)(resources);
+                if (project && project.groupClass === GroupClass.FILTER) {
+                    readonly = true;
+                }
+
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid, readonly));
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: resource.name,