Skip to content

Commit e03e75b

Browse files
committed
Merge branch 'master' of github.com:parse-community/parse-dashboard into excludeHiddenFields
2 parents 81b7c21 + 1ddf29e commit e03e75b

File tree

10 files changed

+201
-10
lines changed

10 files changed

+201
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
### master
44
[Full Changelog](https://github.com/parse-community/parse-dashboard/compare/2.1.0...master)
55

6+
__New features:__
7+
* Added data export in CSV format for classes ([#1494](https://github.com/parse-community/parse-dashboard/pull/1494)), thanks to [Cory Imdieke](https://github.com/Vortec4800), [Manuel Trezza](https://github.com/mtrezza).
8+
69
### 2.1.0
710
[Full Changelog](https://github.com/parse-community/parse-dashboard/compare/2.0.5...2.1.0)
811

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,14 @@ This feature allows you to use the data browser as another user, respecting that
570570

571571
> ⚠️ Logging in as another user will trigger the same Cloud Triggers as if the user logged in themselves using any other login method. Logging in as another user requires to enter that user's password.
572572
573+
## CSV Export
574+
575+
▶️ *Core > Browser > Export*
576+
577+
This feature will take either selected rows or all rows of an individual class and saves them to a CSV file, which is then downloaded. CSV headers are added to the top of the file matching the column names.
578+
579+
> ⚠️ There is currently a 10,000 row limit when exporting all data. If more than 10,000 rows are present in the class, the CSV file will only contain 10,000 rows.
580+
573581
# Contributing
574582

575583
We really want Parse to be yours, to see it grow and thrive in the open source community. Please see the [Contributing to Parse Dashboard guide](CONTRIBUTING.md).

package-lock.json

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"LICENSE"
3636
],
3737
"dependencies": {
38-
"@babel/runtime": "7.14.6",
38+
"@babel/runtime": "7.14.8",
3939
"bcryptjs": "2.3.0",
4040
"body-parser": "1.19.0",
4141
"codemirror-graphql": "github:timsuchanek/codemirror-graphql#details-fix",

src/components/BrowserCell/BrowserCell.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
cursor: default;
1313
color: #0E69A1;
1414
height: 30px;
15-
line-height: 22px;
16-
padding: 5px 4px 0;
15+
line-height: 20px;
16+
padding: 5px;
1717
border-right: 1px solid #e3e3ea;
1818
}
1919

src/components/Pill/Pill.scss

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
overflow: hidden;
2222
text-overflow: ellipsis;
2323
white-space: nowrap;
24-
margin-bottom: 4px;
2524
& a {
2625
height: 20px;
2726
width: 20px;
@@ -49,4 +48,4 @@
4948

5049
.disableIconAction {
5150
cursor: initial;
52-
}
51+
}

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

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import AttachRowsDialog from 'dashboard/Data/Browser/AttachRow
2020
import AttachSelectedRowsDialog from 'dashboard/Data/Browser/AttachSelectedRowsDialog.react';
2121
import CloneSelectedRowsDialog from 'dashboard/Data/Browser/CloneSelectedRowsDialog.react';
2222
import EditRowDialog from 'dashboard/Data/Browser/EditRowDialog.react';
23+
import ExportSelectedRowsDialog from 'dashboard/Data/Browser/ExportSelectedRowsDialog.react';
2324
import history from 'dashboard/history';
2425
import { List, Map } from 'immutable';
2526
import Notification from 'dashboard/Data/Browser/Notification.react';
@@ -59,6 +60,7 @@ class Browser extends DashboardView {
5960
showAttachRowsDialog: false,
6061
showEditRowDialog: false,
6162
rowsToDelete: null,
63+
rowsToExport: null,
6264

6365
relation: null,
6466
counts: {},
@@ -110,6 +112,9 @@ class Browser extends DashboardView {
110112
this.showCloneSelectedRowsDialog = this.showCloneSelectedRowsDialog.bind(this);
111113
this.confirmCloneSelectedRows = this.confirmCloneSelectedRows.bind(this);
112114
this.cancelCloneSelectedRows = this.cancelCloneSelectedRows.bind(this);
115+
this.showExportSelectedRowsDialog = this.showExportSelectedRowsDialog.bind(this);
116+
this.confirmExportSelectedRows = this.confirmExportSelectedRows.bind(this);
117+
this.cancelExportSelectedRows = this.cancelExportSelectedRows.bind(this);
113118
this.getClassRelationColumns = this.getClassRelationColumns.bind(this);
114119
this.showCreateClass = this.showCreateClass.bind(this);
115120
this.refresh = this.refresh.bind(this);
@@ -1063,7 +1068,8 @@ class Browser extends DashboardView {
10631068
this.state.showAttachSelectedRowsDialog ||
10641069
this.state.showCloneSelectedRowsDialog ||
10651070
this.state.showEditRowDialog ||
1066-
this.state.showPermissionsDialog
1071+
this.state.showPermissionsDialog ||
1072+
this.state.showExportSelectedRowsDialog
10671073
);
10681074
}
10691075

@@ -1211,6 +1217,106 @@ class Browser extends DashboardView {
12111217
}
12121218
}
12131219

1220+
showExportSelectedRowsDialog(rows) {
1221+
this.setState({
1222+
rowsToExport: rows
1223+
});
1224+
}
1225+
1226+
cancelExportSelectedRows() {
1227+
this.setState({
1228+
rowsToExport: null
1229+
});
1230+
}
1231+
1232+
async confirmExportSelectedRows(rows) {
1233+
this.setState({ rowsToExport: null });
1234+
const className = this.props.params.className;
1235+
const query = new Parse.Query(className);
1236+
1237+
if (rows['*']) {
1238+
// Export all
1239+
query.limit(10000);
1240+
} else {
1241+
// Export selected
1242+
const objectIds = [];
1243+
for (const objectId in this.state.rowsToExport) {
1244+
objectIds.push(objectId);
1245+
}
1246+
query.containedIn('objectId', objectIds);
1247+
}
1248+
1249+
const classColumns = this.getClassColumns(className, false);
1250+
// create object with classColumns as property keys needed for ColumnPreferences.getOrder function
1251+
const columnsObject = {};
1252+
classColumns.forEach((column) => {
1253+
columnsObject[column.name] = column;
1254+
});
1255+
// get ordered list of class columns
1256+
const columns = ColumnPreferences.getOrder(
1257+
columnsObject,
1258+
this.context.currentApp.applicationId,
1259+
className
1260+
).filter(column => column.visible);
1261+
1262+
const objects = await query.find({ useMasterKey: true });
1263+
let csvString = columns.map(column => column.name).join(',') + '\n';
1264+
for (const object of objects) {
1265+
const row = columns.map(column => {
1266+
const type = columnsObject[column.name].type;
1267+
if (column.name === 'objectId') {
1268+
return object.id;
1269+
} else if (type === 'Relation' || type === 'Pointer') {
1270+
if (object.get(column.name)) {
1271+
return object.get(column.name).id
1272+
} else {
1273+
return ''
1274+
}
1275+
} else {
1276+
let colValue;
1277+
if (column.name === 'ACL') {
1278+
colValue = object.getACL();
1279+
} else {
1280+
colValue = object.get(column.name);
1281+
}
1282+
// Stringify objects and arrays
1283+
if (Object.prototype.toString.call(colValue) === '[object Object]' || Object.prototype.toString.call(colValue) === '[object Array]') {
1284+
colValue = JSON.stringify(colValue);
1285+
}
1286+
if(typeof colValue === 'string') {
1287+
if (colValue.includes('"')) {
1288+
// Has quote in data, escape and quote
1289+
// If the value contains both a quote and delimiter, adding quotes and escaping will take care of both scenarios
1290+
colValue = colValue.split('"').join('""');
1291+
return `"${colValue}"`;
1292+
} else if (colValue.includes(',')) {
1293+
// Has delimiter in data, surround with quote (which the value doesn't already contain)
1294+
return `"${colValue}"`;
1295+
} else {
1296+
// No quote or delimiter, just include plainly
1297+
return `${colValue}`;
1298+
}
1299+
} else if (colValue === undefined) {
1300+
// Export as empty CSV field
1301+
return '';
1302+
} else {
1303+
return `${colValue}`;
1304+
}
1305+
}
1306+
}).join(',');
1307+
csvString += row + '\n';
1308+
}
1309+
1310+
// Deliver to browser to download file
1311+
const element = document.createElement('a');
1312+
const file = new Blob([csvString], { type: 'text/csv' });
1313+
element.href = URL.createObjectURL(file);
1314+
element.download = `${className}.csv`;
1315+
document.body.appendChild(element); // Required for this to work in FireFox
1316+
element.click();
1317+
document.body.removeChild(element);
1318+
}
1319+
12141320
getClassRelationColumns(className) {
12151321
const currentClassName = this.props.params.className;
12161322
return this.getClassColumns(className, false)
@@ -1393,6 +1499,8 @@ class Browser extends DashboardView {
13931499
onCloneSelectedRows={this.showCloneSelectedRowsDialog}
13941500
onEditSelectedRow={this.showEditRowDialog}
13951501
onEditPermissions={this.onDialogToggle}
1502+
onExportSelectedRows={this.showExportSelectedRowsDialog}
1503+
13961504
onSaveNewRow={this.saveNewRow}
13971505
onAbortAddRow={this.abortAddRow}
13981506
onSaveEditCloneRow={this.saveEditCloneRow}
@@ -1583,6 +1691,15 @@ class Browser extends DashboardView {
15831691
useMasterKey={this.state.useMasterKey}
15841692
/>
15851693
)
1694+
} else if (this.state.rowsToExport) {
1695+
extras = (
1696+
<ExportSelectedRowsDialog
1697+
className={SpecialClasses[className] || className}
1698+
selection={this.state.rowsToExport}
1699+
onCancel={this.cancelExportSelectedRows}
1700+
onConfirm={() => this.confirmExportSelectedRows(this.state.rowsToExport)}
1701+
/>
1702+
);
15861703
}
15871704

15881705
let notification = null;

src/dashboard/Data/Browser/Browser.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@
102102
@include MonospaceFont;
103103
font-size: 12px;
104104
white-space: nowrap;
105-
height: auto;
105+
height: 30px;
106106
border-bottom: 1px solid #e3e3ea;
107107

108108
&:nth-child(odd) {

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ let BrowserToolbar = ({
3939
onAttachRows,
4040
onAttachSelectedRows,
4141
onCloneSelectedRows,
42+
onExportSelectedRows,
4243
onExport,
4344
onRemoveColumn,
4445
onDeleteRows,
@@ -242,6 +243,20 @@ let BrowserToolbar = ({
242243
</BrowserMenu>
243244
)}
244245
{onAddRow && <div className={styles.toolbarSeparator} />}
246+
{onAddRow && (
247+
<BrowserMenu title='Export' icon='down-solid' disabled={isUnique || isPendingEditCloneRows} setCurrent={setCurrent}>
248+
<MenuItem
249+
disabled={!selectionLength}
250+
text={`Export ${selectionLength} selected ${selectionLength <= 1 ? 'row' : 'rows'}`}
251+
onClick={() => onExportSelectedRows(selection)}
252+
/>
253+
<MenuItem
254+
text={'Export all rows'}
255+
onClick={() => onExportSelectedRows({ '*': true })}
256+
/>
257+
</BrowserMenu>
258+
)}
259+
{onAddRow && <div className={styles.toolbarSeparator} />}
245260
<a className={classes.join(' ')} onClick={isPendingEditCloneRows ? null : onRefresh}>
246261
<Icon name="refresh-solid" width={14} height={14} />
247262
<span>Refresh</span>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
import Modal from 'components/Modal/Modal.react';
9+
import React from 'react';
10+
11+
export default class ExportSelectedRowsDialog extends React.Component {
12+
constructor() {
13+
super();
14+
15+
this.state = {
16+
confirmation: ''
17+
};
18+
}
19+
20+
valid() {
21+
return true;
22+
}
23+
24+
render() {
25+
let selectionLength = Object.keys(this.props.selection).length;
26+
return (
27+
<Modal
28+
type={Modal.Types.INFO}
29+
icon='warn-outline'
30+
title={this.props.selection['*'] ? 'Export all rows?' : (selectionLength === 1 ? `Export 1 selected row?` : `Export ${selectionLength} selected rows?`)}
31+
subtitle={this.props.selection['*'] ? 'Note: Exporting is limited to the first 10,000 rows.' : ''}
32+
disabled={!this.valid()}
33+
confirmText={'Yes export'}
34+
cancelText={'Never mind, don\u2019t.'}
35+
onCancel={this.props.onCancel}
36+
onConfirm={this.props.onConfirm}>
37+
{}
38+
</Modal>
39+
);
40+
}
41+
}

0 commit comments

Comments
 (0)