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, Typography } 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 { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
13 import { TCheckedList } from "components/data-table/data-table";
14 import { createTree } from "models/tree";
15 import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
16 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
17 import { PaperProps } from "@material-ui/core/Paper";
18 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
19 import classNames from "classnames";
21 type CssRules = "titleWrapper" | "msToolbarStyles" | "subpanelToolbarStyles" | "searchBox" | "headerMenu" | "toolbar" | "footer"| "loadMoreContainer" | "numResults" | "root" | "moreOptionsButton" | "title" | 'subProcessTitle' | 'progressWrapper' | 'progressWrapperNoTitle' | "dataTable" | "container";
23 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
26 justifyContent: "space-between",
31 subpanelToolbarStyles: {
39 paddingRight: theme.spacing.unit,
60 marginBottom: '-0.5rem',
64 display: "inline-block",
65 paddingLeft: theme.spacing.unit * 2,
66 paddingTop: theme.spacing.unit * 2,
71 display: "inline-block",
72 paddingLeft: theme.spacing.unit * 2,
73 paddingTop: theme.spacing.unit * 2,
83 progressWrapperNoTitle: {
100 interface DataExplorerDataProps<T> {
101 fetchMode: DataTableFetchMode;
103 itemsAvailable: number;
104 columns: DataColumns<T, any>;
105 searchLabel?: string;
108 rowsPerPageOptions: number[];
110 contextMenuColumn: boolean;
111 defaultViewIcon?: IconType;
112 defaultViewMessages?: string[];
114 currentRoute?: string;
115 hideColumnSelector?: boolean;
116 paperProps?: PaperProps;
117 actions?: React.ReactNode;
118 hideSearchInput?: boolean;
119 title?: React.ReactNode;
120 progressBar?: React.ReactNode;
122 currentRouteUuid: string;
123 selectedResourceUuid: string;
124 elementPath?: string;
125 isMSToolbarVisible: boolean;
126 checkedList: TCheckedList;
128 searchBarValue: string;
131 interface DataExplorerActionProps<T> {
132 onSetColumns: (columns: DataColumns<T, any>) => void;
133 onSearch: (value: string) => void;
134 onRowClick: (item: T) => void;
135 onRowDoubleClick: (item: T) => void;
136 onColumnToggle: (column: DataColumn<T, any>) => void;
137 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
138 onSortToggle: (column: DataColumn<T, any>) => void;
139 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
140 onChangePage: (page: number) => void;
141 onChangeRowsPerPage: (rowsPerPage: number) => void;
142 onLoadMore: (page: number) => void;
143 extractKey?: (item: T) => React.Key;
144 toggleMSToolbar: (isVisible: boolean) => void;
145 setCheckedListOnStore: (checkedList: TCheckedList) => void;
146 setSelectedUuid: (uuid: string) => void;
149 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
151 export const DataExplorer = withStyles(styles)(
152 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
154 msToolbarInDetailsCard: true,
157 multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
158 maxItemsAvailable = 0;
160 componentDidMount() {
161 if (this.props.onSetColumns) {
162 this.props.onSetColumns(this.props.columns);
166 componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
167 const { selectedResourceUuid, currentRouteUuid } = this.props;
168 if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
170 msToolbarInDetailsCard: selectedResourceUuid === this.props.currentRouteUuid,
173 if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
174 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
176 if (this.props.searchBarValue !== prevProps.searchBarValue) {
177 this.maxItemsAvailable = 0;
207 selectedResourceUuid,
218 setCheckedListOnStore,
224 className={classes.root}
227 data-cy={this.props["data-cy"]}
233 className={classes.container}
235 <div data-cy="title-wrapper" className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
240 className={!!progressBar ? classes.subProcessTitle : classes.title}
246 <div className={classNames({
247 [classes.progressWrapper]: true,
248 [classes.progressWrapperNoTitle]: !title,
249 })}>{progressBar}</div>
251 {this.multiSelectToolbarInTitle && !this.state.msToolbarInDetailsCard && <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />}
252 {(!hideColumnSelector || !hideSearchInput || !!actions) && (
254 className={classes.headerMenu}
258 <Toolbar className={classes.toolbar}>
259 <Grid container justify="space-between" wrap="nowrap" alignItems="center">
260 {!hideSearchInput && (
261 <div className={classes.searchBox}>
262 {!hideSearchInput && (
273 {!hideColumnSelector && (
276 onColumnToggle={onColumnToggle}
280 {doUnMaximizePanel && panelMaximized && (
282 title={`Unmaximize ${panelName || "panel"}`}
285 <IconButton onClick={doUnMaximizePanel}>
290 {doMaximizePanel && !panelMaximized && (
292 title={`Maximize ${panelName || "panel"}`}
295 <IconButton onClick={doMaximizePanel}>
302 title={`Close ${panelName || "panel"}`}
306 disabled={panelMaximized}
307 onClick={doHidePanel}
317 {!this.multiSelectToolbarInTitle && <MultiselectToolbar isSubPanel={true} injectedStyles={classes.subpanelToolbarStyles}/>}
321 className={classes.dataTable}
322 style={currentRoute?.includes('search-results') || !!progressBar ? {marginTop: '-10px'} : {}}
325 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
327 onRowClick={(_, item: T) => onRowClick(item)}
328 onContextMenu={onContextMenu}
329 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
330 onFiltersChange={onFiltersChange}
331 onSortToggle={onSortToggle}
332 extractKey={extractKey}
333 defaultViewIcon={defaultViewIcon}
334 defaultViewMessages={defaultViewMessages}
335 currentRoute={paperKey}
336 toggleMSToolbar={toggleMSToolbar}
337 setCheckedListOnStore={setCheckedListOnStore}
338 checkedList={checkedList}
339 selectedResourceUuid={selectedResourceUuid}
340 setSelectedUuid={this.props.setSelectedUuid}
341 currentRouteUuid={this.props.currentRouteUuid}
343 isNotFound={this.props.isNotFound}
350 <Toolbar className={classes.footer}>
353 <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
357 container={!elementPath}
360 {fetchMode === DataTableFetchMode.PAGINATED ? (
362 count={itemsAvailable}
363 rowsPerPage={rowsPerPage}
364 rowsPerPageOptions={rowsPerPageOptions}
365 page={this.props.page}
366 onChangePage={this.changePage}
367 onChangeRowsPerPage={this.changeRowsPerPage}
368 // Disable next button on empty lists since that's not default behavior
369 nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
373 <Grid className={classes.loadMoreContainer}>
374 <Typography className={classes.numResults}>
375 Showing {items.length} / {this.maxItemsAvailable} results
379 onClick={this.loadMore}
382 style={{width: '100%', margin: '10px'}}
383 disabled={working || items.length >= itemsAvailable}
397 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
398 this.props.onChangePage(page);
401 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
402 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
406 this.props.onLoadMore(this.props.page + 1);
409 renderContextMenuTrigger = (item: T) => (
419 className={this.props.classes.moreOptionsButton}
421 event.stopPropagation()
422 this.props.onContextMenu(event, item)
431 contextMenuColumn: DataColumn<any, any> = {
435 filters: createTree(),
436 key: "context-actions",
437 render: this.renderContextMenuTrigger,