Merge branch '18984-project-type-filters-2' into main. Closes #18984
authorStephen Smith <stephen@curii.com>
Tue, 7 Jun 2022 21:37:00 +0000 (17:37 -0400)
committerStephen Smith <stephen@curii.com>
Tue, 7 Jun 2022 21:37:00 +0000 (17:37 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

24 files changed:
src/components/data-explorer/data-explorer.tsx
src/components/data-table-default-view/data-table-default-view.tsx
src/components/data-table-filters/data-table-filters-popover.tsx
src/components/data-table-filters/data-table-filters-tree.tsx
src/components/data-table/data-table.tsx
src/components/default-view/default-view.tsx
src/components/tree/tree.tsx
src/models/tree.ts
src/store/resource-type-filters/resource-type-filters.test.ts
src/store/resource-type-filters/resource-type-filters.ts
src/views/all-processes-panel/all-processes-panel.tsx
src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx
src/views/collection-content-address-panel/collection-content-address-panel.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/group-details-panel/group-details-panel.tsx
src/views/link-panel/link-panel-root.tsx
src/views/project-panel/project-panel.tsx
src/views/public-favorites-panel/public-favorites-panel.tsx
src/views/shared-with-me-panel/shared-with-me-panel.tsx
src/views/subprocess-panel/subprocess-panel-root.tsx
src/views/trash-panel/trash-panel.tsx
src/views/user-panel/user-panel.tsx
src/views/user-profile-panel/user-profile-panel-root.tsx
src/views/workflow-panel/workflow-panel-view.tsx

index 0363d33399c1dc90299fd2a73f78ca8d22cb6c0b..40617f73336b197149790264b2d379c4616d1e3f 100644 (file)
@@ -11,7 +11,7 @@ import { SearchInput } from 'components/search-input/search-input';
 import { ArvadosTheme } from "common/custom-theme";
 import { createTree } from 'models/tree';
 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
-import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { CloseIcon, IconType, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
 import { PaperProps } from '@material-ui/core/Paper';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 
@@ -64,7 +64,8 @@ interface DataExplorerDataProps<T> {
     rowsPerPageOptions: number[];
     page: number;
     contextMenuColumn: boolean;
-    dataTableDefaultView?: React.ReactNode;
+    defaultViewIcon?: IconType;
+    defaultViewMessages?: string[];
     working?: boolean;
     currentRefresh?: string;
     currentRoute?: string;
@@ -149,7 +150,7 @@ export const DataExplorer = withStyles(styles)(
                 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
-                dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
+                defaultViewIcon, defaultViewMessages, hideColumnSelector, actions, paperProps, hideSearchInput,
                 paperKey, fetchMode, currentItemUuid, title,
                 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
             } = this.props;
@@ -197,7 +198,8 @@ export const DataExplorer = withStyles(styles)(
                     onSortToggle={onSortToggle}
                     extractKey={extractKey}
                     working={this.state.showLoading}
-                    defaultView={dataTableDefaultView}
+                    defaultViewIcon={defaultViewIcon}
+                    defaultViewMessages={defaultViewMessages}
                     currentItemUuid={currentItemUuid}
                     currentRoute={paperKey} /></Grid>
                 <Grid item xs><Toolbar className={classes.footer}>
index 2869ab8270fbe08abc16a48ced60d2c00f9c45d5..b245c19ba96d27290eacb1f5d11065d97b9522d4 100644 (file)
@@ -16,12 +16,13 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginBottom: theme.spacing.unit * 4,
     },
 });
-type DataTableDefaultViewDataProps = Partial<Pick<DefaultViewDataProps, 'icon' | 'messages'>>;
+type DataTableDefaultViewDataProps = Partial<Pick<DefaultViewDataProps, 'icon' | 'messages' | 'filtersApplied'>>;
 type DataTableDefaultViewProps = DataTableDefaultViewDataProps & WithStyles<CssRules>;
 
 export const DataTableDefaultView = withStyles(styles)(
     ({ classes, ...props }: DataTableDefaultViewProps) => {
         const icon = props.icon || DetailsIcon;
-        const messages = props.messages || ['No items found'];
+        const filterWarning: string[] = props.filtersApplied ? ['Filters are applied to the data.'] : [];
+        const messages = filterWarning.concat(props.messages || ['No items found']);
         return <DefaultView {...classes} {...{ icon, messages }} />;
     });
index 3183157bf968e1470c454c9061b4b2338f891030..b51878664449b67a9f3a33b992fa9a0605820173 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import React from "react";
+import React, { useEffect } from "react";
 import {
     WithStyles,
     withStyles,
@@ -23,6 +23,7 @@ import { DefaultTransformOrigin } from "components/popover/helpers";
 import { createTree } from 'models/tree';
 import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
 import { getNodeDescendants } from 'models/tree';
+import debounce from "lodash/debounce";
 
 export type CssRules = "root" | "icon" | "iconButton" | "active" | "checkbox";
 
@@ -127,7 +128,7 @@ export const DataTableFiltersPopover = withStyles(styles)(
                     open={!!this.state.anchorEl}
                     anchorOrigin={DefaultTransformOrigin}
                     transformOrigin={DefaultTransformOrigin}
-                    onClose={this.cancel}>
+                    onClose={this.close}>
                     <Card>
                         <CardContent>
                             <Typography variant="caption">
@@ -137,36 +138,21 @@ export const DataTableFiltersPopover = withStyles(styles)(
                         <DataTableFiltersTree
                             filters={this.state.filters}
                             mutuallyExclusive={this.props.mutuallyExclusive}
-                            onChange={filters => {
-                                this.setState({ filters });
-                                if (this.props.mutuallyExclusive) {
-                                    const { onChange } = this.props;
-                                    if (onChange) {
-                                        onChange(filters);
-                                    }
-                                    this.setState({ anchorEl: undefined });
-                                }
-                            }} />
+                            onChange={this.onChange} />
                         {this.props.mutuallyExclusive ||
                         <CardActions>
-                            <Button
-                                color="primary"
-                                variant='contained'
-                                size="small"
-                                onClick={this.submit}>
-                                Ok
-                            </Button>
                             <Button
                                 color="primary"
                                 variant="outlined"
                                 size="small"
-                                onClick={this.cancel}>
-                                Cancel
+                                onClick={this.close}>
+                                Close
                             </Button>
                         </CardActions >
                         }
                     </Card>
                 </Popover>
+                <this.MountHandler />
             </>;
         }
 
@@ -180,25 +166,43 @@ export const DataTableFiltersPopover = withStyles(styles)(
             this.setState({ anchorEl: this.icon.current || undefined });
         }
 
-        submit = () => {
+        onChange = (filters) => {
+            this.setState({ filters });
+            if (this.props.mutuallyExclusive) {
+                // Mutually exclusive filters apply immediately
+                const { onChange } = this.props;
+                if (onChange) {
+                    onChange(filters);
+                }
+                this.close();
+            } else {
+                // Non-mutually exclusive filters are debounced
+                this.submit();
+            }
+        }
+
+        submit = debounce (() => {
             const { onChange } = this.props;
             if (onChange) {
                 onChange(this.state.filters);
             }
-            this.setState({ anchorEl: undefined });
-        }
+        }, 1000);
+
+        MountHandler = () => {
+            useEffect(() => {
+                return () => {
+                    this.submit.cancel();
+                }
+            },[]);
+            return null;
+        };
 
-        cancel = () => {
+        close = () => {
             this.setState(prev => ({
                 ...prev,
-                filters: prev.prevFilters,
                 anchorEl: undefined
             }));
         }
 
-        setFilters = (filters: DataTableFilters) => {
-            this.setState({ filters });
-        }
-
     }
 );
index 6514078dea44891063a33a132421c9c5fb5ff460..7b97865bba4b546085d6c90ab7cb68a7b5821e45 100644 (file)
@@ -34,7 +34,7 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
             levelIndentation={hasSubfilters ? 20 : 0}
             itemRightPadding={20}
             items={filtersToTree(filters)}
-            render={renderItem}
+            render={this.props.mutuallyExclusive ? renderRadioItem : renderItem}
             showSelection
             useRadioButtons={this.props.mutuallyExclusive}
             disableRipple
@@ -76,13 +76,24 @@ export class DataTableFiltersTree extends React.Component<DataTableFilterProps>
 }
 
 const renderItem = (item: TreeItem<DataTableFilterItem>) =>
-    <span>{item.data.name}</span>;
+    <span>
+        {item.data.name}
+        {item.initialState !== item.selected ? <>
+            *
+        </> : null}
+    </span>;
+
+const renderRadioItem = (item: TreeItem<DataTableFilterItem>) =>
+    <span>
+        {item.data.name}
+    </span>;
 
 const filterToTreeItem = (filters: DataTableFilters) =>
     (id: string): TreeItem<any> => {
         const node = getNode(id)(filters) || initTreeNode({ id: '', value: 'InvalidNode' });
         const items = getNodeChildrenIds(node.id)(filters)
             .map(filterToTreeItem(filters));
+        const isIndeterminate = !node.selected && items.some(i => i.selected || i.indeterminate);
 
         return {
             active: node.active,
@@ -91,6 +102,8 @@ const filterToTreeItem = (filters: DataTableFilters) =>
             items: items.length > 0 ? items : undefined,
             open: node.expanded,
             selected: node.selected,
+            initialState: node.initialState,
+            indeterminate: isIndeterminate,
             status: TreeItemStatus.LOADED,
         };
     };
index 14dfdacaf9707acaba47283555edf80138750d16..d942234d0fcb4841d609850de408ed3c3cdfe3e1 100644 (file)
@@ -9,8 +9,8 @@ import { DataColumn, SortDirection } from './data-column';
 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
 import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
 import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
-import { countNodes } from 'models/tree';
-import { PendingIcon } from 'components/icon/icon';
+import { countNodes, getTreeDirty } from 'models/tree';
+import { IconType, PendingIcon } from 'components/icon/icon';
 import { SvgIconProps } from '@material-ui/core/SvgIcon';
 import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
 
@@ -31,7 +31,8 @@ export interface DataTableDataProps<T> {
     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
     extractKey?: (item: T) => React.Key;
     working?: boolean;
-    defaultView?: React.ReactNode;
+    defaultViewIcon?: IconType;
+    defaultViewMessages?: string[];
     currentItemUuid?: string;
     currentRoute?: string;
 }
@@ -105,15 +106,17 @@ export const DataTable = withStyles(styles)(
                                 icon={PendingIcon}
                                 messages={['Loading data, please wait.']} />
                         </div> }
-                    {items.length === 0 && !working && this.renderNoItemsPlaceholder()}
+                    {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
                 </div>
             </div>;
         }
 
-        renderNoItemsPlaceholder = () => {
-            return this.props.defaultView
-                ? this.props.defaultView
-                : <DataTableDefaultView />;
+        renderNoItemsPlaceholder = (columns: DataColumns<T>) => {
+            const dirty = columns.some((column) => getTreeDirty('')(column.filters));
+            return <DataTableDefaultView
+                icon={this.props.defaultViewIcon}
+                messages={this.props.defaultViewMessages}
+                filtersApplied={dirty} />;
         }
 
         renderHeadCell = (column: DataColumn<T>, index: number) => {
index 6e89db25d573066257d60671d4202878ff4f19d5..014b8cc48a7022fbf04f07e9895521c171175f9d 100644 (file)
@@ -27,6 +27,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export interface DefaultViewDataProps {
     classRoot?: string;
     messages: string[];
+    filtersApplied?: boolean;
     classMessage?: string;
     icon: IconType;
     classIcon?: string;
@@ -43,4 +44,4 @@ export const DefaultView = withStyles(styles)(
                     className={classnames([classes.message, classMessage])}>{msg}</Typography>;
             })}
         </Typography>
-);
\ No newline at end of file
+);
index 3ae884b67cb1e6e51e452ea6e68df0155be7cc1b..fc9dbc743ae19a15d59172bd2c30734eb223cd11 100644 (file)
@@ -97,6 +97,8 @@ export interface TreeItem<T> {
     open: boolean;
     active: boolean;
     selected?: boolean;
+    initialState?: boolean;
+    indeterminate?: boolean;
     flatTree?: boolean;
     status: TreeItemStatus;
     items?: Array<TreeItem<T>>;
@@ -292,6 +294,7 @@ export const Tree = withStyles(styles)(
                             {showSelection(it) && !useRadioButtons &&
                                 <Checkbox
                                     checked={it.selected}
+                                    indeterminate={!it.selected && it.indeterminate}
                                     className={classes.checkbox}
                                     color="primary"
                                     onClick={this.handleCheckboxChange(it)} />}
index e92913887a0dfc7b28e21ae20b047dc68d61f148..996f98a465865ee5fd0861bcc391a505735ef115 100644 (file)
@@ -14,6 +14,7 @@ export interface TreeNode<T = any> {
     parent: string;
     active: boolean;
     selected: boolean;
+    initialState?: boolean;
     expanded: boolean;
     status: TreeNodeStatus;
 }
@@ -197,6 +198,19 @@ export const initTreeNode = <T>(data: Pick<TreeNode<T>, 'id' | 'value'> & { pare
     ...data,
 });
 
+export const getTreeDirty = (id: string) => <T>(tree: Tree<T>): boolean => {
+    const node = getNode(id)(tree);
+    const children = getNodeDescendants(id)(tree);
+    return (node
+            && node.initialState !== undefined
+            && node.selected !== node.initialState
+            )
+            || children.some(child =>
+                child.initialState !== undefined
+                && child.selected !== child.initialState
+            );
+}
+
 const toggleDescendantsSelection = (id: string) => <T>(tree: Tree<T>) => {
     const node = getNode(id)(tree);
     if (node) {
@@ -228,7 +242,6 @@ const toggleParentNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
     return tree;
 };
 
-
 const mapNodeValue = <T, R>(mapFn: (value: T) => R) => (node: TreeNode<T>): TreeNode<R> =>
     ({ ...node, value: mapFn(node.value) });
 
index f001770e8a283e441935044b2fbcdf8171666033..5972f60c1121a8a5da578272b467024cbaa5889d 100644 (file)
@@ -4,7 +4,7 @@
 
 import { getInitialResourceTypeFilters, serializeResourceTypeFilters, ObjectTypeFilter, CollectionTypeFilter, ProcessTypeFilter, GroupTypeFilter, buildProcessStatusFilters, ProcessStatusFilter } from './resource-type-filters';
 import { ResourceKind } from 'models/resource';
-import { deselectNode } from 'models/tree';
+import { selectNode, deselectNode } from 'models/tree';
 import { pipe } from 'lodash/fp';
 import { FilterBuilder } from 'services/api/filter-builder';
 
@@ -31,21 +31,21 @@ describe("serializeResourceTypeFilters", () => {
         const filters = getInitialResourceTypeFilters();
         const serializedFilters = serializeResourceTypeFilters(filters);
         expect(serializedFilters)
-            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.COLLECTION}","${ResourceKind.WORKFLOW}"]]`);
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.COLLECTION}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
     });
 
     it("should serialize all but collection filters", () => {
         const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
         const serializedFilters = serializeResourceTypeFilters(filters);
         expect(serializedFilters)
-            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.WORKFLOW}"]]`);
+            .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
     });
 
     it("should serialize output collections and projects", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROCESS),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
             deselectNode(CollectionTypeFilter.LOG_COLLECTION),
             deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
@@ -59,8 +59,8 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize intermediate collections and projects", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROCESS),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
             deselectNode(CollectionTypeFilter.LOG_COLLECTION),
             deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION),
@@ -75,8 +75,8 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
-            deselectNode(ObjectTypeFilter.PROCESS),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
         )();
 
@@ -91,7 +91,7 @@ describe("serializeResourceTypeFilters", () => {
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ProcessTypeFilter.CHILD_PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -104,8 +104,10 @@ describe("serializeResourceTypeFilters", () => {
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ProcessTypeFilter.MAIN_PROCESS),
+            deselectNode(ObjectTypeFilter.DEFINITION),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+
+            selectNode(ProcessTypeFilter.CHILD_PROCESS),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -116,9 +118,9 @@ describe("serializeResourceTypeFilters", () => {
     it("should serialize all project types", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
-            deselectNode(ObjectTypeFilter.PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -130,9 +132,9 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(GroupTypeFilter.PROJECT),
-            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -144,9 +146,9 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(GroupTypeFilter.FILTER_GROUP),
-            deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.DEFINITION),
+            deselectNode(ProcessTypeFilter.MAIN_PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
-            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
index 64a391ca9e5419ac7c3e9c79e4e4a04606304c93..361b52a6ae47f1449dcd99ffbb6f0028f52df01a 100644 (file)
@@ -25,9 +25,9 @@ export enum ProcessStatusFilter {
 
 export enum ObjectTypeFilter {
     PROJECT = 'Project',
-    PROCESS = 'Process',
-    COLLECTION = 'Data collection',
     WORKFLOW = 'Workflow',
+    COLLECTION = 'Data collection',
+    DEFINITION = 'Definition',
 }
 
 export enum GroupTypeFilter {
@@ -43,11 +43,11 @@ export enum CollectionTypeFilter {
 }
 
 export enum ProcessTypeFilter {
-    MAIN_PROCESS = 'Main',
-    CHILD_PROCESS = 'Child',
+    MAIN_PROCESS = 'Runs',
+    CHILD_PROCESS = 'Intermediate Steps',
 }
 
-const initFilter = (name: string, parent = '', isSelected?: boolean) =>
+const initFilter = (name: string, parent = '', isSelected?: boolean, isExpanded?: boolean) =>
     setNode<DataTableFilterItem>({
         id: name,
         value: { name },
@@ -55,16 +55,17 @@ const initFilter = (name: string, parent = '', isSelected?: boolean) =>
         children: [],
         active: false,
         selected: isSelected !== undefined ? isSelected : true,
-        expanded: false,
+        initialState: isSelected !== undefined ? isSelected : true,
+        expanded: isExpanded !== undefined ? isExpanded : false,
         status: TreeNodeStatus.LOADED,
     });
 
 export const getSimpleObjectTypeFilters = pipe(
     (): DataTableFilters => createTree<DataTableFilterItem>(),
     initFilter(ObjectTypeFilter.PROJECT),
-    initFilter(ObjectTypeFilter.PROCESS),
-    initFilter(ObjectTypeFilter.COLLECTION),
     initFilter(ObjectTypeFilter.WORKFLOW),
+    initFilter(ObjectTypeFilter.COLLECTION),
+    initFilter(ObjectTypeFilter.DEFINITION),
 );
 
 // Using pipe() with more than 7 arguments makes the return type be 'any',
@@ -72,23 +73,23 @@ export const getSimpleObjectTypeFilters = pipe(
 export const getInitialResourceTypeFilters = pipe(
     (): DataTableFilters => createTree<DataTableFilterItem>(),
     pipe(
-        initFilter(ObjectTypeFilter.PROJECT),
+        initFilter(ObjectTypeFilter.PROJECT, '', true, true),
         initFilter(GroupTypeFilter.PROJECT, ObjectTypeFilter.PROJECT),
         initFilter(GroupTypeFilter.FILTER_GROUP, ObjectTypeFilter.PROJECT),
     ),
     pipe(
-        initFilter(ObjectTypeFilter.PROCESS),
-        initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.PROCESS),
-        initFilter(ProcessTypeFilter.CHILD_PROCESS, ObjectTypeFilter.PROCESS)
+        initFilter(ObjectTypeFilter.WORKFLOW, '', false, true),
+        initFilter(ObjectTypeFilter.DEFINITION, ObjectTypeFilter.WORKFLOW),
+        initFilter(ProcessTypeFilter.MAIN_PROCESS, ObjectTypeFilter.WORKFLOW),
+        initFilter(ProcessTypeFilter.CHILD_PROCESS, ObjectTypeFilter.WORKFLOW, false),
     ),
     pipe(
-        initFilter(ObjectTypeFilter.COLLECTION),
+        initFilter(ObjectTypeFilter.COLLECTION, '', true, true),
         initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
     ),
-    initFilter(ObjectTypeFilter.WORKFLOW)
 
 );
 
@@ -133,11 +134,11 @@ const objectTypeToResourceKind = (type: ObjectTypeFilter) => {
     switch (type) {
         case ObjectTypeFilter.PROJECT:
             return ResourceKind.PROJECT;
-        case ObjectTypeFilter.PROCESS:
+        case ObjectTypeFilter.WORKFLOW:
             return ResourceKind.PROCESS;
         case ObjectTypeFilter.COLLECTION:
             return ResourceKind.COLLECTION;
-        case ObjectTypeFilter.WORKFLOW:
+        case ObjectTypeFilter.DEFINITION:
             return ResourceKind.WORKFLOW;
     }
 };
@@ -155,7 +156,7 @@ const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof c
             ? set.add(ObjectTypeFilter.COLLECTION)
             : set,
         set => processFilters.length > 0
-            ? set.add(ObjectTypeFilter.PROCESS)
+            ? set.add(ObjectTypeFilter.WORKFLOW)
             : set,
         set => Array.from(set)
     )();
@@ -163,7 +164,7 @@ const serializeObjectTypeFilters = ({ fb, selectedFilters }: ReturnType<typeof c
     return {
         fb: typeFilters.length > 0
             ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind))
-            : fb,
+            : fb.addIsA('uuid', ResourceKind.NONE),
         selectedFilters,
     };
 };
index b06b08e46b6bbcdc680fe0b6f0d519c49deac3e9..0e08a87912b8e4cca0bfce4c61d0d8eb261e2a95 100644 (file)
@@ -27,7 +27,6 @@ import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { ContainerRequestState } from "models/container-request";
 import { RootState } from 'store/store';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from 'store/resource-type-filters/resource-type-filters';
 import { getProcess } from 'store/processes/process';
@@ -151,10 +150,8 @@ export const AllProcessesPanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
-                    dataTableDefaultView={ <DataTableDefaultView
-                        icon={ProcessIcon}
-                        messages={['Processes list empty.']}
-                        /> } />
+                    defaultViewIcon={ProcessIcon}
+                    defaultViewMessages={['Processes list empty.']} />
                 </div>
             }
         }
index 8f87cb269eaef7a37cdfa438ebb7eef2d2100546..ddca138c6263024be7f737f4a5e6a784a787c915 100644 (file)
@@ -11,7 +11,6 @@ import { ShareMeIcon } from 'components/icon/icon';
 import { createTree } from 'models/tree';
 import { DataColumns } from 'components/data-table/data-table';
 import { SortDirection } from 'components/data-table/data-column';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { API_CLIENT_AUTHORIZATION_PANEL_ID } from '../../store/api-client-authorizations/api-client-authorizations-actions';
 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
 import { ResourcesState } from 'store/resources/resources';
@@ -141,9 +140,7 @@ export const ApiClientAuthorizationPanelRoot = withStyles(styles)(
             contextMenuColumn={true}
             hideColumnSelector
             hideSearchInput
-            dataTableDefaultView={
-                <DataTableDefaultView
-                    icon={ShareMeIcon}
-                    messages={[DEFAULT_MESSAGE]} />
-            } /></div>
-);
\ No newline at end of file
+            defaultViewIcon={ShareMeIcon}
+            defaultViewMessages={[DEFAULT_MESSAGE]} />
+        </div>
+);
index f1278049963b7804b4681b4e1898a5d3f65e4c26..8e8266cc22a732b05be7ce48f3308a4f6db33aa2 100644 (file)
@@ -12,7 +12,6 @@ import {
 import { CollectionIcon } from 'components/icon/icon';
 import { ArvadosTheme } from 'common/custom-theme';
 import { BackIcon } from 'components/icon/icon';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from 'store/collections-content-address-panel/collections-content-address-panel-actions';
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { Dispatch } from 'redux';
@@ -165,12 +164,10 @@ export const CollectionsContentAddressPanel = withStyles(styles)(
                         onContextMenu={this.props.onContextMenu(this.props.resources)}
                         contextMenuColumn={true}
                         title={`Content address: ${this.props.match.params.id}`}
-                        dataTableDefaultView={
-                            <DataTableDefaultView
-                                icon={CollectionIcon}
-                                messages={['Collections with this content address not found.']} />
-                        } /></div>
-                    </div>;
+                        defaultViewIcon={CollectionIcon}
+                        defaultViewMessages={['Collections with this content address not found.']} />
+                    </div>
+                </div>;
             }
         }
     )
index e520a59cb1cbe8750f9e4be93ca8e6147ce3a012..cb02f1ad0785a91728c163d234fd930887fe54dd 100644 (file)
@@ -31,7 +31,6 @@ import { navigateTo } from 'store/navigation/navigation-action';
 import { ContainerRequestState } from "models/container-request";
 import { FavoritesState } from 'store/favorites/favorites-reducer';
 import { RootState } from 'store/store';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getSimpleObjectTypeFilters } from 'store/resource-type-filters/resource-type-filters';
 import { getResource, ResourcesState } from 'store/resources/resources';
@@ -185,12 +184,9 @@ export const FavoritePanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={true}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={FavoriteIcon}
-                            messages={['Your favorites list is empty.']}
-                            />
-                    } /></div>;
+                    defaultViewIcon={FavoriteIcon}
+                    defaultViewMessages={['Your favorites list is empty.']} />
+                </div>;
             }
         }
     )
index 9cee3cbc2fd2dc9d2a5f460b8244c17876aa7729..311bc86e7124f6bf511372e5178177c73e79599c 100644 (file)
@@ -15,16 +15,20 @@ import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, ope
 import { openContextMenu } from 'store/context-menu/context-menu-actions';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { Grid, Button, Tabs, Tab, Paper, WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core';
-import { AddIcon } from 'components/icon/icon';
+import { AddIcon, UserPanelIcon, KeyIcon } from 'components/icon/icon';
 import { getUserUuid } from 'common/getuser';
 import { GroupResource, isBuiltinGroup } from 'models/group';
 import { ArvadosTheme } from 'common/custom-theme';
 
-type CssRules = "root";
+type CssRules = "root" | "content";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         width: '100%',
+    },
+    content: {
+        // reserve space for the tab bar
+        height: `calc(100% - ${theme.spacing.unit * 7}px)`,
     }
 });
 
@@ -44,6 +48,9 @@ export enum GroupDetailsPanelPermissionsColumnNames {
     REMOVE = "Remove",
 }
 
+const MEMBERS_DEFAULT_MESSAGE = 'Members list is empty.';
+const PERMISSIONS_DEFAULT_MESSAGE = 'Permissions list is empty.';
+
 export const groupDetailsMembersPanelColumns: DataColumns<string> = [
     {
         name: GroupDetailsPanelMembersColumnNames.FULL_NAME,
@@ -165,46 +172,52 @@ export const GroupDetailsPanel = withStyles(styles)(connect(
                       <Tab data-cy="group-details-members-tab" label="MEMBERS" />
                       <Tab data-cy="group-details-permissions-tab" label="PERMISSIONS" />
                   </Tabs>
-                  {value === 0 &&
-                      <DataExplorer
-                          id={GROUP_DETAILS_MEMBERS_PANEL_ID}
-                          data-cy="group-members-data-explorer"
-                          onRowClick={noop}
-                          onRowDoubleClick={noop}
-                          onContextMenu={noop}
-                          contextMenuColumn={false}
-                          hideColumnSelector
-                          hideSearchInput
-                          actions={
-                                this.props.groupCanManage &&
-                                <Grid container justify='flex-end'>
-                                    <Button
-                                      data-cy="group-member-add"
-                                      variant="contained"
-                                      color="primary"
-                                      onClick={this.props.onAddUser}>
-                                      <AddIcon /> Add user
-                                    </Button>
-                                </Grid>
-                          }
-                          paperProps={{
-                              elevation: 0,
-                          }} />
-                  }
-                  {value === 1 &&
-                      <DataExplorer
-                          id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
-                          data-cy="group-permissions-data-explorer"
-                          onRowClick={noop}
-                          onRowDoubleClick={noop}
-                          onContextMenu={noop}
-                          contextMenuColumn={false}
-                          hideColumnSelector
-                          hideSearchInput
-                          paperProps={{
-                              elevation: 0,
-                          }} />
-                  }
+                  <div className={this.props.classes.content}>
+                    {value === 0 &&
+                        <DataExplorer
+                            id={GROUP_DETAILS_MEMBERS_PANEL_ID}
+                            data-cy="group-members-data-explorer"
+                            onRowClick={noop}
+                            onRowDoubleClick={noop}
+                            onContextMenu={noop}
+                            contextMenuColumn={false}
+                            defaultViewIcon={UserPanelIcon}
+                            defaultViewMessages={[MEMBERS_DEFAULT_MESSAGE]}
+                            hideColumnSelector
+                            hideSearchInput
+                            actions={
+                                    this.props.groupCanManage &&
+                                    <Grid container justify='flex-end'>
+                                        <Button
+                                        data-cy="group-member-add"
+                                        variant="contained"
+                                        color="primary"
+                                        onClick={this.props.onAddUser}>
+                                        <AddIcon /> Add user
+                                        </Button>
+                                    </Grid>
+                            }
+                            paperProps={{
+                                elevation: 0,
+                            }} />
+                    }
+                    {value === 1 &&
+                        <DataExplorer
+                            id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
+                            data-cy="group-permissions-data-explorer"
+                            onRowClick={noop}
+                            onRowDoubleClick={noop}
+                            onContextMenu={noop}
+                            contextMenuColumn={false}
+                            defaultViewIcon={KeyIcon}
+                            defaultViewMessages={[PERMISSIONS_DEFAULT_MESSAGE]}
+                            hideColumnSelector
+                            hideSearchInput
+                            paperProps={{
+                                elevation: 0,
+                            }} />
+                    }
+                  </div>
                 </Paper>
             );
         }
index b32208cd74a04895c569d5e227b2cf037b4207e3..c24d463700517bf1606e50e548dd56822c1344dc 100644 (file)
@@ -7,7 +7,6 @@ import { LINK_PANEL_ID } from 'store/link-panel/link-panel-actions';
 import { DataExplorer } from 'views-components/data-explorer/data-explorer';
 import { SortDirection } from 'components/data-table/data-column';
 import { DataColumns } from 'components/data-table/data-table';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { ResourcesState } from 'store/resources/resources';
 import { ShareMeIcon } from 'components/icon/icon';
 import { createTree } from 'models/tree';
@@ -94,9 +93,7 @@ export const LinkPanelRoot = withStyles(styles)((props: LinkPanelRootProps) => {
         contextMenuColumn={true}
         hideColumnSelector
         hideSearchInput
-        dataTableDefaultView={
-            <DataTableDefaultView
-                icon={ShareMeIcon}
-                messages={['Your link list is empty.']} />
-        }/></div>;
-});
\ No newline at end of file
+        defaultViewIcon={ShareMeIcon}
+        defaultViewMessages={['Your link list is empty.']} />
+    </div>;
+});
index a5594d8ec9d951cddccc0adf90d0820ba8cea97e..ccb40d53ba958b35645c14fb0af4a874e72f1b88 100644 (file)
@@ -36,7 +36,6 @@ import {
 import { navigateTo } from 'store/navigation/navigation-action';
 import { getProperty } from 'store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { ArvadosTheme } from "common/custom-theme";
 import { createTree } from 'models/tree';
 import {
@@ -155,11 +154,9 @@ export const ProjectPanel = withStyles(styles)(
                         onRowDoubleClick={this.handleRowDoubleClick}
                         onContextMenu={this.handleContextMenu}
                         contextMenuColumn={true}
-                        dataTableDefaultView={
-                            <DataTableDefaultView
-                                icon={ProjectIcon}
-                                messages={DEFAULT_VIEW_MESSAGES} />
-                        } />
+                        defaultViewIcon={ProjectIcon}
+                        defaultViewMessages={DEFAULT_VIEW_MESSAGES}
+                    />
                 </div>;
             }
 
index 9b1e9102b9666c0b7d7b81ca5eae29fd19ebd082..8eb2a87c37f52de69674db808bd7419f530dbd55 100644 (file)
@@ -30,7 +30,6 @@ import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { navigateTo } from 'store/navigation/navigation-action';
 import { ContainerRequestState } from "models/container-request";
 import { RootState } from 'store/store';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getSimpleObjectTypeFilters } from 'store/resource-type-filters/resource-type-filters';
 import { PUBLIC_FAVORITE_PANEL_ID } from 'store/public-favorites-panel/public-favorites-action';
@@ -169,11 +168,9 @@ export const PublicFavoritePanel = withStyles(styles)(
                     onRowDoubleClick={this.props.onItemDoubleClick}
                     onContextMenu={this.props.onContextMenu(this.props.resources)}
                     contextMenuColumn={true}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={PublicFavoriteIcon}
-                            messages={['Public favorites list is empty.']} />
-                    } /></div>;
+                    defaultViewIcon={PublicFavoriteIcon}
+                    defaultViewMessages={['Public favorites list is empty.']} />
+                </div>;
             }
         }
     )
index 7ba9077cac8ec28e7e0bd839552844491ec63538..e6cfccd2694c4765570a76268e57cfa2d22d8c90 100644 (file)
@@ -12,7 +12,6 @@ import { ShareMeIcon } from 'components/icon/icon';
 import { ResourcesState, getResource } from 'store/resources/resources';
 import { navigateTo } from "store/navigation/navigation-action";
 import { loadDetailsPanel } from "store/details-panel/details-panel-action";
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { SHARED_WITH_ME_PANEL_ID } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
 import {
     openContextMenu,
@@ -55,11 +54,9 @@ export const SharedWithMePanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={false}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={ShareMeIcon}
-                            messages={['No shared items']} />
-                    } /></div>;
+                    defaultViewIcon={ShareMeIcon}
+                    defaultViewMessages={['No shared items']} />
+                </div>;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
index 41a8f66b80457a6fdbe693a8f0884e6a7d7edb07..d4ccae9c03abd5cf9cc58ae7bbf48f52e44b9569 100644 (file)
@@ -13,7 +13,6 @@ import { ResourceCreatedAtDate, ProcessStatus, ContainerRunTime } from 'views-co
 import { ProcessIcon } from 'components/icon/icon';
 import { ResourceName } from 'views-components/data-explorer/renderers';
 import { SUBPROCESS_PANEL_ID } from 'store/subprocess-panel/subprocess-panel-actions';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
 import { ResourcesState } from 'store/resources/resources';
@@ -88,13 +87,10 @@ export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps)
         onRowDoubleClick={props.onItemDoubleClick}
         onContextMenu={(event, item) => props.onContextMenu(event, item, props.resources)}
         contextMenuColumn={true}
-        dataTableDefaultView={
-            <DataTableDefaultView
-                icon={ProcessIcon}
-                messages={DEFAULT_VIEW_MESSAGES} />
-        }
+        defaultViewIcon={ProcessIcon}
+        defaultViewMessages={DEFAULT_VIEW_MESSAGES}
         doHidePanel={props.doHidePanel}
         doMaximizePanel={props.doMaximizePanel}
         panelMaximized={props.panelMaximized}
         panelName={props.panelName} />;
-};
\ No newline at end of file
+};
index d303c2f700713337364af00f9965812eefade800..67326829b6ae84763f401baeb3b3e98403cc4fbf 100644 (file)
@@ -30,7 +30,6 @@ import { loadDetailsPanel } from "store/details-panel/details-panel-action";
 import { toggleTrashed } from "store/trash/trash-actions";
 import { ContextMenuKind } from "views-components/context-menu/context-menu";
 import { Dispatch } from "redux";
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import {
     getTrashPanelTypeFilters
@@ -155,11 +154,9 @@ export const TrashPanel = withStyles(styles)(
                     onRowDoubleClick={this.handleRowDoubleClick}
                     onContextMenu={this.handleContextMenu}
                     contextMenuColumn={false}
-                    dataTableDefaultView={
-                        <DataTableDefaultView
-                            icon={TrashIcon}
-                            messages={['Your trash list is empty.']}/>
-                    } /></div>;
+                    defaultViewIcon={TrashIcon}
+                    defaultViewMessages={['Your trash list is empty.']} />
+                </div>;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
index 589353cd880ad91557f80969e6fe1233e818fc2f..f2491dc27182e351d466dd9a7d09b8a1308a0a68 100644 (file)
@@ -20,7 +20,6 @@ import {
     UserResourceAccountStatus,
 } from "views-components/data-explorer/renderers";
 import { navigateToUserProfile } from "store/navigation/navigation-action";
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { createTree } from 'models/tree';
 import { compose, Dispatch } from 'redux';
 import { UserResource } from 'models/user';
@@ -148,11 +147,8 @@ export const UserPanel = compose(
                         paperProps={{
                             elevation: 0,
                         }}
-                        dataTableDefaultView={
-                            <DataTableDefaultView
-                                icon={ShareMeIcon}
-                                messages={['Your user list is empty.']} />
-                        } />
+                        defaultViewIcon={ShareMeIcon}
+                        defaultViewMessages={['Your user list is empty.']} />
                 </Paper>;
             }
 
index 1c8b1da74c65e8ef5daf087acaba3dd04e9660e6..53c0799f79b48432d0aa56636b8f0bf2bde45cbf 100644 (file)
@@ -24,7 +24,6 @@ import {
     IconButton,
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators";
 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
 import { noop } from 'lodash';
@@ -327,11 +326,8 @@ export const UserProfilePanelRoot = withStyles(styles)(
                                     paperProps={{
                                         elevation: 0,
                                     }}
-                                    dataTableDefaultView={
-                                        <DataTableDefaultView
-                                            icon={GroupsIcon}
-                                            messages={['Group list is empty.']} />
-                                    } />
+                                    defaultViewIcon={GroupsIcon}
+                                    defaultViewMessages={['Group list is empty.']} />
                         </div>}
                 </Paper >;
             }
index ca84a0fc95bc524572e9e01a92065f48dc2a9ed8..44e14fd3c333c2cd5ac00fe7a0259567a0e090aa 100644 (file)
@@ -5,7 +5,6 @@
 import React from 'react';
 import { DataExplorer } from "views-components/data-explorer/data-explorer";
 import { WorkflowIcon } from 'components/icon/icon';
-import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
 import { WORKFLOW_PANEL_ID } from 'store/workflow-panel/workflow-panel-actions';
 import {
     ResourceLastModifiedDate,
@@ -131,7 +130,8 @@ export const WorkflowPanelView = (props: WorkflowPanelProps) => {
                 onRowDoubleClick={props.handleRowDoubleClick}
                 contextMenuColumn={false}
                 onContextMenu={e => e}
-                dataTableDefaultView={<DataTableDefaultView icon={WorkflowIcon} />} />
+                defaultViewIcon={WorkflowIcon}
+                defaultViewMessages={['Workflow list is empty.']} />
         </Grid>
         <Grid item xs={6}>
             <Paper style={{ height: '100%' }}>