fix(instances): validation improvements

fixes #1292
This commit is contained in:
Sérgio Ramos 2018-03-01 19:53:30 +00:00
parent d7f83c59fa
commit c6b245aebc
43 changed files with 109 additions and 59 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -307,7 +307,6 @@ exports[`renders <AddServiceForm /> without throwing 1`] = `
id="k"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -643,7 +642,6 @@ exports[`renders <AddServiceForm pristine /> without throwing 1`] = `
id="l"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>

View File

@ -57,7 +57,7 @@ export const Footer = ({ enabled, submitting, onToggle }) => (
</Margin>
{enabled ? (
<Margin bottom={4}>
<P>*All hostnames listed here will be confirmed after deployment.</P>
<P>Please note: All hostnames listed here will be confirmed after deployment.</P>
</Margin>
) : null}
</Fragment>
@ -93,7 +93,6 @@ export const AddServiceForm = ({
onBlur={null}
type="text"
placeholder="Example: mySQLdb"
required
disabled={disabled || submitting}
/>
<FormMeta />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -42,7 +42,6 @@ Array [
"tag",
" ",
"key “",
"two",
"\\" and the instance tag value",
" ",
"equalling",

View File

@ -1038,6 +1038,7 @@ exports[`renders <Package /> without throwing 1`] = `
font-weight: 600;
white-space: pre;
font-size: 0.8125rem;
cursor: pointer;
margin: 0;
}

View File

@ -77,12 +77,12 @@ export const Rule = ({ valid, ...rule }) => (
</FormGroup>
{rule.type === 'tag' ? (
<Fragment>
<FormGroup name="key" field={Field}>
<FormGroup name="name" field={Field}>
<Input
style={style}
onBlur={null}
type="text"
placeholder="key"
placeholder="name"
small
embedded
required
@ -141,7 +141,7 @@ export const Header = ({ rule }) => (
) : (
<Fragment>
{' '}
key {rule.key}" and the instance tag value{' '}
key {rule.name}" and the instance tag value{' '}
{rule.pattern && rule.pattern.split('-').join(' ')} "{rule.value}
</Fragment>
)}

View File

@ -140,8 +140,9 @@ export const Package = ({
{GroupIcons[group]}
<Margin left={1} right={2}>
<FormLabel
noMargin
style={{ fontWeight: sortBy === 'name' ? 'bold' : 'normal' }}
noMargin
actionable
>
{name}
</FormLabel>

View File

@ -13,7 +13,7 @@ const Container = styled.div`
`};
`;
export default ({ icon, children, collapsed = true, ...rest }) => (
export default ({ icon, children, invalid, collapsed = true, ...rest }) => (
<Container {...rest}>
<Flex>
<Margin right={1}>
@ -24,7 +24,7 @@ export default ({ icon, children, collapsed = true, ...rest }) => (
<Small noMargin>{children}</Small>
</Flex>
<Margin top={1} bottom={collapsed ? 7 : 3}>
<Divider height={remcalc(1)} />
<Divider height={remcalc(1)} error={invalid} />
</Margin>
</Container>
);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -44,5 +44,6 @@ export const Values = {
IC_TAG_V_ADD_OPEN: 'INSTANCE_CREATION_TAG_VALUE_ADD_OPEN',
IC_TAG_V_TAGS: 'INSTANCE_CREATION_TAG_VALUE_TAGS',
IC_US_V_PROCEEDED: 'INSTANCE_CREATION_USERSCRIPT_VALUE_PROCEEDED',
IC_US_V_OPEN: 'INSTANCE_CREATION_USERSCRIPT_VALUE_OPEN'
IC_US_V_OPEN: 'INSTANCE_CREATION_USERSCRIPT_VALUE_OPEN',
IC_V_VALIDATING: 'INSTANCE_CREATION_VALUE_VALIDATING'
};

View File

@ -24,7 +24,7 @@ const RULE_DEFAULTS = {
placement: 'same',
type: 'name',
pattern: 'equalling',
key: '',
name: '',
value: ''
};
@ -75,7 +75,7 @@ export const Affinity = ({
destroyOnUnmount={false}
forceUnregisterOnUnmount={false}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidation={handleAsyncValidate}
asyncValidate={handleAsyncValidate}
onSubmit={handleUpdateAffinityRule}
>
{formProps =>
@ -185,7 +185,7 @@ export default compose(
},
handleAsyncValidate: ({ type, ...aff }) => {
return type === 'name'
? validateRule({ ...aff, key: 'default', type })
? validateRule({ ...aff, type, name: 'default' })
: validateRule({ ...aff, type });
},
handleEdit: () => {

View File

@ -48,7 +48,7 @@ const CNSContainer = ({
{expanded ? (
<Description>
Triton CNS is used to automatically update hostnames for your
instances*. You can serve multiple instances (with multiple IP
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"

View File

@ -3,9 +3,9 @@
import React from 'react';
import { Margin } from 'styled-components-spacing';
import ReduxForm from 'declarative-redux-form';
import { SubmissionError, destroy } from 'redux-form';
import { stopAsyncValidation, SubmissionError, destroy } from 'redux-form';
import { connect } from 'react-redux';
import { destroyAll } from 'react-redux-values';
import { set, destroyAll } from 'react-redux-values';
import { graphql, compose } from 'react-apollo';
import intercept from 'apr-intercept';
import constantCase from 'constant-case';
@ -35,6 +35,8 @@ import Firewall from '@containers/create-instance/firewall';
import CNS from '@containers/create-instance/cns';
import Affinity from '@containers/create-instance/affinity';
import CreateInstanceMutation from '@graphql/create-instance.gql';
import GetInstance from '@graphql/get-instance-small.gql';
import createClient from '@state/apollo-client';
import parseError from '@state/parse-error';
import { Forms, Values } from '@root/constants';
@ -46,17 +48,21 @@ const {
IC_AFF_V_AFF,
IC_CNS_V_ENABLED,
IC_CNS_V_SERVICES,
IC_FW_F_ENABLED
IC_FW_F_ENABLED,
IC_V_VALIDATING
} = Values;
const CreateInstance = ({
history,
match,
query,
step,
error,
disabled,
shouldAsyncValidate,
handleAsyncValidate,
handleSubmit,
history,
match,
query
validating
}) => (
<ViewContainer>
<Margin top={4} bottom={4}>
@ -147,10 +153,15 @@ const CreateInstance = ({
</Message>
</Margin>
) : null}
<ReduxForm form={IC_F} onSubmit={handleSubmit}>
<ReduxForm
form={IC_F}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidate}
onSubmit={handleSubmit}
>
{({ handleSubmit, submitting }) => (
<form onSubmit={handleSubmit}>
<Button disabled={disabled} loading={submitting}>
<Button disabled={disabled} loading={submitting || validating}>
Deploy
</Button>
</form>
@ -166,6 +177,8 @@ export default compose(
const query = queryString.parse(location.search);
const step = get(match, 'params.step', 'name');
const validating = get(values, IC_V_VALIDATING, false);
const isNameInvalid = get(form, `${IC_NAME_F}.asyncErrors.name`, null);
const error = get(form, `${IC_F}.error`, null);
const name = get(form, `${IC_NAME_F}.values.name`, '');
const image = get(form, `${IC_IMG_F}.values.image`, '');
@ -173,6 +186,7 @@ export default compose(
const networks = get(form, `${IC_NW_F}.values`, {});
const enabled =
!isNameInvalid &&
name.length &&
image.length &&
pkg.length &&
@ -180,6 +194,7 @@ export default compose(
if (!enabled) {
return {
validating,
error,
query,
disabled: !enabled,
@ -213,6 +228,7 @@ export default compose(
}
return {
validating,
error,
query,
forms: Object.keys(form), // improve this
@ -248,7 +264,7 @@ export default compose(
conditional,
placement,
identity,
key,
name,
pattern,
value
}) => {
@ -264,17 +280,50 @@ export default compose(
ending: value => `/${value}$/`
};
const _key = identity === 'name' ? 'instance' : key;
const _name = identity === 'name' ? 'instance' : name;
const _value = patterns[pattern](type === 'name' ? name : value);
return {
type,
key: _key,
name: _name,
value: _value
};
};
return {
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: async () => {
dispatch(set({ name: IC_V_VALIDATING, value: true }));
const [nameError, res] = await intercept(
createClient().query({
fetchPolicy: 'network-only',
query: GetInstance,
variables: { name }
})
);
if (nameError) {
return dispatch([
set({ name: IC_V_VALIDATING, value: false }),
stopAsyncValidation(IC_F, { _error: parseError(nameError) })
]);
}
const { data } = res;
const { machines = [] } = data;
if (machines.length) {
return dispatch([
set({ name: IC_V_VALIDATING, value: false }),
stopAsyncValidation(IC_F, { _error: `${name} already exists.` })
]);
}
dispatch(set({ name: IC_V_VALIDATING, value: false }));
},
handleSubmit: async () => {
const _affinity = affinity ? parseAffRule(affinity) : null;
const _name = name.toLowerCase();
@ -298,7 +347,7 @@ export default compose(
name: _name,
package: pkg,
image,
affinity: _affinity,
affinity: _affinity ? [_affinity] : [],
metadata: _metadata,
tags: _tags,
firewall_enabled,

View File

@ -13,15 +13,17 @@ import { NameIcon, H3, Button } from 'joyent-ui-toolkit';
import Title from '@components/create-instance/title';
import Name from '@components/create-instance/name';
import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql';
import { instanceName as validateName } from '@state/validators';
import createClient from '@state/apollo-client';
import GetRandomName from '@graphql/get-random-name.gql';
import parseError from '@state/parse-error';
import { Forms, Values } from '@root/constants';
const { IC_NAME_F } = Forms;
const { IC_NAME_V_PROCEEDED, IC_NAME_V_RANDOMIZING } = Values;
const NameContainer = ({
invalid,
expanded,
proceeded,
name,
@ -40,6 +42,7 @@ const NameContainer = ({
onClick={!expanded && !proceeded && handleEdit}
collapsed={!expanded && !proceeded}
icon={<NameIcon />}
invalid={!expanded && invalid}
>
Instance name
</Title>
@ -99,11 +102,12 @@ export default compose(
connect(
({ form, values }, ownProps) => {
const name = get(form, `${IC_NAME_F}.values.name`, '');
const invalid = get(form, `${IC_NAME_F}.asyncErrors.name`, null);
const randomizing = get(values, IC_NAME_V_RANDOMIZING, false);
const proceeded = get(values, IC_NAME_V_PROCEEDED, false);
return {
...ownProps,
invalid,
proceeded: proceeded || name.length,
randomizing,
name

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -586,7 +586,7 @@ exports[`renders <Cns /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -684,7 +684,6 @@ exports[`renders <Cns /> without throwing 1`] = `
id="k"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -784,7 +783,7 @@ exports[`renders <Cns /> without throwing 1`] = `
<p
className="c5"
>
*All hostnames listed here will be confirmed after deployment.
Please note: All hostnames listed here will be confirmed after deployment.
</p>
</div>
</div>
@ -1061,7 +1060,7 @@ exports[`renders <Cns disabled /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -1918,7 +1917,7 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -2218,7 +2217,6 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
id="u"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -2720,7 +2718,7 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
<p
className="c5"
>
*All hostnames listed here will be confirmed after deployment.
Please note: All hostnames listed here will be confirmed after deployment.
</p>
</div>
</div>
@ -2910,7 +2908,7 @@ exports[`renders <Cns loading /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -3469,7 +3467,7 @@ exports[`renders <Cns loadingError /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -3592,7 +3590,6 @@ exports[`renders <Cns loadingError /> without throwing 1`] = `
id="m"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -4543,7 +4540,7 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -4903,7 +4900,6 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
id="p"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -5405,7 +5401,7 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
<p
className="c5"
>
*All hostnames listed here will be confirmed after deployment.
Please note: All hostnames listed here will be confirmed after deployment.
</p>
</div>
</div>
@ -6040,7 +6036,7 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -6163,7 +6159,6 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
id="n"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -6263,7 +6258,7 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
<p
className="c5"
>
*All hostnames listed here will be confirmed after deployment.
Please note: All hostnames listed here will be confirmed after deployment.
</p>
</div>
</div>
@ -6920,7 +6915,7 @@ exports[`renders <Cns services /> without throwing 1`] = `
<p
className="c5"
>
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.
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"
@ -7135,7 +7130,6 @@ exports[`renders <Cns services /> without throwing 1`] = `
id="s"
onBlur={null}
placeholder="Example: mySQLdb"
required={true}
/>
</div>
</div>
@ -7235,7 +7229,7 @@ exports[`renders <Cns services /> without throwing 1`] = `
<p
className="c5"
>
*All hostnames listed here will be confirmed after deployment.
Please note: All hostnames listed here will be confirmed after deployment.
</p>
</div>
</div>
@ -7512,7 +7506,7 @@ exports[`renders <Cns services hostnames /> without throwing 1`] = `
<p
className="c5"
>
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.
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"

View File

@ -48,7 +48,7 @@ const CnsContainer = ({
<Margin bottom={1}>
<Description>
Triton CNS is used to automatically update hostnames for your
instances*. You can serve multiple instances (with multiple IP
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"

View File

@ -59,7 +59,7 @@ export const List = ({
handleCreateImage,
handleSortBy,
history,
filter
filter = ''
}) => {
const _instances = forceArray(instances);
@ -72,7 +72,7 @@ export const List = ({
: null;
const _error =
error && !_instances.length && !_loading ? (
error && !_instances.length && !_loading && !filter.length ? (
<Margin bottom={4}>
<Message error>
<MessageTitle>Ooops!</MessageTitle>

View File

@ -56,12 +56,12 @@ const Schemas = {
cns: {
name: yup
.string()
.required(msgs.required('Service name'))
.matches(matches.nameStart, msgs.nameStart('Service name'))
.matches(matches.nameBody, msgs.nameBody('Service name'))
.required(msgs.required('Service names'))
.matches(matches.nameStart, msgs.nameStart('Service names'))
.matches(matches.nameBody, msgs.nameBody('Service names'))
},
affinityRule: {
key: yup
name: yup
.string()
.required(msgs.required('Key'))
.matches(matches.nameStart, msgs.nameStart('Key'))
@ -91,8 +91,8 @@ const Schemas = {
name: yup
.string()
.required()
.matches(matches.nameStart, msgs.nameStart('Snapshot name'))
.matches(matches.nameBody, msgs.nameBody('Snapshot Name'))
.matches(matches.nameStart, msgs.nameStart('Snapshot names'))
.matches(matches.nameBody, msgs.nameBody('Snapshot names'))
}
};

View File

@ -15,6 +15,10 @@ const Divider = styled(Row)`
${is('vertical')`
transform: rotate(90deg);
`};
${is('error')`
background-color: ${props => props.theme.red};
`};
`;
export default Baseline(Divider);