Skip to content

Commit 859a011

Browse files
douglasmuraokadavimacedo
authored andcommitted
refactor(Database Browser): Table performance improvements (#1241)
* fix(Database Browser): Avoid unnecessary rendering during navigation * fix(Database Browser): Improve scrolling UX * Improve scroll handler readability * fix(Database Browser): Improve initial rendering * fix: Assign const * fix: Scroll not triggering data fetch
1 parent 7158765 commit 859a011

File tree

6 files changed

+330
-217
lines changed

6 files changed

+330
-217
lines changed

src/components/BrowserCell/BrowserCell.react.js

+132-109
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,20 @@ import { dateStringUTC } from 'lib/DateUtils';
99
import getFileName from 'lib/getFileName';
1010
import Parse from 'parse';
1111
import Pill from 'components/Pill/Pill.react';
12-
import React, { useEffect, useRef }
13-
from 'react';
12+
import React, { Component } from 'react';
1413
import styles from 'components/BrowserCell/BrowserCell.scss';
1514
import { unselectable } from 'stylesheets/base.scss';
1615

17-
let BrowserCell = ({ type, value, hidden, width, current, onSelect, onEditChange, setRelation, onPointerClick }) => {
18-
const cellRef = current ? useRef() : null;
19-
if (current) {
20-
useEffect(() => {
21-
const node = cellRef.current;
16+
export default class BrowserCell extends Component {
17+
constructor() {
18+
super();
19+
20+
this.cellRef = React.createRef();
21+
}
22+
23+
componentDidUpdate() {
24+
if (this.props.current) {
25+
const node = this.cellRef.current;
2226
const { left, right, bottom, top } = node.getBoundingClientRect();
2327

2428
// Takes into consideration Sidebar width when over 980px wide.
@@ -28,118 +32,137 @@ let BrowserCell = ({ type, value, hidden, width, current, onSelect, onEditChange
2832
const topBoundary = 126;
2933

3034
if (left < leftBoundary || right > window.innerWidth) {
31-
node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
35+
node.scrollIntoView({ block: 'nearest', inline: 'start' });
3236
} else if (top < topBoundary || bottom > window.innerHeight) {
33-
node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
37+
node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
3438
}
35-
});
39+
}
3640
}
3741

38-
let content = value;
39-
let classes = [styles.cell, unselectable];
40-
if (hidden) {
41-
content = '(hidden)';
42-
classes.push(styles.empty);
43-
} else if (value === undefined) {
44-
if (type === 'ACL') {
45-
content = 'Public Read + Write';
46-
} else {
47-
content = '(undefined)';
48-
classes.push(styles.empty);
49-
}
50-
} else if (value === null) {
51-
content = '(null)';
52-
classes.push(styles.empty);
53-
} else if (value === '') {
54-
content = <span>&nbsp;</span>;
55-
classes.push(styles.empty);
56-
} else if (type === 'Pointer') {
57-
if (value && value.__type) {
58-
const object = new Parse.Object(value.className);
59-
object.id = value.objectId;
60-
value = object;
42+
shouldComponentUpdate(nextProps) {
43+
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
44+
.filter(propName => propName !== 'value');
45+
if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
46+
return true;
6147
}
62-
content = (
63-
<a href='javascript:;' onClick={onPointerClick.bind(undefined, value)}>
64-
<Pill value={value.id} />
65-
</a>
66-
);
67-
} else if (type === 'Date') {
68-
if (typeof value === 'object' && value.__type) {
69-
value = new Date(value.iso);
70-
} else if (typeof value === 'string') {
71-
value = new Date(value);
48+
const { value } = this.props;
49+
const { value: nextValue } = nextProps;
50+
if (typeof value !== typeof nextValue) {
51+
return true;
7252
}
73-
content = dateStringUTC(value);
74-
} else if (type === 'Boolean') {
75-
content = value ? 'True' : 'False';
76-
} else if (type === 'Object' || type === 'Bytes' || type === 'Array') {
77-
content = JSON.stringify(value);
78-
} else if (type === 'File') {
79-
if (value.url()) {
80-
content = <Pill value={getFileName(value)} />;
81-
} else {
82-
content = <Pill value={'Uploading\u2026'} />;
53+
const isRefDifferent = value !== nextValue;
54+
if (isRefDifferent && typeof value === 'object') {
55+
return JSON.stringify(value) !== JSON.stringify(nextValue);
8356
}
84-
} else if (type === 'ACL') {
85-
let pieces = [];
86-
let json = value.toJSON();
87-
if (Object.prototype.hasOwnProperty.call(json, '*')) {
88-
if (json['*'].read && json['*'].write) {
89-
pieces.push('Public Read + Write');
90-
} else if (json['*'].read) {
91-
pieces.push('Public Read');
92-
} else if (json['*'].write) {
93-
pieces.push('Public Write');
57+
return isRefDifferent;
58+
}
59+
60+
render() {
61+
let { type, value, hidden, width, current, onSelect, onEditChange, setRelation, onPointerClick, row, col } = this.props;
62+
let content = value;
63+
let classes = [styles.cell, unselectable];
64+
if (hidden) {
65+
content = '(hidden)';
66+
classes.push(styles.empty);
67+
} else if (value === undefined) {
68+
if (type === 'ACL') {
69+
content = 'Public Read + Write';
70+
} else {
71+
content = '(undefined)';
72+
classes.push(styles.empty);
9473
}
95-
}
96-
for (let role in json) {
97-
if (role !== '*') {
98-
pieces.push(role);
74+
} else if (value === null) {
75+
content = '(null)';
76+
classes.push(styles.empty);
77+
} else if (value === '') {
78+
content = <span>&nbsp;</span>;
79+
classes.push(styles.empty);
80+
} else if (type === 'Pointer') {
81+
if (value && value.__type) {
82+
const object = new Parse.Object(value.className);
83+
object.id = value.objectId;
84+
value = object;
85+
}
86+
content = (
87+
<a href='javascript:;' onClick={onPointerClick.bind(undefined, value)}>
88+
<Pill value={value.id} />
89+
</a>
90+
);
91+
} else if (type === 'Date') {
92+
if (typeof value === 'object' && value.__type) {
93+
value = new Date(value.iso);
94+
} else if (typeof value === 'string') {
95+
value = new Date(value);
9996
}
97+
content = dateStringUTC(value);
98+
} else if (type === 'Boolean') {
99+
content = value ? 'True' : 'False';
100+
} else if (type === 'Object' || type === 'Bytes' || type === 'Array') {
101+
content = JSON.stringify(value);
102+
} else if (type === 'File') {
103+
if (value.url()) {
104+
content = <Pill value={getFileName(value)} />;
105+
} else {
106+
content = <Pill value={'Uploading\u2026'} />;
107+
}
108+
} else if (type === 'ACL') {
109+
let pieces = [];
110+
let json = value.toJSON();
111+
if (Object.prototype.hasOwnProperty.call(json, '*')) {
112+
if (json['*'].read && json['*'].write) {
113+
pieces.push('Public Read + Write');
114+
} else if (json['*'].read) {
115+
pieces.push('Public Read');
116+
} else if (json['*'].write) {
117+
pieces.push('Public Write');
118+
}
119+
}
120+
for (let role in json) {
121+
if (role !== '*') {
122+
pieces.push(role);
123+
}
124+
}
125+
if (pieces.length === 0) {
126+
pieces.push('Master Key Only');
127+
}
128+
content = pieces.join(', ');
129+
} else if (type === 'GeoPoint') {
130+
content = `(${value.latitude}, ${value.longitude})`;
131+
} else if (type === 'Polygon') {
132+
content = value.coordinates.map(coord => `(${coord})`)
133+
} else if (type === 'Relation') {
134+
content = (
135+
<div style={{ textAlign: 'center', cursor: 'pointer' }}>
136+
<Pill onClick={() => setRelation(value)} value='View relation' />
137+
</div>
138+
);
100139
}
101-
if (pieces.length === 0) {
102-
pieces.push('Master Key Only');
140+
141+
if (current) {
142+
classes.push(styles.current);
103143
}
104-
content = pieces.join(', ');
105-
} else if (type === 'GeoPoint') {
106-
content = `(${value.latitude}, ${value.longitude})`;
107-
} else if (type === 'Polygon') {
108-
content = value.coordinates.map(coord => `(${coord})`)
109-
} else if (type === 'Relation') {
110-
content = (
111-
<div style={{ textAlign: 'center', cursor: 'pointer' }}>
112-
<Pill onClick={() => setRelation(value)} value='View relation' />
113-
</div>
144+
return (
145+
<span
146+
ref={this.cellRef}
147+
className={classes.join(' ')}
148+
style={{ width }}
149+
onClick={() => onSelect({ row, col })}
150+
onDoubleClick={() => {
151+
if (type !== 'Relation') {
152+
onEditChange(true)
153+
}
154+
}}
155+
onTouchEnd={e => {
156+
if (current && type !== 'Relation') {
157+
// The touch event may trigger an unwanted change in the column value
158+
if (['ACL', 'Boolean', 'File'].includes(type)) {
159+
e.preventDefault();
160+
}
161+
onEditChange(true);
162+
}
163+
}}>
164+
{content}
165+
</span>
114166
);
115167
}
116-
117-
if (current) {
118-
classes.push(styles.current);
119-
}
120-
return (
121-
<span
122-
ref={cellRef}
123-
className={classes.join(' ')}
124-
style={{ width }}
125-
onClick={onSelect}
126-
onDoubleClick={() => {
127-
if (type !== 'Relation') {
128-
onEditChange(true)
129-
}
130-
}}
131-
onTouchEnd={e => {
132-
if (current && type !== 'Relation') {
133-
// The touch event may trigger an unwanted change in the column value
134-
if (['ACL', 'Boolean', 'File'].includes(type)) {
135-
e.preventDefault();
136-
}
137-
onEditChange(true);
138-
}
139-
}}>
140-
{content}
141-
</span>
142-
);
143-
};
144-
145-
export default BrowserCell;
168+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Parse from 'parse';
2+
import React, { Component } from 'react';
3+
4+
import BrowserCell from 'components/BrowserCell/BrowserCell.react';
5+
import styles from 'dashboard/Data/Browser/Browser.scss';
6+
7+
export default class BrowserRow extends Component {
8+
shouldComponentUpdate(nextProps) {
9+
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
10+
.filter(propName => propName !== 'obj');
11+
if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
12+
return true;
13+
}
14+
const { obj } = this.props;
15+
const { obj: nextObj } = nextProps;
16+
const isRefDifferent = obj !== nextObj;
17+
return isRefDifferent ? JSON.stringify(obj) !== JSON.stringify(nextObj) : isRefDifferent;
18+
}
19+
20+
render() {
21+
const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCurrent, setEditing, setRelation } = this.props;
22+
let attributes = obj.attributes;
23+
return (
24+
<div className={styles.tableRow} style={{ minWidth: rowWidth }}>
25+
<span className={styles.checkCell}>
26+
<input
27+
type='checkbox'
28+
checked={selection['*'] || selection[obj.id]}
29+
onChange={e => selectRow(obj.id, e.target.checked)} />
30+
</span>
31+
{order.map(({ name, width, visible }, j) => {
32+
if (!visible) return null;
33+
let type = columns[name].type;
34+
let attr = obj;
35+
if (!isUnique) {
36+
attr = attributes[name];
37+
if (name === 'objectId') {
38+
attr = obj.id;
39+
} else if (name === 'ACL' && className === '_User' && !attr) {
40+
attr = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
41+
} else if (type === 'Relation' && !attr && obj.id) {
42+
attr = new Parse.Relation(obj, name);
43+
attr.targetClassName = columns[name].targetClass;
44+
} else if (type === 'Array' || type === 'Object') {
45+
// This is needed to avoid unwanted conversions of objects to Parse.Objects.
46+
// "Parse._encoding" is responsible to convert Parse data into raw data.
47+
// Since array and object are generic types, we want to render them the way
48+
// they were stored in the database.
49+
attr = Parse._encode(obj.get(name));
50+
}
51+
}
52+
let hidden = false;
53+
if (name === 'password' && className === '_User') {
54+
hidden = true;
55+
} else if (name === 'sessionToken') {
56+
if (className === '_User' || className === '_Session') {
57+
hidden = true;
58+
}
59+
}
60+
return (
61+
<BrowserCell
62+
key={name}
63+
row={row}
64+
col={j}
65+
type={type}
66+
readonly={isUnique || readOnlyFields.indexOf(name) > -1}
67+
width={width}
68+
current={currentCol === j}
69+
onSelect={setCurrent}
70+
onEditChange={setEditing}
71+
onPointerClick={onPointerClick}
72+
setRelation={setRelation}
73+
value={attr}
74+
hidden={hidden} />
75+
);
76+
})}
77+
</div>
78+
);
79+
}
80+
}

src/dashboard/Data/Browser/Browser.react.js

+6-21
Original file line numberDiff line numberDiff line change
@@ -910,34 +910,20 @@ class Browser extends DashboardView {
910910
</div>
911911
);
912912
} else if (className && classes.get(className)) {
913-
let schema = {};
914-
classes.get(className).forEach(({ type, targetClass }, col) => {
915-
schema[col] = {
916-
type,
917-
targetClass,
918-
};
919-
});
920913

921914
let columns = {
922915
objectId: { type: 'String' }
923916
};
924917
if (this.state.isUnique) {
925918
columns = {};
926919
}
927-
let userPointers = [];
928-
classes.get(className).forEach((field, name) => {
929-
if (name === 'objectId') {
930-
return;
931-
}
932-
if (this.state.isUnique && name !== this.state.uniqueField) {
920+
classes.get(className).forEach(({ type, targetClass }, name) => {
921+
if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) {
933922
return;
934923
}
935-
let info = { type: field.type };
936-
if (field.targetClass) {
937-
info.targetClass = field.targetClass;
938-
if (field.targetClass === '_User') {
939-
userPointers.push(name);
940-
}
924+
const info = { type };
925+
if (targetClass) {
926+
info.targetClass = targetClass;
941927
}
942928
columns[name] = info;
943929
});
@@ -958,8 +944,7 @@ class Browser extends DashboardView {
958944
uniqueField={this.state.uniqueField}
959945
count={count}
960946
perms={this.state.clp[className]}
961-
schema={schema}
962-
userPointers={userPointers}
947+
schema={this.props.schema}
963948
filters={this.state.filters}
964949
onFilterChange={this.updateFilters}
965950
onRemoveColumn={this.showRemoveColumn}

0 commit comments

Comments
 (0)