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
164 return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
165 <Grid container direction="column" wrap="nowrap" className={classes.container}>
167 {title && <Grid item xs className={classes.title}>{title}</Grid>}
169 (!hideColumnSelector || !hideSearchInput || !!actions) &&
170 <Grid className={classes.headerMenu} item xs>
171 <Toolbar className={classes.toolbar}>
172 {!hideSearchInput && <div className={classes.searchBox}>
173 {!hideSearchInput && <SearchInput
176 selfClearProp={currentItemUuid}
177 onSearch={onSearch} />}
180 {!hideColumnSelector && <ColumnSelector
182 onColumnToggle={onColumnToggle} />}
183 { doUnMaximizePanel && panelMaximized &&
184 <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
185 <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
187 { doMaximizePanel && !panelMaximized &&
188 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
189 <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
192 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
193 <IconButton disabled={panelMaximized} onClick={doHidePanel}><CloseIcon /></IconButton>
199 <Grid item xs="auto" className={classes.dataTable}><DataTable
200 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
202 onRowClick={(_, item: T) => onRowClick(item)}
203 onContextMenu={onContextMenu}
204 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
205 onFiltersChange={onFiltersChange}
206 onSortToggle={onSortToggle}
207 extractKey={extractKey}
208 working={this.state.showLoading}
209 defaultViewIcon={defaultViewIcon}
210 defaultViewMessages={defaultViewMessages}
211 currentItemUuid={currentItemUuid}
212 currentRoute={paperKey} /></Grid>
213 <Grid item xs><Toolbar className={classes.footer}>
217 <span data-cy="element-path">
222 <Grid container={!elementPath} justify="flex-end">
223 {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
224 count={itemsAvailable}
225 rowsPerPage={rowsPerPage}
226 rowsPerPageOptions={rowsPerPageOptions}
227 page={this.props.page}
228 onChangePage={this.changePage}
229 onChangeRowsPerPage={this.changeRowsPerPage}
230 // Disable next button on empty lists since that's not default behavior
231 nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
232 component="div" /> : <Button
235 onClick={this.loadMore}
243 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
244 this.props.onChangePage(page);
247 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
248 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
252 this.props.onLoadMore(this.props.page + 1);
255 renderContextMenuTrigger = (item: T) =>
256 <Grid container justify="center">
257 <Tooltip title="More options" disableFocusListener>
258 <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
264 contextMenuColumn: DataColumn<any> = {
268 filters: createTree(),
269 key: "context-actions",
270 render: this.renderContextMenuTrigger