From: Daniel Kutyła Date: Tue, 21 Jun 2022 09:16:57 +0000 (+0200) Subject: Merge branch '18203-Support-setting-multi-properties-at-once' into main X-Git-Tag: 2.5.0~50 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/765f6475a53ac7e635b737642ef375459324a117?hp=6c42bd0ca04911ece6403af74852a1e34d483c8f Merge branch '18203-Support-setting-multi-properties-at-once' into main closes #18203 Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła --- diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index 384b789f..fd8e65b2 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -278,6 +278,13 @@ describe('Collection panel tests', function () { .and(`${isWritable ? '' : 'not.'}contain`, 'Rename') .and(`${isWritable ? '' : 'not.'}contain`, 'Remove'); cy.get('body').click(); // Collapse the menu + // File/dir item 'more options' button + cy.get('[data-cy=file-item-options-btn') + .first() + .click() + cy.get('[data-cy=context-menu]') + .should(`${isWritable ? '' : 'not.'}contain`, 'Remove'); + cy.get('body').click(); // Collapse the menu // Hamburger 'more options' menu button cy.get('[data-cy=collection-files-panel-options-btn]') .click() diff --git a/src/common/config.ts b/src/common/config.ts index 2518c95e..2954d704 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -50,6 +50,7 @@ export interface ClusterConfigJSON { } }; Workbench: { + DisableSharingURLsUI: boolean; ArvadosDocsite: string; FileViewersConfigURL: string; WelcomePageHTML: string; @@ -233,6 +234,7 @@ export const mockClusterConfigJSON = (config: Partial): Clust WebShell: { ExternalURL: "" }, }, Workbench: { + DisableSharingURLsUI: false, ArvadosDocsite: "", FileViewersConfigURL: "", WelcomePageHTML: "", diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx index 42408270..06b3c507 100644 --- a/src/components/collection-panel-files/collection-panel-files.tsx +++ b/src/components/collection-panel-files/collection-panel-files.tsx @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { FixedSizeList } from "react-window"; import AutoSizer from "react-virtualized-auto-sizer"; import servicesProvider from 'common/service-provider'; -import { CustomizeTableIcon, DownloadIcon } from 'components/icon/icon'; +import { CustomizeTableIcon, DownloadIcon, MoreOptionsIcon } from 'components/icon/icon'; import { SearchInput } from 'components/search-input/search-input'; import { ListItemIcon, @@ -60,6 +60,8 @@ type CssRules = "backButton" | "pathPanelPathWrapper" | "uploadButton" | "uploadIcon" + | "moreOptionsButton" + | "moreOptions" | "loader" | "wrapper" | "dataWrapper" @@ -207,7 +209,18 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ }, uploadButton: { float: 'right', - } + }, + moreOptionsButton: { + width: theme.spacing.unit * 3, + height: theme.spacing.unit * 3, + marginRight: theme.spacing.unit, + marginTop: 'auto', + marginBottom: 'auto', + justifyContent: 'center', + }, + moreOptions: { + position: 'absolute' + }, }); const pathPromise = {}; @@ -375,17 +388,23 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState const handleClick = React.useCallback( (event: any) => { let isCheckbox = false; + let isMoreButton = false; let elem = event.target; if (elem.type === 'checkbox') { isCheckbox = true; } + // The "More options" button click event could be triggered on its + // internal graphic element. + else if ((elem.dataset && elem.dataset.id === 'moreOptions') || (elem.parentNode && elem.parentNode.dataset && elem.parentNode.dataset.id === 'moreOptions')) { + isMoreButton = true; + } while (elem && elem.dataset && !elem.dataset.item) { elem = elem.parentNode; } - if (elem && elem.dataset && !isCheckbox) { + if (elem && elem.dataset && !isCheckbox && !isMoreButton) { const { parentPath, subfolderPath, breadcrumbPath, type } = elem.dataset; if (breadcrumbPath) { @@ -418,6 +437,14 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState const item = collectionPanelFiles[id]; props.onSelectionToggle(event, item); } + if (isMoreButton) { + const { id } = elem.dataset; + const item: any = { + id, + data: rightData.find((elem) => elem.id === id), + }; + onItemMenuOpen(event, item, isWritable); + } }, [path, setPath, collectionPanelFiles] // eslint-disable-line react-hooks/exhaustive-deps ); @@ -551,6 +578,15 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState marginLeft: 'auto', marginRight: '1rem' }}> { formatFileSize(size) } + + + + + } } :
This collection is empty
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/file-tree/file-thumbnail.tsx b/src/components/file-tree/file-thumbnail.tsx index 9c651e76..aeb8d68f 100644 --- a/src/components/file-tree/file-thumbnail.tsx +++ b/src/components/file-tree/file-thumbnail.tsx @@ -11,7 +11,7 @@ import { getInlineFileUrl, sanitizeToken } from "views-components/context-menu/a import { connect } from "react-redux"; import { RootState } from "store/store"; -export interface FileThumbnailProps { +interface FileThumbnailProps { file: FileTreeData; } diff --git a/src/components/file-tree/file-tree-item.tsx b/src/components/file-tree/file-tree-item.tsx index b522637d..d94c7297 100644 --- a/src/components/file-tree/file-tree-item.tsx +++ b/src/components/file-tree/file-tree-item.tsx @@ -2,71 +2,7 @@ // // SPDX-License-Identifier: AGPL-3.0 -import React from "react"; -import { TreeItem } from "../tree/tree"; -import { DirectoryIcon, MoreOptionsIcon, DefaultIcon, FileIcon } from "../icon/icon"; -import { Typography, IconButton, StyleRulesCallback, withStyles, WithStyles, Tooltip } from '@material-ui/core'; -import { formatFileSize } from "common/formatters"; -import { ListItemTextIcon } from "../list-item-text-icon/list-item-text-icon"; -import { FileTreeData } from "./file-tree-data"; - -type CssRules = "root" | "spacer" | "sizeInfo" | "button" | "moreOptions"; - -const fileTreeItemStyle: StyleRulesCallback = theme => ({ - root: { - display: "flex", - alignItems: "center", - paddingRight: `${theme.spacing.unit * 1.5}px` - }, - spacer: { - flex: "1" - }, - sizeInfo: { - width: `${theme.spacing.unit * 8}px` - }, - button: { - width: theme.spacing.unit * 3, - height: theme.spacing.unit * 3, - marginRight: theme.spacing.unit, - }, - moreOptions: { - position: 'absolute' - } -}); - -export interface FileTreeItemProps { - item: TreeItem; - onMoreClick: (event: React.MouseEvent, item: TreeItem) => void; -} -export const FileTreeItem = withStyles(fileTreeItemStyle)( - class extends React.Component> { - render() { - const { classes, item } = this.props; - return <> -
- -
- {formatFileSize(item.data.size)} - - - - - -
- ; - } - - handleClick = (event: React.MouseEvent) => { - this.props.onMoreClick(event, this.props.item); - } - }); +import { DirectoryIcon, DefaultIcon, FileIcon } from "../icon/icon"; export const getIcon = (type: string) => { switch (type) { diff --git a/src/components/file-tree/file-tree.tsx b/src/components/file-tree/file-tree.tsx deleted file mode 100644 index e24fbb71..00000000 --- a/src/components/file-tree/file-tree.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import React from "react"; -import { TreeItem, TreeItemStatus } from "../tree/tree"; -import { VirtualTree as Tree } from "../tree/virtual-tree"; -import { FileTreeData } from "./file-tree-data"; -import { FileTreeItem } from "./file-tree-item"; - -export interface FileTreeProps { - items: Array>; - onMenuOpen: (event: React.MouseEvent, item: TreeItem) => void; - onSelectionToggle: (event: React.MouseEvent, item: TreeItem) => void; - onCollapseToggle: (id: string, status: TreeItemStatus) => void; - onFileClick: (id: string) => void; - currentItemUuid?: string; -} - -export class FileTree extends React.Component { - render() { - return ; - } - - handleContextMenu = (event: React.MouseEvent, item: TreeItem) => { - event.stopPropagation(); - this.props.onMenuOpen(event, item); - } - - handleToggle = (event: React.MouseEvent<{}>, { id, status }: TreeItem<{}>) => { - this.props.onCollapseToggle(id, status); - } - - handleToggleActive = (_: React.MouseEvent, item: TreeItem) => { - this.props.onFileClick(item.id); - } - - handleSelectionChange = (event: React.MouseEvent, item: TreeItem) => { - event.stopPropagation(); - this.props.onSelectionToggle(event, item); - } - - renderItem = (item: TreeItem) => - - -} 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/store/sharing-dialog/sharing-dialog-actions.ts b/src/store/sharing-dialog/sharing-dialog-actions.ts index 367eea81..cdc6c0c7 100644 --- a/src/store/sharing-dialog/sharing-dialog-actions.ts +++ b/src/store/sharing-dialog/sharing-dialog-actions.ts @@ -118,13 +118,14 @@ export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, g const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => { const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); + const sharingURLsDisabled = getState().auth.config.clusterConfig.Workbench.DisableSharingURLsUI; if (dialog) { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); try { const resourceUuid = dialog.data.resourceUuid; await dispatch(initializeManagementForm); // For collections, we need to load the public sharing tokens - if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) { + if (!sharingURLsDisabled && extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) { const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid); dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items])); } diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.test.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.test.tsx new file mode 100644 index 00000000..36447a8d --- /dev/null +++ b/src/views-components/sharing-dialog/sharing-dialog-component.test.tsx @@ -0,0 +1,71 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { mount, configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; +import { Provider } from 'react-redux'; +import { combineReducers, createStore } from 'redux'; + +import SharingDialogComponent, { + SharingDialogComponentProps, +} from './sharing-dialog-component'; +import { + extractUuidObjectType, + ResourceObjectType +} from 'models/resource'; + +configure({ adapter: new Adapter() }); + +describe("", () => { + let props: SharingDialogComponentProps; + let store; + + beforeEach(() => { + const initialAuthState = { + config: { + keepWebServiceUrl: 'http://example.com/', + keepWebInlineServiceUrl: 'http://*.collections.example.com/', + } + } + store = createStore(combineReducers({ + auth: (state: any = initialAuthState, action: any) => state, + })); + + props = { + open: true, + loading: false, + saveEnabled: false, + sharedResourceUuid: 'zzzzz-4zz18-zzzzzzzzzzzzzzz', + privateAccess: true, + sharingURLsNr: 2, + sharingURLsDisabled: false, + onClose: jest.fn(), + onSave: jest.fn(), + onCreateSharingToken: jest.fn(), + refreshPermissions: jest.fn(), + }; + }); + + it("show sharing urls tab on collections when not disabled", () => { + expect(props.sharingURLsDisabled).toBe(false); + expect(props.sharingURLsNr).toBe(2); + expect(extractUuidObjectType(props.sharedResourceUuid) === ResourceObjectType.COLLECTION).toBe(true); + let wrapper = mount(); + expect(wrapper.html()).toContain('Sharing URLs (2)'); + + // disable Sharing URLs UI + props.sharingURLsDisabled = true; + wrapper = mount(); + expect(wrapper.html()).not.toContain('Sharing URLs'); + }); + + it("does not show sharing urls on non-collection resources", () => { + props.sharedResourceUuid = 'zzzzz-j7d0g-0123456789abcde'; + expect(extractUuidObjectType(props.sharedResourceUuid) === ResourceObjectType.COLLECTION).toBe(false); + expect(props.sharingURLsDisabled).toBe(false); + let wrapper = mount(); + expect(wrapper.html()).not.toContain('Sharing URLs'); + }); +}); \ No newline at end of file diff --git a/src/views-components/sharing-dialog/sharing-dialog-component.tsx b/src/views-components/sharing-dialog/sharing-dialog-component.tsx index 15d7f660..b2f31397 100644 --- a/src/views-components/sharing-dialog/sharing-dialog-component.tsx +++ b/src/views-components/sharing-dialog/sharing-dialog-component.tsx @@ -47,6 +47,7 @@ export interface SharingDialogDataProps { sharedResourceUuid: string; sharingURLsNr: number; privateAccess: boolean; + sharingURLsDisabled: boolean; } export interface SharingDialogActionProps { onClose: () => void; @@ -58,11 +59,13 @@ enum SharingDialogTab { PERMISSIONS = 0, URLS = 1, } -export default (props: SharingDialogDataProps & SharingDialogActionProps) => { +export type SharingDialogComponentProps = SharingDialogDataProps & SharingDialogActionProps; + +export default (props: SharingDialogComponentProps) => { const { open, loading, saveEnabled, sharedResourceUuid, - sharingURLsNr, privateAccess, + sharingURLsNr, privateAccess, sharingURLsDisabled, onClose, onSave, onCreateSharingToken, refreshPermissions } = props; - const showTabs = extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION; + const showTabs = !sharingURLsDisabled && extractUuidObjectType(sharedResourceUuid) === ResourceObjectType.COLLECTION; const [tabNr, setTabNr] = React.useState(SharingDialogTab.PERMISSIONS); const [expDate, setExpDate] = React.useState(); const [withExpiration, setWithExpiration] = React.useState(false); @@ -151,7 +154,8 @@ export default (props: SharingDialogDataProps & SharingDialogActionProps) => { } - { tabNr === SharingDialogTab.PERMISSIONS && privateAccess && sharingURLsNr > 0 && + { tabNr === SharingDialogTab.PERMISSIONS && !sharingURLsDisabled && + privateAccess && sharingURLsNr > 0 && Although there aren't specific permissions set, this is publicly accessible via Sharing URL(s). diff --git a/src/views-components/sharing-dialog/sharing-dialog.tsx b/src/views-components/sharing-dialog/sharing-dialog.tsx index 6b488e44..01cd390b 100644 --- a/src/views-components/sharing-dialog/sharing-dialog.tsx +++ b/src/views-components/sharing-dialog/sharing-dialog.tsx @@ -35,18 +35,21 @@ type Props = WithDialogProps & WithProgressStateProps; const mapStateToProps = (state: RootState, { working, ...props }: Props): SharingDialogDataProps => { const dialog = getDialog(state.dialog, SHARING_DIALOG_NAME); const sharedResourceUuid = dialog?.data.resourceUuid || ''; + const sharingURLsDisabled = state.auth.config.clusterConfig.Workbench.DisableSharingURLsUI; return ({ ...props, saveEnabled: hasChanges(state), loading: working, sharedResourceUuid, - sharingURLsNr: (filterResources( - (resource: ApiClientAuthorization) => + sharingURLsDisabled, + sharingURLsNr: !sharingURLsDisabled + ? (filterResources( (resource: ApiClientAuthorization) => resource.kind === ResourceKind.API_CLIENT_AUTHORIZATION && resource.scopes.includes(`GET /arvados/v1/collections/${sharedResourceUuid}`) && resource.scopes.includes(`GET /arvados/v1/collections/${sharedResourceUuid}/`) && resource.scopes.includes('GET /arvados/v1/keep_services/accessible') - )(state.resources) as ApiClientAuthorization[]).length, + )(state.resources) as ApiClientAuthorization[]).length + : 0, privateAccess: getSharingPublicAccessFormData(state)?.visibility === VisibilityLevel.PRIVATE, }) }; 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.']} />