????

Your IP : 216.73.216.188


Current Path : C:/opt/pgsql/pgAdmin 4/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/
Upload File :
Current File : C:/opt/pgsql/pgAdmin 4/web/pgadmin/tools/sqleditor/static/js/components/QueryToolDataGrid/index.jsx

/////////////////////////////////////////////////////////////
//
// pgAdmin 4 - PostgreSQL Tools
//
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
// This software is released under the PostgreSQL Licence
//
//////////////////////////////////////////////////////////////
import { Box } from '@mui/material';
import { makeStyles } from '@mui/styles';
import _ from 'lodash';
import React, {useState, useEffect, useContext, useRef, useLayoutEffect, useMemo} from 'react';
import {Row, useRowSelection} from 'react-data-grid';
import LockIcon from '@mui/icons-material/Lock';
import EditIcon from '@mui/icons-material/Edit';
import { QUERY_TOOL_EVENTS } from '../QueryToolConstants';
import * as Editors from './Editors';
import * as Formatters from './Formatters';
import clsx from 'clsx';
import { PgIconButton } from '../../../../../../static/js/components/Buttons';
import MapIcon from '@mui/icons-material/Map';
import { QueryToolEventsContext } from '../QueryToolComponent';
import PropTypes from 'prop-types';
import gettext from 'sources/gettext';
import PgReactDataGrid from '../../../../../../static/js/components/PgReactDataGrid';

export const ROWNUM_KEY = '$_pgadmin_rownum_key_$';
export const GRID_ROW_SELECT_KEY = '$_pgadmin_gridrowselect_key_$';

const useStyles = makeStyles((theme)=>({
  columnHeader: {
    padding: '3px 6px',
    height: '100%',
    display: 'flex',
    lineHeight: '16px',
    alignItems: 'center',
    fontWeight: 'normal',
  },
  columnName: {
    fontWeight: 'bold',
  },
  editedCell: {
    fontWeight: 'bold',
  },
  deletedRow: {
    '&:before': {
      content: '" "',
      position: 'absolute',
      top: '50%',
      left: 0,
      borderTop: '1px solid ' + theme.palette.error.main,
      width: '100%',
    }
  },
  rowNumCell: {
    padding: '0px 8px',
  },
  colHeaderSelected: {
    outlineColor: theme.palette.primary.main,
    backgroundColor: theme.palette.primary.main,
    color: theme.palette.primary.contrastText,
  },
  colSelected: {
    outlineColor: theme.palette.primary.main,
    backgroundColor: theme.palette.primary.light,
    color: theme.otherVars.qtDatagridSelectFg,
  }
}));

export const RowInfoContext = React.createContext();
export const DataGridExtrasContext = React.createContext();

function CustomRow(props) {
  const rowRef = useRef();
  const dataGridExtras = useContext(DataGridExtrasContext);

  const rowInfoValue = useMemo(()=>({
    rowIdx: props.rowIdx,
    getCellElement: (colIdx)=>{
      return rowRef.current?.querySelector(`.rdg-cell[aria-colindex="${colIdx+1}"]`);
    }
  }), [props.rowIdx]);

  if(!props.isRowSelected && props.selectedCellIdx > 0) {
    dataGridExtras.onSelectedCellChange?.([props.row, props.viewportColumns?.find(columns => columns.idx === props.selectedCellIdx)]);
  } else if(props.selectedCellIdx == 0) {
    dataGridExtras.onSelectedCellChange?.(null);
  }
  const openEditorOnEnter = (e)=>{
    if(e.code === 'Enter' && !props.isRowSelected && props.selectedCellIdx > 0) {
      props.selectCell(props.row, props.viewportColumns?.find(columns => columns.idx === props.selectedCellIdx), true);
    }
  };
  return (
    <RowInfoContext.Provider value={rowInfoValue}>
      <Row ref={rowRef} onKeyDown={openEditorOnEnter} {...props} />
    </RowInfoContext.Provider>
  );
}

CustomRow.propTypes = {
  rowIdx: PropTypes.number,
  isRowSelected: PropTypes.bool,
  selectedCellIdx: PropTypes.number,
  row: PropTypes.object,
  viewportColumns: PropTypes.array,
  selectCell: PropTypes.func,
};

function getCopyShortcutHandler(handleCopy) {
  return (e)=>{
    if((e.ctrlKey || e.metaKey) && e.key !== 'Control' && e.keyCode == 67) {
      handleCopy();
    }
  };
}

function SelectAllHeaderRenderer({onAllRowsSelectionChange, isCellSelected}) {
  const [checked, setChecked] = useState(false);
  const cellRef = useRef();
  const eventBus = useContext(QueryToolEventsContext);
  const dataGridExtras = useContext(DataGridExtrasContext);
  const onClick = ()=>{
    eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
      setChecked(!checked);
      onAllRowsSelectionChange(!checked);
    });
  };

  useLayoutEffect(() => {
    if (!isCellSelected) return;
    cellRef.current?.focus({ preventScroll: true });
  }, [isCellSelected]);

  return <div ref={cellRef} style={{width: '100%', height: '100%'}} onClick={onClick}
    tabIndex="0" onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)}></div>;
}
SelectAllHeaderRenderer.propTypes = {
  onAllRowsSelectionChange: PropTypes.func,
  isCellSelected: PropTypes.bool,
};

function SelectableHeaderRenderer({column, selectedColumns, onSelectedColumnsChange, isCellSelected}) {
  const classes = useStyles();
  const cellRef = useRef();
  const eventBus = useContext(QueryToolEventsContext);
  const dataGridExtras = useContext(DataGridExtrasContext);

  if(isCellSelected) {
    dataGridExtras.onSelectedCellChange?.(null);
  }

  const onClick = ()=>{
    eventBus.fireEvent(QUERY_TOOL_EVENTS.FETCH_MORE_ROWS, true, ()=>{
      const newSelectedCols = new Set(selectedColumns);
      if (newSelectedCols.has(column.idx)) {
        newSelectedCols.delete(column.idx);
      } else {
        newSelectedCols.add(column.idx);
      }
      onSelectedColumnsChange(newSelectedCols);
    });
  };

  const isSelected = selectedColumns.has(column.idx);

  useLayoutEffect(() => {
    if (!isCellSelected) return;
    cellRef.current?.focus({ preventScroll: true });
  }, [isCellSelected]);

  return (
    <Box ref={cellRef} className={clsx(classes.columnHeader, isSelected ? classes.colHeaderSelected : null)} onClick={onClick} tabIndex="0"
      onKeyDown={getCopyShortcutHandler(dataGridExtras.handleCopy)} data-column-key={column.key}>
      {(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') &&
      <Box>
        <PgIconButton title={gettext('View all geometries in this column')} icon={<MapIcon data-label="MapIcon"/>} size="small" style={{marginRight: '0.25rem'}} onClick={(e)=>{
          e.stopPropagation();
          eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_RENDER_GEOMETRIES, column);
        }}/>
      </Box>}
      <Box marginRight="auto">
        <span className={classes.columnName}>{column.display_name}</span><br/>
        <span>{column.display_type}</span>
      </Box>
      <Box marginLeft="4px">{column.can_edit ?
        <EditIcon fontSize="small" style={{fontSize: '0.875rem'}} data-label="EditIcon"/>:
        <LockIcon fontSize="small" style={{fontSize: '0.875rem'}} data-label="LockIcon"/>
      }</Box>
    </Box>
  );
}
SelectableHeaderRenderer.propTypes = {
  column: PropTypes.object,
  selectedColumns: PropTypes.objectOf(Set),
  onSelectedColumnsChange: PropTypes.func,
  isCellSelected: PropTypes.bool,
};

function setEditorFormatter(col) {
  // If grid is editable then add editor else make it readonly
  if (col.cell == 'oid' && col.name == 'oid') {
    col.editor = null;
    col.formatter = Formatters.TextFormatter;
  } else if (col.cell == 'Json') {
    col.editor = Editors.JsonTextEditor;
    col.formatter = Formatters.TextFormatter;
  } else if (['number', 'oid'].indexOf(col.cell) != -1 || ['xid', 'real'].indexOf(col.type) != -1) {
    col.formatter = Formatters.NumberFormatter;
    col.editor = Editors.NumberEditor;
  } else if (col.cell == 'boolean') {
    col.editor = Editors.CheckboxEditor;
    col.formatter = Formatters.TextFormatter;
  } else if (col.cell == 'binary') {
    // We do not support editing binary data in SQL editor and data grid.
    col.editor = null;
    col.formatter = Formatters.BinaryFormatter;
  } else {
    col.editor = Editors.TextEditor;
    col.formatter = Formatters.TextFormatter;
  }
}

function cellClassGetter(col, classes, isSelected, dataChangeStore, rowKeyGetter){
  return (row)=>{
    let cellClasses = [];
    if(dataChangeStore && rowKeyGetter) {
      if(rowKeyGetter(row) in (dataChangeStore?.updated || {})
        && !_.isUndefined(dataChangeStore?.updated[rowKeyGetter(row)]?.data[col.key])
        || rowKeyGetter(row) in (dataChangeStore?.added || {})
      ) {
        cellClasses.push(classes.editedCell);
      }
      if(rowKeyGetter(row) in (dataChangeStore?.deleted || {})) {
        cellClasses.push(classes.deletedRow);
      }
    }
    if(isSelected) {
      cellClasses.push(classes.colSelected);
    }
    return clsx(cellClasses);
  };
}

function initialiseColumns(columns, rows, totalRowCount, columnWidthBy) {
  let retColumns = [
    ...columns,
  ];
  const canvas = document.createElement('canvas');
  const canvasContext = canvas.getContext('2d');
  canvasContext.font = '12px Roboto';

  for(const col of retColumns) {
    col.width = getTextWidth(col, rows, canvasContext, columnWidthBy);
    col.resizable = true;
    col.editorOptions = {
      commitOnOutsideClick: false,
      onCellKeyDown: (e)=>{
        // global keyboard shortcuts will work now and will open the the editor for the cell once pgAdmin reopens
        if(!e.metaKey && !e.altKey && !e.shiftKey && !e.ctrlKey){
          /* Do not open the editor */
          e.preventDefault();
        }
      }
    };
    setEditorFormatter(col);
  }

  let rowNumWidth = canvasContext.measureText((totalRowCount||'').toString()).width;
  /* padding 8 on both sides*/
  rowNumWidth += 16;
  let rowNumCol = {
    key: ROWNUM_KEY, name: '', frozen: true, resizable: false,
    minWidth: 45, width: rowNumWidth,
  };
  rowNumCol.cellClass = cellClassGetter(rowNumCol);
  retColumns.unshift(rowNumCol);
  canvas.remove();
  return retColumns;
}

function RowNumColFormatter({row, rowKeyGetter, dataChangeStore, onSelectedColumnsChange}) {
  const {rowIdx} = useContext(RowInfoContext);
  const [isRowSelected, onRowSelectionChange] = useRowSelection();
  const classes = useStyles();

  let rowKey = rowKeyGetter(row);
  let rownum = rowIdx+1;
  if(rowKey in (dataChangeStore?.added || {})) {
    rownum = rownum+'+';
  } else if(rowKey in (dataChangeStore?.deleted || {})) {
    rownum = rownum+'-';
  }
  return (<div className={classes.rowNumCell} onClick={()=>{
    onSelectedColumnsChange(new Set());
    onRowSelectionChange({ row: row, checked: !isRowSelected, isShiftClick: false});
  }} onKeyDown={()=>{/* already taken care by parent */}}>
    {rownum}
  </div>);
}
RowNumColFormatter.propTypes = {
  row: PropTypes.object,
  rowKeyGetter: PropTypes.func,
  dataChangeStore: PropTypes.object,
  onSelectedColumnsChange: PropTypes.func,
};

function formatColumns(columns, dataChangeStore, selectedColumns, onSelectedColumnsChange, rowKeyGetter, classes) {
  let retColumns = [
    ...columns,
  ];

  const HeaderRenderer = (props)=>{
    return <SelectableHeaderRenderer {...props} selectedColumns={selectedColumns} onSelectedColumnsChange={onSelectedColumnsChange}/>;
  };

  for(const [idx, col] of retColumns.entries()) {
    col.headerRenderer = HeaderRenderer;
    col.cellClass = cellClassGetter(col, classes, selectedColumns.has(idx), dataChangeStore, rowKeyGetter);
  }

  let rowNumCol = retColumns[0];
  rowNumCol.headerRenderer = SelectAllHeaderRenderer;
  rowNumCol.formatter = (props)=>{
    return <RowNumColFormatter {...props} rowKeyGetter={rowKeyGetter} dataChangeStore={dataChangeStore} onSelectedColumnsChange={onSelectedColumnsChange} />;
  };

  return retColumns;
}

function getTextWidth(column, rows, canvas, columnWidthBy) {
  const dataWidthReducer = (longest, nextRow) => {
    let value = nextRow[column.key];
    if(_.isNull(value) || _.isUndefined(value)) {
      value = '';
    }
    value = value.toString();
    return longest.length > value.length ? longest : value;
  };

  let columnHeaderLen = column.display_name.length > column.display_type.length ?
    canvas.measureText(column.display_name).width : canvas.measureText(column.display_type).width;
  /* padding 12, icon-width 15 */
  columnHeaderLen += 15 + 12;
  if(column.column_type_internal == 'geometry' || column.column_type_internal == 'geography') {
    columnHeaderLen += 40;
  }
  let width = columnHeaderLen;
  if(typeof(columnWidthBy) == 'number') {
    /* padding 16 */
    width = 16 + Math.ceil(canvas.measureText(rows.reduce(dataWidthReducer, '')).width);
    if(width > columnWidthBy && columnWidthBy > 0) {
      width = columnWidthBy;
    }
    if(width < columnHeaderLen) {
      width = columnHeaderLen;
    }
  }
  /* Gracefull */
  width += 8;
  return width;
}

export default function QueryToolDataGrid({columns, rows, totalRowCount, dataChangeStore,
  onSelectedCellChange, selectedColumns, onSelectedColumnsChange, columnWidthBy, ...props}) {
  const classes = useStyles();
  const [readyColumns, setReadyColumns] = useState([]);
  const eventBus = useContext(QueryToolEventsContext);
  const onSelectedColumnsChangeWrapped = (arg)=>{
    props.onSelectedRowsChange(new Set());
    onSelectedColumnsChange(arg);
  };

  function handleCopy() {
    eventBus.fireEvent(QUERY_TOOL_EVENTS.TRIGGER_COPY_DATA);
  }

  const dataGridExtras = useMemo(()=>({
    onSelectedCellChange, handleCopy
  }), [onSelectedCellChange]);

  useEffect(()=>{
    let initCols = initialiseColumns(columns, rows, totalRowCount, columnWidthBy);
    setReadyColumns(formatColumns(initCols, dataChangeStore, selectedColumns, onSelectedColumnsChangeWrapped, props.rowKeyGetter, classes));
  }, [columns]);

  useEffect(()=>{
    setReadyColumns((prevCols)=>{
      return formatColumns(prevCols, dataChangeStore, selectedColumns, onSelectedColumnsChangeWrapped, props.rowKeyGetter, classes);
    });
  }, [dataChangeStore, selectedColumns]);

  return (
    <DataGridExtrasContext.Provider value={dataGridExtras}>
      <PgReactDataGrid
        id="datagrid"
        columns={readyColumns}
        rows={rows}
        headerRowHeight={40}
        rowHeight={25}
        mincolumnWidthBy={50}
        enableCellSelect={true}
        onCopy={handleCopy}
        onMultiCopy={handleCopy}
        components={{
          rowRenderer: CustomRow,
        }}
        enableRangeSelection={true}
        rangeLeftBoundaryColIdx={0}
        {...props}
      />
    </DataGridExtrasContext.Provider>
  );
}

QueryToolDataGrid.propTypes = {
  columns: PropTypes.array,
  rows: PropTypes.array,
  totalRowCount: PropTypes.number,
  dataChangeStore: PropTypes.object,
  onSelectedCellChange: PropTypes.func,
  onSelectedRowsChange: PropTypes.func,
  selectedColumns: PropTypes.objectOf(Set),
  onSelectedColumnsChange: PropTypes.func,
  rowKeyGetter: PropTypes.func,
  columnWidthBy: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};