Skip to content

Commit a784129

Browse files
authored
feat: Add Cloud Config change history to roll back to previous values (#2554)
1 parent 15d0dfb commit a784129

File tree

4 files changed

+95
-5
lines changed

4 files changed

+95
-5
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
4949
- [Deploying Parse Dashboard](#deploying-parse-dashboard)
5050
- [Preparing for Deployment](#preparing-for-deployment)
5151
- [Security Considerations](#security-considerations)
52+
- [Security Checks](#security-checks)
5253
- [Configuring Basic Authentication](#configuring-basic-authentication)
5354
- [Multi-Factor Authentication (One-Time Password)](#multi-factor-authentication-one-time-password)
5455
- [Separating App Access Based on User Identity](#separating-app-access-based-on-user-identity)
@@ -123,6 +124,7 @@ Parse Dashboard is continuously tested with the most recent releases of Node.js
123124
| `apps.scripts.cloudCodeFunction` | String | no | - | `'deleteUser'` | The name of the Parse Cloud Function to execute. |
124125
| `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. |
125126
| `apps.scripts.confirmationDialogStyle` | String | yes | `info` | `critical` | The style of the confirmation dialog. Valid values: `info` (blue style), `critical` (red style). |
127+
| `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`. |
126128

127129
### File
128130

@@ -539,7 +541,7 @@ var dashboard = new ParseDashboard({
539541
});
540542
```
541543

542-
## Security Checks
544+
### Security Checks
543545

544546
You can view the security status of your Parse Server by enabling the dashboard option `enableSecurityChecks`, and visiting App Settings > Security.
545547

@@ -557,8 +559,6 @@ const dashboard = new ParseDashboard({
557559
});
558560
```
559561

560-
561-
562562
### Configuring Basic Authentication
563563
You can configure your dashboard for Basic Authentication by adding usernames and passwords your `parse-dashboard-config.json` configuration file:
564564

src/dashboard/Data/Config/Config.react.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ import TableHeader from 'components/Table/TableHeader.react';
2020
import TableView from 'dashboard/TableView.react';
2121
import Toolbar from 'components/Toolbar/Toolbar.react';
2222
import browserStyles from 'dashboard/Data/Browser/Browser.scss';
23+
import { CurrentApp } from 'context/currentApp';
2324

2425
@subscribeTo('Config', 'config')
2526
class Config extends TableView {
27+
static contextType = CurrentApp;
2628
constructor() {
2729
super();
2830
this.section = 'Core';
@@ -242,7 +244,7 @@ class Config extends TableView {
242244
return data;
243245
}
244246

245-
saveParam({ name, value, masterKeyOnly }) {
247+
saveParam({ name, value, type, masterKeyOnly }) {
246248
this.props.config
247249
.dispatch(ActionTypes.SET, {
248250
param: name,
@@ -252,6 +254,32 @@ class Config extends TableView {
252254
.then(
253255
() => {
254256
this.setState({ modalOpen: false });
257+
const limit = this.context.cloudConfigHistoryLimit;
258+
const applicationId = this.context.applicationId;
259+
let transformedValue = value;
260+
if(type === 'Date') {
261+
transformedValue = {__type: 'Date', iso: value};
262+
}
263+
if(type === 'File') {
264+
transformedValue = {name: value._name, url: value._url};
265+
}
266+
const configHistory = localStorage.getItem(`${applicationId}_configHistory`);
267+
if(!configHistory) {
268+
localStorage.setItem(`${applicationId}_configHistory`, JSON.stringify({
269+
[name]: [{
270+
time: new Date(),
271+
value: transformedValue
272+
}]
273+
}));
274+
} else {
275+
const oldConfigHistory = JSON.parse(configHistory);
276+
localStorage.setItem(`${applicationId}_configHistory`, JSON.stringify({
277+
...oldConfigHistory,
278+
[name]: !oldConfigHistory[name] ?
279+
[{time: new Date(), value: transformedValue}]
280+
: [{time: new Date(), value: transformedValue}, ...oldConfigHistory[name]].slice(0, limit || 100)
281+
}));
282+
}
255283
},
256284
() => {
257285
// Catch the error
@@ -263,6 +291,15 @@ class Config extends TableView {
263291
this.props.config.dispatch(ActionTypes.DELETE, { param: name }).then(() => {
264292
this.setState({ showDeleteParameterDialog: false });
265293
});
294+
const configHistory = localStorage.getItem('configHistory') && JSON.parse(localStorage.getItem('configHistory'));
295+
if(configHistory) {
296+
delete configHistory[name];
297+
if(Object.keys(configHistory).length === 0) {
298+
localStorage.removeItem('configHistory');
299+
} else {
300+
localStorage.setItem('configHistory', JSON.stringify(configHistory));
301+
}
302+
}
266303
}
267304

268305
createParameter() {

src/dashboard/Data/Config/ConfigDialog.react.js

+51
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import Toggle from 'components/Toggle/Toggle.react';
2020
import validateNumeric from 'lib/validateNumeric';
2121
import styles from 'dashboard/Data/Browser/Browser.scss';
2222
import semver from 'semver/preload.js';
23+
import { dateStringUTC } from 'lib/DateUtils';
24+
import { CurrentApp } from 'context/currentApp';
2325

2426
const PARAM_TYPES = ['Boolean', 'String', 'Number', 'Date', 'Object', 'Array', 'GeoPoint', 'File'];
2527

@@ -90,20 +92,23 @@ const GET_VALUE = {
9092
};
9193

9294
export default class ConfigDialog extends React.Component {
95+
static contextType = CurrentApp;
9396
constructor(props) {
9497
super();
9598
this.state = {
9699
value: null,
97100
type: 'String',
98101
name: '',
99102
masterKeyOnly: false,
103+
selectedIndex: null,
100104
};
101105
if (props.param.length > 0) {
102106
this.state = {
103107
name: props.param,
104108
type: props.type,
105109
value: props.value,
106110
masterKeyOnly: props.masterKeyOnly,
111+
selectedIndex: 0,
107112
};
108113
}
109114
}
@@ -169,6 +174,7 @@ export default class ConfigDialog extends React.Component {
169174
submit() {
170175
this.props.onConfirm({
171176
name: this.state.name,
177+
type: this.state.type,
172178
value: GET_VALUE[this.state.type](this.state.value),
173179
masterKeyOnly: this.state.masterKeyOnly,
174180
});
@@ -190,6 +196,28 @@ export default class ConfigDialog extends React.Component {
190196
))}
191197
</Dropdown>
192198
);
199+
const configHistory = localStorage.getItem(`${this.context.applicationId}_configHistory`) && JSON.parse(localStorage.getItem(`${this.context.applicationId}_configHistory`))[this.state.name];
200+
const handleIndexChange = index => {
201+
if(this.state.type === 'Date'){
202+
return;
203+
}
204+
let value = configHistory[index].value;
205+
if(this.state.type === 'File'){
206+
const fileJSON = {
207+
__type: 'File',
208+
name: value.name,
209+
url: value.url
210+
};
211+
const file = Parse.File.fromJSON(fileJSON);
212+
this.setState({ selectedIndex: index, value: file });
213+
return;
214+
}
215+
if(typeof value === 'object'){
216+
value = JSON.stringify(value);
217+
}
218+
this.setState({ selectedIndex: index, value });
219+
};
220+
193221
return (
194222
<Modal
195223
type={Modal.Types.INFO}
@@ -253,6 +281,29 @@ export default class ConfigDialog extends React.Component {
253281
/>
254282
) : null
255283
}
284+
{
285+
configHistory?.length > 0 &&
286+
<Field
287+
label={
288+
<Label
289+
text="Change History"
290+
description="Select a timestamp in the change history to preview the value in the 'Value' field before saving."
291+
/>
292+
}
293+
input={
294+
<Dropdown
295+
value={this.state.selectedIndex}
296+
onChange={handleIndexChange}>
297+
{configHistory.map((value, i) =>
298+
<Option key={i} value={i}>
299+
{dateStringUTC(new Date(value.time))}
300+
</Option>
301+
)}
302+
</Dropdown>
303+
}
304+
className={styles.addColumnToggleWrapper}
305+
/>
306+
}
256307
</Modal>
257308
);
258309
}

src/lib/ParseApp.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export default class ParseApp {
4848
columnPreference,
4949
scripts,
5050
classPreference,
51-
enableSecurityChecks
51+
enableSecurityChecks,
52+
cloudConfigHistoryLimit
5253
}) {
5354
this.name = appName;
5455
this.createdAt = created_at ? new Date(created_at) : new Date();
@@ -77,6 +78,7 @@ export default class ParseApp {
7778
this.columnPreference = columnPreference;
7879
this.scripts = scripts;
7980
this.enableSecurityChecks = !!enableSecurityChecks;
81+
this.cloudConfigHistoryLimit = cloudConfigHistoryLimit;
8082

8183
if (!supportedPushLocales) {
8284
console.warn(

0 commit comments

Comments
 (0)