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.get": "^4.4.2",
"lodash.includes": "^4.3.0",
"lodash.isarray": "^4.0.0",
"lodash.isfinite": "^3.3.2",
"lodash.isfunction": "^3.0.8",
"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 ? (
<Fragment>
{expanded ? (
<span>{`${initialValues.name}${type === 'metadata'
? '-'
: ':'}`}</span>
<span>{`${initialValues.name}${
type === 'metadata' ? '-' : ':'
}`}</span>
) : (
<b>{`${initialValues.name}${type === 'metadata'
? '-'
: ':'}`}</b>
<b>{`${initialValues.name}${
type === 'metadata' ? '-' : ':'
}`}</b>
)}
<span>{initialValues.value}</span>
</Fragment>

View File

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

View File

@ -547,6 +547,7 @@ Array [
<a
href="https://docs.joyent.com/public-cloud/network/sdn"
rel="noopener noreferrer"
target="__blank"
>
Read more
@ -963,334 +964,7 @@ Array [
/>
</div>
</div>,
.c7 {
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>,
<form />,
.c0 {
margin-bottom: 2rem;
}
@ -1535,6 +1209,7 @@ Array [
<a
href="https://docs.joyent.com/public-cloud/network/sdn"
rel="noopener noreferrer"
target="__blank"
>
Read more
@ -3409,6 +3084,10 @@ Array [
margin-right: 0.25rem;
}
.c10 {
margin-right: 2rem;
}
.c2 {
display: inline-block;
margin-top: 1rem;
@ -3422,10 +3101,6 @@ Array [
margin-left: 1rem;
}
.c10 {
margin-right: 2rem;
}
.c9 {
display: -webkit-box;
display: -webkit-flex;
@ -3446,29 +3121,6 @@ Array [
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;
@ -3493,6 +3145,29 @@ Array [
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 {
display: inline-block;
background-color: rgb(255,255,255);

View File

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

View File

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

View File

@ -4,37 +4,17 @@ import ReduxForm from 'declarative-redux-form';
import { destroy } from 'redux-form';
import { connect } from 'react-redux';
import get from 'lodash.get';
import { Margin, Padding } from 'styled-components-spacing';
import Flex from 'styled-flex-component';
import { Margin } from 'styled-components-spacing';
import { set } from 'react-redux-values';
import punycode from 'punycode';
import remcalc from 'remcalc';
import {
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 { CnsIcon, H3, Button, FormLabel, TagList } from 'joyent-ui-toolkit';
import Cns, { Footer, AddServiceForm } from '@components/cns';
import Tag from '@components/tags';
import Title from '@components/create-instance/title';
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';
@ -50,8 +30,7 @@ const CNSContainer = ({
handleEdit,
handleToggleCnsEnabled,
handleAddService,
handleRemoveService,
loading
handleRemoveService
}) => (
<Fragment>
<Title onClick={!expanded && !proceeded && handleEdit} icon={<CnsIcon />}>
@ -73,81 +52,24 @@ const CNSContainer = ({
) : null}
<div>
{expanded && cnsEnabled ? (
<Card>
<Padding all={4} bottom={0}>
<Header />
{loading ? (
<Margin all={2}>
{' '}
<StatusLoader />
</Margin>
) : (
<Flex column>
{hostnames
.filter(({ service }) => !service)
.map(({ value, ...hostname }) => (
<Hostname key={value} value={value} {...hostname} />
))}
</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>
<Cns
hostnames={hostnames}
services={serviceNames}
onRemoveService={handleRemoveService}
>
<ReduxForm
form={`${CNS_FORM}-new-service`}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleAddService}
>
{props => <AddServiceForm {...props} />}
</ReduxForm>
</Cns>
) : null}
{expanded ? (
<Fragment>
<Margin bottom={4} top={4}>
<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>
<Footer enabled={cnsEnabled} onToggle={handleToggleCnsEnabled} />
<Margin bottom={4}>
<Button type="button" onClick={handleNext}>
Next
@ -184,9 +106,8 @@ const CNSContainer = ({
);
export default compose(
graphql(getAccount, {
props: ({ data: { loading, account: { id } = [] } }) => ({
loading,
graphql(GetAccount, {
props: ({ data: { account: { id = '<account-id>' } = [] } }) => ({
id
})
}),
@ -252,7 +173,7 @@ export default compose(
handleAddService: ({ name }) => {
const serviceName = punycode
.encode(name.toLowerCase().replace(/\s/g, '-'))
.replace(/\-$/, '');
.replace(/-$/, '');
dispatch([
destroy(`${CNS_FORM}-new-service`),
@ -262,11 +183,12 @@ export default compose(
})
]);
},
handleRemoveService: index => {
serviceNames.splice(index, 1);
handleRemoveService: value => {
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
target="__blank"
href="https://docs.joyent.com/public-cloud/network/firewall"
rel="noopener noreferrer"
>
Read more
</a>

View File

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

View File

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

View File

@ -42,6 +42,7 @@ export const Tags = ({
<a
target="__blank"
href="https://docs.joyent.com/public-cloud/tags-metadata/tags"
rel="noopener noreferrer"
>
Read the docs
</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,
subnet: '255.255.255.0',
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 Networks } from './networks';
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 Resize } from './resize';

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { reducer as valuesReducer } from 'react-redux-values';
import paramCase from 'param-case';
const {
REACT_APP_GQL_PORT = 443,
@ -22,7 +23,17 @@ export const client = new ApolloClient({
const initialState = {
ui: {
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"
eslint-plugin-flowtype@^2.39.1:
version "2.41.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.41.0.tgz#fd5221c60ba917c059d7ef69686a99cca09fd871"
version "2.41.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.41.1.tgz#0996e1ea1d501dfc945802453a304ae9e8098b78"
dependencies:
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"
graphql-tools@^2.6.1:
version "2.18.0"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.18.0.tgz#8e2d6436f9adba1d579c1a1710ae95e7f5e7248b"
version "2.19.0"
resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-2.19.0.tgz#04e1065532ab877aff3ad1883530fb56804ce9bf"
dependencies:
apollo-link "^1.0.0"
apollo-utilities "^1.0.1"
@ -6656,6 +6656,10 @@ lodash.intersection@^4.4.0:
version "4.4.0"
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:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.isarraylike/-/lodash.isarraylike-4.2.0.tgz#4623310ab318804b667ddc3619058137559400c4"