????

Your IP : 216.73.216.42


Current Path : C:/opt/pgsql/pgAdmin 4/web/pgadmin/static/js/components/
Upload File :
Current File : C:/opt/pgsql/pgAdmin 4/web/pgadmin/static/js/components/PgTable.jsx

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////

import React from 'react';
import {
  useTable,
  useRowSelect,
  useSortBy,
  useResizeColumns,
  useFlexLayout,
  useGlobalFilter,
  useExpanded,
} from 'react-table';
import { VariableSizeList } from 'react-window';
import { makeStyles } from '@mui/styles';
import clsx from 'clsx';
import PropTypes from 'prop-types';
import AutoSizer from 'react-virtualized-auto-sizer';
import { Checkbox, Box, Switch } from '@mui/material';
import { InputText } from './FormComponents';
import _ from 'lodash';
import gettext from 'sources/gettext';
import SchemaView from '../SchemaView';
import EmptyPanelMessage from './EmptyPanelMessage';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { PgIconButton } from './Buttons';

/* eslint-disable react/display-name */
const useStyles = makeStyles((theme) => ({
  root: {
    display: 'flex',
    flexDirection: 'column',
    height: '100%',
    ...theme.mixins.panelBorder,
    backgroundColor: theme.palette.background.default,
  },
  autoResizerContainer: {
    flexGrow: 1,
    minHeight: 0
  },
  autoResizer: {
    width: '100% !important',
  },
  fixedSizeList: {
    direction: 'ltr',
    overflowX: 'hidden !important',
    overflow: 'overlay !important',
  },
  CustomHeader:{
    marginTop: '8px',
    marginLeft: '4px'
  },
  warning: {
    backgroundColor: theme.palette.warning.main + '!important'
  },
  alert: {
    backgroundColor: theme.palette.error.main + '!important'
  },
  searchInput: {
    minWidth: '300px'
  },
  tableContainer: {
    overflowX: 'auto',
    flexGrow: 1,
    minHeight: 0,
    display: 'flex',
    flexDirection: 'column',
    backgroundColor: theme.otherVars.emptySpaceBg,
  },
  table: {
    borderSpacing: 0,
    overflow: 'hidden',
    borderRadius: theme.shape.borderRadius,
    border: '1px solid '+theme.otherVars.borderColor,
    display: 'flex',
    flexDirection: 'column',
    height: '100%',
  },
  pgTableContainer: {
    display: 'flex',
    flexGrow: 1,
    overflow: 'hidden',
    flexDirection: 'column',
    height: '100%',
  },
  pgTableHeader: {
    display: 'flex',
    background: theme.palette.background.default,
    padding: '8px 8px 4px',
  },
  tableRowContent:{
    display: 'flex',
    flexDirection: 'column',
    minHeight: 0,
  },

  expandedForm: {
    ...theme.mixins.panelBorder.all,
    margin: '8px',
    flexGrow: 1,
  },

  tableCell: {
    margin: 0,
    padding: theme.spacing(0.5),
    ...theme.mixins.panelBorder.bottom,
    ...theme.mixins.panelBorder.right,
    position: 'relative',
    overflow: 'hidden',
    height: '34px',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
    backgroundColor: theme.otherVars.tableBg,
    userSelect: 'text'
  },
  selectCell: {
    textAlign: 'center',
    minWidth: 20
  },
  tableHeader: {
    backgroundColor: theme.otherVars.tableBg,
  },
  tableCellHeader: {
    fontWeight: theme.typography.fontWeightBold,
    padding: theme.spacing(1, 0.5),
    textAlign: 'left',
    alignContent: 'center',
    backgroundColor: theme.otherVars.tableBg,
    overflow: 'hidden',
    ...theme.mixins.panelBorder.bottom,
    ...theme.mixins.panelBorder.right,
    ...theme.mixins.panelBorder.top,
    ...theme.mixins.panelBorder.left,
  },
  resizer: {
    display: 'inline-block',
    width: '5px',
    height: '100%',
    position: 'absolute',
    right: 0,
    top: 0,
    transform: 'translateX(50%)',
    zIndex: 1,
    touchAction: 'none',
  },
  cellIcon: {
    paddingLeft: '1.8em',
    paddingTop: '0.35em',
    borderRadius: 0,
    backgroundPosition: '1%',
  },
  emptyPanel: {
    minHeight: '100%',
    minWidth: '100%',
    overflow: 'auto',
    padding: '8px',
    display: 'flex',
  },
  caveTable: {
    margin: '8px',
  },
  panelIcon: {
    width: '80%',
    margin: '0 auto',
    marginTop: '25px !important',
    position: 'relative',
    textAlign: 'center',
  },
  panelMessage: {
    marginLeft: '0.5rem',
    fontSize: '0.875rem',
  },
  expandedIconCell: {
    backgroundColor: theme.palette.grey[400],
    ...theme.mixins.panelBorder.top,
    borderBottom: 'none',
  },
  btnCell: {
    padding: theme.spacing(0.5, 0),
    textAlign: 'center',
  },
  btnExpanded: {
    backgroundColor: theme.palette.grey[400]
  },
  readOnlySwitch: {
    opacity: 0.75,
    '& .MuiSwitch-track': {
      opacity: theme.palette.action.disabledOpacity,
    }
  }
}));

const IndeterminateCheckbox = React.forwardRef(
  ({ indeterminate, label, ...rest }, ref) => {
    const defaultRef = React.useRef();
    const resolvedRef = ref || defaultRef;

    React.useEffect(() => {
      resolvedRef.current.indeterminate = indeterminate;
    }, [resolvedRef, indeterminate]);
    return (
      <Checkbox
        color="primary"
        ref={resolvedRef} {...rest}
        inputProps={{'aria-label': label}}
      />
    );
  },
);

IndeterminateCheckbox.displayName = 'SelectCheckbox';

IndeterminateCheckbox.propTypes = {
  indeterminate: PropTypes.bool,
  rest: PropTypes.func,
  getToggleAllRowsSelectedProps: PropTypes.func,
  row: PropTypes.object,
  label: PropTypes.string,
};

const ROW_HEIGHT = 34;

function SortIcon ({column}) {
  if (column.isSorted) {
    return column.isSortedDesc ? <KeyboardArrowDownIcon style={{fontSize: '1.2rem'}} /> : <KeyboardArrowUpIcon style={{fontSize: '1.2rem'}} />;
  }
  return '';
}

SortIcon.propTypes = {
  column: PropTypes.object
};

function RenderRow({ index, style, schema, row, prepareRow, setRowHeight, ExpandedComponent }) {
  const [expandComplete, setExpandComplete] = React.useState(false);
  const rowRef = React.useRef() ;
  const classes = useStyles();
  prepareRow(row);

  React.useEffect(()=>{
    if(rowRef.current) {
      if(!expandComplete && rowRef.current.style.height == `${ROW_HEIGHT}px`) {
        return;
      }
      let rowHeight;
      rowRef.current.style.height = 'unset';
      if(expandComplete) {
        rowHeight = rowRef.current.offsetHeight;
      } else {
        rowHeight = ROW_HEIGHT;
        rowRef.current.style.height = ROW_HEIGHT;
      }
      rowRef.current.style.height = rowHeight + 'px';
      setRowHeight(index, rowHeight);
    }
  }, [expandComplete]);

  return (
    <div style={style} key={row.id} ref={rowRef} data-test="row-container">
      <div className={classes.tableRowContent}>
        <div {...row.getRowProps()} className={classes.tr}>
          {row.cells.map((cell) => {
            let classNames = [classes.tableCell];
            if(typeof(cell.column.id) == 'string' && cell.column.id.startsWith('btn-')) {
              classNames.push(classes.btnCell);
            }
            if(cell.column.id == 'btn-edit' && row.isExpanded) {
              classNames.push(classes.expandedIconCell);
            }
            if (row.original.row_type === 'warning'){
              classNames.push(classes.warning);
            }
            if (row.original.row_type === 'alert'){
              classNames.push(classes.alert);
            }
            return (
              <div key={cell.column.id} {...cell.getCellProps()} className={clsx(classNames, cell.column?.dataClassName, row.original.icon?.[cell.column.id], row.original.icon?.[cell.column.id] && classes.cellIcon)}
                title={_.isUndefined(cell.value) || _.isNull(cell.value) ? '': String(cell.value)}>
                {cell.render('Cell')}
              </div>
            );
          })}
        </div>
        {!_.isUndefined(row) && row.isExpanded && (
          <Box key={row.id} className={classes.expandedForm}>
            {schema && <SchemaView
              getInitData={()=>Promise.resolve(row.original)}
              viewHelperProps={{ mode: 'properties' }}
              schema={schema[row.id]??schema}
              showFooter={false}
              onDataChange={()=>{setExpandComplete(true);}}
            />}
            {ExpandedComponent && <ExpandedComponent row={row} onExpandComplete={()=>setExpandComplete(true)}/>}
          </Box>
        )}
      </div>
    </div>
  );
}
RenderRow.propTypes = {
  index: PropTypes.number,
  style: PropTypes.object,
  row: PropTypes.object,
  schema: PropTypes.object,
  prepareRow: PropTypes.func,
  setRowHeight: PropTypes.func,
  ExpandedComponent: PropTypes.node,
};

export default function PgTable({ columns, data, isSelectRow, caveTable=true, schema, ExpandedComponent, sortOptions, tableProps, ...props }) {
  // Use the state and functions returned from useTable to build your UI
  const classes = useStyles();
  const [searchVal, setSearchVal] = React.useState('');
  const windowTableRef = React.useRef();
  const rowHeights = React.useRef({});

  // Reset Search value on tab changes.

  React.useEffect(()=>{
    setSearchVal(prevState => (prevState));
    setGlobalFilter(searchVal || undefined);
    rowHeights.current = {};
    windowTableRef.current?.resetAfterIndex(0);
  }, [data]);

  function getRowHeight(index) {
    return rowHeights.current[index] || ROW_HEIGHT;
  }

  const setRowHeight = (index, size) => {
    if(windowTableRef.current) {
      if(size == ROW_HEIGHT) {
        delete rowHeights.current[index];
      } else {
        rowHeights.current[index] = size;
      }
      windowTableRef.current.resetAfterIndex(index);
    }
  };

  const defaultColumn = React.useMemo(
    () => ({
      minWidth: 50,
    }),
    []
  );

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    rows,
    prepareRow,
    selectedFlatRows,
    state: { selectedRowIds },
    setGlobalFilter,
    setHiddenColumns,
    totalColumnsWidth
  } = useTable(
    {
      columns,
      data,
      defaultColumn,
      isSelectRow,
      autoResetSortBy: false,
      initialState: {
        sortBy: sortOptions || [],
      },
      ...tableProps,
    },
    useGlobalFilter,
    useSortBy,
    useExpanded,
    useRowSelect,
    useResizeColumns,
    useFlexLayout,
    (hooks) => {
      hooks.visibleColumns.push((CLOUMNS) => {
        if (isSelectRow) {
          return [
            // Let's make a column for selection
            {
              id: 'selection',
              resizable: false,
              // The header can use the table's getToggleAllRowsSelectedProps method
              // to render a checkbox
              Header: ({ getToggleAllRowsSelectedProps, toggleRowSelected, isAllRowsSelected, rows }) => {

                const modifiedOnChange = (event) => {
                  rows.forEach((row) => {
                    //check each row if it is not disabled
                    !(!_.isUndefined(row.original.canDrop) && !(row.original.canDrop)) && toggleRowSelected(row.id, event.currentTarget.checked);

                  });
                };

                let allTableRows = 0;
                let selectedTableRows = 0;
                rows.forEach((row) => {
                  row.isSelected && selectedTableRows++;
                  (_.isUndefined(row.original.canDrop) || row.original.canDrop) && allTableRows++;
                });
                const disabled = allTableRows === 0;
                const checked =
                    (isAllRowsSelected ||
                      allTableRows === selectedTableRows) &&
                    !disabled;
                return(
                  <div className={classes.selectCell}>
                    <IndeterminateCheckbox {...getToggleAllRowsSelectedProps()
                    }
                    onChange={modifiedOnChange}
                    checked={checked}
                    label={gettext('Select All Rows')}
                    />
                  </div>
                );},
              // The cell can use the individual row's getToggleRowSelectedProps method
              // to the render a checkbox
              Cell: ({ row }) => (
                <div className={classes.selectCell}>
                  <IndeterminateCheckbox {...row.getToggleRowSelectedProps()}
                    disabled={!_.isUndefined(row.original.canDrop) ? !(row.original.canDrop) : false}
                    label={gettext('Select Row')}
                  />
                </div>
              ),
              sortable: false,
              disableResizing: true,
              width: 35,
              maxWidth: 35,
              minWidth: 35
            },
            ...CLOUMNS,
          ];
        } else {
          return [...CLOUMNS];
        }
      });
    }
  );

  React.useEffect(() => {
    setHiddenColumns(
      columns
        .filter((column) => {
          return !(column.isVisible === undefined || column.isVisible === true);
        }
        )
        .map((column) => column.accessor)
    );
  }, [setHiddenColumns, columns]);

  React.useEffect(() => {
    if (props.setSelectedRows) {
      props.setSelectedRows(selectedFlatRows);
    }
  }, [selectedRowIds]);

  React.useEffect(() => {
    if (props.getSelectedRows) {
      props.getSelectedRows(selectedFlatRows);
    }
  }, [selectedRowIds]);

  React.useEffect(() => {
    setGlobalFilter(searchVal || undefined);
  }, [searchVal]);

  // Render the UI for your table
  return (
    <Box className={classes.pgTableContainer} data-test={props['data-test']}>
      <Box className={classes.pgTableHeader}>
        {props.CustomHeader && (<Box className={classes.customHeader}> <props.CustomHeader /></Box>)}
        <Box marginLeft="auto">
          <InputText
            placeholder={gettext('Search')}
            controlProps={{title: gettext('Search')}}
            className={classes.searchInput}
            value={searchVal}
            onChange={(val) => {
              setSearchVal(val);
            }}
          />
        </Box>
      </Box>
      <div className={classes.tableContainer}>
        <div {...getTableProps({style:{minWidth: totalColumnsWidth}})} className={clsx(classes.table, caveTable ? classes.caveTable : '')}>
          <div>
            {headerGroups.map((headerGroup) => (
              <div key={''} {...headerGroup.getHeaderGroupProps((column)=>({
                style: {
                  ...column.style,
                  height: '40px',
                }
              }))}>
                {headerGroup.headers.map((column) => (
                  <div
                    key={column.id}
                    {...column.getHeaderProps()}
                    className={clsx(classes.tableCellHeader, column.className)}
                  >
                    <div
                      {...(column.sortable ? column.getSortByToggleProps() : {})}
                    >
                      {column.render('Header')}
                      <span>
                        <SortIcon column={column} />
                      </span>
                    </div>
                    {column.resizable && (
                      <div
                        {...column.getResizerProps()}
                        className={classes.resizer}
                      />
                    )}
                  </div>
                ))}
              </div>
            ))}
          </div>
          {
            data.length > 0 ? (
              <div {...getTableBodyProps()} className={classes.autoResizerContainer}>
                <AutoSizer
                  className={classes.autoResizer}
                >
                  {({ height }) => (
                    <VariableSizeList
                      ref={windowTableRef}
                      className={classes.fixedSizeList}
                      height={isNaN(height) ? 100 : height}
                      itemCount={rows.length}
                      itemSize={getRowHeight}
                      itemData={{rows, prepareRow, setRowHeight}}
                    >
                      {({index, style})=>(
                        <RenderRow index={index} style={style} row={rows[index]} schema={schema} prepareRow={prepareRow}
                          setRowHeight={setRowHeight} ExpandedComponent={ExpandedComponent} />
                      )}
                    </VariableSizeList>)}
                </AutoSizer>
              </div>
            ) : (
              <EmptyPanelMessage text={gettext('No rows found')}/>
            )
          }
        </div>
      </div>
    </Box>
  );
}

PgTable.propTypes = {
  stepId: PropTypes.number,
  height: PropTypes.number,
  CustomHeader: PropTypes.func,
  className: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  caveTable: PropTypes.bool,
  fixedSizeList: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
  children: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.node),
    PropTypes.node,
  ]),
  getToggleAllRowsSelectedProps: PropTypes.func,
  toggleRowSelected: PropTypes.func,
  columns: PropTypes.array,
  data: PropTypes.array,
  isSelectRow: PropTypes.bool,
  isAllRowsSelected: PropTypes.bool,
  row: PropTypes.func,
  setSelectedRows: PropTypes.func,
  getSelectedRows: PropTypes.func,
  searchText: PropTypes.string,
  sortOptions: PropTypes.array,
  schema: PropTypes.object,
  rows: PropTypes.object,
  ExpandedComponent: PropTypes.node,
  tableProps: PropTypes.object,
  'data-test': PropTypes.string
};


export function getExpandCell({onClick, ...props}) {
  const Cell = ({ row }) => {
    const classes = useStyles();
    const onClickFinal = (e)=>{
      e.preventDefault();
      row.toggleRowExpanded(!row.isExpanded);
      onClick?.(row, e);
    };
    return (
      <PgIconButton
        size="xs"
        className={row.isExpanded ? classes.btnExpanded : ''}
        icon={
          row.isExpanded ? (
            <KeyboardArrowDownIcon />
          ) : (
            <ChevronRightIcon />
          )
        }
        noBorder
        {...props}
        onClick={onClickFinal}
        aria-label={props.title}
      />
    );
  };

  Cell.displayName = 'ExpandCell';
  Cell.propTypes = {
    title: PropTypes.string,
    row: PropTypes.any,
  };

  return Cell;
}

export function getSwitchCell() {
  const Cell = ({value})=>{
    const classes = useStyles();
    return <Switch color="primary" checked={value} className={classes.readOnlySwitch} value={value} readOnly title={String(value)} />;
  };

  Cell.displayName = 'SwitchCell';
  Cell.propTypes = {
    value: PropTypes.any,
  };

  return Cell;
}