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 { DataTable, DataColumns, DataTableFetchMode } from "components/data-table/data-table";
21 import { DataColumn } from "components/data-table/data-column";
22 import { SearchInput } from "components/search-input/search-input";
23 import { ArvadosTheme } from "common/custom-theme";
24 import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
25 import { TCheckedList } from "components/data-table/data-table";
26 import { createTree } from "models/tree";
27 import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
28 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
29 import { PaperProps } from "@mui/material/Paper";
30 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
31 import classNames from "classnames";
32 import { InlinePulser } from "components/loading/inline-pulser";
53 | 'progressWrapperNoTitle';
55 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
58 justifyContent: "space-between",
66 marginBottom: "-20px",
75 paddingRight: theme.spacing(1),
96 marginBottom: '-0.5rem',
100 display: "inline-block",
101 paddingLeft: theme.spacing(2),
102 paddingTop: theme.spacing(2),
104 paddingRight: "10px",
107 display: "inline-block",
108 paddingLeft: theme.spacing(2),
109 paddingTop: theme.spacing(2),
112 paddingRight: "10px",
119 progressWrapperNoTitle: {
131 flexBasis: "initial",
141 color: theme.palette.grey["600"],
145 interface DataExplorerDataProps<T> {
146 fetchMode: DataTableFetchMode;
148 itemsAvailable: number;
149 loadingItemsAvailable: boolean;
150 columns: DataColumns<T, any>;
151 searchLabel?: string;
154 rowsPerPageOptions: number[];
156 contextMenuColumn: boolean;
157 defaultViewIcon?: IconType;
158 defaultViewMessages?: string[];
160 hideColumnSelector?: boolean;
161 paperProps?: PaperProps;
162 actions?: React.ReactNode;
163 hideSearchInput?: boolean;
164 title?: React.ReactNode;
165 progressBar?: React.ReactNode;
167 currentRouteUuid: string;
168 selectedResourceUuid: string;
169 elementPath?: string;
170 isMSToolbarVisible: boolean;
171 checkedList: TCheckedList;
173 searchBarValue: string;
174 paperClassName?: string;
175 forceMultiSelectMode?: boolean;
178 interface DataExplorerActionProps<T> {
179 onSetColumns: (columns: DataColumns<T, any>) => void;
180 onSearch: (value: string) => void;
181 onRowClick: (item: T) => void;
182 onRowDoubleClick: (item: T) => void;
183 onColumnToggle: (column: DataColumn<T, any>) => void;
184 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
185 onSortToggle: (column: DataColumn<T, any>) => void;
186 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
187 onPageChange: (page: number) => void;
188 onChangeRowsPerPage: (rowsPerPage: number) => void;
189 onLoadMore: (page: number) => void;
190 extractKey?: (item: T) => React.Key;
191 toggleMSToolbar: (isVisible: boolean) => void;
192 setCheckedListOnStore: (checkedList: TCheckedList) => void;
193 setSelectedUuid: (uuid: string) => void;
194 usesDetailsCard: (uuid: string) => boolean;
197 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
199 export const DataExplorer = withStyles(styles)(
200 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
205 multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
206 maxItemsAvailable = 0;
208 componentDidMount() {
209 if (this.props.onSetColumns) {
210 this.props.onSetColumns(this.props.columns);
214 componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
215 const { selectedResourceUuid, currentRouteUuid, path, usesDetailsCard } = this.props;
216 if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
218 hideToolbar: usesDetailsCard(path || '') ? selectedResourceUuid === this.props.currentRouteUuid : false,
221 if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
222 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
224 if (this.props.searchBarValue !== prevProps.searchBarValue) {
225 this.maxItemsAvailable = 0;
244 loadingItemsAvailable,
256 selectedResourceUuid,
266 setCheckedListOnStore,
270 forceMultiSelectMode,
274 className={classNames(classes.root, paperClassName)}
277 data-cy={this.props["data-cy"]}
283 className={classes.container}
285 <div data-cy="title-wrapper" className={classes.titleWrapper}>
290 className={!!progressBar ? classes.subProcessTitle : classes.title}
296 <div className={classNames({
297 [classes.progressWrapper]: true,
298 [classes.progressWrapperNoTitle]: !title,
299 })}>{progressBar}</div>
301 {this.multiSelectToolbarInTitle && !this.state.hideToolbar && <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />}
302 {(!hideColumnSelector || !hideSearchInput || !!actions) && (
304 className={classes.headerMenu}
308 <Toolbar className={classes.toolbar}>
309 <Grid container justifyContent="space-between" wrap="nowrap" alignItems="center">
310 {!hideSearchInput && (
311 <div className={classes.searchBox}>
312 {!hideSearchInput && (
323 {!hideColumnSelector && (
326 onColumnToggle={onColumnToggle}
330 {doUnMaximizePanel && panelMaximized && (
332 title={`Unmaximize ${panelName || "panel"}`}
335 <IconButton onClick={doUnMaximizePanel} size="large">
340 {doMaximizePanel && !panelMaximized && (
342 title={`Maximize ${panelName || "panel"}`}
345 <IconButton onClick={doMaximizePanel} size="large">
352 title={`Close ${panelName || "panel"}`}
355 <IconButton disabled={panelMaximized} onClick={doHidePanel} size="large">
364 {!this.multiSelectToolbarInTitle &&
365 <div className={classes.subToolbarWrapper}>
366 {!this.state.hideToolbar && <MultiselectToolbar
367 forceMultiSelectMode={forceMultiSelectMode}
373 className={classes.dataTable}
376 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
378 onRowClick={(_, item: T) => onRowClick(item)}
379 onContextMenu={onContextMenu}
380 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
381 onFiltersChange={onFiltersChange}
382 onSortToggle={onSortToggle}
383 extractKey={extractKey}
384 defaultViewIcon={defaultViewIcon}
385 defaultViewMessages={defaultViewMessages}
387 toggleMSToolbar={toggleMSToolbar}
388 setCheckedListOnStore={setCheckedListOnStore}
389 checkedList={checkedList}
390 selectedResourceUuid={selectedResourceUuid}
391 setSelectedUuid={this.props.setSelectedUuid}
392 currentRouteUuid={this.props.currentRouteUuid}
394 isNotFound={this.props.isNotFound}
401 <Toolbar className={classes.footer}>
404 <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
408 container={!elementPath}
409 justifyContent="flex-end"
411 {fetchMode === DataTableFetchMode.PAGINATED ? (
413 data-cy="table-pagination"
414 count={itemsAvailable}
415 rowsPerPage={rowsPerPage}
416 rowsPerPageOptions={rowsPerPageOptions}
417 page={this.props.page}
418 onPageChange={this.changePage}
419 onRowsPerPageChange={this.changeRowsPerPage}
420 labelDisplayedRows={renderPaginationLabel(loadingItemsAvailable)}
421 nextIconButtonProps={getPaginiationButtonProps(itemsAvailable, loadingItemsAvailable)}
424 root: classes.paginationRoot,
425 selectLabel: classes.paginationLabel,
426 displayedRows: classes.paginationLabel,
430 <Grid className={classes.loadMoreContainer}>
431 <Typography className={classes.numResults}>
432 Showing {items.length} / {this.maxItemsAvailable} results
436 onClick={this.loadMore}
439 style={{width: '100%', margin: '10px'}}
440 disabled={working || items.length >= itemsAvailable}
454 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
455 this.props.onPageChange(page);
458 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
459 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
463 this.props.onLoadMore(this.props.page + 1);
466 renderContextMenuTrigger = (item: T) => (
469 justifyContent="center"
476 className={this.props.classes.moreOptionsButton}
478 event.stopPropagation()
479 this.props.onContextMenu(event, item)
488 contextMenuColumn: DataColumn<any, any> = {
492 filters: createTree(),
493 key: "context-actions",
494 render: this.renderContextMenuTrigger,
499 const renderPaginationLabel = (loading: boolean) => ({ from, to, count }) => (
502 : <>{from}-{to} of {count}</>
505 const getPaginiationButtonProps = (itemsAvailable: number, loading: boolean) => (
507 ? { disabled: false } // Always allow paging while loading total
510 : { disabled: true } // Disable next button on empty lists since that's not default behavior