Merge branch '17119-support-filter-groups'
authorWard Vandewege <ward@curii.com>
Tue, 30 Mar 2021 21:17:54 +0000 (17:17 -0400)
committerWard Vandewege <ward@curii.com>
Tue, 30 Mar 2021 21:18:28 +0000 (17:18 -0400)
refs #17119

Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

27 files changed:
cypress/integration/side-panel.spec.js
src/common/labels.ts
src/components/icon/icon.tsx
src/components/tree/tree.tsx
src/index.tsx
src/models/group.ts
src/models/project.ts
src/services/project-service/project-service.test.ts
src/services/project-service/project-service.ts
src/store/context-menu/context-menu-actions.test.ts
src/store/context-menu/context-menu-actions.ts
src/store/groups-panel/groups-panel-middleware-service.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/store/tree-picker/tree-picker-actions.ts
src/views-components/context-menu/action-sets/process-resource-action-set.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-components/details-panel/project-details.tsx
src/views-components/sharing-dialog/participant-select.tsx
src/views-components/side-panel-button/side-panel-button.tsx
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views/project-panel/project-panel.tsx

index 309037ec58a9536112f62133cb2b46fa84936f69..e75a366e587d2a27eb3caf1529bfbe1c541d0c8c 100644 (file)
@@ -36,7 +36,7 @@ describe('Side panel tests', function() {
             .and('not.be.disabled');
     })
 
-    it('disables or enables the +NEW side panel button on depending on project permissions', function() {
+    it('disables or enables the +NEW side panel button depending on project permissions', function() {
         cy.loginAs(activeUser);
         [true, false].map(function(isWritable) {
             cy.createGroup(adminUser.token, {
@@ -75,4 +75,21 @@ describe('Side panel tests', function() {
                 .and('be.disabled');
         })
     })
-})
\ No newline at end of file
+
+    it('disables the +NEW side panel button when viewing filter group', function() {
+        cy.loginAs(adminUser);
+        cy.createGroup(adminUser.token, {
+            name: `my-favorite-filter-group`,
+            group_class: 'filter',
+        }).as('myFavoriteFilterGroup').then(function (myFavoriteFilterGroup) {
+            cy.contains('Refresh').click();
+            cy.doSearch(`${myFavoriteFilterGroup.uuid}`);
+            cy.get('[data-cy=breadcrumb-last]').should('contain', 'my-favorite-filter-group');
+
+            cy.get('[data-cy=side-panel-button]')
+                    .should('exist')
+                    .and(`be.disabled`);
+        })
+    })
+
+})
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 9eb60332e7261e0ba75f40bc62334432d988c4ff..6bbacaf4bebf6f368778e968ad275a550e960b19 100644 (file)
@@ -28,6 +28,7 @@ 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';
@@ -126,6 +127,7 @@ 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 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} />;
index 908ee28ca8b24a139c21ef76358a6cbeaa723c5b..cf4d708daaabab31eddd615baac1b82a298b88a7 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { List, ListItem, ListItemIcon, Checkbox, Radio, Collapse } from "@material-ui/core";
 import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core/styles';
-import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon } from '~/components/icon/icon';
+import { CollectionIcon, DefaultIcon, DirectoryIcon, FileIcon, ProjectIcon, FilterGroupIcon } from '~/components/icon/icon';
 import { ReactElement } from "react";
 import CircularProgress from '@material-ui/core/CircularProgress';
 import classnames from "classnames";
@@ -13,6 +13,7 @@ import classnames from "classnames";
 import { ArvadosTheme } from '~/common/custom-theme';
 import { SidePanelRightArrowIcon } from '../icon/icon';
 import { ResourceKind } from '~/models/resource';
+import { GroupClass } from '~/models/group';
 
 type CssRules = 'list'
     | 'listItem'
@@ -162,31 +163,35 @@ const FLAT_TREE_ACTIONS = {
     toggleActive: 'TOGGLE_ACTIVE',
 };
 
-const ItemIcon = React.memo(({type, kind, active, classes}: any) => {
+const ItemIcon = React.memo(({type, kind, active, groupClass, classes}: any) => {
     let Icon = ProjectIcon;
 
-        if (type) {
-            switch (type) {
-                case 'directory':
-                    Icon = DirectoryIcon;
-                    break;
-                case 'file':
-                    Icon = FileIcon;
-                    break;
-                default:
-                    Icon = DefaultIcon;
-            }
+    if (groupClass === GroupClass.FILTER) {
+        Icon = FilterGroupIcon;
+    }
+
+    if (type) {
+        switch (type) {
+            case 'directory':
+                Icon = DirectoryIcon;
+                break;
+            case 'file':
+                Icon = FileIcon;
+                break;
+            default:
+                Icon = DefaultIcon;
         }
+    }
 
-        if (kind) {
-            switch(kind) {
-                case ResourceKind.COLLECTION:
-                    Icon = CollectionIcon;
-                    break;
-                default:
-                    break;
-            }
+    if (kind) {
+        switch(kind) {
+            case ResourceKind.COLLECTION:
+                Icon = CollectionIcon;
+                break;
+            default:
+                break;
         }
+    }
 
     return <Icon className={classnames({ [classes.active]: active }, classes.childItemIcon)} />;
 });
@@ -228,7 +233,7 @@ const FlatTree = (props: FlatTreeProps) =>
                     </i>
                     <div data-action={FLAT_TREE_ACTIONS.toggleActive} className={props.classes.renderContainer}>
                         <span style={{ display: 'flex', alignItems: 'center' }}>
-                            <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} classes={props.classes} />
+                            <ItemIcon type={item.data.type} active={item.active} kind={item.data.kind} groupClass={item.data.kind === ResourceKind.GROUP ? item.data.groupClass : ''} classes={props.classes} />
                             <span style={{ fontSize: '0.875rem' }}>
                                 {item.data.name}
                             </span>
index b32066a46c69c37a9185bece05294319b6453fbc..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';
@@ -37,7 +37,7 @@ import { initWebSocket } from '~/websocket/websocket';
 import { Config } from '~/common/config';
 import { addRouteChangeHandlers } from './routes/route-change-handlers';
 import { setTokenDialogApiHost } from '~/store/token-dialog/token-dialog-actions';
-import { processResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
+import { processResourceActionSet, readOnlyProcessResourceActionSet } from '~/views-components/context-menu/action-sets/process-resource-action-set';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
 import { trashedCollectionActionSet } from '~/views-components/context-menu/action-sets/trashed-collection-action-set';
 import { setBuildInfo } from '~/store/app-info/app-info-actions';
@@ -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);
@@ -81,6 +82,7 @@ addMenuActionSet(ContextMenuKind.OLD_VERSION_COLLECTION, oldCollectionVersionAct
 addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS, processActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
+addMenuActionSet(ContextMenuKind.READONLY_PROCESS_RESOURCE, readOnlyProcessResourceActionSet);
 addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
 addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
@@ -95,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 e18c8ecbb96c6b67652ee51f2245ba022eaddd17..365e9ccebb9fc22341da3589f2dc993287d304c7 100644 (file)
@@ -15,5 +15,6 @@ export interface GroupResource extends TrashableResource {
 }
 
 export enum GroupClass {
-    PROJECT = "project"
+    PROJECT = 'project',
+    FILTER  = 'filter',
 }
index 8e101ce29ffeaac99cb7c2073aae96b8b79ac9e8..86ac04f6dd58222d869bd29980ed03715f0adba7 100644 (file)
@@ -5,7 +5,7 @@
 import { GroupClass, GroupResource } from "./group";
 
 export interface ProjectResource extends GroupResource {
-    groupClass: GroupClass.PROJECT;
+    groupClass: GroupClass.PROJECT | GroupClass.FILTER;
 }
 
 export const getProjectUrl = (uuid: string) => {
index 12eae0fec00be34f3399fc8280db8ea4a7d95681..3634b8cba60a3fc84621b4f12ef87c56ad9b53b6 100644 (file)
@@ -31,7 +31,7 @@ describe("CommonResourceService", () => {
         expect(axiosInstance.get).toHaveBeenCalledWith("/groups", {
             params: {
                 filters: "[" + new FilterBuilder()
-                    .addEqual("group_class", "project")
+                    .addIn("group_class", ["project", "filter"])
                     .getFilters() + "]",
                 order: undefined
             }
index 4ae91d4d088fe1c113e75d4191142719e39be618..515571e7d2a04113199530543eda3312d0dcd16b 100644 (file)
@@ -20,7 +20,7 @@ export class ProjectService extends GroupsService<ProjectResource> {
             filters: joinFilters(
                 args.filters || '',
                 new FilterBuilder()
-                    .addEqual("group_class", GroupClass.PROJECT)
+                    .addIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
                     .getFilters()
             )
         });
index 2778568e7681d1073a7f627e70bf9444b8aaee21..36976336a8f7d1409f18db7ea3a228fbbaa8920e 100644 (file)
@@ -6,6 +6,8 @@ import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { resourceUuidToContextMenuKind } from './context-menu-actions';
 import configureStore from 'redux-mock-store';
 import thunk from 'redux-thunk';
+import { PROJECT_PANEL_CURRENT_UUID } from '../project-panel/project-panel-action';
+import { GroupClass } from '~/models/group';
 
 describe('context-menu-actions', () => {
     describe('resourceUuidToContextMenuKind', () => {
@@ -16,70 +18,80 @@ describe('context-menu-actions', () => {
         const headCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaaa';
         const oldCollectionUuid = 'zzzzz-4zz18-aaaaaaaaaaaaaab';
         const projectUuid = 'zzzzz-j7d0g-ccccccccccccccc';
+        const filterGroupUuid = 'zzzzz-j7d0g-ccccccccccccccd';
         const linkUuid = 'zzzzz-o0j2j-0123456789abcde';
         const containerRequestUuid = 'zzzzz-xvhdp-0123456789abcde';
 
         it('should return the correct menu kind', () => {
             const cases = [
-                // resourceUuid, isAdminUser, isEditable, isTrashed, expected
-                [headCollectionUuid, false, true, true, ContextMenuKind.TRASHED_COLLECTION],
-                [headCollectionUuid, false, true, false, ContextMenuKind.COLLECTION],
-                [headCollectionUuid, false, false, true, ContextMenuKind.READONLY_COLLECTION],
-                [headCollectionUuid, false, false, false, ContextMenuKind.READONLY_COLLECTION],
-                [headCollectionUuid, true, true, true, ContextMenuKind.TRASHED_COLLECTION],
-                [headCollectionUuid, true, true, false, ContextMenuKind.COLLECTION_ADMIN],
-                [headCollectionUuid, true, false, true, ContextMenuKind.TRASHED_COLLECTION],
-                [headCollectionUuid, true, false, false, ContextMenuKind.COLLECTION_ADMIN],
+                // resourceUuid, isAdminUser, isEditable, isTrashed, forceReadonly, expected
+                [headCollectionUuid, false, true, true, false, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, false, true, false, false, ContextMenuKind.COLLECTION],
+                [headCollectionUuid, false, true, false, true, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, false, false, true, false, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, false, false, false, false, ContextMenuKind.READONLY_COLLECTION],
+                [headCollectionUuid, true, true, true, false, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, true, true, false, false, ContextMenuKind.COLLECTION_ADMIN],
+                [headCollectionUuid, true, false, true, false, ContextMenuKind.TRASHED_COLLECTION],
+                [headCollectionUuid, true, false, false, false, ContextMenuKind.COLLECTION_ADMIN],
+                [headCollectionUuid, true, false, false, true, ContextMenuKind.READONLY_COLLECTION],
 
-                [oldCollectionUuid, false, true, true, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, false, false, true, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, false, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, true, true, true, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, true, false, true, ContextMenuKind.OLD_VERSION_COLLECTION],
-                [oldCollectionUuid, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, false, false, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, true, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, true, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, false, true, false, ContextMenuKind.OLD_VERSION_COLLECTION],
+                [oldCollectionUuid, true, false, false, false, ContextMenuKind.OLD_VERSION_COLLECTION],
 
                 // FIXME: WB2 doesn't currently have context menu for trashed projects
-                // [projectUuid, false, true, true, ContextMenuKind.TRASHED_PROJECT],
-                [projectUuid, false, true, false, ContextMenuKind.PROJECT],
-                [projectUuid, false, false, true, ContextMenuKind.READONLY_PROJECT],
-                [projectUuid, false, false, false, ContextMenuKind.READONLY_PROJECT],
-                // [projectUuid, true, true, true, ContextMenuKind.TRASHED_PROJECT],
-                [projectUuid, true, true, false, ContextMenuKind.PROJECT_ADMIN],
-                // [projectUuid, true, false, true, ContextMenuKind.TRASHED_PROJECT],
-                [projectUuid, true, false, false, ContextMenuKind.PROJECT_ADMIN],
+                // [projectUuid, false, true, true, false, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, false, true, false, false, ContextMenuKind.PROJECT],
+                [projectUuid, false, true, false, true, ContextMenuKind.READONLY_PROJECT],
+                [projectUuid, false, false, true, false, ContextMenuKind.READONLY_PROJECT],
+                [projectUuid, false, false, false, false, ContextMenuKind.READONLY_PROJECT],
+                // [projectUuid, true, true, true, false, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, true, true, false, false, ContextMenuKind.PROJECT_ADMIN],
+                // [projectUuid, true, false, true, false, ContextMenuKind.TRASHED_PROJECT],
+                [projectUuid, true, false, false, false, ContextMenuKind.PROJECT_ADMIN],
+                [projectUuid, true, false, false, true, ContextMenuKind.READONLY_PROJECT],
 
-                [linkUuid, false, true, true, ContextMenuKind.LINK],
-                [linkUuid, false, true, false, ContextMenuKind.LINK],
-                [linkUuid, false, false, true, ContextMenuKind.LINK],
-                [linkUuid, false, false, false, ContextMenuKind.LINK],
-                [linkUuid, true, true, true, ContextMenuKind.LINK],
-                [linkUuid, true, true, false, ContextMenuKind.LINK],
-                [linkUuid, true, false, true, ContextMenuKind.LINK],
-                [linkUuid, true, false, false, ContextMenuKind.LINK],
+                [linkUuid, false, true, true, false, ContextMenuKind.LINK],
+                [linkUuid, false, true, false, false, ContextMenuKind.LINK],
+                [linkUuid, false, false, true, false, ContextMenuKind.LINK],
+                [linkUuid, false, false, false, false, ContextMenuKind.LINK],
+                [linkUuid, true, true, true, false, ContextMenuKind.LINK],
+                [linkUuid, true, true, false, false, ContextMenuKind.LINK],
+                [linkUuid, true, false, true, false, ContextMenuKind.LINK],
+                [linkUuid, true, false, false, false, ContextMenuKind.LINK],
 
-                [userUuid, false, true, true, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, false, true, false, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, false, false, true, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, false, false, false, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, true, true, true, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, true, true, false, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, true, false, true, ContextMenuKind.ROOT_PROJECT],
-                [userUuid, true, false, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, true, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, true, false, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, false, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, false, false, false, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, true, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, true, false, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, false, true, false, ContextMenuKind.ROOT_PROJECT],
+                [userUuid, true, false, false, false, ContextMenuKind.ROOT_PROJECT],
 
-                [containerRequestUuid, false, true, true, ContextMenuKind.PROCESS_RESOURCE],
-                [containerRequestUuid, false, true, false, ContextMenuKind.PROCESS_RESOURCE],
-                [containerRequestUuid, false, false, true, ContextMenuKind.PROCESS_RESOURCE],
-                [containerRequestUuid, false, false, false, ContextMenuKind.PROCESS_RESOURCE],
-                [containerRequestUuid, true, true, true, ContextMenuKind.PROCESS_ADMIN],
-                [containerRequestUuid, true, true, false, ContextMenuKind.PROCESS_ADMIN],
-                [containerRequestUuid, true, false, true, ContextMenuKind.PROCESS_ADMIN],
-                [containerRequestUuid, true, false, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, false, true, true, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, true, false, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, true, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, false, false, ContextMenuKind.PROCESS_RESOURCE],
+                [containerRequestUuid, false, false, false, true, ContextMenuKind.READONLY_PROCESS_RESOURCE],
+                [containerRequestUuid, true, true, true, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, true, false, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, true, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, false, false, ContextMenuKind.PROCESS_ADMIN],
+                [containerRequestUuid, true, false, false, true, ContextMenuKind.READONLY_PROCESS_RESOURCE],
             ]
 
-            cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, expected]) => {
+            cases.forEach(([resourceUuid, isAdminUser, isEditable, isTrashed, forceReadonly, expected]) => {
                 const initialState = {
+                    properties: {
+                        [PROJECT_PANEL_CURRENT_UUID]: projectUuid,
+                    },
                     resources: {
                         [headCollectionUuid]: {
                             uuid: headCollectionUuid,
@@ -91,12 +103,18 @@ describe('context-menu-actions', () => {
                             uuid: oldCollectionUuid,
                             currentVersionUuid: headCollectionUuid,
                             isTrashed: isTrashed,
-
                         },
                         [projectUuid]: {
                             uuid: projectUuid,
                             ownerUuid: isEditable ? userUuid : otherUserUuid,
                             writableBy: isEditable ? [userUuid] : [otherUserUuid],
+                            groupClass: GroupClass.PROJECT,
+                        },
+                        [filterGroupUuid]: {
+                            uuid: filterGroupUuid,
+                            ownerUuid: isEditable ? userUuid : otherUserUuid,
+                            writableBy: isEditable ? [userUuid] : [otherUserUuid],
+                            groupClass: GroupClass.FILTER,
                         },
                         [linkUuid]: {
                             uuid: linkUuid,
@@ -118,13 +136,14 @@ describe('context-menu-actions', () => {
                 };
                 const store = mockStore(initialState);
 
-                const menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string))
+                let menuKind: any;
                 try {
+                    menuKind = store.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid as string, forceReadonly 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])} forceReadonly: ${forceReadonly} expected to be ${expected} but got ${menuKind}.`);
                 }
             });
         });
     });
-});
\ No newline at end of file
+});
index 225538859a743a690e2da15143fe600d8e786fe6..83335f83c5aa938d2716f05705695ed7eabce358 100644 (file)
@@ -18,7 +18,7 @@ import { VirtualMachinesResource } from '~/models/virtual-machines';
 import { KeepServiceResource } from '~/models/keep-services';
 import { ProcessResource } from '~/models/process';
 import { CollectionResource } from '~/models/collection';
-import { GroupResource } from '~/models/group';
+import { GroupClass, GroupResource } from '~/models/group';
 import { GroupContentsResource } from '~/services/groups-service/groups-service';
 
 export const contextMenuActions = unionize({
@@ -201,19 +201,24 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
         }
     };
 
-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);
-        const isEditable = isAdminUser || (resource || {} as EditableResource).isEditable;
+
+        const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly;
         switch (kind) {
             case ResourceKind.PROJECT:
-                return !isAdminUser
-                    ? isEditable
-                        ? ContextMenuKind.PROJECT
-                        : ContextMenuKind.READONLY_PROJECT
-                    : ContextMenuKind.PROJECT_ADMIN;
+                return (isAdminUser && !readonly)
+                    ? (resource && resource.groupClass !== GroupClass.FILTER)
+                        ? ContextMenuKind.PROJECT_ADMIN
+                        : ContextMenuKind.FILTER_GROUP_ADMIN
+                    : isEditable
+                        ? (resource && resource.groupClass !== GroupClass.FILTER)
+                            ? ContextMenuKind.PROJECT
+                            : ContextMenuKind.FILTER_GROUP
+                        : ContextMenuKind.READONLY_PROJECT;
             case ResourceKind.COLLECTION:
                 const c = getResource<CollectionResource>(uuid)(getState().resources);
                 if (c === undefined) { return; }
@@ -223,15 +228,17 @@ export const resourceUuidToContextMenuKind = (uuid: string) =>
                     ? ContextMenuKind.OLD_VERSION_COLLECTION
                     : (isTrashed && isEditable)
                         ? ContextMenuKind.TRASHED_COLLECTION
-                        : isAdminUser
+                        : (isAdminUser && !readonly)
                             ? ContextMenuKind.COLLECTION_ADMIN
                             : isEditable
                                 ? ContextMenuKind.COLLECTION
                                 : ContextMenuKind.READONLY_COLLECTION;
             case ResourceKind.PROCESS:
-                return !isAdminUser
-                    ? ContextMenuKind.PROCESS_RESOURCE
-                    : ContextMenuKind.PROCESS_ADMIN;
+                return (isAdminUser && !readonly)
+                    ? ContextMenuKind.PROCESS_ADMIN
+                    : readonly
+                        ? ContextMenuKind.READONLY_PROCESS_RESOURCE
+                        : ContextMenuKind.PROCESS_RESOURCE;
             case ResourceKind.USER:
                 return ContextMenuKind.ROOT_PROJECT;
             case ResourceKind.LINK:
index f1576a23bdffdf26c43538ba151b6f3b1b278619..8589c7687efe496142f97ea16334651777ab6d83 100644 (file)
@@ -36,7 +36,7 @@ export class GroupsPanelMiddlewareService extends DataExplorerMiddlewareService
                     order.addOrder(direction, 'name');
                 }
                 const filters = new FilterBuilder()
-                    .addNotIn('group_class', [GroupClass.PROJECT])
+                    .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
                     .addILike('name', dataExplorer.searchValue)
                     .getFilters();
                 const response = await this.services.groupsService
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 d11f7527b4e0d7d18a1e6e9d1a0ecbddfae67e35..5d12b419ebe898e2666131e9d5bb85b4adcb98b0 100644 (file)
@@ -21,7 +21,7 @@ import { mapTree } from '../../models/tree';
 import { LinkResource, LinkClass } from "~/models/link";
 import { mapTreeValues } from "~/models/tree";
 import { sortFilesTree } from "~/services/collection-service/collection-service-files-response";
-import { GroupResource } from "~/models/group";
+import { GroupClass, GroupResource } from "~/models/group";
 
 export const treePickerActions = unionize({
     LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
@@ -101,11 +101,12 @@ interface LoadProjectParams {
     pickerId: string;
     includeCollections?: boolean;
     includeFiles?: boolean;
+    includeFilterGroups?: boolean;
     loadShared?: boolean;
 }
 export const loadProject = (params: LoadProjectParams) =>
     async (dispatch: Dispatch, _: () => RootState, services: ServiceRepository) => {
-        const { id, pickerId, includeCollections = false, includeFiles = false, loadShared = false } = params;
+        const { id, pickerId, includeCollections = false, includeFiles = false, includeFilterGroups = false, loadShared = false } = params;
 
         dispatch(treePickerActions.LOAD_TREE_PICKER_NODE({ id, pickerId }));
 
@@ -121,7 +122,12 @@ export const loadProject = (params: LoadProjectParams) =>
         dispatch<any>(receiveTreePickerData<GroupContentsResource>({
             id,
             pickerId,
-            data: items,
+            data: items.filter((item) => {
+                    if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
+                        return false;
+                    }
+                    return true;
+                }),
             extractNodeData: item => ({
                 id: item.uuid,
                 value: item,
index 8cab9bfd5171b39f1171def4376cfa2e9dd15df5..73a65a2d417f6006050f9e82939c73418367a27c 100644 (file)
@@ -14,21 +14,7 @@ import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions
 import { openRemoveProcessDialog } from "~/store/processes/processes-actions";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
-export const processResourceActionSet: ContextMenuActionSet = [[
-    {
-        icon: RenameIcon,
-        name: "Edit process",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessUpdateDialog(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
+export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
     {
         component: ToggleFavoriteAction,
         execute: (dispatch, resource) => {
@@ -37,13 +23,6 @@ export const processResourceActionSet: ContextMenuActionSet = [[
             });
         }
     },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openMoveProcessDialog(resource));
-        }
-    },
     {
         icon: CopyIcon,
         name: "Copy to project",
@@ -58,6 +37,31 @@ export const processResourceActionSet: ContextMenuActionSet = [[
             dispatch<any>(toggleDetailsPanel());
         }
     },
+]];
+
+export const processResourceActionSet: ContextMenuActionSet = [[
+    ...readOnlyProcessResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+    {
+        icon: RenameIcon,
+        name: "Edit process",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openProcessUpdateDialog(resource));
+        }
+    },
+    {
+        icon: ShareIcon,
+        name: "Share",
+        execute: (dispatch, { uuid }) => {
+            dispatch<any>(openSharingDialog(uuid));
+        }
+    },
+    {
+        icon: MoveToIcon,
+        name: "Move to",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openMoveProcessDialog(resource));
+        }
+    },
     {
         name: "Remove",
         icon: RemoveIcon,
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 219913cdd13ce4a2549779159a8ab8f62a4be9c7..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",
@@ -85,6 +87,7 @@ export enum ContextMenuKind {
     PROCESS = "Process",
     PROCESS_ADMIN = 'ProcessAdmin',
     PROCESS_RESOURCE = 'ProcessResource',
+    READONLY_PROCESS_RESOURCE = 'ReadOnlyProcessResource',
     PROCESS_LOGS = "ProcessLogs",
     REPOSITORY = "Repository",
     SSH_KEY = "SshKey",
index 6cf29faecf444683540de031966273298fc19314..93abb15e237ddc73424271f26762cbab3d6471bb 100644 (file)
@@ -6,7 +6,7 @@ import * as React from 'react';
 import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
 import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 import { ResourceKind, TrashableResource } from '~/models/resource';
-import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
+import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon } from '~/components/icon/icon';
 import { formatDate, formatFileSize, formatTime } from '~/common/formatters';
 import { resourceLabel } from '~/common/labels';
 import { connect, DispatchProp } from 'react-redux';
@@ -28,6 +28,7 @@ import { withResourceData } from '~/views-components/data-explorer/with-resource
 import { CollectionResource } from '~/models/collection';
 import { IllegalNamingWarning } from '~/components/warning/warning';
 import { loadResource } from '~/store/resources/resources-actions';
+import { GroupClass } from '~/models/group';
 
 const renderName = (dispatch: Dispatch, item: GroupContentsResource) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
@@ -59,6 +60,9 @@ export const ResourceName = connect(
 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) {
@@ -464,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 61797373b69a51ddb8dcd8039cf5180610ed5749..3c2bc0995f9cc1567dd34af7c7e5bcc5490130c0 100644 (file)
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { connect } from 'react-redux';
 import { openProjectPropertiesDialog } from '~/store/details-panel/details-panel-action';
-import { ProjectIcon, RenameIcon } from '~/components/icon/icon';
+import { ProjectIcon, RenameIcon, FilterGroupIcon } from '~/components/icon/icon';
 import { ProjectResource } from '~/models/project';
 import { formatDate } from '~/common/formatters';
 import { ResourceKind } from '~/models/resource';
@@ -18,9 +18,13 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { Dispatch } from 'redux';
 import { getPropertyChip } from '../resource-properties-form/property-chip';
 import { ResourceOwnerWithName } from '../data-explorer/renderers';
+import { GroupClass } from "~/models/group";
 
 export class ProjectDetails extends DetailsData<ProjectResource> {
     getIcon(className?: string) {
+        if (this.item.groupClass === GroupClass.FILTER) {
+            return <FilterGroupIcon className={className} />;
+        }
         return <ProjectIcon className={className} />;
     }
 
@@ -59,7 +63,7 @@ type ProjectDetailsComponentProps = ProjectDetailsComponentDataProps & ProjectDe
 const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
     withStyles(styles)(
         ({ classes, project, onClick }: ProjectDetailsComponentProps) => <div>
-            <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROJECT)} />
+            <DetailsAttribute label='Type' value={project.groupClass === GroupClass.FILTER ? 'Filter group' : resourceLabel(ResourceKind.PROJECT)} />
             <DetailsAttribute label='Owner' linkToUuid={project.ownerUuid}
                 uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
             <DetailsAttribute label='Last modified' value={formatDate(project.modifiedAt)} />
@@ -75,9 +79,12 @@ const ProjectDetailsComponent = connect(null, mapDispatchToProps)(
                 }
             </DetailsAttribute>
             <DetailsAttribute label='Properties'>
-                <div onClick={onClick}>
-                    <RenameIcon className={classes.editIcon} />
-                </div>
+                {project.groupClass !== GroupClass.FILTER ?
+                    <div onClick={onClick}>
+                        <RenameIcon className={classes.editIcon} />
+                    </div>
+                    : ''
+                }
             </DetailsAttribute>
             {
                 Object.keys(project.properties).map(k =>
index ea3775e93ad652a8463b95b3e28656e74a8f03de..0a61926e21ddc581fba0ed662f237fc2d3fd971a 100644 (file)
@@ -134,7 +134,7 @@ export const ParticipantSelect = connect()(
             const userItems: ListResults<any> = await userService.list({ filters: filterUsers, limit, count: "none" });
 
             const filterGroups = new FilterBuilder()
-                .addNotIn('group_class', [GroupClass.PROJECT])
+                .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER])
                 .addILike('name', value)
                 .getFilters();
 
index 3ca2f0d66e95d4cc552c54a70ca27f4644063d96..bf03bf6cb0e9f2b7bcf7be2872debd42fba5363e 100644 (file)
@@ -15,7 +15,7 @@ import { navigateToRunProcess } from '~/store/navigation/navigation-action';
 import { runProcessPanelActions } from '~/store/run-process-panel/run-process-panel-actions';
 import { getUserUuid } from '~/common/getuser';
 import { matchProjectRoute } from '~/routes/routes';
-import { GroupResource } from '~/models/group';
+import { GroupClass, GroupResource } from '~/models/group';
 import { ResourcesState, getResource } from '~/store/resources/resources';
 import { extractUuidKind, ResourceKind } from '~/models/resource';
 
@@ -87,7 +87,8 @@ export const SidePanelButton = withStyles(styles)(
                     const currentProject = getResource<GroupResource>(currentItemId)(resources);
                     if (currentProject &&
                         currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
-                        !isProjectTrashed(currentProject, resources)) {
+                        !isProjectTrashed(currentProject, resources) &&
+                        currentProject.groupClass !== GroupClass.FILTER) {
                         enabled = true;
                     }
                 }
@@ -150,4 +151,4 @@ export const SidePanelButton = withStyles(styles)(
             }
         }
     )
-);
\ No newline at end of file
+);
index e0d9777da006aa317f9684eba4492ffd5934ff48..4c6f01a146c910885de60244924529aaed575f59 100644 (file)
@@ -9,13 +9,15 @@ import { TreePicker, TreePickerProps } from "../tree-picker/tree-picker";
 import { TreeItem } from "~/components/tree/tree";
 import { ProjectResource } from "~/models/project";
 import { ListItemTextIcon } from "~/components/list-item-text-icon/list-item-text-icon";
-import { ProcessIcon, ProjectIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon } from '~/components/icon/icon';
+import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon } from '~/components/icon/icon';
 import { WorkflowIcon } from '~/components/icon/icon';
 import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from '~/store/side-panel-tree/side-panel-tree-actions';
 import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-actions';
 import { noop } from 'lodash';
 import { ResourceKind } from "~/models/resource";
 import { IllegalNamingWarning } from "~/components/warning/warning";
+import { GroupClass } from "~/models/group";
+
 export interface SidePanelTreeProps {
     onItemActivation: (id: string) => void;
     sidePanelProgress?: boolean;
@@ -58,7 +60,9 @@ const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
 const getProjectPickerIcon = (item: TreeItem<ProjectResource | string>) =>
     typeof item.data === 'string'
         ? getSidePanelIcon(item.data)
-        : ProjectIcon;
+        : (item.data && item.data.groupClass === GroupClass.FILTER)
+            ? FilterGroupIcon
+            : ProjectIcon;
 
 const getSidePanelIcon = (category: string) => {
     switch (category) {
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,