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) => ({
28 paddingBottom: theme.spacing.unit * 2
31 paddingTop: theme.spacing.unit,
32 paddingRight: theme.spacing.unit * 2,
44 display: 'inline-block',
45 paddingLeft: theme.spacing.unit * 3,
46 paddingTop: theme.spacing.unit * 3,
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 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
173 {!hideSearchInput && <div className={classes.searchBox}>
174 {!hideSearchInput && <SearchInput
177 selfClearProp={currentItemUuid}
178 onSearch={onSearch} />}
181 {!hideColumnSelector && <ColumnSelector
183 onColumnToggle={onColumnToggle} />}
185 { doUnMaximizePanel && panelMaximized &&
186 <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
187 <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
189 { doMaximizePanel && !panelMaximized &&
190 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
191 <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
194 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
195 <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
201 <Grid item xs="auto" className={classes.dataTable}><DataTable
202 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
204 onRowClick={(_, item: T) => onRowClick(item)}
205 onContextMenu={onContextMenu}
206 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
207 onFiltersChange={onFiltersChange}
208 onSortToggle={onSortToggle}
209 extractKey={extractKey}
210 working={this.state.showLoading}
211 defaultViewIcon={defaultViewIcon}
212 defaultViewMessages={defaultViewMessages}
213 currentItemUuid={currentItemUuid}
214 currentRoute={paperKey} /></Grid>
215 <Grid item xs><Toolbar className={classes.footer}>
219 <span data-cy="element-path">
224 <Grid container={!elementPath} justify="flex-end">
225 {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
226 count={itemsAvailable}
227 rowsPerPage={rowsPerPage}
228 rowsPerPageOptions={rowsPerPageOptions}
229 page={this.props.page}
230 onChangePage={this.changePage}
231 onChangeRowsPerPage={this.changeRowsPerPage}
232 // Disable next button on empty lists since that's not default behavior
233 nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
234 component="div" /> : <Button
237 onClick={this.loadMore}
245 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
246 this.props.onChangePage(page);
249 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
250 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
254 this.props.onLoadMore(this.props.page + 1);
257 renderContextMenuTrigger = (item: T) =>
258 <Grid container justify="center">
259 <Tooltip title="More options" disableFocusListener>
260 <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
266 contextMenuColumn: DataColumn<any> = {
270 filters: createTree(),
271 key: "context-actions",
272 render: this.renderContextMenuTrigger