expect(onRowClick).toHaveBeenCalledWith("rowClick");
});
- it("does not render <DataTable/> if there is no items", () => {
- const dataExplorer = mount(<DataExplorer
- {...mockDataExplorerProps()}
- items={[]}
- onSetColumns={jest.fn()} />);
- expect(dataExplorer.find(DataTable)).toHaveLength(0);
- expect(dataExplorer.find(DefaultView)).toHaveLength(1);
- });
-
it("communicates with <TablePagination/>", () => {
const onChangePage = jest.fn();
const onChangeRowsPerPage = jest.fn();
import { DataTableFilterItem } from '../data-table-filters/data-table-filters';
import { SearchInput } from '../search-input/search-input';
import { ArvadosTheme } from "~/common/custom-theme";
-import { DefaultView } from '../default-view/default-view';
-import { IconType } from '../icon/icon';
-type CssRules = 'searchBox' | "toolbar" | 'defaultRoot' | 'defaultMessage' | 'defaultIcon';
+type CssRules = 'searchBox' | "toolbar";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
toolbar: {
paddingTop: theme.spacing.unit * 2
},
- defaultRoot: {
- position: 'absolute',
- width: '80%',
- left: '50%',
- top: '50%',
- transform: 'translate(-50%, -50%)'
- },
- defaultMessage: {
- fontSize: '1.75rem',
- },
- defaultIcon: {
- fontSize: '6rem'
- }
});
interface DataExplorerDataProps<T> {
rowsPerPage: number;
rowsPerPageOptions: number[];
page: number;
- defaultIcon: IconType;
- defaultMessages: string[];
contextMenuColumn: boolean;
+ dataTableDefaultView?: React.ReactNode;
}
interface DataExplorerActionProps<T> {
const {
columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
- items, itemsAvailable, onRowClick, onRowDoubleClick, defaultIcon, defaultMessages, classes
+ items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
+ dataTableDefaultView
} = this.props;
- return <div>
- { items.length > 0 ? (
- <Paper>
- <Toolbar className={classes.toolbar}>
- <Grid container justify="space-between" wrap="nowrap" alignItems="center">
- <div className={classes.searchBox}>
- <SearchInput
- value={searchValue}
- onSearch={onSearch}/>
- </div>
- <ColumnSelector
- columns={columns}
- onColumnToggle={onColumnToggle}/>
- </Grid>
- </Toolbar>
- <DataTable
- columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
- items={items}
- onRowClick={(_, item: T) => onRowClick(item)}
- onContextMenu={onContextMenu}
- onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
- onFiltersChange={onFiltersChange}
- onSortToggle={onSortToggle}
- extractKey={extractKey}/>
- <Toolbar>
- <Grid container justify="flex-end">
- <TablePagination
- count={itemsAvailable}
- rowsPerPage={rowsPerPage}
- rowsPerPageOptions={rowsPerPageOptions}
- page={this.props.page}
- onChangePage={this.changePage}
- onChangeRowsPerPage={this.changeRowsPerPage}
- component="div" />
- </Grid>
- </Toolbar>
- </Paper>
- ) : (
- <DefaultView
- classRoot={classes.defaultRoot}
- icon={defaultIcon}
- classIcon={classes.defaultIcon}
- messages={defaultMessages}
- classMessage={classes.defaultMessage} />
- )}
- </div>;
+ return <Paper>
+ <Toolbar className={classes.toolbar}>
+ <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+ <div className={classes.searchBox}>
+ <SearchInput
+ value={searchValue}
+ onSearch={onSearch} />
+ </div>
+ <ColumnSelector
+ columns={columns}
+ onColumnToggle={onColumnToggle} />
+ </Grid>
+ </Toolbar>
+ <DataTable
+ columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
+ items={items}
+ onRowClick={(_, item: T) => onRowClick(item)}
+ onContextMenu={onContextMenu}
+ onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
+ onFiltersChange={onFiltersChange}
+ onSortToggle={onSortToggle}
+ extractKey={extractKey}
+ defaultView={dataTableDefaultView}
+ />
+ <Toolbar>
+ <Grid container justify="flex-end">
+ <TablePagination
+ count={itemsAvailable}
+ rowsPerPage={rowsPerPage}
+ rowsPerPageOptions={rowsPerPageOptions}
+ page={this.props.page}
+ onChangePage={this.changePage}
+ onChangeRowsPerPage={this.changeRowsPerPage}
+ component="div" />
+ </Grid>
+ </Toolbar>
+ </Paper>;
}
changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
renderContextMenuTrigger = (item: T) =>
<Grid container justify="flex-end">
<IconButton onClick={event => this.props.onContextMenu(event, item)}>
- <MoreVertIcon/>
+ <MoreVertIcon />
</IconButton>
</Grid>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { DefaultViewDataProps, DefaultView } from '~/components/default-view/default-view';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { DetailsIcon } from '~/components/icon/icon';
+
+type CssRules = 'classRoot';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ classRoot: {
+ marginTop: theme.spacing.unit * 4,
+ marginBottom: theme.spacing.unit * 4,
+ },
+});
+type DataTableDefaultViewDataProps = Partial<Pick<DefaultViewDataProps, 'icon' | 'messages'>>;
+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'];
+ return <DefaultView {...classes} {...{ icon, messages }} />;
+ });
import { DataTable, DataColumns } from "./data-table";
import { DataTableFilters } from "../data-table-filters/data-table-filters";
import { SortDirection, createDataColumn } from "./data-column";
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
configure({ adapter: new Adapter() });
dataTable.find(DataTableFilters).prop("onChange")([]);
expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
});
+
+ it("shows default view if there is no items", () => {
+ const columns: DataColumns<string> = [
+ createDataColumn({
+ name: "Column 1",
+ render: () => <span />,
+ selected: true,
+ configurable: true
+ }),
+ ];
+ const dataTable = mount(<DataTable
+ columns={columns}
+ items={[]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={jest.fn()} />);
+ expect(dataTable.find(DataTableDefaultView)).toHaveLength(1);
+ });
});
import * as React from 'react';
import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core';
import { DataColumn, SortDirection } from './data-column';
-import { DataTableFilters, DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import { DataTableFilters, DataTableFilterItem } from "../data-table-filters/data-table-filters";
+import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
export type DataColumns<T, F extends DataTableFilterItem = DataTableFilterItem> = Array<DataColumn<T, F>>;
onSortToggle: (column: DataColumn<T>) => void;
onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
extractKey?: (item: T) => React.Key;
+ defaultView?: React.ReactNode;
}
-type CssRules = "tableBody" | "tableContainer" | "noItemsInfo";
+type CssRules = "tableBody" | "root" | "content" | "noItemsInfo";
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
- tableContainer: {
+ root: {
overflowX: 'auto',
overflowY: 'hidden'
},
+ content: {
+ display: 'inline-block',
+ },
tableBody: {
background: theme.palette.background.paper
},
noItemsInfo: {
textAlign: "center",
padding: theme.spacing.unit
- }
+ },
});
type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
class Component<T> extends React.Component<DataTableProps<T>> {
render() {
const { items, classes } = this.props;
- return <div
- className={classes.tableContainer}>
- <Table>
- <TableHead>
- <TableRow>
- {this.mapVisibleColumns(this.renderHeadCell)}
- </TableRow>
- </TableHead>
- <TableBody className={classes.tableBody}>
- {items.map(this.renderBodyRow)}
- </TableBody>
- </Table>
+ return <div className={classes.root}>
+ <div className={classes.content}>
+ <Table>
+ <TableHead>
+ <TableRow>
+ {this.mapVisibleColumns(this.renderHeadCell)}
+ </TableRow>
+ </TableHead>
+ <TableBody className={classes.tableBody}>
+ {items.map(this.renderBodyRow)}
+ </TableBody>
+ </Table>
+ {items.length === 0 && this.renderNoItemsPlaceholder()}
+ </div>
</div>;
}
+ renderNoItemsPlaceholder = () => {
+ return this.props.defaultView
+ ? this.props.defaultView
+ : <DataTableDefaultView />;
+ }
+
renderHeadCell = (column: DataColumn<T>, index: number) => {
const { name, key, renderHeader, filters, sortDirection } = column;
const { onSortToggle, onFiltersChange } = this.props;
key={extractKey ? extractKey(item) : index}
onClick={event => onRowClick && onRowClick(event, item)}
onContextMenu={this.handleRowContextMenu(item)}
- onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item) }>
+ onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}>
{this.mapVisibleColumns((column, index) => (
<TableCell key={column.key || index}>
{column.render(item)}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { DefaultViewDataProps, DefaultView } from '~/components/default-view/default-view';
+
+type CssRules = 'classRoot' | 'classIcon' | 'classMessage';
+
+const styles: StyleRulesCallback<CssRules> = () => ({
+ classRoot: {
+ position: 'absolute',
+ width: '80%',
+ left: '50%',
+ top: '50%',
+ transform: 'translate(-50%, -50%)'
+ },
+ classMessage: {
+ fontSize: '1.75rem',
+ },
+ classIcon: {
+ fontSize: '6rem'
+ }
+});
+
+type PanelDefaultViewProps = Pick<DefaultViewDataProps, 'icon' | 'messages'> & WithStyles<CssRules>;
+
+export const PanelDefaultView = withStyles(styles)(
+ ({ classes, ...props }: PanelDefaultViewProps) =>
+ <DefaultView {...classes} {...props} />);
import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
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 { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
type CssRules = "toolbar" | "button";
];
interface FavoritePanelDataProps {
- currentItemId: string;
+ favorites: FavoritesState;
}
interface FavoritePanelActionProps {
onDialogOpen: (ownerUuid: string) => void;
onItemDoubleClick: (item: string) => void;
}
+const mapStateToProps = ({ favorites }: RootState): FavoritePanelDataProps => ({
+ favorites
+});
const mapDispatchToProps = (dispatch: Dispatch): FavoritePanelActionProps => ({
onContextMenu: (event, resourceUuid) => {
& WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const FavoritePanel = withStyles(styles)(
- connect(undefined, mapDispatchToProps)(
+ connect(mapStateToProps, mapDispatchToProps)(
class extends React.Component<FavoritePanelProps> {
render() {
- return <DataExplorer
- id={FAVORITE_PANEL_ID}
- onRowClick={this.props.onItemClick}
- onRowDoubleClick={this.props.onItemDoubleClick}
- onContextMenu={this.props.onContextMenu}
- defaultIcon={FavoriteIcon}
- defaultMessages={['Your favorites list is empty.']}
- contextMenuColumn={true}/>;
+ return this.hasAnyFavorites()
+ ? <DataExplorer
+ id={FAVORITE_PANEL_ID}
+ onRowClick={this.props.onItemClick}
+ onRowDoubleClick={this.props.onItemDoubleClick}
+ onContextMenu={this.props.onContextMenu}
+ contextMenuColumn={true}
+ dataTableDefaultView={<DataTableDefaultView icon={FavoriteIcon}/>} />
+ : <PanelDefaultView
+ icon={FavoriteIcon}
+ messages={['Your favorites list is empty.']} />;
+ }
+
+ hasAnyFavorites = () => {
+ return Object
+ .keys(this.props.favorites)
+ .find(uuid => this.props.favorites[uuid]);
}
}
)
import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
import { ContainerRequestState } from '~/models/container-request';
import { SortDirection } from '~/components/data-table/data-column';
-import { ResourceKind } from '~/models/resource';
+import { ResourceKind, Resource } from '~/models/resource';
import { resourceLabel } from '~/common/labels';
import { ArvadosTheme } from '~/common/custom-theme';
import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
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 { openCollectionCreateDialog } from '../../store/collections/collection-create-actions';
+import { openCollectionCreateDialog } from '~/store/collections/collection-create-actions';
import { openProjectCreateDialog } from '~/store/projects/project-create-actions';
+import { filterResources } from '~/store/resources/resources';
+import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
type CssRules = 'root' | "toolbar" | "button";
New project
</Button>
</div>
- <DataExplorer
- id={PROJECT_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- defaultIcon={ProjectIcon}
- defaultMessages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']}
- contextMenuColumn={true}/>
+ {this.hasAnyItems()
+ ? <DataExplorer
+ id={PROJECT_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ dataTableDefaultView={<DataTableDefaultView icon={ProjectIcon}/>} />
+ : <PanelDefaultView
+ icon={ProjectIcon}
+ messages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />
+ }
+
</div>;
}
+ hasAnyItems = () => {
+ const resources = filterResources(this.isCurrentItemChild)(this.props.resources);
+ return resources.length > 0;
+ }
+
+ isCurrentItemChild = (resource: Resource) => {
+ return resource.ownerUuid === this.props.currentItemId;
+ }
+
handleNewProjectClick = () => {
this.props.dispatch<any>(openProjectCreateDialog(this.props.currentItemId));
}
} from "~/views-components/data-explorer/renderers";
import { navigateTo } from "~/store/navigation/navigation-action";
import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
-import { toggleCollectionTrashed, toggleProjectTrashed, toggleTrashed } from "~/store/trash/trash-actions";
+import { toggleTrashed } from "~/store/trash/trash-actions";
import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
import { Dispatch } from "redux";
+import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
+import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
type CssRules = "toolbar" | "button";
));
}
}}>
- <RestoreFromTrashIcon/>
+ <RestoreFromTrashIcon />
</IconButton>
);
configurable: true,
sortDirection: SortDirection.ASC,
filters: [],
- render: uuid => <ResourceName uuid={uuid}/>,
+ render: uuid => <ResourceName uuid={uuid} />,
width: "450px"
},
{
type: ResourceKind.PROJECT
}
],
- render: uuid => <ResourceType uuid={uuid}/>,
+ render: uuid => <ResourceType uuid={uuid} />,
width: "125px"
},
{
configurable: false,
sortDirection: SortDirection.NONE,
filters: [],
- render: uuid => <ResourceRestore uuid={uuid}/>,
+ render: uuid => <ResourceRestore uuid={uuid} />,
width: "50px"
}
];
}))(
class extends React.Component<TrashPanelProps> {
render() {
- return <DataExplorer
- id={TRASH_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- defaultIcon={TrashIcon}
- defaultMessages={['Your trash list is empty.']}
- contextMenuColumn={false}/>
- ;
+ return this.hasAnyTrashedResources()
+ ? <DataExplorer
+ id={TRASH_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={false}
+ dataTableDefaultView={<DataTableDefaultView icon={TrashIcon}/>} />
+ : <PanelDefaultView
+ icon={TrashIcon}
+ messages={['Your trash list is empty.']} />;
+ }
+
+ hasAnyTrashedResources = () => {
+ // TODO: implement check if there is anything in the trash,
+ // without taking pagination into the account
+ return true;
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {