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';
20 } from 'components/icon/icon';
21 import { PaperProps } from '@material-ui/core/Paper';
22 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
24 type CssRules = 'searchBox' | 'headerMenu' | "toolbar" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
26 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
32 paddingRight: theme.spacing.unit,
44 display: 'inline-block',
45 paddingLeft: theme.spacing.unit * 2,
46 paddingTop: theme.spacing.unit * 2,
58 display: 'inline-block',
62 interface DataExplorerDataProps<T> {
63 fetchMode: DataTableFetchMode;
65 itemsAvailable: number;
66 columns: DataColumns<T>;
70 rowsPerPageOptions: number[];
72 contextMenuColumn: boolean;
73 defaultViewIcon?: IconType;
74 defaultViewMessages?: string[];
76 currentRefresh?: string;
77 currentRoute?: string;
78 hideColumnSelector?: boolean;
79 paperProps?: PaperProps;
80 actions?: React.ReactNode;
81 hideSearchInput?: boolean;
82 title?: React.ReactNode;
84 currentItemUuid: string;
88 interface DataExplorerActionProps<T> {
89 onSetColumns: (columns: DataColumns<T>) => void;
90 onSearch: (value: string) => void;
91 onRowClick: (item: T) => void;
92 onRowDoubleClick: (item: T) => void;
93 onColumnToggle: (column: DataColumn<T>) => void;
94 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
95 onSortToggle: (column: DataColumn<T>) => void;
96 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
97 onChangePage: (page: number) => void;
98 onChangeRowsPerPage: (rowsPerPage: number) => void;
99 onLoadMore: (page: number) => void;
100 extractKey?: (item: T) => React.Key;
103 type DataExplorerProps<T> = DataExplorerDataProps<T> &
104 DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
106 export const DataExplorer = withStyles(styles)(
107 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
114 componentDidUpdate(prevProps: DataExplorerProps<T>) {
115 const currentRefresh = this.props.currentRefresh || '';
116 const currentRoute = this.props.currentRoute || '';
118 if (currentRoute !== this.state.prevRoute) {
119 // Component already mounted, but the user comes from a route change,
120 // like browsing through a project hierarchy.
122 showLoading: this.props.working,
123 prevRoute: currentRoute,
127 if (currentRefresh !== this.state.prevRefresh) {
128 // Component already mounted, but the user just clicked the
131 showLoading: this.props.working,
132 prevRefresh: currentRefresh,
135 if (this.state.showLoading && !this.props.working) {
142 componentDidMount() {
143 if (this.props.onSetColumns) {
144 this.props.onSetColumns(this.props.columns);
146 // Component just mounted, so we need to show the loading indicator.
148 showLoading: this.props.working,
149 prevRefresh: this.props.currentRefresh || '',
150 prevRoute: this.props.currentRoute || '',
156 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
157 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
158 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
159 defaultViewIcon, defaultViewMessages, hideColumnSelector, actions, paperProps, hideSearchInput,
160 paperKey, fetchMode, currentItemUuid, title,
161 doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName, panelMaximized, elementPath
163 return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
164 <Grid container direction="column" wrap="nowrap" className={classes.container}>
166 {title && <Grid item xs className={classes.title}>{title}</Grid>}
168 (!hideColumnSelector || !hideSearchInput || !!actions) &&
169 <Grid className={classes.headerMenu} item xs>
170 <Toolbar className={classes.toolbar}>
171 {!hideSearchInput && <div className={classes.searchBox}>
172 {!hideSearchInput && <SearchInput
176 onSearch={onSearch} />}
179 {!hideColumnSelector && <ColumnSelector
181 onColumnToggle={onColumnToggle} />}
182 { doUnMaximizePanel && panelMaximized &&
183 <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
184 <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
186 { doMaximizePanel && !panelMaximized &&
187 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
188 <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
191 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
192 <IconButton disabled={panelMaximized} onClick={doHidePanel}><CloseIcon /></IconButton>
198 <Grid item xs="auto" className={classes.dataTable}><DataTable
199 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
201 onRowClick={(_, item: T) => onRowClick(item)}
202 onContextMenu={onContextMenu}
203 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
204 onFiltersChange={onFiltersChange}
205 onSortToggle={onSortToggle}
206 extractKey={extractKey}
207 working={this.state.showLoading}
208 defaultViewIcon={defaultViewIcon}
209 defaultViewMessages={defaultViewMessages}
210 currentItemUuid={currentItemUuid}
211 currentRoute={paperKey} /></Grid>
212 <Grid item xs><Toolbar className={classes.footer}>
216 <span data-cy="element-path">
221 <Grid container={!elementPath} justify="flex-end">
222 {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
223 count={itemsAvailable}
224 rowsPerPage={rowsPerPage}
225 rowsPerPageOptions={rowsPerPageOptions}
226 page={this.props.page}
227 onChangePage={this.changePage}
228 onChangeRowsPerPage={this.changeRowsPerPage}
229 // Disable next button on empty lists since that's not default behavior
230 nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
231 component="div" /> : <Button
234 onClick={this.loadMore}
242 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
243 this.props.onChangePage(page);
246 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
247 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
251 this.props.onLoadMore(this.props.page + 1);
254 renderContextMenuTrigger = (item: T) =>
255 <Grid container justify="center">
256 <Tooltip title="More options" disableFocusListener>
257 <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
263 contextMenuColumn: DataColumn<any> = {
267 filters: createTree(),
268 key: "context-actions",
269 render: this.renderContextMenuTrigger