Skip to content
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

feat: Import data via CSV #2360

Open
wants to merge 7 commits into
base: alpha
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions src/components/BrowserCell/BrowserCell.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ export default class BrowserCell extends Component {
} else if (this.props.type === 'Object' || this.props.type === 'Bytes') {
this.copyableValue = content = JSON.stringify(this.props.value);
} else if (this.props.type === 'File') {
const fileName = this.props.value.url() ? getFileName(this.props.value) : 'Uploading\u2026';
content = <Pill value={fileName} fileDownloadLink={this.props.value.url()} shrinkablePill />;
const fileName = this.props.value.url?.() ? getFileName(this.props.value) : 'Uploading\u2026';
content = <Pill value={fileName} fileDownloadLink={this.props.value.url?.()} shrinkablePill />;
this.copyableValue = fileName;
} else if (this.props.type === 'ACL') {
let pieces = [];
Expand Down
2 changes: 1 addition & 1 deletion src/components/FileEditor/FileEditor.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export default class FileEditor extends React.Component {
return (
<div ref={this.inputRef} style={{ minWidth: this.props.width, display: 'none' }} className={styles.editor}>
<a className={styles.upload}>
<input ref={this.fileInputRef} id='fileInput' type='file' onChange={this.handleChange.bind(this)} />
<input ref={this.fileInputRef} id='fileInput' type='file' onChange={this.handleChange.bind(this)} accept={this.props.accept} />
<span>{file ? 'Replace file' : 'Upload file'}</span>
</a>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Modal/Modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
position: absolute;
font-size: 14px;
color: white;
top: 52px;
top: 56px;
left: 28px;
}

Expand Down
102 changes: 101 additions & 1 deletion src/dashboard/Data/Browser/Browser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import DeleteRowsDialog from 'dashboard/Data/Browser/DeleteRow
import DropClassDialog from 'dashboard/Data/Browser/DropClassDialog.react';
import EmptyState from 'components/EmptyState/EmptyState.react';
import ExportDialog from 'dashboard/Data/Browser/ExportDialog.react';
import ImportDialog from 'dashboard/Data/Browser/ImportDialog.react';
import AttachRowsDialog from 'dashboard/Data/Browser/AttachRowsDialog.react';
import AttachSelectedRowsDialog from 'dashboard/Data/Browser/AttachSelectedRowsDialog.react';
import CloneSelectedRowsDialog from 'dashboard/Data/Browser/CloneSelectedRowsDialog.react';
Expand Down Expand Up @@ -57,6 +58,7 @@ class Browser extends DashboardView {
showRemoveColumnDialog: false,
showDropClassDialog: false,
showExportDialog: false,
showImportDialog: false,
showExportSchemaDialog: false,
showAttachRowsDialog: false,
showEditRowDialog: false,
Expand Down Expand Up @@ -105,6 +107,7 @@ class Browser extends DashboardView {
this.showDeleteRows = this.showDeleteRows.bind(this);
this.showDropClass = this.showDropClass.bind(this);
this.showExport = this.showExport.bind(this);
this.showImport = this.showImport.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
this.toggleMasterKeyUsage = this.toggleMasterKeyUsage.bind(this);
Expand Down Expand Up @@ -282,6 +285,10 @@ class Browser extends DashboardView {
this.setState({ showExportDialog: true });
}

showImport() {
this.setState({ showImportDialog: true });
}

async login(username, password) {
if (Parse.User.current()) {
await Parse.User.logOut();
Expand Down Expand Up @@ -1125,6 +1132,7 @@ class Browser extends DashboardView {
this.state.showRemoveColumnDialog ||
this.state.showDropClassDialog ||
this.state.showExportDialog ||
this.state.showImportDialog ||
this.state.showExportSchema ||
this.state.rowsToDelete ||
this.state.showAttachRowsDialog ||
Expand Down Expand Up @@ -1445,6 +1453,89 @@ class Browser extends DashboardView {
}
}

async confirmImport(file) {
this.setState({ showImportDialog: null });
const className = this.props.params.className;
const classColumns = this.getClassColumns(className, false);
const columnsObject = {};
classColumns.forEach((column) => {
columnsObject[column.name] = column;
});
const { base64, type} = file._source;
if (type === 'text/csv') {
const csvToArray =(text) => {
let p = '', row = [''], ret = [row], i = 0, r = 0, s = !0, l;
for (l of text) {
if ('"' === l) {
if (s && l === p) row[i] += l;
s = !s;
} else if (',' === l && s) l = row[++i] = '';
else if ('\n' === l && s) {
if ('\r' === p) row[i] = row[i].slice(0, -1);
row = ret[++r] = [l = '']; i = 0;
} else row[i] += l;
p = l;
}
return ret;
};
const csv = atob(base64);
const [columns, ...rows] = csvToArray(csv);
await Parse.Object.saveAll(rows.filter(row => row.join() !== '').map(row => {
const json = {className};
for (let i = 1; i < row.length; i++) {
const column = columns[i];
const value = row[i];
if (value === 'null') {
continue;
}
const {type, targetClass, name} = columnsObject[column] || {};
if (type === 'Relation') {
json[column] = {
__type: 'Relation',
className: targetClass,
};
continue;
}
if (type === 'Pointer') {
json[column] = {
__type: 'Pointer',
className: targetClass,
objectId: value,
};
continue;
}
if (name === 'ACL') {
json.ACL = new Parse.ACL(JSON.parse(value));
continue;
}
if (type === 'Date') {
json[column] = new Date(value);
continue;
}
if (type === 'Boolean') {
json[column] = value === 'true';
continue;
}
if (type === 'String') {
json[column] = value;
continue;
}
if (type === 'Number') {
json[column] = Number(value);
continue;
}
try {
json[column] = JSON.parse(value);
} catch (e) {
/* */
}
}
return Parse.Object.fromJSON(json, false, true);
}), {useMasterKey: true});
}
this.refresh();
}

getClassRelationColumns(className) {
const currentClassName = this.props.params.className;
return this.getClassColumns(className, false)
Expand Down Expand Up @@ -1640,6 +1731,7 @@ class Browser extends DashboardView {
onExport={this.showExport}
onChangeCLP={this.handleCLPChange}
onRefresh={this.refresh}
onImport={this.showImport}
onAttachRows={this.showAttachRowsDialog}
onAttachSelectedRows={this.showAttachSelectedRowsDialog}
onCloneSelectedRows={this.showCloneSelectedRowsDialog}
Expand Down Expand Up @@ -1761,6 +1853,14 @@ class Browser extends DashboardView {
onCancel={() => this.setState({ showExportDialog: false })}
onConfirm={() => this.exportClass(className)} />
);
}
else if (this.state.showImportDialog) {
extras = (
<ImportDialog
className={className}
onCancel={() => this.setState({ showImportDialog: false })}
onConfirm={(file) => this.confirmImport(file)} />
);
} else if (this.state.showExportSchemaDialog) {
extras = (
<ExportSchemaDialog
Expand All @@ -1776,7 +1876,7 @@ class Browser extends DashboardView {
onCancel={this.cancelAttachRows}
onConfirm={this.confirmAttachRows}
/>
)
);
} else if (this.state.showAttachSelectedRowsDialog) {
extras = (
<AttachSelectedRowsDialog
Expand Down
6 changes: 6 additions & 0 deletions src/dashboard/Data/Browser/BrowserToolbar.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ let BrowserToolbar = ({
onDropClass,
onChangeCLP,
onRefresh,
onImport,
onEditPermissions,
hidePerms,
isUnique,
Expand Down Expand Up @@ -265,6 +266,11 @@ let BrowserToolbar = ({
</BrowserMenu>
)}
{onAddRow && <div className={styles.toolbarSeparator} />}
<a className={classes.join(' ')} onClick={isPendingEditCloneRows ? null : onImport}>
<Icon name="up-solid" width={14} height={14} />
<span>Import</span>
</a>
<div className={styles.toolbarSeparator} />
<a className={classes.join(' ')} onClick={isPendingEditCloneRows ? null : onRefresh}>
<Icon name="refresh-solid" width={14} height={14} />
<span>Refresh</span>
Expand Down
66 changes: 66 additions & 0 deletions src/dashboard/Data/Browser/ImportDialog.react.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Modal from 'components/Modal/Modal.react';
import FileEditor from 'components/FileEditor/FileEditor.react';
import React from 'react';
import Pill from 'components/Pill/Pill.react';
import getFileName from 'lib/getFileName';
import { CurrentApp } from 'context/currentApp';

export default class ImportDialog extends React.Component {
constructor() {
super();
this.state = {
file: null,
showFileEditor: false
};
}

openFileEditor() {
this.setState({
showFileEditor: true
});
}

hideFileEditor(file) {
this.setState({
showFileEditor: false,
file
});
}

render() {
return (
<div>
<Modal
type={Modal.Types.INFO}
icon='up-outline'
iconSize={40}
title={`Import Data into ${this.props.className}`}
subtitle='Note: Please make sure columns are defined in SCHEMA to import.'
confirmText='Import'
cancelText='Cancel'
disabled={!this.state.file}
buttonsInCenter={true}
onCancel={this.props.onCancel}
onConfirm={() => this.props.onConfirm(this.state.file)}>
<div style={{ padding: '25px' }}>
{this.state.file && <Pill value={getFileName(this.state.file) }/>}
<div style={{ cursor: 'pointer' }}>
<Pill
value={this.state.file ? 'Change file' : 'Select file'}
onClick={() => this.openFileEditor()}
/>
{this.state.showFileEditor && (
<FileEditor
value={this.state.file}
accept='.csv'
onCommit={(file) => this.hideFileEditor(file)}
onCancel={() => this.hideFileEditor()}
/>
)}
</div>
</div>
</Modal>
</div>
);
}
}