????
Current Path : C:/opt/pgsql/pgAdmin 4/web/pgadmin/static/js/components/ |
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; }