Skip to content

feat: Add script execution on parallel batches with option script.executionBatchSize #2828

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
39 changes: 20 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
69 changes: 54 additions & 15 deletions src/dashboard/Data/Browser/Browser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading