marcandrewb
10/5/2016 - 10:59 AM

React Form Example / Live version: http://react-form-example.surge.sh/

React Form Example / Live version: http://react-form-example.surge.sh/

{
  "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"
  },
  "devDependencies": {
    "eslint-config-jonnybuchanan": "4.6.0",
    "nwb": "0.12.1"
  }
}
module.exports = {
  polyfill: false,
  babel: {
    cherryPick: 'react-bootstrap'
  }
}
/**
 * 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',
  },
}
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')
)
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,
    onChange: t.func.isRequired,
    release: t.string.isRequired,
    value: t.array.isRequired,
  },

  handleToggleAll(e) {
    this.props.onChange(
      e.target.checked
        ? Object.keys(APPS_BY_RELEASE[this.props.release])
        : []
    )
  },
  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} = this.props
    let versions = APPS_BY_RELEASE[release]
    let isV4 = /^emr-4/.test(release)
    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

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 {
  Alert,
  Button,
  Col, ControlLabel,
  Form, FormControl, FormGroup,
  HelpBlock,
} from 'react-bootstrap'

import {APPS, APPS_BY_RELEASE} from './metadata'
import SelectApps from './SelectApps'

const EMR_RELEASES = Object.keys(APPS_BY_RELEASE)

function validate(values) {
  let errors = {}
  if (values.applications.length === 0) {
    errors.applications = 'Please select some applications to install.'
  }
  return errors
}

let FormExample = React.createClass({
  getInitialState() {
    return {
      errors: {},
      touched: {
        applications: false,
      },
      values: {
        applications: ['hue', 'hive', 'pig', 'spark', 'hadoop'],
        releaseLabel: '',
      },
    }
  },

  /**
   * Update selected application state.
   */
  handleApplicationChange(applications) {
    let values = {...this.state.values, applications}
    let stateChange = {errors: validate(values), values}
    if (!this.state.touched.applications) {
      stateChange.touched = {...this.state.touched, applications: true}
    }
    this.setState(stateChange)
  },

  /**
   * Correct selected applications when the release label is changed.
   */
  handleReleaseLabelChange(e) {
    let {value: releaseLabel} = e.target
    // If the release label has been cleared, don't do anything else
    if (releaseLabel === '') {
      return this.setState({values: {...this.state.values, releaseLabel}})
    }
    let {applications} = this.state.values
    let nextAppIds = Object.keys(APPS_BY_RELEASE[releaseLabel])
    let values = {
      ...this.state.values,
       // Deselect any selected apps which no longer apply
      applications: applications.filter(appId => nextAppIds.indexOf(appId) !== -1),
      releaseLabel,
    }
    this.setState({errors: validate(values), values})
  },

  /**
   * Generic onChange handler.
   */
  handleChange(e) {
    let input = e.target
    this.setState({
      values: {
        ...this.state.values,
        [input.name]: input.type === 'checkbox' ? input.checked : input.value,
      }
    })
  },

  handleSubmit(e) {
    e.preventDefault()

    let errors = validate(this.state.values)
    if (Object.keys(errors).length > 0) {
      return this.setState({errors})
    }

    // Create a cluster model object for submission
    let {applications, ...cluster} = this.state.values
    let isV4 = /^emr-4/.test(cluster.releaseLabel)
    // Convert application ids to names for submission
    cluster.applications = applications.map(appId => {
      let {name, v4Sandbox} = APPS[appId]
      // Certain apps need a '-Sandbox' suffix in EMR v4
      return `${name}${isV4 && v4Sandbox ? '-Sandbox' : ''}`
    })

    console.log(JSON.stringify(cluster, null, 2))
  },

  render() {
    let {errors, touched, values} = this.state
    let releaseSelected = values.releaseLabel !== ''
    return <Form onSubmit={this.handleSubmit} horizontal>
      <legend>React Form Example</legend>
      <FormGroup controlId="releaseLabel">
        <Col componentClass={ControlLabel} sm={3} md={2}>Release Label:</Col>
        <Col sm={9} md={6}>
          <FormControl required componentClass="select" name="releaseLabel"
            value={values.releaseLabel} onChange={this.handleReleaseLabelChange}
          >
            <option value=""></option>
            {EMR_RELEASES.map(version =>
              <option key={version} value={version}>{version}</option>
            )}
          </FormControl>
          <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>
        <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>
            <SelectApps
              onChange={this.handleApplicationChange}
              release={values.releaseLabel}
              value={values.applications}
            />
            {errors.applications && touched.applications
            ? <Alert bsStyle="danger">{errors.applications}</Alert>
            : <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