diff --git a/README.md b/README.md index 50c81c7f9..faf84cd2b 100644 --- a/README.md +++ b/README.md @@ -126,25 +126,26 @@ Parse Dashboard is continuously tested with the most recent releases of Node.js ### Options -| Parameter | Type | Optional | Default | Example | Description | -|----------------------------------------|---------------------|----------|---------|--------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| -| `apps` | Array<Object> | no | - | `[{ ... }, { ... }]` | The apps that are configured for the dashboard. | -| `apps.appId` | String | yes | - | `"myAppId"` | The Application ID for your Parse Server instance. | -| `apps.masterKey` | String \| Function | yes | - | `"exampleMasterKey"`, `() => "exampleMasterKey"` | The master key for full access to Parse Server. It can be provided directly as a String or as a Function returning a String. | -| `apps.masterKeyTtl` | Number | no | - | `3600` | Time-to-live (TTL) for the master key in seconds. This defines how long the master key is cached before the `masterKey` function is re-triggered. | -| `apps.serverURL` | String | yes | - | `"http://localhost:1337/parse"` | The URL where your Parse Server is running. | -| `apps.appName` | String | no | - | `"MyApp"` | The display name of the app in the dashboard. | -| `infoPanel` | Array<Object> | yes | - | `[{ ... }, { ... }]` | The [info panel](#info-panel) configuration. | -| `infoPanel[*].title` | String | no | - | `User Details` | The panel title. | -| `infoPanel[*].classes` | Array<String> | no | - | `["_User"]` | The classes for which the info panel should be displayed. | -| `infoPanel[*].cloudCodeFunction` | String | no | - | `getUserDetails` | The Cloud Code Function which received the selected object in the data browser and returns the response to be displayed in the info panel. | -| `apps.scripts` | Array<Object> | yes | `[]` | `[{ ... }, { ... }]` | The scripts that can be executed for that app. | -| `apps.scripts.title` | String | no | - | `'Delete User'` | The title that will be displayed in the data browser context menu and the script run confirmation dialog. | -| `apps.scripts.classes` | Array<String> | no | - | `['_User']` | The classes of Parse Objects for which the scripts can be executed. | -| `apps.scripts.cloudCodeFunction` | String | no | - | `'deleteUser'` | The name of the Parse Cloud Function to execute. | -| `apps.scripts.showConfirmationDialog` | Bool | yes | `false` | `true` | Is `true` if a confirmation dialog should be displayed before the script is executed, `false` if the script should be executed immediately. | -| `apps.scripts.confirmationDialogStyle` | String | yes | `info` | `critical` | The style of the confirmation dialog. Valid values: `info` (blue style), `critical` (red style). | -| `apps.cloudConfigHistoryLimit` | Integer | yes | `100` | `100` | The number of historic values that should be saved in the Cloud Config change history. Valid values: `0`...`Number.MAX_SAFE_INTEGER`. | +| Parameter | Type | Optional | Default | Example | Description | +|----------------------------------------|---------------------|----------|---------|--------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `apps` | Array<Object> | no | - | `[{ ... }, { ... }]` | The apps that are configured for the dashboard. | +| `apps.appId` | String | yes | - | `"myAppId"` | The Application ID for your Parse Server instance. | +| `apps.masterKey` | String \| Function | yes | - | `"exampleMasterKey"`, `() => "exampleMasterKey"` | The master key for full access to Parse Server. It can be provided directly as a String or as a Function returning a String. | +| `apps.masterKeyTtl` | Number | no | - | `3600` | Time-to-live (TTL) for the master key in seconds. This defines how long the master key is cached before the `masterKey` function is re-triggered. | +| `apps.serverURL` | String | yes | - | `"http://localhost:1337/parse"` | The URL where your Parse Server is running. | +| `apps.appName` | String | no | - | `"MyApp"` | The display name of the app in the dashboard. | +| `infoPanel` | Array<Object> | yes | - | `[{ ... }, { ... }]` | The [info panel](#info-panel) configuration. | +| `infoPanel[*].title` | String | no | - | `User Details` | The panel title. | +| `infoPanel[*].classes` | Array<String> | no | - | `["_User"]` | The classes for which the info panel should be displayed. | +| `infoPanel[*].cloudCodeFunction` | String | no | - | `getUserDetails` | The Cloud Code Function which received the selected object in the data browser and returns the response to be displayed in the info panel. | +| `apps.scripts` | Array<Object> | yes | `[]` | `[{ ... }, { ... }]` | The scripts that can be executed for that app. | +| `apps.scripts.title` | String | no | - | `'Delete User'` | The title that will be displayed in the data browser context menu and the script run confirmation dialog. | +| `apps.scripts.classes` | Array<String> | no | - | `['_User']` | The classes of Parse Objects for which the scripts can be executed. | +| `apps.scripts.cloudCodeFunction` | String | no | - | `'deleteUser'` | The name of the Parse Cloud Function to execute. | +| `apps.scripts.executionBatchSize` | Integer | yes | `1` | `10` | The batch size with which a script should be executed on all selected objects. For example, with 50 objects selected, a batch size of 10 means the script will run on 10 objects in parallel, running a total of 5 batches in serial. | +| `apps.scripts.showConfirmationDialog` | Bool | yes | `false` | `true` | Is `true` if a confirmation dialog should be displayed before the script is executed, `false` if the script should be executed immediately. | +| `apps.scripts.confirmationDialogStyle` | String | yes | `info` | `critical` | The style of the confirmation dialog. Valid values: `info` (blue style), `critical` (red style). | +| `apps.cloudConfigHistoryLimit` | Integer | yes | `100` | `100` | The number of historic values that should be saved in the Cloud Config change history. Valid values: `0`...`Number.MAX_SAFE_INTEGER`. | ### File diff --git a/src/dashboard/Data/Browser/Browser.react.js b/src/dashboard/Data/Browser/Browser.react.js index 937db6066..9ec624719 100644 --- a/src/dashboard/Data/Browser/Browser.react.js +++ b/src/dashboard/Data/Browser/Browser.react.js @@ -1571,24 +1571,63 @@ class Browser extends DashboardView { } async confirmExecuteScriptRows(script) { + const batchSize = script.executionBatchSize || 1; try { - const objects = []; - Object.keys(this.state.selection).forEach(key => - objects.push(Parse.Object.extend(this.props.params.className).createWithoutData(key)) + const objects = Object.keys(this.state.selection).map(key => + Parse.Object.extend(this.props.params.className).createWithoutData(key) ); - for (const object of objects) { - const response = await Parse.Cloud.run( - script.cloudCodeFunction, - { object: object.toPointer() }, - { useMasterKey: true } + + let totalErrorCount = 0; + let batchCount = 0; + const totalBatchCount = Math.ceil(objects.length / batchSize); + + for (let i = 0; i < objects.length; i += batchSize) { + batchCount++; + const batch = objects.slice(i, i + batchSize); + const promises = batch.map(object => + Parse.Cloud.run( + script.cloudCodeFunction, + { object: object.toPointer() }, + { useMasterKey: true } + ).then(response => ({ + objectId: object.id, + response, + })).catch(error => ({ + objectId: object.id, + error, + })) ); - this.setState(prevState => ({ - processedScripts: prevState.processedScripts + 1, - })); - const note = - (typeof response === 'object' ? JSON.stringify(response) : response) || - `Ran script "${script.title}" on "${object.id}".`; - this.showNote(note); + + const results = await Promise.all(promises); + + let batchErrorCount = 0; + results.forEach(({ objectId, response, error }) => { + this.setState(prevState => ({ + processedScripts: prevState.processedScripts + 1, + })); + + if (error) { + batchErrorCount += 1; + const errorMessage = `Error running script "${script.title}" on "${objectId}": ${error.message}`; + this.showNote(errorMessage, true); + console.error(errorMessage, error); + } else { + const note = + (typeof response === 'object' ? JSON.stringify(response) : response) || + `Ran script "${script.title}" on "${objectId}".`; + this.showNote(note); + } + }); + + totalErrorCount += batchErrorCount; + + if (objects.length > 1) { + this.showNote(`Ran script "${script.title}" on ${batch.length} objects in batch ${batchCount}/${totalBatchCount} with ${batchErrorCount} errors.`, batchErrorCount > 0); + } + } + + if (objects.length > 1) { + this.showNote(`Ran script "${script.title}" on ${objects.length} objects in ${batchCount} batches with ${totalErrorCount} errors.`, totalErrorCount > 0); } this.refresh(); } catch (e) {