Skip to content

feat: Add column freezing in data browser #2877

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Audio Item](#audio-item)
- [Button Item](#button-item)
- [Panel Item](#panel-item)
- [Freeze Columns](#freeze-columns)
- [Browse as User](#browse-as-user)
- [Change Pointer Key](#change-pointer-key)
- [Limitations](#limitations)
Expand Down Expand Up @@ -1154,6 +1155,12 @@ Example:
}
```

### Freeze Columns

▶️ *Core > Browser > Freeze column*

Right-click on a table column header to freeze columns from the left up to the clicked column in the data browser. When scrolling horizontally, the frozen columns remain visible while the other columns scroll underneath.

## Browse as User

▶️ *Core > Browser > Browse*
Expand Down
11 changes: 10 additions & 1 deletion src/components/BrowserCell/BrowserCell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,20 @@ export default class BrowserCell extends Component {
classes.push(styles.selected);
}

const style = { width };
if (this.props.stickyLeft !== undefined) {
style.position = 'sticky';
style.left = this.props.stickyLeft;
style.zIndex = 1;
style.background = this.props.rowBackground;
style.borderBottom = '1px solid #e3e3ea';
}

return (
<span
ref={this.cellRef}
className={classes.join(' ')}
style={{ width }}
style={style}
onClick={e => {
if (e.metaKey === true && type === 'Pointer') {
onPointerCmdClick(value);
Expand Down
19 changes: 18 additions & 1 deletion src/components/BrowserRow/BrowserRow.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export default class BrowserRow extends Component {
onMouseUpRowCheckBox,
onMouseOverRowCheckBox,
onMouseOverRow,
stickyLefts,
freezeIndex,
} = this.props;
const attributes = obj.attributes;
let requiredCols = [];
Expand All @@ -69,12 +71,25 @@ export default class BrowserRow extends Component {
} else if (obj.className === '_User' && obj.get('authData') !== undefined) {
requiredCols = ['authData'];
}
const rowBackground = row % 2 ? '#F4F5F7' : '#fdfafb';
const rowStyle = { minWidth: rowWidth };
return (
<div className={styles.tableRow} style={{ minWidth: rowWidth }} onMouseOver={() => onMouseOverRow(obj.id)}>
<div className={styles.tableRow} style={rowStyle} onMouseOver={() => onMouseOverRow(obj.id)}>
<span
className={styles.checkCell}
onMouseUp={onMouseUpRowCheckBox}
onMouseOver={() => onMouseOverRowCheckBox(obj.id)}
style={
freezeIndex >= 0
? {
position: 'sticky',
left: 0,
zIndex: 1,
background: rowBackground,
borderBottom: '1px solid #e3e3ea',
}
: {}
}
>
<input
type="checkbox"
Expand Down Expand Up @@ -133,6 +148,8 @@ export default class BrowserRow extends Component {
type={type}
readonly={isUnique || readOnlyFields.indexOf(name) > -1}
width={width}
stickyLeft={freezeIndex >= j ? stickyLefts[j] : undefined}
rowBackground={rowBackground}
current={currentCol === j}
onSelect={setCurrent}
onEditChange={setEditing}
Expand Down
51 changes: 48 additions & 3 deletions src/components/DataBrowserHeaderBar/DataBrowserHeaderBar.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import styles from 'components/DataBrowserHeaderBar/DataBrowserHeaderBar.scss';
import { DndProvider } from 'react-dnd';

export default class DataBrowserHeaderBar extends React.Component {
handleContextMenu = (index, event) => {
event.preventDefault();
const { freezeIndex, freezeColumns, unfreezeColumns, setContextMenu } = this.props;
const items =
freezeIndex >= 0 && index <= freezeIndex
? [{ text: 'Unfreeze column', callback: () => unfreezeColumns() }]
: [{ text: 'Freeze column', callback: () => freezeColumns(index) }];
setContextMenu(event.pageX, event.pageY, items);
};

render() {
const {
headers,
Expand All @@ -26,9 +36,16 @@ export default class DataBrowserHeaderBar extends React.Component {
isDataLoaded,
setSelectedObjectId,
setCurrent,
stickyLefts,
handleLefts,
freezeIndex,
} = this.props;
const elements = [
<div key="check" className={[styles.wrap, styles.check].join(' ')}>
<div
key="check"
className={[styles.wrap, styles.check].join(' ')}
style={freezeIndex >= 0 ? { position: 'sticky', left: 0, zIndex: 11 } : {}}
>
{readonly ? null : (
<input type="checkbox" checked={selected} onChange={e => selectAll(e.target.checked)} />
)}
Expand All @@ -40,6 +57,11 @@ export default class DataBrowserHeaderBar extends React.Component {
return;
}
const wrapStyle = { width };
if (freezeIndex >= 0 && typeof stickyLefts[i] !== 'undefined' && i <= freezeIndex) {
wrapStyle.position = 'sticky';
wrapStyle.left = stickyLefts[i];
wrapStyle.zIndex = 11;
}
if (i % 2) {
wrapStyle.background = '#726F85';
} else {
Expand All @@ -63,7 +85,13 @@ export default class DataBrowserHeaderBar extends React.Component {
}

elements.push(
<div onClick={onClick} key={'header' + i} className={className} style={wrapStyle}>
<div
onClick={onClick}
onContextMenu={e => this.handleContextMenu(i, e)}
key={'header' + i}
className={className}
style={wrapStyle}
>
<DataBrowserHeader
name={name}
type={type}
Expand All @@ -74,8 +102,25 @@ export default class DataBrowserHeaderBar extends React.Component {
/>
</div>
);
const handleStyle = {};
if (freezeIndex >= 0 && typeof handleLefts[i] !== 'undefined' && i <= freezeIndex) {
handleStyle.position = 'sticky';
handleStyle.left = handleLefts[i];
handleStyle.zIndex = 11;
if (i === freezeIndex) {
handleStyle.marginRight = 0;
handleStyle.width = 4;
} else {
handleStyle.background = wrapStyle.background;
}
}
elements.push(
<DragHandle key={'handle' + i} className={styles.handle} onDrag={onResize.bind(null, i)} />
<DragHandle
key={'handle' + i}
className={styles.handle}
onDrag={onResize.bind(null, i)}
style={handleStyle}
/>
);
});

Expand Down
28 changes: 28 additions & 0 deletions src/dashboard/Data/Browser/BrowserTable.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,22 @@ export default class BrowserTable extends React.Component {
preventSort,
required,
}));

const stickyLefts = [];
const handleLefts = [];
if (
typeof this.props.freezeIndex === 'number' &&
this.props.freezeIndex >= 0
) {
let left = 30;
headers.forEach((h, i) => {
stickyLefts[i] = left;
handleLefts[i] = left + h.width;
if (h.visible) {
left += h.width;
}
});
}
let editor = null;
let table = <div ref={this.tableRef} />;
if (this.props.data) {
Expand Down Expand Up @@ -170,6 +186,8 @@ export default class BrowserTable extends React.Component {
callCloudFunction={this.props.callCloudFunction}
isPanelVisible={this.props.isPanelVisible}
setContextMenu={this.props.setContextMenu}
stickyLefts={stickyLefts}
freezeIndex={this.props.freezeIndex}
onEditSelectedRow={this.props.onEditSelectedRow}
markRequiredFieldRow={this.props.markRequiredFieldRow}
showNote={this.props.showNote}
Expand Down Expand Up @@ -251,6 +269,8 @@ export default class BrowserTable extends React.Component {
callCloudFunction={this.props.callCloudFunction}
isPanelVisible={this.props.isPanelVisible}
setContextMenu={this.props.setContextMenu}
stickyLefts={stickyLefts}
freezeIndex={this.props.freezeIndex}
onEditSelectedRow={this.props.onEditSelectedRow}
markRequiredFieldRow={this.props.markRequiredFieldRow}
showNote={this.props.showNote}
Expand Down Expand Up @@ -342,6 +362,8 @@ export default class BrowserTable extends React.Component {
setSelectedObjectId={this.props.setSelectedObjectId}
isPanelVisible={this.props.isPanelVisible}
setContextMenu={this.props.setContextMenu}
stickyLefts={stickyLefts}
freezeIndex={this.props.freezeIndex}
onEditSelectedRow={this.props.onEditSelectedRow}
showNote={this.props.showNote}
onRefresh={this.props.onRefresh}
Expand Down Expand Up @@ -554,6 +576,11 @@ export default class BrowserTable extends React.Component {
this.props.data.forEach(({ id }) => this.props.selectRow(id, checked))
}
headers={headers}
stickyLefts={stickyLefts}
handleLefts={handleLefts}
freezeIndex={this.props.freezeIndex}
freezeColumns={this.props.freezeColumns}
unfreezeColumns={this.props.unfreezeColumns}
updateOrdering={this.props.updateOrdering}
readonly={!!this.props.relation || !!this.props.isUnique}
handleDragDrop={this.props.handleHeaderDragDrop}
Expand All @@ -563,6 +590,7 @@ export default class BrowserTable extends React.Component {
isDataLoaded={!!this.props.data}
setSelectedObjectId={this.props.setSelectedObjectId}
setCurrent={this.props.setCurrent}
setContextMenu={this.props.setContextMenu}
/>
{table}
</div>
Expand Down
17 changes: 16 additions & 1 deletion src/dashboard/Data/Browser/DataBrowser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default class DataBrowser extends React.Component {
isResizing: false,
maxWidth: window.innerWidth - 300,
showAggregatedData: true,
frozenColumnIndex: -1,
};

this.handleResizeDiv = this.handleResizeDiv.bind(this);
Expand All @@ -66,6 +67,8 @@ export default class DataBrowser extends React.Component {
this.setCopyableValue = this.setCopyableValue.bind(this);
this.setSelectedObjectId = this.setSelectedObjectId.bind(this);
this.setContextMenu = this.setContextMenu.bind(this);
this.freezeColumns = this.freezeColumns.bind(this);
this.unfreezeColumns = this.unfreezeColumns.bind(this);
this.handleCellClick = this.handleCellClick.bind(this);
this.saveOrderTimeout = null;
}
Expand All @@ -88,6 +91,7 @@ export default class DataBrowser extends React.Component {
selectedCells: { list: new Set(), rowStart: -1, rowEnd: -1, colStart: -1, colEnd: -1 },
firstSelectedCell: null,
selectedData: [],
frozenColumnIndex: -1,
});
} else if (
Object.keys(props.columns).length !== Object.keys(this.props.columns).length ||
Expand All @@ -100,7 +104,7 @@ export default class DataBrowser extends React.Component {
props.className,
columnPreferences[props.className]
);
this.setState({ order });
this.setState({ order, frozenColumnIndex: -1 });
}
if (props && props.className) {
if (
Expand Down Expand Up @@ -539,6 +543,14 @@ export default class DataBrowser extends React.Component {
this.setState({ contextMenuX, contextMenuY, contextMenuItems });
}

freezeColumns(index) {
this.setState({ frozenColumnIndex: index });
}

unfreezeColumns() {
this.setState({ frozenColumnIndex: -1 });
}

handleColumnsOrder(order, shouldReload) {
this.setState({ order: [...order] }, () => {
this.updatePreferences(order, shouldReload);
Expand Down Expand Up @@ -643,6 +655,9 @@ export default class DataBrowser extends React.Component {
setSelectedObjectId={this.setSelectedObjectId}
callCloudFunction={this.props.callCloudFunction}
setContextMenu={this.setContextMenu}
freezeIndex={this.state.frozenColumnIndex}
freezeColumns={this.freezeColumns}
unfreezeColumns={this.unfreezeColumns}
onFilterChange={this.props.onFilterChange}
onFilterSave={this.props.onFilterSave}
selectedCells={this.state.selectedCells}
Expand Down
Loading