Skip to content

Instantly share code, notes, and snippets.

@jquense
Forked from insin/FormExample.js
Last active October 5, 2016 17:04
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save jquense/c10a61956c156f11c3cfd4e47a0d2795 to your computer and use it in GitHub Desktop.
React Form Example / Live version: http://react-form-example.surge.sh/

To clone this example and run a development server:

git clone https://gist.github.com/7a08958a71e7443d658564f15fd0191a.git react-form-example
cd react-form-example
npm install
npm start

To create a static build:

npm run build

This example uses nwb for build tooling - nwb's react run and react build commands allow you to develop quick prototypes without having to set up a project structure.

import React from 'react'
import Form from 'react-formal'
import {
Alert,
Button,
Col, ControlLabel,
Form as BsForm, FormControl, FormGroup as BsFormGroup,
HelpBlock,
} from 'react-bootstrap'
import {APPS, APPS_BY_RELEASE} from './metadata'
import schema, { serialize } from './schema';
import SelectApps from './SelectApps'
const EMR_RELEASES = Object.keys(APPS_BY_RELEASE)
function trimApplications(label, applications) {
// If the release label has been cleared, don't do anything else
if (label === '') {
return applications
}
let nextAppIds = Object.keys(APPS_BY_RELEASE[label])
return applications.filter(appId => nextAppIds.indexOf(appId) !== -1)
}
function ReleaseSelect({ value, onChange, children }) {
return (
<FormControl
value={value}
onChange={onChange}
componentClass="select"
>
<option value=""></option>
{EMR_RELEASES.map(version =>
<option key={version} value={version}>{version}</option>
)}
</FormControl>
);
}
// not necessary but takes advantage of the twbs error styling
function FormGroup({ for: forFields, ...props }) {
if (!forFields) return <BsFormGroup {...props} />
return (
<Form.Trigger for={forFields} events={null}>
{({ messages, ...triggerProps }) =>
<BsFormGroup {...props} {...triggerProps}
validationState={Object.keys(messages).length ? 'error' : null}
/>
}
</Form.Trigger>
);
}
let FormExample = React.createClass({
getInitialState() {
return {
value: schema.default(),
errors: {}
}
},
handleChange(value) {
// we could also adjust `errors` here to remove application errors
// when releaseLabel is empty instead of the `alsoValidates` on the field
this.setState({ value })
},
handleErrors(errors) {
this.setState({ errors })
},
handleSubmit(cluster) {
console.log(JSON.stringify(serialize(cluster), null, 2))
},
render() {
let { errors, value } = this.state
let releaseSelected = !!value.releaseLabel
let isV4 = /^emr-4/.test(value.releaseLabel)
return (
<Form
debug
horizontal
component={BsForm}
value={value}
errors={errors}
schema={schema}
onChange={this.handleChange}
onError={this.handleErrors}
onSubmit={this.handleSubmit}
>
<legend>React Form Example</legend>
<FormGroup controlId="releaseLabel" for="releaseLabel">
<Col componentClass={ControlLabel} sm={3} md={2}>Release Label:</Col>
<Col sm={9} md={6}>
<Form.Field
type={ReleaseSelect}
name="releaseLabel"
alsoValidates="applications"
mapFromValue={{
releaseLabel: e => e.target.value,
applications: e => trimApplications(e.target.value, value.applications)
}}
/>
<Form.Message for="releaseLabel" component={HelpBlock} />
<HelpBlock>
The identifier for the EMR release, which includes a set of software,
to use with Amazon EC2 instances that are part of an Amazon EMR cluster.
See <a href="http://docs.aws.amazon.com/ElasticMapReduce/latest/ReleaseGuide/emr-release-components.html" target="_blank">About Amazon EMR Releases</a>.
</HelpBlock>
</Col>
</FormGroup>
<FormGroup for='applications'>
<Col componentClass={ControlLabel} sm={3} md={2}>Applications:</Col>
<Col sm={9} md={6}>
{!releaseSelected && <HelpBlock>Select a Release Label to show available applications.</HelpBlock>}
{releaseSelected && <div>
<Form.Field
name="applications"
type={SelectApps}
isV4={isV4}
versions={APPS_BY_RELEASE[value.releaseLabel]}
/>
{errors.applications
? <Form.Message for="applications" component={Alert} bsStyle="danger" />
: <HelpBlock>Select applications to install on your cluster.</HelpBlock>
}
</div>}
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={3} sm={9} mdOffset={2} md={6}>
<hr/>
<Button bsStyle="primary" type="submit">Create Cluster</Button>
</Col>
</FormGroup>
</Form>
)
}
})
export default FormExample
import 'bootstrap/dist/css/bootstrap.css'
import React from 'react'
import {Grid} from 'react-bootstrap'
import {render} from 'react-dom'
import FormExample from './FormExample'
render(
<Grid>
<FormExample/>
</Grid>,
document.querySelector('#app')
)
/**
* Application names match the names expected by the EMR API.
*
* Sandbox apps must have a '-Sandbox' suffix added when submitting application
* names to the API for EMR v4.
*/
export const APPS = {
hadoop: {
name: 'Hadoop',
url: 'http://ganglia.info/',
},
ganglia: {
name: 'Ganglia',
url: 'http://hadoop.apache.org/docs/current/',
},
hbase: {
name: 'HBase',
url: 'http://hbase.apache.org/',
},
hive: {
name: 'Hive',
url: 'https://cwiki.apache.org/confluence/display/Hive',
},
hcatalog: {
name: 'HCatalog',
url: 'https://cwiki.apache.org/confluence/display/Hive/HCatalog',
},
hue: {
name: 'Hue',
url: 'http://gethue.com/',
},
mahout: {
name: 'Mahout',
url: 'http://mahout.apache.org/',
},
oozie: {
name: 'Oozie',
v4Sandbox: true,
url: 'http://oozie.apache.org/',
},
phoenix: {
name: 'Phoenix',
url: 'https://phoenix.apache.org/',
},
pig: {
name: 'Pig',
url: 'http://pig.apache.org/',
},
presto: {
name: 'Presto',
v4Sandbox: true,
url: 'https://prestodb.io/',
},
spark: {
name: 'Spark',
url: 'https://spark.apache.org/docs/latest/',
},
sqoop: {
name: 'Sqoop',
v4Sandbox: true,
url: 'http://sqoop.apache.org/',
},
tez: {
name: 'Tez',
url: 'https://tez.apache.org/',
},
zeppelin: {
name: 'Zeppelin',
v4Sandbox: true,
url: 'https://zeppelin.incubator.apache.org/',
},
zookeeper: {
name: 'ZooKeeper',
v4Sandbox: true,
url: 'https://zookeeper.apache.org/',
},
}
/**
* Applications and versions available in each EMR release.
*/
export const APPS_BY_RELEASE = {
'emr-4.2.0': {
hadoop: '2.6.0',
ganglia: '3.6.0',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.11.0',
oozie: '4.2.0',
pig: '0.14.0',
presto: '0.125',
spark: '1.5.2',
zeppelin: '0.5.5',
},
'emr-4.3.0': {
hadoop: '2.7.1',
ganglia: '3.7.2',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.11.0',
oozie: '4.2.0',
pig: '0.14.0',
presto: '0.130',
spark: '1.6.0',
zeppelin: '0.5.5',
},
'emr-4.4.0': {
hadoop: '2.7.1',
ganglia: '3.7.2',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.11.1',
oozie: '4.2.0',
pig: '0.14.0',
presto: '0.136',
spark: '1.6.0',
sqoop: '1.4.6',
zeppelin: '0.5.6',
},
'emr-4.5.0': {
hadoop: '2.7.2',
ganglia: '3.7.2',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.11.1',
oozie: '4.2.0',
pig: '0.14.0',
presto: '0.140',
spark: '1.6.1',
sqoop: '1.4.6',
zeppelin: '0.5.6',
},
'emr-4.6.0': {
hadoop: '2.7.2',
ganglia: '3.7.2',
hbase: '1.2.0',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.11.1',
oozie: '4.2.0',
pig: '0.14.0',
presto: '0.143',
spark: '1.6.1',
sqoop: '1.4.6',
zeppelin: '0.5.6',
zookeeper: '3.4.8',
},
'emr-4.7.1': {
hadoop: '2.7.2',
ganglia: '3.7.2',
hbase: '1.2.1',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.12.0',
oozie: '4.2.0',
phoenix: '4.7.0',
pig: '0.14.0',
presto: '0.147',
spark: '1.6.1',
sqoop: '1.4.6',
tez: '0.8.3',
zeppelin: '0.5.6',
zookeeper: '3.4.8',
},
'emr-4.7.2': {
hadoop: '2.7.2',
ganglia: '3.7.2',
hbase: '1.2.1',
hive: '1.0.0',
hcatalog: '1.0.0',
hue: '3.7.1',
mahout: '0.12.2',
oozie: '4.2.0',
phoenix: '4.7.0',
pig: '0.14.0',
presto: '0.148',
spark: '1.6.2',
sqoop: '1.4.6',
tez: '0.8.3',
zeppelin: '0.5.6',
zookeeper: '3.4.8',
},
'emr-5.0.0': {
hadoop: '2.7.2',
ganglia: '3.7.2',
hbase: '1.2.2',
hive: '2.1.0',
hcatalog: '2.1.0',
hue: '3.10.0',
mahout: '0.12.2',
oozie: '4.2.0',
phoenix: '4.7.0',
pig: '0.16.0',
presto: '0.150',
spark: '2.0.0',
sqoop: '1.4.6',
tez: '0.8.4',
zeppelin: '0.6.1',
zookeeper: '3.4.8',
},
}
module.exports = {
polyfill: false,
babel: {
cherryPick: 'react-bootstrap'
},
webpack: {
extra: {
devtool: 'inline-source-map'
}
}
}
{
"name": "react-form-example",
"scripts": {
"build": "react build index.js --title \"React Form Example\"",
"lint": "eslint *.js",
"start": "react run index.js"
},
"dependencies": {
"bootstrap": "3.3.7",
"react": "15.3.2",
"react-bootstrap": "0.30.5",
"react-dom": "15.3.2",
"react-fa": "4.1.2",
"react-formal": "^0.22.0",
"yup": "^0.21.2"
},
"devDependencies": {
"eslint-config-jonnybuchanan": "4.6.0",
"nwb": "0.12.1"
}
}
import { object, string, array, bool } from 'yup';
import { APPS } from './metadata';
const application = object({
name: string(),
url: string().url(),
v4Sandbox: bool(),
})
const schema = object({
applications: array(
string().oneOf(Object.keys(APPS))
)
.default(() => ['hue', 'hive', 'pig', 'spark', 'hadoop'])
.when('releaseLabel', {
is: '',
otherwise: schema => schema
.required('Please select some applications to install.')
}),
releaseLabel: string()
.required('Select a release label')
.default(''),
})
export default schema;
export const serialize = (value) => schema.shape({
applications: array(
string().when('releaseLabel', {
is: v => /^emr-4/.test(v),
then: string().transform(id =>
`${APPS[id].name}${APPS[id].v4Sandbox ? '-Sandbox' : ''}`),
otherwise: string().transform(id => APPS[id].name),
})
)
})
.cast(value)
import React, {PropTypes as t} from 'react'
import {Table} from 'react-bootstrap'
import Icon from 'react-fa'
import {APPS, APPS_BY_RELEASE} from './metadata'
let SelectApps = React.createClass({
propTypes: {
disabled: t.bool,
isV4: t.bool,
versions: t.object,
onChange: t.func.isRequired,
value: t.array.isRequired,
},
handleToggleAll(e) {
this.props.onChange(
e.target.checked
? Object.keys(this.props.versions)
: []
)
},
handleToggleApp(e) {
let appIds = this.props.value.slice()
if (e.target.checked) {
appIds.push(e.target.value)
}
else {
appIds.splice(appIds.indexOf(e.target.value), 1)
}
this.props.onChange(appIds)
},
render() {
let {disabled, release, value, isV4, versions } = this.props
let appIds = Object.keys(APPS).filter(id => id in versions)
return <Table bordered striped condensed>
<colgroup>
<col width="1%"/>
<col width="49%"/>
<col width="49%"/>
<col width="1%"/>
</colgroup>
<thead>
<tr>
<th>
<input
checked={value.length === appIds.length}
disabled={disabled}
onChange={this.handleToggleAll}
type="checkbox"
/>
</th>
<th>Application</th>
<th>Version</th>
<th>Info</th>
</tr>
</thead>
<tbody>
{appIds.map(appId => {
let checked = value.indexOf(appId) !== -1
let controlId = `application-${appId}`
let version = versions[appId]
let {name, url, v4Sandbox} = APPS[appId]
return <tr key={appId}>
<td>
<input
checked={checked}
disabled={disabled}
id={controlId}
onChange={this.handleToggleApp}
type="checkbox"
value={appId}
/>
</td>
<td>
<label style={{fontWeight: 'normal'}} htmlFor={controlId}>
{`${name}${isV4 && v4Sandbox ? ' (Sandbox)' : ''}`}
</label>
</td>
<td>{version}</td>
<td style={{whiteSpace: 'nowrap'}}>
<a href={url} target="_blank" title={`Find out more about ${name}`}>
<Icon name="info-circle"/> <Icon name="external-link"/>
</a>
</td>
</tr>
})}
</tbody>
</Table>
}
})
export default SelectApps
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment