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; margin-bottom: 1rem;
} }
.c8 { .c10 {
margin-left: 0.25rem;
}
.c14 {
margin-top: 0.5rem; margin-top: 0.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.c0 { .c4 {
box-sizing: border-box;
display: -webkit-box; display: -webkit-box;
display: -webkit-flex; display: -webkit-flex;
display: -ms-flexbox; display: -ms-flexbox;
display: flex; display: flex;
-webkit-flex: 0 1 auto;
-ms-flex: 0 1 auto;
flex: 0 1 auto;
-webkit-flex-direction: row; -webkit-flex-direction: row;
-ms-flex-direction: row; -ms-flex-direction: row;
flex-direction: row; flex-direction: row;
-webkit-flex-wrap: wrap; -webkit-flex-wrap: nowrap;
-ms-flex-wrap: wrap; -ms-flex-wrap: nowrap;
flex-wrap: wrap; flex-wrap: nowrap;
margin-right: -0.5rem; -webkit-box-pack: start;
margin-left: -0.5rem; -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 { .c5 {
box-sizing: border-box; -webkit-order: 0;
-webkit-flex: 0 0 auto; -ms-flex-order: 0;
-ms-flex: 0 0 auto; order: 0;
flex: 0 0 auto; -webkit-flex-basis: auto;
padding-right: 0.5rem; -ms-flex-preferred-size: auto;
padding-left: 0.5rem; 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-family: sans-serif;
font-size: 100%; font-size: 100%;
line-height: 1.15; line-height: 1.15;
@ -57,22 +69,22 @@ exports[`renders <Name expanded /> without throwing 1`] = `
min-width: 7.5rem; min-width: 7.5rem;
} }
.c11::-moz-focus-inner, .c13::-moz-focus-inner,
.c11[type='button']::-moz-focus-inner, .c13[type='button']::-moz-focus-inner,
.c11[type='reset']::-moz-focus-inner, .c13[type='reset']::-moz-focus-inner,
.c11[type='submit']::-moz-focus-inner { .c13[type='submit']::-moz-focus-inner {
border-style: none; border-style: none;
padding: 0; padding: 0;
} }
.c11:-moz-focusring, .c13:-moz-focusring,
.c11[type='button']:-moz-focusring, .c13[type='button']:-moz-focusring,
.c11[type='reset']:-moz-focusring, .c13[type='reset']:-moz-focusring,
.c11[type='submit']:-moz-focusring { .c13[type='submit']:-moz-focusring {
outline: 0.0625rem dotted ButtonText; outline: 0.0625rem dotted ButtonText;
} }
.c11 + button { .c13 + button {
margin-left: 0.375rem; margin-left: 0.375rem;
} }
@ -96,11 +108,116 @@ exports[`renders <Name expanded /> without throwing 1`] = `
padding-bottom: 2.25rem; padding-bottom: 2.25rem;
} }
.c10 { .c12 {
display: inline-block; 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; box-sizing: border-box;
display: inline-block; display: inline-block;
-webkit-box-pack: center; -webkit-box-pack: center;
@ -140,33 +257,33 @@ exports[`renders <Name expanded /> without throwing 1`] = `
border: solid 0.0625rem rgb(45,56,132); border: solid 0.0625rem rgb(45,56,132);
} }
.c9:focus { .c15:focus {
outline: 0; outline: 0;
text-decoration: none; text-decoration: none;
background-color: rgb(59,70,204); background-color: rgb(59,70,204);
border-color: rgb(45,56,132); border-color: rgb(45,56,132);
} }
.c9:hover { .c15:hover {
background-color: rgb(72,83,217); background-color: rgb(72,83,217);
border: solid 0.0625rem rgb(45,56,132); border: solid 0.0625rem rgb(45,56,132);
} }
.c9:active, .c15:active,
.c9:active:hover, .c15:active:hover,
.c9:active:focus { .c15:active:focus {
background-image: none; background-image: none;
outline: 0; outline: 0;
background-color: rgb(45,56,132); background-color: rgb(45,56,132);
border-color: rgb(45,56,132); border-color: rgb(45,56,132);
} }
.c9[disabled] { .c15[disabled] {
cursor: not-allowed; cursor: not-allowed;
pointer-events: none; pointer-events: none;
} }
.c5 { .c7 {
font-size: 0.9375rem; font-size: 0.9375rem;
font-style: normal; font-style: normal;
font-stretch: normal; font-stretch: normal;
@ -179,7 +296,7 @@ exports[`renders <Name expanded /> without throwing 1`] = `
font-size: 0.8125rem; font-size: 0.8125rem;
} }
.c7 { .c9 {
font-size: 0.9375rem; font-size: 0.9375rem;
font-style: normal; font-style: normal;
font-stretch: normal; font-stretch: normal;
@ -190,9 +307,38 @@ exports[`renders <Name expanded /> without throwing 1`] = `
font-size: 0.8125rem; font-size: 0.8125rem;
float: none; float: none;
margin-left: 1.75rem; 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; display: inline-block;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -208,7 +354,7 @@ exports[`renders <Name expanded /> without throwing 1`] = `
width: 100%; width: 100%;
} }
.c6 { .c8 {
box-sizing: border-box; box-sizing: border-box;
width: 18.75rem; width: 18.75rem;
height: 3rem; height: 3rem;
@ -232,41 +378,41 @@ exports[`renders <Name expanded /> without throwing 1`] = `
outline: 0; outline: 0;
} }
.c6::-webkit-input-placeholder { .c8::-webkit-input-placeholder {
color: rgba(73,73,73,0.5); color: rgba(73,73,73,0.5);
} }
.c6::-moz-placeholder { .c8::-moz-placeholder {
color: rgba(73,73,73,0.5); color: rgba(73,73,73,0.5);
} }
.c6:-ms-input-placeholder { .c8:-ms-input-placeholder {
color: rgba(73,73,73,0.5); color: rgba(73,73,73,0.5);
} }
.c6:invalid { .c8:invalid {
box-shadow: none; box-shadow: none;
} }
.c6:disabled { .c8:disabled {
background-color: rgb(250,250,250); background-color: rgb(250,250,250);
color: rgb(216,216,216); color: rgb(216,216,216);
cursor: not-allowed; cursor: not-allowed;
} }
.c6:disabled::-webkit-input-placeholder { .c8:disabled::-webkit-input-placeholder {
color: rgba(73,73,73,0.5); color: rgba(73,73,73,0.5);
} }
.c6:disabled::-moz-placeholder { .c8:disabled::-moz-placeholder {
color: rgba(73,73,73,0.5); color: rgba(73,73,73,0.5);
} }
.c6:disabled:-ms-input-placeholder { .c8:disabled:-ms-input-placeholder {
color: rgba(73,73,73,0.5); color: rgba(73,73,73,0.5);
} }
.c6:focus { .c8:focus {
border-color: rgb(59,70,204); border-color: rgb(59,70,204);
outline: 0; outline: 0;
} }
@ -312,32 +458,86 @@ exports[`renders <Name expanded /> without throwing 1`] = `
</div> </div>
</div> </div>
<div <div
className="baseline-jVaZNU kXgQxt c4" className="c4"
name="name"
role="group"
style={undefined}
> >
<label <div
className="c5" className="c5"
htmlFor="Y"
> >
Instance Name <div
</label> className="baseline-jVaZNU kXgQxt c6"
<input name="name"
className="c6" role="group"
disabled={false} style={undefined}
id="Y" >
onBlur={null} <label
/> className="c7"
<label htmlFor="Y"
className="c7" >
/> Instance Name
</label>
<input
className="c8"
disabled={false}
id="Y"
onBlur={null}
placeholder={undefined}
/>
<label
className="c9"
/>
</div>
</div>
<div
className="c5"
>
<label
className="c7"
htmlFor=""
>
</label>
<div
className="c10"
>
<button
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>
<div <div
className="c8" className="c14"
> >
<button <button
className="c9 c10 c11" className="c15 c12 c13"
disabled={undefined} disabled={undefined}
href="" href=""
type="submit" type="submit"

View File

@ -1,7 +1,8 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { Field } from 'redux-form'; import { Field } from 'redux-form';
import { Margin } from 'styled-components-spacing'; 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 { import {
H3, H3,
@ -9,23 +10,57 @@ import {
FormLabel, FormLabel,
Input, Input,
FormMeta, FormMeta,
Button Button,
RandomizeIcon
} from 'joyent-ui-toolkit'; } 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}> <form onSubmit={handleSubmit}>
{expanded ? ( {expanded ? (
<Fragment> <Fragment>
<Description> <Description>
Your instance name will be used to identify this specific instance. Your instance name will be used to identify this specific instance.
</Description> </Description>
<FormGroup name="name" fluid field={Field}> <Flex>
<FormLabel>Instance Name</FormLabel> <FlexItem>
<Input onBlur={null} /> <FormGroup name="name" fluid field={Field}>
<FormMeta /> <FormLabel>Instance Name</FormLabel>
</FormGroup> <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}> <Margin top={2} bottom={4}>
<Button type="submit" disabled={pristine}> <Button type="submit" disabled={pristine} loading={asyncValidating}>
Next Next
</Button> </Button>
</Margin> </Margin>

View File

@ -1,29 +1,54 @@
import React, { Fragment } from 'react'; 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 ReduxForm from 'declarative-redux-form';
import { change } from 'redux-form';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import intercept from 'apr-intercept';
import get from 'lodash.get'; import get from 'lodash.get';
import punycode from 'punycode';
import { NameIcon } from 'joyent-ui-toolkit'; import { NameIcon } from 'joyent-ui-toolkit';
import Name from '@components/create-instance/name'; import Name from '@components/create-instance/name';
import Title from '@components/create-instance/title'; 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> <Fragment>
<Title icon={<NameIcon />}>Instance name</Title> <Title icon={<NameIcon />}>Instance name</Title>
<ReduxForm <ReduxForm
form="create-instance-name" form={FORM_NAME}
destroyOnUnmount={false} destroyOnUnmount={false}
forceUnregisterOnUnmount={true} forceUnregisterOnUnmount={true}
onSubmit={handleSubmit} onSubmit={handleNext}
asyncValidate={handleAsyncValidation}
shouldAsyncValidate={shouldAsyncValidate}
> >
{props => ( {props => (
<Name <Name
{...props} {...props}
name={name} name={name}
placeholderName={placeholderName}
expanded={expanded} expanded={expanded}
randomizing={randomizing}
onCancel={handleCancel} onCancel={handleCancel}
onRandomize={handleRandomize}
/> />
)} )}
</ReduxForm> </ReduxForm>
@ -31,14 +56,89 @@ const NameContainer = ({ expanded, name, handleSubmit, handleCancel }) => (
); );
export default compose( export default compose(
graphql(GetRandomName, {
fetchPolicy: 'network-only',
props: ({ data }) => ({
placeholderName: data.rndName || ''
})
}),
connect( connect(
(state, ownProps) => ({ ({ form, values }, ownProps) => {
...ownProps, const randomizing = get(
name: get(state, 'form.create-instance-name.values.name') values,
}), 'create-instance-name-randomizing',
false
);
const name = get(form, `${FORM_NAME}.values.name`, '');
return {
...ownProps,
randomizing,
name
};
},
(dispatch, { history }) => ({ (dispatch, { history }) => ({
handleSubmit: () => history.push(`/instances/~create/image`), shouldAsyncValidate: ({ trigger }) => trigger === 'submit',
handleCancel: () => history.push(`/instances/~create/name`) 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); )(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)}; font-size: ${remcalc(13)};
float: none; float: none;
margin-left: ${remcalc(28)}; margin-left: ${remcalc(28)};
${is('marginless')`
margin-left: 0;
`};
`; `;
const Meta = props => { const Meta = props => {