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",
"joyent-manifest-editor": "^1.0.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.isstring": "^4.0.1",
"lodash.remove": "^4.7.0",
"lodash.uniq": "^4.5.0",
"normalized-styled-components": "^1.0.8",
"param-case": "^2.1.1",
"prop-types": "^15.5.10",

View File

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

View File

@ -1,11 +1,11 @@
import React, { Component } from 'react';
import { Field } from 'redux-form';
import styled from 'styled-components';
import SimpleTable from 'react-simple-table';
import { Row, Col } from 'react-styled-flexboxgrid';
import Bundle from 'react-bundle';
import remcalc from 'remcalc';
import forceArray from 'force-array';
import is from 'styled-is';
import { Loader } from '@components/messaging';
@ -19,36 +19,56 @@ import {
ProgressbarItem,
ProgressbarButton,
H3,
P,
typography,
StatusLoader
Divider,
Chevron
} 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`
margin: ${remcalc(13)} ${remcalc(19)};
margin: 0;
`;
const ServiceName = H3.extend`
margin-top: 0;
margin-bottom: 0;
margin-bottom: ${remcalc(5)};
line-height: 1.6;
font-weight: 600;
font-size: ${remcalc(18)};
`;
const ServiceCard = Card.extend`
min-height: ${remcalc(72)};
`;
const ImageTitle = ServiceName.extend`
const ImageTitle = H3.extend`
display: inline-block;
margin: 0;
`;
const Image = styled.span`
${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`
margin-top: ${remcalc(29)};
margin-bottom: ${remcalc(60)};
margin: ${remcalc(29)} 0 ${remcalc(60)} 0;
`;
const FilenameContainer = styled.span`
@ -64,17 +84,36 @@ const FilenameInput = styled(Input)`
order: 0;
flex: 1 1 auto;
align-self: stretch;
margin: 0 0 ${remcalc(13)} 0;
`;
const FilenameRemove = Button.extend`
order: 0;
flex: 0 1 auto;
align-self: auto;
margin: ${remcalc(8)};
margin-right: 0;
margin: 0 0 0 ${remcalc(8)};
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 {
constructor() {
super();
@ -112,18 +151,20 @@ class ManifestEditorBundle extends Component {
}
}
const MEditor = ({ input, defaultValue }) =>
const MEditor = ({ input, defaultValue, readOnly }) =>
<ManifestEditorBundle
mode="yaml"
{...input}
value={input.value || defaultValue}
readOnly={readOnly}
/>;
const EEditor = ({ input, defaultValue }) =>
const EEditor = ({ input, defaultValue, readOnly }) =>
<ManifestEditorBundle
mode="ini"
{...input}
value={input.value || defaultValue}
readOnly={readOnly}
/>;
export const Name = ({ handleSubmit, onCancel, dirty }) =>
@ -137,7 +178,7 @@ export const Name = ({ handleSubmit, onCancel, dirty }) =>
</Col>
</Row>
<ButtonsRow>
<Button onClick={onCancel} secondary>
<Button type="button" onClick={onCancel} secondary>
Cancel
</Button>
<Button type="submit" disabled={!dirty}>
@ -156,53 +197,74 @@ export const Manifest = ({
<form onSubmit={handleSubmit}>
<Field name="manifest" defaultValue={defaultValue} component={MEditor} />
<ButtonsRow>
<Button onClick={onCancel} secondary>
<Button type="button" onClick={onCancel} secondary>
Cancel
</Button>
<Button
disabled={!(dirty || !loading || defaultValue.length)}
loading={loading}
type="submit"
>
{loading ? <StatusLoader /> : 'Environment'}
Environment
</Button>
</ButtonsRow>
</form>;
const Filename = ({ name, onRemoveFile }) =>
<FilenameContainer>
<FilenameInput
type="text"
placeholder="Filename including extension…"
defaultValue={name}
/>
<FilenameRemove type="button" onClick={onRemoveFile} secondary>
Remove
</FilenameRemove>
</FilenameContainer>;
const File = ({ id, name, value, onRemoveFile, readOnly }) => {
const removeButton = !readOnly
? <FilenameRemove type="button" onClick={onRemoveFile} secondary>
Remove
</FilenameRemove>
: null;
export const Files = ({ loading, files, onRemoveFile }) => {
if (loading) {
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
const fileEditor = !readOnly
? <Field
name={`file-value-${id}`}
defaultValue={value}
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 (
<div>
<H3>Files:</H3>
{_files}
{files.map(({ id, ...rest }) =>
<File
key={id}
id={id}
onRemoveFile={() => onRemoveFile(id)}
readOnly={readOnly}
{...rest}
/>
)}
{footer}
</div>
);
};
@ -215,66 +277,101 @@ export const Environment = ({
dirty,
defaultValue = '',
files = [],
readOnly = false,
loading
}) =>
<form onSubmit={handleSubmit}>
<Field name="environment" defaultValue={defaultValue} component={EEditor} />
<Files files={files} onRemoveFile={onRemoveFile} loading={loading} />
<ButtonsRow>
<Button onClick={onCancel} secondary>
Cancel
</Button>
<Button type="button" onClick={onAddFile} secondary>
Add File
</Button>
<Button
disabled={!(dirty || !loading || defaultValue.length)}
type="submit"
>
{loading ? <StatusLoader /> : 'Review'}
</Button>
</ButtonsRow>
</form>;
}) => {
const envEditor = !readOnly
? <Field
name="environment"
defaultValue={defaultValue}
component={EEditor}
/>
: <EEditor input={{ value: defaultValue }} readOnly />;
const footerDivider = !readOnly ? <EnvironmentDivider /> : null;
const footer = !readOnly
? <ButtonsRow>
<Button type="button" onClick={onCancel} secondary>
Cancel
</Button>
<Button
disabled={!(dirty || !loading || defaultValue.length)}
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 = ({
handleSubmit,
onEnvironmentToggle = () => null,
onCancel,
dirty,
loading,
environmentToggles,
...state
}) => {
const serviceList = forceArray(state.services).map(({ name, config }) =>
<ServiceCard key={name}>
<ServiceName>
{name}
</ServiceName>
<Dl>
<dt>
<ServiceName>
{name}
</ServiceName>
</dt>
<dt>
<ImageTitle>Image:</ImageTitle> <Image>{config.image}</Image>
</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>
<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>
);
@ -282,11 +379,11 @@ export const Review = ({
<form onSubmit={handleSubmit}>
{serviceList}
<ButtonsRow>
<Button onClick={onCancel} disabled={loading} secondary>
<Button type="button" onClick={onCancel} disabled={loading} secondary>
Cancel
</Button>
<Button disabled={loading} type="submit">
{loading ? <StatusLoader /> : 'Confirm and Deploy'}
<Button disabled={loading} loading={loading} type="submit">
Confirm and Deploy
</Button>
</ButtonsRow>
</form>

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import { Title } from '@components/navigation';
import { ErrorMessage, Loader } from '@components/messaging';
import DeploymentGroupsQuery from '@graphql/DeploymentGroups.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';
const DGsRows = Row.extend`
@ -134,8 +134,9 @@ const DeploymentGroupList = ({
<LayoutContainer>
{_title}
<ErrorMessage
title='Ooops!'
message='An error occured while loading your deployment groups.' />
title="Ooops!"
message="An error occured while loading your deployment groups."
/>
</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 InstancesQuery from '@graphql/Instances.gql';
import { Row } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import forceArray from 'force-array';
import sortBy from 'lodash.sortby';
@ -30,8 +28,9 @@ const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
<LayoutContainer>
{_title}
<ErrorMessage
title='Ooops!'
message='An error occured while loading your instances.' />
title="Ooops!"
message="An error occured while loading your instances."
/>
</LayoutContainer>
);
}

View File

@ -6,6 +6,10 @@ import { Redirect } from 'react-router-dom';
import intercept from 'apr-intercept';
import paramCase from 'param-case';
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 DeploymentGroupBySlugQuery from '@graphql/DeploymentGroupBySlug.gql';
@ -22,13 +26,15 @@ import {
Review
} 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
// state between refreshes
class DeploymentGroupEditOrCreate extends Component {
constructor(props) {
super(props);
const { create, edit, files = [] } = props;
const { create, files = [], manifest } = props;
const type = create ? 'create' : 'edit';
const NameForm =
@ -38,63 +44,59 @@ class DeploymentGroupEditOrCreate extends Component {
destroyOnUnmount: true,
forceUnregisterOnUnmount: true,
asyncValidate: async ({ name = '' }) => {
const [err] = await intercept(client.query({
fetchPolicy: 'network-only',
query: DeploymentGroupBySlugQuery,
variables: {
slug: paramCase(name.trim())
}
}));
const [err, res] = await intercept(
client.query({
fetchPolicy: 'network-only',
query: DeploymentGroupBySlugQuery,
variables: {
slug: paramCase(name.trim())
}
})
);
if (!err) {
// eslint-disable-next-line no-throw-literal
throw { name: `"${name}" already exists!` };
if (err) {
return;
}
if (!res.data.deploymentGroups.length) {
return;
}
// eslint-disable-next-line no-throw-literal
throw { name: `"${name}" already exists!` };
}
})(Name);
const ManifestForm = reduxForm({
form: `${type}-deployment-group`,
destroyOnUnmount: true,
forceUnregisterOnUnmount: true
form: `${type}-deployment-group`
})(Manifest);
const EnvironmentForm = reduxForm({
form: `${type}-deployment-group`,
destroyOnUnmount: true,
forceUnregisterOnUnmount: true
})(Environment);
const ReviewForm = reduxForm({
form: `${type}-deployment-group`,
destroyOnUnmount: true,
forceUnregisterOnUnmount: true
form: `${type}-deployment-group`
})(Review);
if (!files.length) {
files.push({
id: uuid(),
name: '',
value: '#'
});
}
this.state = {
type,
defaultStage: create ? 'name' : 'edit',
manifestStage: create ? 'manifest' : 'edit',
name: '',
manifest: '',
environment: '',
files,
files: this.resolveManifestFiles(files, manifest),
services: [],
environmentToggles: {},
loading: false,
error: null,
NameForm,
ManifestForm,
EnvironmentForm,
ReviewForm
ReviewForm,
ManifestForm
};
this.state.EnvironmentForm = this.getEnvironmentForm(
this.state.files,
manifest
);
this.stages = {
name: create && this.renderNameForm.bind(this),
[create ? 'manifest' : 'edit']: this.renderManifestEditor.bind(this),
@ -111,10 +113,77 @@ class DeploymentGroupEditOrCreate extends Component {
this.handleCancel = this.handleCancel.bind(this);
this.handleFileAdd = this.handleFileAdd.bind(this);
this.handleRemoveFile = this.handleRemoveFile.bind(this);
this.handleEnvironmentToggle = this.handleEnvironmentToggle.bind(this);
}
if (edit) {
setTimeout(this.getDeploymentGroup, 16);
resolveManifestFiles(currentFiles = [], manifestStr = '') {
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 () => {
@ -176,9 +245,19 @@ class DeploymentGroupEditOrCreate extends Component {
}
handleManifestSubmit({ manifest = '' }) {
this.setState({ manifest: manifest || this.props.manifest }, () => {
this.redirect({ stage: 'environment', prog: true });
});
const { files } = this.state;
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) {
@ -270,23 +349,33 @@ class DeploymentGroupEditOrCreate extends Component {
const { history, create, deploymentGroup } = this.props;
history.push(create ? '/' : `/deployment-groups/${deploymentGroup.slug}`);
return false;
}
handleFileAdd() {
const { files = [] } = this.state;
this.setState({
files: this.state.files.concat([
{
id: uuid(),
name: '',
value: '#'
}
])
files: files.concat([this.getDefaultFile()])
});
}
handleRemoveFile(fileId) {
const { files = [] } = this.state;
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() {
const { EnvironmentForm } = this.state;
const { EnvironmentForm, files, loading } = this.state;
return (
<EnvironmentForm
defaultValue={this.props.environment}
files={this.state.files}
defaultValue={this.getEnvironmentDefaultValue()}
files={files}
onSubmit={this.handleEnvironmentSubmit}
onCancel={this.handleCancel}
onAddFile={this.handleFileAdd}
onRemoveFile={this.handleRemoveFile}
loading={this.state.loading}
loading={loading}
/>
);
}
renderReview() {
const { ReviewForm } = this.state;
const { ReviewForm, environmentToggles } = this.state;
return (
<ReviewForm
onSubmit={this.handleReviewSubmit}
onCancel={this.handleCancel}
onEnvironmentToggle={this.handleEnvironmentToggle}
environmentToggles={environmentToggles}
{...this.state}
/>
);
}
render() {
const { error, loading, defaultStage, manifestStage } = this.state;
const { error, defaultStage, manifestStage, manifest, name } = this.state;
if (error) {
return <ErrorMessage
title='Ooops!'
message={error} />;
return <ErrorMessage title="Ooops!" message={error} />;
}
const { match, create } = this.props;
@ -374,11 +463,11 @@ class DeploymentGroupEditOrCreate extends Component {
return this.redirect({ stage: defaultStage });
}
if (create && stage !== 'name' && !this.state.name) {
if (create && stage !== 'name' && !name) {
return this.redirect({ stage: defaultStage });
}
if (stage === 'environment' && !this.state.manifest) {
if (stage === 'environment' && !manifest) {
return this.redirect({ stage: manifestStage });
}

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import { LayoutContainer } from '@components/layout';
import { Title } from '@components/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`
float: left;

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ const GLOBAL =
};
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';
export const client = new ApolloClient({

View File

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

View File

@ -2,6 +2,7 @@ const { v4: uuid } = require('uuid');
const paramCase = require('param-case');
const camelCase = require('camel-case');
const buildArray = require('build-array');
const forceArray = require('force-array');
const lfind = require('lodash.find');
const findIndex = require('lodash.findindex');
const flatten = require('lodash.flatten');
@ -31,6 +32,8 @@ const instances = wpData.instances
.concat(cpData.instances)
.concat(complexData.instances);
const INTERPOLATE_REGEX = /\$([_a-z][_a-z0-9]*)/gi;
const find = (query = {}) => item =>
Object.keys(query).every(key => item[key] === query[key]);
@ -185,14 +188,41 @@ const deleteDeploymentGroup = options => {
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 version = {
id: uuid(),
manifest: {
id: uuid(),
type,
format,
environment,
files,
raw
}
};
Object.keys(manifest).forEach(name => {
const _service = {
deploymentGroupId,
slug: paramCase(name),
name
name,
config: lfind(_config, ['name', name]).config
};
const service = Object.assign({}, _service, {
@ -213,6 +243,12 @@ const createServicesFromManifest = ({ deploymentGroupId, raw }) => {
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);
};
@ -409,6 +445,86 @@ const restartServices = options => {
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 = {
portal: getPortal,
deploymentGroups: getDeploymentGroups,
@ -430,5 +546,6 @@ module.exports = {
scale: (options, reguest, fn) => fn(null, scale(options)),
restartServices: (options, request, fn) => fn(null, restartServices(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",
"eslint": "^3.19.0",
"eslint-config-joyent-portal": "2.0.0",
"js-yaml": "^3.8.4",
"js-yaml": "^3.9.1",
"lab": "^14.0.1"
}
}

View File

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

View File

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

View File

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

View File

@ -43,7 +43,13 @@ const StyledCard = Row.extend`
/**
* @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 newValue = {
fromHeader: (value || {}).fromHeader,
@ -54,7 +60,13 @@ const Card = ({ children, collapsed = false, headed = false, disabled = false, .
return (
<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}
</StyledCard>
</Broadcast>

View File

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

View File

@ -79,7 +79,11 @@ const StyledCircle = styled.div`
`;
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">
<StyledButton
secondary={!fromHeader}

View File

@ -27,7 +27,13 @@ const StyledCol = Col.extend`
const Outlet = ({ children, ...rest }) => {
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}
</StyledCol>;

View File

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

View File

@ -59,7 +59,11 @@ const Title = ({ children, ...rest }) => {
</Span>
: children;
const render = ({ collapsed = false, disabled = false, fromHeader = false }) =>
const render = ({
collapsed = false,
disabled = false,
fromHeader = false
}) =>
<Container
collapsed={collapsed}
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 Topology } from './topology';
export { default as Modal, ModalHeading, ModalText } from './modal';
export { default as Chevron } from './chevron';
export { default as CloseButton } from './close-button';
export { default as Divider } from './divider';
export { default as IconButton } from './icon-button';
export { Tooltip, TooltipButton, TooltipDivider } from './tooltip';
export { Dropdown } from './dropdown';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ const GraphNode = ({
onQuickActions
}) => {
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 y = data.y;
@ -75,7 +75,6 @@ const GraphNode = ({
).children
: <GraphNodeContent data={data} />;
const nodeShadow = instancesActive
? <GraphShadowRect
x={0}

118
yarn.lock
View File

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