feat(cp-frontend): add and edit env_file's

This commit is contained in:
Sérgio Ramos 2017-07-15 04:34:34 +01:00 committed by Judit Greskovits
parent 0f74cb5453
commit f68c2ae78a
12 changed files with 191 additions and 25 deletions

View File

@ -30,6 +30,7 @@
"joyent-ui-toolkit": "^1.1.0", "joyent-ui-toolkit": "^1.1.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"lodash.remove": "^4.7.0",
"normalized-styled-components": "^1.0.8", "normalized-styled-components": "^1.0.8",
"param-case": "^2.1.1", "param-case": "^2.1.1",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
@ -46,7 +47,7 @@
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-actions": "^2.0.3", "redux-actions": "^2.0.3",
"redux-batched-actions": "^0.2.0", "redux-batched-actions": "^0.2.0",
"redux-form": "^6.8.0", "redux-form": "^7.0.0",
"remcalc": "^1.0.8", "remcalc": "^1.0.8",
"reselect": "^3.0.1", "reselect": "^3.0.1",
"simple-statistics": "^4.1.0", "simple-statistics": "^4.1.0",
@ -54,7 +55,8 @@
"styled-is": "^1.0.11", "styled-is": "^1.0.11",
"styled-text-spinners": "^1.0.1", "styled-text-spinners": "^1.0.1",
"title-case": "^2.1.1", "title-case": "^2.1.1",
"unitcalc": "^1.0.8" "unitcalc": "^1.0.8",
"uuid": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"apr-for-each": "^1.0.6", "apr-for-each": "^1.0.6",

View File

@ -49,6 +49,30 @@ const ButtonsRow = Row.extend`
margin-bottom: ${remcalc(60)}; margin-bottom: ${remcalc(60)};
`; `;
const FilenameContainer = styled.span`
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: space-between;
align-content: stretch;
align-items: stretch;
`;
const FilenameInput = styled(Input)`
order: 0;
flex: 1 1 auto;
align-self: stretch;
`;
const FilenameRemove = Button.extend`
order: 0;
flex: 0 1 auto;
align-self: auto;
margin: ${remcalc(8)};
margin-right: 0;
height: ${remcalc(48)};
`;
const MEditor = ManifestEditor => ({ input, defaultValue }) => const MEditor = ManifestEditor => ({ input, defaultValue }) =>
<ManifestEditor mode="yaml" {...input} value={input.value || defaultValue} />; <ManifestEditor mode="yaml" {...input} value={input.value || defaultValue} />;
@ -71,7 +95,13 @@ export const Name = ({ handleSubmit, onCancel, dirty }) =>
</ButtonsRow> </ButtonsRow>
</form>; </form>;
export const Manifest = ({ handleSubmit, onCancel, dirty, defaultValue }) => export const Manifest = ({
handleSubmit,
onCancel,
dirty,
defaultValue = '',
loading
}) =>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Bundle load={() => import('joyent-manifest-editor')}> <Bundle load={() => import('joyent-manifest-editor')}>
{ManifestEditor => {ManifestEditor =>
@ -85,17 +115,67 @@ export const Manifest = ({ handleSubmit, onCancel, dirty, defaultValue }) =>
</Bundle> </Bundle>
<ButtonsRow> <ButtonsRow>
<Button onClick={onCancel} secondary>Cancel</Button> <Button onClick={onCancel} secondary>Cancel</Button>
<Button disabled={!dirty} type="submit"> <Button
disabled={!(dirty || !loading || defaultValue.length)}
type="submit"
>
Environment Environment
</Button> </Button>
</ButtonsRow> </ButtonsRow>
</form>; </form>;
const Filename = ({ name, onRemoveFile }) =>
<FilenameContainer>
<FilenameInput
type="text"
placeholder="Filename including extension…"
defaultValue={name}
/>
<FilenameRemove type="button" onClick={onRemoveFile} secondary>
Remove
</FilenameRemove>
</FilenameContainer>;
export const Files = ({ loading, files, onRemoveFile }) => {
if (loading) {
return null;
}
const _files = files.map(({ id, name, value }) =>
<div key={id}>
<FormGroup name={`file-name-${id}`} reduxForm>
<FormMeta left />
<Filename name={name} onRemoveFile={() => onRemoveFile(id)} />
</FormGroup>
<Bundle load={() => import('joyent-manifest-editor')}>
{ManifestEditor =>
ManifestEditor
? <Field
name={`file-value-${id}`}
defaultValue={value}
component={EEditor(ManifestEditor)}
/>
: <Dots2 />}
</Bundle>
</div>
);
return (
<div>
<H3>Files:</H3>
{_files}
</div>
);
};
export const Environment = ({ export const Environment = ({
handleSubmit, handleSubmit,
onCancel, onCancel,
onAddFile,
onRemoveFile,
dirty, dirty,
defaultValue, defaultValue = '',
files = [],
loading loading
}) => }) =>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
@ -109,9 +189,14 @@ export const Environment = ({
/> />
: <Dots2 />} : <Dots2 />}
</Bundle> </Bundle>
<Files files={files} onRemoveFile={onRemoveFile} loading={loading} />
<ButtonsRow> <ButtonsRow>
<Button onClick={onCancel} secondary>Cancel</Button> <Button onClick={onCancel} secondary>Cancel</Button>
<Button disabled={loading || !dirty} type="submit"> <Button type="button" onClick={onAddFile} secondary>Add File</Button>
<Button
disabled={!(dirty || !loading || defaultValue.length)}
type="submit"
>
{loading ? <Dots2 /> : 'Review'} {loading ? <Dots2 /> : 'Review'}
</Button> </Button>
</ButtonsRow> </ButtonsRow>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import DeploymentGroupEditOrCreate from './edit-or-create'; import ManifestEditOrCreate from '@containers/manifest/edit-or-create';
import { Progress } from '@components/deployment-groups/create'; import { Progress } from '@components/deployment-groups/create';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { DeploymentGroupsLoading } from '@components/deployment-groups'; import { DeploymentGroupsLoading } from '@components/deployment-groups';
@ -21,7 +21,7 @@ export default ({
{loading && <DeploymentGroupsLoading />} {loading && <DeploymentGroupsLoading />}
{error && <span>{error.toString()}</span>} {error && <span>{error.toString()}</span>}
<Progress stage={stage} create /> <Progress stage={stage} create />
<DeploymentGroupEditOrCreate <ManifestEditOrCreate
create create
manifest={manifest} manifest={manifest}
deploymentGroup={deploymentGroup} deploymentGroup={deploymentGroup}

View File

@ -1,4 +1,3 @@
export { default as DeploymentGroupList } from './list'; export { default as DeploymentGroupList } from './list';
export { default as DeploymentGroupCreate } from './create'; export { default as DeploymentGroupCreate } from './create';
export { default as DeploymentGroupImport } from './import'; export { default as DeploymentGroupImport } from './import';
export { default as DeploymentGroupManifest } from './manifest';

View File

@ -5,6 +5,8 @@ import { withRouter } from 'react-router';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import intercept from 'apr-intercept'; import intercept from 'apr-intercept';
import paramCase from 'param-case'; import paramCase from 'param-case';
import remove from 'lodash.remove';
import uuid from 'uuid/v4';
import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql'; import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql';
import DeploymentGroupCreateMutation from '@graphql/DeploymentGroupCreate.gql'; import DeploymentGroupCreateMutation from '@graphql/DeploymentGroupCreate.gql';
@ -25,7 +27,7 @@ class DeploymentGroupEditOrCreate extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { create, edit } = props; const { create, edit, files = [] } = props;
const type = create ? 'create' : 'edit'; const type = create ? 'create' : 'edit';
const NameForm = const NameForm =
@ -68,12 +70,21 @@ class DeploymentGroupEditOrCreate extends Component {
forceUnregisterOnUnmount: true forceUnregisterOnUnmount: true
})(Review); })(Review);
if (!files.length) {
files.push({
id: uuid(),
name: '',
value: '#'
});
}
this.state = { this.state = {
defaultStage: create ? 'name' : 'edit', defaultStage: create ? 'name' : 'edit',
manifestStage: create ? 'manifest' : 'edit', manifestStage: create ? 'manifest' : 'edit',
name: '', name: '',
manifest: '', manifest: '',
environment: '', environment: '',
files,
services: [], services: [],
loading: false, loading: false,
error: null, error: null,
@ -97,6 +108,8 @@ class DeploymentGroupEditOrCreate extends Component {
this.handleEnvironmentSubmit = this.handleEnvironmentSubmit.bind(this); this.handleEnvironmentSubmit = this.handleEnvironmentSubmit.bind(this);
this.handleReviewSubmit = this.handleReviewSubmit.bind(this); this.handleReviewSubmit = this.handleReviewSubmit.bind(this);
this.handleCancel = this.handleCancel.bind(this); this.handleCancel = this.handleCancel.bind(this);
this.handleFileAdd = this.handleFileAdd.bind(this);
this.handleRemoveFile = this.handleRemoveFile.bind(this);
if (edit) { if (edit) {
setTimeout(this.getDeploymentGroup, 16); setTimeout(this.getDeploymentGroup, 16);
@ -132,7 +145,7 @@ class DeploymentGroupEditOrCreate extends Component {
}; };
provision = async deploymentGroupId => { provision = async deploymentGroupId => {
const { manifest, environment } = this.state; const { manifest, environment, files } = this.state;
const { provisionManifest } = this.props; const { provisionManifest } = this.props;
const [err] = await intercept( const [err] = await intercept(
@ -141,6 +154,7 @@ class DeploymentGroupEditOrCreate extends Component {
type: 'COMPOSE', type: 'COMPOSE',
format: 'YAML', format: 'YAML',
environment, environment,
files,
raw: manifest raw: manifest
}) })
); );
@ -161,15 +175,39 @@ class DeploymentGroupEditOrCreate extends Component {
} }
handleManifestSubmit({ manifest = '' }) { handleManifestSubmit({ manifest = '' }) {
this.setState({ manifest }, () => { this.setState({ manifest: manifest || this.props.manifest }, () => {
this.redirect({ stage: 'environment', prog: true }); this.redirect({ stage: 'environment', prog: true });
}); });
} }
handleEnvironmentSubmit({ environment = '' }) { handleEnvironmentSubmit(change) {
const { environment = '' } = change;
const { name, manifest } = this.state; 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 getConfig = async () => {
const { environment } = this.state;
const [err, conf] = await intercept( const [err, conf] = await intercept(
client.query({ client.query({
query: DeploymentGroupConfigQuery, query: DeploymentGroupConfigQuery,
@ -179,6 +217,7 @@ class DeploymentGroupEditOrCreate extends Component {
type: 'COMPOSE', type: 'COMPOSE',
format: 'YAML', format: 'YAML',
environment, environment,
files,
raw: manifest raw: manifest
} }
}) })
@ -193,12 +232,15 @@ class DeploymentGroupEditOrCreate extends Component {
const { data } = conf; const { data } = conf;
const { config: services } = data; const { config: services } = data;
this.setState({ loading: false, services }, () => { this.setState({ loading: false, services, files }, () => {
this.redirect({ stage: 'review', prog: true }); this.redirect({ stage: 'review', prog: true });
}); });
}; };
this.setState({ environment, loading: true }, getConfig); this.setState(
{ environment: environment || this.props.environment, loading: true },
getConfig
);
} }
handleReviewSubmit() { handleReviewSubmit() {
@ -229,6 +271,24 @@ class DeploymentGroupEditOrCreate extends Component {
history.push(create ? '/' : `/deployment-groups/${deploymentGroup.slug}`); 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 }) { redirect({ stage = 'name', prog = false }) {
const { match, history, create } = this.props; const { match, history, create } = this.props;
@ -271,8 +331,11 @@ class DeploymentGroupEditOrCreate extends Component {
return ( return (
<EnvironmentForm <EnvironmentForm
defaultValue={this.props.environment} defaultValue={this.props.environment}
files={this.state.files}
onSubmit={this.handleEnvironmentSubmit} onSubmit={this.handleEnvironmentSubmit}
onCancel={this.handleCancel} onCancel={this.handleCancel}
onAddFile={this.handleFileAdd}
onRemoveFile={this.handleRemoveFile}
loading={this.state.loading} loading={this.state.loading}
/> />
); );

View File

@ -5,7 +5,7 @@ import get from 'lodash.get';
import ManifestQuery from '@graphql/Manifest.gql'; import ManifestQuery from '@graphql/Manifest.gql';
import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql'; import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql';
import DeploymentGroupEditOrCreate from './edit-or-create'; import ManifestEditOrCreate from '@containers/manifest/edit-or-create';
import { Progress } from '@components/deployment-groups/create'; import { Progress } from '@components/deployment-groups/create';
import { LayoutContainer } from '@components/layout'; import { LayoutContainer } from '@components/layout';
import { DeploymentGroupsLoading } from '@components/deployment-groups'; import { DeploymentGroupsLoading } from '@components/deployment-groups';
@ -15,6 +15,7 @@ const Manifest = ({
loading, loading,
error, error,
manifest = '', manifest = '',
environment = '',
deploymentGroup = null, deploymentGroup = null,
match match
}) => { }) => {
@ -24,9 +25,10 @@ const Manifest = ({
const _view = loading || !deploymentGroup const _view = loading || !deploymentGroup
? null ? null
: <DeploymentGroupEditOrCreate : <ManifestEditOrCreate
edit edit
manifest={manifest} manifest={manifest}
environment={environment}
deploymentGroup={deploymentGroup} deploymentGroup={deploymentGroup}
/>; />;
@ -36,7 +38,8 @@ const Manifest = ({
deploymentGroup.imported && deploymentGroup.imported &&
!manifest !manifest
? <span> ? <span>
Since this DeploymentGroup was imported, it doesn't have the initial Since this DeploymentGroup was imported, it doesn&#x27;t have the
initial
manifest manifest
</span> </span>
: null; : null;
@ -62,6 +65,7 @@ export default compose(
}), }),
props: ({ data: { deploymentGroup, loading, error } }) => ({ props: ({ data: { deploymentGroup, loading, error } }) => ({
manifest: get(deploymentGroup, 'version.manifest.raw', ''), manifest: get(deploymentGroup, 'version.manifest.raw', ''),
environment: get(deploymentGroup, 'version.manifest.environment', ''),
loading, loading,
error error
}) })

View File

@ -0,0 +1 @@
export default null;

View File

@ -1,7 +1,7 @@
#import "./ServiceInfo.gql" #import "./ServiceInfo.gql"
query config($deploymentGroupName: String!, $type: ManifestType!, $format: ManifestFormat!, $environment: String!, $raw: String!) { query config($deploymentGroupName: String!, $type: ManifestType!, $format: ManifestFormat!, $environment: String!, $files: [KeyValueInput]!, $raw: String!) {
config(deploymentGroupName: $deploymentGroupName, type: $type, format: $format, environment: $environment, raw: $raw) { config(deploymentGroupName: $deploymentGroupName, type: $type, format: $format, environment: $environment, files: $files, raw: $raw) {
...ServiceInfo ...ServiceInfo
config { config {
id id

View File

@ -1,5 +1,5 @@
mutation provisionManifest($deploymentGroupId: ID!, $type: ManifestType!, $format: ManifestFormat!, $environment: String!, $raw: String!) { mutation provisionManifest($deploymentGroupId: ID!, $type: ManifestType!, $format: ManifestFormat!, $environment: String!, $files: [KeyValueInput]!, $raw: String!) {
provisionManifest(deploymentGroupId: $deploymentGroupId, type: $type, format: $format, environment: $environment, raw: $raw) { provisionManifest(deploymentGroupId: $deploymentGroupId, type: $type, format: $format, environment: $environment, files: $files, raw: $raw) {
scale { scale {
serviceName serviceName
replicas replicas

View File

@ -4,6 +4,7 @@ query ManifestById($deploymentGroupSlug: String!) {
manifest { manifest {
id id
type type
environment
format format
raw raw
} }

View File

@ -5,12 +5,13 @@ import styled from 'styled-components';
import { Header, Breadcrumb, Menu } from '@containers/navigation'; import { Header, Breadcrumb, Menu } from '@containers/navigation';
import { ServiceScale, ServiceDelete } from '@containers/service'; import { ServiceScale, ServiceDelete } from '@containers/service';
import { InstanceList } from '@containers/instances'; import { InstanceList } from '@containers/instances';
import Manifest from '@containers/manifest';
import Rollback from '@containers/rollback';
import { import {
DeploymentGroupList, DeploymentGroupList,
DeploymentGroupCreate, DeploymentGroupCreate,
DeploymentGroupImport, DeploymentGroupImport
DeploymentGroupManifest
} from '@containers/deployment-groups'; } from '@containers/deployment-groups';
import { import {
@ -116,7 +117,13 @@ const Router = (
<Route <Route
path="/deployment-groups/:deploymentGroup/manifest/:stage?" path="/deployment-groups/:deploymentGroup/manifest/:stage?"
exact exact
component={DeploymentGroupManifest} component={Manifest}
/>
<Route
path="/deployment-groups/:deploymentGroup/rollback"
exact
component={Rollback}
/> />
<Route <Route

View File

@ -13,6 +13,10 @@ const state = {
{ {
pathname: 'manifest', pathname: 'manifest',
name: 'Manifest' name: 'Manifest'
},
{
pathname: 'rollback',
name: 'Rollback'
} }
], ],
services: [ services: [