Skip to content
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Prevent columns sorting](#prevent-columns-sorting)
- [Custom order in the filter popup](#custom-order-in-the-filter-popup)
- [Persistent Filters](#persistent-filters)
- [Keyboard Shortcuts](#keyboard-shortcuts)
- [Scripts](#scripts)
- [Resource Cache](#resource-cache)
- [Running as Express Middleware](#running-as-express-middleware)
Expand Down Expand Up @@ -530,6 +531,12 @@ For example:

You can conveniently create a filter definition without having to write it by hand by first saving a filter in the data browser, then exporting the filter definition under *App Settings > Export Class Preferences*.

### Keyboard Shortcuts

Configure custom keyboard shortcuts for dashboard actions in **App Settings > Keyboard Shortcuts**.

Delete a shortcut key to disable the shortcut.

### Scripts

You can specify scripts to execute Cloud Functions with the `scripts` option:
Expand Down
4 changes: 4 additions & 0 deletions src/components/TextInput/TextInput.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class TextInput extends React.Component {
value={this.props.value}
onChange={this.changeValue.bind(this)}
onBlur={this.updateValue.bind(this)}
onFocus={this.props.onFocus}
maxLength={this.props.maxLength}
/>
);
}
Expand All @@ -87,11 +89,13 @@ TextInput.propTypes = {
'A function fired when the input is changed. It receives the new value as its only parameter.'
),
onBlur: PropTypes.func.describe('A function fired when the input is blurred.'),
onFocus: PropTypes.func.describe('A function fired when the input is focused.'),
placeholder: PropTypes.string.describe('A placeholder string, for when the input is empty'),
value: PropTypes.string.describe('The current value of the controlled input'),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).describe(
'The height of the field. Can be a string containing any CSS unit, or a number of pixels. Default is 80px.'
),
maxLength: PropTypes.number.describe('The maximum length of the input.'),
};

export default withForwardedRef(TextInput);
5 changes: 5 additions & 0 deletions src/components/TextInput/TextInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
vertical-align: top;
resize: both;

@include placeholder {
color: #999;
opacity: 1;
}

&:disabled {
color: $mainTextColor;
}
Expand Down
7 changes: 4 additions & 3 deletions src/dashboard/Dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import RestConsole from './Data/ApiConsole/RestConsole.react';
import Retention from './Analytics/Retention/Retention.react';
import SchemaOverview from './Data/Browser/SchemaOverview.react';
import SecuritySettings from './Settings/SecuritySettings.react';
import SettingsData from './Settings/SettingsData.react';
import SlowQueries from './Analytics/SlowQueries/SlowQueries.react';
import styles from 'dashboard/Apps/AppsIndex.scss';
import UsersSettings from './Settings/UsersSettings.react';
Expand All @@ -55,6 +54,7 @@ import { Helmet } from 'react-helmet';
import Playground from './Data/Playground/Playground.react';
import DashboardSettings from './Settings/DashboardSettings/DashboardSettings.react';
import Security from './Settings/Security/Security.react';
import KeyboardShortcutsSettings from './Settings/KeyboardShortcutsSettings.react';
import semver from 'semver';
import packageInfo from '../../package.json';

Expand Down Expand Up @@ -232,16 +232,17 @@ export default class Dashboard extends React.Component {
);

const SettingsRoute = (
<Route element={<SettingsData />}>
<>
<Route path="dashboard" element={<DashboardSettings />} />
<Route path="security" element={<Security />} />
<Route path="keyboard-shortcuts" element={<KeyboardShortcutsSettings />} />
<Route path="general" element={<GeneralSettings />} />
<Route path="keys" element={<SecuritySettings />} />
<Route path="users" element={<UsersSettings />} />
<Route path="push" element={<PushSettings />} />
<Route path="hosting" element={<HostingSettings />} />
<Route index element={<Navigate replace to="dashboard" />} />
</Route>
</>
);

const JobsRoute = (
Expand Down
22 changes: 18 additions & 4 deletions src/dashboard/DashboardView.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,22 @@ export default class DashboardView extends React.Component {

onRouteChanged() {
const path = this.props.location?.pathname ?? window.location.pathname;
const route = path.split('apps')[1].split('/')[2];
const route = path.split('apps')[1]?.split('/')[2] || '';

if (route !== this.state.route) {
this.setState({ route });
}
}

getCurrentRoute() {
// If state.route is set, use it; otherwise extract from current location
if (this.state.route) {
return this.state.route;
}
const path = this.props.location?.pathname ?? window.location.pathname;
return path.split('apps')[1]?.split('/')[2] || '';
}

render() {
let sidebarChildren = null;
if (typeof this.renderSidebar === 'function') {
Expand Down Expand Up @@ -212,6 +221,10 @@ export default class DashboardView extends React.Component {
name: 'Dashboard',
link: '/settings/dashboard',
},
{
name: 'Keyboard Shortcuts',
link: '/settings/keyboard-shortcuts',
},
];

if (this.context.enableSecurityChecks) {
Expand Down Expand Up @@ -313,9 +326,10 @@ export default class DashboardView extends React.Component {
);

let content = <div className={styles.content}>{this.renderContent()}</div>;
const canRoute = [...coreSubsections, ...pushSubsections, ...settingsSections]
.map(({ link }) => link.split('/')[1])
.includes(this.state.route);
const allSections = [...coreSubsections, ...pushSubsections, ...settingsSections];
const validRoutes = allSections.map(({ link }) => link.split('/')[1]);
const currentRoute = this.getCurrentRoute();
const canRoute = validRoutes.includes(currentRoute);

if (!canRoute) {
content = (
Expand Down
41 changes: 40 additions & 1 deletion src/dashboard/Data/Browser/DataBrowser.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React from 'react';
import { ResizableBox } from 'react-resizable';
import ScriptConfirmationModal from '../../../components/ScriptConfirmationModal/ScriptConfirmationModal.react';
import styles from './Databrowser.scss';
import KeyboardShortcutsManager, { matchesShortcut } from 'lib/KeyboardShortcutsPreferences';

import AggregationPanel from '../../../components/AggregationPanel/AggregationPanel';

Expand Down Expand Up @@ -146,6 +147,7 @@ export default class DataBrowser extends React.Component {
multiPanelData: {}, // Object mapping objectId to panel data
_objectsToFetch: [], // Temporary field for async fetch handling
loadingObjectIds: new Set(),
keyboardShortcuts: null, // Keyboard shortcuts from server
showScriptConfirmationDialog: false,
selectedScript: null,
contextMenuX: null,
Expand Down Expand Up @@ -260,9 +262,18 @@ export default class DataBrowser extends React.Component {
this.checkClassNameChange(this.state.prevClassName, props.className);
}

componentDidMount() {
async componentDidMount() {
document.body.addEventListener('keydown', this.handleKey);
window.addEventListener('resize', this.updateMaxWidth);

// Load keyboard shortcuts from server
try {
const manager = new KeyboardShortcutsManager(this.props.app);
const shortcuts = await manager.getKeyboardShortcuts(this.props.app.applicationId);
this.setState({ keyboardShortcuts: shortcuts });
} catch (error) {
console.warn('Failed to load keyboard shortcuts:', error);
}
}

componentWillUnmount() {
Expand Down Expand Up @@ -849,6 +860,34 @@ export default class DataBrowser extends React.Component {
}
break;
}
default: {
// Handle custom keyboard shortcuts from server
const shortcuts = this.state.keyboardShortcuts;
if (!shortcuts) {
break;
}

// Reload data shortcut (only if enabled)
if (matchesShortcut(e, shortcuts.dataBrowserReloadData)) {
this.handleRefresh();
e.preventDefault();
break;
}

// Toggle panels shortcut (only if enabled and class has info panels configured)
if (matchesShortcut(e, shortcuts.dataBrowserToggleInfoPanels)) {
const hasAggregation =
this.props.classwiseCloudFunctions?.[
`${this.props.app.applicationId}${this.props.appName}`
]?.[this.props.className];
if (hasAggregation) {
this.togglePanelVisibility();
e.preventDefault();
}
break;
}
break;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export default class DashboardSettings extends DashboardView {
{this.viewPreferencesManager && this.scriptManager && this.viewPreferencesManager.isServerConfigEnabled() && (
<Fieldset legend="Settings Storage">
<div style={{ marginBottom: '20px', color: '#666', fontSize: '14px', textAlign: 'center' }}>
Storing dashboard settings on the server rather than locally in the browser storage makes the settings available across devices and browsers. It also prevents them from getting lost when resetting the browser website data. Settings that can be stored on the server are currently Views and JS Console scripts.
Storing dashboard settings on the server rather than locally in the browser storage makes the settings available across devices and browsers. It also prevents them from getting lost when resetting the browser website data. Settings that can be stored on the server are currently Views, Keyboard Shortcuts and JS Console scripts.
</div>
<Field
label={
Expand Down
Loading
Loading