From: Stephen Smith Date: Tue, 7 Jun 2022 21:37:00 +0000 (-0400) Subject: Merge branch '18984-project-type-filters-2' into main. Closes #18984 X-Git-Tag: 2.5.0~53 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/9a59fad2b6a97af963728a5111395f9caa71802f?hp=852a6a393297d03ca5259ddb6aa7aedff4a000ea Merge branch '18984-project-type-filters-2' into main. Closes #18984 Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index 0363d333..40617f73 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -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 { 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} /> diff --git a/src/components/data-table-default-view/data-table-default-view.tsx b/src/components/data-table-default-view/data-table-default-view.tsx index 2869ab82..b245c19b 100644 --- a/src/components/data-table-default-view/data-table-default-view.tsx +++ b/src/components/data-table-default-view/data-table-default-view.tsx @@ -16,12 +16,13 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ marginBottom: theme.spacing.unit * 4, }, }); -type DataTableDefaultViewDataProps = Partial>; +type DataTableDefaultViewDataProps = Partial>; type DataTableDefaultViewProps = DataTableDefaultViewDataProps & WithStyles; 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 ; }); diff --git a/src/components/data-table-filters/data-table-filters-popover.tsx b/src/components/data-table-filters/data-table-filters-popover.tsx index 3183157b..b5187866 100644 --- a/src/components/data-table-filters/data-table-filters-popover.tsx +++ b/src/components/data-table-filters/data-table-filters-popover.tsx @@ -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}> @@ -137,36 +138,21 @@ export const DataTableFiltersPopover = withStyles(styles)( { - 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 || - } + ; } @@ -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 }); - } - } ); diff --git a/src/components/data-table-filters/data-table-filters-tree.tsx b/src/components/data-table-filters/data-table-filters-tree.tsx index 6514078d..7b97865b 100644 --- a/src/components/data-table-filters/data-table-filters-tree.tsx +++ b/src/components/data-table-filters/data-table-filters-tree.tsx @@ -34,7 +34,7 @@ export class DataTableFiltersTree extends React.Component 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 } const renderItem = (item: TreeItem) => - {item.data.name}; + + {item.data.name} + {item.initialState !== item.selected ? <> + * + : null} + ; + +const renderRadioItem = (item: TreeItem) => + + {item.data.name} + ; const filterToTreeItem = (filters: DataTableFilters) => (id: string): TreeItem => { 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, }; }; diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index 14dfdaca..d942234d 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -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 { onFiltersChange: (filters: DataTableFilters, column: DataColumn) => 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.']} /> } - {items.length === 0 && !working && this.renderNoItemsPlaceholder()} + {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)} ; } - renderNoItemsPlaceholder = () => { - return this.props.defaultView - ? this.props.defaultView - : ; + renderNoItemsPlaceholder = (columns: DataColumns) => { + const dirty = columns.some((column) => getTreeDirty('')(column.filters)); + return ; } renderHeadCell = (column: DataColumn, index: number) => { diff --git a/src/components/default-view/default-view.tsx b/src/components/default-view/default-view.tsx index 6e89db25..014b8cc4 100644 --- a/src/components/default-view/default-view.tsx +++ b/src/components/default-view/default-view.tsx @@ -27,6 +27,7 @@ const styles: StyleRulesCallback = (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}; })} -); \ No newline at end of file +); diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 3ae884b6..fc9dbc74 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -97,6 +97,8 @@ export interface TreeItem { open: boolean; active: boolean; selected?: boolean; + initialState?: boolean; + indeterminate?: boolean; flatTree?: boolean; status: TreeItemStatus; items?: Array>; @@ -292,6 +294,7 @@ export const Tree = withStyles(styles)( {showSelection(it) && !useRadioButtons && } diff --git a/src/models/tree.ts b/src/models/tree.ts index e9291388..996f98a4 100644 --- a/src/models/tree.ts +++ b/src/models/tree.ts @@ -14,6 +14,7 @@ export interface TreeNode { parent: string; active: boolean; selected: boolean; + initialState?: boolean; expanded: boolean; status: TreeNodeStatus; } @@ -197,6 +198,19 @@ export const initTreeNode = (data: Pick, 'id' | 'value'> & { pare ...data, }); +export const getTreeDirty = (id: string) => (tree: Tree): 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) => (tree: Tree) => { const node = getNode(id)(tree); if (node) { @@ -228,7 +242,6 @@ const toggleParentNodeSelection = (id: string) => (tree: Tree) => { return tree; }; - const mapNodeValue = (mapFn: (value: T) => R) => (node: TreeNode): TreeNode => ({ ...node, value: mapFn(node.value) }); diff --git a/src/store/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts index f001770e..5972f60c 100644 --- a/src/store/resource-type-filters/resource-type-filters.test.ts +++ b/src/store/resource-type-filters/resource-type-filters.test.ts @@ -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); diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts index 64a391ca..361b52a6 100644 --- a/src/store/resource-type-filters/resource-type-filters.ts +++ b/src/store/resource-type-filters/resource-type-filters.ts @@ -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({ 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(), 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(), 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 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 0 ? fb.addIsA('uuid', typeFilters.map(objectTypeToResourceKind)) - : fb, + : fb.addIsA('uuid', ResourceKind.NONE), selectedFilters, }; }; diff --git a/src/views/all-processes-panel/all-processes-panel.tsx b/src/views/all-processes-panel/all-processes-panel.tsx index b06b08e4..0e08a879 100644 --- a/src/views/all-processes-panel/all-processes-panel.tsx +++ b/src/views/all-processes-panel/all-processes-panel.tsx @@ -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={ } /> + defaultViewIcon={ProcessIcon} + defaultViewMessages={['Processes list empty.']} /> } } diff --git a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx index 8f87cb26..ddca138c 100644 --- a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx +++ b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx @@ -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={ - - } /> -); \ No newline at end of file + defaultViewIcon={ShareMeIcon} + defaultViewMessages={[DEFAULT_MESSAGE]} /> + +); diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx index f1278049..8e8266cc 100644 --- a/src/views/collection-content-address-panel/collection-content-address-panel.tsx +++ b/src/views/collection-content-address-panel/collection-content-address-panel.tsx @@ -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={ - - } /> - ; + defaultViewIcon={CollectionIcon} + defaultViewMessages={['Collections with this content address not found.']} /> + + ; } } ) diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index e520a59c..cb02f1ad 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -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={ - - } />; + defaultViewIcon={FavoriteIcon} + defaultViewMessages={['Your favorites list is empty.']} /> + ; } } ) diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx index 9cee3cbc..311bc86e 100644 --- a/src/views/group-details-panel/group-details-panel.tsx +++ b/src/views/group-details-panel/group-details-panel.tsx @@ -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 = (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 = [ { name: GroupDetailsPanelMembersColumnNames.FULL_NAME, @@ -165,46 +172,52 @@ export const GroupDetailsPanel = withStyles(styles)(connect( - {value === 0 && - - - - } - paperProps={{ - elevation: 0, - }} /> - } - {value === 1 && - - } +
+ {value === 0 && + + + + } + paperProps={{ + elevation: 0, + }} /> + } + {value === 1 && + + } +
); } diff --git a/src/views/link-panel/link-panel-root.tsx b/src/views/link-panel/link-panel-root.tsx index b32208cd..c24d4637 100644 --- a/src/views/link-panel/link-panel-root.tsx +++ b/src/views/link-panel/link-panel-root.tsx @@ -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={ - - }/>; -}); \ No newline at end of file + defaultViewIcon={ShareMeIcon} + defaultViewMessages={['Your link list is empty.']} /> + ; +}); diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index a5594d8e..ccb40d53 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -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={ - - } /> + defaultViewIcon={ProjectIcon} + defaultViewMessages={DEFAULT_VIEW_MESSAGES} + /> ; } diff --git a/src/views/public-favorites-panel/public-favorites-panel.tsx b/src/views/public-favorites-panel/public-favorites-panel.tsx index 9b1e9102..8eb2a87c 100644 --- a/src/views/public-favorites-panel/public-favorites-panel.tsx +++ b/src/views/public-favorites-panel/public-favorites-panel.tsx @@ -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={ - - } />; + defaultViewIcon={PublicFavoriteIcon} + defaultViewMessages={['Public favorites list is empty.']} /> + ; } } ) diff --git a/src/views/shared-with-me-panel/shared-with-me-panel.tsx b/src/views/shared-with-me-panel/shared-with-me-panel.tsx index 7ba9077c..e6cfccd2 100644 --- a/src/views/shared-with-me-panel/shared-with-me-panel.tsx +++ b/src/views/shared-with-me-panel/shared-with-me-panel.tsx @@ -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={ - - } />; + defaultViewIcon={ShareMeIcon} + defaultViewMessages={['No shared items']} /> + ; } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { diff --git a/src/views/subprocess-panel/subprocess-panel-root.tsx b/src/views/subprocess-panel/subprocess-panel-root.tsx index 41a8f66b..d4ccae9c 100644 --- a/src/views/subprocess-panel/subprocess-panel-root.tsx +++ b/src/views/subprocess-panel/subprocess-panel-root.tsx @@ -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={ - - } + defaultViewIcon={ProcessIcon} + defaultViewMessages={DEFAULT_VIEW_MESSAGES} doHidePanel={props.doHidePanel} doMaximizePanel={props.doMaximizePanel} panelMaximized={props.panelMaximized} panelName={props.panelName} />; -}; \ No newline at end of file +}; diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx index d303c2f7..67326829 100644 --- a/src/views/trash-panel/trash-panel.tsx +++ b/src/views/trash-panel/trash-panel.tsx @@ -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={ - - } />; + defaultViewIcon={TrashIcon} + defaultViewMessages={['Your trash list is empty.']} /> + ; } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx index 589353cd..f2491dc2 100644 --- a/src/views/user-panel/user-panel.tsx +++ b/src/views/user-panel/user-panel.tsx @@ -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={ - - } /> + defaultViewIcon={ShareMeIcon} + defaultViewMessages={['Your user list is empty.']} /> ; } diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx index 1c8b1da7..53c0799f 100644 --- a/src/views/user-profile-panel/user-profile-panel-root.tsx +++ b/src/views/user-profile-panel/user-profile-panel-root.tsx @@ -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={ - - } /> + defaultViewIcon={GroupsIcon} + defaultViewMessages={['Group list is empty.']} /> } ; } diff --git a/src/views/workflow-panel/workflow-panel-view.tsx b/src/views/workflow-panel/workflow-panel-view.tsx index ca84a0fc..44e14fd3 100644 --- a/src/views/workflow-panel/workflow-panel-view.tsx +++ b/src/views/workflow-panel/workflow-panel-view.tsx @@ -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={} /> + defaultViewIcon={WorkflowIcon} + defaultViewMessages={['Workflow list is empty.']} />