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

View File

@ -57,7 +57,7 @@ export const Footer = ({ enabled, submitting, onToggle }) => (
</Margin> </Margin>
{enabled ? ( {enabled ? (
<Margin bottom={4}> <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> </Margin>
) : null} ) : null}
</Fragment> </Fragment>
@ -93,7 +93,6 @@ export const AddServiceForm = ({
onBlur={null} onBlur={null}
type="text" type="text"
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required
disabled={disabled || submitting} disabled={disabled || submitting}
/> />
<FormMeta /> <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", "tag",
" ", " ",
"key “", "key “",
"two",
"\\" and the instance tag value", "\\" and the instance tag value",
" ", " ",
"equalling", "equalling",

View File

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

View File

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

View File

@ -140,8 +140,9 @@ export const Package = ({
{GroupIcons[group]} {GroupIcons[group]}
<Margin left={1} right={2}> <Margin left={1} right={2}>
<FormLabel <FormLabel
noMargin
style={{ fontWeight: sortBy === 'name' ? 'bold' : 'normal' }} style={{ fontWeight: sortBy === 'name' ? 'bold' : 'normal' }}
noMargin
actionable
> >
{name} {name}
</FormLabel> </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}> <Container {...rest}>
<Flex> <Flex>
<Margin right={1}> <Margin right={1}>
@ -24,7 +24,7 @@ export default ({ icon, children, collapsed = true, ...rest }) => (
<Small noMargin>{children}</Small> <Small noMargin>{children}</Small>
</Flex> </Flex>
<Margin top={1} bottom={collapsed ? 7 : 3}> <Margin top={1} bottom={collapsed ? 7 : 3}>
<Divider height={remcalc(1)} /> <Divider height={remcalc(1)} error={invalid} />
</Margin> </Margin>
</Container> </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_ADD_OPEN: 'INSTANCE_CREATION_TAG_VALUE_ADD_OPEN',
IC_TAG_V_TAGS: 'INSTANCE_CREATION_TAG_VALUE_TAGS', IC_TAG_V_TAGS: 'INSTANCE_CREATION_TAG_VALUE_TAGS',
IC_US_V_PROCEEDED: 'INSTANCE_CREATION_USERSCRIPT_VALUE_PROCEEDED', 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', placement: 'same',
type: 'name', type: 'name',
pattern: 'equalling', pattern: 'equalling',
key: '', name: '',
value: '' value: ''
}; };
@ -75,7 +75,7 @@ export const Affinity = ({
destroyOnUnmount={false} destroyOnUnmount={false}
forceUnregisterOnUnmount={false} forceUnregisterOnUnmount={false}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
asyncValidation={handleAsyncValidate} asyncValidate={handleAsyncValidate}
onSubmit={handleUpdateAffinityRule} onSubmit={handleUpdateAffinityRule}
> >
{formProps => {formProps =>
@ -185,7 +185,7 @@ export default compose(
}, },
handleAsyncValidate: ({ type, ...aff }) => { handleAsyncValidate: ({ type, ...aff }) => {
return type === 'name' return type === 'name'
? validateRule({ ...aff, key: 'default', type }) ? validateRule({ ...aff, type, name: 'default' })
: validateRule({ ...aff, type }); : validateRule({ ...aff, type });
}, },
handleEdit: () => { handleEdit: () => {

View File

@ -48,7 +48,7 @@ const CNSContainer = ({
{expanded ? ( {expanded ? (
<Description> <Description>
Triton CNS is used to automatically update hostnames for your 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.{' '} addresses) under the same hostname by matching the CNS service names.{' '}
<a <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"

View File

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

View File

@ -13,15 +13,17 @@ import { NameIcon, H3, Button } from 'joyent-ui-toolkit';
import Title from '@components/create-instance/title'; import Title from '@components/create-instance/title';
import Name from '@components/create-instance/name'; import Name from '@components/create-instance/name';
import Description from '@components/description'; import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql';
import { instanceName as validateName } from '@state/validators'; import { instanceName as validateName } from '@state/validators';
import createClient from '@state/apollo-client'; 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'; import { Forms, Values } from '@root/constants';
const { IC_NAME_F } = Forms; const { IC_NAME_F } = Forms;
const { IC_NAME_V_PROCEEDED, IC_NAME_V_RANDOMIZING } = Values; const { IC_NAME_V_PROCEEDED, IC_NAME_V_RANDOMIZING } = Values;
const NameContainer = ({ const NameContainer = ({
invalid,
expanded, expanded,
proceeded, proceeded,
name, name,
@ -40,6 +42,7 @@ const NameContainer = ({
onClick={!expanded && !proceeded && handleEdit} onClick={!expanded && !proceeded && handleEdit}
collapsed={!expanded && !proceeded} collapsed={!expanded && !proceeded}
icon={<NameIcon />} icon={<NameIcon />}
invalid={!expanded && invalid}
> >
Instance name Instance name
</Title> </Title>
@ -99,11 +102,12 @@ export default compose(
connect( connect(
({ form, values }, ownProps) => { ({ form, values }, ownProps) => {
const name = get(form, `${IC_NAME_F}.values.name`, ''); 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 randomizing = get(values, IC_NAME_V_RANDOMIZING, false);
const proceeded = get(values, IC_NAME_V_PROCEEDED, false); const proceeded = get(values, IC_NAME_V_PROCEEDED, false);
return { return {
...ownProps, invalid,
proceeded: proceeded || name.length, proceeded: proceeded || name.length,
randomizing, randomizing,
name 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 <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -684,7 +684,6 @@ exports[`renders <Cns /> without throwing 1`] = `
id="k" id="k"
onBlur={null} onBlur={null}
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required={true}
/> />
</div> </div>
</div> </div>
@ -784,7 +783,7 @@ exports[`renders <Cns /> without throwing 1`] = `
<p <p
className="c5" className="c5"
> >
*All hostnames listed here will be confirmed after deployment. Please note: All hostnames listed here will be confirmed after deployment.
</p> </p>
</div> </div>
</div> </div>
@ -1061,7 +1060,7 @@ exports[`renders <Cns disabled /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -1918,7 +1917,7 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -2218,7 +2217,6 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
id="u" id="u"
onBlur={null} onBlur={null}
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required={true}
/> />
</div> </div>
</div> </div>
@ -2720,7 +2718,7 @@ exports[`renders <Cns hostnames /> without throwing 1`] = `
<p <p
className="c5" className="c5"
> >
*All hostnames listed here will be confirmed after deployment. Please note: All hostnames listed here will be confirmed after deployment.
</p> </p>
</div> </div>
</div> </div>
@ -2910,7 +2908,7 @@ exports[`renders <Cns loading /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -3469,7 +3467,7 @@ exports[`renders <Cns loadingError /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -3592,7 +3590,6 @@ exports[`renders <Cns loadingError /> without throwing 1`] = `
id="m" id="m"
onBlur={null} onBlur={null}
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required={true}
/> />
</div> </div>
</div> </div>
@ -4543,7 +4540,7 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -4903,7 +4900,6 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
id="p" id="p"
onBlur={null} onBlur={null}
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required={true}
/> />
</div> </div>
</div> </div>
@ -5405,7 +5401,7 @@ exports[`renders <Cns mutating /> without throwing 1`] = `
<p <p
className="c5" className="c5"
> >
*All hostnames listed here will be confirmed after deployment. Please note: All hostnames listed here will be confirmed after deployment.
</p> </p>
</div> </div>
</div> </div>
@ -6040,7 +6036,7 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -6163,7 +6159,6 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
id="n" id="n"
onBlur={null} onBlur={null}
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required={true}
/> />
</div> </div>
</div> </div>
@ -6263,7 +6258,7 @@ exports[`renders <Cns mutationError /> without throwing 1`] = `
<p <p
className="c5" className="c5"
> >
*All hostnames listed here will be confirmed after deployment. Please note: All hostnames listed here will be confirmed after deployment.
</p> </p>
</div> </div>
</div> </div>
@ -6920,7 +6915,7 @@ exports[`renders <Cns services /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"
@ -7135,7 +7130,6 @@ exports[`renders <Cns services /> without throwing 1`] = `
id="s" id="s"
onBlur={null} onBlur={null}
placeholder="Example: mySQLdb" placeholder="Example: mySQLdb"
required={true}
/> />
</div> </div>
</div> </div>
@ -7235,7 +7229,7 @@ exports[`renders <Cns services /> without throwing 1`] = `
<p <p
className="c5" className="c5"
> >
*All hostnames listed here will be confirmed after deployment. Please note: All hostnames listed here will be confirmed after deployment.
</p> </p>
</div> </div>
</div> </div>
@ -7512,7 +7506,7 @@ exports[`renders <Cns services hostnames /> without throwing 1`] = `
<p <p
className="c5" 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 <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"

View File

@ -48,7 +48,7 @@ const CnsContainer = ({
<Margin bottom={1}> <Margin bottom={1}>
<Description> <Description>
Triton CNS is used to automatically update hostnames for your 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.{' '} addresses) under the same hostname by matching the CNS service names.{' '}
<a <a
href="https://docs.joyent.com/private-cloud/install/cns" href="https://docs.joyent.com/private-cloud/install/cns"

View File

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

View File

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

View File

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