1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from '@material-ui/core';
7 import { ColumnSelector } from 'components/column-selector/column-selector';
8 import { DataTable, DataColumns, DataTableFetchMode } from 'components/data-table/data-table';
9 import { DataColumn } from 'components/data-table/data-column';
10 import { SearchInput } from 'components/search-input/search-input';
11 import { ArvadosTheme } from 'common/custom-theme';
12 import { createTree } from 'models/tree';
13 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
14 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
15 import { PaperProps } from '@material-ui/core/Paper';
16 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
17 import { MultiselectToolbar, defaultActions } from 'components/multiselectToolbar/MultiselectToolbar';
18 import { TCheckedList } from 'components/data-table/data-table';
20 type CssRules = 'searchBox' | 'headerMenu' | 'toolbar' | 'footer' | 'root' | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
22 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
28 paddingRight: theme.spacing.unit,
40 display: 'inline-block',
41 paddingLeft: theme.spacing.unit * 2,
42 paddingTop: theme.spacing.unit * 2,
56 flexDirection: 'row-reverse',
57 justifyContent: 'space-between',
61 interface DataExplorerDataProps<T> {
62 fetchMode: DataTableFetchMode;
64 itemsAvailable: number;
65 columns: DataColumns<T, any>;
69 rowsPerPageOptions: number[];
71 contextMenuColumn: boolean;
72 defaultViewIcon?: IconType;
73 defaultViewMessages?: string[];
75 currentRefresh?: string;
76 currentRoute?: string;
77 hideColumnSelector?: boolean;
78 paperProps?: PaperProps;
79 actions?: React.ReactNode;
80 hideSearchInput?: boolean;
81 title?: React.ReactNode;
83 currentItemUuid: string;
85 isMSToolbarVisible: boolean;
88 interface DataExplorerActionProps<T> {
89 onSetColumns: (columns: DataColumns<T, any>) => void;
90 onSearch: (value: string) => void;
91 onRowClick: (item: T) => void;
92 onRowDoubleClick: (item: T) => void;
93 onColumnToggle: (column: DataColumn<T, any>) => void;
94 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
95 onSortToggle: (column: DataColumn<T, any>) => void;
96 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
97 onChangePage: (page: number) => void;
98 onChangeRowsPerPage: (rowsPerPage: number) => void;
99 onLoadMore: (page: number) => void;
100 extractKey?: (item: T) => React.Key;
101 toggleMSToolbar: (isVisible: boolean) => void;
102 setCheckedListOnStore: (checkedList: TCheckedList) => void;
105 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
107 export const DataExplorer = withStyles(styles)(
108 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
115 componentDidUpdate(prevProps: DataExplorerProps<T>) {
116 const currentRefresh = this.props.currentRefresh || '';
117 const currentRoute = this.props.currentRoute || '';
119 if (currentRoute !== this.state.prevRoute) {
120 // Component already mounted, but the user comes from a route change,
121 // like browsing through a project hierarchy.
123 showLoading: this.props.working,
124 prevRoute: currentRoute,
128 if (currentRefresh !== this.state.prevRefresh) {
129 // Component already mounted, but the user just clicked the
132 showLoading: this.props.working,
133 prevRefresh: currentRefresh,
136 if (this.state.showLoading && !this.props.working) {
143 componentDidMount() {
144 if (this.props.onSetColumns) {
145 this.props.onSetColumns(this.props.columns);
147 // Component just mounted, so we need to show the loading indicator.
149 showLoading: this.props.working,
150 prevRefresh: this.props.currentRefresh || '',
151 prevRoute: this.props.currentRoute || '',
191 setCheckedListOnStore,
194 <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props['data-cy']}>
195 <Grid container direction='column' wrap='nowrap' className={classes.container}>
198 <Grid item xs className={classes.title}>
202 {(!hideColumnSelector || !hideSearchInput || !!actions) && (
203 <Grid className={classes.headerMenu} item xs>
204 <Toolbar className={classes.toolbar}>
205 {!hideSearchInput && (
206 <div className={classes.searchBox}>
207 {!hideSearchInput && <SearchInput label={searchLabel} value={searchValue} selfClearProp={''} onSearch={onSearch} />}
211 {!hideColumnSelector && <ColumnSelector columns={columns} onColumnToggle={onColumnToggle} />}
212 {doUnMaximizePanel && panelMaximized && (
213 <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
214 <IconButton onClick={doUnMaximizePanel}>
219 {doMaximizePanel && !panelMaximized && (
220 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
221 <IconButton onClick={doMaximizePanel}>
227 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
228 <IconButton disabled={panelMaximized} onClick={doHidePanel}>
234 {/* {isMSToolbarVisible && <MultiselectToolbar buttons={defaultActions} />} */}
235 <MultiselectToolbar buttons={defaultActions} />
239 <Grid item xs='auto' className={classes.dataTable}>
241 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
243 onRowClick={(_, item: T) => onRowClick(item)}
244 onContextMenu={onContextMenu}
245 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
246 onFiltersChange={onFiltersChange}
247 onSortToggle={onSortToggle}
248 extractKey={extractKey}
249 working={this.state.showLoading}
250 defaultViewIcon={defaultViewIcon}
251 defaultViewMessages={defaultViewMessages}
252 currentItemUuid={currentItemUuid}
253 currentRoute={paperKey}
254 toggleMSToolbar={toggleMSToolbar}
255 setCheckedListOnStore={setCheckedListOnStore}
259 <Toolbar className={classes.footer}>
262 <span data-cy='element-path'>{elementPath}</span>
265 <Grid container={!elementPath} justify='flex-end'>
266 {fetchMode === DataTableFetchMode.PAGINATED ? (
268 count={itemsAvailable}
269 rowsPerPage={rowsPerPage}
270 rowsPerPageOptions={rowsPerPageOptions}
271 page={this.props.page}
272 onChangePage={this.changePage}
273 onChangeRowsPerPage={this.changeRowsPerPage}
274 // Disable next button on empty lists since that's not default behavior
275 nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
279 <Button variant='text' size='medium' onClick={this.loadMore}>
291 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
292 this.props.onChangePage(page);
295 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
296 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
300 this.props.onLoadMore(this.props.page + 1);
303 renderContextMenuTrigger = (item: T) => (
304 <Grid container justify='center'>
305 <Tooltip title='More options' disableFocusListener>
306 <IconButton className={this.props.classes.moreOptionsButton} onClick={(event) => this.props.onContextMenu(event, item)}>
313 contextMenuColumn: DataColumn<any, any> = {
317 filters: createTree(),
318 key: 'context-actions',
319 render: this.renderContextMenuTrigger,