Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
- [Limitations](#limitations)
- [CSV Export](#csv-export)
- [Views](#views)
- [Data Sources](#data-sources)
- [Aggregation Pipeline](#aggregation-pipeline)
- [Cloud Function](#cloud-function)
- [View Table](#view-table)
- [Pointer](#pointer)
- [Link](#link)
Expand Down Expand Up @@ -1260,11 +1263,23 @@ This feature will take either selected rows or all rows of an individual class a

▶️ *Core > Views*

Views are saved queries that display aggregated data from your classes. Create a view by providing a name, selecting a class and defining an aggregation pipeline. Optionally enable the object counter to show how many items match the view. Saved views appear in the sidebar, where you can select, edit, or delete them.
Views are saved queries that display data in a table format. Saved views appear in the sidebar, where you can select, edit, or delete them. Optionally you can enable the object counter to show in the sidebar how many items match the view.

> [!Caution]
> Values are generally rendered without sanitization in the resulting data table. If rendered values come from user input or untrusted data, make sure to remove potentially dangerous HTML or JavaScript, to prevent an attacker from injecting malicious code, to exploit vulnerabilities like Cross-Site Scripting (XSS).

### Data Sources

Views can pull their data from the following data sources.

#### Aggregation Pipeline

Display aggregated data from your classes using a MongoDB aggregation pipeline. Create a view by selecting a class and defining an aggregation pipeline.

#### Cloud Function

Display data returned by a Parse Cloud Function. Create a view specifying a Cloud Function that returns an array of objects. Cloud Functions enable custom business logic, computed fields, and complex data transformations.

### View Table

When designing the aggregation pipeline, consider that some values are rendered specially in the output table.
Expand Down
95 changes: 71 additions & 24 deletions src/dashboard/Data/Views/CreateViewDialog.react.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import Checkbox from 'components/Checkbox/Checkbox.react';
import Dropdown from 'components/Dropdown/Dropdown.react';
import Option from 'components/Dropdown/Option.react';
import Field from 'components/Field/Field.react';
import Label from 'components/Label/Label.react';
import Modal from 'components/Modal/Modal.react';
import Option from 'components/Dropdown/Option.react';
import React from 'react';
import TextInput from 'components/TextInput/TextInput.react';
import Checkbox from 'components/Checkbox/Checkbox.react';
import React from 'react';

/**
* The data source types available for views.
*
* @param {string} query An aggregation pipeline query data source.
* @param {string} cloudFunction A Cloud Function data source.
*/
const DataSourceTypes = {
query: 'query',
cloudFunction: 'cloudFunction'
};

function isValidJSON(value) {
try {
Expand All @@ -22,17 +33,28 @@ export default class CreateViewDialog extends React.Component {
this.state = {
name: '',
className: '',
dataSourceType: DataSourceTypes.query,
query: '[]',
cloudFunction: '',
showCounter: false,
};
}

valid() {
return (
this.state.name.length > 0 &&
this.state.className.length > 0 &&
isValidJSON(this.state.query)
);
if (this.state.dataSourceType === DataSourceTypes.query) {
return (
this.state.name.length > 0 &&
this.state.className.length > 0 &&
this.state.query.trim() !== '' &&
this.state.query !== '[]' &&
isValidJSON(this.state.query)
);
} else {
return (
this.state.name.length > 0 &&
this.state.cloudFunction.trim() !== ''
);
}
}

render() {
Expand All @@ -43,16 +65,17 @@ export default class CreateViewDialog extends React.Component {
icon="plus"
iconSize={40}
title="Create a new view?"
subtitle="Define a custom query to display data."
subtitle="Define a data source to display data."
confirmText="Create"
cancelText="Cancel"
disabled={!this.valid()}
onCancel={onCancel}
onConfirm={() =>
onConfirm({
name: this.state.name,
className: this.state.className,
query: JSON.parse(this.state.query),
className: this.state.dataSourceType === DataSourceTypes.query ? this.state.className : null,
query: this.state.dataSourceType === DataSourceTypes.query ? JSON.parse(this.state.query) : null,
cloudFunction: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.cloudFunction : null,
showCounter: this.state.showCounter,
})
}
Expand All @@ -67,32 +90,56 @@ export default class CreateViewDialog extends React.Component {
}
/>
<Field
label={<Label text="Class" />}
label={<Label text="Data Source" />}
input={
<Dropdown
value={this.state.className}
onChange={className => this.setState({ className })}
value={this.state.dataSourceType}
onChange={dataSourceType => this.setState({ dataSourceType })}
>
{classes.map(c => (
<Option key={c} value={c}>
{c}
</Option>
))}
<Option value={DataSourceTypes.query}>Aggregation Pipeline</Option>
<Option value={DataSourceTypes.cloudFunction}>Cloud Function</Option>
</Dropdown>
}
/>
{this.state.dataSourceType === DataSourceTypes.query && (
<Field
label={<Label text="Class" />}
input={
<Dropdown
value={this.state.className}
onChange={className => this.setState({ className })}
>
{classes.map(c => (
<Option key={c} value={c}>
{c}
</Option>
))}
</Dropdown>
}
/>
)}
<Field
label={
<Label
text="Query"
description="An aggregation pipeline that returns an array of items."
text={this.state.dataSourceType === DataSourceTypes.query ? 'Query' : 'Cloud Function'}
description={
this.state.dataSourceType === DataSourceTypes.query
? 'An aggregation pipeline that returns an array of items.'
: 'A Parse Cloud Function that returns an array of items.'
}
/>
}
input={
<TextInput
multiline={true}
value={this.state.query}
onChange={query => this.setState({ query })}
multiline={this.state.dataSourceType === DataSourceTypes.query}
value={this.state.dataSourceType === DataSourceTypes.query ? this.state.query : this.state.cloudFunction}
onChange={value =>
this.setState(
this.state.dataSourceType === DataSourceTypes.query
? { query: value }
: { cloudFunction: value }
)
}
/>
}
/>
Expand Down
95 changes: 70 additions & 25 deletions src/dashboard/Data/Views/EditViewDialog.react.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Checkbox from 'components/Checkbox/Checkbox.react';
import Dropdown from 'components/Dropdown/Dropdown.react';
import Option from 'components/Dropdown/Option.react';
import Field from 'components/Field/Field.react';
import Label from 'components/Label/Label.react';
import Modal from 'components/Modal/Modal.react';
import Option from 'components/Dropdown/Option.react';
import React from 'react';
import TextInput from 'components/TextInput/TextInput.react';
import Checkbox from 'components/Checkbox/Checkbox.react';
import React from 'react';

function isValidJSON(value) {
try {
Expand All @@ -20,20 +20,40 @@
constructor(props) {
super();
const view = props.view || {};

Check failure on line 23 in src/dashboard/Data/Views/EditViewDialog.react.js

View workflow job for this annotation

GitHub Actions / Lint

Trailing spaces not allowed
// Determine data source type based on existing view properties
let dataSourceType = 'query'; // default
if (view.cloudFunction) {
dataSourceType = 'cloudFunction';
} else if (view.query && Array.isArray(view.query) && view.query.length > 0) {
dataSourceType = 'query';
}

Check failure on line 31 in src/dashboard/Data/Views/EditViewDialog.react.js

View workflow job for this annotation

GitHub Actions / Lint

Trailing spaces not allowed
this.state = {
name: view.name || '',
className: view.className || '',
query: JSON.stringify(view.query || [], null, 2),
dataSourceType,
query: view.query ? JSON.stringify(view.query, null, 2) : '[]',
cloudFunction: view.cloudFunction || '',
showCounter: !!view.showCounter,
};
}

valid() {
return (
this.state.name.length > 0 &&
this.state.className.length > 0 &&
isValidJSON(this.state.query)
);
if (this.state.dataSourceType === 'query') {
return (
this.state.name.length > 0 &&
this.state.className.length > 0 &&
this.state.query.trim() !== '' &&
this.state.query !== '[]' &&
isValidJSON(this.state.query)
);
} else {
return (
this.state.name.length > 0 &&
this.state.cloudFunction.trim() !== ''
);
}
}

render() {
Expand All @@ -44,16 +64,17 @@
icon="edit-solid"
iconSize={40}
title="Edit view?"
subtitle="Update the custom query."
subtitle="Update the data source configuration."
confirmText="Save"
cancelText="Cancel"
disabled={!this.valid()}
onCancel={onCancel}
onConfirm={() =>
onConfirm({
name: this.state.name,
className: this.state.className,
query: JSON.parse(this.state.query),
className: this.state.dataSourceType === 'query' ? this.state.className : null,
query: this.state.dataSourceType === 'query' ? JSON.parse(this.state.query) : null,
cloudFunction: this.state.dataSourceType === 'cloudFunction' ? this.state.cloudFunction : null,
showCounter: this.state.showCounter,
})
}
Expand All @@ -68,32 +89,56 @@
}
/>
<Field
label={<Label text="Class" />}
label={<Label text="Data Source" />}
input={
<Dropdown
value={this.state.className}
onChange={className => this.setState({ className })}
value={this.state.dataSourceType}
onChange={dataSourceType => this.setState({ dataSourceType })}
>
{classes.map(c => (
<Option key={c} value={c}>
{c}
</Option>
))}
<Option value="query">Aggregation Pipeline</Option>
<Option value="cloudFunction">Cloud Function</Option>
</Dropdown>
}
/>
{this.state.dataSourceType === 'query' && (
<Field
label={<Label text="Class" />}
input={
<Dropdown
value={this.state.className}
onChange={className => this.setState({ className })}
>
{classes.map(c => (
<Option key={c} value={c}>
{c}
</Option>
))}
</Dropdown>
}
/>
)}
<Field
label={
<Label
text="Query"
description="An aggregation pipeline that returns an array of items."
text={this.state.dataSourceType === 'query' ? 'Query' : 'Cloud Function'}
description={
this.state.dataSourceType === 'query'
? 'An aggregation pipeline that returns an array of items.'
: 'A Parse Cloud Function that returns an array of items.'
}
/>
}
input={
<TextInput
multiline={true}
value={this.state.query}
onChange={query => this.setState({ query })}
multiline={this.state.dataSourceType === 'query'}
value={this.state.dataSourceType === 'query' ? this.state.query : this.state.cloudFunction}
onChange={value =>
this.setState(
this.state.dataSourceType === 'query'
? { query: value }
: { cloudFunction: value }
)
}
/>
}
/>
Expand Down
Loading
Loading