Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8d84e31
feat
mtrezza Mar 1, 2026
ccc79c3
file button
mtrezza Mar 1, 2026
a971235
menu
mtrezza Mar 1, 2026
f376346
Update README.md
mtrezza Mar 1, 2026
8daa201
lint
mtrezza Mar 1, 2026
113007a
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
5ac1c59
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
c010c80
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
6e42d26
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
8ab4e9c
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
17e5008
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
ce190b8
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
e4f8042
optimizaton
mtrezza Mar 1, 2026
ba31257
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 1, 2026
45a0fed
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 2, 2026
40479da
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 2, 2026
595ccf3
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 2, 2026
f5a333f
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 2, 2026
a20d6ac
fix https://github.com/parse-community/parse-dashboard/pull/3244#disc…
mtrezza Mar 2, 2026
f330d4a
refactor checkDuplicates
mtrezza Mar 2, 2026
3d173cf
docs
mtrezza Mar 2, 2026
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
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Response Fields](#response-fields)
- [Form Elements](#form-elements)
- [Drop-Down](#drop-down)
- [Checkbox](#checkbox)
- [Toggle](#toggle)
- [Text Input](#text-input)
- [Graph](#graph)
- [Calculated Values](#calculated-values)
- [Formula Operator](#formula-operator)
Expand All @@ -103,7 +106,8 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Browse as User](#browse-as-user)
- [Change Pointer Key](#change-pointer-key)
- [Limitations](#limitations)
- [CSV Export](#csv-export)
- [Data Export](#data-export)
- [Data Import](#data-import)
- [AI Agent](#ai-agent)
- [Configuration](#configuration-1)
- [Providers](#providers)
Expand Down Expand Up @@ -1731,14 +1735,35 @@ This feature allows you to change how a pointer is represented in the browser. B

> ⚠️ For each custom pointer key in each row, a server request is triggered to resolve the custom pointer key. For example, if the browser shows a class with 50 rows and each row contains 3 custom pointer keys, a total of 150 separate server requests are triggered.

### CSV Export
### Data Export

▶️ *Core > Browser > Export*

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.

> ⚠️ 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.

### Data Import

▶️ *Core > Browser > Data > Import*

Import data into a class from a JSON or CSV file. The file format is the same as the export format, so you can export data from one class and import it into another.

- **JSON** — An array of objects, e.g. `[{ "name": "Alice", "score": 100 }, ...]`. Typed fields such as `Pointer`, `Date`, `GeoPoint`, and `File` are expected in Parse `_toFullJSON()` format.
- **CSV** — Comma-separated with a header row. Column types are reconstructed from the class schema.

The import dialog provides the following options:

| Option | Description |
|---|---|
| Preserve object IDs | Use `objectId` values from the file instead of generating new ones. Requires the server option `allowCustomObjectId`. |
| Preserve timestamps | Use `createdAt` / `updatedAt` from the file. Requires `apps[].maintenanceKey` in the dashboard config. |
| Duplicate handling | When preserving object IDs: overwrite, skip, or fail on duplicates. |
| Unknown columns | Auto-create new columns, ignore them, or fail on unknown columns. |
| Continue on errors | Skip failing rows and continue, or stop on the first error. |

> ⚠️ Disabling *Preserve object IDs* means new object IDs are generated. Any `Pointer` or `Relation` fields that reference objects within the same import file will not resolve correctly.

## AI Agent

The Parse Dashboard includes an AI agent that can help manage your Parse Server data through natural language commands. The agent can perform operations like creating classes, adding data, querying records, and more.
Expand Down
4 changes: 3 additions & 1 deletion src/components/FileInput/FileInput.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class FileInput extends React.Component {
if (this.props.accept) {
inputProps.accept = this.props.accept;
}
const label = this.renderLabel();
const label = this.props.buttonText ? null : this.renderLabel();
const buttonStyles = [styles.button];
if (this.props.disabled || this.props.uploading) {
buttonStyles.push(styles.disabled);
Expand All @@ -58,6 +58,8 @@ export default class FileInput extends React.Component {
<div className={buttonStyles.join(' ')}>
{this.props.uploading ? (
<div className={styles.spinner}></div>
) : this.props.buttonText ? (
<span>{this.props.buttonText}</span>
) : label ? (
<span>Change file</span>
) : (
Expand Down
173 changes: 173 additions & 0 deletions src/dashboard/Data/Browser/Browser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ import ScriptResponseModal from 'dashboard/Data/Browser/ScriptResponseModal.reac
import ExportDialog from 'dashboard/Data/Browser/ExportDialog.react';
import ExportSchemaDialog from 'dashboard/Data/Browser/ExportSchemaDialog.react';
import ExportSelectedRowsDialog from 'dashboard/Data/Browser/ExportSelectedRowsDialog.react';
import ImportDataDialog from 'dashboard/Data/Browser/ImportDataDialog.react';
import {
parseImportJSON,
parseImportCSV,
buildBatchRequests,
sendBatchImport,
checkDuplicates,
} from 'lib/importData';
import Notification from 'dashboard/Data/Browser/Notification.react';
import PointerKeyDialog from 'dashboard/Data/Browser/PointerKeyDialog.react';
import RemoveColumnDialog from 'dashboard/Data/Browser/RemoveColumnDialog.react';
Expand Down Expand Up @@ -152,6 +160,7 @@ class Browser extends DashboardView {
showPointerKeyDialog: false,
rowsToDelete: null,
rowsToExport: null,
showImportDialog: false,

relation: null,
counts: {},
Expand Down Expand Up @@ -255,6 +264,9 @@ class Browser extends DashboardView {
this.showExportSchemaDialog = this.showExportSchemaDialog.bind(this);
this.confirmExportSelectedRows = this.confirmExportSelectedRows.bind(this);
this.cancelExportSelectedRows = this.cancelExportSelectedRows.bind(this);
this.showImportDialog = this.showImportDialog.bind(this);
this.confirmImport = this.confirmImport.bind(this);
this.cancelImportDialog = this.cancelImportDialog.bind(this);
this.getClassRelationColumns = this.getClassRelationColumns.bind(this);
this.showCreateClass = this.showCreateClass.bind(this);
this.refresh = this.refresh.bind(this);
Expand Down Expand Up @@ -303,6 +315,8 @@ class Browser extends DashboardView {
// Map of objectId -> promise for ongoing info panel cloud function requests
this.infoPanelQueries = {};

this.importDialogRef = null;

this.dataBrowserRef = React.createRef();

window.addEventListener('popstate', () => {
Expand Down Expand Up @@ -2071,6 +2085,7 @@ class Browser extends DashboardView {
this.state.showEditRowDialog ||
this.state.showPermissionsDialog ||
this.state.showExportSelectedRowsDialog ||
this.state.showImportDialog ||
this.state.showAddToConfigDialog
);
}
Expand Down Expand Up @@ -2420,6 +2435,153 @@ class Browser extends DashboardView {
});
}

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

cancelImportDialog() {
this.setState({ showImportDialog: false });
this.refresh();
}

async confirmImport(importOptions) {
const className = this.props.params.className;
const classColumns = this.getClassColumns(className, false);
const schema = {};
classColumns.forEach(column => {
schema[column.name] = column;
});
const knownColumns = classColumns.map(c => c.name);

// Parse the file
let parseResult;
if (importOptions.fileType === '.json') {
parseResult = parseImportJSON(importOptions.content);
} else {
parseResult = parseImportCSV(importOptions.content, schema);
}

if (parseResult.error) {
this.showNote(parseResult.error, true);
if (this.importDialogRef) {
this.importDialogRef.resetForm();
}
return;
}

// Check for unknown columns if fail mode
if (importOptions.unknownColumns === 'fail') {
const unknownCols = [];
for (const row of parseResult.rows) {
for (const key of Object.keys(row)) {
if (!knownColumns.includes(key) && !unknownCols.includes(key)) {
unknownCols.push(key);
}
}
}
if (unknownCols.length > 0) {
this.showNote(`Unknown columns found: ${unknownCols.join(', ')}. Import aborted.`, true);
if (this.importDialogRef) {
this.importDialogRef.resetForm();
}
return;
}
}

try {
// Track rows before duplicate filtering for skip count
const totalRowsBefore = parseResult.rows.length;

// Check for duplicates if needed
if (importOptions.preserveObjectIds && importOptions.duplicateHandling !== 'overwrite') {
const objectIds = parseResult.rows
.map(r => r.objectId)
.filter(Boolean);

if (objectIds.length > 0) {
const existing = await checkDuplicates(objectIds, className, {
serverURL: this.context.serverURL,
applicationId: this.context.applicationId,
masterKey: this.context.masterKey,
});

if (importOptions.duplicateHandling === 'fail' && existing.length > 0) {
this.showNote(
`Duplicate objectIds found: ${existing.slice(0, 5).join(', ')}${existing.length > 5 ? '...' : ''}. Import aborted.`,
true
);
if (this.importDialogRef) {
this.importDialogRef.resetForm();
}
return;
}

if (importOptions.duplicateHandling === 'skip') {
const existingSet = new Set(existing);
parseResult.rows = parseResult.rows.filter(r => !existingSet.has(r.objectId));
if (parseResult.rows.length === 0) {
this.showNote('All rows already exist. Nothing to import.', true);
if (this.importDialogRef) {
this.importDialogRef.resetForm();
}
return;
}
}
}
}

const skippedCount = totalRowsBefore - parseResult.rows.length;

// Build batch requests
const requests = buildBatchRequests(parseResult.rows, className, {
preserveObjectIds: importOptions.preserveObjectIds,
preserveTimestamps: importOptions.preserveTimestamps,
duplicateHandling: importOptions.duplicateHandling,
unknownColumns: importOptions.unknownColumns,
knownColumns,
});

// Set dialog to importing state
if (this.importDialogRef) {
this.importDialogRef.setImporting();
}

// Send batch import
const results = await sendBatchImport(requests, {
serverURL: this.context.serverURL,
applicationId: this.context.applicationId,
masterKey: this.context.masterKey,
maintenanceKey: importOptions.preserveTimestamps ? this.context.maintenanceKey : undefined,
continueOnError: importOptions.continueOnError,
onProgress: (progress) => {
if (this.importDialogRef) {
this.importDialogRef.setProgress(progress);
}
},
});

// Show results in dialog with skip count included
if (this.importDialogRef) {
this.importDialogRef.setResults({
...results,
skipped: results.skipped + skippedCount,
});
}
} catch (error) {
const msg = typeof error === 'string' ? error : error.message;
this.showNote(msg || 'Import failed due to a network error.', true);
if (this.importDialogRef) {
this.importDialogRef.setResults({
imported: 0,
skipped: 0,
failed: 0,
errors: [],
stopped: true,
});
}
}
}

cancelExportSelectedRows() {
this.setState({
rowsToExport: null,
Expand Down Expand Up @@ -2957,6 +3119,7 @@ class Browser extends DashboardView {
onEditPermissions={this.onDialogToggle}
onExportSelectedRows={this.showExportSelectedRowsDialog}
onExportSchema={this.showExportSchemaDialog}
onImport={this.showImportDialog}
onSaveNewRow={this.saveNewRow}
onShowPointerKey={this.showPointerKeyDialog}
onAbortAddRow={this.abortAddRow}
Expand Down Expand Up @@ -3247,6 +3410,16 @@ class Browser extends DashboardView {
}
/>
);
} else if (this.state.showImportDialog) {
extras = (
<ImportDataDialog
ref={(ref) => { this.importDialogRef = ref; }}
className={className}
maintenanceKey={this.context.maintenanceKey}
onCancel={this.cancelImportDialog}
onConfirm={this.confirmImport}
/>
);
} else if (this.state.showAddToConfigDialog) {
extras = (
<AddArrayEntryDialog
Expand Down
9 changes: 8 additions & 1 deletion src/dashboard/Data/Browser/BrowserToolbar.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const BrowserToolbar = ({
onCloneSelectedRows,
onExportSelectedRows,
onExportSchema,
onImport,
onExport,
onRemoveColumn,
onDeleteRows,
Expand Down Expand Up @@ -378,7 +379,7 @@ const BrowserToolbar = ({
{onAddRow && <div className={styles.toolbarSeparator} />}
{onAddRow && (
<BrowserMenu
title="Export"
title="Data"
icon="down-solid"
disabled={isUnique || isPendingEditCloneRows}
setCurrent={setCurrent}
Expand All @@ -390,6 +391,12 @@ const BrowserToolbar = ({
/>
<MenuItem text={'Export all rows'} onClick={() => onExportSelectedRows({ '*': true })} />
<MenuItem text={'Export schema'} onClick={() => onExportSchema()} />
{!relation && (
<>
<Separator />
<MenuItem text={'Import'} onClick={() => onImport()} />
</>
)}
</BrowserMenu>
)}
{onAddRow && <div className={styles.toolbarSeparator} />}
Expand Down
Loading
Loading