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, MoreOptionsIcon } from 'components/icon/icon';
15 import { PaperProps } from '@material-ui/core/Paper';
16 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
18 type CssRules = 'searchBox' | 'headerMenu' | "toolbar" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
20 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
22 paddingBottom: theme.spacing.unit * 2
25 paddingTop: theme.spacing.unit,
26 paddingRight: theme.spacing.unit * 2,
38 display: 'inline-block',
39 paddingLeft: theme.spacing.unit * 3,
40 paddingTop: theme.spacing.unit * 3,
52 display: 'inline-block'
56 interface DataExplorerDataProps<T> {
57 fetchMode: DataTableFetchMode;
59 itemsAvailable: number;
60 columns: DataColumns<T>;
64 rowsPerPageOptions: number[];
66 contextMenuColumn: boolean;
67 defaultViewIcon?: IconType;
68 defaultViewMessages?: string[];
70 currentRefresh?: string;
71 currentRoute?: string;
72 hideColumnSelector?: boolean;
73 paperProps?: PaperProps;
74 actions?: React.ReactNode;
75 hideSearchInput?: boolean;
76 title?: React.ReactNode;
78 currentItemUuid: string;
82 interface DataExplorerActionProps<T> {
83 onSetColumns: (columns: DataColumns<T>) => void;
84 onSearch: (value: string) => void;
85 onRowClick: (item: T) => void;
86 onRowDoubleClick: (item: T) => void;
87 onColumnToggle: (column: DataColumn<T>) => void;
88 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
89 onSortToggle: (column: DataColumn<T>) => void;
90 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
91 onChangePage: (page: number) => void;
92 onChangeRowsPerPage: (rowsPerPage: number) => void;
93 onLoadMore: (page: number) => void;
94 extractKey?: (item: T) => React.Key;
97 type DataExplorerProps<T> = DataExplorerDataProps<T> &
98 DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
100 export const DataExplorer = withStyles(styles)(
101 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
108 componentDidUpdate(prevProps: DataExplorerProps<T>) {
109 const currentRefresh = this.props.currentRefresh || '';
110 const currentRoute = this.props.currentRoute || '';
112 if (currentRoute !== this.state.prevRoute) {
113 // Component already mounted, but the user comes from a route change,
114 // like browsing through a project hierarchy.
116 showLoading: this.props.working,
117 prevRoute: currentRoute,
121 if (currentRefresh !== this.state.prevRefresh) {
122 // Component already mounted, but the user just clicked the
125 showLoading: this.props.working,
126 prevRefresh: currentRefresh,
129 if (this.state.showLoading && !this.props.working) {
136 componentDidMount() {
137 if (this.props.onSetColumns) {
138 this.props.onSetColumns(this.props.columns);
140 // Component just mounted, so we need to show the loading indicator.
142 showLoading: this.props.working,
143 prevRefresh: this.props.currentRefresh || '',
144 prevRoute: this.props.currentRoute || '',
150 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
151 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
152 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
153 defaultViewIcon, defaultViewMessages, hideColumnSelector, actions, paperProps, hideSearchInput,
154 paperKey, fetchMode, currentItemUuid, title,
155 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
158 return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
159 <Grid container direction="column" wrap="nowrap" className={classes.container}>
161 {title && <Grid item xs className={classes.title}>{title}</Grid>}
163 (!hideColumnSelector || !hideSearchInput || !!actions) &&
164 <Grid className={classes.headerMenu} item xs>
165 <Toolbar className={classes.toolbar}>
166 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
167 {!hideSearchInput && <div className={classes.searchBox}>
168 {!hideSearchInput && <SearchInput
171 selfClearProp={currentItemUuid}
172 onSearch={onSearch} />}
175 {!hideColumnSelector && <ColumnSelector
177 onColumnToggle={onColumnToggle} />}
179 { doMaximizePanel && !panelMaximized &&
180 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
181 <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
184 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
185 <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
191 <Grid item xs="auto" className={classes.dataTable}><DataTable
192 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
194 onRowClick={(_, item: T) => onRowClick(item)}
195 onContextMenu={onContextMenu}
196 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
197 onFiltersChange={onFiltersChange}
198 onSortToggle={onSortToggle}
199 extractKey={extractKey}
200 working={this.state.showLoading}
201 defaultViewIcon={defaultViewIcon}
202 defaultViewMessages={defaultViewMessages}
203 currentItemUuid={currentItemUuid}
204 currentRoute={paperKey} /></Grid>
205 <Grid item xs><Toolbar className={classes.footer}>
209 <span data-cy="element-path">
214 <Grid container={!elementPath} justify="flex-end">
215 {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
216 count={itemsAvailable}
217 rowsPerPage={rowsPerPage}
218 rowsPerPageOptions={rowsPerPageOptions}
219 page={this.props.page}
220 onChangePage={this.changePage}
221 onChangeRowsPerPage={this.changeRowsPerPage}
222 // Disable next button on empty lists since that's not default behavior
223 nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
224 component="div" /> : <Button
227 onClick={this.loadMore}
235 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
236 this.props.onChangePage(page);
239 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
240 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
244 this.props.onLoadMore(this.props.page + 1);
247 renderContextMenuTrigger = (item: T) =>
248 <Grid container justify="center">
249 <Tooltip title="More options" disableFocusListener>
250 <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
256 contextMenuColumn: DataColumn<any> = {
260 filters: createTree(),
261 key: "context-actions",
262 render: this.renderContextMenuTrigger