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";
35 | 'subpanelToolbarStyles'
51 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
54 justifyContent: "space-between",
59 subpanelToolbarStyles: {
67 paddingRight: theme.spacing(1),
88 marginBottom: '-0.5rem',
92 display: "inline-block",
93 paddingLeft: theme.spacing(2),
94 paddingTop: theme.spacing(2),
99 display: "inline-block",
100 paddingLeft: theme.spacing(2),
101 paddingTop: theme.spacing(2),
104 paddingRight: "10px",
115 flexBasis: "initial",
125 color: theme.palette.grey["600"],
129 interface DataExplorerDataProps<T> {
130 fetchMode: DataTableFetchMode;
132 itemsAvailable: number;
133 columns: DataColumns<T, any>;
134 searchLabel?: string;
137 rowsPerPageOptions: number[];
139 contextMenuColumn: boolean;
140 defaultViewIcon?: IconType;
141 defaultViewMessages?: string[];
143 currentRoute?: string;
144 hideColumnSelector?: boolean;
145 paperProps?: PaperProps;
146 actions?: React.ReactNode;
147 hideSearchInput?: boolean;
148 title?: React.ReactNode;
149 progressBar?: React.ReactNode;
151 currentRouteUuid: string;
152 selectedResourceUuid: string;
153 elementPath?: string;
154 isMSToolbarVisible: boolean;
155 checkedList: TCheckedList;
157 searchBarValue: string;
160 interface DataExplorerActionProps<T> {
161 onSetColumns: (columns: DataColumns<T, any>) => void;
162 onSearch: (value: string) => void;
163 onRowClick: (item: T) => void;
164 onRowDoubleClick: (item: T) => void;
165 onColumnToggle: (column: DataColumn<T, any>) => void;
166 onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
167 onSortToggle: (column: DataColumn<T, any>) => void;
168 onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
169 onPageChange: (page: number) => void;
170 onChangeRowsPerPage: (rowsPerPage: number) => void;
171 onLoadMore: (page: number) => void;
172 extractKey?: (item: T) => React.Key;
173 toggleMSToolbar: (isVisible: boolean) => void;
174 setCheckedListOnStore: (checkedList: TCheckedList) => void;
175 setSelectedUuid: (uuid: string) => void;
178 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
180 export const DataExplorer = withStyles(styles)(
181 class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
183 msToolbarInDetailsCard: true,
186 multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
187 maxItemsAvailable = 0;
189 componentDidMount() {
190 if (this.props.onSetColumns) {
191 this.props.onSetColumns(this.props.columns);
195 componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
196 const { selectedResourceUuid, currentRouteUuid } = this.props;
197 if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
199 msToolbarInDetailsCard: selectedResourceUuid === this.props.currentRouteUuid,
202 if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
203 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
205 if (this.props.searchBarValue !== prevProps.searchBarValue) {
206 this.maxItemsAvailable = 0;
236 selectedResourceUuid,
247 setCheckedListOnStore,
253 className={classes.root}
256 data-cy={this.props["data-cy"]}
262 className={classes.container}
264 <div data-cy="title-wrapper" className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
269 className={!!progressBar ? classes.subProcessTitle : classes.title}
274 {!!progressBar && progressBar}
275 {this.multiSelectToolbarInTitle && !this.state.msToolbarInDetailsCard && <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />}
276 {(!hideColumnSelector || !hideSearchInput || !!actions) && (
278 className={classes.headerMenu}
282 <Toolbar className={classes.toolbar}>
283 <Grid container justifyContent="space-between" wrap="nowrap" alignItems="center">
284 {!hideSearchInput && (
285 <div className={classes.searchBox}>
286 {!hideSearchInput && (
297 {!hideColumnSelector && (
300 onColumnToggle={onColumnToggle}
304 {doUnMaximizePanel && panelMaximized && (
306 title={`Unmaximize ${panelName || "panel"}`}
309 <IconButton onClick={doUnMaximizePanel} size="large">
314 {doMaximizePanel && !panelMaximized && (
316 title={`Maximize ${panelName || "panel"}`}
319 <IconButton onClick={doMaximizePanel} size="large">
326 title={`Close ${panelName || "panel"}`}
329 <IconButton disabled={panelMaximized} onClick={doHidePanel} size="large">
338 {!this.multiSelectToolbarInTitle && <MultiselectToolbar isSubPanel={true} injectedStyles={classes.subpanelToolbarStyles}/>}
341 className={classes.dataTable}
342 style={currentRoute?.includes('search-results') || !!progressBar ? {marginTop: '-10px'} : {}}
345 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
347 onRowClick={(_, item: T) => onRowClick(item)}
348 onContextMenu={onContextMenu}
349 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
350 onFiltersChange={onFiltersChange}
351 onSortToggle={onSortToggle}
352 extractKey={extractKey}
353 defaultViewIcon={defaultViewIcon}
354 defaultViewMessages={defaultViewMessages}
355 currentRoute={paperKey}
356 toggleMSToolbar={toggleMSToolbar}
357 setCheckedListOnStore={setCheckedListOnStore}
358 checkedList={checkedList}
359 selectedResourceUuid={selectedResourceUuid}
360 setSelectedUuid={this.props.setSelectedUuid}
361 currentRouteUuid={this.props.currentRouteUuid}
363 isNotFound={this.props.isNotFound}
370 <Toolbar className={classes.footer}>
373 <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
377 container={!elementPath}
378 justifyContent="flex-end"
380 {fetchMode === DataTableFetchMode.PAGINATED ? (
382 data-cy="table-pagination"
383 count={itemsAvailable}
384 rowsPerPage={rowsPerPage}
385 rowsPerPageOptions={rowsPerPageOptions}
386 page={this.props.page}
387 onPageChange={this.changePage}
388 onRowsPerPageChange={this.changeRowsPerPage}
389 // Disable next button on empty lists since that's not default behavior
390 nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
393 root: classes.paginationRoot,
394 selectLabel: classes.paginationLabel,
395 displayedRows: classes.paginationLabel,
399 <Grid className={classes.loadMoreContainer}>
400 <Typography className={classes.numResults}>
401 Showing {items.length} / {this.maxItemsAvailable} results
405 onClick={this.loadMore}
408 style={{width: '100%', margin: '10px'}}
409 disabled={working || items.length >= itemsAvailable}
423 changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
424 this.props.onPageChange(page);
427 changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
428 this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
432 this.props.onLoadMore(this.props.page + 1);
435 renderContextMenuTrigger = (item: T) => (
438 justifyContent="center"
445 className={this.props.classes.moreOptionsButton}
447 event.stopPropagation()
448 this.props.onContextMenu(event, item)
457 contextMenuColumn: DataColumn<any, any> = {
461 filters: createTree(),
462 key: "context-actions",
463 render: this.renderContextMenuTrigger,