Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
DataGridView.jsx707 linesDownload Raw Back to SchemaView
1/////////////////////////////////////////////////////////////2//3// pgAdmin 4 - PostgreSQL Tools4//5// Copyright (C) 2013 - 2024, The pgAdmin Development Team6// This software is released under the PostgreSQL Licence7//8//////////////////////////////////////////////////////////////9 10/* The DataGridView component is based on react-table component */11 12import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';13import { Box } from '@mui/material';14import { makeStyles } from '@mui/styles';15import { PgIconButton } from '../components/Buttons';16import AddIcon from '@mui/icons-material/AddOutlined';17import { MappedCellControl } from './MappedControl';18import DragIndicatorRoundedIcon from '@mui/icons-material/DragIndicatorRounded';19import EditRoundedIcon from '@mui/icons-material/EditRounded';20import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';21import { useTable, useFlexLayout, useResizeColumns, useSortBy, useExpanded, useGlobalFilter } from 'react-table';22import clsx from 'clsx';23import PropTypes from 'prop-types';24import _ from 'lodash';25import { DndProvider, useDrag, useDrop } from 'react-dnd';26import {HTML5Backend} from 'react-dnd-html5-backend';27 28import gettext from 'sources/gettext';29import { SCHEMA_STATE_ACTIONS, StateUtilsContext } from '.';30import FormView, { getFieldMetaData } from './FormView';31import CustomPropTypes from 'sources/custom_prop_types';32import { evalFunc } from 'sources/utils';33import { DepListenerContext } from './DepListener';34import { useIsMounted } from '../custom_hooks';35import { InputText } from '../components/FormComponents';36import { usePgAdmin } from '../BrowserComponent';37import { requestAnimationAndFocus } from '../utils';38 39const useStyles = makeStyles((theme)=>({40  grid: {41    ...theme.mixins.panelBorder,42    backgroundColor: theme.palette.background.default,43  },44  gridHeader: {45    display: 'flex',46    ...theme.mixins.panelBorder.bottom,47    backgroundColor: theme.otherVars.headerBg,48  },49  gridHeaderText: {50    padding: theme.spacing(0.5, 1),51    fontWeight: theme.typography.fontWeightBold,52  },53  gridControls: {54    marginLeft: 'auto',55  },56  gridControlsButton: {57    border: 0,58    borderRadius: 0,59    ...theme.mixins.panelBorder.left,60  },61  gridRowButton: {62    border: 0,63    borderRadius: 0,64    padding: 0,65    minWidth: 0,66    backgroundColor: 'inherit',67    '&.Mui-disabled': {68      border: 0,69    },70  },71  gridTableContainer: {72    overflow: 'auto',73    width: '100%',74  },75  table: {76    borderSpacing: 0,77    width: '100%',78    overflow: 'auto',79    backgroundColor: theme.otherVars.tableBg,80  },81  tableRowHovered: {82    position: 'relative',83    '& .hover-overlay': {84      backgroundColor: theme.palette.primary.light,85      position: 'absolute',86      inset: 0,87      opacity: 0.75,88    }89  },90  tableCell: {91    margin: 0,92    padding: theme.spacing(0.5),93    ...theme.mixins.panelBorder.bottom,94    ...theme.mixins.panelBorder.right,95    position: 'relative',96    overflow: 'hidden',97    textOverflow: 'ellipsis',98    whiteSpace: 'nowrap',99  },100  tableCellHeader: {101    fontWeight: theme.typography.fontWeightBold,102    padding: theme.spacing(1, 0.5),103    textAlign: 'left',104  },105  tableContentWidth: {106    width: 'calc(100% - 3px)',107  },108  btnCell: {109    padding: theme.spacing(0.5, 0),110    textAlign: 'center',111  },112  btnReorder: {113    cursor: 'move',114  },115  resizer: {116    display: 'inline-block',117    width: '5px',118    height: '100%',119    position: 'absolute',120    right: 0,121    top: 0,122    transform: 'translateX(50%)',123    zIndex: 1,124    touchAction: 'none',125  },126  expandedForm: {127    borderTopWidth: theme.spacing(0.5),128    borderStyle: 'solid ',129    borderColor: theme.palette.grey[400],130  },131  expandedIconCell: {132    backgroundColor: theme.palette.grey[400],133    borderBottom: 'none',134  }135}));136 137function DataTableHeader({headerGroups, viewHelperProps, schema}) {138  const classes = useStyles();139 140  /* Using ref so that schema variable is not frozen in columns closure */141  const schemaRef = useRef(schema);142 143  const sortIcon = (isDesc) => {144    return isDesc ? ' 🔽' : ' 🔼';145  };146  return (147    <div className={classes.tableContentWidth}>148      {headerGroups.map((headerGroup, hi) => (149        <div key={hi} {...headerGroup.getHeaderGroupProps()}>150          {headerGroup.headers.map((column, ci) => {151            let {modeSupported} = column.field ? getFieldMetaData(column.field, schemaRef.current, {}, viewHelperProps) : {modeSupported: true};152            return( modeSupported &&153              <div key={ci} {...column.getHeaderProps()}>154                <div {...(column.sortable ? column.getSortByToggleProps() : {})} className={clsx(classes.tableCell, classes.tableCellHeader)}>155                  {column.render('Header')}156                  <span>157                    {column.isSorted ? sortIcon(column.isSortedDesc) : ''}158                  </span>159                </div>160                {!column.disableResizing &&161                  <div162                    {...column.getResizerProps()}163                    className={classes.resizer}164                  />}165              </div>166            );167          })}168        </div>169      ))}170    </div>171  );172}173 174DataTableHeader.propTypes = {175  headerGroups: PropTypes.array.isRequired,176  viewHelperProps: PropTypes.object.isRequired,177  schema: CustomPropTypes.schemaUI.isRequired,178};179 180function DataTableRow({index, row, totalRows, isResizing, isHovered, schema, schemaRef, accessPath, moveRow, setHoverIndex, viewHelperProps}) {181  const classes = useStyles();182  const [key, setKey] = useState(false);183  const depListener = useContext(DepListenerContext);184  const rowRef = useRef(null);185  const dragHandleRef = useRef(null);186 187  /* Memoize the row to avoid unnecessary re-render.188   * If table data changes, then react-table re-renders the complete tables189   * We can avoid re-render by if row data is not changed190   */191  let depsMap = _.values(row.values, Object.keys(row.values).filter((k)=>!k.startsWith('btn')));192  const externalDeps = useMemo(()=>{193    let retVal = [];194    /* Calculate the fields which depends on the current field195    deps has info on fields which the current field depends on. */196    schema.fields.forEach((field)=>{197      (evalFunc(null, field.deps) || []).forEach((dep)=>{198        let source = accessPath.concat(dep);199        if(_.isArray(dep)) {200          source = dep;201          /* If its an array, then dep is from the top schema and external */202          retVal.push(source);203        }204      });205    });206    return retVal;207  }, []);208 209  useEffect(()=>{210    schemaRef.current.fields.forEach((field)=>{211      /* Self change is also dep change */212      if(field.depChange || field.deferredDepChange) {213        depListener?.addDepListener(accessPath.concat(field.id), accessPath.concat(field.id), field.depChange, field.deferredDepChange);214      }215      (evalFunc(null, field.deps) || []).forEach((dep)=>{216        let source = accessPath.concat(dep);217        if(_.isArray(dep)) {218          source = dep;219        }220        if(field.depChange) {221          depListener?.addDepListener(source, accessPath.concat(field.id), field.depChange);222        }223      });224    });225 226    // Try autofocus on newly added row.227    requestAnimationAndFocus(rowRef.current?.querySelector('input'));228 229    return ()=>{230      /* Cleanup the listeners when unmounting */231      depListener?.removeDepListener(accessPath);232    };233  }, []);234 235  const [{ handlerId }, drop] = useDrop({236    accept: 'row',237    collect(monitor) {238      return {239        handlerId: monitor.getHandlerId(),240      };241    },242    hover(item, monitor) {243      if (!rowRef.current) {244        return;245      }246      item.hoverIndex = null;247      // Don't replace items with themselves248      if (item.index === index) {249        return;250      }251      // Determine rectangle on screen252      const hoverBoundingRect = rowRef.current?.getBoundingClientRect();253      // Determine mouse position254      const clientOffset = monitor.getClientOffset();255      // Get pixels to the top256      const hoverClientY = clientOffset.y - hoverBoundingRect.top;257      // Only perform the move when the mouse has crossed certain part of the items height258      // Dragging downwards259      if (item.index < index && hoverClientY < (hoverBoundingRect.bottom - hoverBoundingRect.top)/3) {260        return;261      }262      // Dragging upwards263      if (item.index > index && hoverClientY > ((hoverBoundingRect.bottom - hoverBoundingRect.top)*2/3)) {264        return;265      }266      setHoverIndex(index);267      item.hoverIndex = index;268    },269  });270 271  const [, drag] = useDrag({272    type: 'row',273    item: () => {274      return {index};275    },276    end: (item)=>{277      // Time to actually perform the action278      setHoverIndex(null);279      if(item.hoverIndex >= 0) {280        moveRow(item.index, item.hoverIndex);281      }282    }283  });284 285  /* External deps values are from top schema sess data */286  depsMap = depsMap.concat(externalDeps.map((source)=>_.get(schemaRef.current.top?.sessData, source)));287  depsMap = depsMap.concat([totalRows, row.isExpanded, key, isResizing, isHovered]);288 289  drag(dragHandleRef);290  drop(rowRef);291 292  return useMemo(()=>293    <div {...row.getRowProps()} ref={rowRef} data-handler-id={handlerId}294      className={isHovered ? classes.tableRowHovered : null}295      data-test='data-table-row'296    >297      {row.cells.map((cell, ci) => {298        let classNames = [classes.tableCell];299 300        let {modeSupported} = cell.column.field? getFieldMetaData(cell.column.field, schemaRef.current, {}, viewHelperProps) : {modeSupported: true};301 302        if(typeof(cell.column.id) == 'string' && cell.column.id.startsWith('btn-')) {303          classNames.push(classes.btnCell);304        }305        if(cell.column.id == 'btn-edit' && row.isExpanded) {306          classNames.push(classes.expandedIconCell);307        }308        return (modeSupported &&309            <div ref={cell.column.id == 'btn-reorder' ? dragHandleRef : null} key={ci} {...cell.getCellProps()} className={clsx(classNames)}>310              {cell.render('Cell', {311                reRenderRow: ()=>{setKey((currKey)=>!currKey);}312              })}313            </div>314        );315      })}316      <div className='hover-overlay'></div>317    </div>, depsMap);318}319 320export function DataGridHeader({label, canAdd, onAddClick, canSearch, onSearchTextChange}) {321  const classes = useStyles();322  const [searchText, setSearchText] = useState('');323 324  return (325    <Box className={classes.gridHeader}>326      { label &&327      <Box className={classes.gridHeaderText}>{label}</Box>328      }329      { canSearch &&330        <Box className={classes.gridHeaderText} width={'100%'}>331          <InputText value={searchText}332            onChange={(value)=>{333              onSearchTextChange(value);334              setSearchText(value);335            }}336            placeholder={gettext('Search')}>337          </InputText>338        </Box>339      }340      <Box className={classes.gridControls}>341        {canAdd && <PgIconButton data-test="add-row" title={gettext('Add row')} onClick={()=>{342          setSearchText('');343          onSearchTextChange('');344          onAddClick();345        }} icon={<AddIcon />} className={classes.gridControlsButton} />}346      </Box>347    </Box>348  );349}350DataGridHeader.propTypes = {351  label: PropTypes.string,352  canAdd: PropTypes.bool,353  onAddClick: PropTypes.func,354  canSearch: PropTypes.bool,355  onSearchTextChange: PropTypes.func,356};357 358export default function DataGridView({359  value, viewHelperProps, schema, accessPath, dataDispatch, containerClassName,360  fixedRows, ...props}) {361  const classes = useStyles();362  const stateUtils = useContext(StateUtilsContext);363  const checkIsMounted = useIsMounted();364  const [hoverIndex, setHoverIndex] = useState();365  const newRowIndex = useRef();366  const pgAdmin = usePgAdmin();367 368  /* Using ref so that schema variable is not frozen in columns closure */369  const schemaRef = useRef(schema);370  let columns = useMemo(371    ()=>{372      let cols = [];373      if(props.canReorder) {374        let colInfo = {375          Header: <>&nbsp;</>,376          id: 'btn-reorder',377          accessor: ()=>{/*This is intentional (SonarQube)*/},378          disableResizing: true,379          sortable: false,380          dataType: 'reorder',381          width: 26,382          minWidth: 26,383          maxWidth: 26,384          Cell: ()=>{385            return <div className={classes.btnReorder}>386              <DragIndicatorRoundedIcon fontSize="small" />387            </div>;388          }389        };390        colInfo.Cell.displayName = 'Cell';391        cols.push(colInfo);392      }393      if(props.canEdit) {394        let colInfo = {395          Header: <>&nbsp;</>,396          id: 'btn-edit',397          accessor: ()=>{/*This is intentional (SonarQube)*/},398          disableResizing: true,399          sortable: false,400          dataType: 'edit',401          width: 26,402          minWidth: 26,403          maxWidth: 26,404          Cell: ({row})=>{405            let canEditRow = true;406            if(props.canEditRow) {407              canEditRow = evalFunc(schemaRef.current, props.canEditRow, row.original || {});408            }409            return <PgIconButton data-test="expand-row" title={gettext('Edit row')} icon={<EditRoundedIcon fontSize="small" />} className={classes.gridRowButton}410              onClick={()=>{411                row.toggleRowExpanded(!row.isExpanded);412              }} disabled={!canEditRow}413            />;414          }415        };416        colInfo.Cell.displayName = 'Cell';417        colInfo.Cell.propTypes = {418          row: PropTypes.object.isRequired,419        };420        cols.push(colInfo);421      }422      if(props.canDelete) {423        let colInfo = {424          Header: <>&nbsp;</>,425          id: 'btn-delete',426          accessor: ()=>{/*This is intentional (SonarQube)*/},427          disableResizing: true,428          sortable: false,429          dataType: 'delete',430          width: 26,431          minWidth: 26,432          maxWidth: 26,433          Cell: ({row}) => {434            let canDeleteRow = true;435            if(props.canDeleteRow) {436              canDeleteRow = evalFunc(schemaRef.current, props.canDeleteRow, row.original || {});437            }438 439            return (440              <PgIconButton data-test="delete-row" title={gettext('Delete row')} icon={<DeleteRoundedIcon fontSize="small" />}441                onClick={()=>{442                  const deleteRow = ()=> {443                    dataDispatch({444                      type: SCHEMA_STATE_ACTIONS.DELETE_ROW,445                      path: accessPath,446                      value: row.index,447                    });448                    return true;449                  };450 451                  if (props.onDelete){452                    props.onDelete(row.original || {}, deleteRow);453                  } else {454                    pgAdmin.Browser.notifier.confirm(455                      props.customDeleteTitle || gettext('Delete Row'),456                      props.customDeleteMsg || gettext('Are you sure you wish to delete this row?'),457                      deleteRow,458                      function() {459                        return true;460                      }461                    );462                  }463                }} className={classes.gridRowButton} disabled={!canDeleteRow} />464            );465          }466        };467        colInfo.Cell.displayName = 'Cell';468        colInfo.Cell.propTypes = {469          row: PropTypes.object.isRequired,470        };471        cols.push(colInfo);472      }473 474      cols = cols.concat(475        schemaRef.current.fields.filter((f)=>{476          return _.isArray(props.columns) ? props.columns.indexOf(f.id) > -1 : true;477        }).sort((firstF, secondF)=>{478          if(_.isArray(props.columns)) {479            return props.columns.indexOf(firstF.id) < props.columns.indexOf(secondF.id) ? -1 : 1;480          }481          return 0;482        }).map((field)=>{483          let widthParms = {};484          if(field.width) {485            widthParms.width = field.width;486            widthParms.minWidth = field.width;487          } else {488            widthParms.width = 75;489            widthParms.minWidth = 75;490          }491          if(field.minWidth) {492            widthParms.minWidth = field.minWidth;493          }494          if(field.maxWidth) {495            widthParms.maxWidth = field.maxWidth;496          }497          widthParms.disableResizing = Boolean(field.disableResizing);498 499          let colInfo = {500            Header: field.label||<>&nbsp;</>,501            accessor: field.id,502            field: field,503            disableResizing: false,504            sortable: true,505            ...widthParms,506            Cell: ({value, row, ...other}) => {507              /* Make sure to take the latest field info from schema */508              field = _.find(schemaRef.current.fields, (f)=>f.id==field.id) || field;509 510              let {editable, disabled, modeSupported} = getFieldMetaData(field, schemaRef.current, row.original || {}, viewHelperProps);511 512              if(_.isUndefined(field.cell)) {513                console.error('cell is required ', field);514              }515 516              return modeSupported && <MappedCellControl rowIndex={row.index} value={value}517                row={row.original} {...field}518                readonly={!editable}519                disabled={disabled}520                visible={true}521                onCellChange={(changeValue)=>{522                  if(field.radioType) {523                    dataDispatch({524                      type: SCHEMA_STATE_ACTIONS.BULK_UPDATE,525                      path: accessPath,526                      value: changeValue,527                      id: field.id528                    });529                  }530                  dataDispatch({531                    type: SCHEMA_STATE_ACTIONS.SET_VALUE,532                    path: accessPath.concat([row.index, field.id]),533                    value: changeValue,534                  });535                }}536                reRenderRow={other.reRenderRow}537              />;538            },539          };540          colInfo.Cell.displayName = 'Cell';541          colInfo.Cell.propTypes = {542            row: PropTypes.object.isRequired,543            value: PropTypes.any,544            onCellChange: PropTypes.func,545          };546          return colInfo;547        })548      );549      return cols;550    },[props.canEdit, props.canDelete, props.canReorder]551  );552 553  const onAddClick = useCallback(()=>{554    if(!props.canAddRow) {555      return;556    }557 558    let newRow = schemaRef.current.getNewData();559    if(props.expandEditOnAdd && props.canEdit) {560      newRowIndex.current = rows.length;561    }562    dataDispatch({563      type: SCHEMA_STATE_ACTIONS.ADD_ROW,564      path: accessPath,565      value: newRow,566      addOnTop: props.addOnTop567    });568  }, [props.canAddRow, rows?.length]);569 570  const defaultColumn = useMemo(()=>({571  }), []);572 573  let tablePlugins = [574    useGlobalFilter,575    useFlexLayout,576    useResizeColumns,577    useSortBy,578    useExpanded,579  ];580 581  const {582    getTableProps,583    getTableBodyProps,584    headerGroups,585    rows,586    prepareRow,587    setGlobalFilter,588  } = useTable(589    {590      columns,591      data: value,592      defaultColumn,593      manualSortBy: true,594      autoResetSortBy: false,595      autoResetExpanded: false,596    },597    ...tablePlugins,598  );599 600  useEffect(()=>{601    let rowsPromise = fixedRows;602 603    /* If fixedRows is defined, fetch the details */604    if(typeof rowsPromise === 'function') {605      rowsPromise = rowsPromise();606    }607    if(rowsPromise) {608      Promise.resolve(rowsPromise)609        .then((res)=>{610          /* If component unmounted, dont update state */611          if(checkIsMounted()) {612            stateUtils.initOrigData(accessPath, res);613          }614        });615    }616  }, []);617 618  useEffect(()=>{619    if(newRowIndex.current >= 0) {620      rows[newRowIndex.current]?.toggleRowExpanded(true);621      newRowIndex.current = null;622    }623  }, [rows?.length]);624 625  const moveRow = (dragIndex, hoverIndex) => {626    dataDispatch({627      type: SCHEMA_STATE_ACTIONS.MOVE_ROW,628      path: accessPath,629      oldIndex: dragIndex,630      newIndex: hoverIndex,631    });632  };633 634  const isResizing = _.flatMap(headerGroups, headerGroup => headerGroup.headers.map(col=>col.isResizing)).includes(true);635 636  if(!props.visible) {637    return <></>;638  }639 640  return (641    <Box className={containerClassName}>642      <Box className={classes.grid}>643        {(props.label || props.canAdd) && <DataGridHeader label={props.label} canAdd={props.canAdd} onAddClick={onAddClick}644          canSearch={props.canSearch}645          onSearchTextChange={(value)=>{646            setGlobalFilter(value || undefined);647          }}648        />}649        <DndProvider backend={HTML5Backend}>650          <div {...getTableProps(()=>({style: {minWidth: 'unset'}}))} className={classes.table} data-test="data-grid-view">651            <DataTableHeader headerGroups={headerGroups} viewHelperProps={viewHelperProps} schema={schema} />652            <div {...getTableBodyProps()} className={classes.tableContentWidth}>653              {rows.map((row, i) => {654                prepareRow(row);655                return <React.Fragment key={row.index}>656                  <DataTableRow index={i} row={row} totalRows={rows.length} isResizing={isResizing}657                    schema={schemaRef.current} schemaRef={schemaRef} accessPath={accessPath.concat([row.index])}658                    moveRow={moveRow} isHovered={i == hoverIndex} setHoverIndex={setHoverIndex} viewHelperProps={viewHelperProps}/>659                  {props.canEdit && row.isExpanded &&660                    <FormView value={row.original} viewHelperProps={viewHelperProps} dataDispatch={dataDispatch}661                      schema={schemaRef.current} accessPath={accessPath.concat([row.index])} isNested={true} className={classes.expandedForm}662                      isDataGridForm={true} firstEleRef={(ele)=>{663                        requestAnimationAndFocus(ele);664                      }}/>665                  }666                </React.Fragment>;667              })}668            </div>669          </div>670        </DndProvider>671      </Box>672    </Box>673  );674}675 676DataGridView.propTypes = {677  label: PropTypes.string,678  value: PropTypes.array,679  viewHelperProps: PropTypes.object,680  schema: CustomPropTypes.schemaUI,681  accessPath: PropTypes.array.isRequired,682  dataDispatch: PropTypes.func,683  containerClassName: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),684  fixedRows: PropTypes.oneOfType([PropTypes.array, PropTypes.instanceOf(Promise), PropTypes.func]),685  columns: PropTypes.array,686  canEdit: PropTypes.bool,687  canAdd: PropTypes.bool,688  canDelete: PropTypes.bool,689  canReorder: PropTypes.bool,690  visible: PropTypes.bool,691  canAddRow: PropTypes.oneOfType([692    PropTypes.bool, PropTypes.func,693  ]),694  canEditRow: PropTypes.oneOfType([695    PropTypes.bool, PropTypes.func,696  ]),697  canDeleteRow: PropTypes.oneOfType([698    PropTypes.bool, PropTypes.func,699  ]),700  expandEditOnAdd: PropTypes.bool,701  customDeleteTitle: PropTypes.string,702  customDeleteMsg: PropTypes.string,703  canSearch: PropTypes.bool,704  onDelete: PropTypes.func,705  addOnTop: PropTypes.bool706};707 
codekingpro/portable-devtools · Team Ai