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, 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 dataTableDefaultView?: React.ReactNode;
69 currentRefresh?: string;
70 currentRoute?: string;
71 hideColumnSelector?: boolean;
72 paperProps?: PaperProps;
73 actions?: React.ReactNode;
74 hideSearchInput?: boolean;
75 title?: React.ReactNode;
77 currentItemUuid: string;
81 interface DataExplorerActionProps<T> {
82 onSetColumns: (columns: DataColumns<T>) => void;
83 onSearch: (value: string) => void;
84 onRowClick: (item: T) => void;
85 onRowDoubleClick: (item: T) => void;
86 onColumnToggle: (column: DataColumn<T>) => void;
87 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
88 onSortToggle: (column: DataColumn<T>) => void;
89 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
90 onChangePage: (page: number) => void;
91 onChangeRowsPerPage: (rowsPerPage: number) => void;
92 onLoadMore: (page: number) => void;
93 extractKey?: (item: T) => React.Key;
96 type DataExplorerProps<T> = DataExplorerDataProps<T> &
97 DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
99 export const DataExplorer = withStyles(styles)(
100 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
107 componentDidUpdate(prevProps: DataExplorerProps<T>) {
108 const currentRefresh = this.props.currentRefresh || '';
109 const currentRoute = this.props.currentRoute || '';
111 if (currentRoute !== this.state.prevRoute) {
112 // Component already mounted, but the user comes from a route change,
113 // like browsing through a project hierarchy.
115 showLoading: this.props.working,
116 prevRoute: currentRoute,
120 if (currentRefresh !== this.state.prevRefresh) {
121 // Component already mounted, but the user just clicked the
124 showLoading: this.props.working,
125 prevRefresh: currentRefresh,
128 if (this.state.showLoading && !this.props.working) {
135 componentDidMount() {
136 if (this.props.onSetColumns) {
137 this.props.onSetColumns(this.props.columns);
139 // Component just mounted, so we need to show the loading indicator.
141 showLoading: this.props.working,
142 prevRefresh: this.props.currentRefresh || '',
143 prevRoute: this.props.currentRoute || '',
149 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
150 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
151 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
152 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
153 paperKey, fetchMode, currentItemUuid, title,
154 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
157 return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
158 <Grid container direction="column" wrap="nowrap" className={classes.container}>
160 {title && <Grid item xs className={classes.title}>{title}</Grid>}
162 (!hideColumnSelector || !hideSearchInput || !!actions) &&
163 <Grid className={classes.headerMenu} item xs>
164 <Toolbar className={classes.toolbar}>
165 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
166 {!hideSearchInput && <div className={classes.searchBox}>
167 {!hideSearchInput && <SearchInput
170 selfClearProp={currentItemUuid}
171 onSearch={onSearch} />}
174 {!hideColumnSelector && <ColumnSelector
176 onColumnToggle={onColumnToggle} />}
178 { doMaximizePanel && !panelMaximized &&
179 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
180 <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
183 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
184 <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
190 <Grid item xs="auto" className={classes.dataTable}><DataTable
191 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
193 onRowClick={(_, item: T) => onRowClick(item)}
194 onContextMenu={onContextMenu}
195 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
196 onFiltersChange={onFiltersChange}
197 onSortToggle={onSortToggle}
198 extractKey={extractKey}
199 working={this.state.showLoading}
200 defaultView={dataTableDefaultView}
201 currentItemUuid={currentItemUuid}
202 currentRoute={paperKey} /></Grid>
203 <Grid item xs><Toolbar className={classes.footer}>
207 <span data-cy="element-path">
212 <Grid container={!elementPath} justify="flex-end">
213 {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
214 count={itemsAvailable}
215 rowsPerPage={rowsPerPage}
216 rowsPerPageOptions={rowsPerPageOptions}
217 page={this.props.page}
218 onChangePage={this.changePage}
219 onChangeRowsPerPage={this.changeRowsPerPage}
220 // Disable next button on empty lists since that's not default behavior
221 nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
222 component="div" /> : <Button
225 onClick={this.loadMore}
233 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
234 this.props.onChangePage(page);
237 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
238 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
242 this.props.onLoadMore(this.props.page + 1);
245 renderContextMenuTrigger = (item: T) =>
246 <Grid container justify="center">
247 <Tooltip title="More options" disableFocusListener>
248 <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
254 contextMenuColumn: DataColumn<any> = {
258 filters: createTree(),
259 key: "context-actions",
260 render: this.renderContextMenuTrigger