-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: Add custom data views with aggregation query #2888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
1e707d5
5f61237
eab8cf3
d485379
8825239
49cd9fc
6ed37ba
bb8eabd
e97fbd9
1de7b4c
5781c2b
c0ca3e2
0404b3e
1708ad9
d490260
7c6230a
510dea2
d18cb6a
80ef8c1
eef72bd
146ec5d
5b96bea
f9514dd
dc36630
d74b4f8
d657be1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import Dropdown from 'components/Dropdown/Dropdown.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'; | ||
|
||
function isValidJSON(value) { | ||
try { | ||
JSON.parse(value); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
|
||
export default class CreateViewDialog extends React.Component { | ||
constructor() { | ||
super(); | ||
this.state = { | ||
name: '', | ||
className: '', | ||
query: '[]', | ||
showCounter: false, | ||
}; | ||
} | ||
|
||
valid() { | ||
return ( | ||
this.state.name.length > 0 && | ||
this.state.className.length > 0 && | ||
isValidJSON(this.state.query) | ||
); | ||
} | ||
|
||
render() { | ||
const { classes, onConfirm, onCancel } = this.props; | ||
return ( | ||
<Modal | ||
type={Modal.Types.INFO} | ||
icon="plus" | ||
iconSize={40} | ||
title="Create a new view?" | ||
subtitle="Define a custom query 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), | ||
showCounter: this.state.showCounter, | ||
}) | ||
} | ||
> | ||
<Field | ||
label={<Label text="Name" />} | ||
input={ | ||
<TextInput | ||
value={this.state.name} | ||
onChange={name => this.setState({ name })} | ||
/> | ||
} | ||
/> | ||
<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." | ||
/> | ||
} | ||
input={ | ||
<TextInput | ||
multiline={true} | ||
value={this.state.query} | ||
onChange={query => this.setState({ query })} | ||
/> | ||
} | ||
/> | ||
<Field | ||
label={<Label text="Show object counter" />} | ||
input={ | ||
<Checkbox | ||
checked={this.state.showCounter} | ||
onChange={showCounter => this.setState({ showCounter })} | ||
/> | ||
} | ||
/> | ||
</Modal> | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,239 @@ | ||||||
import CategoryList from 'components/CategoryList/CategoryList.react'; | ||||||
import SidebarAction from 'components/Sidebar/SidebarAction'; | ||||||
import TableHeader from 'components/Table/TableHeader.react'; | ||||||
import TableView from 'dashboard/TableView.react'; | ||||||
import Toolbar from 'components/Toolbar/Toolbar.react'; | ||||||
import Parse from 'parse'; | ||||||
import React from 'react'; | ||||||
import Notification from 'dashboard/Data/Browser/Notification.react'; | ||||||
import CreateViewDialog from './CreateViewDialog.react'; | ||||||
import * as ViewPreferences from 'lib/ViewPreferences'; | ||||||
import { withRouter } from 'lib/withRouter'; | ||||||
import subscribeTo from 'lib/subscribeTo'; | ||||||
import { ActionTypes as SchemaActionTypes } from 'lib/stores/SchemaStore'; | ||||||
|
||||||
export default | ||||||
@subscribeTo('Schema', 'schema') | ||||||
@withRouter | ||||||
class Views extends TableView { | ||||||
constructor() { | ||||||
super(); | ||||||
this.section = 'Core'; | ||||||
this.subsection = 'Views'; | ||||||
this.state = { | ||||||
views: [], | ||||||
counts: {}, | ||||||
data: [], | ||||||
order: [], | ||||||
columns: {}, | ||||||
showCreate: false, | ||||||
lastError: null, | ||||||
lastNote: null, | ||||||
}; | ||||||
this.noteTimeout = null; | ||||||
this.action = new SidebarAction('Create a view', () => | ||||||
this.setState({ showCreate: true }) | ||||||
); | ||||||
} | ||||||
|
||||||
componentWillMount() { | ||||||
this.props.schema | ||||||
.dispatch(SchemaActionTypes.FETCH) | ||||||
.then(() => this.loadViews(this.context)); | ||||||
} | ||||||
|
||||||
componentWillReceiveProps(nextProps, nextContext) { | ||||||
if (this.context !== nextContext) { | ||||||
this.props.schema | ||||||
.dispatch(SchemaActionTypes.FETCH) | ||||||
.then(() => this.loadViews(nextContext)); | ||||||
} | ||||||
if (this.props.params.name !== nextProps.params.name || this.context !== nextContext) { | ||||||
this.loadData(nextProps.params.name); | ||||||
} | ||||||
} | ||||||
|
||||||
loadViews(app) { | ||||||
const views = ViewPreferences.getViews(app.applicationId); | ||||||
this.setState({ views, counts: {} }, () => { | ||||||
views.forEach(view => { | ||||||
if (view.showCounter) { | ||||||
new Parse.Query(view.className) | ||||||
.aggregate(view.query, { useMasterKey: true }) | ||||||
.then(res => { | ||||||
this.setState(({ counts }) => ({ | ||||||
counts: { ...counts, [view.name]: res.length }, | ||||||
})); | ||||||
}) | ||||||
.catch(error => { | ||||||
this.showNote( | ||||||
`Request failed: ${error.message || 'Unknown error occurred'}`, | ||||||
true | ||||||
); | ||||||
}); | ||||||
} | ||||||
}); | ||||||
this.loadData(this.props.params.name); | ||||||
}); | ||||||
} | ||||||
|
||||||
loadData(name) { | ||||||
if (!name) { | ||||||
this.setState({ data: [], order: [], columns: {} }); | ||||||
return; | ||||||
} | ||||||
const view = (this.state.views || []).find(v => v.name === name); | ||||||
if (!view) { | ||||||
this.setState({ data: [], order: [], columns: {} }); | ||||||
return; | ||||||
} | ||||||
new Parse.Query(view.className) | ||||||
.aggregate(view.query, { useMasterKey: true }) | ||||||
.then(results => { | ||||||
const columns = {}; | ||||||
results.forEach(item => { | ||||||
Object.keys(item).forEach(key => { | ||||||
if (columns[key]) { | ||||||
return; | ||||||
} | ||||||
const val = item[key]; | ||||||
let type = 'String'; | ||||||
if (typeof val === 'number') { | ||||||
type = 'Number'; | ||||||
} else if (typeof val === 'boolean') { | ||||||
type = 'Boolean'; | ||||||
} else if (val && typeof val === 'object') { | ||||||
if (val.__type === 'Date') { | ||||||
type = 'Date'; | ||||||
} else if (val.__type === 'Pointer') { | ||||||
type = 'Pointer'; | ||||||
} else if (val.__type === 'File') { | ||||||
type = 'File'; | ||||||
} else if (val.__type === 'GeoPoint') { | ||||||
type = 'GeoPoint'; | ||||||
} else { | ||||||
type = 'Object'; | ||||||
} | ||||||
} | ||||||
columns[key] = { type }; | ||||||
}); | ||||||
}); | ||||||
const colNames = Object.keys(columns); | ||||||
const width = colNames.length > 0 ? 100 / colNames.length : 0; | ||||||
const order = colNames.map(name => ({ name, width })); | ||||||
this.setState({ data: results, order, columns }); | ||||||
}) | ||||||
.catch(error => { | ||||||
this.showNote( | ||||||
`Request failed: ${error.message || 'Unknown error occurred'}`, | ||||||
true | ||||||
); | ||||||
this.setState({ data: [], order: [], columns: {} }); | ||||||
}); | ||||||
} | ||||||
|
||||||
tableData() { | ||||||
return this.state.data; | ||||||
} | ||||||
|
||||||
renderRow(row) { | ||||||
return ( | ||||||
<tr key={JSON.stringify(row)}> | ||||||
|
<tr key={JSON.stringify(row)}> | |
<tr key={row.objectId || row.id || JSON.stringify(row)}> |
🤖 Prompt for AI Agents
In src/dashboard/Data/Views/Views.react.js at line 121, replace the use of
JSON.stringify(row) as the key for the table row with a unique and stable
identifier from the row data, such as an ID property. This change improves
rendering performance and avoids potential issues with key collisions or
inefficiency when handling large datasets.
Uh oh!
There was an error while loading. Please reload this page.