diff --git a/src/dashboard/Dashboard.js b/src/dashboard/Dashboard.js
index 9993dc0722..516133dead 100644
--- a/src/dashboard/Dashboard.js
+++ b/src/dashboard/Dashboard.js
@@ -51,6 +51,7 @@ import { setBasePath } from 'lib/AJAX';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import Playground from './Data/Playground/Playground.react';
+import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.react';
const ShowSchemaOverview = false; //In progress features. Change false to true to work on this feature.
@@ -199,12 +200,13 @@ export default class Dashboard extends React.Component {
const SettingsRoute = (
}>
+ } />
} />
} />
} />
} />
} />
- } />
+ } />
)
diff --git a/src/dashboard/DashboardView.react.js b/src/dashboard/DashboardView.react.js
index 8402dcc04f..b224ac6778 100644
--- a/src/dashboard/DashboardView.react.js
+++ b/src/dashboard/DashboardView.react.js
@@ -198,7 +198,10 @@ export default class DashboardView extends React.Component {
}
*/
- let settingsSections = [];
+ const settingsSections = [{
+ name: 'Dashboard',
+ link: '/settings/dashboard'
+ }];
// Settings - nothing remotely like this in parse-server yet. Maybe it will arrive soon.
/*
@@ -292,7 +295,7 @@ export default class DashboardView extends React.Component {
);
let content =
{this.renderContent()}
;
- const canRoute = [...coreSubsections, ...pushSubsections]
+ const canRoute = [...coreSubsections, ...pushSubsections, ...settingsSections]
.map(({ link }) => link.split('/')[1])
.includes(this.state.route);
diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
new file mode 100644
index 0000000000..3dfc4d4bc7
--- /dev/null
+++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.react.js
@@ -0,0 +1,242 @@
+import DashboardView from 'dashboard/DashboardView.react';
+import Field from 'components/Field/Field.react';
+import Fieldset from 'components/Fieldset/Fieldset.react';
+import FlowView from 'components/FlowView/FlowView.react';
+import FormButton from 'components/FormButton/FormButton.react';
+import Label from 'components/Label/Label.react';
+import Button from 'components/Button/Button.react';
+import React from 'react';
+import styles from 'dashboard/Settings/DashboardSettings/DashboardSettings.scss';
+import TextInput from 'components/TextInput/TextInput.react';
+import Toggle from 'components/Toggle/Toggle.react';
+import Icon from 'components/Icon/Icon.react';
+import Dropdown from 'components/Dropdown/Dropdown.react';
+import Option from 'components/Dropdown/Option.react';
+import Toolbar from 'components/Toolbar/Toolbar.react';
+import CodeSnippet from 'components/CodeSnippet/CodeSnippet.react';
+import Notification from 'dashboard/Data/Browser/Notification.react';
+import * as ColumnPreferences from 'lib/ColumnPreferences';
+import bcrypt from 'bcryptjs';
+import * as OTPAuth from 'otpauth';
+import QRCode from 'qrcode';
+
+export default class DashboardSettings extends DashboardView {
+ constructor() {
+ super();
+ this.section = 'App Settings';
+ this.subsection = 'Dashboard Configuration';
+
+ this.state = {
+ createUserInput: false,
+ username: '',
+ password: '',
+ encrypt: true,
+ mfa: false,
+ mfaDigits: 6,
+ mfaPeriod: 30,
+ mfaAlgorithm: 'SHA1',
+ message: null,
+ passwordInput: '',
+ passwordHidden: true,
+ columnData: {
+ data: '',
+ show: false,
+ },
+ newUser: {
+ data: '',
+ show: false,
+ mfa: '',
+ },
+ };
+ }
+
+ getColumns() {
+ const data = ColumnPreferences.getAllPreferences(this.context.applicationId);
+ this.setState({
+ columnData: { data: JSON.stringify(data, null, 2), show: true },
+ });
+ }
+
+ copy(data, label) {
+ navigator.clipboard.writeText(data);
+ this.showNote(`${label} copied to clipboard`);
+ }
+
+ createUser() {
+ if (!this.state.username) {
+ this.showNote('Please enter a username');
+ return;
+ }
+ if (!this.state.password) {
+ this.showNote('Please enter a password');
+ return;
+ }
+
+ let pass = this.state.password;
+ if (this.state.encrypt) {
+ const salt = bcrypt.genSaltSync(10);
+ pass = bcrypt.hashSync(pass, salt);
+ }
+
+ const user = {
+ username: this.state.username,
+ pass,
+ };
+
+ let mfa;
+ if (this.state.mfa) {
+ const secret = new OTPAuth.Secret();
+ const totp = new OTPAuth.TOTP({
+ issuer: this.context.name,
+ label: user.username,
+ algorithm: this.state.mfaAlgorithm || 'SHA1',
+ digits: this.state.mfaDigits || 6,
+ period: this.state.mfaPeriod || 30,
+ secret,
+ });
+ mfa = totp.toString();
+ user.mfa = secret.base32;
+ if (totp.algorithm !== 'SHA1') {
+ user.mfaAlgorithm = totp.algorithm;
+ }
+ if (totp.digits != 6) {
+ user.mfaDigits = totp.digits;
+ }
+ if (totp.period != 30) {
+ user.mfaPeriod = totp.period;
+ }
+
+ setTimeout(() => {
+ const canvas = document.getElementById('canvas');
+ QRCode.toCanvas(canvas, mfa);
+ }, 10);
+ }
+
+ this.setState({
+ newUser: {
+ show: true,
+ data: JSON.stringify(user, null, 2),
+ mfa,
+ },
+ });
+ }
+
+ generatePassword() {
+ let chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+ let pwordLength = 20;
+ let password = '';
+
+ const array = new Uint32Array(chars.length);
+ window.crypto.getRandomValues(array);
+
+ for (let i = 0; i < pwordLength; i++) {
+ password += chars[array[i] % chars.length];
+ }
+ this.setState({ password });
+ }
+
+ showNote(message) {
+ if (!message) {
+ return;
+ }
+
+ clearTimeout(this.noteTimeout);
+
+ this.setState({ message });
+
+ this.noteTimeout = setTimeout(() => {
+ this.setState({ message: null });
+ }, 3500);
+ }
+
+ renderForm() {
+ const createUserInput = (
+
+ );
+ const columnPreferences = (
+
+
+
+
+
+
+
+ );
+ const userData = (
+
+ Add the following data to your Parse Dashboard configuration "users":
+ {this.state.encrypt &&
Make sure the dashboard option useEncryptedPasswords is set to true.
}
+
+
+
+ {this.state.mfa && (
+
+ )}
+
+ this.copy(this.state.newUser.data, 'New User')} />
+ this.setState({ username: '', password: '', passwordHidden: true, mfaAlgorithm: 'SHA1', mfaDigits: 6, mfaPeriod: 30, encrypt: true, createUserInput: false, newUser: { data: '', show: false } })} />
+
+
+ );
+ return (
+
+
+ {this.state.columnData.show && columnPreferences}
+ {this.state.createUserInput && createUserInput}
+ {this.state.newUser.show && userData}
+
+
+
+ );
+ }
+
+ renderContent() {
+ return {}} onSubmit={() => {}} renderForm={() => this.renderForm()} />;
+ }
+}
diff --git a/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss
new file mode 100644
index 0000000000..579c813055
--- /dev/null
+++ b/src/dashboard/Settings/DashboardSettings/DashboardSettings.scss
@@ -0,0 +1,28 @@
+.columnData {
+ max-height: 50vh;
+ overflow-y: scroll;
+}
+.newUser {
+ max-height: 100px;
+ overflow-y: scroll;
+}
+.settings_page {
+ padding: 120px 0 80px 0;
+}
+.footer {
+ display: flex;
+ padding: 10px;
+ justify-content: end;
+ gap: 10px;
+}
+.password {
+ display: flex;
+ gap: 4px;
+}
+.userData {
+ padding: 10px;
+}
+.mfa {
+ display: block;
+ margin-top: 10px;
+}
diff --git a/src/lib/ColumnPreferences.js b/src/lib/ColumnPreferences.js
index a9c691d0ef..78619dbe18 100644
--- a/src/lib/ColumnPreferences.js
+++ b/src/lib/ColumnPreferences.js
@@ -45,6 +45,23 @@ export function getPreferences(appId, className) {
}
}
+export function getAllPreferences(appId) {
+ const storageKeys = Object.keys(localStorage);
+ const result = {};
+ for (const key of storageKeys) {
+ const split = key.split(':')
+ if (split.length <= 1) {
+ continue;
+ }
+ const className = split.at(-1);
+ const preferences = getPreferences(appId, className);
+ if (preferences) {
+ result[className] = preferences;
+ }
+ }
+ return result;
+}
+
export function getColumnSort(sortBy, appId, className) {
let cachedSort = getPreferences(appId, COLUMN_SORT) || [ { name: className, value: DEFAULT_COLUMN_SORT } ];
let ordering = [].concat(cachedSort);
@@ -74,7 +91,7 @@ export function getColumnSort(sortBy, appId, className) {
export function getOrder(cols, appId, className, defaultPrefs) {
let prefs = getPreferences(appId, className) || [ { name: 'objectId', width: DEFAULT_WIDTH, visible: true, cached: true } ];
-
+
if (defaultPrefs) {
// Check that every default pref is in the prefs array.
@@ -85,7 +102,7 @@ export function getOrder(cols, appId, className, defaultPrefs) {
}
});
- // Iterate over the current prefs
+ // Iterate over the current prefs
prefs = prefs.map((prefsItem) => {
// Get the default prefs item.
const defaultPrefsItem = defaultPrefs.find(defaultPrefsItem => defaultPrefsItem.name === prefsItem.name) || {};