Skip to content

Commit 666e078

Browse files
authored
feat: Add App Settings option to store dashboard settings on server (#2958)
1 parent 4da06c7 commit 666e078

File tree

6 files changed

+810
-46
lines changed

6 files changed

+810
-46
lines changed

src/dashboard/Data/Views/Views.react.js

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Notification from 'dashboard/Data/Browser/Notification.react';
1414
import TableView from 'dashboard/TableView.react';
1515
import tableStyles from 'dashboard/TableView.scss';
1616
import * as ViewPreferences from 'lib/ViewPreferences';
17+
import ViewPreferencesManager from 'lib/ViewPreferencesManager';
1718
import generatePath from 'lib/generatePath';
1819
import stringCompare from 'lib/stringCompare';
1920
import { ActionTypes as SchemaActionTypes } from 'lib/stores/SchemaStore';
@@ -37,6 +38,7 @@ class Views extends TableView {
3738
this.section = 'Core';
3839
this.subsection = 'Views';
3940
this._isMounted = false;
41+
this.viewPreferencesManager = null; // Will be initialized when context is available
4042
this.state = {
4143
views: [],
4244
counts: {},
@@ -83,49 +85,65 @@ class Views extends TableView {
8385
}
8486
}
8587

86-
loadViews(app) {
87-
const views = ViewPreferences.getViews(app.applicationId);
88-
this.setState({ views, counts: {} }, () => {
89-
views.forEach(view => {
90-
if (view.showCounter) {
91-
if (view.cloudFunction) {
92-
// For Cloud Function views, call the function to get count
93-
Parse.Cloud.run(view.cloudFunction, {}, { useMasterKey: true })
94-
.then(res => {
95-
if (this._isMounted) {
96-
this.setState(({ counts }) => ({
97-
counts: { ...counts, [view.name]: Array.isArray(res) ? res.length : 0 },
98-
}));
99-
}
100-
})
101-
.catch(error => {
102-
if (this._isMounted) {
103-
this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
104-
}
105-
});
106-
} else if (view.query && Array.isArray(view.query)) {
107-
// For aggregation pipeline views, use existing logic
108-
new Parse.Query(view.className)
109-
.aggregate(view.query, { useMasterKey: true })
110-
.then(res => {
111-
if (this._isMounted) {
112-
this.setState(({ counts }) => ({
113-
counts: { ...counts, [view.name]: res.length },
114-
}));
115-
}
116-
})
117-
.catch(error => {
118-
if (this._isMounted) {
119-
this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
120-
}
121-
});
88+
async loadViews(app) {
89+
// Initialize ViewPreferencesManager if not already done or if app changed
90+
if (!this.viewPreferencesManager || this.viewPreferencesManager.app !== app) {
91+
this.viewPreferencesManager = new ViewPreferencesManager(app);
92+
}
93+
94+
try {
95+
const views = await this.viewPreferencesManager.getViews(app.applicationId);
96+
this.setState({ views, counts: {} }, () => {
97+
views.forEach(view => {
98+
if (view.showCounter) {
99+
if (view.cloudFunction) {
100+
// For Cloud Function views, call the function to get count
101+
Parse.Cloud.run(view.cloudFunction, {}, { useMasterKey: true })
102+
.then(res => {
103+
if (this._isMounted) {
104+
this.setState(({ counts }) => ({
105+
counts: { ...counts, [view.name]: Array.isArray(res) ? res.length : 0 },
106+
}));
107+
}
108+
})
109+
.catch(error => {
110+
if (this._isMounted) {
111+
this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
112+
}
113+
});
114+
} else if (view.query && Array.isArray(view.query)) {
115+
// For aggregation pipeline views, use existing logic
116+
new Parse.Query(view.className)
117+
.aggregate(view.query, { useMasterKey: true })
118+
.then(res => {
119+
if (this._isMounted) {
120+
this.setState(({ counts }) => ({
121+
counts: { ...counts, [view.name]: res.length },
122+
}));
123+
}
124+
})
125+
.catch(error => {
126+
if (this._isMounted) {
127+
this.showNote(`Request failed: ${error.message || 'Unknown error occurred'}`, true);
128+
}
129+
});
130+
}
122131
}
132+
});
133+
if (this._isMounted) {
134+
this.loadData(this.props.params.name);
123135
}
124136
});
125-
if (this._isMounted) {
126-
this.loadData(this.props.params.name);
127-
}
128-
});
137+
} catch (error) {
138+
console.error('Failed to load views from server, falling back to local storage:', error);
139+
// Fallback to local storage
140+
const views = ViewPreferences.getViews(app.applicationId);
141+
this.setState({ views, counts: {} }, () => {
142+
if (this._isMounted) {
143+
this.loadData(this.props.params.name);
144+
}
145+
});
146+
}
129147
}
130148

131149
loadData(name) {
@@ -671,8 +689,15 @@ class Views extends TableView {
671689
onConfirm={view => {
672690
this.setState(
673691
state => ({ showCreate: false, views: [...state.views, view] }),
674-
() => {
675-
ViewPreferences.saveViews(this.context.applicationId, this.state.views);
692+
async () => {
693+
if (this.viewPreferencesManager) {
694+
try {
695+
await this.viewPreferencesManager.saveViews(this.context.applicationId, this.state.views);
696+
} catch (error) {
697+
console.error('Failed to save views:', error);
698+
this.showNote('Failed to save view changes', true);
699+
}
700+
}
676701
this.loadViews(this.context);
677702
}
678703
);
@@ -699,8 +724,15 @@ class Views extends TableView {
699724
newViews[state.editIndex] = view;
700725
return { editView: null, editIndex: null, views: newViews };
701726
},
702-
() => {
703-
ViewPreferences.saveViews(this.context.applicationId, this.state.views);
727+
async () => {
728+
if (this.viewPreferencesManager) {
729+
try {
730+
await this.viewPreferencesManager.saveViews(this.context.applicationId, this.state.views);
731+
} catch (error) {
732+
console.error('Failed to save views:', error);
733+
this.showNote('Failed to save view changes', true);
734+
}
735+
}
704736
this.loadViews(this.context);
705737
}
706738
);
@@ -719,8 +751,15 @@ class Views extends TableView {
719751
const newViews = state.views.filter((_, i) => i !== state.deleteIndex);
720752
return { deleteIndex: null, views: newViews };
721753
},
722-
() => {
723-
ViewPreferences.saveViews(this.context.applicationId, this.state.views);
754+
async () => {
755+
if (this.viewPreferencesManager) {
756+
try {
757+
await this.viewPreferencesManager.saveViews(this.context.applicationId, this.state.views);
758+
} catch (error) {
759+
console.error('Failed to save views:', error);
760+
this.showNote('Failed to save view changes', true);
761+
}
762+
}
724763
if (this.props.params.name === name) {
725764
const path = generatePath(this.context, 'views');
726765
this.props.navigate(path);

src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react';
1717
import Notification from 'dashboard/Data/Browser/Notification.react';
1818
import * as ColumnPreferences from 'lib/ColumnPreferences';
1919
import * as ClassPreferences from 'lib/ClassPreferences';
20+
import ViewPreferencesManager from 'lib/ViewPreferencesManager';
2021
import bcrypt from 'bcryptjs';
2122
import * as OTPAuth from 'otpauth';
2223
import QRCode from 'qrcode';
@@ -26,6 +27,7 @@ export default class DashboardSettings extends DashboardView {
2627
super();
2728
this.section = 'App Settings';
2829
this.subsection = 'Dashboard Configuration';
30+
this.viewPreferencesManager = null;
2931

3032
this.state = {
3133
createUserInput: false,
@@ -39,6 +41,8 @@ export default class DashboardSettings extends DashboardView {
3941
message: null,
4042
passwordInput: '',
4143
passwordHidden: true,
44+
migrationLoading: false,
45+
storagePreference: 'local', // Will be updated in componentDidMount
4246
copyData: {
4347
data: '',
4448
show: false,
@@ -52,6 +56,81 @@ export default class DashboardSettings extends DashboardView {
5256
};
5357
}
5458

59+
componentDidMount() {
60+
this.initializeViewPreferencesManager();
61+
}
62+
63+
initializeViewPreferencesManager() {
64+
if (this.context) {
65+
this.viewPreferencesManager = new ViewPreferencesManager(this.context);
66+
this.loadStoragePreference();
67+
}
68+
}
69+
70+
loadStoragePreference() {
71+
if (this.viewPreferencesManager) {
72+
const preference = this.viewPreferencesManager.getStoragePreference(this.context.applicationId);
73+
this.setState({ storagePreference: preference });
74+
}
75+
}
76+
77+
handleStoragePreferenceChange(preference) {
78+
if (this.viewPreferencesManager) {
79+
this.viewPreferencesManager.setStoragePreference(this.context.applicationId, preference);
80+
this.setState({ storagePreference: preference });
81+
82+
// Show a notification about the change
83+
this.showNote(`Storage preference changed to ${preference === 'server' ? 'server' : 'browser'}`);
84+
}
85+
}
86+
87+
async migrateToServer() {
88+
if (!this.viewPreferencesManager) {
89+
this.showNote('ViewPreferencesManager not initialized');
90+
return;
91+
}
92+
93+
if (!this.viewPreferencesManager.isServerConfigEnabled()) {
94+
this.showNote('Server configuration is not enabled for this app. Please add a "config" section to your app configuration.');
95+
return;
96+
}
97+
98+
this.setState({ migrationLoading: true });
99+
100+
try {
101+
const result = await this.viewPreferencesManager.migrateToServer(this.context.applicationId);
102+
if (result.success) {
103+
if (result.viewCount > 0) {
104+
this.showNote(`Successfully migrated ${result.viewCount} view(s) to server storage.`);
105+
} else {
106+
this.showNote('No views found to migrate.');
107+
}
108+
}
109+
} catch (error) {
110+
this.showNote(`Failed to migrate views: ${error.message}`);
111+
} finally {
112+
this.setState({ migrationLoading: false });
113+
}
114+
}
115+
116+
async deleteFromBrowser() {
117+
if (!window.confirm('Are you sure you want to delete all dashboard settings from browser storage? This action cannot be undone.')) {
118+
return;
119+
}
120+
121+
if (!this.viewPreferencesManager) {
122+
this.showNote('ViewPreferencesManager not initialized');
123+
return;
124+
}
125+
126+
const success = this.viewPreferencesManager.deleteFromBrowser(this.context.applicationId);
127+
if (success) {
128+
this.showNote('Successfully deleted views from browser storage.');
129+
} else {
130+
this.showNote('Failed to delete views from browser storage.');
131+
}
132+
}
133+
55134
getColumns() {
56135
const data = ColumnPreferences.getAllPreferences(this.context.applicationId);
57136
this.setState({
@@ -382,6 +461,61 @@ export default class DashboardSettings extends DashboardView {
382461
}
383462
/>
384463
</Fieldset>
464+
{this.viewPreferencesManager && this.viewPreferencesManager.isServerConfigEnabled() && (
465+
<Fieldset legend="Settings Storage">
466+
<Field
467+
label={
468+
<Label
469+
text="Storage Location"
470+
description="Choose where your dashboard settings are stored and loaded from. Server storage allows sharing settings across devices and users, while Browser storage is local to this device."
471+
/>
472+
}
473+
input={
474+
<Toggle
475+
value={this.state.storagePreference}
476+
type={Toggle.Types.CUSTOM}
477+
optionLeft="local"
478+
optionRight="server"
479+
labelLeft="Browser"
480+
labelRight="Server"
481+
colored={true}
482+
onChange={(preference) => this.handleStoragePreferenceChange(preference)}
483+
/>
484+
}
485+
/>
486+
<Field
487+
label={
488+
<Label
489+
text="Migrate Settings to Server"
490+
description="Migrates your current browser-stored dashboard settings to the server. This does not change your storage preference - use the switch above to select the server as storage location after migration. ⚠️ This overwrites existing server settings."
491+
/>
492+
}
493+
input={
494+
<FormButton
495+
color="blue"
496+
value={this.state.migrationLoading ? 'Migrating...' : 'Migrate to Server'}
497+
disabled={this.state.migrationLoading}
498+
onClick={() => this.migrateToServer()}
499+
/>
500+
}
501+
/>
502+
<Field
503+
label={
504+
<Label
505+
text="Delete Settings from Browser"
506+
description="Removes your dashboard settings from the browser's local storage. This action is irreversible. Make sure to migrate your settings to server and test them first."
507+
/>
508+
}
509+
input={
510+
<FormButton
511+
color="red"
512+
value="Delete from Browser"
513+
onClick={() => this.deleteFromBrowser()}
514+
/>
515+
}
516+
/>
517+
</Fieldset>
518+
)}
385519
{this.state.copyData.show && copyData}
386520
{this.state.createUserInput && createUserInput}
387521
{this.state.newUser.show && userData}

src/lib/ParseApp.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default class ParseApp {
5050
classPreference,
5151
enableSecurityChecks,
5252
cloudConfigHistoryLimit,
53+
config,
5354
}) {
5455
this.name = appName;
5556
this.createdAt = created_at ? new Date(created_at) : new Date();
@@ -79,6 +80,7 @@ export default class ParseApp {
7980
this.scripts = scripts;
8081
this.enableSecurityChecks = !!enableSecurityChecks;
8182
this.cloudConfigHistoryLimit = cloudConfigHistoryLimit;
83+
this.config = config;
8284

8385
if (!supportedPushLocales) {
8486
console.warn(

0 commit comments

Comments
 (0)