Skip to content
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

feat: Add Cloud Config change history to roll back to previous values #2554

Merged
merged 21 commits into from
May 19, 2024
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Deploying Parse Dashboard](#deploying-parse-dashboard)
- [Preparing for Deployment](#preparing-for-deployment)
- [Security Considerations](#security-considerations)
- [Security Checks](#security-checks)
- [Configuring Basic Authentication](#configuring-basic-authentication)
- [Multi-Factor Authentication (One-Time Password)](#multi-factor-authentication-one-time-password)
- [Separating App Access Based on User Identity](#separating-app-access-based-on-user-identity)
Expand Down Expand Up @@ -123,6 +124,7 @@ Parse Dashboard is continuously tested with the most recent releases of Node.js
| `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`. |

### File

Expand Down Expand Up @@ -539,7 +541,7 @@ var dashboard = new ParseDashboard({
});
```

## Security Checks
### Security Checks

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

Expand All @@ -557,8 +559,6 @@ const dashboard = new ParseDashboard({
});
```



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

Expand Down
39 changes: 38 additions & 1 deletion src/dashboard/Data/Config/Config.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import TableHeader from 'components/Table/TableHeader.react';
import TableView from 'dashboard/TableView.react';
import Toolbar from 'components/Toolbar/Toolbar.react';
import browserStyles from 'dashboard/Data/Browser/Browser.scss';
import { CurrentApp } from 'context/currentApp';

@subscribeTo('Config', 'config')
class Config extends TableView {
static contextType = CurrentApp;
constructor() {
super();
this.section = 'Core';
Expand Down Expand Up @@ -242,7 +244,7 @@ class Config extends TableView {
return data;
}

saveParam({ name, value, masterKeyOnly }) {
saveParam({ name, value, type, masterKeyOnly }) {
this.props.config
.dispatch(ActionTypes.SET, {
param: name,
Expand All @@ -252,6 +254,32 @@ class Config extends TableView {
.then(
() => {
this.setState({ modalOpen: false });
const limit = this.context.cloudConfigHistoryLimit;
const applicationId = this.context.applicationId;
let transformedValue = value;
if(type === 'Date') {
transformedValue = {__type: 'Date', iso: value};
}
if(type === 'File') {
transformedValue = {name: value._name, url: value._url};
}
const configHistory = localStorage.getItem(`${applicationId}_configHistory`);
if(!configHistory) {
localStorage.setItem(`${applicationId}_configHistory`, JSON.stringify({
[name]: [{
time: new Date(),
value: transformedValue
}]
}));
} else {
const oldConfigHistory = JSON.parse(configHistory);
localStorage.setItem(`${applicationId}_configHistory`, JSON.stringify({
...oldConfigHistory,
[name]: !oldConfigHistory[name] ?
[{time: new Date(), value: transformedValue}]
: [{time: new Date(), value: transformedValue}, ...oldConfigHistory[name]].slice(0, limit || 100)
}));
}
},
() => {
// Catch the error
Expand All @@ -263,6 +291,15 @@ class Config extends TableView {
this.props.config.dispatch(ActionTypes.DELETE, { param: name }).then(() => {
this.setState({ showDeleteParameterDialog: false });
});
const configHistory = localStorage.getItem('configHistory') && JSON.parse(localStorage.getItem('configHistory'));
if(configHistory) {
delete configHistory[name];
if(Object.keys(configHistory).length === 0) {
localStorage.removeItem('configHistory');
} else {
localStorage.setItem('configHistory', JSON.stringify(configHistory));
}
}
}

createParameter() {
Expand Down
51 changes: 51 additions & 0 deletions src/dashboard/Data/Config/ConfigDialog.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import Toggle from 'components/Toggle/Toggle.react';
import validateNumeric from 'lib/validateNumeric';
import styles from 'dashboard/Data/Browser/Browser.scss';
import semver from 'semver/preload.js';
import { dateStringUTC } from 'lib/DateUtils';
import { CurrentApp } from 'context/currentApp';

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

Expand Down Expand Up @@ -90,20 +92,23 @@ const GET_VALUE = {
};

export default class ConfigDialog extends React.Component {
static contextType = CurrentApp;
constructor(props) {
super();
this.state = {
value: null,
type: 'String',
name: '',
masterKeyOnly: false,
selectedIndex: null,
};
if (props.param.length > 0) {
this.state = {
name: props.param,
type: props.type,
value: props.value,
masterKeyOnly: props.masterKeyOnly,
selectedIndex: 0,
};
}
}
Expand Down Expand Up @@ -169,6 +174,7 @@ export default class ConfigDialog extends React.Component {
submit() {
this.props.onConfirm({
name: this.state.name,
type: this.state.type,
value: GET_VALUE[this.state.type](this.state.value),
masterKeyOnly: this.state.masterKeyOnly,
});
Expand All @@ -190,6 +196,28 @@ export default class ConfigDialog extends React.Component {
))}
</Dropdown>
);
const configHistory = localStorage.getItem(`${this.context.applicationId}_configHistory`) && JSON.parse(localStorage.getItem(`${this.context.applicationId}_configHistory`))[this.state.name];
const handleIndexChange = index => {
if(this.state.type === 'Date'){
return;
}
let value = configHistory[index].value;
if(this.state.type === 'File'){
const fileJSON = {
__type: 'File',
name: value.name,
url: value.url
};
const file = Parse.File.fromJSON(fileJSON);
this.setState({ selectedIndex: index, value: file });
return;
}
if(typeof value === 'object'){
value = JSON.stringify(value);
}
this.setState({ selectedIndex: index, value });
};

return (
<Modal
type={Modal.Types.INFO}
Expand Down Expand Up @@ -253,6 +281,29 @@ export default class ConfigDialog extends React.Component {
/>
) : null
}
{
configHistory?.length > 0 &&
<Field
label={
<Label
text="Change History"
description="Select a timestamp in the change history to preview the value in the 'Value' field before saving."
/>
}
input={
<Dropdown
value={this.state.selectedIndex}
onChange={handleIndexChange}>
{configHistory.map((value, i) =>
<Option key={i} value={i}>
{dateStringUTC(new Date(value.time))}
</Option>
)}
</Dropdown>
}
className={styles.addColumnToggleWrapper}
/>
}
</Modal>
);
}
Expand Down
4 changes: 3 additions & 1 deletion src/lib/ParseApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export default class ParseApp {
columnPreference,
scripts,
classPreference,
enableSecurityChecks
enableSecurityChecks,
cloudConfigHistoryLimit
}) {
this.name = appName;
this.createdAt = created_at ? new Date(created_at) : new Date();
Expand Down Expand Up @@ -77,6 +78,7 @@ export default class ParseApp {
this.columnPreference = columnPreference;
this.scripts = scripts;
this.enableSecurityChecks = !!enableSecurityChecks;
this.cloudConfigHistoryLimit = cloudConfigHistoryLimit;

if (!supportedPushLocales) {
console.warn(
Expand Down
Loading