feat: improved validation of attrs

This commit is contained in:
Sérgio Ramos 2018-03-01 01:15:16 +00:00 committed by Sérgio Ramos
parent 9d10a3fa92
commit cd242d7505
38 changed files with 580 additions and 526 deletions

View File

@ -13,7 +13,7 @@
"build:lib": "NODE_ENV=production redrun -p build:es build:umd",
"build:bundle": "echo 0",
"prepublish": "NODE_ENV=production redrun build:lib",
"lint": "redrun lint:ci -- -- --fix",
"lint": "redrun lint:ci -- --fix",
"lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test:ci": "redrun test",

View File

@ -13,7 +13,7 @@
"build:lib": "NODE_ENV=production redrun -p build:es build:umd",
"build:bundle": "echo 0",
"prepublish": "NODE_ENV=production redrun build:lib",
"lint": "redrun lint:ci -- -- --fix",
"lint": "redrun lint:ci -- --fix",
"lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "echo 0",
"test:ci": "redrun test",

View File

@ -11,9 +11,9 @@
"build:lib": "echo 0",
"build:bundle": "NODE_ENV=production redrun -p build:frontend build:ssr",
"prepublish": "NODE_ENV=production redrun build:bundle",
"lint": "redrun lint:ci -- -- --fix",
"lint": "redrun lint:ci -- --fix",
"lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "redrun test:ci",
"test": "echo 0",
"test:ci": "echo 0",
"build:frontend": "joyent-react-scripts build",
"build:ssr": "SSR=1 UMD=1 babel src --out-dir lib/app --copy-files"
@ -42,7 +42,6 @@
"lodash.uniqby": "^4.7.0",
"lunr": "^2.1.6",
"param-case": "^2.1.1",
"punycode": "^2.1.0",
"react": "^16.2.0",
"react-apollo": "^2.0.4",
"react-dom": "^16.2.0",

View File

@ -2,13 +2,12 @@ import React, { Fragment } from 'react';
import { compose, graphql } from 'react-apollo';
import { set } from 'react-redux-values';
import ReduxForm from 'declarative-redux-form';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Margin } from 'styled-components-spacing';
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 { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { NameIcon, H3, Button, H4, P } from 'joyent-ui-toolkit';
@ -16,7 +15,8 @@ import Title from '@components/create-image/title';
import Details from '@components/create-image/details';
import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql';
import createStore from '@state/apollo-client';
import createClient from '@state/apollo-client';
import { instanceName as validateName } from '@state/validators';
import { Forms } from '@root/constants';
const NameContainer = ({
@ -27,7 +27,7 @@ const NameContainer = ({
description,
placeholderName,
randomizing,
handleAsyncValidation,
handleAsyncValidate,
shouldAsyncValidate,
handleNext,
handleRandomize,
@ -54,9 +54,9 @@ const NameContainer = ({
form={Forms.FORM_DETAILS}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleNext}
asyncValidate={handleAsyncValidation}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleNext}
>
{props =>
expanded ? (
@ -121,6 +121,7 @@ export default compose(
({ form, values }, ownProps) => {
const name = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const version = get(form, `${Forms.FORM_DETAILS}.values.version`, '');
const description = get(
form,
`${Forms.FORM_DETAILS}.values.description`,
@ -128,7 +129,6 @@ export default compose(
);
const proceeded = get(values, `${Forms.FORM_DETAILS}-proceeded`, false);
const randomizing = get(values, 'create-image-name-randomizing', false);
return {
@ -141,35 +141,23 @@ export default compose(
};
},
(dispatch, { history, match }) => ({
shouldAsyncValidate: ({ trigger }) => trigger === 'submit',
handleAsyncValidation: async ({ name }) => {
const sanitized = punycode.encode(name).replace(/-$/, '');
if (sanitized !== name) {
// eslint-disable-next-line no-throw-literal
throw {
name: 'Special characters are not accepted'
};
}
if (!/^[a-zA-Z0-9][a-zA-Z0-9\\_\\.\\-]*$/.test(name)) {
// eslint-disable-next-line no-throw-literal
throw {
name: 'Invalid name'
};
}
},
handleNext: () => {
dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true }));
return history.push(`/~create/${match.params.instance}/tag`);
},
handleEdit: () => history.push(`/~create/${match.params.instance}/name`),
handleEdit: () => {
dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true }));
return history.push(`/~create/${match.params.instance}/name`);
},
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'change';
},
handleAsyncValidate: validateName,
handleRandomize: async () => {
dispatch(set({ name: 'create-image-name-randomizing', value: true }));
const [err, res] = await intercept(
createStore().query({
createClient().query({
fetchPolicy: 'network-only',
query: GetRandomName
})

View File

@ -21,6 +21,7 @@ import {
import Title from '@components/create-image/title';
import Description from '@components/description';
import Tag from '@components/tags';
import { addTag as validateTag } from '@state/validators';
import { Forms } from '@root/constants';
export const Tags = ({
@ -34,6 +35,8 @@ export const Tags = ({
handleToggleExpanded,
handleCancelEdit,
handleChangeAddOpen,
handleAsyncValidate,
shouldAsyncValidate,
handleNext,
step,
handleEdit,
@ -84,6 +87,8 @@ export const Tags = ({
form={Forms.FORM_TAGS_CREATE}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleAddTag}
>
{props =>
@ -141,6 +146,10 @@ export default compose(
handleEdit: () => {
return history.push(`/~create/${match.params.instance}/tag`);
},
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleAddTag: value => {
const toggleToClosed = set({
name: `${Forms.CREATE_TAGS}-add-open`,

View File

@ -27,6 +27,7 @@ import Tag, { AddForm } from '@components/tags';
import ToolbarForm from '@components/toolbar';
import UpdateImageTags from '@graphql/update-image-tags.gql';
import GetTags from '@graphql/get-tags.gql';
import { addTag as validateTag } from '@state/validators';
import parseError from '@state/parse-error';
const { TAGS_TOOLBAR_FORM, TAGS_ADD_FORM } = Forms;
@ -38,6 +39,8 @@ export const Tags = ({
error = null,
mutationError = null,
mutating = false,
handleAsyncValidate,
shouldAsyncValidate,
handleToggleAddOpen,
handleRemoveTag,
handleAddTag
@ -76,7 +79,12 @@ export const Tags = ({
</Message>
</Margin>
) : null}
<ReduxForm form={TAGS_ADD_FORM} onSubmit={handleAddTag}>
<ReduxForm
form={TAGS_ADD_FORM}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleAddTag}
>
{props =>
addOpen ? (
<Margin bottom={4}>
@ -162,6 +170,10 @@ export default compose(
};
},
(dispatch, { image, tags = [], updateTags, refetch }) => ({
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleToggleAddOpen: addOpen => {
dispatch(set({ name: `${image.id}-add-open`, value: addOpen }));
},

View File

@ -0,0 +1,67 @@
import intercept from 'apr-intercept';
import keys from 'lodash.keys';
import reduce from 'apr-reduce';
import assign from 'lodash.assign';
import yup from 'yup';
/*****************************************************************************/
const validateField = async (field, value) => {
const [err] = await intercept(field.validate(value));
return err ? err.errors.shift() : '';
};
const validateSchema = async (schema, value) => {
const errors = await reduce(
keys(schema),
async (errors, name) =>
assign(errors, {
[name]: await validateField(schema[name], value[name])
}),
{}
);
throw errors;
};
/*****************************************************************************/
const matches = {
nameStart: /^[a-zA-Z]|\d/,
nameBody: /^([a-zA-Z]|\d|\.|_|-)+$/
};
const msgs = {
required: prefix => `${prefix} must be defined.`,
nameStart: prefix => `${prefix} can only start with letters and numbers.`,
nameBody: prefix =>
`${prefix} cannot contain spaces and can only contain letters, numbers, periods (.), underscores (_), and hyphens (-).`
};
const Schemas = {
tag: {
name: yup
.string()
.required(msgs.required('Key'))
.matches(matches.nameStart, msgs.nameStart('Key'))
.matches(matches.nameBody, msgs.nameBody('Key')),
value: yup
.string()
.required(msgs.required('Value'))
.matches(matches.nameStart, msgs.nameStart('Value'))
.matches(matches.nameBody, msgs.nameBody('Value'))
},
instanceName: {
name: yup
.string()
.matches(matches.nameStart, msgs.nameStart('Instance name'))
.matches(matches.nameBody, msgs.nameBody('Instance Name'))
}
};
/*****************************************************************************/
export const addTag = tag => validateSchema(Schemas.tag, tag);
export const instanceName = ({ name }) =>
!name ? null : validateSchema(Schemas.instanceName, { name });

View File

@ -11,7 +11,7 @@
"build:lib": "echo 0",
"build:bundle": "NODE_ENV=production redrun -p build:frontend build:ssr",
"prepublish": "NODE_ENV=production redrun build:bundle",
"lint": "redrun lint:ci -- -- --fix",
"lint": "redrun lint:ci -- --fix",
"lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "DEFAULT_TIMEOUT_INTERVAL=100000 NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test:ci": "NODE_ENV=test joyent-react-scripts test --env=jsdom --testPathIgnorePatterns='.ui.js'",
@ -22,6 +22,7 @@
"@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2",
"apr-intercept": "^3.0.3",
"apr-reduce": "^3.0.3",
"bytes": "^3.0.0",
"clipboard-copy": "^1.4.2",
"constant-case": "^2.0.0",
@ -47,13 +48,13 @@
"lodash.isfunction": "^3.0.9",
"lodash.isinteger": "^4.0.4",
"lodash.omit": "^4.5.0",
"lodash.reduce": "^4.6.0",
"lodash.reverse": "^4.0.1",
"lodash.some": "^4.6.0",
"lodash.sortby": "^4.7.0",
"lodash.uniqby": "^4.7.0",
"lodash.values": "^4.3.0",
"param-case": "^2.1.1",
"punycode": "^2.1.0",
"query-string": "^5.1.0",
"react": "^16.2.0",
"react-apollo": "^2.0.4",
@ -68,7 +69,8 @@
"styled-components": "^3.1.6",
"styled-components-spacing": "^2.1.3",
"styled-flex-component": "^2.2.1",
"title-case": "^2.1.1"
"title-case": "^2.1.1",
"yup": "^0.24.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -466,11 +466,6 @@ exports[`renders <AddServiceForm pristine /> without throwing 1`] = `
background-color: rgb(59,70,204);
border-radius: 0.25rem;
border: solid 0.0625rem rgb(45,56,132);
cursor: not-allowed;
pointer-events: none;
color: rgb(216,216,216);
background-color: rgb(250,250,250);
border-color: rgb(216,216,216);
}
.c7:focus {
@ -500,23 +495,6 @@ exports[`renders <AddServiceForm pristine /> without throwing 1`] = `
pointer-events: none;
}
.c7:focus {
background-color: rgb(250,250,250);
border-color: rgb(216,216,216);
}
.c7:hover {
background-color: rgb(250,250,250);
border-color: rgb(250,250,250);
}
.c7:active,
.c7:active:hover,
.c7:active:focus {
background-color: rgb(250,250,250);
border-color: rgb(250,250,250);
}
.c3 {
font-size: 0.9375rem;
line-height: 1.125rem;
@ -678,7 +656,7 @@ exports[`renders <AddServiceForm pristine /> without throwing 1`] = `
>
<button
className="c7 c8 c9"
disabled={true}
disabled={undefined}
href=""
type="submit"
>

View File

@ -104,7 +104,7 @@ export const AddServiceForm = ({
<Margin top={3.5} left={2}>
<Button
type="submit"
disabled={pristine || invalid}
disabled={submitting || invalid}
loading={submitting}
inline
>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -12,15 +12,13 @@ Array [
:
</span>,
" be on a",
" ",
" be on a ",
" node as the instance(s) identified by the instance ",
" ",
"key “",
"\\" and the instance tag value",
" ",
" ",
"\\"",
" \\"",
"”",
]
`;
@ -37,22 +35,20 @@ Array [
:
</span>,
" be on a",
" ",
" be on a ",
" node as the instance(s) identified by the instance ",
" ",
"key “",
"\\" and the instance tag value",
" ",
" ",
"\\"",
" \\"",
"”",
]
`;
exports[`renders <Rule/> without throwing 1`] = `
.c0 {
margin-bottom: 1.5rem;
margin-bottom: 3rem;
}
.c12 {
@ -667,8 +663,8 @@ exports[`renders <Rule/> without throwing 1`] = `
className="c2"
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
>
@ -676,7 +672,7 @@ exports[`renders <Rule/> without throwing 1`] = `
</h4>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-conditional"
name="conditional"
role="group"
style={undefined}
>
@ -685,8 +681,8 @@ exports[`renders <Rule/> without throwing 1`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="4.125rem"
@ -698,8 +694,8 @@ exports[`renders <Rule/> without throwing 1`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="4.125rem"
@ -721,8 +717,8 @@ exports[`renders <Rule/> without throwing 1`] = `
className="c2"
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
>
@ -730,7 +726,7 @@ exports[`renders <Rule/> without throwing 1`] = `
</h4>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-placement"
name="placement"
role="group"
style={undefined}
>
@ -739,8 +735,8 @@ exports[`renders <Rule/> without throwing 1`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="6.25rem"
@ -752,8 +748,8 @@ exports[`renders <Rule/> without throwing 1`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="6.25rem"
@ -775,8 +771,8 @@ exports[`renders <Rule/> without throwing 1`] = `
className="c2"
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
>
@ -784,7 +780,7 @@ exports[`renders <Rule/> without throwing 1`] = `
</h4>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-type"
name="type"
role="group"
style={undefined}
>
@ -793,8 +789,8 @@ exports[`renders <Rule/> without throwing 1`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.4375rem"
@ -806,8 +802,8 @@ exports[`renders <Rule/> without throwing 1`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.4375rem"
@ -827,7 +823,7 @@ exports[`renders <Rule/> without throwing 1`] = `
</div>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-name-pattern"
name="pattern"
role="group"
style={undefined}
>
@ -839,8 +835,8 @@ exports[`renders <Rule/> without throwing 1`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.125rem"
@ -852,8 +848,8 @@ exports[`renders <Rule/> without throwing 1`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.125rem"
@ -889,7 +885,7 @@ exports[`renders <Rule/> without throwing 1`] = `
</div>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-name"
name="value"
role="group"
style={undefined}
>
@ -902,8 +898,8 @@ exports[`renders <Rule/> without throwing 1`] = `
required={true}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
/>
@ -914,7 +910,7 @@ exports[`renders <Rule/> without throwing 1`] = `
exports[`renders <Rule/> without throwing 2`] = `
.c0 {
margin-bottom: 1.5rem;
margin-bottom: 3rem;
}
.c12 {
@ -1529,8 +1525,8 @@ exports[`renders <Rule/> without throwing 2`] = `
className="c2"
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
>
@ -1538,7 +1534,7 @@ exports[`renders <Rule/> without throwing 2`] = `
</h4>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-conditional"
name="conditional"
role="group"
style={undefined}
>
@ -1547,8 +1543,8 @@ exports[`renders <Rule/> without throwing 2`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="4.125rem"
@ -1560,8 +1556,8 @@ exports[`renders <Rule/> without throwing 2`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="4.125rem"
@ -1583,8 +1579,8 @@ exports[`renders <Rule/> without throwing 2`] = `
className="c2"
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
>
@ -1592,7 +1588,7 @@ exports[`renders <Rule/> without throwing 2`] = `
</h4>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-placement"
name="placement"
role="group"
style={undefined}
>
@ -1601,8 +1597,8 @@ exports[`renders <Rule/> without throwing 2`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="6.25rem"
@ -1614,8 +1610,8 @@ exports[`renders <Rule/> without throwing 2`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="6.25rem"
@ -1637,8 +1633,8 @@ exports[`renders <Rule/> without throwing 2`] = `
className="c2"
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
>
@ -1646,7 +1642,7 @@ exports[`renders <Rule/> without throwing 2`] = `
</h4>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-type"
name="type"
role="group"
style={undefined}
>
@ -1655,8 +1651,8 @@ exports[`renders <Rule/> without throwing 2`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.4375rem"
@ -1668,8 +1664,8 @@ exports[`renders <Rule/> without throwing 2`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.4375rem"
@ -1689,7 +1685,7 @@ exports[`renders <Rule/> without throwing 2`] = `
</div>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-name-pattern"
name="pattern"
role="group"
style={undefined}
>
@ -1701,8 +1697,8 @@ exports[`renders <Rule/> without throwing 2`] = `
disabled={false}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.125rem"
@ -1714,8 +1710,8 @@ exports[`renders <Rule/> without throwing 2`] = `
onBlur={undefined}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
width="8.125rem"
@ -1751,7 +1747,7 @@ exports[`renders <Rule/> without throwing 2`] = `
</div>
<div
className="baseline-jVaZNU kXgQxt c3"
name="rule-instance-name"
name="value"
role="group"
style={undefined}
>
@ -1764,8 +1760,8 @@ exports[`renders <Rule/> without throwing 2`] = `
required={true}
style={
Object {
"fontSize": "18px",
"lineHeight": "48px",
"fontSize": "1.125rem",
"lineHeight": "3rem",
}
}
/>

View File

@ -24,14 +24,12 @@ it('renders <Rule/> without throwing', () => {
<Theme>
<Rule
rule={{
'rule-instance-name': 'test',
'rule-instance-conditional': 'must',
'rule-instance-placement': 'same',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': '',
'rule-instance-tag-key': '',
'rule-type': 'name'
conditional: 'must',
placement: 'same',
pattern: 'equalling',
value: 'test',
key: '',
type: 'name'
}}
/>
</Theme>
@ -47,14 +45,12 @@ it('renders <Header /> without throwing', () => {
<Theme>
<Header
rule={{
'rule-instance-name': 'test',
'rule-instance-conditional': 'must',
'rule-instance-placement': 'same',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': '',
'rule-instance-tag-key': '',
'rule-type': 'name'
conditional: 'must',
placement: 'same',
pattern: 'equalling',
value: 'test',
key: '',
type: 'name'
}}
/>
</Theme>
@ -70,14 +66,12 @@ it('renders <Header tag/> without throwing', () => {
<Theme>
<Header
rule={{
'rule-instance-name': 'test',
'rule-instance-conditional': 'must',
'rule-instance-placement': 'same',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': 'one',
'rule-instance-tag-key': 'two',
'rule-type': 'tag'
conditional: 'must',
placement: 'same',
pattern: 'equalling',
value: 'one',
key: 'two',
type: 'tag'
}}
/>
</Theme>

View File

@ -25,14 +25,12 @@ it('<Rule/>', async () => {
<Theme ss>
<Rule
rule={{
'rule-instance-name': 'test',
'rule-instance-conditional': 'must',
'rule-instance-placement': 'same',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': '',
'rule-instance-tag-key': '',
'rule-type': 'name'
conditional: 'must',
placement: 'same',
pattern: 'equalling',
value: 'test',
key: '',
type: 'name'
}}
/>
</Theme>
@ -46,14 +44,12 @@ it('<Header />', async () => {
<Theme ss>
<Header
rule={{
'rule-instance-name': 'test',
'rule-instance-conditional': 'must',
'rule-instance-placement': 'same',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': '',
'rule-instance-tag-key': '',
'rule-type': 'name'
conditional: 'must',
placement: 'same',
pattern: 'equalling',
value: 'test',
key: '',
type: 'name'
}}
/>
</Theme>
@ -67,14 +63,12 @@ it('<Header tag/>', async () => {
<Theme ss>
<Header
rule={{
'rule-instance-name': 'test',
'rule-instance-conditional': 'must',
'rule-instance-placement': 'same',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': 'one',
'rule-instance-tag-key': 'two',
'rule-type': 'tag'
conditional: 'must',
placement: 'same',
pattern: 'equalling',
value: 'one',
key: 'two',
type: 'tag'
}}
/>
</Theme>

View File

@ -9,8 +9,8 @@ import remcalc from 'remcalc';
import { H5, Select, Input, FormGroup, FormMeta } from 'joyent-ui-toolkit';
const style = {
lineHeight: '48px',
fontSize: '18px'
lineHeight: remcalc(48),
fontSize: remcalc(18)
};
const Bold = styled.span`
@ -19,7 +19,7 @@ const Bold = styled.span`
const Values = touched => (
<Margin right={1}>
<Select style={style} touched={touched} embedded width={remcalc(130)}>
<Select style={style} touched={touched} width={remcalc(130)} embedded>
<option value="equalling">equalling</option>
<option value="not-equalling">not equalling</option>
<option value="containing">containing</option>
@ -29,16 +29,16 @@ const Values = touched => (
</Margin>
);
export const Rule = rule => (
<Margin bottom={4}>
export const Rule = ({ valid, ...rule }) => (
<Margin bottom={valid ? 4 : 8}>
<Flex alignCenter wrap>
<H5 style={style} inline noMargin>
The instance
</H5>
<FormGroup name="rule-instance-conditional" field={Field}>
<FormGroup name="conditional" field={Field}>
<Select
style={style}
touched={rule['rule-instance-conditional']}
touched={rule.conditional}
width={remcalc(66)}
embedded
>
@ -49,10 +49,10 @@ export const Rule = rule => (
<H5 style={style} inline noMargin>
be on
</H5>
<FormGroup name="rule-instance-placement" field={Field}>
<FormGroup name="placement" field={Field}>
<Select
style={style}
touched={rule['rule-instance-placement']}
touched={rule.placement}
width={remcalc(100)}
embedded
>
@ -63,10 +63,10 @@ export const Rule = rule => (
<H5 style={style} inline noMargin>
node as the instance(s) identified by the
</H5>
<FormGroup name="rule-type" field={Field}>
<FormGroup name="type" field={Field}>
<Select
style={style}
touched={rule['rule-type']}
touched={rule.type}
width={remcalc(135)}
embedded
left
@ -75,54 +75,53 @@ export const Rule = rule => (
<option value="tag">tag</option>
</Select>
</FormGroup>
{rule['rule-type'] === 'tag' ? (
{rule.type === 'tag' ? (
<Fragment>
<FormGroup name="rule-instance-tag-key" field={Field}>
<FormGroup name="key" field={Field}>
<Input
style={style}
onBlur={null}
type="text"
placeholder="key"
small
embedded
type="text"
required
placeholder="key"
/>
<FormMeta small />
<FormMeta small absolute />
</FormGroup>
<H5 style={style} inline noMargin>
and value{' '}
</H5>
<FormGroup name="rule-instance-tag-value-pattern" field={Field}>
{Values(rule['rule-instance-tag-value-pattern'])}
<FormGroup name="pattern" field={Field}>
{Values(rule.pattern)}
</FormGroup>
<FormGroup name="rule-instance-tag-value" field={Field}>
<FormGroup name="value" field={Field}>
<Input
style={style}
onBlur={null}
small
embedded
type="text"
required
placeholder="value"
embedded
required
/>
<FormMeta small />
<FormMeta small absolute />
</FormGroup>
</Fragment>
) : (
<Fragment>
<FormGroup name="rule-instance-name-pattern" field={Field}>
{Values(rule['rule-instance-name-pattern'])}
<FormGroup name="pattern" field={Field}>
{Values(rule.pattern)}
</FormGroup>
<FormGroup name="rule-instance-name" field={Field}>
<FormGroup name="value" field={Field}>
<Input
onBlur={null}
embedded
style={style}
type="text"
required
placeholder="Example instance name: nginx"
embedded
required
/>
<FormMeta />
<FormMeta absolute />
</FormGroup>
</Fragment>
)}
@ -132,21 +131,18 @@ export const Rule = rule => (
export const Header = rule => (
<Fragment>
<Bold>{titleCase(rule['rule-instance-conditional'])}:</Bold> be on a{' '}
{rule['rule-instance-placement']} node as the instance(s) identified by the
instance {rule['rule-type']}
{rule['rule-type'] === 'name' ? (
<Bold>{titleCase(rule.conditional)}:</Bold> be on a {rule.placement} node as
the instance(s) identified by the instance {rule.type}
{rule.type === 'name' ? (
<Fragment>
{' '}
{rule['rule-instance-name-pattern']} {rule['rule-instance-name']}
{rule.pattern} {rule.value}
</Fragment>
) : (
<Fragment>
{' '}
key {rule['rule-instance-tag-key']}" and the instance tag value{' '}
{rule['rule-instance-tag-value-pattern'] &&
rule['rule-instance-tag-value-pattern'].split('-').join(' ')}{' '}
"{rule['rule-instance-tag-value']}
key {rule.key}" and the instance tag value{' '}
{rule.pattern && rule.pattern.split('-').join(' ')} "{rule.value}
</Fragment>
)}
</Fragment>

View File

@ -1,2 +0,0 @@
export const fieldError =
'Please enter only letters, numbers, periods (.), underscores (_), and hyphens (-).';

View File

@ -8,50 +8,48 @@ import { connect } from 'react-redux';
import get from 'lodash.get';
import remcalc from 'remcalc';
import { AffinityIcon, Button, H3, Divider, KeyValue } from 'joyent-ui-toolkit';
import { AffinityIcon, Button, Divider, KeyValue } from 'joyent-ui-toolkit';
import Title from '@components/create-instance/title';
import { Rule, Header } from '@components/create-instance/affinity';
import Description from '@components/description';
import { fieldError } from '@root/constants';
import { addAffinityRule as validateRule } from '@state/validators';
const FORM_NAME_CREATE = 'CREATE-INSTANCE-AFFINITY-ADD';
const FORM_NAME_EDIT = i => `CREATE-INSTANCE-AFFINITY-EDIT-${i}`;
const FORM_NAME_EDIT = 'CREATE-INSTANCE-AFFINITY-EDIT';
const RULE_DEFAULTS = {
'rule-instance-name': '',
'rule-instance-conditional': 'should',
'rule-instance-placement': 'same',
'rule-instance-tag-key-pattern': 'equalling',
'rule-instance-tag-value-pattern': 'equalling',
'rule-instance-name-pattern': 'equalling',
'rule-instance-tag-value': '',
'rule-instance-tag-key': '',
'rule-type': 'name'
conditional: 'should',
placement: 'same',
type: 'name',
pattern: 'equalling',
key: '',
value: ''
};
export const Affinity = ({
affinityRules = [],
step,
expanded,
proceeded,
addOpen,
handleAddAffinityRules,
editOpen,
editingRule,
creatingRule,
exitingRule,
shouldAsyncValidate,
handleAsyncValidate,
handleCreateAffinityRules,
handleRemoveAffinityRule,
handleUpdateAffinityRule,
shouldAsyncValidate,
handleAsyncValidation,
handleToggleExpanded,
handleCancelEdit,
handleChangeAddOpen,
handleEdit,
rule,
step
handleEdit
}) => (
<Fragment>
<Title
id={step}
onClick={!expanded && !proceeded && handleEdit}
collapsed={!expanded && !proceeded}
onClick={!expanded && !exitingRule && handleEdit}
collapsed={!expanded && !exitingRule}
icon={<AffinityIcon />}
>
Affinity
@ -70,55 +68,64 @@ export const Affinity = ({
</a>
</Description>
) : null}
{proceeded ? (
<Margin bottom={4}>
<H3>{affinityRules.length} Affinity Rule</H3>
</Margin>
) : null}
{affinityRules.map((rule, index) => (
<ReduxForm
form={FORM_NAME_EDIT(index)}
key={index}
initialValues={rule}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidation={handleAsyncValidation}
onSubmit={newValue => handleUpdateAffinityRule(index, newValue)}
>
{props => (
<ReduxForm
form={FORM_NAME_EDIT}
initialValues={exitingRule}
destroyOnUnmount={false}
forceUnregisterOnUnmount={false}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidation={handleAsyncValidate}
onSubmit={handleUpdateAffinityRule}
>
{formProps =>
exitingRule ? (
<Fragment>
<KeyValue
{...props}
expanded={rule.expanded}
customHeader={<Header {...rule} />}
{...formProps}
expanded={editOpen}
customHeader={<Header {...exitingRule} />}
method="edit"
input={props => <Rule {...rule} {...props} />}
input={inputProps => (
<Rule
{...editingRule}
{...inputProps}
valid={formProps.valid}
/>
)}
type="an affinity rule"
onToggleExpanded={() => handleToggleExpanded(index)}
onCancel={() => handleCancelEdit(index)}
onRemove={() => handleRemoveAffinityRule(index)}
onToggleExpanded={() =>
handleToggleExpanded(!exitingRule.expanded)
}
onCancel={handleCancelEdit}
onRemove={handleRemoveAffinityRule}
/>
<Divider height={remcalc(12)} transparent />
</Fragment>
)}
</ReduxForm>
))}
) : null
}
</ReduxForm>
<ReduxForm
form={FORM_NAME_CREATE}
initialValues={RULE_DEFAULTS}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
forceUnregisterOnUnmount={false}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidation}
onSubmit={handleAddAffinityRules}
asyncValidate={handleAsyncValidate}
onSubmit={handleCreateAffinityRules}
>
{props =>
{formProps =>
expanded && addOpen ? (
<Fragment>
<KeyValue
{...props}
{...formProps}
method="create"
input={props => <Rule {...rule} {...props} />}
input={inputProps => (
<Rule
{...creatingRule}
{...inputProps}
valid={formProps.valid}
/>
)}
type="an affinity rule"
expanded
noRemove
@ -131,7 +138,7 @@ export const Affinity = ({
</ReduxForm>
{expanded ? (
<Margin top={2} bottom={4}>
{!addOpen && affinityRules.length === 0 ? (
{!addOpen && !exitingRule ? (
<Button
type="button"
onClick={() => handleChangeAddOpen(true)}
@ -141,7 +148,7 @@ export const Affinity = ({
</Button>
) : null}
</Margin>
) : proceeded ? (
) : exitingRule ? (
<Margin top={2} bottom={4}>
<Button type="button" onClick={handleEdit} secondary>
Edit
@ -149,61 +156,45 @@ export const Affinity = ({
</Margin>
) : null}
<Margin bottom={7}>
{expanded || proceeded ? <Divider height={remcalc(1)} /> : null}
{expanded ? <Divider height={remcalc(1)} /> : null}
</Margin>
</Fragment>
);
export default compose(
connect(({ values, form }, ownProps) => {
const proceeded = get(values, 'create-instance-affinity-proceeded', false);
const editingRule = get(form, `${FORM_NAME_EDIT}.values`, null);
const creatingRule = get(form, `${FORM_NAME_CREATE}.values`, null);
const exitingRule = get(values, 'create-instance-affinity', null);
const addOpen = get(values, 'create-instance-affinity-add-open', false);
const affinityRules = get(values, 'create-instance-affinity', []);
const rule = get(form, `${FORM_NAME_CREATE}.values`, {});
const editOpen = get(values, 'create-instance-affinity-edit-open', false);
return {
proceeded: proceeded || affinityRules.length,
addOpen,
affinityRules,
rule
editOpen,
creatingRule,
editingRule,
exitingRule
};
}),
connect(null, (dispatch, { affinityRules = [], rule, history }) => ({
shouldAsyncValidate: ({ trigger }) => trigger === 'change',
handleAsyncValidation: async rule => {
const validName = /^[a-zA-Z_.-]{1,16}$/.test(rule['rule-instance-name']);
const validKey = /^[a-zA-Z_.-]{1,16}$/.test(
rule['rule-instance-tag-key']
);
const validValue = /^[a-zA-Z_.-]{1,16}$/.test(
rule['rule-instance-tag-value']
);
if (validName && validKey && validValue) {
return;
}
// eslint-disable-next-line no-throw-literal
throw {
'rule-instance-name': fieldError,
'rule-instance-tag-key': fieldError,
'rule-instance-tag-value': fieldError
};
connect(null, (dispatch, { history }) => ({
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateRule,
handleEdit: () => {
return history.push(`/~create/affinity${history.location.search}`);
},
handleAddAffinityRules: ({ ...rule }) => {
handleCreateAffinityRules: value => {
const toggleToClosed = set({
name: `create-instance-affinity-add-open`,
name: 'create-instance-affinity-add-open',
value: false
});
const appendAffinityRule = set({
name: `create-instance-affinity`,
value: affinityRules.concat([
{ ...RULE_DEFAULTS, ...rule, expanded: false }
])
name: 'create-instance-affinity',
value
});
return dispatch([
@ -212,53 +203,32 @@ export default compose(
appendAffinityRule
]);
},
handleUpdateAffinityRule: (index, newAffinityRule) => {
affinityRules[index] = {
...newAffinityRule,
expanded: false
};
handleUpdateAffinityRule: value => {
return dispatch([
destroy(FORM_NAME_EDIT(index)),
set({ name: `create-instance-affinity`, value: affinityRules.slice() })
destroy(FORM_NAME_EDIT),
set({ name: 'create-instance-affinity', value })
]);
},
handleChangeAddOpen: value => {
return dispatch([
reset(FORM_NAME_CREATE),
set({ name: `create-instance-affinity-add-open`, value })
set({ name: 'create-instance-affinity-add-open', value })
]);
},
handleToggleExpanded: index => {
affinityRules[index] = {
...affinityRules[index],
expanded: !affinityRules[index].expanded
};
handleToggleExpanded: value => {
return dispatch(
set({
name: `create-instance-affinity`,
value: affinityRules.slice()
})
set({ name: 'create-instance-affinity-edit-open', value })
);
},
handleCancelEdit: index => {
affinityRules[index] = {
...affinityRules[index],
expanded: false
};
handleCancelEdit: () => {
return dispatch([
reset(FORM_NAME_EDIT(index)),
set({ name: `create-instance-affinity`, value: affinityRules.slice() })
set({ name: 'create-instance-affinity-edit-open', value: false })
]);
},
handleRemoveAffinityRule: index => {
affinityRules.splice(index, 1);
handleRemoveAffinityRule: () => {
return dispatch([
destroy(FORM_NAME_EDIT(index)),
set({ name: `create-instance-affinity`, value: affinityRules.slice() })
destroy(FORM_NAME_EDIT),
set({ name: 'create-instance-affinity', value: null })
]);
}
}))

View File

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import get from 'lodash.get';
import { Margin } from 'styled-components-spacing';
import { set } from 'react-redux-values';
import punycode from 'punycode';
import { CnsIcon, H3, Button } from 'joyent-ui-toolkit';
@ -14,7 +13,7 @@ import Title from '@components/create-instance/title';
import Cns, { Footer, AddServiceForm } from '@components/cns';
import Description from '@components/description';
import GetAccount from '@graphql/get-account.gql';
import { fieldError } from '@root/constants';
import { addCnsService as validateServiceName } from '@state/validators';
const CNS_FORM = 'create-instance-cns';
@ -175,32 +174,18 @@ export default compose(
dispatch(set({ name: `${CNS_FORM}-proceeded`, value: true }));
history.push(`/~create/cns${history.location.search}`);
},
shouldAsyncValidate: ({ trigger }) => trigger === 'change',
handleAsyncValidate: async ({ name = '', value = '' }) => {
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
const isValueValid = /^[a-zA-Z_.-]{1,16}$/.test(value);
if (isNameValid && isValueValid) {
return;
}
throw {
name: isNameValid ? null : fieldError,
value: isValueValid ? null : fieldError
};
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateServiceName,
handleToggleCnsEnabled: ({ target }) =>
dispatch(set({ name: `${CNS_FORM}-enabled`, value: !cnsEnabled })),
handleAddService: ({ name }) => {
const serviceName = punycode
.encode(name.toLowerCase().replace(/\s/g, '-'))
.replace(/-$/, '');
dispatch([
destroy(`${CNS_FORM}-new-service`),
set({
name: `${CNS_FORM}-services`,
value: serviceNames.concat(serviceName)
value: serviceNames.concat(name)
})
]);
},

View File

@ -240,15 +240,8 @@ export default compose(
handleSubmit: async () => {
const _affinity = affinity
.map(aff => ({
conditional: aff['rule-instance-conditional'],
placement: aff['rule-instance-placement'],
identity: aff['rule-type'],
key: aff['rule-instance-tag-key'],
pattern: aff['rule-instance-tag-value-pattern'],
value:
aff['rule-type'] === 'name'
? aff['rule-instance-name']
: aff['rule-instance-tag-value']
...aff,
value: aff.type === 'name' ? aff.name : aff.value
}))
.map(({ conditional, placement, identity, key, pattern, value }) => {
const type = constantCase(

View File

@ -13,6 +13,7 @@ import Editor from 'joyent-ui-toolkit/dist/es/editor';
import Title from '@components/create-instance/title';
import Description from '@components/description';
import { addMetadata as validateMetadata } from '@state/validators';
const FORM_NAME_CREATE = 'CREATE-INSTANCE-METADATA-ADD';
const FORM_NAME_EDIT = i => `CREATE-INSTANCE-METADATA-EDIT-${i}`;
@ -179,19 +180,7 @@ export default compose(
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
asyncValidate: async ({ name = '', value = '' }) => {
const isNameInvalid = name.length === 0;
const isValueInvalid = value.length === 0;
if (!isNameInvalid && !isValueInvalid) {
return;
}
throw {
name: isNameInvalid,
value: isValueInvalid
};
},
handleAsyncValidate: validateMetadata,
handleAddMetadata: value => {
const toggleToClosed = set({
name: `create-instance-metadata-add-open`,

View File

@ -7,18 +7,15 @@ 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, 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 GetInstance from '@graphql/get-instance-small.gql';
import GetRandomName from '@graphql/get-random-name.gql';
import { instanceName as validateName } from '@state/validators';
import createClient from '@state/apollo-client';
import parseError from '@state/parse-error';
import { fieldError } from '@root/constants';
import GetRandomName from '@graphql/get-random-name.gql';
const FORM_NAME = 'create-instance-name';
@ -28,7 +25,7 @@ const NameContainer = ({
name,
placeholderName,
randomizing,
handleAsyncValidation,
handleAsyncValidate,
shouldAsyncValidate,
handleNext,
handleRandomize,
@ -54,7 +51,7 @@ const NameContainer = ({
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleNext}
asyncValidate={handleAsyncValidation}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
>
{props =>
@ -127,49 +124,10 @@ export default compose(
handleEdit: () => {
history.push(`/~create/name${history.location.search}`);
},
shouldAsyncValidate: ({ trigger }) => trigger === 'change',
handleAsyncValidation: async ({ name }) => {
const sanitized = punycode.encode(name).replace(/-$/, '');
if (sanitized !== name) {
// eslint-disable-next-line no-throw-literal
throw {
name: fieldError
};
}
if (!/^[a-zA-Z0-9][a-zA-Z0-9\\_\\.\\-]*$/.test(name)) {
// eslint-disable-next-line no-throw-literal
throw {
name: fieldError
};
}
const [err, res] = await intercept(
createClient().query({
fetchPolicy: 'network-only',
query: GetInstance,
variables: { name }
})
);
if (err) {
// eslint-disable-next-line no-throw-literal
throw {
name: parseError(err)
};
}
const { data } = res;
const { machines = [] } = data;
if (machines.length) {
// eslint-disable-next-line no-throw-literal
throw {
name: `${name} already exists`
};
}
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'change';
},
handleAsyncValidate: validateName,
handleRandomize: async () => {
dispatch(
set({ name: 'create-instance-name-randomizing', value: true })

View File

@ -20,7 +20,7 @@ import {
import Title from '@components/create-instance/title';
import Description from '@components/description';
import Tag from '@components/tags';
import { fieldError } from '@root/constants';
import { addTag as validateTag } from '@state/validators';
const FORM_NAME_CREATE = 'CREATE-INSTANCE-TAGS-ADD';
const FORM_NAME_EDIT = i => `CREATE-INSTANCE-TAGS-EDIT-${i}`;
@ -87,9 +87,9 @@ export const Tags = ({
form={FORM_NAME_CREATE}
destroyOnUnmount={false}
forceUnregisterOnUnmount={true}
onSubmit={handleAddTag}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidate}
onSubmit={handleAddTag}
>
{props =>
expanded && addOpen ? (
@ -151,20 +151,10 @@ export default compose(
dispatch(set({ name: 'create-instance-tags-proceeded', value: true }));
return history.push(`/~create/tags${history.location.search}`);
},
shouldAsyncValidate: ({ trigger }) => trigger === 'submit',
handleAsyncValidate: async ({ name = '', value = '' }) => {
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
const isValueValid = /^[a-zA-Z_.-]{1,16}$/.test(value);
if (isNameValid && isValueValid) {
return;
}
throw {
name: isNameValid ? null : fieldError,
value: isValueValid ? null : fieldError
};
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleAddTag: value => {
const toggleToClosed = set({
name: `create-instance-tags-add-open`,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 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

View File

@ -26,7 +26,7 @@ import DeleteTag from '@graphql/delete-tag.gql';
import UpdateTags from '@graphql/update-tags.gql';
import GetTags from '@graphql/list-tags.gql';
import parseError from '@state/parse-error';
import { fieldError } from '@root/constants';
import { addCnsService as validateServiceName } from '@state/validators';
const FORM_NAME = 'cns-new-service';
@ -258,18 +258,10 @@ export default compose(
return refetch();
},
shouldAsyncValidate: ({ trigger }) => trigger === 'change',
handleAsyncValidate: async ({ name }) => {
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
if (isNameValid) {
return;
}
throw {
name: fieldError
};
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'change';
},
handleAsyncValidate: validateServiceName,
handleRemoveService: async (name, services) => {
const value = services.filter(svc => name !== svc);

View File

@ -28,6 +28,7 @@ import DeleteMetadata from '@graphql/delete-metadata.gql';
import parseError from '@state/parse-error';
import ToolbarForm from '@components/instances/toolbar';
import Confirm from '@state/confirm';
import { addMetadata as validateMetadata } from '@state/validators';
import {
AddForm as MetadataAddForm,
@ -43,14 +44,14 @@ export const Metadata = ({
addOpen,
loading,
error,
shouldAsyncValidate,
handleAsyncValidate,
handleToggleAddOpen,
handleUpdateExpanded,
handleCancel,
handleCreate,
handleUpdate,
handleRemove,
shouldAsyncValidate,
asyncValidate
handleRemove
}) => {
const _loading = !(loading && !metadata.length) ? null : <StatusLoader />;
@ -59,7 +60,7 @@ export const Metadata = ({
form={ADD_FORM_NAME}
onSubmit={handleCreate}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={asyncValidate}
asyncValidate={handleAsyncValidate}
>
{props => (
<MetadataAddForm
@ -94,9 +95,9 @@ export const Metadata = ({
key={form}
initialValues={initialValues}
destroyOnUnmount={false}
onSubmit={handleUpdate}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={asyncValidate}
asyncValidate={handleAsyncValidate}
onSubmit={handleUpdate}
>
{props => (
<MetadataEditForm
@ -239,19 +240,7 @@ export default compose(
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
asyncValidate: async ({ name = '', value = '' }) => {
const isNameInvalid = name.length === 0;
const isValueInvalid = value.length === 0;
if (!isNameInvalid && !isValueInvalid) {
return;
}
throw {
name: isNameInvalid,
value: isValueInvalid
};
},
handleAsyncValidate: validateMetadata,
handleCreate: async ({ name, value }) => {
// call mutation
const [err] = await intercept(

View File

@ -32,9 +32,9 @@ import RemoveSnapshot from '@graphql/remove-snapshot.gql';
import CreateSnapshotMutation from '@graphql/create-snapshot.gql';
import ToolbarForm from '@components/instances/toolbar';
import SnapshotsListActions from '@components/instances/footer';
import { addSnapshot as validateSnapshot } from '@state/validators';
import parseError from '@state/parse-error';
import Confirm from '@state/confirm';
import { fieldError } from '@root/constants';
const MENU_FORM_NAME = 'snapshot-list-menu';
const TABLE_FORM_NAME = 'snapshot-list-table';
@ -89,14 +89,12 @@ const Snapshots = ({
asyncValidate={handleAsyncValidate}
onSubmit={handleCreateSnapshot}
>
{props => {
return (
<SnapshotAddForm
{...props}
onCancel={() => toggleCreateSnapshotOpen(false)}
/>
);
}}
{props => (
<SnapshotAddForm
{...props}
onCancel={() => toggleCreateSnapshotOpen(false)}
/>
)}
</ReduxForm>
</Margin>
) : null;
@ -252,11 +250,29 @@ export default compose(
};
},
(dispatch, ownProps) => {
const { instance, createSnapshot, refetch } = ownProps;
const { instance, snapshots, createSnapshot, refetch } = ownProps;
return {
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: async ({ name }) => {
const [err] = await intercept(validateSnapshot({ name }));
if (err) {
throw err;
}
const snapshot = find(snapshots, ['name', name]);
if (snapshot) {
// eslint-disable-next-line no-throw-literal
throw {
name: `${name} already exists`
};
}
},
handleSortBy: (newSortBy, sortOrder) => {
dispatch([
return dispatch([
set({
name: `snapshots-list-sort-order`,
value: sortOrder === 'desc' ? 'asc' : 'desc'
@ -267,25 +283,14 @@ export default compose(
})
]);
},
shouldAsyncValidate: ({ trigger }) => trigger === 'change',
handleAsyncValidate: async ({ name }) => {
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
if (isNameValid) {
return;
}
throw {
name: fieldError
};
},
toggleCreateSnapshotOpen: value =>
dispatch(
toggleCreateSnapshotOpen: value => {
return dispatch(
set({
name: `snapshots-create-open`,
value
})
),
);
},
toggleSelectAll: ({ selected = [], snapshots = [] }) => () => {
const same = selected.length === snapshots.length;
const hasSelected = selected.length > 0;
@ -311,7 +316,6 @@ export default compose(
);
}
},
handleCreateSnapshot: async ({ name }) => {
const [err] = await intercept(
createSnapshot({
@ -342,7 +346,6 @@ export default compose(
})
);
},
handleAction: async ({ name, selected = [] }) => {
// eslint-disable-next-line no-alert
if (

View File

@ -31,8 +31,8 @@ import GetTags from '@graphql/list-tags.gql';
import UpdateTags from '@graphql/update-tags.gql';
import DeleteTag from '@graphql/delete-tag.gql';
import parseError from '@state/parse-error';
import { addTag as validateTag } from '@state/validators';
import Confirm from '@state/confirm';
import { fieldError } from '@root/constants';
const MENU_FORM_NAME = 'instance-tags-list-menu';
const ADD_FORM_NAME = 'instance-tags-add-new';
@ -58,10 +58,10 @@ export const Tags = ({
const _add = addOpen ? (
<ReduxForm
form={ADD_FORM_NAME}
onSubmit={handleCreate}
onCancel={() => handleToggleAddOpen(false)}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidate}
onSubmit={handleCreate}
onCancel={() => handleToggleAddOpen(false)}
>
{TagsAddForm}
</ReduxForm>
@ -208,26 +208,16 @@ export default compose(
},
(dispatch, ownProps) => {
return {
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleToggleAddOpen: value => {
return dispatch(set({ name: `add-tags-open`, value }));
},
handleToggleEditing: value => {
return dispatch(set({ name: `editing-tag`, value }));
},
shouldAsyncValidate: ({ trigger }) => trigger === 'submit',
handleAsyncValidate: async ({ name = '', value = '' }) => {
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
const isValueValid = /^[a-zA-Z_.-]{1,16}$/.test(value);
if (isNameValid && isValueValid) {
return;
}
throw {
name: isNameValid ? null : fieldError,
value: isValueValid ? null : fieldError
};
},
handleEdit: async ({ name, value }, _, { form, initialValues }) => {
const { instance, deleteTag, updateTags, refetch } = ownProps;

View File

@ -0,0 +1,6 @@
query snapshot($machine: ID!, $name: String!) {
snapshot(machine: $machine, name: $name) {
id
name
}
}

View File

@ -0,0 +1,112 @@
import intercept from 'apr-intercept';
import keys from 'lodash.keys';
import reduce from 'apr-reduce';
import assign from 'lodash.assign';
import yup from 'yup';
/*****************************************************************************/
const validateField = async (field, value) => {
const [err] = await intercept(field.validate(value));
return err ? err.errors.shift() : '';
};
const validateSchema = async (schema, value) => {
const errors = await reduce(
keys(schema),
async (errors, name) =>
assign(errors, {
[name]: await validateField(schema[name], value[name])
}),
{}
);
throw errors;
};
/*****************************************************************************/
const matches = {
nameStart: /^[a-zA-Z]|\d/,
nameBody: /^([a-zA-Z]|\d|\.|_|-)+$/
};
const msgs = {
required: prefix => `${prefix} must be defined.`,
nameStart: prefix => `${prefix} can only start with letters and numbers.`,
nameBody: prefix =>
`${prefix} cannot contain spaces and can only contain letters, numbers, periods (.), underscores (_), and hyphens (-).`
};
const Schemas = {
tag: {
name: yup
.string()
.required(msgs.required('Key'))
.matches(matches.nameStart, msgs.nameStart('Key'))
.matches(matches.nameBody, msgs.nameBody('Key')),
value: yup
.string()
.required(msgs.required('Value'))
.matches(matches.nameStart, msgs.nameStart('Value'))
.matches(matches.nameBody, msgs.nameBody('Value'))
},
cns: {
name: yup
.string()
.required(msgs.required('Service name'))
.matches(matches.nameStart, msgs.nameStart('Service name'))
.matches(matches.nameBody, msgs.nameBody('Service name'))
},
affinityRule: {
key: yup
.string()
.required(msgs.required('Key'))
.matches(matches.nameStart, msgs.nameStart('Key'))
.matches(matches.nameBody, msgs.nameBody('Key')),
value: yup
.string()
.required(msgs.required('Value'))
.matches(matches.nameStart, msgs.nameStart('Value'))
.matches(matches.nameBody, msgs.nameBody('Value'))
},
metadata: {
name: yup
.string()
.required(msgs.required('Key'))
.matches(matches.nameStart, msgs.nameStart('Key'))
.matches(matches.nameBody, msgs.nameBody('Key')),
value: yup.string().required(msgs.required('Value'))
},
instanceName: {
name: yup
.string()
.notRequired()
.matches(matches.nameStart, msgs.nameStart('Instance name'))
.matches(matches.nameBody, msgs.nameBody('Instance Name'))
},
snapshot: {
name: yup
.string()
.required()
.matches(matches.nameStart, msgs.nameStart('Snapshot name'))
.matches(matches.nameBody, msgs.nameBody('Snapshot Name'))
}
};
/*****************************************************************************/
export const addTag = tag => validateSchema(Schemas.tag, tag);
export const addCnsService = service => validateSchema(Schemas.cns, service);
export const addAffinityRule = aff => validateSchema(Schemas.affinityRule, aff);
export const addSnapshot = snapshot =>
validateSchema(Schemas.snapshot, snapshot);
export const addMetadata = metadata =>
validateSchema(Schemas.metadata, metadata);
export const instanceName = ({ name }) =>
!name ? null : validateSchema(Schemas.instanceName, { name });
export const editMetadata = addMetadata;

View File

@ -9,7 +9,7 @@
"build:lib": "echo 0",
"build:bundle": "NODE_ENV=production redrun build",
"prepublish": "NODE_ENV=production redrun build",
"lint": "redrun lint:ci -- -- --fix",
"lint": "redrun lint:ci -- --fix",
"lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "echo 0",
"test:ci": "redrun test",

View File

@ -13,7 +13,7 @@
"build:lib": "NODE_ENV=production redrun -p build:es build:umd",
"build:bundle": "echo 0",
"prepublish": "NODE_ENV=production redrun build:lib",
"lint": "redrun lint:ci -- -- --fix",
"lint": "redrun lint:ci -- --fix",
"lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test:ci": "redrun test",

View File

@ -40,6 +40,10 @@ const StyledLabel = Label.extend`
${is('small')`
width: ${remcalc(120)};
`};
${is('absolute')`
position: absolute;
`};
`;
const Meta = props => {

View File

@ -2319,6 +2319,10 @@ case-sensitive-paths-webpack-plugin@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.1.tgz#3d29ced8c1f124bf6f53846fb3f5894731fdc909"
case@^1.2.1:
version "1.5.4"
resolved "https://registry.yarnpkg.com/case/-/case-1.5.4.tgz#b201642aae9e374feb5750d1181a76850153830c"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@ -4690,6 +4694,10 @@ flush-write-stream@^1.0.0:
inherits "^2.0.1"
readable-stream "^2.0.4"
fn-name@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-1.0.1.tgz#de8d8a15388b33cbf2145782171f73770c6030f0"
follow-redirects@^1.2.3:
version "1.4.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
@ -7065,6 +7073,10 @@ lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
lodash.reduce@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b"
lodash.reverse@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.reverse/-/lodash.reverse-4.0.1.tgz#1f2afedace2e16e660f3aa7c59d3300a6f25d13c"
@ -7102,7 +7114,7 @@ lodash.values@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347"
"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1:
"lodash@>=3.5 <5", lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1:
version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
@ -8773,6 +8785,10 @@ prop-types@^15.0.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6,
loose-envify "^1.3.1"
object-assign "^4.1.1"
property-expr@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.4.0.tgz#e28cfe4e7a5a231fb14c8ad687a93a5342e05a8c"
proxy-addr@~2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341"
@ -8828,7 +8844,7 @@ punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
punycode@2.x.x, punycode@^2.1.0:
punycode@2.x.x:
version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
@ -10943,6 +10959,10 @@ symbol@^0.2.1:
version "0.2.3"
resolved "https://registry.yarnpkg.com/symbol/-/symbol-0.2.3.tgz#3b9873b8a901e47c6efe21526a3ac372ef28bbc7"
synchronous-promise@^1.0.18:
version "1.0.18"
resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-1.0.18.tgz#936e8763e6554088cdcf78dc64f7473b972fcefc"
table@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
@ -11197,6 +11217,10 @@ topo@3.x.x:
dependencies:
hoek "5.x.x"
toposort@^0.2.10:
version "0.2.12"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-0.2.12.tgz#c7d2984f3d48c217315cc32d770888b779491e81"
toposort@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec"
@ -11292,6 +11316,10 @@ type-is@~1.6.15:
media-typer "0.3.0"
mime-types "~2.1.18"
type-name@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4"
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@ -12282,6 +12310,18 @@ yauzl@2.4.1:
dependencies:
fd-slicer "~1.0.1"
yup@^0.24.1:
version "0.24.1"
resolved "https://registry.yarnpkg.com/yup/-/yup-0.24.1.tgz#2c8a81b5f929ef29aaf77a8b7c9acfa52ab6a7d1"
dependencies:
case "^1.2.1"
fn-name "~1.0.1"
lodash "^4.17.0"
property-expr "^1.2.0"
synchronous-promise "^1.0.18"
toposort "^0.2.10"
type-name "^2.0.1"
zen-observable-ts@^0.8.6:
version "0.8.8"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.8.tgz#1a586dc204fa5632a88057f879500e0d2ba06869"