grommet/grommet-ferret

View on GitHub
src/js/components/virtualMachine/VirtualMachineAdd.js

Summary

Maintainability
F
5 days
Test Coverage
// (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { addMultipleVms, loadVmSizes, loadVmImages,
  addItem } from '../../actions/actions';
import Article from 'grommet/components/Article';
import Header from 'grommet/components/Header';
import Heading from 'grommet/components/Heading';
import Form from 'grommet/components/Form';
import Footer from 'grommet/components/Footer';
import FormFields from 'grommet/components/FormFields';
import FormField from 'grommet/components/FormField';
import Box from 'grommet/components/Box';
import List from 'grommet/components/List';
import ListItem from 'grommet/components/ListItem';
import Select from 'grommet/components/Select';
import NumberInput from 'grommet/components/NumberInput';
import RadioButton from 'grommet/components/RadioButton';
import Tiles from 'grommet/components/Tiles';
import Button from 'grommet/components/Button';
import AddIcon from 'grommet/components/icons/base/Add';
import CloseIcon from 'grommet/components/icons/base/Close';
import EditIcon from 'grommet/components/icons/base/Edit';
import SizeTile from '../size/SizeTile';
import SizeSuggestion from '../size/SizeSuggestion';
import Anchor from 'grommet/components/Anchor';
import VirtualMachineEditNetwork from './VirtualMachineEditNetwork';

class VirtualMachineAdd extends Component {

  constructor (props) {
    super(props);
    this._onSubmit = this._onSubmit.bind(this);
    this._onLayerOpen = this._onLayerOpen.bind(this);
    this._onLayerClose = this._onLayerClose.bind(this);
    this._onSizeSearch = this._onSizeSearch.bind(this);
    this._onSizeSuggestionSelect = this._onSizeSuggestionSelect.bind(this);
    this._onImageSearch = this._onImageSearch.bind(this);
    this._onImageSelect = this._onImageSelect.bind(this);
    this._onNetworkAdd = this._onNetworkAdd.bind(this);
    this._onNetworkEdit = this._onNetworkEdit.bind(this);
    this._onNetworkRemove = this._onNetworkRemove.bind(this);
    this._onNetworkChange = this._onNetworkChange.bind(this);
    this._onSizeTileSelect = this._onSizeTileSelect.bind(this);

    this.state = {
      count: 1,
      errors: {},
      images: [],
      imageSearchText: undefined,
      layer: undefined,
      naming: {
        type: 'prefix', // prefix | manual | extend
        prefix: 'vm-',
        start: 1,
        names: [],
        extend: undefined // {name: , uri: }
      },
      sizes: [],
      template: {
        category: 'virtual-machines',
        image: {}, // {name: , uri: }
        name: undefined,
        networks: [], // {name: , uri: }, ...
        size: {} // {
        //   name: null,
        //   uri: null,
        //   vCpus: 4,
        //   memory: 16,
        //   diskSpace: 2
        // },
      }
    };
  }

  componentDidMount () {
    this.props.dispatch(loadVmSizes());
    this.props.dispatch(loadVmImages());
  }

  componentWillReceiveProps (nextProps) {
    this.setState({
      sizes: nextProps.sizes,
      maxSizes: Math.max(nextProps.sizes.length, this.state.sizes.length)
    });
  }

  _onSubmit (event) {
    event.preventDefault();
    const { router } = this.context;
    const { count, template } = this.state;
    let errors = {};
    let noErrors = true;
    if (1 === count && ! template.name) {
      errors.name = 'required';
      noErrors = false;
    }
    if (! template.size || ! template.size.name) {
      errors.size = 'required';
      noErrors = false;
    }
    if (! template.image || ! template.image.uri) {
      errors.image = 'required';
      noErrors = false;
    }
    if (noErrors) {
      if (1 === count) {
        this.props.dispatch(addItem(this.state.template));
      } else {
        this.props.dispatch(addMultipleVms(this.props.count, this.props.naming,
          this.props.template));
      }
      router.push({
        pathname: '/virtual-machines',
        search: document.location.search
      });
    } else {
      this.setState({ errors: errors });
    }
  }

  _change (contextProperty, property, index) {
    return (event) => {
      let errors = { ...this.state.errors };
      let value = event.target.value;

      delete errors[property];
      let nextState = { errors: errors };

      if (contextProperty) {
        let context = { ...this.state[contextProperty] };
        if (undefined !== index) {
          let values = context[property].splice(0);
          values[index] = value;
          context[property] = values;
        } else {
          context[property] = value;
        }
        nextState[contextProperty] = context;
      } else {
        if ('count' === property) {
          value = parseInt(value, 10);
        }
        nextState[property] = value;
      }

      this.setState(nextState);
    };
  }

  _onLayerOpen (name) {
    this.setState({layer: name});
  }

  _onLayerClose () {
    this.setState({layer: undefined});
  }

  _onSizeTileSelect (selectedIndex) {
    let template = { ...this.state.template };
    let errors = { ...this.state.errors };
    template.size = this.state.sizes[selectedIndex];
    delete errors.size;
    this.setState({ template: template, errors: errors });
  }

  _onSizeSearch (event) {
    const sizeSearchText = event.target.value;
    const regexp = new RegExp(sizeSearchText, 'i');
    const sizes = this.props.sizes.filter(size => size.name.match(regexp));
    this.setState({ sizes: sizes });
  }

  _onSizeSuggestionSelect (pseudoEvent) {
    let template = { ...this.state.template };
    let errors = { ...this.state.errors };
    template.size = pseudoEvent.option.size;
    delete errors.size;
    this.setState({
      errors: errors,
      sizes: this.props.sizes,
      template: template
    });
  }

  _onImageSearch (event) {
    const imageSearchText = event.target.value;
    let template = { ...this.state.template };
    template.image = {};
    this.setState({ imageSearchText: imageSearchText, template: template });
    this.props.dispatch(loadVmImages(imageSearchText));
  }

  _onImageSelect (pseudoEvent) {
    let template = { ...this.state.template };
    let errors = { ...this.state.errors };
    template.image = pseudoEvent.option;
    delete errors.image;
    this.setState({
      errors: errors,
      imageSearchText: undefined,
      template: template
    });
  }

  _onNetworkEdit (index) {
    this.setState({ editNetworkIndex: index, layer: 'network' });
  }

  _onNetworkAdd () {
    this.setState({ addNetwork: true, layer: 'network' });
  }

  _onNetworkRemove () {
    let template = { ...this.state.template };
    template.networks.splice(this.state.editNetworkIndex, 1);
    this.setState({
      editNetworkIndex: -1,
      layer: undefined,
      template: template
    });
    this.props.dispatch(changeVm(vm));
  }

  _onNetworkChange (network) {
    let template = { ...this.state.template };
    let networks = template.networks.splice(0);
    if (this.state.addNetwork) {
      networks.push(network);
    } else {
      networks[this.state.editNetworkIndex] = network;
    }
    template.networks = networks;
    this.setState({
      addNetwork: false,
      editNetworkIndex: -1,
      layer: undefined,
      template: template
    });
  }

  _renderCountField () {
    const { count } = this.state;
    return (
      <FormField label="Virtual machines" key="count" htmlFor="count">
        <NumberInput id="count" name="count" min={1} max={16}
          value={count} onChange={this._change(null, 'count')} />
      </FormField>
    );
  }

  _renderNameFields () {
    const { count, naming, template, errors } = this.state;
    let nameFields;
    if (1 === count) {

      nameFields = (
        <FormField label="Name" htmlFor="name" error={errors.name}>
          <input id="name" name={"template.name"} type="text"
            value={template.name || ''}
            onChange={this._change('template', 'name')} />
        </FormField>
      );

    } else if (count > 1) {

      nameFields = [
        <FormField key="type" label="Name">
          <RadioButton id="nameStrategyPrefix" name="naming.type"
            label="Add a name prefix"
            value="prefix"
            checked={'prefix' === naming.type}
            onChange={this._change('naming', 'type')}/>
          <RadioButton id="nameStrategyManual" name="naming.type"
            label="Provide each individual name"
            value="manual"
            checked={'manual' === naming.type}
            onChange={this._change('naming', 'type')}/>
          <RadioButton id="nameStrategyExtend" name="naming.type"
            label="Extend an existing set of virtual machines"
            value="extend"
            checked={'extend' === naming.type}
            onChange={this._change('naming', 'type')}/>
        </FormField>
      ];

      switch (naming.type) {

        case 'prefix':
          nameFields.push(
            <FormFields key="prefix">
              <FormField label="Prefix" htmlFor="namePrefix">
                <input id="namePrefix" name="naming.prefix" type="text"
                  value={naming.prefix}
                  onChange={this._change('naming', 'prefix')} />
              </FormField>
              <FormField label="Start numbering with" htmlFor="nameStart">
                <input id="nameStart" name="naming.start" type="number"
                  value={naming.start}
                  onChange={this._change('naming', 'prefix')} />
              </FormField>
            </FormFields>
          );
          break;

        case 'manual':
          for (let i = 0; i < count; i += 1) {
            nameFields.push(
              <FormField key={i} label={'Virtual machine ' + (i + 1)}
                htmlFor={'vm' + i}>
                <input id={'vm' + i} name={"naming.names." + i} type="text"
                  value={naming.names[i]}
                  onChange={this._change('naming', 'names', i)} />
              </FormField>
            );
          }
          break;

        case 'extend':
          nameFields.push(
            <FormField key="extend" label="Existing virtual machine"
              htmlFor="namingExtend">
              <Select id="namingExtend" name="naming.extend"
                value={naming.extend} options={undefined}
                onChange={this._change('naming', 'extend')}
                onSearch={this._onNameExtendSearch} />
            </FormField>
          );
          break;
      }
    }
    return nameFields;
  }

  _renderSizeTiles () {
    const { sizes, template, errors } = this.state;
    let selectedIndex;
    const sizeTiles = sizes.map((size, index) => {
      if (size.name === template.size.name) {
        selectedIndex = index;
      }
      return (
        <SizeTile key={size.name} item={size} editable={false} />
      );
    });
    let error;
    if (errors.size) {
      error = <span className="error">required</span>;
    }

    return (
      <fieldset>
        <Box direction="row" justify="between">
          <Heading tag="h3">Size</Heading>
          {error}
        </Box>
        <Tiles selectable={true} fill={true} flush={true}
          selected={selectedIndex} onSelect={this._onSizeTileSelect}>
          {sizeTiles}
        </Tiles>
      </fieldset>
    );
  }

  _renderSizeInput () {
    const { sizes, template, errors } = this.state;
    const options = sizes.map(size => ({
      size: size,
      label: <SizeSuggestion size={size} />
    }));
    return (
      <fieldset>
        <Box direction="row" justify="between">
          <Heading tag="h3">Size</Heading>
        </Box>
        <FormField label="Size" htmlFor="size" error={errors.size}>
          <Select id="size" name="size"
            value={template.size.name} options={options}
            onChange={this._onSizeSuggestionSelect}
            onSearch={this._onSizeSearch} />
        </FormField>
      </fieldset>
    );
  }

  _renderSizeFields () {
    const { maxSizes } = this.state;
    if (maxSizes > 6) {
      return this._renderSizeInput();
    } else {
      return this._renderSizeTiles();
    }
  }

  _renderImageField () {
    const { template, errors } = this.state;
    const { images } = this.props;
    return (
      <fieldset>
        <Heading tag="h3">Initial Disk Image</Heading>
        <FormField label="Image" htmlFor="image" error={errors.image}>
          <Select id="image" name="image"
            value={template.image.name || ''} options={images}
            onChange={this._onImageSelect}
            onSearch={this._onImageSearch} />
        </FormField>
      </fieldset>
    );
  }

  _renderNetworkFields () {
    const { template } = this.state;
    const networks = template.networks.map((network, index) => {
      return (
        <ListItem key={index} justify="between" pad="none"
          separator={index === 0 ? 'horizontal' : 'bottom'}
          responsive={false}>
          <span>
            {network.name}
            <span className="secondary">{network.vLanId}</span>
          </span>
          <Button icon={<EditIcon />}
            onClick={this._onNetworkEdit.bind(this, index)}
            a11yTitle={`Edit ${network.name} Network`} />
        </ListItem>
      );
    });
    return (
      <fieldset>
        <Header size="small" justify="between">
          <Heading tag="h3">Networks</Heading>
          <Button icon={<AddIcon />} onClick={this._onNetworkAdd}
            a11yTitle='Add Network' />
        </Header>
        <List>
          {networks}
        </List>
      </fieldset>
    );
  }

  _renderLayer () {
    const { addNetwork, editNetworkIndex, layer, template } = this.state;
    let result;
    if (layer) {
      if ('network' === layer) {
        let network = (addNetwork ? { dhcp: true } :
          template.networks[editNetworkIndex]);
        let heading = (addNetwork ? 'Add Network' : 'Edit Network');
        let onRemove = (addNetwork ? undefined : this._onNetworkRemove);
        result = (
          <VirtualMachineEditNetwork onClose={this._onLayerClose}
            heading={heading}
            network={network} onChange={this._onNetworkChange}
            onRemove={onRemove} />
        );
      }
    }
    return result;
  }

  render () {

    const countField = this._renderCountField();
    const nameFields = this._renderNameFields();
    const sizeFields = this._renderSizeFields();
    const imageField = this._renderImageField();
    const networkFields = this._renderNetworkFields();

    let layer = this._renderLayer();

    return (
      <Article align="center" pad={{horizontal: 'medium'}} primary={true}>
        <Form onSubmit={this._onSubmit}>

          <Header size="large" justify="between" pad="none">
            <Heading tag="h2" margin="none" strong={true}>
              Add Virtual Machine
            </Heading>
            <Anchor icon={<CloseIcon />} path="/virtual-machines"
              a11yTitle='Close Add Virtual Machine Form' />
          </Header>

          <FormFields>

            <fieldset>
              {countField}
              {nameFields}
            </fieldset>

            {sizeFields}
            {imageField}
            {networkFields}

          </FormFields>

          <Footer pad={{vertical: 'medium'}}>
            <span />
            <Button type="submit" primary={true} label="Add"
              onClick={this._onSubmit} />
          </Footer>
        </Form>
        {layer}
      </Article>
    );
  }
}

VirtualMachineAdd.propTypes = {
  adding: PropTypes.bool.isRequired,
  images: PropTypes.arrayOf(PropTypes.object).isRequired,
  sizes: PropTypes.arrayOf(PropTypes.object).isRequired
};

VirtualMachineAdd.contextTypes = {
  router: PropTypes.object
};

let select = (state) => {
  return {
    adding: state.vm.adding || state.item.changing,
    images: state.vm.images,
    sizes: state.vm.sizes
  };
};

export default connect(select)(VirtualMachineAdd);