import React, { Component } from 'react'; import { reduxForm } from 'redux-form'; import { compose, graphql } from 'react-apollo'; import { withRouter } from 'react-router'; import { Redirect } from 'react-router-dom'; import intercept from 'apr-intercept'; import paramCase from 'param-case'; import remove from 'lodash.remove'; import uuid from 'uuid/v4'; import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql'; import DeploymentGroupCreateMutation from '@graphql/DeploymentGroupCreate.gql'; import DeploymentGroupProvisionMutation from '@graphql/DeploymentGroupProvision.gql'; import DeploymentGroupConfigQuery from '@graphql/DeploymentGroupConfig.gql'; import { client } from '@state/store'; import { Name, Manifest, Environment, Review } from '@components/manifest/edit-or-create'; // TODO: move state to redux. why: because in redux we can cache transactional // state between refreshes class DeploymentGroupEditOrCreate extends Component { constructor(props) { super(props); const { create, edit, files = [] } = props; const type = create ? 'create' : 'edit'; const NameForm = create && reduxForm({ form: `${type}-deployment-group`, destroyOnUnmount: true, forceUnregisterOnUnmount: true, asyncValidate: async ({ name = '' }) => { const { data } = await client.query({ fetchPolicy: 'network-only', query: DeploymentGroupBySlugQuery, variables: { slug: paramCase(name.trim()) } }); if (data.deploymentGroups.length) { // eslint-disable-next-line no-throw-literal throw { name: `"${name}" already exists!` }; } } })(Name); const ManifestForm = reduxForm({ form: `${type}-deployment-group`, destroyOnUnmount: true, forceUnregisterOnUnmount: true })(Manifest); const EnvironmentForm = reduxForm({ form: `${type}-deployment-group`, destroyOnUnmount: true, forceUnregisterOnUnmount: true })(Environment); const ReviewForm = reduxForm({ form: `${type}-deployment-group`, destroyOnUnmount: true, forceUnregisterOnUnmount: true })(Review); if (!files.length) { files.push({ id: uuid(), name: '', value: '#' }); } this.state = { defaultStage: create ? 'name' : 'edit', manifestStage: create ? 'manifest' : 'edit', name: '', manifest: '', environment: '', files, services: [], loading: false, error: null, NameForm, ManifestForm, EnvironmentForm, ReviewForm }; this.stages = { name: create && this.renderNameForm.bind(this), [create ? 'manifest' : 'edit']: this.renderManifestEditor.bind(this), environment: this.renderEnvironmentEditor.bind(this), review: this.renderReview.bind(this) }; this.handleNameSubmit = type === 'create' && this.handleNameSubmit.bind(this); this.handleManifestSubmit = this.handleManifestSubmit.bind(this); this.handleEnvironmentSubmit = this.handleEnvironmentSubmit.bind(this); this.handleReviewSubmit = this.handleReviewSubmit.bind(this); this.handleCancel = this.handleCancel.bind(this); this.handleFileAdd = this.handleFileAdd.bind(this); this.handleRemoveFile = this.handleRemoveFile.bind(this); if (edit) { setTimeout(this.getDeploymentGroup, 16); } } createDeploymentGroup = async () => { const { createDeploymentGroup, deploymentGroup, edit } = this.props; if (edit && (!deploymentGroup || !deploymentGroup.id)) { this.setState({ error: 'Unexpected Error: Inexistent DeploymentGroup!' }); return {}; } if (deploymentGroup && deploymentGroup.id) { return deploymentGroup; } const { name } = this.state; const [err, res] = await intercept(createDeploymentGroup({ name })); if (err) { this.setState({ error: err.message }); } return err ? {} : res.data.createDeploymentGroup; }; provision = async deploymentGroupId => { const { manifest, environment, files } = this.state; const { provisionManifest } = this.props; const [err] = await intercept( provisionManifest({ deploymentGroupId, type: 'COMPOSE', format: 'YAML', environment, files, raw: manifest }) ); if (err) { this.setState({ error: err.message }); } return err ? null : true; }; handleNameSubmit({ name = '' }) { this.setState({ name }, () => this.redirect({ stage: 'manifest', prog: true }) ); } handleManifestSubmit({ manifest = '' }) { this.setState({ manifest: manifest || this.props.manifest }, () => { this.redirect({ stage: 'environment', prog: true }); }); } handleEnvironmentSubmit(change) { const { environment = '' } = change; const { name, manifest } = this.state; const files = Object.values( Object.keys(change).reduce((acc, key) => { const match = key.match(/file-(name|value)-(.*)/); if (!match) { return acc; } const [_, type, id] = match; if (!acc[id]) { acc[id] = { id }; } acc[id][type] = change[key]; return acc; }, {}) ); const getConfig = async () => { const { environment } = this.state; const [err, conf] = await intercept( client.query({ query: DeploymentGroupConfigQuery, fetchPolicy: 'network-only', variables: { deploymentGroupName: name, type: 'COMPOSE', format: 'YAML', environment: environment || '', files, raw: manifest } }) ); if (err) { return this.setState({ error: err.message }); } const { data } = conf; const { config: services } = data; this.setState({ loading: false, services, files }, () => { this.redirect({ stage: 'review', prog: true }); }); }; this.setState( { environment: environment || this.props.environment, loading: true }, getConfig ); } handleReviewSubmit() { const { history } = this.props; const submit = async () => { const { id, slug } = await this.createDeploymentGroup(); if (!id) { return; } const manifest = await this.provision(id); if (!manifest) { return; } history.push(`/deployment-groups/${slug}`); }; this.setState({ loading: true }, submit); } handleCancel() { const { history, create, deploymentGroup } = this.props; history.push(create ? '/' : `/deployment-groups/${deploymentGroup.slug}`); } handleFileAdd() { this.setState({ files: this.state.files.concat([ { id: uuid(), name: '', value: '#' } ]) }); } handleRemoveFile(fileId) { this.setState({ files: remove(this.state.files, ({ id }) => id !== fileId) }); } redirect({ stage = 'name', prog = false }) { const { match, history, create } = this.props; const regex = create ? /\/~create(.*)/ : /\/manifest(.*)/; const to = match.url.replace( regex, create ? `/~create/${stage}` : `/manifest/${stage}` ); if (!prog) { return ; } history.push(to); } renderNameForm() { const { NameForm } = this.state; return ( ); } renderManifestEditor() { const { ManifestForm } = this.state; return ( ); } renderEnvironmentEditor() { const { EnvironmentForm } = this.state; return ( ); } renderReview() { const { ReviewForm } = this.state; return ( ); } render() { const { error, defaultStage, manifestStage } = this.state; if (error) { return {error}; } const { match, create } = this.props; const stage = match.params.stage; if (!stage) { return this.redirect({ stage: defaultStage }); } if (!this.stages[stage]) { return this.redirect({ stage: defaultStage }); } if (create && stage !== 'name' && !this.state.name) { return this.redirect({ stage: defaultStage }); } if (stage === 'environment' && !this.state.manifest) { return this.redirect({ stage: manifestStage }); } if (stage === 'review' && !this.state.environment) { if (!this.state.manifest) { return this.redirect({ stage: manifestStage }); } if (!this.state.environment) { return this.redirect({ stage: 'environment' }); } } return this.stages[stage](); } } export default compose( graphql(DeploymentGroupCreateMutation, { props: ({ mutate }) => ({ createDeploymentGroup: variables => mutate({ variables }) }) }), graphql(DeploymentGroupProvisionMutation, { props: ({ mutate }) => ({ provisionManifest: variables => mutate({ variables }) }) }) )(withRouter(DeploymentGroupEditOrCreate));