Skip to content

refactor(Database Browser): Table performance improvements #1241

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
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
241 changes: 132 additions & 109 deletions src/components/BrowserCell/BrowserCell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ import { dateStringUTC } from 'lib/DateUtils';
import getFileName from 'lib/getFileName';
import Parse from 'parse';
import Pill from 'components/Pill/Pill.react';
import React, { useEffect, useRef }
from 'react';
import React, { Component } from 'react';
import styles from 'components/BrowserCell/BrowserCell.scss';
import { unselectable } from 'stylesheets/base.scss';

let BrowserCell = ({ type, value, hidden, width, current, onSelect, onEditChange, setRelation, onPointerClick }) => {
const cellRef = current ? useRef() : null;
if (current) {
useEffect(() => {
const node = cellRef.current;
export default class BrowserCell extends Component {
constructor() {
super();

this.cellRef = React.createRef();
}

componentDidUpdate() {
if (this.props.current) {
const node = this.cellRef.current;
const { left, right, bottom, top } = node.getBoundingClientRect();

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

if (left < leftBoundary || right > window.innerWidth) {
node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
node.scrollIntoView({ block: 'nearest', inline: 'start' });
} else if (top < topBoundary || bottom > window.innerHeight) {
node.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
});
}
}

let content = value;
let classes = [styles.cell, unselectable];
if (hidden) {
content = '(hidden)';
classes.push(styles.empty);
} else if (value === undefined) {
if (type === 'ACL') {
content = 'Public Read + Write';
} else {
content = '(undefined)';
classes.push(styles.empty);
}
} else if (value === null) {
content = '(null)';
classes.push(styles.empty);
} else if (value === '') {
content = <span>&nbsp;</span>;
classes.push(styles.empty);
} else if (type === 'Pointer') {
if (value && value.__type) {
const object = new Parse.Object(value.className);
object.id = value.objectId;
value = object;
shouldComponentUpdate(nextProps) {
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
.filter(propName => propName !== 'value');
if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
return true;
}
content = (
<a href='javascript:;' onClick={onPointerClick.bind(undefined, value)}>
<Pill value={value.id} />
</a>
);
} else if (type === 'Date') {
if (typeof value === 'object' && value.__type) {
value = new Date(value.iso);
} else if (typeof value === 'string') {
value = new Date(value);
const { value } = this.props;
const { value: nextValue } = nextProps;
if (typeof value !== typeof nextValue) {
return true;
}
content = dateStringUTC(value);
} else if (type === 'Boolean') {
content = value ? 'True' : 'False';
} else if (type === 'Object' || type === 'Bytes' || type === 'Array') {
content = JSON.stringify(value);
} else if (type === 'File') {
if (value.url()) {
content = <Pill value={getFileName(value)} />;
} else {
content = <Pill value={'Uploading\u2026'} />;
const isRefDifferent = value !== nextValue;
if (isRefDifferent && typeof value === 'object') {
return JSON.stringify(value) !== JSON.stringify(nextValue);
}
} else if (type === 'ACL') {
let pieces = [];
let json = value.toJSON();
if (Object.prototype.hasOwnProperty.call(json, '*')) {
if (json['*'].read && json['*'].write) {
pieces.push('Public Read + Write');
} else if (json['*'].read) {
pieces.push('Public Read');
} else if (json['*'].write) {
pieces.push('Public Write');
return isRefDifferent;
}

render() {
let { type, value, hidden, width, current, onSelect, onEditChange, setRelation, onPointerClick, row, col } = this.props;
let content = value;
let classes = [styles.cell, unselectable];
if (hidden) {
content = '(hidden)';
classes.push(styles.empty);
} else if (value === undefined) {
if (type === 'ACL') {
content = 'Public Read + Write';
} else {
content = '(undefined)';
classes.push(styles.empty);
}
}
for (let role in json) {
if (role !== '*') {
pieces.push(role);
} else if (value === null) {
content = '(null)';
classes.push(styles.empty);
} else if (value === '') {
content = <span>&nbsp;</span>;
classes.push(styles.empty);
} else if (type === 'Pointer') {
if (value && value.__type) {
const object = new Parse.Object(value.className);
object.id = value.objectId;
value = object;
}
content = (
<a href='javascript:;' onClick={onPointerClick.bind(undefined, value)}>
<Pill value={value.id} />
</a>
);
} else if (type === 'Date') {
if (typeof value === 'object' && value.__type) {
value = new Date(value.iso);
} else if (typeof value === 'string') {
value = new Date(value);
}
content = dateStringUTC(value);
} else if (type === 'Boolean') {
content = value ? 'True' : 'False';
} else if (type === 'Object' || type === 'Bytes' || type === 'Array') {
content = JSON.stringify(value);
} else if (type === 'File') {
if (value.url()) {
content = <Pill value={getFileName(value)} />;
} else {
content = <Pill value={'Uploading\u2026'} />;
}
} else if (type === 'ACL') {
let pieces = [];
let json = value.toJSON();
if (Object.prototype.hasOwnProperty.call(json, '*')) {
if (json['*'].read && json['*'].write) {
pieces.push('Public Read + Write');
} else if (json['*'].read) {
pieces.push('Public Read');
} else if (json['*'].write) {
pieces.push('Public Write');
}
}
for (let role in json) {
if (role !== '*') {
pieces.push(role);
}
}
if (pieces.length === 0) {
pieces.push('Master Key Only');
}
content = pieces.join(', ');
} else if (type === 'GeoPoint') {
content = `(${value.latitude}, ${value.longitude})`;
} else if (type === 'Polygon') {
content = value.coordinates.map(coord => `(${coord})`)
} else if (type === 'Relation') {
content = (
<div style={{ textAlign: 'center', cursor: 'pointer' }}>
<Pill onClick={() => setRelation(value)} value='View relation' />
</div>
);
}
if (pieces.length === 0) {
pieces.push('Master Key Only');

if (current) {
classes.push(styles.current);
}
content = pieces.join(', ');
} else if (type === 'GeoPoint') {
content = `(${value.latitude}, ${value.longitude})`;
} else if (type === 'Polygon') {
content = value.coordinates.map(coord => `(${coord})`)
} else if (type === 'Relation') {
content = (
<div style={{ textAlign: 'center', cursor: 'pointer' }}>
<Pill onClick={() => setRelation(value)} value='View relation' />
</div>
return (
<span
ref={this.cellRef}
className={classes.join(' ')}
style={{ width }}
onClick={() => onSelect({ row, col })}
onDoubleClick={() => {
if (type !== 'Relation') {
onEditChange(true)
}
}}
onTouchEnd={e => {
if (current && type !== 'Relation') {
// The touch event may trigger an unwanted change in the column value
if (['ACL', 'Boolean', 'File'].includes(type)) {
e.preventDefault();
}
onEditChange(true);
}
}}>
{content}
</span>
);
}

if (current) {
classes.push(styles.current);
}
return (
<span
ref={cellRef}
className={classes.join(' ')}
style={{ width }}
onClick={onSelect}
onDoubleClick={() => {
if (type !== 'Relation') {
onEditChange(true)
}
}}
onTouchEnd={e => {
if (current && type !== 'Relation') {
// The touch event may trigger an unwanted change in the column value
if (['ACL', 'Boolean', 'File'].includes(type)) {
e.preventDefault();
}
onEditChange(true);
}
}}>
{content}
</span>
);
};

export default BrowserCell;
}
80 changes: 80 additions & 0 deletions src/components/BrowserRow/BrowserRow.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Parse from 'parse';
import React, { Component } from 'react';

import BrowserCell from 'components/BrowserCell/BrowserCell.react';
import styles from 'dashboard/Data/Browser/Browser.scss';

export default class BrowserRow extends Component {
shouldComponentUpdate(nextProps) {
const shallowVerifyProps = [...new Set(Object.keys(this.props).concat(Object.keys(nextProps)))]
.filter(propName => propName !== 'obj');
if (shallowVerifyProps.some(propName => this.props[propName] !== nextProps[propName])) {
return true;
}
const { obj } = this.props;
const { obj: nextObj } = nextProps;
const isRefDifferent = obj !== nextObj;
return isRefDifferent ? JSON.stringify(obj) !== JSON.stringify(nextObj) : isRefDifferent;
}

render() {
const { className, columns, currentCol, isUnique, obj, onPointerClick, order, readOnlyFields, row, rowWidth, selection, selectRow, setCurrent, setEditing, setRelation } = this.props;
let attributes = obj.attributes;
return (
<div className={styles.tableRow} style={{ minWidth: rowWidth }}>
<span className={styles.checkCell}>
<input
type='checkbox'
checked={selection['*'] || selection[obj.id]}
onChange={e => selectRow(obj.id, e.target.checked)} />
</span>
{order.map(({ name, width, visible }, j) => {
if (!visible) return null;
let type = columns[name].type;
let attr = obj;
if (!isUnique) {
attr = attributes[name];
if (name === 'objectId') {
attr = obj.id;
} else if (name === 'ACL' && className === '_User' && !attr) {
attr = new Parse.ACL({ '*': { read: true }, [obj.id]: { read: true, write: true }});
} else if (type === 'Relation' && !attr && obj.id) {
attr = new Parse.Relation(obj, name);
attr.targetClassName = columns[name].targetClass;
} else if (type === 'Array' || type === 'Object') {
// This is needed to avoid unwanted conversions of objects to Parse.Objects.
// "Parse._encoding" is responsible to convert Parse data into raw data.
// Since array and object are generic types, we want to render them the way
// they were stored in the database.
attr = Parse._encode(obj.get(name));
}
}
let hidden = false;
if (name === 'password' && className === '_User') {
hidden = true;
} else if (name === 'sessionToken') {
if (className === '_User' || className === '_Session') {
hidden = true;
}
}
return (
<BrowserCell
key={name}
row={row}
col={j}
type={type}
readonly={isUnique || readOnlyFields.indexOf(name) > -1}
width={width}
current={currentCol === j}
onSelect={setCurrent}
onEditChange={setEditing}
onPointerClick={onPointerClick}
setRelation={setRelation}
value={attr}
hidden={hidden} />
);
})}
</div>
);
}
}
27 changes: 6 additions & 21 deletions src/dashboard/Data/Browser/Browser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -910,34 +910,20 @@ class Browser extends DashboardView {
</div>
);
} else if (className && classes.get(className)) {
let schema = {};
classes.get(className).forEach(({ type, targetClass }, col) => {
schema[col] = {
type,
targetClass,
};
});

let columns = {
objectId: { type: 'String' }
};
if (this.state.isUnique) {
columns = {};
}
let userPointers = [];
classes.get(className).forEach((field, name) => {
if (name === 'objectId') {
return;
}
if (this.state.isUnique && name !== this.state.uniqueField) {
classes.get(className).forEach(({ type, targetClass }, name) => {
if (name === 'objectId' || this.state.isUnique && name !== this.state.uniqueField) {
return;
}
let info = { type: field.type };
if (field.targetClass) {
info.targetClass = field.targetClass;
if (field.targetClass === '_User') {
userPointers.push(name);
}
const info = { type };
if (targetClass) {
info.targetClass = targetClass;
}
columns[name] = info;
});
Expand All @@ -958,8 +944,7 @@ class Browser extends DashboardView {
uniqueField={this.state.uniqueField}
count={count}
perms={this.state.clp[className]}
schema={schema}
userPointers={userPointers}
schema={this.props.schema}
filters={this.state.filters}
onFilterChange={this.updateFilters}
onRemoveColumn={this.showRemoveColumn}
Expand Down
Loading