fix(my-joy-beta): sanitize and validate instance name

fixes #1009
This commit is contained in:
Sérgio Ramos 2018-01-18 11:56:01 +00:00 committed by Sérgio Ramos
parent ccf704a1ee
commit 9fba1860b0
9 changed files with 437 additions and 90 deletions

View File

@ -11,40 +11,52 @@ exports[`renders <Name expanded /> without throwing 1`] = `
margin-bottom: 1rem;
}
.c8 {
.c10 {
margin-left: 0.25rem;
}
.c14 {
margin-top: 0.5rem;
margin-bottom: 2rem;
}
.c0 {
box-sizing: border-box;
.c4 {
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;
-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;
}
.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;
.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;
}
.c11 {
.c13 {
font-family: sans-serif;
font-size: 100%;
line-height: 1.15;
@ -57,22 +69,22 @@ exports[`renders <Name expanded /> without throwing 1`] = `
min-width: 7.5rem;
}
.c11::-moz-focus-inner,
.c11[type='button']::-moz-focus-inner,
.c11[type='reset']::-moz-focus-inner,
.c11[type='submit']::-moz-focus-inner {
.c13::-moz-focus-inner,
.c13[type='button']::-moz-focus-inner,
.c13[type='reset']::-moz-focus-inner,
.c13[type='submit']::-moz-focus-inner {
border-style: none;
padding: 0;
}
.c11:-moz-focusring,
.c11[type='button']:-moz-focusring,
.c11[type='reset']:-moz-focusring,
.c11[type='submit']:-moz-focusring {
.c13:-moz-focusring,
.c13[type='button']:-moz-focusring,
.c13[type='reset']:-moz-focusring,
.c13[type='submit']:-moz-focusring {
outline: 0.0625rem dotted ButtonText;
}
.c11 + button {
.c13 + button {
margin-left: 0.375rem;
}
@ -96,11 +108,116 @@ exports[`renders <Name expanded /> without throwing 1`] = `
padding-bottom: 2.25rem;
}
.c10 {
.c12 {
display: inline-block;
}
.c9 {
.c11 {
box-sizing: border-box;
display: inline-block;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
min-height: 3rem;
height: 3rem;
min-width: 7.5rem;
margin-bottom: 0.5rem;
margin-top: 0.5rem;
padding: 0.9375rem 1.125rem;
position: relative;
font-size: 0.9375rem;
text-align: center;
font-style: normal;
font-stretch: normal;
line-height: normal;
-webkit-letter-spacing: normal;
-moz-letter-spacing: normal;
-ms-letter-spacing: normal;
letter-spacing: normal;
text-decoration: none;
white-space: nowrap;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
color: rgb(255,255,255);
-webkit-text-fill-color: currentcolor;
background-image: none;
background-color: rgb(59,70,204);
border-radius: 0.25rem;
border: solid 0.0625rem rgb(45,56,132);
color: rgb(70,70,70);
-webkit-text-fill-color: currentcolor;
background-color: rgb(255,255,255);
border-color: rgb(216,216,216);
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
margin: 0;
margin-top: 0.5rem;
}
.c11:focus {
outline: 0;
text-decoration: none;
background-color: rgb(59,70,204);
border-color: rgb(45,56,132);
}
.c11:hover {
background-color: rgb(72,83,217);
border: solid 0.0625rem rgb(45,56,132);
}
.c11:active,
.c11:active:hover,
.c11:active:focus {
background-image: none;
outline: 0;
background-color: rgb(45,56,132);
border-color: rgb(45,56,132);
}
.c11[disabled] {
cursor: not-allowed;
pointer-events: none;
}
.c11:focus {
background-color: rgb(255,255,255);
border-color: rgb(216,216,216);
}
.c11:hover {
background-color: rgb(247,247,247);
border-color: rgb(216,216,216);
}
.c11:active,
.c11:active:hover,
.c11:active:focus {
background-color: rgb(230,230,230);
border-color: rgb(216,216,216);
}
.c11 svg + span {
margin-left: 0.75rem;
}
.c11 svg {
max-height: 1.125rem;
}
.c15 {
box-sizing: border-box;
display: inline-block;
-webkit-box-pack: center;
@ -140,33 +257,33 @@ exports[`renders <Name expanded /> without throwing 1`] = `
border: solid 0.0625rem rgb(45,56,132);
}
.c9:focus {
.c15:focus {
outline: 0;
text-decoration: none;
background-color: rgb(59,70,204);
border-color: rgb(45,56,132);
}
.c9:hover {
.c15:hover {
background-color: rgb(72,83,217);
border: solid 0.0625rem rgb(45,56,132);
}
.c9:active,
.c9:active:hover,
.c9:active:focus {
.c15:active,
.c15:active:hover,
.c15:active:focus {
background-image: none;
outline: 0;
background-color: rgb(45,56,132);
border-color: rgb(45,56,132);
}
.c9[disabled] {
.c15[disabled] {
cursor: not-allowed;
pointer-events: none;
}
.c5 {
.c7 {
font-size: 0.9375rem;
font-style: normal;
font-stretch: normal;
@ -179,7 +296,7 @@ exports[`renders <Name expanded /> without throwing 1`] = `
font-size: 0.8125rem;
}
.c7 {
.c9 {
font-size: 0.9375rem;
font-style: normal;
font-stretch: normal;
@ -190,9 +307,38 @@ exports[`renders <Name expanded /> without throwing 1`] = `
font-size: 0.8125rem;
float: none;
margin-left: 1.75rem;
margin-left: 0;
}
.c4 {
.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;
}
.c6 {
display: inline-block;
margin: 0;
padding: 0;
@ -208,7 +354,7 @@ exports[`renders <Name expanded /> without throwing 1`] = `
width: 100%;
}
.c6 {
.c8 {
box-sizing: border-box;
width: 18.75rem;
height: 3rem;
@ -232,41 +378,41 @@ exports[`renders <Name expanded /> without throwing 1`] = `
outline: 0;
}
.c6::-webkit-input-placeholder {
.c8::-webkit-input-placeholder {
color: rgba(73,73,73,0.5);
}
.c6::-moz-placeholder {
.c8::-moz-placeholder {
color: rgba(73,73,73,0.5);
}
.c6:-ms-input-placeholder {
.c8:-ms-input-placeholder {
color: rgba(73,73,73,0.5);
}
.c6:invalid {
.c8:invalid {
box-shadow: none;
}
.c6:disabled {
.c8:disabled {
background-color: rgb(250,250,250);
color: rgb(216,216,216);
cursor: not-allowed;
}
.c6:disabled::-webkit-input-placeholder {
.c8:disabled::-webkit-input-placeholder {
color: rgba(73,73,73,0.5);
}
.c6:disabled::-moz-placeholder {
.c8:disabled::-moz-placeholder {
color: rgba(73,73,73,0.5);
}
.c6:disabled:-ms-input-placeholder {
.c8:disabled:-ms-input-placeholder {
color: rgba(73,73,73,0.5);
}
.c6:focus {
.c8:focus {
border-color: rgb(59,70,204);
outline: 0;
}
@ -312,32 +458,86 @@ exports[`renders <Name expanded /> without throwing 1`] = `
</div>
</div>
<div
className="baseline-jVaZNU kXgQxt c4"
className="c4"
>
<div
className="c5"
>
<div
className="baseline-jVaZNU kXgQxt c6"
name="name"
role="group"
style={undefined}
>
<label
className="c5"
className="c7"
htmlFor="Y"
>
Instance Name
</label>
<input
className="c6"
className="c8"
disabled={false}
id="Y"
onBlur={null}
placeholder={undefined}
/>
<label
className="c7"
className="c9"
/>
</div>
</div>
<div
className="c8"
className="c5"
>
<label
className="c7"
htmlFor=""
>
</label>
<div
className="c10"
>
<button
className="c9 c10 c11"
className="c11 c12 c13"
href=""
icon={true}
onClick={undefined}
type="button"
>
<svg
className=""
height="17"
innerRef={undefined}
style={
Object {
"transform": "rotate(0deg)",
}
}
version="1.1"
viewBox="0 0 18.22 18.22"
width="17"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<path
d="M6.32,1l4.55,5.31L5.56,10.87,1,5.56,6.32,1m0-1a1,1,0,0,0-.65.24L.35,4.8A1,1,0,0,0,.24,6.21L4.8,11.52a1,1,0,0,0,1.41.11l5.31-4.56a1,1,0,0,0,.11-1.4L7.07.35A1,1,0,0,0,6.32,0Zm4.14,8.65,6.76,1.81-1.8,6.76-6.77-1.8,1.81-6.77m0-1a1,1,0,0,0-1,.74l-1.8,6.77a1,1,0,0,0,.7,1.22l6.77,1.81a1.09,1.09,0,0,0,.26,0,1,1,0,0,0,1-.74l1.81-6.77a1,1,0,0,0-.71-1.22l-6.77-1.8a.73.73,0,0,0-.25,0ZM6.86,3.17a1,1,0,1,0-.11,1.41A1,1,0,0,0,6.86,3.17Zm2,2.28A1,1,0,1,0,8.7,6.86,1,1,0,0,0,8.81,5.45Zm-2.28,2a1,1,0,1,0-.11,1.41A1,1,0,0,0,6.53,7.4ZM4.58,5.12a1,1,0,1,0-.11,1.41A1,1,0,0,0,4.58,5.12Zm6.33,5.72a1,1,0,1,0,1.22-.71A1,1,0,0,0,10.91,10.84ZM13,14.51A1,1,0,1,0,15,15,1,1,0,0,0,13,14.51Z"
fill="rgba(73, 73, 73, 1)"
/>
</svg>
<span>
Randomize
</span>
</button>
</div>
</div>
</div>
<div
className="c14"
>
<button
className="c15 c12 c13"
disabled={undefined}
href=""
type="submit"

View File

@ -1,7 +1,8 @@
import React, { Fragment } from 'react';
import { Field } from 'redux-form';
import { Margin } from 'styled-components-spacing';
import Description from '@components/create-instance/description';
import Flex, { FlexItem } from 'styled-flex-component';
import remcalc from 'remcalc';
import {
H3,
@ -9,23 +10,57 @@ import {
FormLabel,
Input,
FormMeta,
Button
Button,
RandomizeIcon
} from 'joyent-ui-toolkit';
export default ({ handleSubmit, pristine, expanded, name, onCancel }) => (
import Description from '@components/create-instance/description';
export default ({
handleSubmit,
pristine,
asyncValidating,
expanded,
name,
placeholderName,
randomizing,
onCancel,
onRandomize
}) => (
<form onSubmit={handleSubmit}>
{expanded ? (
<Fragment>
<Description>
Your instance name will be used to identify this specific instance.
</Description>
<Flex>
<FlexItem>
<FormGroup name="name" fluid field={Field}>
<FormLabel>Instance Name</FormLabel>
<Input onBlur={null} />
<FormMeta />
<Input placeholder={placeholderName} onBlur={null} />
<FormMeta marginless />
</FormGroup>
</FlexItem>
<FlexItem>
<FormLabel>&#8291;</FormLabel>
<Margin left={1}>
<Button
type="button"
marginTop={remcalc(8)}
onClick={onRandomize}
loading={randomizing}
marginless
secondary
icon
>
<RandomizeIcon />
<span>Randomize</span>
</Button>
</Margin>
</FlexItem>
</Flex>
<Margin top={2} bottom={4}>
<Button type="submit" disabled={pristine}>
<Button type="submit" disabled={pristine} loading={asyncValidating}>
Next
</Button>
</Margin>

View File

@ -1,29 +1,54 @@
import React, { Fragment } from 'react';
import { compose } from 'react-apollo';
import { compose, graphql } from 'react-apollo';
import { set } from 'react-redux-values';
import ReduxForm from 'declarative-redux-form';
import { change } from 'redux-form';
import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import get from 'lodash.get';
import punycode from 'punycode';
import { NameIcon } from 'joyent-ui-toolkit';
import Name from '@components/create-instance/name';
import Title from '@components/create-instance/title';
import GetInstance from '@graphql/get-instance-small.gql';
import GetRandomName from '@graphql/get-random-name.gql';
import { client } from '@state/store';
import parseError from '@state/parse-error';
const NameContainer = ({ expanded, name, handleSubmit, handleCancel }) => (
const FORM_NAME = 'CREATE_INSTANCE_NAME';
const NameContainer = ({
expanded,
name,
placeholderName,
randomizing,
handleAsyncValidation,
shouldAsyncValidate,
handleNext,
handleCancel,
handleRandomize
}) => (
<Fragment>
<Title icon={<NameIcon />}>Instance name</Title>
<ReduxForm
form="create-instance-name"
form={FORM_NAME}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleSubmit}
onSubmit={handleNext}
asyncValidate={handleAsyncValidation}
shouldAsyncValidate={shouldAsyncValidate}
>
{props => (
<Name
{...props}
name={name}
placeholderName={placeholderName}
expanded={expanded}
randomizing={randomizing}
onCancel={handleCancel}
onRandomize={handleRandomize}
/>
)}
</ReduxForm>
@ -31,14 +56,89 @@ const NameContainer = ({ expanded, name, handleSubmit, handleCancel }) => (
);
export default compose(
connect(
(state, ownProps) => ({
...ownProps,
name: get(state, 'form.create-instance-name.values.name')
graphql(GetRandomName, {
fetchPolicy: 'network-only',
props: ({ data }) => ({
placeholderName: data.rndName || ''
})
}),
connect(
({ form, values }, ownProps) => {
const randomizing = get(
values,
'create-instance-name-randomizing',
false
);
const name = get(form, `${FORM_NAME}.values.name`, '');
return {
...ownProps,
randomizing,
name
};
},
(dispatch, { history }) => ({
handleSubmit: () => history.push(`/instances/~create/image`),
handleCancel: () => history.push(`/instances/~create/name`)
shouldAsyncValidate: ({ trigger }) => trigger === 'submit',
handleAsyncValidation: async ({ name }) => {
const sanitized = punycode.encode(name).replace(/\-$/, '');
if (sanitized !== name) {
throw {
name: 'Special characters are not accepted'
};
}
const [err, res] = await intercept(
client.query({
fetchPolicy: 'network-only',
query: GetInstance,
variables: { name }
})
);
if (err) {
throw {
name: parseError(err)
};
}
const { data } = res;
const { machines = [] } = data;
if (machines.length) {
throw {
name: `${name} already exists`
};
}
},
handleNext: () => history.push(`/instances/~create/image`),
handleCancel: () => history.push(`/instances/~create/name`),
handleRandomize: async () => {
dispatch(
set({ name: 'create-instance-name-randomizing', value: true })
);
const [err, res] = await intercept(
client.query({
fetchPolicy: 'network-only',
query: GetRandomName
})
);
dispatch(
set({ name: 'create-instance-name-randomizing', value: false })
);
if (err) {
console.error(err);
return;
}
const { data } = res;
const { rndName } = data;
return dispatch(change(FORM_NAME, 'name', rndName));
}
})
)
)(NameContainer);

View File

@ -0,0 +1,5 @@
query instance($name: String) {
machines(name: $name) {
id
}
}

View File

@ -0,0 +1,3 @@
query rndName {
rndName
}

View File

@ -30,6 +30,10 @@ const StyledLabel = Label.extend`
font-size: ${remcalc(13)};
float: none;
margin-left: ${remcalc(28)};
${is('marginless')`
margin-left: 0;
`};
`;
const Meta = props => {