Skip to content

Commit f5831c7

Browse files
authored
feat: Add Cloud Function as data source for views with optional text or file upload (#2939)
1 parent 6c4a7f2 commit f5831c7

File tree

5 files changed

+463
-86
lines changed

5 files changed

+463
-86
lines changed

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ Parse Dashboard is a standalone dashboard for managing your [Parse Server](https
7979
- [Limitations](#limitations)
8080
- [CSV Export](#csv-export)
8181
- [Views](#views)
82+
- [Data Sources](#data-sources)
83+
- [Aggregation Pipeline](#aggregation-pipeline)
84+
- [Cloud Function](#cloud-function)
8285
- [View Table](#view-table)
8386
- [Pointer](#pointer)
8487
- [Link](#link)
@@ -1260,11 +1263,38 @@ This feature will take either selected rows or all rows of an individual class a
12601263

12611264
▶️ *Core > Views*
12621265

1263-
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.
1266+
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.
12641267

12651268
> [!Caution]
12661269
> 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).
12671270
1271+
### Data Sources
1272+
1273+
Views can pull their data from the following data sources.
1274+
1275+
#### Aggregation Pipeline
1276+
1277+
Display aggregated data from your classes using a MongoDB aggregation pipeline. Create a view by selecting a class and defining an aggregation pipeline.
1278+
1279+
#### Cloud Function
1280+
1281+
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.
1282+
1283+
Cloud Function views can prompt users for text input and/or file upload when opened. Enable "Require text input" or "Require file upload" checkboxes when creating the view. The user provided data will then be available in the Cloud Function as parameters.
1284+
1285+
Cloud Function example:
1286+
1287+
```js
1288+
Parse.Cloud.define("myViewFunction", request => {
1289+
const text = request.params.text;
1290+
const fileData = request.params.fileData;
1291+
return processDataWithTextAndFile(text, fileData);
1292+
});
1293+
```
1294+
1295+
> [!Note]
1296+
> Text and file data are ephemeral and only available to the Cloud Function during that request. Files are base64 encoded, increasing the data during transfer by ~33%.
1297+
12681298
### View Table
12691299

12701300
When designing the aggregation pipeline, consider that some values are rendered specially in the output table.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import Field from 'components/Field/Field.react';
2+
import FileInput from 'components/FileInput/FileInput.react';
3+
import Label from 'components/Label/Label.react';
4+
import Modal from 'components/Modal/Modal.react';
5+
import TextInput from 'components/TextInput/TextInput.react';
6+
import React from 'react';
7+
8+
export default class CloudFunctionInputDialog extends React.Component {
9+
constructor(props) {
10+
super(props);
11+
this.state = {
12+
textInput: '',
13+
uploadedFile: null,
14+
};
15+
}
16+
17+
handleFileChange = (file) => {
18+
this.setState({ uploadedFile: file });
19+
};
20+
21+
handleConfirm = () => {
22+
const { requireTextInput, requireFileUpload } = this.props;
23+
const params = {};
24+
25+
if (requireTextInput) {
26+
params.text = this.state.textInput;
27+
}
28+
29+
if (requireFileUpload && this.state.uploadedFile) {
30+
// For file uploads, we'll pass the raw file data
31+
// The cloud function will receive this as base64 encoded data
32+
const file = this.state.uploadedFile;
33+
const reader = new FileReader();
34+
reader.onload = () => {
35+
if (reader.result && typeof reader.result === 'string') {
36+
params.fileData = {
37+
name: file.name,
38+
type: file.type,
39+
size: file.size,
40+
data: reader.result.split(',')[1], // Remove the data URL prefix
41+
};
42+
}
43+
this.props.onConfirm(params);
44+
};
45+
reader.readAsDataURL(file);
46+
} else {
47+
this.props.onConfirm(params);
48+
}
49+
};
50+
51+
render() {
52+
const { requireTextInput, requireFileUpload, onCancel } = this.props;
53+
54+
// Check if we have all required inputs
55+
const hasRequiredText = !requireTextInput || this.state.textInput.trim().length > 0;
56+
const hasRequiredFile = !requireFileUpload || this.state.uploadedFile !== null;
57+
const isValid = hasRequiredText && hasRequiredFile;
58+
59+
return (
60+
<Modal
61+
type={Modal.Types.INFO}
62+
icon="gear"
63+
iconSize={40}
64+
title="Cloud Function Input"
65+
subtitle="Provide the required input for this view."
66+
confirmText="Send"
67+
cancelText="Cancel"
68+
disabled={!isValid}
69+
onCancel={onCancel}
70+
onConfirm={this.handleConfirm}
71+
>
72+
{requireTextInput && (
73+
<Field
74+
label={<Label text="Text" />}
75+
input={
76+
<TextInput
77+
multiline
78+
value={this.state.textInput}
79+
onChange={textInput => this.setState({ textInput })}
80+
placeholder="Enter text here..."
81+
/>
82+
}
83+
/>
84+
)}
85+
{requireFileUpload && (
86+
<Field
87+
label={<Label text="File Upload" />}
88+
input={
89+
<FileInput
90+
value={this.state.uploadedFile}
91+
onChange={this.handleFileChange}
92+
/>
93+
}
94+
/>
95+
)}
96+
</Modal>
97+
);
98+
}
99+
}

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

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1+
import Checkbox from 'components/Checkbox/Checkbox.react';
12
import Dropdown from 'components/Dropdown/Dropdown.react';
3+
import Option from 'components/Dropdown/Option.react';
24
import Field from 'components/Field/Field.react';
35
import Label from 'components/Label/Label.react';
46
import Modal from 'components/Modal/Modal.react';
5-
import Option from 'components/Dropdown/Option.react';
6-
import React from 'react';
77
import TextInput from 'components/TextInput/TextInput.react';
8-
import Checkbox from 'components/Checkbox/Checkbox.react';
8+
import React from 'react';
9+
10+
/**
11+
* The data source types available for views.
12+
*
13+
* @param {string} query An aggregation pipeline query data source.
14+
* @param {string} cloudFunction A Cloud Function data source.
15+
*/
16+
const DataSourceTypes = {
17+
query: 'query',
18+
cloudFunction: 'cloudFunction'
19+
};
920

1021
function isValidJSON(value) {
1122
try {
@@ -22,17 +33,30 @@ export default class CreateViewDialog extends React.Component {
2233
this.state = {
2334
name: '',
2435
className: '',
36+
dataSourceType: DataSourceTypes.query,
2537
query: '[]',
38+
cloudFunction: '',
2639
showCounter: false,
40+
requireTextInput: false,
41+
requireFileUpload: false,
2742
};
2843
}
2944

3045
valid() {
31-
return (
32-
this.state.name.length > 0 &&
33-
this.state.className.length > 0 &&
34-
isValidJSON(this.state.query)
35-
);
46+
if (this.state.dataSourceType === DataSourceTypes.query) {
47+
return (
48+
this.state.name.length > 0 &&
49+
this.state.className.length > 0 &&
50+
this.state.query.trim() !== '' &&
51+
this.state.query !== '[]' &&
52+
isValidJSON(this.state.query)
53+
);
54+
} else {
55+
return (
56+
this.state.name.length > 0 &&
57+
this.state.cloudFunction.trim() !== ''
58+
);
59+
}
3660
}
3761

3862
render() {
@@ -43,17 +67,20 @@ export default class CreateViewDialog extends React.Component {
4367
icon="plus"
4468
iconSize={40}
4569
title="Create a new view?"
46-
subtitle="Define a custom query to display data."
70+
subtitle="Define a data source to display data."
4771
confirmText="Create"
4872
cancelText="Cancel"
4973
disabled={!this.valid()}
5074
onCancel={onCancel}
5175
onConfirm={() =>
5276
onConfirm({
5377
name: this.state.name,
54-
className: this.state.className,
55-
query: JSON.parse(this.state.query),
78+
className: this.state.dataSourceType === DataSourceTypes.query ? this.state.className : null,
79+
query: this.state.dataSourceType === DataSourceTypes.query ? JSON.parse(this.state.query) : null,
80+
cloudFunction: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.cloudFunction : null,
5681
showCounter: this.state.showCounter,
82+
requireTextInput: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.requireTextInput : false,
83+
requireFileUpload: this.state.dataSourceType === DataSourceTypes.cloudFunction ? this.state.requireFileUpload : false,
5784
})
5885
}
5986
>
@@ -67,32 +94,56 @@ export default class CreateViewDialog extends React.Component {
6794
}
6895
/>
6996
<Field
70-
label={<Label text="Class" />}
97+
label={<Label text="Data Source" />}
7198
input={
7299
<Dropdown
73-
value={this.state.className}
74-
onChange={className => this.setState({ className })}
100+
value={this.state.dataSourceType}
101+
onChange={dataSourceType => this.setState({ dataSourceType })}
75102
>
76-
{classes.map(c => (
77-
<Option key={c} value={c}>
78-
{c}
79-
</Option>
80-
))}
103+
<Option value={DataSourceTypes.query}>Aggregation Pipeline</Option>
104+
<Option value={DataSourceTypes.cloudFunction}>Cloud Function</Option>
81105
</Dropdown>
82106
}
83107
/>
108+
{this.state.dataSourceType === DataSourceTypes.query && (
109+
<Field
110+
label={<Label text="Class" />}
111+
input={
112+
<Dropdown
113+
value={this.state.className}
114+
onChange={className => this.setState({ className })}
115+
>
116+
{classes.map(c => (
117+
<Option key={c} value={c}>
118+
{c}
119+
</Option>
120+
))}
121+
</Dropdown>
122+
}
123+
/>
124+
)}
84125
<Field
85126
label={
86127
<Label
87-
text="Query"
88-
description="An aggregation pipeline that returns an array of items."
128+
text={this.state.dataSourceType === DataSourceTypes.query ? 'Query' : 'Cloud Function'}
129+
description={
130+
this.state.dataSourceType === DataSourceTypes.query
131+
? 'An aggregation pipeline that returns an array of items.'
132+
: 'A Parse Cloud Function that returns an array of items.'
133+
}
89134
/>
90135
}
91136
input={
92137
<TextInput
93-
multiline={true}
94-
value={this.state.query}
95-
onChange={query => this.setState({ query })}
138+
multiline={this.state.dataSourceType === DataSourceTypes.query}
139+
value={this.state.dataSourceType === DataSourceTypes.query ? this.state.query : this.state.cloudFunction}
140+
onChange={value =>
141+
this.setState(
142+
this.state.dataSourceType === DataSourceTypes.query
143+
? { query: value }
144+
: { cloudFunction: value }
145+
)
146+
}
96147
/>
97148
}
98149
/>
@@ -105,6 +156,28 @@ export default class CreateViewDialog extends React.Component {
105156
/>
106157
}
107158
/>
159+
{this.state.dataSourceType === DataSourceTypes.cloudFunction && (
160+
<>
161+
<Field
162+
label={<Label text="Require text input" description="When checked, users will be prompted to enter text when opening this view." />}
163+
input={
164+
<Checkbox
165+
checked={this.state.requireTextInput}
166+
onChange={requireTextInput => this.setState({ requireTextInput })}
167+
/>
168+
}
169+
/>
170+
<Field
171+
label={<Label text="Require file upload" description="When checked, users will be prompted to upload a file when opening this view." />}
172+
input={
173+
<Checkbox
174+
checked={this.state.requireFileUpload}
175+
onChange={requireFileUpload => this.setState({ requireFileUpload })}
176+
/>
177+
}
178+
/>
179+
</>
180+
)}
108181
</Modal>
109182
);
110183
}

0 commit comments

Comments
 (0)