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 hideColumnSelector?: boolean;
70 paperProps?: PaperProps;
71 actions?: React.ReactNode;
72 hideSearchInput?: boolean;
73 title?: React.ReactNode;
75 currentItemUuid: string;
79 interface DataExplorerActionProps<T> {
80 onSetColumns: (columns: DataColumns<T>) => void;
81 onSearch: (value: string) => void;
82 onRowClick: (item: T) => void;
83 onRowDoubleClick: (item: T) => void;
84 onColumnToggle: (column: DataColumn<T>) => void;
85 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
86 onSortToggle: (column: DataColumn<T>) => void;
87 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
88 onChangePage: (page: number) => void;
89 onChangeRowsPerPage: (rowsPerPage: number) => void;
90 onLoadMore: (page: number) => void;
91 extractKey?: (item: T) => React.Key;
94 type DataExplorerProps<T> = DataExplorerDataProps<T> &
95 DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
97 export const DataExplorer = withStyles(styles)(
98 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
100 componentDidMount() {
101 if (this.props.onSetColumns) {
102 this.props.onSetColumns(this.props.columns);
108 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
109 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
110 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
111 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
112 paperKey, fetchMode, currentItemUuid, title,
113 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
116 const dataCy = this.props["data-cy"];
117 return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={dataCy}>
118 <Grid container direction="column" wrap="nowrap" className={classes.container}>
120 {title && <Grid item xs className={classes.title}>{title}</Grid>}
122 (!hideColumnSelector || !hideSearchInput || !!actions) &&
123 <Grid className={classes.headerMenu} item xs>
124 <Toolbar className={classes.toolbar}>
125 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
126 {!hideSearchInput && <div className={classes.searchBox}>
127 {!hideSearchInput && <SearchInput
130 selfClearProp={currentItemUuid}
131 onSearch={onSearch} />}
134 {!hideColumnSelector && <ColumnSelector
136 onColumnToggle={onColumnToggle} />}
138 { doMaximizePanel && !panelMaximized &&
139 <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
140 <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
143 <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
144 <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
150 <Grid item xs="auto" className={classes.dataTable}><DataTable
151 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
153 onRowClick={(_, item: T) => onRowClick(item)}
154 onContextMenu={onContextMenu}
155 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
156 onFiltersChange={onFiltersChange}
157 onSortToggle={onSortToggle}
158 extractKey={extractKey}
160 defaultView={dataTableDefaultView}
161 currentItemUuid={currentItemUuid}
162 currentRoute={paperKey} /></Grid>
163 <Grid item xs><Toolbar className={classes.footer}>
167 <span data-cy="element-path">
172 <Grid container={!elementPath} justify="flex-end">
173 {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
174 count={itemsAvailable}
175 rowsPerPage={rowsPerPage}
176 rowsPerPageOptions={rowsPerPageOptions}
177 page={this.props.page}
178 onChangePage={this.changePage}
179 onChangeRowsPerPage={this.changeRowsPerPage}
180 // Disable next button on empty lists since that's not default behavior
181 nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
182 component="div" /> : <Button
185 onClick={this.loadMore}
193 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
194 this.props.onChangePage(page);
197 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
198 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
202 this.props.onLoadMore(this.props.page + 1);
205 renderContextMenuTrigger = (item: T) =>
206 <Grid container justify="center">
207 <Tooltip title="More options" disableFocusListener>
208 <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
214 contextMenuColumn: DataColumn<any> = {
218 filters: createTree(),
219 key: "context-actions",
220 render: this.renderContextMenuTrigger