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";
37 | 'searchResultsTitleWrapper'
53 | 'runsToolbarWrapper'
54 | 'searchResultsToolbar'
56 | 'progressWrapperNoTitle';
58 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
61 justifyContent: "space-between",
65 searchResultsTitleWrapper: {
67 justifyContent: "space-between",
82 searchResultsToolbar: {
91 paddingRight: theme.spacing(1),
112 marginBottom: '-0.5rem',
116 display: "inline-block",
117 paddingLeft: theme.spacing(2),
118 paddingTop: theme.spacing(2),
121 paddingRight: "10px",
126 paddingRight: "20px",
128 progressWrapperNoTitle: {
140 flexBasis: "initial",
150 color: theme.palette.grey["600"],
154 interface DataExplorerDataProps<T> {
155 fetchMode: DataTableFetchMode;
157 itemsAvailable: number;
158 loadingItemsAvailable: boolean;
159 columns: DataColumns<T, any>;
160 searchLabel?: string;
163 rowsPerPageOptions: number[];
165 contextMenuColumn: boolean;
166 defaultViewIcon?: IconType;
167 defaultViewMessages?: string[];
169 hideColumnSelector?: boolean;
170 paperProps?: PaperProps;
171 actions?: React.ReactNode;
172 hideSearchInput?: boolean;
173 title?: React.ReactNode;
174 progressBar?: React.ReactNode;
176 currentRouteUuid: string;
177 selectedResourceUuid: string;
178 elementPath?: string;
179 isMSToolbarVisible: boolean;
180 checkedList: TCheckedList;
182 searchBarValue: string;
183 paperClassName?: string;
184 forceMultiSelectMode?: boolean;
185 detailsPanelResourceUuid: string;
188 interface DataExplorerActionProps<T> {
189 onSetColumns: (columns: DataColumns<T, any>) => void;
190 onSearch: (value: string) => void;
191 onRowClick: (item: T) => void;
192 onRowDoubleClick: (item: T) => void;
193 onColumnToggle: (column: DataColumn<T, any>) => void;
194 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
195 onSortToggle: (column: DataColumn<T, any>) => void;
196 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
197 onPageChange: (page: number) => void;
198 onChangeRowsPerPage: (rowsPerPage: number) => void;
199 onLoadMore: (page: number) => void;
200 extractKey?: (item: T) => React.Key;
201 toggleMSToolbar: (isVisible: boolean) => void;
202 setCheckedListOnStore: (checkedList: TCheckedList) => void;
203 setSelectedUuid: (uuid: string) => void;
204 usesDetailsCard: (uuid: string) => boolean;
205 loadDetailsPanel: (uuid: string) => void;
208 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
210 export const DataExplorer = withStyles(styles)(
211 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
214 isSearchResults: false,
217 multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
218 maxItemsAvailable = 0;
220 componentDidMount() {
221 if (this.props.onSetColumns) {
222 this.props.onSetColumns(this.props.columns);
224 this.setState({ isSearchResults: this.props.path?.includes("search-results") ? true : false })
227 componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
228 const { selectedResourceUuid, currentRouteUuid, path, usesDetailsCard } = this.props;
229 if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
231 hideToolbar: usesDetailsCard(path || '') ? selectedResourceUuid === this.props.currentRouteUuid : false,
234 if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
235 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
237 if (this.props.searchBarValue !== prevProps.searchBarValue) {
238 this.maxItemsAvailable = 0;
240 if (this.props.path !== prevProps.path) {
241 this.setState({ isSearchResults: this.props.path?.includes("search-results") ? true : false })
260 loadingItemsAvailable,
272 selectedResourceUuid,
282 setCheckedListOnStore,
286 forceMultiSelectMode,
287 detailsPanelResourceUuid,
292 className={classNames(classes.root, paperClassName)}
295 data-cy={this.props["data-cy"]}
297 {title && this.state.isSearchResults && (
301 className={classes.title}
311 className={classes.container}
313 {!!progressBar && !title &&
314 <div className={classNames({
315 [classes.progressWrapper]: true,
316 [classes.progressWrapperNoTitle]: !title,
317 })}>{progressBar}</div>
319 <div data-cy="title-wrapper" className={classNames(this.state.isSearchResults ? classes.searchResultsTitleWrapper : classes.titleWrapper)}>
320 {title && !this.state.isSearchResults && (
324 className={classes.title}
330 {!this.state.hideToolbar && (this.multiSelectToolbarInTitle
331 ? <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />
332 : <MultiselectToolbar
333 forceMultiSelectMode={forceMultiSelectMode}
334 injectedStyles={classNames(panelName === 'Subprocesses' ? classes.subToolbarWrapper : panelName === 'Runs' ? classes.runsToolbarWrapper : '')}/>)
336 {(!hideColumnSelector || !hideSearchInput || !!actions) && (
338 className={classes.headerMenu}
342 <Toolbar className={classes.toolbar}>
343 <Grid container justifyContent="space-between" wrap="nowrap" alignItems="center">
344 {!hideSearchInput && (
345 <div className={classes.searchBox}>
346 {!hideSearchInput && (
357 {!hideColumnSelector && (
360 onColumnToggle={onColumnToggle}
364 {doUnMaximizePanel && panelMaximized && (
366 title={`Unmaximize ${panelName || "panel"}`}
369 <IconButton onClick={doUnMaximizePanel} size="large">
374 {doMaximizePanel && !panelMaximized && (
376 title={`Maximize ${panelName || "panel"}`}
379 <IconButton onClick={doMaximizePanel} size="large">
386 title={`Close ${panelName || "panel"}`}
389 <IconButton disabled={panelMaximized} onClick={doHidePanel} size="large">
401 className={classes.dataTable}
403 {!!progressBar && !!title &&
404 <div className={classNames({
405 [classes.progressWrapper]: true,
406 [classes.progressWrapperNoTitle]: !title,
407 })}>{progressBar}</div>
410 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
412 onRowClick={(_, item: T) => onRowClick(item)}
413 onContextMenu={onContextMenu}
414 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
415 onFiltersChange={onFiltersChange}
416 onSortToggle={onSortToggle}
417 extractKey={extractKey}
418 defaultViewIcon={defaultViewIcon}
419 defaultViewMessages={defaultViewMessages}
421 toggleMSToolbar={toggleMSToolbar}
422 setCheckedListOnStore={setCheckedListOnStore}
423 checkedList={checkedList}
424 selectedResourceUuid={selectedResourceUuid}
425 setSelectedUuid={this.props.setSelectedUuid}
426 currentRouteUuid={this.props.currentRouteUuid}
428 isNotFound={this.props.isNotFound}
429 detailsPanelResourceUuid={detailsPanelResourceUuid}
430 loadDetailsPanel={loadDetailsPanel}
437 <Toolbar className={classes.footer}>
440 <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
444 container={!elementPath}
445 justifyContent="flex-end"
447 {fetchMode === DataTableFetchMode.PAGINATED ? (
449 data-cy="table-pagination"
450 count={itemsAvailable}
451 rowsPerPage={rowsPerPage}
452 rowsPerPageOptions={rowsPerPageOptions}
453 page={this.props.page}
454 onPageChange={this.changePage}
455 onRowsPerPageChange={this.changeRowsPerPage}
456 labelDisplayedRows={renderPaginationLabel(loadingItemsAvailable)}
457 nextIconButtonProps={getPaginiationButtonProps(itemsAvailable, loadingItemsAvailable)}
460 root: classes.paginationRoot,
461 selectLabel: classes.paginationLabel,
462 displayedRows: classes.paginationLabel,
466 <Grid className={classes.loadMoreContainer}>
467 <Typography className={classes.numResults}>
468 Showing {items.length} / {this.maxItemsAvailable} results
472 onClick={this.loadMore}
475 style={{width: '100%', margin: '10px'}}
476 disabled={working || items.length >= itemsAvailable}
490 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
491 this.props.onPageChange(page);
494 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
495 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
499 this.props.onLoadMore(this.props.page + 1);
502 renderContextMenuTrigger = (item: T) => (
505 justifyContent="center"
512 className={this.props.classes.moreOptionsButton}
514 event.stopPropagation()
515 this.props.onContextMenu(event, item)
524 contextMenuColumn: DataColumn<any, any> = {
528 filters: createTree(),
529 key: "context-actions",
530 render: this.renderContextMenuTrigger,
535 const renderPaginationLabel = (loading: boolean) => ({ from, to, count }) => (
538 : <>{from}-{to} of {count}</>
541 const getPaginiationButtonProps = (itemsAvailable: number, loading: boolean) => (
543 ? { disabled: false } // Always allow paging while loading total
546 : { disabled: true } // Disable next button on empty lists since that's not default behavior