Merge branch '21128-toolbar-context-menu'
[arvados-workbench2.git] / src / components / data-explorer / data-explorer.tsx
index febd71c25216be5888e8230279d4fd29e8fe2b41..27e46d584962c8d3e1cb1ca536b21ab1b4577ecf 100644 (file)
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import * as React from 'react';
-import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from '@material-ui/core';
+import React from "react";
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from "@material-ui/core";
 import { ColumnSelector } from "components/column-selector/column-selector";
 import { DataTable, DataColumns, DataTableFetchMode } from "components/data-table/data-table";
 import { DataColumn } from "components/data-table/data-column";
-import { SearchInput } from 'components/search-input/search-input';
+import { SearchInput } from "components/search-input/search-input";
 import { ArvadosTheme } from "common/custom-theme";
-import { createTree } from 'models/tree';
-import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
-import { MoreOptionsIcon } from 'components/icon/icon';
-import { PaperProps } from '@material-ui/core/Paper';
+import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
+import { TCheckedList } from "components/data-table/data-table";
+import { createTree } from "models/tree";
+import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
+import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
+import { PaperProps } from "@material-ui/core/Paper";
+import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
 
-type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title';
+type CssRules = "titleWrapper" | "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | 'subProcessTitle' | "dataTable" | "container";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    titleWrapper: {
+        display: "flex",
+        justifyContent: "space-between",
+    },
     searchBox: {
-        paddingBottom: theme.spacing.unit * 2
+        paddingBottom: 0,
     },
     toolbar: {
-        paddingTop: theme.spacing.unit * 2
-    },
-    toolbarUnderTitle: {
-        paddingTop: 0
+        paddingTop: 0,
+        paddingRight: theme.spacing.unit,
+        paddingLeft: "10px",
     },
     footer: {
-        overflow: 'auto'
+        overflow: "auto",
     },
     root: {
-        height: '100%'
+        height: "100%",
     },
     moreOptionsButton: {
-        padding: 0
+        padding: 0,
     },
     title: {
-        paddingLeft: theme.spacing.unit * 3,
-        paddingTop: theme.spacing.unit * 3,
-        fontSize: '18px'
-    }
+        display: "inline-block",
+        paddingLeft: theme.spacing.unit * 2,
+        paddingTop: theme.spacing.unit * 2,
+        fontSize: "18px",
+        paddingRight: "10px",
+    },
+    subProcessTitle: {
+        display: "inline-block",
+        paddingLeft: theme.spacing.unit * 2,
+        paddingTop: theme.spacing.unit * 2,
+        fontSize: "18px",
+        flexGrow: 0,
+        paddingRight: "10px",
+    },
+    dataTable: {
+        height: "100%",
+        overflow: "auto",
+    },
+    container: {
+        height: "100%",
+    },
+    headerMenu: {
+        marginLeft: "auto",
+        flexBasis: "initial",
+        flexGrow: 0,
+    },
 });
 
 interface DataExplorerDataProps<T> {
     fetchMode: DataTableFetchMode;
     items: T[];
     itemsAvailable: number;
-    columns: DataColumns<T>;
+    columns: DataColumns<T, any>;
     searchLabel?: string;
     searchValue: string;
     rowsPerPage: number;
     rowsPerPageOptions: number[];
     page: number;
     contextMenuColumn: boolean;
-    dataTableDefaultView?: React.ReactNode;
+    defaultViewIcon?: IconType;
+    defaultViewMessages?: string[];
     working?: boolean;
+    currentRefresh?: string;
+    currentRoute?: string;
     hideColumnSelector?: boolean;
     paperProps?: PaperProps;
     actions?: React.ReactNode;
     hideSearchInput?: boolean;
     title?: React.ReactNode;
+    progressBar?: React.ReactNode;
     paperKey?: string;
     currentItemUuid: string;
+    elementPath?: string;
+    isMSToolbarVisible: boolean;
+    checkedList: TCheckedList;
 }
 
 interface DataExplorerActionProps<T> {
-    onSetColumns: (columns: DataColumns<T>) => void;
+    onSetColumns: (columns: DataColumns<T, any>) => void;
     onSearch: (value: string) => void;
     onRowClick: (item: T) => void;
     onRowDoubleClick: (item: T) => void;
-    onColumnToggle: (column: DataColumn<T>) => void;
+    onColumnToggle: (column: DataColumn<T, any>) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
-    onSortToggle: (column: DataColumn<T>) => void;
-    onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
+    onSortToggle: (column: DataColumn<T, any>) => void;
+    onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
     onChangePage: (page: number) => void;
     onChangeRowsPerPage: (rowsPerPage: number) => void;
     onLoadMore: (page: number) => void;
     extractKey?: (item: T) => React.Key;
+    toggleMSToolbar: (isVisible: boolean) => void;
+    setCheckedListOnStore: (checkedList: TCheckedList) => void;
 }
 
-type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules>;
+type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
 
 export const DataExplorer = withStyles(styles)(
     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
+        state = {
+            showLoading: false,
+            prevRefresh: "",
+            prevRoute: "",
+        };
+
+        multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
+
+        componentDidUpdate(prevProps: DataExplorerProps<T>) {
+            const currentRefresh = this.props.currentRefresh || "";
+            const currentRoute = this.props.currentRoute || "";
+
+            if (currentRoute !== this.state.prevRoute) {
+                // Component already mounted, but the user comes from a route change,
+                // like browsing through a project hierarchy.
+                this.setState({
+                    showLoading: this.props.working,
+                    prevRoute: currentRoute,
+                });
+            }
+
+            if (currentRefresh !== this.state.prevRefresh) {
+                // Component already mounted, but the user just clicked the
+                // refresh button.
+                this.setState({
+                    showLoading: this.props.working,
+                    prevRefresh: currentRefresh,
+                });
+            }
+            if (this.state.showLoading && !this.props.working) {
+                this.setState({
+                    showLoading: false,
+                });
+            }
+        }
+
         componentDidMount() {
             if (this.props.onSetColumns) {
                 this.props.onSetColumns(this.props.columns);
             }
+            // Component just mounted, so we need to show the loading indicator.
+            this.setState({
+                showLoading: this.props.working,
+                prevRefresh: this.props.currentRefresh || "",
+                prevRoute: this.props.currentRoute || "",
+            });
         }
+
         render() {
             const {
-                columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
-                rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
-                items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
-                dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
-                paperKey, fetchMode, currentItemUuid, title
+                columns,
+                onContextMenu,
+                onFiltersChange,
+                onSortToggle,
+                extractKey,
+                rowsPerPage,
+                rowsPerPageOptions,
+                onColumnToggle,
+                searchLabel,
+                searchValue,
+                onSearch,
+                items,
+                itemsAvailable,
+                onRowClick,
+                onRowDoubleClick,
+                classes,
+                defaultViewIcon,
+                defaultViewMessages,
+                hideColumnSelector,
+                actions,
+                paperProps,
+                hideSearchInput,
+                paperKey,
+                fetchMode,
+                currentItemUuid,
+                currentRoute,
+                title,
+                progressBar,
+                doHidePanel,
+                doMaximizePanel,
+                doUnMaximizePanel,
+                panelName,
+                panelMaximized,
+                elementPath,
+                toggleMSToolbar,
+                setCheckedListOnStore,
+                checkedList,
             } = this.props;
-            return <Paper className={classes.root} {...paperProps} key={paperKey}>
-                {title && <div className={classes.title}>{title}</div>}
-                {(!hideColumnSelector || !hideSearchInput) && <Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
-                    <Grid container justify="space-between" wrap="nowrap" alignItems="center">
-                        <div className={classes.searchBox}>
-                            {!hideSearchInput && <SearchInput
-                                label={searchLabel}
-                                value={searchValue}
-                                onSearch={onSearch} />}
+            return (
+                <Paper
+                    className={classes.root}
+                    {...paperProps}
+                    key={paperKey}
+                    data-cy={this.props["data-cy"]}
+                >
+                    <Grid
+                        container
+                        direction="column"
+                        wrap="nowrap"
+                        className={classes.container}
+                    >
+                        <div className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
+                            {title && (
+                                <Grid
+                                    item
+                                    xs
+                                    className={!!progressBar ? classes.subProcessTitle : classes.title}
+                                >
+                                    {title}
+                                </Grid>
+                            )}
+                            {!!progressBar && progressBar}
+                            {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
+                            {(!hideColumnSelector || !hideSearchInput || !!actions) && (
+                                <Grid
+                                    className={classes.headerMenu}
+                                    item
+                                    xs
+                                >
+                                    <Toolbar className={classes.toolbar}>
+                                        <Grid container justify="space-between" wrap="nowrap" alignItems="center">
+                                            {!hideSearchInput && (
+                                                <div className={classes.searchBox}>
+                                                    {!hideSearchInput && (
+                                                        <SearchInput
+                                                            label={searchLabel}
+                                                            value={searchValue}
+                                                            selfClearProp={""}
+                                                            onSearch={onSearch}
+                                                        />
+                                                    )}
+                                                </div>
+                                            )}
+                                            {actions}
+                                            {!hideColumnSelector && (
+                                                <ColumnSelector
+                                                    columns={columns}
+                                                    onColumnToggle={onColumnToggle}
+                                                />
+                                            )}
+                                        </Grid>
+                                        {doUnMaximizePanel && panelMaximized && (
+                                            <Tooltip
+                                                title={`Unmaximize ${panelName || "panel"}`}
+                                                disableFocusListener
+                                            >
+                                                <IconButton onClick={doUnMaximizePanel}>
+                                                    <UnMaximizeIcon />
+                                                </IconButton>
+                                            </Tooltip>
+                                        )}
+                                        {doMaximizePanel && !panelMaximized && (
+                                            <Tooltip
+                                                title={`Maximize ${panelName || "panel"}`}
+                                                disableFocusListener
+                                            >
+                                                <IconButton onClick={doMaximizePanel}>
+                                                    <MaximizeIcon />
+                                                </IconButton>
+                                            </Tooltip>
+                                        )}
+                                        {doHidePanel && (
+                                            <Tooltip
+                                                title={`Close ${panelName || "panel"}`}
+                                                disableFocusListener
+                                            >
+                                                <IconButton
+                                                    disabled={panelMaximized}
+                                                    onClick={doHidePanel}
+                                                >
+                                                    <CloseIcon />
+                                                </IconButton>
+                                            </Tooltip>
+                                        )}
+                                    </Toolbar>
+                                </Grid>
+                            )}
                         </div>
-                        {actions}
-                        {!hideColumnSelector && <ColumnSelector
-                            columns={columns}
-                            onColumnToggle={onColumnToggle} />}
-                    </Grid>
-                </Toolbar>}
-                <DataTable
-                    columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
-                    items={items}
-                    onRowClick={(_, item: T) => onRowClick(item)}
-                    onContextMenu={onContextMenu}
-                    onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
-                    onFiltersChange={onFiltersChange}
-                    onSortToggle={onSortToggle}
-                    extractKey={extractKey}
-                    working={working}
-                    defaultView={dataTableDefaultView}
-                    currentItemUuid={currentItemUuid}
-                    currentRoute={paperKey} />
-                <Toolbar className={classes.footer}>
-                    <Grid container justify="flex-end">
-                        {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
-                            count={itemsAvailable}
-                            rowsPerPage={rowsPerPage}
-                            rowsPerPageOptions={rowsPerPageOptions}
-                            page={this.props.page}
-                            onChangePage={this.changePage}
-                            onChangeRowsPerPage={this.changeRowsPerPage}
-                            component="div" /> : <Button
-                                variant="text"
-                                size="medium"
-                                onClick={this.loadMore}
-                            >Load more</Button>}
+                        {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
+                        <Grid
+                            item
+                            xs="auto"
+                            className={classes.dataTable}
+                            style={currentRoute?.includes('search-results')  || !!progressBar ? {marginTop: '-10px'} : {}}
+                        >
+                            <DataTable
+                                columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
+                                items={items}
+                                onRowClick={(_, item: T) => onRowClick(item)}
+                                onContextMenu={onContextMenu}
+                                onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
+                                onFiltersChange={onFiltersChange}
+                                onSortToggle={onSortToggle}
+                                extractKey={extractKey}
+                                working={this.state.showLoading}
+                                defaultViewIcon={defaultViewIcon}
+                                defaultViewMessages={defaultViewMessages}
+                                currentItemUuid={currentItemUuid}
+                                currentRoute={paperKey}
+                                toggleMSToolbar={toggleMSToolbar}
+                                setCheckedListOnStore={setCheckedListOnStore}
+                                checkedList={checkedList}
+                            />
+                        </Grid>
+                        <Grid
+                            item
+                            xs
+                        >
+                            <Toolbar className={classes.footer}>
+                                {elementPath && (
+                                    <Grid container>
+                                        <span data-cy="element-path">{elementPath}</span>
+                                    </Grid>
+                                )}
+                                <Grid
+                                    container={!elementPath}
+                                    justify="flex-end"
+                                >
+                                    {fetchMode === DataTableFetchMode.PAGINATED ? (
+                                        <TablePagination
+                                            count={itemsAvailable}
+                                            rowsPerPage={rowsPerPage}
+                                            rowsPerPageOptions={rowsPerPageOptions}
+                                            page={this.props.page}
+                                            onChangePage={this.changePage}
+                                            onChangeRowsPerPage={this.changeRowsPerPage}
+                                            // Disable next button on empty lists since that's not default behavior
+                                            nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
+                                            component="div"
+                                        />
+                                    ) : (
+                                        <Button
+                                            variant="text"
+                                            size="medium"
+                                            onClick={this.loadMore}
+                                        >
+                                            Load more
+                                        </Button>
+                                    )}
+                                </Grid>
+                            </Toolbar>
+                        </Grid>
                     </Grid>
-                </Toolbar>
-            </Paper>;
+                </Paper>
+            );
         }
 
         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
             this.props.onChangePage(page);
-        }
+        };
 
-        changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
+        changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
-        }
+        };
 
         loadMore = () => {
             this.props.onLoadMore(this.props.page + 1);
-        }
+        };
 
-        renderContextMenuTrigger = (item: T) =>
-            <Grid container justify="center">
-                <Tooltip title="More options" disableFocusListener>
-                    <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
-                        <MoreOptionsIcon />
+        renderContextMenuTrigger = (item: T) => (
+            <Grid
+                container
+                justify="center"
+            >
+                <Tooltip
+                    title="More options"
+                    disableFocusListener
+                >
+                    <IconButton
+                        className={this.props.classes.moreOptionsButton}
+                        onClick={event => {
+                            event.stopPropagation()
+                            this.props.onContextMenu(event, item)
+                        }}
+                    >
+                        <MoreVerticalIcon />
                     </IconButton>
                 </Tooltip>
             </Grid>
+        );
 
-        contextMenuColumn: DataColumn<any> = {
+        contextMenuColumn: DataColumn<any, any> = {
             name: "Actions",
             selected: true,
             configurable: false,
             filters: createTree(),
             key: "context-actions",
-            render: this.renderContextMenuTrigger
+            render: this.renderContextMenuTrigger,
         };
     }
 );