1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from "react";
6 import { CustomStyleRulesCallback } from 'common/custom-theme';
16 } from "@mui/material";
17 import { WithStyles } from '@mui/styles';
18 import withStyles from '@mui/styles/withStyles';
19 import { ColumnSelector } from "components/column-selector/column-selector";
20 import { DataColumns } from "components/data-table/data-column";
21 import { DataTable, DataTableFetchMode } from "components/data-table/data-table";
22 import { DataColumn } from "components/data-table/data-column";
23 import { SearchInput } from "components/search-input/search-input";
24 import { ArvadosTheme } from "common/custom-theme";
25 import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
26 import { TCheckedList } from "components/data-table/data-table";
27 import { createTree } from "models/tree";
28 import { DataTableFilters } from "components/data-table-filters/data-table-filters";
29 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
30 import { PaperProps } from "@mui/material/Paper";
31 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
32 import classNames from "classnames";
33 import { InlinePulser } from "components/loading/inline-pulser";
54 | 'progressWrapperNoTitle';
56 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
59 justifyContent: "space-between",
67 marginBottom: "-20px",
76 paddingRight: theme.spacing(1),
97 marginBottom: '-0.5rem',
101 display: "inline-block",
102 paddingLeft: theme.spacing(2),
103 paddingTop: theme.spacing(2),
105 paddingRight: "10px",
108 display: "inline-block",
109 paddingLeft: theme.spacing(2),
110 paddingTop: theme.spacing(2),
113 paddingRight: "10px",
120 progressWrapperNoTitle: {
132 flexBasis: "initial",
142 color: theme.palette.grey["600"],
146 interface DataExplorerDataProps<T> {
147 fetchMode: DataTableFetchMode;
149 itemsAvailable: number;
150 loadingItemsAvailable: boolean;
151 columns: DataColumns<T, any>;
152 searchLabel?: string;
155 rowsPerPageOptions: number[];
157 contextMenuColumn: boolean;
158 defaultViewIcon?: IconType;
159 defaultViewMessages?: string[];
161 hideColumnSelector?: boolean;
162 paperProps?: PaperProps;
163 actions?: React.ReactNode;
164 hideSearchInput?: boolean;
165 title?: React.ReactNode;
166 progressBar?: React.ReactNode;
168 currentRouteUuid: string;
169 selectedResourceUuid: string;
170 elementPath?: string;
171 isMSToolbarVisible: boolean;
172 checkedList: TCheckedList;
174 searchBarValue: string;
175 paperClassName?: string;
176 forceMultiSelectMode?: boolean;
179 interface DataExplorerActionProps<T> {
180 onSetColumns: (columns: DataColumns<T, any>) => void;
181 onSearch: (value: string) => void;
182 onRowClick: (item: T) => void;
183 onRowDoubleClick: (item: T) => void;
184 onColumnToggle: (column: DataColumn<T, any>) => void;
185 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
186 onSortToggle: (column: DataColumn<T, any>) => void;
187 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
188 onPageChange: (page: number) => void;
189 onChangeRowsPerPage: (rowsPerPage: number) => void;
190 onLoadMore: (page: number) => void;
191 extractKey?: (item: T) => React.Key;
192 toggleMSToolbar: (isVisible: boolean) => void;
193 setCheckedListOnStore: (checkedList: TCheckedList) => void;
194 setSelectedUuid: (uuid: string) => void;
195 usesDetailsCard: (uuid: string) => boolean;
198 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
200 export const DataExplorer = withStyles(styles)(
201 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
206 multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
207 maxItemsAvailable = 0;
209 componentDidMount() {
210 if (this.props.onSetColumns) {
211 this.props.onSetColumns(this.props.columns);
215 componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
216 const { selectedResourceUuid, currentRouteUuid, path, usesDetailsCard } = this.props;
217 if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
219 hideToolbar: usesDetailsCard(path || '') ? selectedResourceUuid === this.props.currentRouteUuid : false,
222 if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
223 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
225 if (this.props.searchBarValue !== prevProps.searchBarValue) {
226 this.maxItemsAvailable = 0;
245 loadingItemsAvailable,
257 selectedResourceUuid,
267 setCheckedListOnStore,
271 forceMultiSelectMode,
275 className={classNames(classes.root, paperClassName)}
278 data-cy={this.props["data-cy"]}
284 className={classes.container}
286 <div data-cy="title-wrapper" className={classes.titleWrapper}>
291 className={!!progressBar ? classes.subProcessTitle : classes.title}
297 <div className={classNames({
298 [classes.progressWrapper]: true,
299 [classes.progressWrapperNoTitle]: !title,
300 })}>{progressBar}</div>
302 {this.multiSelectToolbarInTitle && !this.state.hideToolbar && <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />}
303 {(!hideColumnSelector || !hideSearchInput || !!actions) && (
305 className={classes.headerMenu}
309 <Toolbar className={classes.toolbar}>
310 <Grid container justifyContent="space-between" wrap="nowrap" alignItems="center">
311 {!hideSearchInput && (
312 <div className={classes.searchBox}>
313 {!hideSearchInput && (
324 {!hideColumnSelector && (
327 onColumnToggle={onColumnToggle}
331 {doUnMaximizePanel && panelMaximized && (
333 title={`Unmaximize ${panelName || "panel"}`}
336 <IconButton onClick={doUnMaximizePanel} size="large">
341 {doMaximizePanel && !panelMaximized && (
343 title={`Maximize ${panelName || "panel"}`}
346 <IconButton onClick={doMaximizePanel} size="large">
353 title={`Close ${panelName || "panel"}`}
356 <IconButton disabled={panelMaximized} onClick={doHidePanel} size="large">
365 {!this.multiSelectToolbarInTitle &&
366 <div className={classes.subToolbarWrapper}>
367 {!this.state.hideToolbar && <MultiselectToolbar
368 forceMultiSelectMode={forceMultiSelectMode}
374 className={classes.dataTable}
377 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
379 onRowClick={(_, item: T) => onRowClick(item)}
380 onContextMenu={onContextMenu}
381 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
382 onFiltersChange={onFiltersChange}
383 onSortToggle={onSortToggle}
384 extractKey={extractKey}
385 defaultViewIcon={defaultViewIcon}
386 defaultViewMessages={defaultViewMessages}
388 toggleMSToolbar={toggleMSToolbar}
389 setCheckedListOnStore={setCheckedListOnStore}
390 checkedList={checkedList}
391 selectedResourceUuid={selectedResourceUuid}
392 setSelectedUuid={this.props.setSelectedUuid}
393 currentRouteUuid={this.props.currentRouteUuid}
395 isNotFound={this.props.isNotFound}
402 <Toolbar className={classes.footer}>
405 <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
409 container={!elementPath}
410 justifyContent="flex-end"
412 {fetchMode === DataTableFetchMode.PAGINATED ? (
414 data-cy="table-pagination"
415 count={itemsAvailable}
416 rowsPerPage={rowsPerPage}
417 rowsPerPageOptions={rowsPerPageOptions}
418 page={this.props.page}
419 onPageChange={this.changePage}
420 onRowsPerPageChange={this.changeRowsPerPage}
421 labelDisplayedRows={renderPaginationLabel(loadingItemsAvailable)}
422 nextIconButtonProps={getPaginiationButtonProps(itemsAvailable, loadingItemsAvailable)}
425 root: classes.paginationRoot,
426 selectLabel: classes.paginationLabel,
427 displayedRows: classes.paginationLabel,
431 <Grid className={classes.loadMoreContainer}>
432 <Typography className={classes.numResults}>
433 Showing {items.length} / {this.maxItemsAvailable} results
437 onClick={this.loadMore}
440 style={{width: '100%', margin: '10px'}}
441 disabled={working || items.length >= itemsAvailable}
455 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
456 this.props.onPageChange(page);
459 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
460 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
464 this.props.onLoadMore(this.props.page + 1);
467 renderContextMenuTrigger = (item: T) => (
470 justifyContent="center"
477 className={this.props.classes.moreOptionsButton}
479 event.stopPropagation()
480 this.props.onContextMenu(event, item)
489 contextMenuColumn: DataColumn<any, any> = {
493 filters: createTree(),
494 key: "context-actions",
495 render: this.renderContextMenuTrigger,
500 const renderPaginationLabel = (loading: boolean) => ({ from, to, count }) => (
503 : <>{from}-{to} of {count}</>
506 const getPaginiationButtonProps = (itemsAvailable: number, loading: boolean) => (
508 ? { disabled: false } // Always allow paging while loading total
511 : { disabled: true } // Disable next button on empty lists since that's not default behavior