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
112 changes: 110 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Scripts](#scripts)
- [Resource Cache](#resource-cache)
- [Running as Express Middleware](#running-as-express-middleware)
- [Browser Control API (Development Only)](#browser-control-api-development-only)
- [Browser Control API (Development Only)](#browser-control-api-development-only)
- [⚠️ Security Requirements](#️-security-requirements)
- [Configuration](#configuration)
- [Usage](#usage)
- [Deploying Parse Dashboard](#deploying-parse-dashboard)
- [Preparing for Deployment](#preparing-for-deployment)
- [Security Considerations](#security-considerations)
Expand Down Expand Up @@ -84,13 +87,20 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Button Item](#button-item)
- [Panel Item](#panel-item)
- [Prefetching](#prefetching)
- [Graph](#graph)
- [Calculated Values](#calculated-values)
- [Formula Operator](#formula-operator)
- [Arithmetic Operators](#arithmetic-operators)
- [Comparison Operators](#comparison-operators)
- [Conditional Operator](#conditional-operator)
- [Math Functions](#math-functions)
- [Freeze Columns](#freeze-columns)
- [Browse as User](#browse-as-user)
- [Change Pointer Key](#change-pointer-key)
- [Limitations](#limitations)
- [CSV Export](#csv-export)
- [AI Agent](#ai-agent)
- [Configuration](#configuration)
- [Configuration](#configuration-1)
- [Providers](#providers)
- [OpenAI](#openai)
- [Views](#views)
Expand Down Expand Up @@ -1449,6 +1459,104 @@ Prefetching is particularly useful when navigating through lists of objects. To

When `prefetchObjects` is enabled, media content (images, videos, and audio) in the info panel can also be prefetched to improve loading performance. By default, all media types are prefetched, but you can selectively disable prefetching for specific media types using the `prefetchImage`, `prefetchVideo`, and `prefetchAudio` options.

### Graph

▶️ *Core > Browser > Graph*

The data browser includes a graph feature that allows you to visualize data in pie charts, bar charts, or line charts. You can configure calculated values to display aggregated or computed data.

#### Calculated Values

Calculated values allow you to derive new values from your data. The following operators are available:

| Operator | Description |
|----------|-------------|
| Sum | Sum of all values in the selected field |
| Percent | Percentage of numerator relative to denominator |
| Average | Average of all values in the selected field |
| Difference | Difference between two fields |
| Ratio | Ratio of numerator to denominator |
| Formula | Custom formula using mathematical expressions |

**Naming Rules:**
Calculated value names must follow Parse field naming conventions:
- Start with a letter or underscore
- Contain only letters, numbers, and underscores
- No spaces or special characters

#### Formula Operator

The Formula operator allows you to define custom calculations using a safe expression syntax. You can reference field values directly by their names.

**Syntax:**
- Use field names directly as variables (e.g., `price`, `quantity`)
- Reference previous calculated values by name (e.g., `profit`, `total_cost`)
- Optionally prefix field names with `$` (e.g., `$price`, `$quantity`)

> [!TIP]
> If a field name conflicts with a reserved function name (like `round`, `min`, `max`), prefix it with `$` to reference the field. For example, use `$round` to reference a field named "round": `round($round, 2)`.

**Example formulas:**
```
price * quantity # Multiply two fields
round(revenue / cost * 100, 2) # Calculate percentage with rounding
max(value, 0) # Floor at 0 (no negatives)
min(value, 100) # Cap at 100
score > 50 ? score : 0 # Conditional logic
round((revenue - cost) / revenue * 100, 1) # Profit margin calculation
```

##### Arithmetic Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `+` | Addition | `price + tax` |
| `-` | Subtraction | `revenue - cost` |
| `*` | Multiplication | `price * quantity` |
| `/` | Division | `total / count` |
| `%` | Modulo (remainder) | `value % 10` |
| `^` | Power | `base ^ 2` |
| `()` | Grouping | `(a + b) * c` |

##### Comparison Operators

Comparison operators return `1` for true and `0` for false.

| Operator | Description | Example |
|----------|-------------|---------|
| `>` | Greater than | `value > 100` |
| `<` | Less than | `value < 0` |
| `>=` | Greater than or equal | `value >= 50` |
| `<=` | Less than or equal | `value <= 100` |
| `==` | Equal | `status == 1` |
| `!=` | Not equal | `status != 0` |

##### Conditional Operator

| Operator | Description | Example |
|----------|-------------|---------|
| `? :` | Ternary conditional | `value > 0 ? value : 0` |

##### Math Functions

| Function | Description | Example |
|----------|-------------|---------|
| `round(value)` | Round to nearest integer | `round(3.7)` → `4` |
| `round(value, decimals)` | Round to decimal places | `round(3.14159, 2)` → `3.14` |
| `floor(value)` | Round down | `floor(3.7)` → `3` |
| `ceil(value)` | Round up | `ceil(3.2)` → `4` |
| `trunc(value)` | Truncate decimal part | `trunc(3.7)` → `3` |
| `abs(value)` | Absolute value | `abs(-5)` → `5` |
| `sign(value)` | Sign of number (-1, 0, 1) | `sign(-5)` → `-1` |
| `min(a, b, ...)` | Minimum value | `min(10, 5, 8)` → `5` |
| `max(a, b, ...)` | Maximum value | `max(10, 5, 8)` → `10` |
| `sqrt(value)` | Square root | `sqrt(16)` → `4` |
| `cbrt(value)` | Cube root | `cbrt(27)` → `3` |
| `exp(value)` | Exponential (e^x) | `exp(1)` → `2.718...` |
| `log(value)` | Natural logarithm | `log(2.718)` → `1` |
| `log10(value)` | Base-10 logarithm | `log10(100)` → `2` |
| `log2(value)` | Base-2 logarithm | `log2(8)` → `3` |

### Freeze Columns

▶️ *Core > Browser > Freeze column*
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"dependencies": {
"@babel/runtime": "7.28.4",
"@babel/runtime-corejs3": "7.28.3",
"expr-eval-fork": "3.0.1",
"bcryptjs": "3.0.3",
"body-parser": "2.2.1",
"chart.js": "4.5.1",
Expand Down
90 changes: 89 additions & 1 deletion src/dashboard/Data/Browser/GraphDialog.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Option from 'components/Dropdown/Option.react';
import Toggle from 'components/Toggle/Toggle.react';
import TextInput from 'components/TextInput/TextInput.react';
import styles from 'components/Modal/Modal.scss';
import { validateFormula } from 'lib/FormulaEvaluator';

const CHART_TYPES = [
{ value: 'bar', label: 'Bar Chart' },
Expand All @@ -42,6 +43,7 @@ const CALCULATED_VALUE_OPERATORS = [
{ value: 'average', label: 'Average' },
{ value: 'difference', label: 'Difference' },
{ value: 'ratio', label: 'Ratio' },
{ value: 'formula', label: 'Formula' },
];

export default class GraphDialog extends React.Component {
Expand Down Expand Up @@ -89,6 +91,15 @@ export default class GraphDialog extends React.Component {
const hasCalculatedValues = Array.isArray(calculatedValues) && calculatedValues.length > 0;
const hasValuesToDisplay = hasValueColumn || hasCalculatedValues;

// Check for any name errors in calculated values
if (hasCalculatedValues) {
for (let i = 0; i < calculatedValues.length; i++) {
if (this.getNameError(i)) {
return false;
}
}
}

switch (chartType) {
case 'pie':
case 'doughnut':
Expand Down Expand Up @@ -205,6 +216,58 @@ export default class GraphDialog extends React.Component {
return false;
}

// Validate formula and return error message if invalid
getFormulaError(calcIndex) {
const calc = this.state.calculatedValues[calcIndex];
if (!calc || calc.operator !== 'formula' || !calc.formula || calc.formula.trim() === '') {
return null;
}

// Build list of available variables for the formula
const numericColumns = this.getNumericColumns();
// Include previous calculated value names
const previousCalcNames = this.state.calculatedValues
.slice(0, calcIndex)
.filter(c => c.name && c.name.trim() !== '')
.map(c => c.name);

const availableVariables = [...numericColumns, ...previousCalcNames];

const validation = validateFormula(calc.formula, availableVariables);
return validation.isValid ? null : validation.error;
}

// Validate calculated value name (must follow Parse field name rules)
getNameError(calcIndex) {
const calc = this.state.calculatedValues[calcIndex];
if (!calc || !calc.name || calc.name.trim() === '') {
return null; // Empty names are allowed (will use default "Calculated Value N")
}

const name = calc.name.trim();

// Check for valid characters (alphanumeric and underscore only)
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
if (/^\d/.test(name)) {
return 'Name cannot start with a number';
}
if (/\s/.test(name)) {
return 'Name cannot contain spaces';
}
return 'Name can only contain letters, numbers, and underscores';
}

// Check for duplicate names
const duplicateIndex = this.state.calculatedValues.findIndex(
(c, idx) => idx !== calcIndex && c.name && c.name.trim() === name
);
if (duplicateIndex >= 0) {
return 'Name is already used by another calculated value';
}

return null;
}

addCalculatedValue = () => {
this.setState({
calculatedValues: [
Expand Down Expand Up @@ -335,6 +398,8 @@ export default class GraphDialog extends React.Component {
const numericAndCalculatedFields = this.getNumericAndCalculatedFields(index);
// Check for circular reference
const hasCircular = this.hasCircularReference(index);
// Check for formula errors
const formulaError = this.getFormulaError(index);

return (
<div key={index} style={{ paddingTop: '10px', paddingLeft: '10px', paddingRight: '10px', paddingBottom: isExpanded ? '0' : '10px', borderTop: '1px solid #e3e3e3', borderLeft: '1px solid #e3e3e3', borderRight: '1px solid #e3e3e3', borderBottom: index === this.state.calculatedValues.length - 1 ? '1px solid #e3e3e3' : 'none' }}>
Expand Down Expand Up @@ -377,7 +442,30 @@ export default class GraphDialog extends React.Component {
</Dropdown>
</div>
</div>
{calc.operator === 'percent' ? (
{calc.operator === 'formula' ? (
<>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', borderTop: '1px solid #e3e3e3' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Label text="Formula" description="e.g., price * quantity" />
</div>
<div>
<TextInput
value={calc.formula || ''}
onChange={formula => this.updateCalculatedValue(index, 'formula', formula)}
placeholder="e.g., round(price * quantity, 2)"
/>
</div>
</div>
{formulaError && (
<div style={{ borderTop: '1px solid #e3e3e3', padding: '12px', background: '#ffebee', color: '#c62828' }}>
<strong>Formula Error</strong>
<p style={{ margin: '4px 0 0 0', fontSize: '12px' }}>
{formulaError}
</p>
</div>
)}
</>
) : calc.operator === 'percent' ? (
<>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', borderTop: '1px solid #e3e3e3' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
Expand Down
Loading
Loading