mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-19 01:15:04 -05:00
Restructured these modules for ease of maintenance and apply the single responsibility principle (wherever applicable). * SchemaView - Split the code based on the functionality and responsibility. - Introduced a new View 'InlineView' instead of using the 'nextInline' configuration of the fields to have a better, and manageable view. - Using the separate class 'SchemaState' for managing the data and states of the SchemaView (separated from the 'useSchemaState' custom hook). - Introduced three new custom hooks 'useFieldValue', 'useFieldOptions', 'useFieldError' for the individual control to use for each Schema Field. - Don't pass value as the parameter props, and let the 'useFieldValue' and other custom hooks to decide, whether to rerender the control itself or the whole dialog/view. (single responsibility principle) - Introduced a new data store with a subscription facility. - Moving the field metadata (option) evaluation to a separate place for better management, and each option can be defined for a particular kind of field (for example - collection, row, cell, general, etc). - Allow to provide custom control for all kind of Schema field. * DataGridView - Same as SchemaView, split the DataGridView call into smaller, manageable chunks. (For example - grid, row, mappedCell, etc). - Use context based approach for providing the row and table data instead of passing them as parameters to every component separately. - Have a facility to extend this feature separately in future. (for example - selectable cell, column grouping, etc.) - Separated the features like deletable, editable, reorder, expandable etc. cells using the above feature support. - Added ability to provide the CustomHeader, and CustomRow through the Schema field, which will extend the ability to customize better. - Removed the 'DataGridViewWithHeaderForm' as it has been achieved through providing 'CustomHeader', and also introduced 'DataGridFormHeader' (a custom header) to achieve the same feature as 'DataGridViewWithHeaderForm'.
475 lines
13 KiB
React
475 lines
13 KiB
React
/////////////////////////////////////////////////////////////
|
|
//
|
|
// pgAdmin 4 - PostgreSQL Tools
|
|
//
|
|
// Copyright (C) 2013 - 2024, The pgAdmin Development Team
|
|
// This software is released under the PostgreSQL Licence
|
|
//
|
|
//////////////////////////////////////////////////////////////
|
|
|
|
import React, { forwardRef, useEffect } from 'react';
|
|
import { flexRender } from '@tanstack/react-table';
|
|
import { styled } from '@mui/material/styles';
|
|
import PropTypes from 'prop-types';
|
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
|
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
|
|
import { PgIconButton } from './Buttons';
|
|
import CustomPropTypes from '../custom_prop_types';
|
|
import { InputSwitch } from './FormComponents';
|
|
import { Checkbox } from '@mui/material';
|
|
|
|
|
|
const StyledDiv = styled('div')(({theme})=>({
|
|
'&.pgrt': {
|
|
display: 'grid',
|
|
overflow: 'auto',
|
|
position: 'relative',
|
|
flexGrow: 1,
|
|
},
|
|
|
|
// by default the table has no outer border.
|
|
// the parent container has to take care of border.
|
|
'& .pgrt-table': {
|
|
borderSpacing: 0,
|
|
borderRadius: theme.shape.borderRadius,
|
|
display: 'grid',
|
|
gridAutoRows: 'max-content',
|
|
flexGrow: 1,
|
|
flexDirection: 'column',
|
|
|
|
'& .pgrt-header': {
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 1,
|
|
|
|
'& .pgrt-header-row': {
|
|
height: '34px',
|
|
display: 'flex',
|
|
|
|
'& .pgrt-header-cell': {
|
|
position: 'relative',
|
|
fontWeight: theme.typography.fontWeightBold,
|
|
padding: theme.spacing(0.5),
|
|
textAlign: 'left',
|
|
alignContent: 'center',
|
|
backgroundColor: theme.otherVars.tableBg,
|
|
overflow: 'hidden',
|
|
...theme.mixins.panelBorder.bottom,
|
|
...theme.mixins.panelBorder.right,
|
|
|
|
'& > div': {
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
textWrap: 'nowrap'
|
|
},
|
|
|
|
'& .pgrt-header-resizer': {
|
|
display: 'inline-block',
|
|
width: '5px',
|
|
height: '100%',
|
|
position: 'absolute',
|
|
right: 0,
|
|
top: 0,
|
|
transform: 'translateX(50%)',
|
|
zIndex: 1,
|
|
cursor: 'col-resize',
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
'& .pgrt-body': {
|
|
position: 'relative',
|
|
flexGrow: 1,
|
|
minHeight: 0,
|
|
|
|
'& .pgrt-row': {
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
position: 'absolute',
|
|
width: '100%',
|
|
|
|
'& .pgrt-row-content': {
|
|
display: 'flex',
|
|
minHeight: 0,
|
|
|
|
'& .pgrd-row-cell': {
|
|
margin: 0,
|
|
padding: theme.spacing(0.25, 0.5),
|
|
...theme.mixins.panelBorder.bottom,
|
|
...theme.mixins.panelBorder.right,
|
|
position: 'relative',
|
|
height: '30px',
|
|
display: 'flex',
|
|
alignItems: 'flex-start',
|
|
backgroundColor: theme.otherVars.tableBg,
|
|
|
|
'&.btn-cell': {
|
|
textAlign: 'center',
|
|
},
|
|
'&.expanded-icon-cell': {
|
|
backgroundColor: theme.palette.grey[400],
|
|
borderBottom: 'none',
|
|
},
|
|
'&.row-warning': {
|
|
backgroundColor: theme.palette.warning.main + '!important'
|
|
},
|
|
'&.row-alert': {
|
|
backgroundColor: theme.palette.error.main + '!important'
|
|
},
|
|
'&.cell-with-icon': {
|
|
paddingLeft: '1.8em',
|
|
borderRadius: 0,
|
|
backgroundPosition: '1%',
|
|
},
|
|
|
|
'& .pgrd-row-cell-content': {
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
userSelect: 'text',
|
|
width: '100%',
|
|
},
|
|
|
|
'& .reorder-cell': {
|
|
cursor: 'move',
|
|
padding: '4px 2px',
|
|
},
|
|
'& .pgrt-cell-button': {
|
|
border: 0,
|
|
borderRadius: 0,
|
|
padding: 0,
|
|
minWidth: 0,
|
|
backgroundColor: 'inherit',
|
|
'&.Mui-disabled': {
|
|
border: 0,
|
|
},
|
|
}
|
|
}
|
|
},
|
|
|
|
'& .pgrt-expanded-content': {
|
|
...theme.mixins.panelBorder.all,
|
|
margin: '8px',
|
|
flexGrow: 1,
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}));
|
|
|
|
export const PgReactTableCell = forwardRef(({row, cell, children, className}, ref)=>{
|
|
let classNames = ['pgrd-row-cell'];
|
|
if (typeof (cell.column.id) == 'string' && cell.column.id.startsWith('btn-')) {
|
|
classNames.push('btn-cell');
|
|
}
|
|
if (cell.column.id == 'btn-edit' && row.getIsExpanded()) {
|
|
classNames.push('expanded-icon-cell');
|
|
}
|
|
if (row.original.row_type === 'warning') {
|
|
classNames.push('row-warning');
|
|
}
|
|
if (row.original.row_type === 'alert') {
|
|
classNames.push('row-alert');
|
|
}
|
|
if(row.original.icon?.[cell.column.id]) {
|
|
classNames.push(row.original.icon[cell.column.id], 'cell-with-icon');
|
|
}
|
|
if(cell.column.columnDef.dataClassName){
|
|
classNames.push(cell.column.columnDef.dataClassName);
|
|
}
|
|
|
|
classNames.push(className);
|
|
|
|
return (
|
|
<div ref={ref} key={cell.id} style={{
|
|
flex: `var(--col-${cell.column.id.replace(/\W/g, '_')}-size) 0 auto`,
|
|
width: `calc(var(--col-${cell.column.id.replace(/\W/g, '_')}-size)*1px)`,
|
|
...(cell.column.columnDef.maxSize ? { maxWidth: `${cell.column.columnDef.maxSize}px` } : {})
|
|
}}
|
|
className={classNames.join(' ')}
|
|
title={String(cell.getValue() ?? '')}>
|
|
<div className='pgrd-row-cell-content'>{children}</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
PgReactTableCell.displayName = 'PgReactTableCell';
|
|
PgReactTableCell.propTypes = {
|
|
row: PropTypes.object,
|
|
cell: PropTypes.object,
|
|
children: CustomPropTypes.children,
|
|
className: PropTypes.any,
|
|
};
|
|
|
|
export const PgReactTableRow = forwardRef(({ children, className, ...props }, ref)=>{
|
|
return (
|
|
<div className={['pgrt-row', className].join(' ')} ref={ref} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
});
|
|
PgReactTableRow.displayName = 'PgReactTableRow';
|
|
PgReactTableRow.propTypes = {
|
|
children: CustomPropTypes.children,
|
|
className: PropTypes.any,
|
|
};
|
|
|
|
export const PgReactTableRowContent = forwardRef(({children, className, ...props}, ref)=>{
|
|
return (
|
|
<div className={['pgrt-row-content', className].join(' ')} ref={ref} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
});
|
|
PgReactTableRowContent.displayName = 'PgReactTableRowContent';
|
|
PgReactTableRowContent.propTypes = {
|
|
children: CustomPropTypes.children,
|
|
className: PropTypes.any,
|
|
};
|
|
|
|
|
|
export function PgReactTableRowExpandContent({row, children}) {
|
|
if(!row.getIsExpanded()) {
|
|
return <></>;
|
|
}
|
|
return (
|
|
<div className='pgrt-expanded-content' style={{ maxWidth: 'calc(var(--expand-width)*1px)' }}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
PgReactTableRowExpandContent.propTypes = {
|
|
row: PropTypes.object,
|
|
children: CustomPropTypes.children,
|
|
};
|
|
|
|
export function PgReactTableHeader({table}) {
|
|
return (
|
|
<div className='pgrt-header'>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<div key={headerGroup.id} className='pgrt-header-row' style={{ }}>
|
|
{headerGroup.headers.map((header) => (
|
|
<div
|
|
key={header.id}
|
|
className='pgrt-header-cell'
|
|
style={{
|
|
flex: `var(--header-${header?.id.replace(/\W/g, '_')}-size) 0 auto`,
|
|
width: `calc(var(--header-${header?.id.replace(/\W/g, '_')}-size)*1px)`,
|
|
...(header.column.columnDef.maxSize ? { maxWidth: `${header.column.columnDef.maxSize}px` } : {}),
|
|
}}
|
|
>
|
|
<div title={flexRender(header.column.columnDef.header, header.getContext())}
|
|
style={{cursor: header.column.getCanSort() ? 'pointer' : 'initial'}}
|
|
onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined}
|
|
>
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
{header.column.getCanSort() && header.column.getIsSorted() &&
|
|
<span>
|
|
{header.column.getIsSorted() == 'desc' ?
|
|
<KeyboardArrowDownIcon style={{ fontSize: '1.2rem' }} />
|
|
: <KeyboardArrowUpIcon style={{ fontSize: '1.2rem' }} />}
|
|
</span>}
|
|
</div>
|
|
{header.column.getCanResize() && (
|
|
<div
|
|
onDoubleClick={() => header.column.resetSize()}
|
|
onMouseDown={header.getResizeHandler()}
|
|
onTouchStart={header.getResizeHandler()}
|
|
className='pgrt-header-resizer'
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
PgReactTableHeader.propTypes = {
|
|
table: PropTypes.object,
|
|
};
|
|
|
|
export function PgReactTableBody({children, style}) {
|
|
return (
|
|
<div className='pgrt-body' style={style}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
PgReactTableBody.propTypes = {
|
|
style: PropTypes.object,
|
|
children: CustomPropTypes.children,
|
|
};
|
|
|
|
export const PgReactTable = forwardRef(({children, table, rootClassName, tableClassName, onScrollFunc, ...props}, ref)=>{
|
|
const columns = table.getAllColumns();
|
|
|
|
useEffect(()=>{
|
|
const setMaxExpandWidth = ()=>{
|
|
if(ref.current) {
|
|
ref.current.style['--expand-width'] = (ref.current.getBoundingClientRect().width ?? 430) - 30; //margin,scrollbar,etc.
|
|
}
|
|
};
|
|
const tableResizeObserver = new ResizeObserver(()=>{
|
|
setMaxExpandWidth();
|
|
});
|
|
tableResizeObserver.observe(ref.current);
|
|
}, []);
|
|
|
|
const columnSizeVars = React.useMemo(() => {
|
|
const headers = table.getFlatHeaders();
|
|
const colSizes = {};
|
|
for (let value of headers) {
|
|
const header = value;
|
|
colSizes[`--header-${header.id.replace(/\W/g, '_')}-size`] = header.getSize();
|
|
colSizes[`--col-${header.column.id.replace(/\W/g, '_')}-size`] = header.column.getSize();
|
|
}
|
|
return colSizes;
|
|
}, [columns, table.getState().columnSizingInfo]);
|
|
|
|
return (
|
|
<StyledDiv className={['pgrt', rootClassName].join(' ')} ref={ref} onScroll={e => onScrollFunc?.(e.target)}>
|
|
<div className={['pgrt-table', tableClassName].join(' ')} style={{ ...columnSizeVars }} {...props}>
|
|
{children}
|
|
</div>
|
|
</StyledDiv>
|
|
);
|
|
});
|
|
PgReactTable.displayName = 'PgReactTable';
|
|
PgReactTable.propTypes = {
|
|
table: PropTypes.object,
|
|
rootClassName: PropTypes.any,
|
|
tableClassName: PropTypes.any,
|
|
children: CustomPropTypes.children,
|
|
onScrollFunc: PropTypes.any,
|
|
};
|
|
|
|
export function getExpandCell({ onClick, title }) {
|
|
const Cell = ({ row }) => {
|
|
const onClickFinal = (e) => {
|
|
e.preventDefault();
|
|
row.toggleExpanded();
|
|
onClick?.(row, e);
|
|
};
|
|
return (
|
|
<PgIconButton
|
|
size="xs"
|
|
icon={
|
|
row.getIsExpanded() ? (
|
|
<KeyboardArrowDownIcon />
|
|
) : (
|
|
<ChevronRightIcon />
|
|
)
|
|
}
|
|
noBorder
|
|
onClick={onClickFinal}
|
|
aria-label={title}
|
|
/>
|
|
);
|
|
};
|
|
|
|
Cell.displayName = 'ExpandCell';
|
|
Cell.propTypes = {
|
|
row: PropTypes.any,
|
|
};
|
|
|
|
return Cell;
|
|
}
|
|
|
|
export function getSwitchCell() {
|
|
const Cell = ({ getValue }) => {
|
|
return <InputSwitch value={getValue()} readonly />;
|
|
};
|
|
|
|
Cell.displayName = 'SwitchCell';
|
|
Cell.propTypes = {
|
|
getValue: PropTypes.func,
|
|
};
|
|
|
|
return Cell;
|
|
}
|
|
|
|
export function getCheckboxCell({title}) {
|
|
const Cell = ({ table }) => {
|
|
return (
|
|
<div style={{textAlign: 'center', minWidth: 20}}>
|
|
<Checkbox
|
|
color="primary"
|
|
checked={table.getIsAllRowsSelected()}
|
|
indeterminate={table.getIsSomeRowsSelected()}
|
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
inputProps={{ 'aria-label': title }}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Cell.displayName = 'CheckboxCell';
|
|
Cell.propTypes = {
|
|
table: PropTypes.object,
|
|
};
|
|
|
|
return Cell;
|
|
}
|
|
|
|
export function getCheckboxHeaderCell({title}) {
|
|
const Cell = ({ row }) => {
|
|
return (
|
|
<div style={{textAlign: 'center', minWidth: 20}}>
|
|
<Checkbox
|
|
color="primary"
|
|
checked={row.getIsSelected()}
|
|
indeterminate={row.getIsSomeSelected()}
|
|
disabled={!row.getCanSelect()}
|
|
onChange={row.getToggleSelectedHandler()}
|
|
inputProps={{ 'aria-label': title }}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
Cell.displayName = 'CheckboxHeaderCell';
|
|
Cell.propTypes = {
|
|
row: PropTypes.object,
|
|
};
|
|
|
|
return Cell;
|
|
}
|
|
|
|
export function getEditCell({isDisabled, title}) {
|
|
const Cell = ({ row }) => {
|
|
return <PgIconButton data-test="expand-row" title={title} icon={<EditRoundedIcon fontSize="small" />} className='pgrt-cell-button'
|
|
onClick={()=>{
|
|
row.toggleExpanded();
|
|
}} disabled={isDisabled?.(row)}
|
|
/>;
|
|
};
|
|
|
|
Cell.displayName = 'EditCell';
|
|
Cell.propTypes = {
|
|
row: PropTypes.any,
|
|
};
|
|
|
|
return Cell;
|
|
}
|
|
|
|
export function getDeleteCell({isDisabled, title, onClick}) {
|
|
const Cell = ({ row }) => (
|
|
<PgIconButton data-test="delete-row" title={title} icon={<DeleteRoundedIcon fontSize="small" />}
|
|
onClick={()=>onClick?.(row)}
|
|
className='pgrt-cell-button' disabled={isDisabled?.(row)}
|
|
/>
|
|
);
|
|
|
|
Cell.displayName = 'DeleteCell';
|
|
Cell.propTypes = {
|
|
row: PropTypes.any,
|
|
};
|
|
|
|
return Cell;
|
|
}
|