1
0
mirror of https://github.com/yldio/copilot.git synced 2024-11-28 06:00:06 +02:00

feat(ui-toolkit, cp-fronted, portal-api): Env variables input redesign

This commit is contained in:
JUDIT GRESKOVITS 2017-08-08 11:07:18 +01:00 committed by Judit Greskovits
parent 2eb7f4197f
commit 2fb4a77c96
43 changed files with 856 additions and 387 deletions

View File

@ -27,9 +27,13 @@
"jest-cli": "^20.0.4", "jest-cli": "^20.0.4",
"joyent-manifest-editor": "^1.0.0", "joyent-manifest-editor": "^1.0.0",
"joyent-ui-toolkit": "^1.1.0", "joyent-ui-toolkit": "^1.1.0",
"js-yaml": "^3.9.1",
"lodash.find": "^4.6.0",
"lodash.flatten": "^4.4.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", "lodash.remove": "^4.7.0",
"lodash.uniq": "^4.5.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",

View File

@ -12,7 +12,6 @@
.CodeMirror { .CodeMirror {
border: solid 1px #d8d8d8; border: solid 1px #d8d8d8;
margin-bottom: 8px;
} }
html, body, #root { html, body, #root {

View File

@ -1,11 +1,11 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Field } from 'redux-form'; import { Field } from 'redux-form';
import styled from 'styled-components'; import styled from 'styled-components';
import SimpleTable from 'react-simple-table';
import { Row, Col } from 'react-styled-flexboxgrid'; import { Row, Col } from 'react-styled-flexboxgrid';
import Bundle from 'react-bundle'; import Bundle from 'react-bundle';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import forceArray from 'force-array'; import forceArray from 'force-array';
import is from 'styled-is';
import { Loader } from '@components/messaging'; import { Loader } from '@components/messaging';
@ -19,36 +19,56 @@ import {
ProgressbarItem, ProgressbarItem,
ProgressbarButton, ProgressbarButton,
H3, H3,
P,
typography, typography,
StatusLoader Divider,
Chevron
} from 'joyent-ui-toolkit'; } from 'joyent-ui-toolkit';
const EnvironmentChevron = Chevron.extend`
float: right;
`;
const EnvironmentDivider = Divider.extend`
margin-top: ${remcalc(34)};
`;
const ServiceDivider = Divider.extend`
margin: ${remcalc(13)} ${remcalc(-20)} 0 ${remcalc(-20)};
`;
const Dl = styled.dl` const Dl = styled.dl`
margin: ${remcalc(13)} ${remcalc(19)}; margin: 0;
`; `;
const ServiceName = H3.extend` const ServiceName = H3.extend`
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: ${remcalc(5)};
line-height: 1.6; line-height: 1.6;
font-weight: 600; font-size: ${remcalc(18)};
`; `;
const ServiceCard = Card.extend` const ImageTitle = H3.extend`
min-height: ${remcalc(72)};
`;
const ImageTitle = ServiceName.extend`
display: inline-block; display: inline-block;
margin: 0;
`; `;
const Image = styled.span` const Image = styled.span`
${typography.fontFamily}; ${typography.fontFamily};
font-size: ${remcalc(15)};
`;
const ServiceEnvironmentTitle = P.extend`
margin: ${remcalc(13)} 0 0 0;
${is('expanded')`
margin-bottom: ${remcalc(13)};
`};
`; `;
const ButtonsRow = Row.extend` const ButtonsRow = Row.extend`
margin-top: ${remcalc(29)}; margin: ${remcalc(29)} 0 ${remcalc(60)} 0;
margin-bottom: ${remcalc(60)};
`; `;
const FilenameContainer = styled.span` const FilenameContainer = styled.span`
@ -64,17 +84,36 @@ const FilenameInput = styled(Input)`
order: 0; order: 0;
flex: 1 1 auto; flex: 1 1 auto;
align-self: stretch; align-self: stretch;
margin: 0 0 ${remcalc(13)} 0;
`; `;
const FilenameRemove = Button.extend` const FilenameRemove = Button.extend`
order: 0; order: 0;
flex: 0 1 auto; flex: 0 1 auto;
align-self: auto; align-self: auto;
margin: ${remcalc(8)}; margin: 0 0 0 ${remcalc(8)};
margin-right: 0;
height: ${remcalc(48)}; height: ${remcalc(48)};
`; `;
const FileCard = Card.extend`
padding: ${remcalc(24)} ${remcalc(19)};
`;
const ServiceCard = Card.extend`
padding: ${remcalc(13)} ${remcalc(19)};
min-height: initial;
`;
const Subtitle = H3.extend`
margin-top: ${remcalc(34)};
margin-bottom: ${remcalc(3)};
`;
const Description = P.extend`
margin-top: ${remcalc(3)};
margin-bottom: ${remcalc(20)};
`;
class ManifestEditorBundle extends Component { class ManifestEditorBundle extends Component {
constructor() { constructor() {
super(); super();
@ -112,18 +151,20 @@ class ManifestEditorBundle extends Component {
} }
} }
const MEditor = ({ input, defaultValue }) => const MEditor = ({ input, defaultValue, readOnly }) =>
<ManifestEditorBundle <ManifestEditorBundle
mode="yaml" mode="yaml"
{...input} {...input}
value={input.value || defaultValue} value={input.value || defaultValue}
readOnly={readOnly}
/>; />;
const EEditor = ({ input, defaultValue }) => const EEditor = ({ input, defaultValue, readOnly }) =>
<ManifestEditorBundle <ManifestEditorBundle
mode="ini" mode="ini"
{...input} {...input}
value={input.value || defaultValue} value={input.value || defaultValue}
readOnly={readOnly}
/>; />;
export const Name = ({ handleSubmit, onCancel, dirty }) => export const Name = ({ handleSubmit, onCancel, dirty }) =>
@ -137,7 +178,7 @@ export const Name = ({ handleSubmit, onCancel, dirty }) =>
</Col> </Col>
</Row> </Row>
<ButtonsRow> <ButtonsRow>
<Button onClick={onCancel} secondary> <Button type="button" onClick={onCancel} secondary>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={!dirty}> <Button type="submit" disabled={!dirty}>
@ -156,53 +197,74 @@ export const Manifest = ({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Field name="manifest" defaultValue={defaultValue} component={MEditor} /> <Field name="manifest" defaultValue={defaultValue} component={MEditor} />
<ButtonsRow> <ButtonsRow>
<Button onClick={onCancel} secondary> <Button type="button" onClick={onCancel} secondary>
Cancel Cancel
</Button> </Button>
<Button <Button
disabled={!(dirty || !loading || defaultValue.length)} disabled={!(dirty || !loading || defaultValue.length)}
loading={loading}
type="submit" type="submit"
> >
{loading ? <StatusLoader /> : 'Environment'} Environment
</Button> </Button>
</ButtonsRow> </ButtonsRow>
</form>; </form>;
const Filename = ({ name, onRemoveFile }) => const File = ({ id, name, value, onRemoveFile, readOnly }) => {
<FilenameContainer> const removeButton = !readOnly
<FilenameInput ? <FilenameRemove type="button" onClick={onRemoveFile} secondary>
type="text" Remove
placeholder="Filename including extension…" </FilenameRemove>
defaultValue={name} : null;
/>
<FilenameRemove type="button" onClick={onRemoveFile} secondary>
Remove
</FilenameRemove>
</FilenameContainer>;
export const Files = ({ loading, files, onRemoveFile }) => { const fileEditor = !readOnly
if (loading) { ? <Field
return <Loader />;
}
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>
<Field
name={`file-value-${id}`} name={`file-value-${id}`}
defaultValue={value} defaultValue={value}
component={EEditor} component={EEditor}
/> />
</div> : <EEditor input={{ value }} readOnly />;
const input = !readOnly
? <FilenameInput type="text" placeholder="Filename including extension…" />
: <FilenameInput
type="text"
placeholder="Filename including extension…"
value={name}
/>;
return (
<FileCard>
<FormGroup name={`file-name-${id}`} reduxForm={!readOnly}>
<FilenameContainer>
{input}
{removeButton}
</FilenameContainer>
</FormGroup>
{fileEditor}
</FileCard>
); );
};
const Files = ({ files, onAddFile, onRemoveFile, readOnly }) => {
const footer = !readOnly
? <Button type="button" onClick={onAddFile} secondary>
Create new .env file
</Button>
: null;
return ( return (
<div> <div>
<H3>Files:</H3> {files.map(({ id, ...rest }) =>
{_files} <File
key={id}
id={id}
onRemoveFile={() => onRemoveFile(id)}
readOnly={readOnly}
{...rest}
/>
)}
{footer}
</div> </div>
); );
}; };
@ -215,66 +277,101 @@ export const Environment = ({
dirty, dirty,
defaultValue = '', defaultValue = '',
files = [], files = [],
readOnly = false,
loading loading
}) => }) => {
<form onSubmit={handleSubmit}> const envEditor = !readOnly
<Field name="environment" defaultValue={defaultValue} component={EEditor} /> ? <Field
<Files files={files} onRemoveFile={onRemoveFile} loading={loading} /> name="environment"
<ButtonsRow> defaultValue={defaultValue}
<Button onClick={onCancel} secondary> component={EEditor}
Cancel />
</Button> : <EEditor input={{ value: defaultValue }} readOnly />;
<Button type="button" onClick={onAddFile} secondary>
Add File const footerDivider = !readOnly ? <EnvironmentDivider /> : null;
</Button>
<Button const footer = !readOnly
disabled={!(dirty || !loading || defaultValue.length)} ? <ButtonsRow>
type="submit" <Button type="button" onClick={onCancel} secondary>
> Cancel
{loading ? <StatusLoader /> : 'Review'} </Button>
</Button> <Button
</ButtonsRow> disabled={!(dirty || !loading || defaultValue.length)}
</form>; loading={loading}
type="submit"
>
Continue
</Button>
</ButtonsRow>
: null;
return (
<form onSubmit={handleSubmit}>
<Subtitle>Global variables</Subtitle>
<Description>
These variables are going to be availabe for interpolation in the
manifest
</Description>
{envEditor}
<EnvironmentDivider />
<Subtitle>Enviroment files</Subtitle>
<Description>
The variables from this files will be applied to the services that
require them
</Description>
<Files
files={files}
onAddFile={onAddFile}
onRemoveFile={onRemoveFile}
readOnly={readOnly}
/>
{footerDivider}
{footer}
</form>
);
};
const EnvironmentReview = ({ environment }) => {
const value = environment
.map(({ name, value }) => `${name}=${value}`)
.join('\n');
return <EEditor input={{ value }} />;
};
export const Review = ({ export const Review = ({
handleSubmit, handleSubmit,
onEnvironmentToggle = () => null,
onCancel, onCancel,
dirty, dirty,
loading, loading,
environmentToggles,
...state ...state
}) => { }) => {
const serviceList = forceArray(state.services).map(({ name, config }) => const serviceList = forceArray(state.services).map(({ name, config }) =>
<ServiceCard key={name}> <ServiceCard key={name}>
<ServiceName>
{name}
</ServiceName>
<Dl> <Dl>
<dt>
<ServiceName>
{name}
</ServiceName>
</dt>
<dt> <dt>
<ImageTitle>Image:</ImageTitle> <Image>{config.image}</Image> <ImageTitle>Image:</ImageTitle> <Image>{config.image}</Image>
</dt> </dt>
{config.environment.length
? <dt>
<ImageTitle>Environment:</ImageTitle>
</dt>
: undefined}
{config.environment.length
? <SimpleTable
columns={[
{
columnHeader: 'Name',
path: 'name'
},
{
columnHeader: 'Value',
path: 'value'
}
]}
data={config.environment}
/>
: undefined}
</Dl> </Dl>
<ServiceDivider />
<ServiceEnvironmentTitle
expanded={environmentToggles[name]}
onClick={() => onEnvironmentToggle(name)}
>
Environment variables{' '}
<EnvironmentChevron
down={!environmentToggles[name]}
up={environmentToggles[name]}
/>
</ServiceEnvironmentTitle>
{environmentToggles[name]
? <EnvironmentReview environment={config.environment} />
: null}
</ServiceCard> </ServiceCard>
); );
@ -282,11 +379,11 @@ export const Review = ({
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
{serviceList} {serviceList}
<ButtonsRow> <ButtonsRow>
<Button onClick={onCancel} disabled={loading} secondary> <Button type="button" onClick={onCancel} disabled={loading} secondary>
Cancel Cancel
</Button> </Button>
<Button disabled={loading} type="submit"> <Button disabled={loading} loading={loading} type="submit">
{loading ? <StatusLoader /> : 'Confirm and Deploy'} Confirm and Deploy
</Button> </Button>
</ButtonsRow> </ButtonsRow>
</form> </form>

View File

@ -2,15 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Message } from 'joyent-ui-toolkit'; import { Message } from 'joyent-ui-toolkit';
const ErrorMessage = ({ const ErrorMessage = ({ title, message = "Ooops, there's been an error" }) =>
title, <Message title={title} message={message} type="ERROR" />;
message = 'Ooops, there\'s been an error'
}) =>
<Message
title={title}
message={message}
type='ERROR'
/>
ErrorMessage.propTypes = { ErrorMessage.propTypes = {
title: PropTypes.string, title: PropTypes.string,

View File

@ -2,15 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Message } from 'joyent-ui-toolkit'; import { Message } from 'joyent-ui-toolkit';
const WarningMessage = ({ const WarningMessage = ({ title, message }) =>
title, <Message title={title} message={message} type="WARNING" />;
message
}) =>
<Message
title={title}
message={message}
type='WARNING'
/>
WarningMessage.propTypes = { WarningMessage.propTypes = {
title: PropTypes.string, title: PropTypes.string,

View File

@ -9,13 +9,12 @@ import { Modal, ModalHeading, Button } from 'joyent-ui-toolkit'
import { withNotFound, GqlPaths } from '@containers/navigation'; import { withNotFound, GqlPaths } from '@containers/navigation';
class DeploymentGroupDelete extends Component { class DeploymentGroupDelete extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
error: null error: null
} };
} }
render() { render() {
@ -40,7 +39,8 @@ class DeploymentGroupDelete extends Component {
<ModalErrorMessage <ModalErrorMessage
title='Ooops!' title='Ooops!'
message='An error occurred while loading your deployment group.' message='An error occurred while loading your deployment group.'
onCloseClick={handleCloseClick} /> onCloseClick={handleCloseClick}
/>
</Modal> </Modal>
); );
} }
@ -56,7 +56,8 @@ class DeploymentGroupDelete extends Component {
<ModalErrorMessage <ModalErrorMessage
title='Ooops!' title='Ooops!'
message={`An error occured while attempting to delete the ${deploymentGroup.name} deployment group.`} message={`An error occured while attempting to delete the ${deploymentGroup.name} deployment group.`}
onCloseClick={handleCloseClick} /> onCloseClick={handleCloseClick}
/>
</Modal> </Modal>
); );
} }
@ -64,8 +65,7 @@ class DeploymentGroupDelete extends Component {
const handleConfirmClick = evt => { const handleConfirmClick = evt => {
deleteDeploymentGroup(deploymentGroup.id) deleteDeploymentGroup(deploymentGroup.id)
.then(() => handleCloseClick()) .then(() => handleCloseClick())
.catch((err) => { .catch(err => {
console.log('err = ', err);
this.setState({ error: err }); this.setState({ error: err });
}); });
}; };

View File

@ -46,8 +46,9 @@ class DeploymentGroupImport extends Component {
<LayoutContainer> <LayoutContainer>
{_title} {_title}
<ErrorMessage <ErrorMessage
title='Ooops!' title="Ooops!"
message='An error occurred while importing your deployment groups.' /> message="An error occurred while importing your deployment groups."
/>
</LayoutContainer> </LayoutContainer>
); );
} }

View File

@ -12,7 +12,7 @@ import { Title } from '@components/navigation';
import { ErrorMessage, Loader } from '@components/messaging'; import { ErrorMessage, Loader } from '@components/messaging';
import DeploymentGroupsQuery from '@graphql/DeploymentGroups.gql'; import DeploymentGroupsQuery from '@graphql/DeploymentGroups.gql';
import DeploymentGroupsImportableQuery from '@graphql/DeploymentGroupsImportable.gql'; import DeploymentGroupsImportableQuery from '@graphql/DeploymentGroupsImportable.gql';
import { H2, H3, Small, IconButton, BinIcon } from 'joyent-ui-toolkit'; import { H3, Small, IconButton, BinIcon } from 'joyent-ui-toolkit';
import { withNotFound, GqlPaths } from '@containers/navigation'; import { withNotFound, GqlPaths } from '@containers/navigation';
const DGsRows = Row.extend` const DGsRows = Row.extend`
@ -134,8 +134,9 @@ const DeploymentGroupList = ({
<LayoutContainer> <LayoutContainer>
{_title} {_title}
<ErrorMessage <ErrorMessage
title='Ooops!' title="Ooops!"
message='An error occured while loading your deployment groups.' /> message="An error occured while loading your deployment groups."
/>
</LayoutContainer> </LayoutContainer>
); );
} }

View File

@ -0,0 +1,64 @@
import React, { Component } from 'react';
import { compose, graphql } from 'react-apollo';
import get from 'lodash.get';
import ManifestQuery from '@graphql/Manifest.gql';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
import { Loader, ErrorMessage, WarningMessage } from '@components/messaging';
import { Environment } from '@components/manifest/edit-or-create';
const EnvironmentReadOnly = ({
files = [],
environment = '',
loading,
error
}) => {
const _title = <Title>Environment</Title>;
if (loading && !environment.length && !files.length) {
return (
<LayoutContainer center>
{_title}
<Loader />
</LayoutContainer>
);
}
if (error) {
return (
<LayoutContainer>
{_title}
<ErrorMessage
title="Ooops!"
message="An error occured while loading environment data."
/>
</LayoutContainer>
);
}
return (
<LayoutContainer>
{_title}
<Environment defaultValue={environment} files={files} readOnly />
</LayoutContainer>
);
};
export default compose(
graphql(ManifestQuery, {
options: props => ({
fetchPolicy: 'cache-and-network',
variables: {
deploymentGroupSlug: props.match.params.deploymentGroup
}
}),
props: ({ data: { deploymentGroup, loading, error } }) => ({
files: get(deploymentGroup, 'version.manifest.files', []),
environment: get(deploymentGroup, 'version.manifest.environment', ''),
loading,
error
})
})
)(EnvironmentReadOnly);

View File

@ -1,8 +1,6 @@
import React, { Component } from 'react'; import React from 'react';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import InstancesQuery from '@graphql/Instances.gql'; import InstancesQuery from '@graphql/Instances.gql';
import { Row } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import forceArray from 'force-array'; import forceArray from 'force-array';
import sortBy from 'lodash.sortby'; import sortBy from 'lodash.sortby';
@ -30,8 +28,9 @@ const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
<LayoutContainer> <LayoutContainer>
{_title} {_title}
<ErrorMessage <ErrorMessage
title='Ooops!' title="Ooops!"
message='An error occured while loading your instances.' /> message="An error occured while loading your instances."
/>
</LayoutContainer> </LayoutContainer>
); );
} }

View File

@ -6,6 +6,10 @@ 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 remove from 'lodash.remove';
import flatten from 'lodash.flatten';
import uniq from 'lodash.uniq';
import find from 'lodash.find';
import { safeLoad } from 'js-yaml';
import uuid from 'uuid/v4'; import uuid from 'uuid/v4';
import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql'; import DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql';
@ -22,13 +26,15 @@ import {
Review Review
} from '@components/manifest/edit-or-create'; } from '@components/manifest/edit-or-create';
const INTERPOLATE_REGEX = /\$([_a-z][_a-z0-9]*)/gi;
// TODO: move state to redux. why: because in redux we can cache transactional // TODO: move state to redux. why: because in redux we can cache transactional
// state between refreshes // state between refreshes
class DeploymentGroupEditOrCreate extends Component { class DeploymentGroupEditOrCreate extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { create, edit, files = [] } = props; const { create, files = [], manifest } = props;
const type = create ? 'create' : 'edit'; const type = create ? 'create' : 'edit';
const NameForm = const NameForm =
@ -38,63 +44,59 @@ class DeploymentGroupEditOrCreate extends Component {
destroyOnUnmount: true, destroyOnUnmount: true,
forceUnregisterOnUnmount: true, forceUnregisterOnUnmount: true,
asyncValidate: async ({ name = '' }) => { asyncValidate: async ({ name = '' }) => {
const [err] = await intercept(client.query({ const [err, res] = await intercept(
fetchPolicy: 'network-only', client.query({
query: DeploymentGroupBySlugQuery, fetchPolicy: 'network-only',
variables: { query: DeploymentGroupBySlugQuery,
slug: paramCase(name.trim()) variables: {
} slug: paramCase(name.trim())
})); }
})
);
if (!err) { if (err) {
// eslint-disable-next-line no-throw-literal return;
throw { name: `"${name}" already exists!` };
} }
if (!res.data.deploymentGroups.length) {
return;
}
// eslint-disable-next-line no-throw-literal
throw { name: `"${name}" already exists!` };
} }
})(Name); })(Name);
const ManifestForm = reduxForm({ const ManifestForm = reduxForm({
form: `${type}-deployment-group`, form: `${type}-deployment-group`
destroyOnUnmount: true,
forceUnregisterOnUnmount: true
})(Manifest); })(Manifest);
const EnvironmentForm = reduxForm({
form: `${type}-deployment-group`,
destroyOnUnmount: true,
forceUnregisterOnUnmount: true
})(Environment);
const ReviewForm = reduxForm({ const ReviewForm = reduxForm({
form: `${type}-deployment-group`, form: `${type}-deployment-group`
destroyOnUnmount: true,
forceUnregisterOnUnmount: true
})(Review); })(Review);
if (!files.length) {
files.push({
id: uuid(),
name: '',
value: '#'
});
}
this.state = { this.state = {
type,
defaultStage: create ? 'name' : 'edit', defaultStage: create ? 'name' : 'edit',
manifestStage: create ? 'manifest' : 'edit', manifestStage: create ? 'manifest' : 'edit',
name: '', name: '',
manifest: '', manifest: '',
environment: '', environment: '',
files, files: this.resolveManifestFiles(files, manifest),
services: [], services: [],
environmentToggles: {},
loading: false, loading: false,
error: null, error: null,
NameForm, NameForm,
ManifestForm, ReviewForm,
EnvironmentForm, ManifestForm
ReviewForm
}; };
this.state.EnvironmentForm = this.getEnvironmentForm(
this.state.files,
manifest
);
this.stages = { this.stages = {
name: create && this.renderNameForm.bind(this), name: create && this.renderNameForm.bind(this),
[create ? 'manifest' : 'edit']: this.renderManifestEditor.bind(this), [create ? 'manifest' : 'edit']: this.renderManifestEditor.bind(this),
@ -111,10 +113,77 @@ class DeploymentGroupEditOrCreate extends Component {
this.handleCancel = this.handleCancel.bind(this); this.handleCancel = this.handleCancel.bind(this);
this.handleFileAdd = this.handleFileAdd.bind(this); this.handleFileAdd = this.handleFileAdd.bind(this);
this.handleRemoveFile = this.handleRemoveFile.bind(this); this.handleRemoveFile = this.handleRemoveFile.bind(this);
this.handleEnvironmentToggle = this.handleEnvironmentToggle.bind(this);
}
if (edit) { resolveManifestFiles(currentFiles = [], manifestStr = '') {
setTimeout(this.getDeploymentGroup, 16); if (!manifestStr.length) {
return [];
} }
let manifest = {};
try {
manifest = safeLoad(manifestStr);
} catch (err) {
console.error(err);
return [];
}
const services = manifest.services ? manifest.services : manifest;
const filenames = uniq(
// eslint-disable-next-line camelcase
flatten(Object.values(services).map(({ env_file }) => env_file))
);
return filenames
.filter(filename => !find(currentFiles, ['name', filename]))
.map(this.getDefaultFile)
.concat(currentFiles);
}
getEnvironmentForm(files = [], manifest = '') {
const { type } = this.state;
const initialValues = files.reduce(
(acc, { id, name, value }) =>
Object.assign(acc, {
[`file-name-${id}`]: name,
[`file-value-${id}`]: value
}),
{}
);
return reduxForm({
form: `${type}-deployment-group`,
initialValues
})(Environment);
}
getEnvironmentDefaultValue() {
const { environment = '' } = this.props;
const { manifest = '' } = this.state;
if (environment.length) {
return environment;
}
const names = manifest
.match(INTERPOLATE_REGEX)
.map(name => name.replace(/^\$/, ''));
const vars = uniq(names).map(name => `\n${name}=`).join('');
return `# define your interpolatable variables here\n${vars}`;
}
getDefaultFile(name = '') {
return {
id: uuid(),
name,
value: '# define your environment variables here\n'
};
} }
createDeploymentGroup = async () => { createDeploymentGroup = async () => {
@ -176,9 +245,19 @@ class DeploymentGroupEditOrCreate extends Component {
} }
handleManifestSubmit({ manifest = '' }) { handleManifestSubmit({ manifest = '' }) {
this.setState({ manifest: manifest || this.props.manifest }, () => { const { files } = this.state;
this.redirect({ stage: 'environment', prog: true });
}); const _manifest = manifest || this.props.manifest;
const _files = this.resolveManifestFiles(files, _manifest);
const EnvironmentForm = this.getEnvironmentForm(_files, _manifest);
this.setState(
{ manifest: _manifest, EnvironmentForm, files: _files },
() => {
this.redirect({ stage: 'environment', prog: true });
}
);
} }
handleEnvironmentSubmit(change) { handleEnvironmentSubmit(change) {
@ -270,23 +349,33 @@ class DeploymentGroupEditOrCreate extends Component {
const { history, create, deploymentGroup } = this.props; const { history, create, deploymentGroup } = this.props;
history.push(create ? '/' : `/deployment-groups/${deploymentGroup.slug}`); history.push(create ? '/' : `/deployment-groups/${deploymentGroup.slug}`);
return false;
} }
handleFileAdd() { handleFileAdd() {
const { files = [] } = this.state;
this.setState({ this.setState({
files: this.state.files.concat([ files: files.concat([this.getDefaultFile()])
{
id: uuid(),
name: '',
value: '#'
}
])
}); });
} }
handleRemoveFile(fileId) { handleRemoveFile(fileId) {
const { files = [] } = this.state;
this.setState({ this.setState({
files: remove(this.state.files, ({ id }) => id !== fileId) files: remove(files, ({ id }) => id !== fileId)
});
}
handleEnvironmentToggle(serviceName) {
const { environmentToggles } = this.state;
this.setState({
environmentToggles: Object.assign({}, environmentToggles, {
[serviceName]: !environmentToggles[serviceName]
})
}); });
} }
@ -327,40 +416,40 @@ class DeploymentGroupEditOrCreate extends Component {
} }
renderEnvironmentEditor() { renderEnvironmentEditor() {
const { EnvironmentForm } = this.state; const { EnvironmentForm, files, loading } = this.state;
return ( return (
<EnvironmentForm <EnvironmentForm
defaultValue={this.props.environment} defaultValue={this.getEnvironmentDefaultValue()}
files={this.state.files} files={files}
onSubmit={this.handleEnvironmentSubmit} onSubmit={this.handleEnvironmentSubmit}
onCancel={this.handleCancel} onCancel={this.handleCancel}
onAddFile={this.handleFileAdd} onAddFile={this.handleFileAdd}
onRemoveFile={this.handleRemoveFile} onRemoveFile={this.handleRemoveFile}
loading={this.state.loading} loading={loading}
/> />
); );
} }
renderReview() { renderReview() {
const { ReviewForm } = this.state; const { ReviewForm, environmentToggles } = this.state;
return ( return (
<ReviewForm <ReviewForm
onSubmit={this.handleReviewSubmit} onSubmit={this.handleReviewSubmit}
onCancel={this.handleCancel} onCancel={this.handleCancel}
onEnvironmentToggle={this.handleEnvironmentToggle}
environmentToggles={environmentToggles}
{...this.state} {...this.state}
/> />
); );
} }
render() { render() {
const { error, loading, defaultStage, manifestStage } = this.state; const { error, defaultStage, manifestStage, manifest, name } = this.state;
if (error) { if (error) {
return <ErrorMessage return <ErrorMessage title="Ooops!" message={error} />;
title='Ooops!'
message={error} />;
} }
const { match, create } = this.props; const { match, create } = this.props;
@ -374,11 +463,11 @@ class DeploymentGroupEditOrCreate extends Component {
return this.redirect({ stage: defaultStage }); return this.redirect({ stage: defaultStage });
} }
if (create && stage !== 'name' && !this.state.name) { if (create && stage !== 'name' && !name) {
return this.redirect({ stage: defaultStage }); return this.redirect({ stage: defaultStage });
} }
if (stage === 'environment' && !this.state.manifest) { if (stage === 'environment' && !manifest) {
return this.redirect({ stage: manifestStage }); return this.redirect({ stage: manifestStage });
} }

View File

@ -17,13 +17,15 @@ const Manifest = ({
error, error,
manifest = '', manifest = '',
environment = '', environment = '',
files = [],
deploymentGroup = null, deploymentGroup = null,
hasManifest = false,
match match
}) => { }) => {
const stage = match.params.stage; const stage = match.params.stage;
const _title = <Title>Edit Manifest</Title>; const _title = <Title>Edit Manifest</Title>;
if (loading || !deploymentGroup) { if (loading || !deploymentGroup || !hasManifest) {
return ( return (
<LayoutContainer center> <LayoutContainer center>
{_title} {_title}
@ -37,8 +39,9 @@ const Manifest = ({
<LayoutContainer> <LayoutContainer>
{_title} {_title}
<ErrorMessage <ErrorMessage
title='Ooops!' title="Ooops!"
message='An error occured while loading your deployment group.' /> message="An error occured while loading your deployment group."
/>
</LayoutContainer> </LayoutContainer>
); );
} }
@ -46,8 +49,9 @@ const Manifest = ({
const _notice = const _notice =
deploymentGroup && deploymentGroup.imported && !manifest deploymentGroup && deploymentGroup.imported && !manifest
? <WarningMessage ? <WarningMessage
title='Be aware' title="Be aware"
message='Since this DeploymentGroup was imported, it doesn&#x27;t have the initial manifest.' /> message="Since this DeploymentGroup was imported, it doesn&#x27;t have the initial manifest."
/>
: null; : null;
return ( return (
@ -58,6 +62,7 @@ const Manifest = ({
<ManifestEditOrCreate <ManifestEditOrCreate
manifest={manifest} manifest={manifest}
environment={environment} environment={environment}
files={files}
deploymentGroup={deploymentGroup} deploymentGroup={deploymentGroup}
edit edit
/> />
@ -74,8 +79,10 @@ export default compose(
} }
}), }),
props: ({ data: { deploymentGroup, loading, error } }) => ({ props: ({ data: { deploymentGroup, loading, error } }) => ({
files: get(deploymentGroup, 'version.manifest.files', []),
manifest: get(deploymentGroup, 'version.manifest.raw', ''), manifest: get(deploymentGroup, 'version.manifest.raw', ''),
environment: get(deploymentGroup, 'version.manifest.environment', ''), environment: get(deploymentGroup, 'version.manifest.environment', ''),
hasManifest: Boolean(get(deploymentGroup, 'version.manifest')),
loading, loading,
error error
}) })

View File

@ -9,13 +9,12 @@ import ServiceGql from './service-gql';
import { withNotFound, GqlPaths } from '@containers/navigation'; import { withNotFound, GqlPaths } from '@containers/navigation';
class ServiceDelete extends Component { class ServiceDelete extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
error: null error: null
} };
} }
render() { render() {
@ -40,30 +39,30 @@ class ServiceDelete extends Component {
<ModalErrorMessage <ModalErrorMessage
title='Ooops!' title='Ooops!'
message='An error occured while loading your service.' message='An error occured while loading your service.'
onCloseClick={handleCloseClick} /> onCloseClick={handleCloseClick}
/>
</Modal> </Modal>
); );
} }
const { service, deleteServices } = this.props; const { service, deleteServices } = this.props;
if(this.state.error) { if (this.state.error) {
return ( return (
<Modal width={460} onCloseClick={handleCloseClick}> <Modal width={460} onCloseClick={handleCloseClick}>
<ModalErrorMessage <ModalErrorMessage
title='Ooops!' title='Ooops!'
message={`An error occured while attempting to delete the ${service.name} service.`} message={`An error occured while attempting to delete the ${service.name} service.`}
onCloseClick={handleCloseClick} /> onCloseClick={handleCloseClick}
/>
</Modal> </Modal>
); );
} }
const handleConfirmClick = evt => { const handleConfirmClick = evt => {
deleteServices(service.id) deleteServices(service.id).then(() => handleCloseClick()).catch(err => {
.then(() => handleCloseClick()) this.setState({ error: err });
.catch((err) => { });
this.setState({ error: err });
});
}; };
return ( return (

View File

@ -10,13 +10,12 @@ import ServiceGql from './service-gql';
import { withNotFound, GqlPaths } from '@containers/navigation'; import { withNotFound, GqlPaths } from '@containers/navigation';
class ServiceScale extends Component { class ServiceScale extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
error: null error: null
} };
} }
render() { render() {
@ -41,20 +40,22 @@ class ServiceScale extends Component {
<ModalErrorMessage <ModalErrorMessage
title='Ooops!' title='Ooops!'
message='An error occured while loading your service.' message='An error occured while loading your service.'
onCloseClick={handleCloseClick} /> onCloseClick={handleCloseClick}
/>
</Modal> </Modal>
); );
} }
const { service, scale } = this.props; const { service, scale } = this.props;
if(this.state.error) { if (this.state.error) {
return ( return (
<Modal width={460} onCloseClick={handleCloseClick}> <Modal width={460} onCloseClick={handleCloseClick}>
<ModalErrorMessage <ModalErrorMessage
title='Ooops!' title='Ooops!'
message={`An error occured while attempting to scale the ${service.name} service.`} message={`An error occured while attempting to scale the ${service.name} service.`}
onCloseClick={handleCloseClick} /> onCloseClick={handleCloseClick}
/>
</Modal> </Modal>
); );
} }
@ -69,11 +70,9 @@ class ServiceScale extends Component {
}; };
const handleSubmitClick = values => { const handleSubmitClick = values => {
scale(service.id, values.replicas) scale(service.id, values.replicas).then(handleCloseClick).catch(err => {
.then(handleCloseClick) this.setState({ error: err });
.catch((err) => { });
this.setState({ error: err });
});
}; };
if (!service) { if (!service) {

View File

@ -19,8 +19,6 @@ import { ServiceListItem } from '@components/services';
import { ServicesQuickActions } from '@components/services'; import { ServicesQuickActions } from '@components/services';
import { Message } from 'joyent-ui-toolkit';
import { withNotFound, GqlPaths } from '@containers/navigation'; import { withNotFound, GqlPaths } from '@containers/navigation';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -28,13 +26,12 @@ const StyledContainer = styled.div`
`; `;
class ServiceList extends Component { class ServiceList extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
errors: {} errors: {}
} };
} }
ref(name) { ref(name) {
@ -73,8 +70,9 @@ class ServiceList extends Component {
return ( return (
<LayoutContainer> <LayoutContainer>
<ErrorMessage <ErrorMessage
title='Ooops!' title="Ooops!"
message='An error occured while loading your services.' /> message="An error occured while loading your services."
/>
</LayoutContainer> </LayoutContainer>
); );
} }
@ -113,26 +111,23 @@ class ServiceList extends Component {
const handleRestartClick = (evt, service) => { const handleRestartClick = (evt, service) => {
this.setState({ errors: {} }); this.setState({ errors: {} });
restartServices(service.id) restartServices(service.id).catch(err => {
.catch((err) => { this.setState({ errors: { restart: err } });
this.setState({ errors: { restart: err }}); });
});
}; };
const handleStopClick = (evt, service) => { const handleStopClick = (evt, service) => {
this.setState({ errors: {} }); this.setState({ errors: {} });
stopServices(service.id) stopServices(service.id).catch(err => {
.catch((err) => { this.setState({ errors: { stop: err } });
this.setState({ errors: { stop: err }}); });
});
}; };
const handleStartClick = (evt, service) => { const handleStartClick = (evt, service) => {
this.setState({ errors: {} }); this.setState({ errors: {} });
startServices(service.id) startServices(service.id).catch(err => {
.catch((err) => { this.setState({ errors: { start: err } });
this.setState({ errors: { start: err }}); });
});
}; };
const handleScaleClick = (evt, service) => { const handleScaleClick = (evt, service) => {
@ -151,21 +146,22 @@ class ServiceList extends Component {
let renderedError = null; let renderedError = null;
if (this.state.errors.stop || this.state.errors.start || this.state.errors.restart) { if (
this.state.errors.stop ||
this.state.errors.start ||
this.state.errors.restart
) {
const message = this.state.errors.stop const message = this.state.errors.stop
? 'An error occured while attempting to stop your service.' ? 'An error occured while attempting to stop your service.'
: this.state.errors.start : this.state.errors.start
? 'An error occured while attempting to start your service.' ? 'An error occured while attempting to start your service.'
: this.state.errors.restart : this.state.errors.restart
? 'An error occured while attempting to restart your service.' ? 'An error occured while attempting to restart your service.'
: ''; : '';
renderedError = ( renderedError = (
<LayoutContainer> <LayoutContainer>
<ErrorMessage <ErrorMessage title="Ooops!" message={message} />
title='Ooops!'
message={message} />
</LayoutContainer> </LayoutContainer>
); );
} }

View File

@ -8,7 +8,7 @@ import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation'; import { Title } from '@components/navigation';
import { withNotFound } from '@containers/navigation'; import { withNotFound } from '@containers/navigation';
import { H2, FormGroup, Toggle, ToggleList, Legend } from 'joyent-ui-toolkit'; import { FormGroup, Toggle, ToggleList, Legend } from 'joyent-ui-toolkit';
const StyledLegend = Legend.extend` const StyledLegend = Legend.extend`
float: left; float: left;

View File

@ -30,23 +30,20 @@ const StyledContainer = styled.div`
`; `;
class ServicesTopology extends Component { class ServicesTopology extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
errors: {} errors: {}
} };
} }
render() { render() {
const { const {
url, url,
push, push,
deploymentGroup, deploymentGroup,
services, services,
datacenter,
loading, loading,
error, error,
servicesQuickActions, servicesQuickActions,
@ -69,8 +66,9 @@ class ServicesTopology extends Component {
return ( return (
<LayoutContainer> <LayoutContainer>
<ErrorMessage <ErrorMessage
title='Ooops!' title="Ooops!"
message='An error occured while loading your services.' /> message="An error occured while loading your services."
/>
</LayoutContainer> </LayoutContainer>
); );
} }
@ -97,26 +95,23 @@ class ServicesTopology extends Component {
const handleRestartClick = (evt, service) => { const handleRestartClick = (evt, service) => {
this.setState({ errors: {} }); this.setState({ errors: {} });
restartServices(service.id) restartServices(service.id).catch(err => {
.catch((err) => { this.setState({ errors: { restart: err } });
this.setState({ errors: { restart: err }}); });
});
}; };
const handleStopClick = (evt, service) => { const handleStopClick = (evt, service) => {
this.setState({ errors: {} }); this.setState({ errors: {} });
stopServices(service.id) stopServices(service.id).catch(err => {
.catch((err) => { this.setState({ errors: { stop: err } });
this.setState({ errors: { stop: err }}); });
});
}; };
const handleStartClick = (evt, service) => { const handleStartClick = (evt, service) => {
this.setState({ errors: {} }); this.setState({ errors: {} });
startServices(service.id) startServices(service.id).catch(err => {
.catch((err) => { this.setState({ errors: { start: err } });
this.setState({ errors: { start: err }}); });
});
}; };
const handleScaleClick = (evt, service) => { const handleScaleClick = (evt, service) => {
@ -135,28 +130,29 @@ class ServicesTopology extends Component {
let renderedError = null; let renderedError = null;
if (this.state.errors.stop || this.state.errors.start || this.state.errors.restart) { if (
this.state.errors.stop ||
this.state.errors.start ||
this.state.errors.restart
) {
const message = this.state.errors.stop const message = this.state.errors.stop
? 'An error occured while attempting to stop your service.' ? 'An error occured while attempting to stop your service.'
: this.state.errors.start : this.state.errors.start
? 'An error occured while attempting to start your service.' ? 'An error occured while attempting to start your service.'
: this.state.errors.restart : this.state.errors.restart
? 'An error occured while attempting to restart your service.' ? 'An error occured while attempting to restart your service.'
: ''; : '';
renderedError = ( renderedError = (
<LayoutContainer> <LayoutContainer>
<ErrorMessage <ErrorMessage title="Ooops!" message={message} />
title='Ooops!'
message={message} />
</LayoutContainer> </LayoutContainer>
); );
} }
return ( return (
<div> <div>
{ renderedError } {renderedError}
<StyledBackground> <StyledBackground>
<StyledContainer> <StyledContainer>
<Topology <Topology

View File

@ -5,6 +5,11 @@ query ManifestById($deploymentGroupSlug: String!) {
id id
type type
environment environment
files {
id
name
value
}
format format
raw raw
} }

View File

@ -6,6 +6,7 @@ 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 Manifest from '@containers/manifest';
import Environment from '@containers/environment';
import { import {
DeploymentGroupList, DeploymentGroupList,
@ -48,9 +49,8 @@ const serviceRedirect = p =>
.params.service}/instances`} .params.service}/instances`}
/>; />;
const App = p => ( const App = p =>
<div> <div>
<Switch> <Switch>
<Route <Route
path="/deployment-groups/:deploymentGroup/services/:service" path="/deployment-groups/:deploymentGroup/services/:service"
@ -118,6 +118,12 @@ const App = p => (
component={Manifest} component={Manifest}
/> />
<Route
path="/deployment-groups/:deploymentGroup/environment"
exact
component={Environment}
/>
<Route <Route
path="/deployment-groups/:deploymentGroup/services-list" path="/deployment-groups/:deploymentGroup/services-list"
component={ServiceList} component={ServiceList}
@ -178,8 +184,7 @@ const App = p => (
component={servicesTopologyRedirect} component={servicesTopologyRedirect}
/> />
</Switch> </Switch>
</div> </div>;
)
const Router = ( const Router = (
<BrowserRouter> <BrowserRouter>

View File

@ -13,6 +13,10 @@ const state = {
{ {
pathname: 'manifest/edit', pathname: 'manifest/edit',
name: 'Manifest' name: 'Manifest'
},
{
pathname: 'environment',
name: 'Environment'
} }
], ],
services: [ services: [

View File

@ -14,7 +14,8 @@ const GLOBAL =
}; };
const GQL_PORT = process.env.REACT_APP_GQL_PORT || 443; const GQL_PORT = process.env.REACT_APP_GQL_PORT || 443;
const GQL_HOSTNAME = process.env.REACT_APP_GQL_HOSTNAME || GLOBAL.location.hostname; const GQL_HOSTNAME =
process.env.REACT_APP_GQL_HOSTNAME || GLOBAL.location.hostname;
const GQL_PROTOCOL = process.env.REACT_APP_GQL_PROTOCOL || 'https'; const GQL_PROTOCOL = process.env.REACT_APP_GQL_PROTOCOL || 'https';
export const client = new ApolloClient({ export const client = new ApolloClient({

View File

@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"build-array": "^1.0.0", "build-array": "^1.0.0",
"camel-case": "^3.0.0", "camel-case": "^3.0.0",
"force-array": "^3.1.0",
"good": "^7.2.0", "good": "^7.2.0",
"good-console": "^6.4.0", "good-console": "^6.4.0",
"good-squeeze": "^5.0.2", "good-squeeze": "^5.0.2",
@ -25,7 +26,7 @@
"hasha": "^3.0.0", "hasha": "^3.0.0",
"joi": "^10.6.0", "joi": "^10.6.0",
"joyent-cp-gql-schema": "^1.0.4", "joyent-cp-gql-schema": "^1.0.4",
"js-yaml": "^3.8.4", "js-yaml": "^3.9.1",
"lodash.find": "^4.6.0", "lodash.find": "^4.6.0",
"lodash.findindex": "^4.6.0", "lodash.findindex": "^4.6.0",
"lodash.flatten": "^4.4.0", "lodash.flatten": "^4.4.0",
@ -47,8 +48,12 @@
"tap-xunit": "^1.7.0" "tap-xunit": "^1.7.0"
}, },
"ava": { "ava": {
"files": ["test/*.js"], "files": [
"source": ["src/*.js"], "test/*.js"
],
"source": [
"src/*.js"
],
"failFast": true "failFast": true
} }
} }

View File

@ -2,6 +2,7 @@ const { v4: uuid } = require('uuid');
const paramCase = require('param-case'); const paramCase = require('param-case');
const camelCase = require('camel-case'); const camelCase = require('camel-case');
const buildArray = require('build-array'); const buildArray = require('build-array');
const forceArray = require('force-array');
const lfind = require('lodash.find'); const lfind = require('lodash.find');
const findIndex = require('lodash.findindex'); const findIndex = require('lodash.findindex');
const flatten = require('lodash.flatten'); const flatten = require('lodash.flatten');
@ -31,6 +32,8 @@ const instances = wpData.instances
.concat(cpData.instances) .concat(cpData.instances)
.concat(complexData.instances); .concat(complexData.instances);
const INTERPOLATE_REGEX = /\$([_a-z][_a-z0-9]*)/gi;
const find = (query = {}) => item => const find = (query = {}) => item =>
Object.keys(query).every(key => item[key] === query[key]); Object.keys(query).every(key => item[key] === query[key]);
@ -185,14 +188,41 @@ const deleteDeploymentGroup = options => {
return Promise.resolve(deleteDeploymentGroup); return Promise.resolve(deleteDeploymentGroup);
}; };
const createServicesFromManifest = ({ deploymentGroupId, raw }) => { const createServicesFromManifest = ({
deploymentGroupId = '',
environment = '',
files = [],
type,
format,
raw = ''
}) => {
const _config = config({
environment,
files,
raw,
_plain: true
});
const manifest = yaml.safeLoad(raw); const manifest = yaml.safeLoad(raw);
const version = {
id: uuid(),
manifest: {
id: uuid(),
type,
format,
environment,
files,
raw
}
};
Object.keys(manifest).forEach(name => { Object.keys(manifest).forEach(name => {
const _service = { const _service = {
deploymentGroupId, deploymentGroupId,
slug: paramCase(name), slug: paramCase(name),
name name,
config: lfind(_config, ['name', name]).config
}; };
const service = Object.assign({}, _service, { const service = Object.assign({}, _service, {
@ -213,6 +243,12 @@ const createServicesFromManifest = ({ deploymentGroupId, raw }) => {
instances.push(instance); instances.push(instance);
}); });
const dgIndex = findIndex(deploymentGroups, ['id', deploymentGroupId]);
deploymentGroups[dgIndex] = Object.assign(deploymentGroups[dgIndex], {
version,
history: forceArray(deploymentGroups[dgIndex].history).concat([version])
});
return Promise.resolve(undefined); return Promise.resolve(undefined);
}; };
@ -409,6 +445,86 @@ const restartServices = options => {
return Promise.resolve(restartService); return Promise.resolve(restartService);
}; };
const parseEnvVars = (str = '') =>
str
.split(/\n/g)
.filter(line => line.match(/\=/g))
.map(line => line.split(/\=/))
.reduce(
(acc, [name, value]) =>
Object.assign(acc, {
[name.trim()]: value.trim()
}),
{}
);
const findEnvInterpolation = (str = '') =>
uniq(str.match(INTERPOLATE_REGEX).map(name => name.replace(/^\$/, '')));
const config = ({
environment = '',
files = [],
raw = '',
_plain = false
}) => {
const interpolatableNames = findEnvInterpolation(raw);
const interpolatableEnv = parseEnvVars(environment);
const interpolatedRaw = interpolatableNames.reduce(
(str = '', name) =>
str.replace(new RegExp(`\\$${name}`), interpolatableEnv[name]),
raw
);
const manifest = yaml.safeLoad(interpolatedRaw);
const services = manifest.services || manifest;
const config = Object.keys(services)
.map(name =>
Object.assign(services[name], {
name
})
)
// eslint-disable-next-line camelcase
.map(({ name, image, env_file, environment }) => ({
name,
slug: paramCase(name),
instances: [],
config: {
image,
environment: forceArray(env_file).reduce(
(env, file) =>
Object.assign(
env,
parseEnvVars(lfind(files, ['name', file]).value)
),
forceArray(environment)
.map(parseEnvVars)
.reduce(
(genv, variable) => Object.assign(genv, variable),
interpolatableEnv
)
)
}
}))
.map(service =>
Object.assign(service, {
id: hasha(JSON.stringify(service)),
config: Object.assign(service.config, {
id: hasha(JSON.stringify(service.config)),
environment: Object.keys(service.config.environment).map(name => ({
name,
id: hasha(JSON.stringify(service.config.environment[name])),
value: service.config.environment[name]
})
)
})
})
);
return _plain ? config : Promise.resolve(config);
};
module.exports = { module.exports = {
portal: getPortal, portal: getPortal,
deploymentGroups: getDeploymentGroups, deploymentGroups: getDeploymentGroups,
@ -430,5 +546,6 @@ module.exports = {
scale: (options, reguest, fn) => fn(null, scale(options)), scale: (options, reguest, fn) => fn(null, scale(options)),
restartServices: (options, request, fn) => fn(null, restartServices(options)), restartServices: (options, request, fn) => fn(null, restartServices(options)),
stopServices: (options, request, fn) => fn(null, stopServices(options)), stopServices: (options, request, fn) => fn(null, stopServices(options)),
startServices: (options, request, fn) => fn(null, startServices(options)) startServices: (options, request, fn) => fn(null, startServices(options)),
config
}; };

View File

@ -18,7 +18,7 @@
"code": "^4.1.0", "code": "^4.1.0",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-config-joyent-portal": "2.0.0", "eslint-config-joyent-portal": "2.0.0",
"js-yaml": "^3.8.4", "js-yaml": "^3.9.1",
"lab": "^14.0.1" "lab": "^14.0.1"
} }
} }

View File

@ -1,6 +1,5 @@
'use strict'; 'use strict';
const Yamljs = require('yamljs');
const ParamCase = require('param-case'); const ParamCase = require('param-case');
const Uuid = require('uuid/v4'); const Uuid = require('uuid/v4');
@ -138,8 +137,7 @@ exports.toManifest = function (clientManifest) {
id: m.id || Uuid() id: m.id || Uuid()
}); });
}) : undefined, }) : undefined,
raw: clientManifest.raw, raw: clientManifest.raw
json: clientManifest.json || Yamljs.parse(clientManifest.raw)
}); });
}; };

View File

@ -55,7 +55,6 @@
"triton": "^5.2.0", "triton": "^5.2.0",
"triton-watch": "^1.1.0", "triton-watch": "^1.1.0",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"vasync": "^1.6.4", "vasync": "^1.6.4"
"yamljs": "^0.2.10"
} }
} }

View File

@ -9,6 +9,7 @@ import { bottomShaddow, borderRadius } from '../boxes';
import paperEffect from '../paper-effect'; import paperEffect from '../paper-effect';
import typography from '../typography'; import typography from '../typography';
import Baseline from '../baseline'; import Baseline from '../baseline';
import StatusLoader from '../status-loader';
// Based on bootstrap 4 // Based on bootstrap 4
const style = css` const style = css`
@ -169,7 +170,7 @@ const StyledLink = styled(Link)`
* @example ./usage.md * @example ./usage.md
*/ */
const Button = props => { const Button = props => {
const { href = '', to = '' } = props; const { href = '', to = '', loading = false, secondary, tertiary } = props;
const Views = [ const Views = [
() => (to ? StyledLink : null), () => (to ? StyledLink : null),
@ -179,9 +180,13 @@ const Button = props => {
const View = Views.reduce((sel, view) => (sel ? sel : view()), null); const View = Views.reduce((sel, view) => (sel ? sel : view()), null);
const children = loading
? <StatusLoader secondary={!secondary} tertiary={tertiary} />
: props.children;
return ( return (
<View {...props}> <View {...props}>
{props.children} {children}
</View> </View>
); );
}; };

View File

@ -43,7 +43,13 @@ const StyledCard = Row.extend`
/** /**
* @example ./usage.md * @example ./usage.md
*/ */
const Card = ({ children, collapsed = false, headed = false, disabled = false, ...rest }) => { const Card = ({
children,
collapsed = false,
headed = false,
disabled = false,
...rest
}) => {
const render = value => { const render = value => {
const newValue = { const newValue = {
fromHeader: (value || {}).fromHeader, fromHeader: (value || {}).fromHeader,
@ -54,7 +60,13 @@ const Card = ({ children, collapsed = false, headed = false, disabled = false, .
return ( return (
<Broadcast channel="card" value={newValue}> <Broadcast channel="card" value={newValue}>
<StyledCard name="card" disabled={disabled} collapsed={collapsed} headed={headed} {...rest}> <StyledCard
name="card"
disabled={disabled}
collapsed={collapsed}
headed={headed}
{...rest}
>
{children} {children}
</StyledCard> </StyledCard>
</Broadcast> </Broadcast>

View File

@ -35,7 +35,13 @@ const Header = ({ children, ...rest }) => {
return ( return (
<Broadcast channel="card" value={newValue}> <Broadcast channel="card" value={newValue}>
<StyledCard name="card-header" disabled={disabled} collapsed headed {...rest}> <StyledCard
name="card-header"
disabled={disabled}
collapsed
headed
{...rest}
>
{children} {children}
</StyledCard> </StyledCard>
</Broadcast> </Broadcast>

View File

@ -79,7 +79,11 @@ const StyledCircle = styled.div`
`; `;
const Options = ({ children, ...rest }) => { const Options = ({ children, ...rest }) => {
const render = ({ fromHeader = false, collapsed = false, disabled = false }) => const render = ({
fromHeader = false,
collapsed = false,
disabled = false
}) =>
<StyledNav disabled={disabled} fromHeader={fromHeader} name="card-options"> <StyledNav disabled={disabled} fromHeader={fromHeader} name="card-options">
<StyledButton <StyledButton
secondary={!fromHeader} secondary={!fromHeader}

View File

@ -27,7 +27,13 @@ const StyledCol = Col.extend`
const Outlet = ({ children, ...rest }) => { const Outlet = ({ children, ...rest }) => {
const render = ({ disabled = false, collapsed = false }) => const render = ({ disabled = false, collapsed = false }) =>
<StyledCol name="card-outlet" disabled={disabled} collapsed={collapsed} xs={6} {...rest}> <StyledCol
name="card-outlet"
disabled={disabled}
collapsed={collapsed}
xs={6}
{...rest}
>
{children} {children}
</StyledCol>; </StyledCol>;

View File

@ -47,7 +47,11 @@ const StyledTitle = Title.extend`
`; `;
const Subtitle = ({ children, ...props }) => { const Subtitle = ({ children, ...props }) => {
const render = ({ disabled = false, fromHeader = false, collapsed = false }) => const render = ({
disabled = false,
fromHeader = false,
collapsed = false
}) =>
<StyledTitle <StyledTitle
name="card-subtitle" name="card-subtitle"
fromHeader={fromHeader} fromHeader={fromHeader}

View File

@ -59,7 +59,11 @@ const Title = ({ children, ...rest }) => {
</Span> </Span>
: children; : children;
const render = ({ collapsed = false, disabled = false, fromHeader = false }) => const render = ({
collapsed = false,
disabled = false,
fromHeader = false
}) =>
<Container <Container
collapsed={collapsed} collapsed={collapsed}
fromHeader={fromHeader} fromHeader={fromHeader}

View File

@ -0,0 +1,27 @@
import is from 'styled-is';
import typography from '../typography';
import P from '../text/p';
export default P.extend`
${typography.fontFamily};
display: inline-block;
margin: 0;
${is('up')`
transform: rotate(-90deg);
`};
${is('down')`
transform: rotate(90deg);
`};
${is('left')`
transform: rotate(180deg);
`};
&:before {
content: "\\003e";
}
`;

View File

@ -0,0 +1,9 @@
import styled from 'styled-components';
import { Row } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
export default styled(Row)`
background-color: ${props => props.theme.grey};
height: ${remcalc(1)};
margin: 0;
`;

View File

@ -13,7 +13,9 @@ export { default as theme } from './theme';
export { default as typography, fonts } from './typography'; export { default as typography, fonts } from './typography';
export { default as Topology } from './topology'; export { default as Topology } from './topology';
export { default as Modal, ModalHeading, ModalText } from './modal'; export { default as Modal, ModalHeading, ModalText } from './modal';
export { default as Chevron } from './chevron';
export { default as CloseButton } from './close-button'; export { default as CloseButton } from './close-button';
export { default as Divider } from './divider';
export { default as IconButton } from './icon-button'; export { default as IconButton } from './icon-button';
export { Tooltip, TooltipButton, TooltipDivider } from './tooltip'; export { Tooltip, TooltipButton, TooltipDivider } from './tooltip';
export { Dropdown } from './dropdown'; export { Dropdown } from './dropdown';

View File

@ -23,10 +23,11 @@ const StyledColor = styled.div`
width: ${unitcalc(6)}; width: ${unitcalc(6)};
height: 100%; height: 100%;
background-color: ${props => background-color: ${props =>
props.type === 'ERROR' ? props.theme.red props.type === 'ERROR'
: props.type === 'WARNING' ? props.theme.orange ? props.theme.red
: props.type === 'EDUCATION' ? props.theme.green : props.type === 'WARNING'
: props.theme.green}; ? props.theme.orange
: props.type === 'EDUCATION' ? props.theme.green : props.theme.green};
`; `;
const StyledMessageContainer = styled.div` const StyledMessageContainer = styled.div`
@ -48,29 +49,27 @@ const StyledClose = styled(CloseButton)`
top: ${unitcalc(0.5)}; top: ${unitcalc(0.5)};
`; `;
const Message = ({ const Message = ({ title, message, onCloseClick, type = 'MESSAGE' }) => {
title, const renderTitle = title
message, ? <StyledTitle>
onCloseClick, {title}
type='MESSAGE' </StyledTitle>
}) => {
const renderTitle = title
? <StyledTitle>{title}</StyledTitle>
: null; : null;
const renderClose = onCloseClick const renderClose = onCloseClick
? <StyledClose onClick={ onCloseClick } /> ? <StyledClose onClick={onCloseClick} />
: null; : null;
return ( return (
<StyledContainer> <StyledContainer>
<StyledColor type={type} /> <StyledColor type={type} />
<StyledMessageContainer> <StyledMessageContainer>
{ renderTitle } {renderTitle}
<StyledMessage>{message}</StyledMessage> <StyledMessage>
{message}
</StyledMessage>
</StyledMessageContainer> </StyledMessageContainer>
{ renderClose } {renderClose}
</StyledContainer> </StyledContainer>
); );
}; };
@ -79,12 +78,7 @@ Message.propTypes = {
title: PropTypes.string, title: PropTypes.string,
message: PropTypes.string.isRequired, message: PropTypes.string.isRequired,
onCloseClick: PropTypes.func, onCloseClick: PropTypes.func,
type: PropTypes.oneOf([ type: PropTypes.oneOf(['ERROR', 'WARNING', 'EDUCATION', 'MESSAGE'])
'ERROR',
'WARNING',
'EDUCATION',
'MESSAGE'
])
}; };
export default Message; export default Message;

View File

@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import disableScroll from 'disable-scroll'; import disableScroll from 'disable-scroll';
import remcalc from 'remcalc'; import remcalc from 'remcalc';
import theme from '../theme';
import { modalShadow } from '../boxes'; import { modalShadow } from '../boxes';
import CloseButton from '../close-button'; import CloseButton from '../close-button';
import P from '../text/p'; import P from '../text/p';

View File

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import theme from '../theme';
const StyledList = styled.ul` const StyledList = styled.ul`
display: table; display: table;
list-style-type: none; list-style-type: none;
background-color: ${theme.white};
padding: 0; padding: 0;
`; `;

View File

@ -4,6 +4,7 @@ import Baseline from '../baseline';
const StyledItem = styled.li` const StyledItem = styled.li`
float: left; float: left;
background-color: ${props => props.theme.white};
`; `;
const ProgressbarItem = ({ children, ...props }) => const ProgressbarItem = ({ children, ...props }) =>

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import styled, { keyframes } from 'styled-components'; import styled, { keyframes } from 'styled-components';
import is from 'styled-is';
const animationName = keyframes` const animationName = keyframes`
0% { 0% {
opacity: 1; opacity: 1;
stroke-width: 2; stroke-width: 2;
} }
100% { 100% {
opacity: 0.25; opacity: 0.25;
stroke-width: 0; stroke-width: 0;
@ -15,6 +17,17 @@ const animationName = keyframes`
const StyledFirstRect = styled.rect` const StyledFirstRect = styled.rect`
fill: ${props => props.theme.primary}; fill: ${props => props.theme.primary};
stroke: ${props => props.theme.primary}; stroke: ${props => props.theme.primary};
${is('secondary')`
fill: ${props => props.theme.white};
stroke: ${props => props.theme.white};
`};
${is('tertiary')`
fill: ${props => props.theme.secondary};
stroke: ${props => props.theme.secondary};
`};
animation: ${animationName} 1.5s ease-out 0s infinite; animation: ${animationName} 1.5s ease-out 0s infinite;
`; `;
@ -26,9 +39,30 @@ const StyledThirdRect = StyledFirstRect.extend`
animation-delay: 1s; animation-delay: 1s;
`; `;
export default () => export default ({ secondary, tertiary }) =>
<svg width="28" height="10"> <svg width="28" height="10">
<StyledFirstRect x="2" y="2" width="6" height="6" /> <StyledFirstRect
<StyledSecondRect x="11" y="2" width="6" height="6" /> tertiary={tertiary}
<StyledThirdRect x="20" y="2" width="6" height="6" /> secondary={secondary}
x="2"
y="2"
width="6"
height="6"
/>
<StyledSecondRect
tertiary={tertiary}
secondary={secondary}
x="11"
y="2"
width="6"
height="6"
/>
<StyledThirdRect
tertiary={tertiary}
secondary={secondary}
x="20"
y="2"
width="6"
height="6"
/>
</svg>; </svg>;

View File

@ -16,7 +16,7 @@ const GraphNode = ({
onQuickActions onQuickActions
}) => { }) => {
const { left, top, width, height } = data.nodeRect; const { left, top, width, height } = data.nodeRect;
const { connected, id, children, instancesActive, isConsul, status } = data; const { connected, id, children, instancesActive, isConsul } = data;
let x = data.x; let x = data.x;
let y = data.y; let y = data.y;
@ -75,7 +75,6 @@ const GraphNode = ({
).children ).children
: <GraphNodeContent data={data} />; : <GraphNodeContent data={data} />;
const nodeShadow = instancesActive const nodeShadow = instancesActive
? <GraphShadowRect ? <GraphShadowRect
x={0} x={0}

118
yarn.lock
View File

@ -85,7 +85,7 @@ abbrev@1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f"
accept@2.x.x: accept@^2.1.4:
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/accept/-/accept-2.1.4.tgz#887af54ceee5c7f4430461971ec400c61d09acbb" resolved "https://registry.yarnpkg.com/accept/-/accept-2.1.4.tgz#887af54ceee5c7f4430461971ec400c61d09acbb"
dependencies: dependencies:
@ -201,7 +201,7 @@ amdefine@>=0.0.4:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
ammo@2.x.x: ammo@2.x.x, ammo@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/ammo/-/ammo-2.0.4.tgz#bf80aab211698ea78f63ef5e7f113dd5d9e8917f" resolved "https://registry.yarnpkg.com/ammo/-/ammo-2.0.4.tgz#bf80aab211698ea78f63ef5e7f113dd5d9e8917f"
dependencies: dependencies:
@ -1981,7 +1981,7 @@ call-signature@0.0.2:
version "0.0.2" version "0.0.2"
resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996" resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996"
call@4.x.x: call@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/call/-/call-4.0.2.tgz#df76f5f51ee8dd48b856ac8400f7e69e6d7399c4" resolved "https://registry.yarnpkg.com/call/-/call-4.0.2.tgz#df76f5f51ee8dd48b856ac8400f7e69e6d7399c4"
dependencies: dependencies:
@ -2042,12 +2042,12 @@ caniuse-api@^1.5.2:
lodash.uniq "^4.5.0" lodash.uniq "^4.5.0"
caniuse-db@^1.0.30000187, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: caniuse-db@^1.0.30000187, caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000709" version "1.0.30000710"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000709.tgz#0b600072b7cdbbf6336a8758b71b9ad03268ede2" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000710.tgz#f03614ef04b76ba41232755b7d4e45d7cc1c13b8"
caniuse-lite@^1.0.30000669, caniuse-lite@^1.0.30000670, caniuse-lite@^1.0.30000697, caniuse-lite@^1.0.30000704: caniuse-lite@^1.0.30000669, caniuse-lite@^1.0.30000670, caniuse-lite@^1.0.30000697, caniuse-lite@^1.0.30000704:
version "1.0.30000709" version "1.0.30000710"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000709.tgz#e027c7a0dfd5ada58f931a1080fc71965375559b" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000710.tgz#1c249bf7c6a61161c9b10906e3ad9fa5b6761af1"
capture-stack-trace@^1.0.0: capture-stack-trace@^1.0.0:
version "1.0.0" version "1.0.0"
@ -2061,13 +2061,13 @@ caseless@~0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
catbox-memory@2.x.x: catbox-memory@^2.0.4:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/catbox-memory/-/catbox-memory-2.0.4.tgz#433e255902caf54233d1286429c8f4df14e822d5" resolved "https://registry.yarnpkg.com/catbox-memory/-/catbox-memory-2.0.4.tgz#433e255902caf54233d1286429c8f4df14e822d5"
dependencies: dependencies:
hoek "4.x.x" hoek "4.x.x"
catbox@7.x.x: catbox@^7.1.5:
version "7.1.5" version "7.1.5"
resolved "https://registry.yarnpkg.com/catbox/-/catbox-7.1.5.tgz#c56f7e8e9555d27c0dc038a96ef73e57d186bb1f" resolved "https://registry.yarnpkg.com/catbox/-/catbox-7.1.5.tgz#c56f7e8e9555d27c0dc038a96ef73e57d186bb1f"
dependencies: dependencies:
@ -2929,7 +2929,7 @@ cryptiles@2.x.x:
dependencies: dependencies:
boom "2.x.x" boom "2.x.x"
cryptiles@3.x.x: cryptiles@3.x.x, cryptiles@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
dependencies: dependencies:
@ -3744,8 +3744,8 @@ dtrace-provider@~0.6:
nan "^2.0.8" nan "^2.0.8"
dtrace-provider@~0.8: dtrace-provider@~0.8:
version "0.8.4" version "0.8.5"
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.4.tgz#f27c12dc0ec3105606f9833c118b8d711c8d532a" resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.5.tgz#98ebba221afac46e1c39fd36858d8f9367524b92"
dependencies: dependencies:
nan "^2.3.3" nan "^2.3.3"
@ -3789,8 +3789,8 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.16: electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.16:
version "1.3.16" version "1.3.17"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz#d0e026735754770901ae301a21664cba45d92f7d" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.17.tgz#41c13457cc7166c5c15e767ae61d86a8cacdee5d"
elliptic@^6.0.0: elliptic@^6.0.0:
version "6.4.0" version "6.4.0"
@ -3869,13 +3869,14 @@ error-ex@^1.2.0:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
es-abstract@^1.7.0: es-abstract@^1.7.0:
version "1.7.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.0.tgz#3b00385e85729932beffa9163bbea1234e932914"
dependencies: dependencies:
es-to-primitive "^1.1.1" es-to-primitive "^1.1.1"
function-bind "^1.1.0" function-bind "^1.1.0"
has "^1.0.1"
is-callable "^1.1.3" is-callable "^1.1.3"
is-regex "^1.0.3" is-regex "^1.0.4"
es-to-primitive@^1.1.1: es-to-primitive@^1.1.1:
version "1.1.1" version "1.1.1"
@ -4595,10 +4596,10 @@ fillers@^1.0.0:
resolved "https://registry.yarnpkg.com/fillers/-/fillers-1.1.1.tgz#9d1a8f0150d47f78a898de4cd43cf079d417148e" resolved "https://registry.yarnpkg.com/fillers/-/fillers-1.1.1.tgz#9d1a8f0150d47f78a898de4cd43cf079d417148e"
finalhandler@~1.0.3: finalhandler@~1.0.3:
version "1.0.3" version "1.0.4"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7"
dependencies: dependencies:
debug "2.6.7" debug "2.6.8"
encodeurl "~1.0.1" encodeurl "~1.0.1"
escape-html "~1.0.3" escape-html "~1.0.3"
on-finished "~2.3.0" on-finished "~2.3.0"
@ -5225,27 +5226,27 @@ hapi-swagger@^7.7.0:
swagger-parser "^3.4.1" swagger-parser "^3.4.1"
hapi@^16.4.3: hapi@^16.4.3:
version "16.5.0" version "16.5.2"
resolved "https://registry.yarnpkg.com/hapi/-/hapi-16.5.0.tgz#89e4770f0034e3b69ee99ed929cc5b573805f303" resolved "https://registry.yarnpkg.com/hapi/-/hapi-16.5.2.tgz#d1dadf33721c6ac3aaa905ce086d9c7ffb883092"
dependencies: dependencies:
accept "2.x.x" accept "^2.1.4"
ammo "2.x.x" ammo "^2.0.4"
boom "5.x.x" boom "^5.2.0"
call "4.x.x" call "^4.0.2"
catbox "7.x.x" catbox "^7.1.5"
catbox-memory "2.x.x" catbox-memory "^2.0.4"
cryptiles "3.x.x" cryptiles "^3.1.2"
heavy "4.x.x" heavy "^4.0.4"
hoek "4.x.x" hoek "^4.2.0"
iron "4.x.x" iron "^4.0.5"
items "2.x.x" items "^2.1.1"
joi "10.x.x" joi "^10.6.0"
mimos "3.x.x" mimos "^3.0.3"
podium "1.x.x" podium "^1.3.0"
shot "3.x.x" shot "^3.4.2"
statehood "5.x.x" statehood "^5.0.3"
subtext "5.x.x" subtext "^5.0.0"
topo "2.x.x" topo "^2.0.2"
har-schema@^1.0.5: har-schema@^1.0.5:
version "1.0.5" version "1.0.5"
@ -5342,7 +5343,7 @@ he@1.1.x:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
heavy@4.x.x: heavy@^4.0.4:
version "4.0.4" version "4.0.4"
resolved "https://registry.yarnpkg.com/heavy/-/heavy-4.0.4.tgz#36c91336c00ccfe852caa4d153086335cd2f00e9" resolved "https://registry.yarnpkg.com/heavy/-/heavy-4.0.4.tgz#36c91336c00ccfe852caa4d153086335cd2f00e9"
dependencies: dependencies:
@ -5380,7 +5381,7 @@ hoek@2.x.x:
version "2.16.3" version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoek@4.x.x, hoek@^4.0.1, hoek@^4.1.1: hoek@4.x.x, hoek@^4.0.1, hoek@^4.1.1, hoek@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
@ -5748,7 +5749,7 @@ ipaddr.js@1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
iron@4.x.x: iron@4.x.x, iron@^4.0.5:
version "4.0.5" version "4.0.5"
resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428" resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428"
dependencies: dependencies:
@ -5990,7 +5991,7 @@ is-redirect@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
is-regex@^1.0.3: is-regex@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
dependencies: dependencies:
@ -6189,7 +6190,7 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0" has-to-string-tag-x "^1.2.0"
is-object "^1.0.1" is-object "^1.0.1"
items@2.x.x: items@2.x.x, items@^2.1.1:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198" resolved "https://registry.yarnpkg.com/items/-/items-2.1.1.tgz#8bd16d9c83b19529de5aea321acaada78364a198"
@ -7484,7 +7485,7 @@ mimic-response@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
mimos@3.x.x: mimos@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/mimos/-/mimos-3.0.3.tgz#b9109072ad378c2b72f6a0101c43ddfb2b36641f" resolved "https://registry.yarnpkg.com/mimos/-/mimos-3.0.3.tgz#b9109072ad378c2b72f6a0101c43ddfb2b36641f"
dependencies: dependencies:
@ -7556,8 +7557,8 @@ moment@2.x.x, moment@^2.10.6, moment@^2.6.0:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f" resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
mooremachine@^2.0.1: mooremachine@^2.0.1:
version "2.1.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/mooremachine/-/mooremachine-2.1.0.tgz#e935cf356ca6b6a28b92fbd446d1b31a5c19848d" resolved "https://registry.yarnpkg.com/mooremachine/-/mooremachine-2.2.0.tgz#ec70bf284f5ae478afa7b359b294af67e2c97906"
dependencies: dependencies:
assert-plus ">=0.2.0 <0.3.0" assert-plus ">=0.2.0 <0.3.0"
optionalDependencies: optionalDependencies:
@ -8448,7 +8449,7 @@ pluralize@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-6.0.0.tgz#d9b51afad97d3d51075cc1ddba9b132cacccb7ba" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-6.0.0.tgz#d9b51afad97d3d51075cc1ddba9b132cacccb7ba"
podium@1.x.x: podium@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/podium/-/podium-1.3.0.tgz#3c490f54d16f10f5260cbe98641f1cb733a8851c" resolved "https://registry.yarnpkg.com/podium/-/podium-1.3.0.tgz#3c490f54d16f10f5260cbe98641f1cb733a8851c"
dependencies: dependencies:
@ -10401,7 +10402,7 @@ sherlock@1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/sherlock/-/sherlock-1.0.0.tgz#e246eacfd72c0e3b3e8243a6c9e55340d80c854e" resolved "https://registry.yarnpkg.com/sherlock/-/sherlock-1.0.0.tgz#e246eacfd72c0e3b3e8243a6c9e55340d80c854e"
shot@3.x.x: shot@^3.4.2:
version "3.4.2" version "3.4.2"
resolved "https://registry.yarnpkg.com/shot/-/shot-3.4.2.tgz#1e5c3f6f2b26649adc42f7eb350214a5a0291d67" resolved "https://registry.yarnpkg.com/shot/-/shot-3.4.2.tgz#1e5c3f6f2b26649adc42f7eb350214a5a0291d67"
dependencies: dependencies:
@ -10755,7 +10756,7 @@ state-toggle@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.0.tgz#d20f9a616bb4f0c3b98b91922d25b640aa2bc425"
statehood@5.x.x: statehood@^5.0.3:
version "5.0.3" version "5.0.3"
resolved "https://registry.yarnpkg.com/statehood/-/statehood-5.0.3.tgz#c07a75620db5379b60d2edd47f538002a8ac7dd6" resolved "https://registry.yarnpkg.com/statehood/-/statehood-5.0.3.tgz#c07a75620db5379b60d2edd47f538002a8ac7dd6"
dependencies: dependencies:
@ -10940,8 +10941,8 @@ style-search@^0.1.0:
resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902" resolved "https://registry.yarnpkg.com/style-search/-/style-search-0.1.0.tgz#7958c793e47e32e07d2b5cafe5c0bf8e12e77902"
styled-components@^2.1.1: styled-components@^2.1.1:
version "2.1.1" version "2.1.2"
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.1.1.tgz#7e9b5bc319ee3963b47aebb74f4658119ea9d484" resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-2.1.2.tgz#bb419978e1287c5d0d88fa9106b2dd75f66a324c"
dependencies: dependencies:
buffer "^5.0.3" buffer "^5.0.3"
css-to-react-native "^2.0.3" css-to-react-native "^2.0.3"
@ -11127,7 +11128,7 @@ stylis@^3.2.1:
version "3.2.8" version "3.2.8"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.2.8.tgz#9b23a3e06597f7944a3d9ae880d5796248b8784f" resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.2.8.tgz#9b23a3e06597f7944a3d9ae880d5796248b8784f"
subtext@5.x.x: subtext@^5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/subtext/-/subtext-5.0.0.tgz#9c3f083018bb1586b167ad8cfd87083f5ccdfe0f" resolved "https://registry.yarnpkg.com/subtext/-/subtext-5.0.0.tgz#9c3f083018bb1586b167ad8cfd87083f5ccdfe0f"
dependencies: dependencies:
@ -11551,7 +11552,7 @@ to-vfile@^2.1.1:
is-buffer "^1.1.4" is-buffer "^1.1.4"
vfile "^2.0.0" vfile "^2.0.0"
topo@2.x.x: topo@2.x.x, topo@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182"
dependencies: dependencies:
@ -12576,13 +12577,6 @@ yamlish@0.0.7:
version "0.0.7" version "0.0.7"
resolved "https://registry.yarnpkg.com/yamlish/-/yamlish-0.0.7.tgz#b4af9a1dcc63618873c3d6e451ec3213c39a57fb" resolved "https://registry.yarnpkg.com/yamlish/-/yamlish-0.0.7.tgz#b4af9a1dcc63618873c3d6e451ec3213c39a57fb"
yamljs@^0.2.10:
version "0.2.10"
resolved "https://registry.yarnpkg.com/yamljs/-/yamljs-0.2.10.tgz#481cc7c25ca73af59f591f0c96e3ce56c757a40f"
dependencies:
argparse "^1.0.7"
glob "^7.0.5"
yargs-parser@^4.2.0: yargs-parser@^4.2.0:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c"