Skip to content

Commit b81078a

Browse files
authored
Blockchain screen (#400)
* create Blockchain screen * get blockchain classes * added api call & move to modal * move to blockchain api * remove class from blockchain modal & api * show request error msg note * showing not in blockchain classes only in dropdown * typo fixes, updated add modal, sorting alphabetically * New badge on Blockchain * update appBalance * Add Info title at top * fixed typo
1 parent 45429a7 commit b81078a

File tree

7 files changed

+578
-0
lines changed

7 files changed

+578
-0
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/*
2+
* Copyright (c) 2016-present, Parse, LLC
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the license found in the LICENSE file in
6+
* the root directory of this source tree.
7+
*/
8+
import { ActionTypes } from 'lib/stores/SchemaStore';
9+
import DashboardView from 'dashboard/DashboardView.react';
10+
import Dropdown from 'components/Dropdown/Dropdown.react';
11+
import Field from 'components/Field/Field.react';
12+
import Fieldset from 'components/Fieldset/Fieldset.react';
13+
import Icon from 'components/Icon/Icon.react';
14+
import Label from 'components/Label/Label.react';
15+
import LoaderContainer from 'components/LoaderContainer/LoaderContainer.react';
16+
import Option from 'components/Dropdown/Option.react';
17+
import ParseApp from 'lib/ParseApp';
18+
import PropTypes from 'lib/PropTypes';
19+
import React from 'react';
20+
import TextInput from 'components/TextInput/TextInput.react';
21+
import Toolbar from 'components/Toolbar/Toolbar.react';
22+
import styles from './BlockChainPage.scss' ;
23+
import subscribeTo from 'lib/subscribeTo';
24+
import MoveToBlockchainModal from './MoveToBlockchainModal.react';
25+
import RemoveFromBlockchainModal from './RemoveFromBlockchainModal.react';
26+
import Notification from '../Data/Browser/Notification.react';
27+
28+
const sortClasses = (classes) => {
29+
return classes.sort((a, b) => {
30+
if (a[0] === '_' && b[0] !== '_') {
31+
return -1
32+
}
33+
if (b[0] === '_' && a[0] !== '_') {
34+
return 1
35+
}
36+
return a.toUpperCase() < b.toUpperCase() ? -1 : 1
37+
})
38+
}
39+
40+
@subscribeTo("Schema", "schema")
41+
class BlockChainPage extends DashboardView {
42+
constructor() {
43+
super();
44+
this.section = "Database";
45+
this.subsection = "Blockchain";
46+
47+
this.state = {
48+
loading: true,
49+
appBalanceLoading: true,
50+
blockChainClassesLoading: true,
51+
classes: [],
52+
appBalance: "",
53+
blockChainClasses: [],
54+
selectedClass: "",
55+
showAddClassModal: false,
56+
showRemoveClassModal: false,
57+
inProgress: false,
58+
lastError: "",
59+
lastNote: "",
60+
};
61+
this.moveClassToBlockChain = this.moveClassToBlockChain.bind(this);
62+
this.removeClassFromBlockChain = this.removeClassFromBlockChain.bind(this);
63+
this.showNote = this.showNote.bind(this);
64+
}
65+
66+
componentWillMount() {
67+
this.props.schema.dispatch(ActionTypes.FETCH).then(() => {
68+
if (this.props.schema.data.get("classes")) {
69+
let classes = this.props.schema.data.get("classes").keySeq().toArray();
70+
this.setState({ loading: false, classes: sortClasses(classes) });
71+
}
72+
});
73+
74+
this.context.currentApp.getAppBalance().then(({ balance }) => {
75+
this.setState({ appBalanceLoading: false, appBalance: balance });
76+
});
77+
78+
this.context.currentApp.getBlockchainClassNames().then(({ classNames }) => {
79+
this.setState({
80+
blockChainClassesLoading: false,
81+
blockChainClasses: sortClasses(classNames),
82+
});
83+
});
84+
}
85+
86+
showNote(message, isError) {
87+
if (!message) {
88+
return;
89+
}
90+
91+
clearTimeout(this.noteTimeout);
92+
93+
if (isError) {
94+
this.setState({ lastError: message, lastNote: null });
95+
} else {
96+
this.setState({ lastNote: message, lastError: null });
97+
}
98+
99+
this.noteTimeout = setTimeout(() => {
100+
this.setState({ lastError: null, lastNote: null });
101+
}, 3500);
102+
}
103+
104+
moveClassToBlockChain() {
105+
let selectedClassName = this.state.selectedClass;
106+
this.setState({
107+
inProgress: true,
108+
});
109+
this.context.currentApp
110+
.moveClassToBlockchain(selectedClassName)
111+
.then(() => {
112+
let newClassArray = [ ...this.state.blockChainClasses, selectedClassName ];
113+
this.setState({
114+
blockChainClasses: sortClasses(newClassArray),
115+
classes: this.state.classes.filter(name => name !== selectedClassName)
116+
});
117+
})
118+
.catch((err) => {
119+
console.log(err);
120+
this.showNote(err.response?.data || err.message, true);
121+
})
122+
.finally(() => {
123+
this.setState({
124+
inProgress: false,
125+
showAddClassModal: false,
126+
selectedClass: ''
127+
});
128+
})
129+
}
130+
131+
removeClassFromBlockChain() {
132+
let selectedClassName = this.state.selectedClass;
133+
this.setState({
134+
inProgress: true,
135+
});
136+
this.context.currentApp
137+
.removeFromBlockchain(selectedClassName)
138+
.then(() => {
139+
this.setState({
140+
blockChainClasses: this.state.blockChainClasses.filter(name => name !== selectedClassName),
141+
});
142+
})
143+
.catch((err) => {
144+
console.log(err);
145+
this.showNote(err.response?.data || err.message, true);
146+
})
147+
.finally(() => {
148+
this.setState({
149+
inProgress: false,
150+
showRemoveClassModal: false,
151+
selectedClass: ''
152+
})
153+
})
154+
}
155+
156+
renderClassesAtBlockchain() {
157+
if (this.state.blockChainClasses.length === 0) {
158+
return null;
159+
}
160+
return (
161+
<div>
162+
<div className={styles.headerRow}>
163+
<div className={styles.className}>Class</div>
164+
<div></div>
165+
</div>
166+
{this.state.blockChainClasses.map((name, idx) => (
167+
<div key={idx} className={styles.row}>
168+
<div className={styles.className}>{name}</div>
169+
<a
170+
className={styles.action}
171+
onClick={() =>
172+
this.setState({
173+
showRemoveClassModal: true,
174+
selectedClass: name,
175+
})
176+
}
177+
>
178+
<Icon name="delete-icon" fill="#169CEE" width={24} height={20} />
179+
</a>
180+
</div>
181+
))}
182+
</div>
183+
);
184+
}
185+
186+
renderForm() {
187+
const classes = this.state.classes.filter(name => !this.state.blockChainClasses.includes(name));
188+
189+
return (
190+
<div className={styles.content}>
191+
<div className={styles.fieldset}>
192+
<div className={styles.legendText}>Blockchain Data Storage</div>
193+
<div className={styles.descText}>
194+
Save your App’s data on the Blockchain Network of your choice
195+
<br/> NOTE: This feature is on the alpha version.
196+
</div>
197+
</div>
198+
<Fieldset
199+
legend="Network"
200+
description="You can only connect to a private Ethereum compatible network in this alpha version. Use this network for development purposes at no cost."
201+
>
202+
<Field
203+
label={<Label text="BlockChain Network" />}
204+
input={
205+
<TextInput
206+
value="Back4App ETH Development"
207+
disabled
208+
onChange={() => {}}
209+
/>
210+
}
211+
/>
212+
<Field
213+
label={<Label text="Balance ETH(Development)" />}
214+
input={
215+
this.state.appBalanceLoading ? (
216+
<div className={styles.spinnerWrapper}>
217+
<div className={styles.spinner}></div>
218+
</div>
219+
) : (
220+
<TextInput
221+
value={(Number(this.state.appBalance) / Math.pow(10, 18)).toFixed(2)}
222+
disabled
223+
onChange={() => {}}
224+
/>
225+
)
226+
}
227+
/>
228+
</Fieldset>
229+
<Fieldset
230+
legend="Classes in Blockchain"
231+
description="After selecting a class, any object created on these classes will be automatically replicated to the Blockchain. Please note that two new fields will be added to these classes’ objects (blockchainStatus and blockchainResult), and it is not permitted to update nor modify blockchain objects."
232+
>
233+
<Field
234+
label={<Label text="Add new Classes at Blockchain" />}
235+
input={
236+
<Dropdown
237+
placeHolder="Select a class"
238+
onChange={(value) =>
239+
this.setState({
240+
selectedClass: value,
241+
showAddClassModal: true,
242+
})
243+
}
244+
value={this.state.selectedClass}
245+
>
246+
{classes.map((cls, idx) => (
247+
<Option key={idx} value={cls}>
248+
{cls}
249+
</Option>
250+
))}
251+
</Dropdown>
252+
}
253+
/>
254+
{this.renderClassesAtBlockchain()}
255+
</Fieldset>
256+
</div>
257+
);
258+
}
259+
260+
renderContent() {
261+
let extra = null;
262+
if (this.state.showAddClassModal) {
263+
extra = (
264+
<MoveToBlockchainModal
265+
className={this.state.selectedClass}
266+
onConfirm={this.moveClassToBlockChain}
267+
onCancel={() =>
268+
this.setState({ selectedClass: "", showAddClassModal: false })
269+
}
270+
progress={this.state.inProgress}
271+
/>
272+
);
273+
} else if (this.state.showRemoveClassModal) {
274+
extra = (
275+
<RemoveFromBlockchainModal
276+
className={this.state.selectedClass}
277+
onConfirm={this.removeClassFromBlockChain}
278+
onCancel={() =>
279+
this.setState({ selectedClass: "", showRemoveClassModal: false })
280+
}
281+
progress={this.state.inProgress}
282+
/>
283+
);
284+
}
285+
286+
let notification = null;
287+
if (this.state.lastError) {
288+
notification = (
289+
<Notification note={this.state.lastError} isErrorNote={true} />
290+
);
291+
} else if (this.state.lastNote) {
292+
notification = (
293+
<Notification note={this.state.lastNote} isErrorNote={false} />
294+
);
295+
}
296+
297+
return (
298+
<div>
299+
<LoaderContainer loading={this.state.loading}>
300+
{this.renderForm()}
301+
{extra}
302+
{notification}
303+
</LoaderContainer>
304+
<Toolbar subsection="Blockchain" />
305+
</div>
306+
);
307+
}
308+
}
309+
310+
BlockChainPage.contextTypes = {
311+
currentApp: PropTypes.instanceOf(ParseApp),
312+
};
313+
314+
export default BlockChainPage

0 commit comments

Comments
 (0)