feat(my-joy-beta): imc cns

fixes #1076
fixes #886
This commit is contained in:
Sérgio Ramos 2018-01-24 02:11:33 +00:00 committed by Sérgio Ramos
parent 396af2d2e6
commit 52d651a598
25 changed files with 7564 additions and 652 deletions

View File

@ -29,6 +29,7 @@
"lodash.find": "^4.6.0", "lodash.find": "^4.6.0",
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.includes": "^4.3.0", "lodash.includes": "^4.3.0",
"lodash.isarray": "^4.0.0",
"lodash.isfinite": "^3.3.2", "lodash.isfinite": "^3.3.2",
"lodash.isfunction": "^3.0.8", "lodash.isfunction": "^3.0.8",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",

View File

@ -0,0 +1,209 @@
import React, { Fragment } from 'react';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { Margin, Padding } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import { Field } from 'redux-form';
import {
P,
H3,
Card,
Divider,
TagList,
Input,
Toggle,
Small,
Button,
FormGroup,
FormLabel,
PublicIcon,
PrivateIcon
} from 'joyent-ui-toolkit';
import Tag from '@components/tags';
const SmallBordered = styled(Small)`
padding-right: ${remcalc(12)};
margin-right: ${remcalc(12)};
border-right: ${remcalc(1)} solid ${props => props.theme.grey};
`;
export const Header = () => (
<Fragment>
<H3>Hostnames</H3>
<Padding bottom={2}>
<P>
Default hostnames are automatically generated from both the instance
name and any attached networks.
</P>
</Padding>
</Fragment>
);
export const Footer = ({ enabled, submitting, onToggle }) => (
<Fragment>
<Margin bottom={4} top={4}>
<FormGroup name="cns-enabled">
<Flex alignCenter>
<FormLabel>Disabled CNS</FormLabel>
<Toggle checked={enabled} onChange={onToggle} disabled={submitting}>
Enabled CNS
</Toggle>
</Flex>
</FormGroup>
</Margin>
{enabled ? (
<Margin bottom={4}>
<P>*All hostnames listed here will be confirmed after deployment.</P>
</Margin>
) : null}
</Fragment>
);
export const HostnamesHeader = () => (
<Margin top={4}>
<H3>CNS service hostnames</H3>
<Padding bottom={3}>
<P>
CNS service hostnames are created by attaching a CNS service name to one
or more instances. You can serve multiple instances under the same
hostname by assigning them to a matching CNS service name.
</P>
</Padding>
</Margin>
);
export const AddServiceForm = ({
handleSubmit,
submitting,
disabled,
pristine
}) => (
<form onSubmit={handleSubmit}>
<Flex alignEnd>
<FormGroup name="name" field={Field}>
<FormLabel>Attach to new CNS service name</FormLabel>
<Input
onBlur={null}
type="text"
placeholder="Example: mySQLdb"
disabled={disabled || submitting}
/>
</FormGroup>
<Margin left={2}>
<Button
type="submit"
disabled={disabled || pristine}
loading={submitting}
>
Add
</Button>
</Margin>
</Flex>
</form>
);
export const Hostname = ({ values = [], network, service, ...hostname }) => (
<Fragment>
{values.length ? (
<Margin bottom={4}>
<Flex>
<SmallBordered bold noMargin>
{network && service
? 'Network CNS service'
: network
? 'Network'
: service ? 'CNS service' : 'Instance name'}{' '}
hostname{values.length === 1 ? '' : 's'}
</SmallBordered>
<FlexItem>
<Margin right={1}>
{hostname.public ? <PublicIcon /> : <PrivateIcon />}
</Margin>
</FlexItem>
<FlexItem>
<Small noMargin>{hostname.public ? 'Public' : 'Private'}</Small>
</FlexItem>
</Flex>
{values.map(value => (
<Input onBlur={null} disabled monospace fluid value={value} />
))}
</Margin>
) : null}
</Fragment>
);
const DefaultHostnames = ({ hostnames }) => (
<Fragment>
<Header />
<Flex column>
{hostnames.map(({ value, ...hostname }) => (
<Hostname key={value} value={value} {...hostname} />
))}
</Flex>
</Fragment>
);
const CnsHostnames = ({
hostnames = [],
services = [],
onRemoveService = () => null,
children = null
}) => (
<Fragment>
<HostnamesHeader />
{services.length ? (
<Margin bottom={3}>
<FormLabel>Existing CNS service name(s)</FormLabel>
<Margin top={1}>
<TagList>
{services.map(value => (
<Tag
active
key={value}
value={value}
onRemoveClick={
onRemoveService && (() => onRemoveService(value))
}
/>
))}
</TagList>
</Margin>
</Margin>
) : null}
{children}
<Margin top={4}>
<Flex column>
{hostnames.map(({ value, ...hostname }) => (
<Hostname key={value} value={value} {...hostname} />
))}
</Flex>
</Margin>
</Fragment>
);
export default ({
hostnames = [],
services = [],
onRemoveService,
children = null
}) => (
<Card>
<Padding all={4} bottom={0}>
<DefaultHostnames
hostnames={hostnames.filter(({ service }) => !service)}
/>
<Divider height={remcalc(1)} />
<Margin top={4}>
<CnsHostnames
services={services}
hostnames={hostnames.filter(({ service }) => service)}
onRemoveService={onRemoveService}
>
{children}
</CnsHostnames>
</Margin>
</Padding>
</Card>
);

View File

@ -1,95 +0,0 @@
import React, { Fragment } from 'react';
import styled from 'styled-components';
import remcalc from 'remcalc';
import { Margin, Padding } from 'styled-components-spacing';
import Flex, { FlexItem } from 'styled-flex-component';
import { Field } from 'redux-form';
import {
P,
H3,
Input,
Small,
Button,
FormGroup,
FormLabel,
PublicIcon,
PrivateIcon
} from 'joyent-ui-toolkit';
const SmallBordered = styled(Small)`
padding-right: ${remcalc(12)};
margin-right: ${remcalc(12)};
border-right: ${remcalc(1)} solid ${props => props.theme.grey};
`;
export const Header = () => (
<Fragment>
<H3>Hostnames</H3>
<Padding bottom={2}>
<P>
Default hostnames are automatically generated from both the instance
name and any attached networks.
</P>
</Padding>
</Fragment>
);
export const HostnamesHeader = () => (
<Margin top={4}>
<H3>CNS service hostnames</H3>
<Padding bottom={3}>
<P>
CNS service hostnames are created by attaching a CNS service name to one
or more instances. You can serve multiple instances under the same
hostname by assigning them to a matching CNS service name.
</P>
</Padding>
</Margin>
);
export const AddServiceForm = ({ handleSubmit, pristine }) => (
<form onSubmit={handleSubmit}>
<Flex alignEnd>
<FormGroup name="name" field={Field}>
<FormLabel>Attach to new CNS service name</FormLabel>
<Input onBlur={null} type="text" placeholder="Example: mySQLdb" />
</FormGroup>
<Margin left={2}>
<Button type="submit" disabled={pristine}>
Add
</Button>
</Margin>
</Flex>
</form>
);
export const Hostname = ({ values = [], network, service, ...hostname }) => (
<Fragment>
{values.length ? (
<Margin bottom={4}>
<Flex>
<SmallBordered bold noMargin>
{network && service
? 'Network CNS service'
: network
? 'Network'
: service ? 'CNS service' : 'Instance name'}{' '}
hostname{values.length === 1 ? '' : 's'}
</SmallBordered>
<FlexItem>
<Margin right={1}>
{hostname.public ? <PublicIcon /> : <PrivateIcon />}
</Margin>
</FlexItem>
<FlexItem>
<Small noMargin>{hostname.public ? 'Public' : 'Private'}</Small>
</FlexItem>
</Flex>
{values.map(value => (
<Input onBlur={null} disabled monospace fluid value={value} />
))}
</Margin>
) : null}
</Fragment>
);

View File

@ -163,13 +163,13 @@ export const KeyValue = ({
{initialValues.name ? ( {initialValues.name ? (
<Fragment> <Fragment>
{expanded ? ( {expanded ? (
<span>{`${initialValues.name}${type === 'metadata' <span>{`${initialValues.name}${
? '-' type === 'metadata' ? '-' : ':'
: ':'}`}</span> }`}</span>
) : ( ) : (
<b>{`${initialValues.name}${type === 'metadata' <b>{`${initialValues.name}${
? '-' type === 'metadata' ? '-' : ':'
: ':'}`}</b> }`}</b>
)} )}
<span>{initialValues.value}</span> <span>{initialValues.value}</span>
</Fragment> </Fragment>

View File

@ -398,6 +398,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/tags-metadata/metadata" href="https://docs.joyent.com/public-cloud/tags-metadata/metadata"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read the docs Read the docs
@ -1930,6 +1931,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/tags-metadata/metadata" href="https://docs.joyent.com/public-cloud/tags-metadata/metadata"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read the docs Read the docs
@ -4756,6 +4758,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/tags-metadata/metadata" href="https://docs.joyent.com/public-cloud/tags-metadata/metadata"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read the docs Read the docs

View File

@ -547,6 +547,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/network/sdn" href="https://docs.joyent.com/public-cloud/network/sdn"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read more Read more
@ -963,334 +964,7 @@ Array [
/> />
</div> </div>
</div>, </div>,
.c7 { <form />,
color: rgba(73,73,73,1);
line-height: 1.5rem;
font-size: 0.9375rem;
margin: 0;
}
.c7 + p,
.c7 + small,
.c7 + h1,
.c7 + h2,
.c7 + label,
.c7 + h3,
.c7 + h4,
.c7 + h5,
.c7 + div,
.c7 + span {
padding-bottom: 2.25rem;
}
.c0 {
box-sizing: border-box;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex: 0 1 auto;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
margin-right: -0.5rem;
margin-left: -0.5rem;
}
.c1 {
box-sizing: border-box;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
padding-right: 0.5rem;
padding-left: 0.5rem;
}
.c8 {
background-color: rgb(216,216,216);
margin: 0;
height: 0.0625rem;
}
.c5 {
-webkit-order: 0;
-ms-flex-order: 0;
order: 0;
-webkit-flex-basis: auto;
-ms-flex-preferred-size: auto;
flex-basis: auto;
-webkit-box-flex: 0;
-webkit-flex-grow: 0;
-ms-flex-positive: 0;
flex-grow: 0;
-webkit-flex-shrink: 1;
-ms-flex-negative: 1;
flex-shrink: 1;
}
.c12 {
margin-right: 0.25rem;
}
.c2 {
display: inline-block;
margin-top: 1rem;
margin-right: 1rem;
}
.c6 {
margin-top: 0.5rem;
margin-right: 1rem;
margin-bottom: 0.5rem;
margin-left: 1rem;
}
.c10 {
margin-right: 2rem;
}
.c9 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c11 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c3 {
display: inline-block;
background-color: rgb(255,255,255);
border: 0.0625rem solid rgb(216,216,216);
border-radius: 0.25rem;
min-width: 18.75rem;
}
@media only screen and (min-width:0em) {
.c1 {
-webkit-flex-basis: 100%;
-ms-flex-preferred-size: 100%;
flex-basis: 100%;
max-width: 100%;
display: block;
}
}
@media only screen and (min-width:48em) {
.c1 {
-webkit-flex-basis: 66.66666666666667%;
-ms-flex-preferred-size: 66.66666666666667%;
flex-basis: 66.66666666666667%;
max-width: 66.66666666666667%;
display: block;
}
}
<form>
<div
className="c0"
>
<div
className="c1"
>
<div
className="c2"
>
<div
className="c3"
>
<div
className="c4"
>
<div
className="c5"
>
<div
className="c6"
>
<p
className="c7"
>
name2
</p>
</div>
</div>
<div
className="c5"
>
<div
className="c8 c0"
height="0.0625rem"
/>
</div>
<div
className="c5"
>
<div
className="c6"
>
<div
className="c9"
>
<div
className="c10"
>
<div
className="c5"
>
<div
className="c11"
>
<div
className="c5"
>
<div
className="c12"
>
<svg
className=""
height="16"
innerRef={undefined}
style={
Object {
"transform": "rotate(0deg)",
}
}
viewBox="0 0 12 16"
width="12"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10,6V4A4,4,0,0,0,2,4V6H2A2,2,0,0,0,0,8v6a2,2,0,0,0,2,2h8a2,2,0,0,0,2-2V8A2,2,0,0,0,10,6ZM4,4c0-1.65.35-2,2-2s2,.35,2,2V6H4Zm6,9a1,1,0,0,1-1,1H3a1,1,0,0,1-1-1V9A1,1,0,0,1,3,8H9a1,1,0,0,1,1,1ZM6,13H6a1,1,0,0,1-1-1V10A1,1,0,0,1,6,9H6a1,1,0,0,1,1,1v2A1,1,0,0,1,6,13Z"
fill="rgba(73, 73, 73, 1)"
/>
</svg>
</div>
</div>
<div
className="c5"
>
<p
className="c7"
>
Private
</p>
</div>
</div>
</div>
</div>
<div
className=""
>
<div
className="c5"
>
<div
className="c11"
>
<div
className="c5"
>
<div
className="c12"
>
<svg
height="13"
viewBox="0 0 9 13"
width="9"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0,0V13H9V0ZM7,11H2V2H7ZM3,4H6V3H3ZM3,6H6V5H3ZM3,8H6V7H3Z"
fill="rgba(73, 73, 73, 1)"
/>
</svg>
</div>
</div>
<div
className="c5"
>
<p
className="c7"
>
Data center network
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>,
.c0 { .c0 {
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@ -1535,6 +1209,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/network/sdn" href="https://docs.joyent.com/public-cloud/network/sdn"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read more Read more
@ -3409,6 +3084,10 @@ Array [
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.c10 {
margin-right: 2rem;
}
.c2 { .c2 {
display: inline-block; display: inline-block;
margin-top: 1rem; margin-top: 1rem;
@ -3422,10 +3101,6 @@ Array [
margin-left: 1rem; margin-left: 1rem;
} }
.c10 {
margin-right: 2rem;
}
.c9 { .c9 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
@ -3446,29 +3121,6 @@ Array [
align-content: stretch; align-content: stretch;
} }
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c11 { .c11 {
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
@ -3493,6 +3145,29 @@ Array [
align-items: center; align-items: center;
} }
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c3 { .c3 {
display: inline-block; display: inline-block;
background-color: rgb(255,255,255); background-color: rgb(255,255,255);

View File

@ -384,6 +384,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/tags-metadata/tags" href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read the docs Read the docs
@ -1889,6 +1890,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/tags-metadata/tags" href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read the docs Read the docs
@ -2525,6 +2527,7 @@ Array [
<a <a
href="https://docs.joyent.com/public-cloud/tags-metadata/tags" href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
target="__blank" target="__blank"
> >
Read the docs Read the docs

View File

@ -60,6 +60,7 @@ export const Affinity = ({
<a <a
target="__blank" target="__blank"
href="https://docs.joyent.com/public-cloud/instances/docker/how/start-containers#controlling-container-placement" href="https://docs.joyent.com/public-cloud/instances/docker/how/start-containers#controlling-container-placement"
rel="noopener noreferrer"
> >
Read the docs Read the docs
</a> </a>

View File

@ -4,37 +4,17 @@ import ReduxForm from 'declarative-redux-form';
import { destroy } from 'redux-form'; import { destroy } from 'redux-form';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import get from 'lodash.get'; import get from 'lodash.get';
import { Margin, Padding } from 'styled-components-spacing'; import { Margin } from 'styled-components-spacing';
import Flex from 'styled-flex-component';
import { set } from 'react-redux-values'; import { set } from 'react-redux-values';
import punycode from 'punycode'; import punycode from 'punycode';
import remcalc from 'remcalc';
import { import { CnsIcon, H3, Button, FormLabel, TagList } from 'joyent-ui-toolkit';
CnsIcon,
P,
Card,
H3,
Button,
FormGroup,
FormLabel,
Toggle,
Divider,
TagList,
StatusLoader
} from 'joyent-ui-toolkit';
import {
Hostname,
Header,
AddServiceForm,
HostnamesHeader
} from '@components/create-instance/cns';
import Cns, { Footer, AddServiceForm } from '@components/cns';
import Tag from '@components/tags'; import Tag from '@components/tags';
import Title from '@components/create-instance/title'; import Title from '@components/create-instance/title';
import Description from '@components/description'; import Description from '@components/description';
import getAccount from '@graphql/get-account.gql'; import GetAccount from '@graphql/get-account.gql';
const CNS_FORM = 'create-instance-cns'; const CNS_FORM = 'create-instance-cns';
@ -50,8 +30,7 @@ const CNSContainer = ({
handleEdit, handleEdit,
handleToggleCnsEnabled, handleToggleCnsEnabled,
handleAddService, handleAddService,
handleRemoveService, handleRemoveService
loading
}) => ( }) => (
<Fragment> <Fragment>
<Title onClick={!expanded && !proceeded && handleEdit} icon={<CnsIcon />}> <Title onClick={!expanded && !proceeded && handleEdit} icon={<CnsIcon />}>
@ -73,81 +52,24 @@ const CNSContainer = ({
) : null} ) : null}
<div> <div>
{expanded && cnsEnabled ? ( {expanded && cnsEnabled ? (
<Card> <Cns
<Padding all={4} bottom={0}> hostnames={hostnames}
<Header /> services={serviceNames}
{loading ? ( onRemoveService={handleRemoveService}
<Margin all={2}> >
{' '} <ReduxForm
<StatusLoader /> form={`${CNS_FORM}-new-service`}
</Margin> destroyOnUnmount={false}
) : ( forceUnregisterOnUnmount={true}
<Flex column> onSubmit={handleAddService}
{hostnames >
.filter(({ service }) => !service) {props => <AddServiceForm {...props} />}
.map(({ value, ...hostname }) => ( </ReduxForm>
<Hostname key={value} value={value} {...hostname} /> </Cns>
))}
</Flex>
)}
<Divider height={remcalc(1)} />
<Margin top={4}>
<HostnamesHeader />
{serviceNames.length ? (
<Margin bottom={3}>
<FormLabel>Existing CNS service name(s)</FormLabel>
<Margin top={1}>
<TagList>
{serviceNames.map((value, index) => (
<Tag
active
key={index}
value={value}
onRemoveClick={() => handleRemoveService(index)}
/>
))}
</TagList>
</Margin>
</Margin>
) : null}
<ReduxForm
form={`${CNS_FORM}-new-service`}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleAddService}
>
{props => <AddServiceForm {...props} />}
</ReduxForm>
<Margin top={4}>
<Flex column>
{hostnames
.filter(({ service }) => service)
.map(({ value, ...hostname }) => (
<Hostname key={value} value={value} {...hostname} />
))}
</Flex>
</Margin>
</Margin>
</Padding>
</Card>
) : null} ) : null}
{expanded ? ( {expanded ? (
<Fragment> <Fragment>
<Margin bottom={4} top={4}> <Footer enabled={cnsEnabled} onToggle={handleToggleCnsEnabled} />
<FormGroup name="cns-enabled">
<Flex alignCenter>
<FormLabel>Disabled CNS</FormLabel>
<Toggle checked={cnsEnabled} onChange={handleToggleCnsEnabled}>
Enabled CNS
</Toggle>
</Flex>
</FormGroup>
</Margin>
<Margin bottom={4}>
<P>
*All hostnames listed here will be confirmed after deployment.
</P>
</Margin>
<Margin bottom={4}> <Margin bottom={4}>
<Button type="button" onClick={handleNext}> <Button type="button" onClick={handleNext}>
Next Next
@ -184,9 +106,8 @@ const CNSContainer = ({
); );
export default compose( export default compose(
graphql(getAccount, { graphql(GetAccount, {
props: ({ data: { loading, account: { id } = [] } }) => ({ props: ({ data: { account: { id = '<account-id>' } = [] } }) => ({
loading,
id id
}) })
}), }),
@ -252,7 +173,7 @@ export default compose(
handleAddService: ({ name }) => { handleAddService: ({ name }) => {
const serviceName = punycode const serviceName = punycode
.encode(name.toLowerCase().replace(/\s/g, '-')) .encode(name.toLowerCase().replace(/\s/g, '-'))
.replace(/\-$/, ''); .replace(/-$/, '');
dispatch([ dispatch([
destroy(`${CNS_FORM}-new-service`), destroy(`${CNS_FORM}-new-service`),
@ -262,11 +183,12 @@ export default compose(
}) })
]); ]);
}, },
handleRemoveService: index => { handleRemoveService: value => {
serviceNames.splice(index, 1);
return dispatch( return dispatch(
set({ name: `${CNS_FORM}-services`, value: serviceNames.slice() }) set({
name: `${CNS_FORM}-services`,
value: serviceNames.filter(name => name !== value)
})
); );
} }
})) }))

View File

@ -43,6 +43,7 @@ const Firewall = ({
<a <a
target="__blank" target="__blank"
href="https://docs.joyent.com/public-cloud/network/firewall" href="https://docs.joyent.com/public-cloud/network/firewall"
rel="noopener noreferrer"
> >
Read more Read more
</a> </a>

View File

@ -45,6 +45,7 @@ export const Metadata = ({
<a <a
target="__blank" target="__blank"
href="https://docs.joyent.com/public-cloud/tags-metadata/metadata" href="https://docs.joyent.com/public-cloud/tags-metadata/metadata"
rel="noopener noreferrer"
> >
Read the docs Read the docs
</a> </a>

View File

@ -44,6 +44,7 @@ export const Networks = ({
<a <a
target="__blank" target="__blank"
href="https://docs.joyent.com/public-cloud/network/sdn" href="https://docs.joyent.com/public-cloud/network/sdn"
rel="noopener noreferrer"
> >
Read more Read more
</a> </a>
@ -65,7 +66,7 @@ export const Networks = ({
<form> <form>
{networks.map( {networks.map(
({ id, selected, infoExpanded, machinesExpanded, ...network }) => ({ id, selected, infoExpanded, machinesExpanded, ...network }) =>
!expanded && !selected ? null : ( expanded || (selected && proceeded) ? (
<Network <Network
key={id} key={id}
id={id} id={id}
@ -79,7 +80,7 @@ export const Networks = ({
} }
{...network} {...network}
/> />
) ) : null
)} )}
</form> </form>
)} )}

View File

@ -42,6 +42,7 @@ export const Tags = ({
<a <a
target="__blank" target="__blank"
href="https://docs.joyent.com/public-cloud/tags-metadata/tags" href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
> >
Read the docs Read the docs
</a> </a>

View File

@ -0,0 +1,205 @@
import React from 'react';
import renderer from 'react-test-renderer';
import 'jest-styled-components';
import { Cns } from '../cns';
import Theme from '@mocks/theme';
// services = [],
// hostnames = [],
// disabled = false,
// loading = false,
// mutationError = false,
// loadingError = null
it('renders <Cns /> without throwing', () => {
expect(
renderer
.create(
<Theme>
<Cns />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns loading /> without throwing', () => {
expect(
renderer
.create(
<Theme>
<Cns loading />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns loadingError /> without throwing', () => {
expect(
renderer
.create(
<Theme>
<Cns loadingError />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns mutationError /> without throwing', () => {
expect(
renderer
.create(
<Theme>
<Cns mutationError="mutation error" />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns mutating /> without throwing', () => {
const services = ['serbice', 'dssasda', 'dsasd'];
const hostnames = [
{
values: ['stuffy-stuff'],
public: true
},
{
values: ['stuffy-stuff']
},
{
values: ['serbice', 'dssasda', 'dsasd'],
public: true,
service: true
},
{
values: ['serbice', 'dssasda', 'dsasd'],
service: true
}
];
expect(
renderer
.create(
<Theme>
<Cns mutating services={services} hostnames={hostnames} />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns disabled /> without throwing', () => {
const services = ['serbice', 'dssasda', 'dsasd'];
const hostnames = [
{
values: ['stuffy-stuff'],
public: true
},
{
values: ['stuffy-stuff']
},
{
values: ['serbice', 'dssasda', 'dsasd'],
public: true,
service: true
},
{
values: ['serbice', 'dssasda', 'dsasd'],
service: true
}
];
expect(
renderer
.create(
<Theme>
<Cns disabled services={services} hostnames={hostnames} />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns services /> without throwing', () => {
const services = ['serbice', 'dssasda', 'dsasd'];
expect(
renderer
.create(
<Theme>
<Cns services={services} />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns hostnames /> without throwing', () => {
const hostnames = [
{
values: ['stuffy-stuff'],
public: true
},
{
values: ['stuffy-stuff']
},
{
values: ['serbice', 'dssasda', 'dsasd'],
public: true,
service: true
},
{
values: ['serbice', 'dssasda', 'dsasd'],
service: true
}
];
expect(
renderer
.create(
<Theme>
<Cns hostnames={hostnames} />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});
it('renders <Cns services hostnames /> without throwing', () => {
const services = ['serbice', 'dssasda', 'dsasd'];
const hostnames = [
{
values: ['stuffy-stuff'],
public: true
},
{
values: ['stuffy-stuff']
},
{
values: ['serbice', 'dssasda', 'dsasd'],
public: true,
service: true
},
{
values: ['serbice', 'dssasda', 'dsasd'],
service: true
}
];
expect(
renderer
.create(
<Theme>
<Cns disabled services={services} hostnames={hostnames} />
</Theme>
)
.toJSON()
).toMatchSnapshot();
});

View File

@ -60,7 +60,7 @@ it('renders <Networks networks /> without throwing', () => {
fabric: false, fabric: false,
subnet: '255.255.255.0', subnet: '255.255.255.0',
provision_start_ip: '192.168.1.2', provision_start_ip: '192.168.1.2',
provision_end_ip: '192.168.1.253', provision_end_ip: '192.168.1.253'
} }
]; ];

View File

@ -0,0 +1,350 @@
import React, { PureComponent } from 'react';
import intercept from 'apr-intercept';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import { SubmissionError, destroy } from 'redux-form';
import ReduxForm from 'declarative-redux-form';
import { set, destroy as destroyValue } from 'react-redux-values';
import { Margin } from 'styled-components-spacing';
import find from 'lodash.find';
import isBoolean from 'lodash.isboolean';
import isArray from 'lodash.isarray';
import get from 'lodash.get';
import {
ViewContainer,
StatusLoader,
Message,
MessageTitle,
MessageDescription
} from 'joyent-ui-toolkit';
import Description from '@components/description';
import Cns, { Footer, AddServiceForm } from '@components/cns';
import GetAccount from '@graphql/get-account.gql';
import UpdateTags from '@graphql/update-tags.gql';
import GetTags from '@graphql/list-tags.gql';
import parseError from '@state/parse-error';
const CNS_FORM = 'cns-new-service';
const CnsContainer = ({
services = [],
hostnames = [],
disabled = false,
handleToggleCnsEnabled,
handleAddService,
handleRemoveService,
mutating = false,
loading = false,
mutationError = false,
loadingError = null
}) => (
<ViewContainer main>
<Margin bottom={1}>
<Description>
Triton CNS is used to automatically update hostnames for your
instances*. You can serve multiple instances (with multiple IP
addresses) under the same hostname by matching the CNS service names.{' '}
<a
href="https://docs.joyent.com/private-cloud/install/cns"
target="_blank"
rel="noopener noreferrer"
>
Read the docs
</a>
</Description>
</Margin>
{loading ? <StatusLoader /> : null}
{!loading && loadingError ? (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your CNS services
</MessageDescription>
</Message>
) : null}
{!loading && mutationError ? (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>{mutationError}</MessageDescription>
</Message>
) : null}
{!loading && !disabled ? (
<Cns
services={services}
hostnames={hostnames}
onRemoveService={
!mutating && (name => handleRemoveService(name, services))
}
>
<ReduxForm
form={CNS_FORM}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={val => handleAddService(val, services)}
>
{props => <AddServiceForm {...props} disabled={mutating} />}
</ReduxForm>
</Cns>
) : null}
{!loading && !loadingError ? (
<Footer
enabled={!disabled}
submitting={mutating}
onToggle={() => handleToggleCnsEnabled(!disabled)}
/>
) : null}
</ViewContainer>
);
export { CnsContainer as Cns };
class CnsClass extends PureComponent {
componentWillMount() {
const { reset = () => null } = this.props;
reset();
}
render() {
const { reset, children, ...rest } = this.props;
return <CnsContainer {...rest}>{children}</CnsContainer>;
}
}
export default compose(
graphql(UpdateTags, { name: 'updateTags' }),
graphql(GetAccount, {
props: ({ data }) => {
const { account = {} } = data;
const { id = '<account-id>' } = account;
return { id };
}
}),
graphql(GetTags, {
options: ({ match }) => ({
variables: {
fetchPolicy: 'network-only',
name: get(match, 'params.instance')
}
}),
props: ({ data }) => {
const { loading, error, variables, refetch, ...rest } = data;
const { name } = variables;
const instance = find(get(rest, 'machines', []), ['name', name]);
const tags = get(instance, 'tags', []);
return {
tags,
instance,
loading,
loadingError: error,
refetch
};
}
}),
connect(
({ values, form }, { id, instance = {}, tags = [] }) => {
const { name = '<instance-name>' } = instance;
const cnsDisable = find(tags, ['name', 'triton.cns.disable']) || {};
const cnsServices = find(tags, ['name', 'triton.cns.services']) || {};
let disabled = JSON.parse(cnsDisable.value || 'false');
let services = (cnsServices.value || '').split(/,/gi).filter(Boolean);
const adding = get(form, `${CNS_FORM}.submitting`, false);
const toggling = get(values, `cns-${instance.id}-toggling`, false);
const removing = get(values, `cns-${instance.id}-removing`, false);
const enabled = get(values, `cns-${instance.id}-enabled`, undefined);
const togglingError = get(
values,
`cns-${instance.id}-toggling-error`,
null
);
const removingError = get(
values,
`cns-${instance.id}-removing-error`,
null
);
const svcs = get(values, `cns-${instance.id}-svcs`, undefined);
if (isBoolean(enabled)) {
disabled = !enabled;
}
if (isArray(svcs)) {
services = svcs;
}
// REPLACE WITH DATA CENTER
const dataCenter = 'us-east-1';
const defaultHostnames = [
{
values: [`${name}.inst.${id}.${dataCenter}.triton.zone`],
public: true
},
{
values: [`${name}.inst.${id}.${dataCenter}.cns.joyent.com`]
},
{
values: [],
public: true,
service: true
},
{
values: [],
service: true
}
];
const hostnames = defaultHostnames.map(hostname => {
if (!hostname.service) {
return hostname;
}
return {
...hostname,
values: services.map(name => {
const postfix = hostname.public
? '.triton.zone'
: '.cns.joyent.com';
return `${name}.svc.${id}.${dataCenter}${postfix}`;
})
};
});
return {
hostnames,
disabled,
services,
mutating: toggling || removing || adding,
mutationError: togglingError || removingError
};
},
(dispatch, { instance = {}, refetch, updateTags }) => ({
reset: () => {
dispatch([
destroyValue({ name: `cns-${instance.id}-removing` }),
destroyValue({ name: `cns-${instance.id}-svcs` }),
destroyValue({ name: `cns-${instance.id}-removing-error` }),
destroyValue({ name: `cns-${instance.id}-toggling` }),
destroyValue({ name: `cns-${instance.id}-enabled` }),
destroyValue({ name: `cns-${instance.id}-toggling-error` })
]);
return refetch();
},
handleRemoveService: async (name, services) => {
const value = services.filter(svc => name !== svc);
dispatch([
set({ name: `cns-${instance.id}-removing`, value: true }),
set({ name: `cns-${instance.id}-svcs`, value })
]);
const [err] = await intercept(
updateTags({
variables: {
id: instance.id,
tags: [
{
name: 'triton.cns.services',
value: value.join(',')
}
]
}
})
);
const setLoadingFalse = set({
name: `cns-${instance.id}-removing`,
value: false
});
if (err) {
return dispatch([
setLoadingFalse,
set({
name: `cns-${instance.id}-removing-error`,
value: parseError(err)
}),
set({ name: `cns-${instance.id}-svcs`, value: null })
]);
}
return dispatch(setLoadingFalse);
},
handleToggleCnsEnabled: async disabled => {
dispatch([
set({ name: `cns-${instance.id}-toggling`, value: true }),
set({ name: `cns-${instance.id}-enabled`, value: !disabled })
]);
const [err] = await intercept(
updateTags({
variables: {
id: instance.id,
tags: [
{
name: 'triton.cns.disable',
value: disabled ? 'true' : 'false'
}
]
}
})
);
const setLoadingFalse = set({
name: `cns-${instance.id}-toggling`,
value: false
});
if (err) {
return dispatch([
setLoadingFalse,
set({
name: `cns-${instance.id}-toggling-error`,
value: parseError(err)
}),
set({ name: `cns-${instance.id}-enabled`, value: null })
]);
}
return dispatch(setLoadingFalse);
},
handleAddService: async ({ name }, services) => {
const value = services.concat(name);
dispatch(set({ name: `cns-${instance.id}-svcs`, value }));
const [err] = await intercept(
updateTags({
variables: {
id: instance.id,
tags: [
{
name: 'triton.cns.services',
value: value.join(',')
}
]
}
})
);
if (err) {
dispatch(set({ name: `cns-${instance.id}-svcs`, services }));
throw new SubmissionError({
_error: parseError(err)
});
}
return dispatch(destroy(CNS_FORM));
}
})
)
)(CnsClass);

View File

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import find from 'lodash.find';
import get from 'lodash.get';
import {
ViewContainer,
StatusLoader,
Message,
MessageDescription,
MessageTitle
} from 'joyent-ui-toolkit';
import ListDNS from '@graphql/list-dns.gql';
const DNS = ({ instance, loading, error }) => {
// eslint-disable-next-line camelcase
const { name, dns_names } = instance || {};
// eslint-disable-next-line camelcase
const _loading = loading && !name && !dns_names && <StatusLoader />;
const _summary = !_loading &&
instance && <pre>{JSON.stringify(dns_names, null, 2)}</pre>;
const _error = error &&
!_loading &&
!instance && (
<Message error>
<MessageTitle>Ooops!</MessageTitle>
<MessageDescription>
An error occurred while loading your instance DNS
</MessageDescription>
</Message>
);
return (
<ViewContainer center={Boolean(_loading)} main>
{_loading}
{_error}
{_summary}
</ViewContainer>
);
};
DNS.propTypes = {
loading: PropTypes.bool
};
export default compose(
graphql(ListDNS, {
options: ({ match }) => ({
variables: {
name: get(match, 'params.instance')
}
}),
props: ({ data: { loading, error, variables, ...rest } }) => ({
instance: find(get(rest, 'machines', []), ['name', variables.name]),
loading,
error
})
})
)(DNS);

View File

@ -4,6 +4,6 @@ export { default as Tags } from './tags';
export { default as Metadata } from './metadata'; export { default as Metadata } from './metadata';
export { default as Networks } from './networks'; export { default as Networks } from './networks';
export { default as Firewall } from './firewall'; export { default as Firewall } from './firewall';
export { default as Dns } from './dns'; export { default as Cns } from './cns';
export { default as Snapshots } from './snapshots'; export { default as Snapshots } from './snapshots';
export { default as Resize } from './resize'; export { default as Resize } from './resize';

View File

@ -1,7 +1,4 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import paramCase from 'param-case';
import titleCase from 'title-case';
import isString from 'lodash.isstring';
import get from 'lodash.get'; import get from 'lodash.get';
import { Menu } from '@components/navigation'; import { Menu } from '@components/navigation';
@ -11,20 +8,10 @@ export default connect((state, { match }) => {
const allSections = get(state, 'ui.sections'); const allSections = get(state, 'ui.sections');
const sections = instanceSlug ? allSections.instances : []; const sections = instanceSlug ? allSections.instances : [];
const links = sections const links = sections.map(({ name, pathname }) => ({
.map( name,
section => pathname: `/instances/${instanceSlug}/${pathname}`
!isString(section) }));
? section
: {
pathname: paramCase(section),
name: titleCase(section)
}
)
.map(({ name, pathname }) => ({
name,
pathname: `/instances/${instanceSlug}/${pathname}`
}));
return { return {
links links

View File

@ -15,7 +15,7 @@ import {
Metadata as InstanceMetadata, Metadata as InstanceMetadata,
Networks as InstanceNetworks, Networks as InstanceNetworks,
Firewall as InstanceFirewall, Firewall as InstanceFirewall,
Dns as InstanceDns, Cns as InstanceCns,
Snapshots as InstanceSnapshots, Snapshots as InstanceSnapshots,
Resize as InstanceResize Resize as InstanceResize
} from '@containers/instances'; } from '@containers/instances';
@ -77,12 +77,16 @@ export default () => (
exact exact
component={InstanceFirewall} component={InstanceFirewall}
/> />
<Route path="/instances/:instance/dns" exact component={InstanceDns} />
<Route <Route
path="/instances/:instance/snapshots" path="/instances/:instance/snapshots"
exact exact
component={InstanceSnapshots} component={InstanceSnapshots}
/> />
<Route
path="/instances/:instance/cns-dns"
exact
component={InstanceCns}
/>
<Route <Route
path="/instances/:instance" path="/instances/:instance"
exact exact

View File

@ -5,6 +5,7 @@ import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http'; import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory'; import { InMemoryCache } from 'apollo-cache-inmemory';
import { reducer as valuesReducer } from 'react-redux-values'; import { reducer as valuesReducer } from 'react-redux-values';
import paramCase from 'param-case';
const { const {
REACT_APP_GQL_PORT = 443, REACT_APP_GQL_PORT = 443,
@ -22,7 +23,17 @@ export const client = new ApolloClient({
const initialState = { const initialState = {
ui: { ui: {
sections: { sections: {
instances: ['summary', 'tags', 'metadata', 'networks', 'snapshots'] instances: [
'Summary',
'CNS & DNS',
'Snapshots',
'Tags',
'Metadata',
'Networks'
].map(name => ({
pathname: paramCase(name),
name
}))
} }
} }
}; };

View File

@ -3829,8 +3829,8 @@ eslint-plugin-flowtype@2.39.1:
lodash "^4.15.0" lodash "^4.15.0"
eslint-plugin-flowtype@^2.39.1: eslint-plugin-flowtype@^2.39.1:
version "2.41.0" version "2.41.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.41.0.tgz#fd5221c60ba917c059d7ef69686a99cca09fd871" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.41.1.tgz#0996e1ea1d501dfc945802453a304ae9e8098b78"
dependencies: dependencies:
lodash "^4.15.0" lodash "^4.15.0"
@ -4926,8 +4926,8 @@ graphql-tag@^2.6.1:
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.6.1.tgz#4788d509f6e29607d947fc47a40c4e18f736d34a" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.6.1.tgz#4788d509f6e29607d947fc47a40c4e18f736d34a"
graphql-tools@^2.6.1: graphql-tools@^2.6.1:
version "2.18.0" version "2.19.0"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.18.0.tgz#8e2d6436f9adba1d579c1a1710ae95e7f5e7248b" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.19.0.tgz#04e1065532ab877aff3ad1883530fb56804ce9bf"
dependencies: dependencies:
apollo-link "^1.0.0" apollo-link "^1.0.0"
apollo-utilities "^1.0.1" apollo-utilities "^1.0.1"
@ -6656,6 +6656,10 @@ lodash.intersection@^4.4.0:
version "4.4.0" version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.intersection/-/lodash.intersection-4.4.0.tgz#0a11ba631d0e95c23c7f2f4cbb9a692ed178e705" resolved "https://registry.yarnpkg.com/lodash.intersection/-/lodash.intersection-4.4.0.tgz#0a11ba631d0e95c23c7f2f4cbb9a692ed178e705"
lodash.isarray@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-4.0.0.tgz#2aca496b28c4ca6d726715313590c02e6ea34403"
lodash.isarraylike@^4.2.0: lodash.isarraylike@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.isarraylike/-/lodash.isarraylike-4.2.0.tgz#4623310ab318804b667ddc3619058137559400c4" resolved "https://registry.yarnpkg.com/lodash.isarraylike/-/lodash.isarraylike-4.2.0.tgz#4623310ab318804b667ddc3619058137559400c4"