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:lib": "NODE_ENV=production redrun -p build:es build:umd",
"build:bundle": "echo 0", "build:bundle": "echo 0",
"prepublish": "NODE_ENV=production redrun build:lib", "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", "lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "NODE_ENV=test joyent-react-scripts test --env=jsdom", "test": "NODE_ENV=test joyent-react-scripts test --env=jsdom",
"test:ci": "redrun test", "test:ci": "redrun test",

View File

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

View File

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

View File

@ -2,13 +2,12 @@ import React, { Fragment } from 'react';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { set } from 'react-redux-values'; import { set } from 'react-redux-values';
import ReduxForm from 'declarative-redux-form'; import ReduxForm from 'declarative-redux-form';
import { Row, Col } from 'joyent-react-styled-flexboxgrid';
import { Margin } from 'styled-components-spacing'; import { Margin } from 'styled-components-spacing';
import { change } from 'redux-form'; import { change } from 'redux-form';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import intercept from 'apr-intercept'; import intercept from 'apr-intercept';
import get from 'lodash.get'; 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'; 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 Details from '@components/create-image/details';
import Description from '@components/description'; import Description from '@components/description';
import GetRandomName from '@graphql/get-random-name.gql'; 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'; import { Forms } from '@root/constants';
const NameContainer = ({ const NameContainer = ({
@ -27,7 +27,7 @@ const NameContainer = ({
description, description,
placeholderName, placeholderName,
randomizing, randomizing,
handleAsyncValidation, handleAsyncValidate,
shouldAsyncValidate, shouldAsyncValidate,
handleNext, handleNext,
handleRandomize, handleRandomize,
@ -54,9 +54,9 @@ const NameContainer = ({
form={Forms.FORM_DETAILS} form={Forms.FORM_DETAILS}
destroyOnUnmount={false} destroyOnUnmount={false}
forceUnregisterOnUnmount={true} forceUnregisterOnUnmount={true}
onSubmit={handleNext} asyncValidate={handleAsyncValidate}
asyncValidate={handleAsyncValidation}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleNext}
> >
{props => {props =>
expanded ? ( expanded ? (
@ -121,6 +121,7 @@ export default compose(
({ form, values }, ownProps) => { ({ form, values }, ownProps) => {
const name = get(form, `${Forms.FORM_DETAILS}.values.name`, ''); const name = get(form, `${Forms.FORM_DETAILS}.values.name`, '');
const version = get(form, `${Forms.FORM_DETAILS}.values.version`, ''); const version = get(form, `${Forms.FORM_DETAILS}.values.version`, '');
const description = get( const description = get(
form, form,
`${Forms.FORM_DETAILS}.values.description`, `${Forms.FORM_DETAILS}.values.description`,
@ -128,7 +129,6 @@ export default compose(
); );
const proceeded = get(values, `${Forms.FORM_DETAILS}-proceeded`, false); const proceeded = get(values, `${Forms.FORM_DETAILS}-proceeded`, false);
const randomizing = get(values, 'create-image-name-randomizing', false); const randomizing = get(values, 'create-image-name-randomizing', false);
return { return {
@ -141,35 +141,23 @@ export default compose(
}; };
}, },
(dispatch, { history, match }) => ({ (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: () => { handleNext: () => {
dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true })); dispatch(set({ name: `${Forms.FORM_DETAILS}-proceeded`, value: true }));
return history.push(`/~create/${match.params.instance}/tag`); 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 () => { handleRandomize: async () => {
dispatch(set({ name: 'create-image-name-randomizing', value: true })); dispatch(set({ name: 'create-image-name-randomizing', value: true }));
const [err, res] = await intercept( const [err, res] = await intercept(
createStore().query({ createClient().query({
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
query: GetRandomName query: GetRandomName
}) })

View File

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

View File

@ -27,6 +27,7 @@ import Tag, { AddForm } from '@components/tags';
import ToolbarForm from '@components/toolbar'; import ToolbarForm from '@components/toolbar';
import UpdateImageTags from '@graphql/update-image-tags.gql'; import UpdateImageTags from '@graphql/update-image-tags.gql';
import GetTags from '@graphql/get-tags.gql'; import GetTags from '@graphql/get-tags.gql';
import { addTag as validateTag } from '@state/validators';
import parseError from '@state/parse-error'; import parseError from '@state/parse-error';
const { TAGS_TOOLBAR_FORM, TAGS_ADD_FORM } = Forms; const { TAGS_TOOLBAR_FORM, TAGS_ADD_FORM } = Forms;
@ -38,6 +39,8 @@ export const Tags = ({
error = null, error = null,
mutationError = null, mutationError = null,
mutating = false, mutating = false,
handleAsyncValidate,
shouldAsyncValidate,
handleToggleAddOpen, handleToggleAddOpen,
handleRemoveTag, handleRemoveTag,
handleAddTag handleAddTag
@ -76,7 +79,12 @@ export const Tags = ({
</Message> </Message>
</Margin> </Margin>
) : null} ) : null}
<ReduxForm form={TAGS_ADD_FORM} onSubmit={handleAddTag}> <ReduxForm
form={TAGS_ADD_FORM}
asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate}
onSubmit={handleAddTag}
>
{props => {props =>
addOpen ? ( addOpen ? (
<Margin bottom={4}> <Margin bottom={4}>
@ -162,6 +170,10 @@ export default compose(
}; };
}, },
(dispatch, { image, tags = [], updateTags, refetch }) => ({ (dispatch, { image, tags = [], updateTags, refetch }) => ({
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleToggleAddOpen: addOpen => { handleToggleAddOpen: addOpen => {
dispatch(set({ name: `${image.id}-add-open`, value: 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:lib": "echo 0",
"build:bundle": "NODE_ENV=production redrun -p build:frontend build:ssr", "build:bundle": "NODE_ENV=production redrun -p build:frontend build:ssr",
"prepublish": "NODE_ENV=production redrun build:bundle", "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", "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": "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'", "test:ci": "NODE_ENV=test joyent-react-scripts test --env=jsdom --testPathIgnorePatterns='.ui.js'",
@ -22,6 +22,7 @@
"@manaflair/redux-batch": "^0.1.0", "@manaflair/redux-batch": "^0.1.0",
"apollo": "^0.2.2", "apollo": "^0.2.2",
"apr-intercept": "^3.0.3", "apr-intercept": "^3.0.3",
"apr-reduce": "^3.0.3",
"bytes": "^3.0.0", "bytes": "^3.0.0",
"clipboard-copy": "^1.4.2", "clipboard-copy": "^1.4.2",
"constant-case": "^2.0.0", "constant-case": "^2.0.0",
@ -47,13 +48,13 @@
"lodash.isfunction": "^3.0.9", "lodash.isfunction": "^3.0.9",
"lodash.isinteger": "^4.0.4", "lodash.isinteger": "^4.0.4",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"lodash.reduce": "^4.6.0",
"lodash.reverse": "^4.0.1", "lodash.reverse": "^4.0.1",
"lodash.some": "^4.6.0", "lodash.some": "^4.6.0",
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"lodash.uniqby": "^4.7.0", "lodash.uniqby": "^4.7.0",
"lodash.values": "^4.3.0", "lodash.values": "^4.3.0",
"param-case": "^2.1.1", "param-case": "^2.1.1",
"punycode": "^2.1.0",
"query-string": "^5.1.0", "query-string": "^5.1.0",
"react": "^16.2.0", "react": "^16.2.0",
"react-apollo": "^2.0.4", "react-apollo": "^2.0.4",
@ -68,7 +69,8 @@
"styled-components": "^3.1.6", "styled-components": "^3.1.6",
"styled-components-spacing": "^2.1.3", "styled-components-spacing": "^2.1.3",
"styled-flex-component": "^2.2.1", "styled-flex-component": "^2.2.1",
"title-case": "^2.1.1" "title-case": "^2.1.1",
"yup": "^0.24.1"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0", "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); background-color: rgb(59,70,204);
border-radius: 0.25rem; border-radius: 0.25rem;
border: solid 0.0625rem rgb(45,56,132); 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 { .c7:focus {
@ -500,23 +495,6 @@ exports[`renders <AddServiceForm pristine /> without throwing 1`] = `
pointer-events: none; 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 { .c3 {
font-size: 0.9375rem; font-size: 0.9375rem;
line-height: 1.125rem; line-height: 1.125rem;
@ -678,7 +656,7 @@ exports[`renders <AddServiceForm pristine /> without throwing 1`] = `
> >
<button <button
className="c7 c8 c9" className="c7 c8 c9"
disabled={true} disabled={undefined}
href="" href=""
type="submit" type="submit"
> >

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@ import remcalc from 'remcalc';
import { H5, Select, Input, FormGroup, FormMeta } from 'joyent-ui-toolkit'; import { H5, Select, Input, FormGroup, FormMeta } from 'joyent-ui-toolkit';
const style = { const style = {
lineHeight: '48px', lineHeight: remcalc(48),
fontSize: '18px' fontSize: remcalc(18)
}; };
const Bold = styled.span` const Bold = styled.span`
@ -19,7 +19,7 @@ const Bold = styled.span`
const Values = touched => ( const Values = touched => (
<Margin right={1}> <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="equalling">equalling</option>
<option value="not-equalling">not equalling</option> <option value="not-equalling">not equalling</option>
<option value="containing">containing</option> <option value="containing">containing</option>
@ -29,16 +29,16 @@ const Values = touched => (
</Margin> </Margin>
); );
export const Rule = rule => ( export const Rule = ({ valid, ...rule }) => (
<Margin bottom={4}> <Margin bottom={valid ? 4 : 8}>
<Flex alignCenter wrap> <Flex alignCenter wrap>
<H5 style={style} inline noMargin> <H5 style={style} inline noMargin>
The instance The instance
</H5> </H5>
<FormGroup name="rule-instance-conditional" field={Field}> <FormGroup name="conditional" field={Field}>
<Select <Select
style={style} style={style}
touched={rule['rule-instance-conditional']} touched={rule.conditional}
width={remcalc(66)} width={remcalc(66)}
embedded embedded
> >
@ -49,10 +49,10 @@ export const Rule = rule => (
<H5 style={style} inline noMargin> <H5 style={style} inline noMargin>
be on be on
</H5> </H5>
<FormGroup name="rule-instance-placement" field={Field}> <FormGroup name="placement" field={Field}>
<Select <Select
style={style} style={style}
touched={rule['rule-instance-placement']} touched={rule.placement}
width={remcalc(100)} width={remcalc(100)}
embedded embedded
> >
@ -63,10 +63,10 @@ export const Rule = rule => (
<H5 style={style} inline noMargin> <H5 style={style} inline noMargin>
node as the instance(s) identified by the node as the instance(s) identified by the
</H5> </H5>
<FormGroup name="rule-type" field={Field}> <FormGroup name="type" field={Field}>
<Select <Select
style={style} style={style}
touched={rule['rule-type']} touched={rule.type}
width={remcalc(135)} width={remcalc(135)}
embedded embedded
left left
@ -75,54 +75,53 @@ export const Rule = rule => (
<option value="tag">tag</option> <option value="tag">tag</option>
</Select> </Select>
</FormGroup> </FormGroup>
{rule['rule-type'] === 'tag' ? ( {rule.type === 'tag' ? (
<Fragment> <Fragment>
<FormGroup name="rule-instance-tag-key" field={Field}> <FormGroup name="key" field={Field}>
<Input <Input
style={style} style={style}
onBlur={null} onBlur={null}
type="text"
placeholder="key"
small small
embedded embedded
type="text"
required required
placeholder="key"
/> />
<FormMeta small /> <FormMeta small absolute />
</FormGroup> </FormGroup>
<H5 style={style} inline noMargin> <H5 style={style} inline noMargin>
and value{' '} and value{' '}
</H5> </H5>
<FormGroup name="rule-instance-tag-value-pattern" field={Field}> <FormGroup name="pattern" field={Field}>
{Values(rule['rule-instance-tag-value-pattern'])} {Values(rule.pattern)}
</FormGroup> </FormGroup>
<FormGroup name="rule-instance-tag-value" field={Field}> <FormGroup name="value" field={Field}>
<Input <Input
style={style} style={style}
onBlur={null} onBlur={null}
small
embedded
type="text" type="text"
required
placeholder="value" placeholder="value"
embedded
required
/> />
<FormMeta small /> <FormMeta small absolute />
</FormGroup> </FormGroup>
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
<FormGroup name="rule-instance-name-pattern" field={Field}> <FormGroup name="pattern" field={Field}>
{Values(rule['rule-instance-name-pattern'])} {Values(rule.pattern)}
</FormGroup> </FormGroup>
<FormGroup name="rule-instance-name" field={Field}> <FormGroup name="value" field={Field}>
<Input <Input
onBlur={null} onBlur={null}
embedded
style={style} style={style}
type="text" type="text"
required
placeholder="Example instance name: nginx" placeholder="Example instance name: nginx"
embedded
required
/> />
<FormMeta /> <FormMeta absolute />
</FormGroup> </FormGroup>
</Fragment> </Fragment>
)} )}
@ -132,21 +131,18 @@ export const Rule = rule => (
export const Header = rule => ( export const Header = rule => (
<Fragment> <Fragment>
<Bold>{titleCase(rule['rule-instance-conditional'])}:</Bold> be on a{' '} <Bold>{titleCase(rule.conditional)}:</Bold> be on a {rule.placement} node as
{rule['rule-instance-placement']} node as the instance(s) identified by the the instance(s) identified by the instance {rule.type}
instance {rule['rule-type']} {rule.type === 'name' ? (
{rule['rule-type'] === 'name' ? (
<Fragment> <Fragment>
{' '} {' '}
{rule['rule-instance-name-pattern']} {rule['rule-instance-name']} {rule.pattern} {rule.value}
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
{' '} {' '}
key {rule['rule-instance-tag-key']}" and the instance tag value{' '} key {rule.key}" and the instance tag value{' '}
{rule['rule-instance-tag-value-pattern'] && {rule.pattern && rule.pattern.split('-').join(' ')} "{rule.value}
rule['rule-instance-tag-value-pattern'].split('-').join(' ')}{' '}
"{rule['rule-instance-tag-value']}
</Fragment> </Fragment>
)} )}
</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 get from 'lodash.get';
import remcalc from 'remcalc'; 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 Title from '@components/create-instance/title';
import { Rule, Header } from '@components/create-instance/affinity'; import { Rule, Header } from '@components/create-instance/affinity';
import Description from '@components/description'; 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_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 = { const RULE_DEFAULTS = {
'rule-instance-name': '', conditional: 'should',
'rule-instance-conditional': 'should', placement: 'same',
'rule-instance-placement': 'same', type: 'name',
'rule-instance-tag-key-pattern': 'equalling', pattern: 'equalling',
'rule-instance-tag-value-pattern': 'equalling', key: '',
'rule-instance-name-pattern': 'equalling', value: ''
'rule-instance-tag-value': '',
'rule-instance-tag-key': '',
'rule-type': 'name'
}; };
export const Affinity = ({ export const Affinity = ({
affinityRules = [], step,
expanded, expanded,
proceeded,
addOpen, addOpen,
handleAddAffinityRules, editOpen,
editingRule,
creatingRule,
exitingRule,
shouldAsyncValidate,
handleAsyncValidate,
handleCreateAffinityRules,
handleRemoveAffinityRule, handleRemoveAffinityRule,
handleUpdateAffinityRule, handleUpdateAffinityRule,
shouldAsyncValidate,
handleAsyncValidation,
handleToggleExpanded, handleToggleExpanded,
handleCancelEdit, handleCancelEdit,
handleChangeAddOpen, handleChangeAddOpen,
handleEdit, handleEdit
rule,
step
}) => ( }) => (
<Fragment> <Fragment>
<Title <Title
id={step} id={step}
onClick={!expanded && !proceeded && handleEdit} onClick={!expanded && !exitingRule && handleEdit}
collapsed={!expanded && !proceeded} collapsed={!expanded && !exitingRule}
icon={<AffinityIcon />} icon={<AffinityIcon />}
> >
Affinity Affinity
@ -70,55 +68,64 @@ export const Affinity = ({
</a> </a>
</Description> </Description>
) : null} ) : null}
{proceeded ? ( <ReduxForm
<Margin bottom={4}> form={FORM_NAME_EDIT}
<H3>{affinityRules.length} Affinity Rule</H3> initialValues={exitingRule}
</Margin> destroyOnUnmount={false}
) : null} forceUnregisterOnUnmount={false}
{affinityRules.map((rule, index) => ( shouldAsyncValidate={shouldAsyncValidate}
<ReduxForm asyncValidation={handleAsyncValidate}
form={FORM_NAME_EDIT(index)} onSubmit={handleUpdateAffinityRule}
key={index} >
initialValues={rule} {formProps =>
destroyOnUnmount={false} exitingRule ? (
forceUnregisterOnUnmount={true}
shouldAsyncValidate={shouldAsyncValidate}
asyncValidation={handleAsyncValidation}
onSubmit={newValue => handleUpdateAffinityRule(index, newValue)}
>
{props => (
<Fragment> <Fragment>
<KeyValue <KeyValue
{...props} {...formProps}
expanded={rule.expanded} expanded={editOpen}
customHeader={<Header {...rule} />} customHeader={<Header {...exitingRule} />}
method="edit" method="edit"
input={props => <Rule {...rule} {...props} />} input={inputProps => (
<Rule
{...editingRule}
{...inputProps}
valid={formProps.valid}
/>
)}
type="an affinity rule" type="an affinity rule"
onToggleExpanded={() => handleToggleExpanded(index)} onToggleExpanded={() =>
onCancel={() => handleCancelEdit(index)} handleToggleExpanded(!exitingRule.expanded)
onRemove={() => handleRemoveAffinityRule(index)} }
onCancel={handleCancelEdit}
onRemove={handleRemoveAffinityRule}
/> />
<Divider height={remcalc(12)} transparent /> <Divider height={remcalc(12)} transparent />
</Fragment> </Fragment>
)} ) : null
</ReduxForm> }
))} </ReduxForm>
<ReduxForm <ReduxForm
form={FORM_NAME_CREATE} form={FORM_NAME_CREATE}
initialValues={RULE_DEFAULTS}
destroyOnUnmount={false} destroyOnUnmount={false}
forceUnregisterOnUnmount={true} forceUnregisterOnUnmount={false}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidation} asyncValidate={handleAsyncValidate}
onSubmit={handleAddAffinityRules} onSubmit={handleCreateAffinityRules}
> >
{props => {formProps =>
expanded && addOpen ? ( expanded && addOpen ? (
<Fragment> <Fragment>
<KeyValue <KeyValue
{...props} {...formProps}
method="create" method="create"
input={props => <Rule {...rule} {...props} />} input={inputProps => (
<Rule
{...creatingRule}
{...inputProps}
valid={formProps.valid}
/>
)}
type="an affinity rule" type="an affinity rule"
expanded expanded
noRemove noRemove
@ -131,7 +138,7 @@ export const Affinity = ({
</ReduxForm> </ReduxForm>
{expanded ? ( {expanded ? (
<Margin top={2} bottom={4}> <Margin top={2} bottom={4}>
{!addOpen && affinityRules.length === 0 ? ( {!addOpen && !exitingRule ? (
<Button <Button
type="button" type="button"
onClick={() => handleChangeAddOpen(true)} onClick={() => handleChangeAddOpen(true)}
@ -141,7 +148,7 @@ export const Affinity = ({
</Button> </Button>
) : null} ) : null}
</Margin> </Margin>
) : proceeded ? ( ) : exitingRule ? (
<Margin top={2} bottom={4}> <Margin top={2} bottom={4}>
<Button type="button" onClick={handleEdit} secondary> <Button type="button" onClick={handleEdit} secondary>
Edit Edit
@ -149,61 +156,45 @@ export const Affinity = ({
</Margin> </Margin>
) : null} ) : null}
<Margin bottom={7}> <Margin bottom={7}>
{expanded || proceeded ? <Divider height={remcalc(1)} /> : null} {expanded ? <Divider height={remcalc(1)} /> : null}
</Margin> </Margin>
</Fragment> </Fragment>
); );
export default compose( export default compose(
connect(({ values, form }, ownProps) => { 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 addOpen = get(values, 'create-instance-affinity-add-open', false);
const affinityRules = get(values, 'create-instance-affinity', []); const editOpen = get(values, 'create-instance-affinity-edit-open', false);
const rule = get(form, `${FORM_NAME_CREATE}.values`, {});
return { return {
proceeded: proceeded || affinityRules.length,
addOpen, addOpen,
affinityRules, editOpen,
rule creatingRule,
editingRule,
exitingRule
}; };
}), }),
connect(null, (dispatch, { affinityRules = [], rule, history }) => ({ connect(null, (dispatch, { history }) => ({
shouldAsyncValidate: ({ trigger }) => trigger === 'change', shouldAsyncValidate: ({ trigger }) => {
handleAsyncValidation: async rule => { return trigger === 'submit';
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
};
}, },
handleAsyncValidate: validateRule,
handleEdit: () => { handleEdit: () => {
return history.push(`/~create/affinity${history.location.search}`); return history.push(`/~create/affinity${history.location.search}`);
}, },
handleAddAffinityRules: ({ ...rule }) => { handleCreateAffinityRules: value => {
const toggleToClosed = set({ const toggleToClosed = set({
name: `create-instance-affinity-add-open`, name: 'create-instance-affinity-add-open',
value: false value: false
}); });
const appendAffinityRule = set({ const appendAffinityRule = set({
name: `create-instance-affinity`, name: 'create-instance-affinity',
value: affinityRules.concat([ value
{ ...RULE_DEFAULTS, ...rule, expanded: false }
])
}); });
return dispatch([ return dispatch([
@ -212,53 +203,32 @@ export default compose(
appendAffinityRule appendAffinityRule
]); ]);
}, },
handleUpdateAffinityRule: (index, newAffinityRule) => { handleUpdateAffinityRule: value => {
affinityRules[index] = {
...newAffinityRule,
expanded: false
};
return dispatch([ return dispatch([
destroy(FORM_NAME_EDIT(index)), destroy(FORM_NAME_EDIT),
set({ name: `create-instance-affinity`, value: affinityRules.slice() }) set({ name: 'create-instance-affinity', value })
]); ]);
}, },
handleChangeAddOpen: value => { handleChangeAddOpen: value => {
return dispatch([ return dispatch([
reset(FORM_NAME_CREATE), reset(FORM_NAME_CREATE),
set({ name: `create-instance-affinity-add-open`, value }) set({ name: 'create-instance-affinity-add-open', value })
]); ]);
}, },
handleToggleExpanded: index => { handleToggleExpanded: value => {
affinityRules[index] = {
...affinityRules[index],
expanded: !affinityRules[index].expanded
};
return dispatch( return dispatch(
set({ set({ name: 'create-instance-affinity-edit-open', value })
name: `create-instance-affinity`,
value: affinityRules.slice()
})
); );
}, },
handleCancelEdit: index => { handleCancelEdit: () => {
affinityRules[index] = {
...affinityRules[index],
expanded: false
};
return dispatch([ return dispatch([
reset(FORM_NAME_EDIT(index)), set({ name: 'create-instance-affinity-edit-open', value: false })
set({ name: `create-instance-affinity`, value: affinityRules.slice() })
]); ]);
}, },
handleRemoveAffinityRule: index => { handleRemoveAffinityRule: () => {
affinityRules.splice(index, 1);
return dispatch([ return dispatch([
destroy(FORM_NAME_EDIT(index)), destroy(FORM_NAME_EDIT),
set({ name: `create-instance-affinity`, value: affinityRules.slice() }) set({ name: 'create-instance-affinity', value: null })
]); ]);
} }
})) }))

View File

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import get from 'lodash.get'; import get from 'lodash.get';
import { Margin } from 'styled-components-spacing'; import { Margin } from 'styled-components-spacing';
import { set } from 'react-redux-values'; import { set } from 'react-redux-values';
import punycode from 'punycode';
import { CnsIcon, H3, Button } from 'joyent-ui-toolkit'; 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 Cns, { Footer, AddServiceForm } from '@components/cns';
import Description from '@components/description'; import Description from '@components/description';
import GetAccount from '@graphql/get-account.gql'; 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'; const CNS_FORM = 'create-instance-cns';
@ -175,32 +174,18 @@ export default compose(
dispatch(set({ name: `${CNS_FORM}-proceeded`, value: true })); dispatch(set({ name: `${CNS_FORM}-proceeded`, value: true }));
history.push(`/~create/cns${history.location.search}`); history.push(`/~create/cns${history.location.search}`);
}, },
shouldAsyncValidate: ({ trigger }) => trigger === 'change', shouldAsyncValidate: ({ trigger }) => {
handleAsyncValidate: async ({ name = '', value = '' }) => { return trigger === 'submit';
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
};
}, },
handleAsyncValidate: validateServiceName,
handleToggleCnsEnabled: ({ target }) => handleToggleCnsEnabled: ({ target }) =>
dispatch(set({ name: `${CNS_FORM}-enabled`, value: !cnsEnabled })), dispatch(set({ name: `${CNS_FORM}-enabled`, value: !cnsEnabled })),
handleAddService: ({ name }) => { handleAddService: ({ name }) => {
const serviceName = punycode
.encode(name.toLowerCase().replace(/\s/g, '-'))
.replace(/-$/, '');
dispatch([ dispatch([
destroy(`${CNS_FORM}-new-service`), destroy(`${CNS_FORM}-new-service`),
set({ set({
name: `${CNS_FORM}-services`, name: `${CNS_FORM}-services`,
value: serviceNames.concat(serviceName) value: serviceNames.concat(name)
}) })
]); ]);
}, },

View File

@ -240,15 +240,8 @@ export default compose(
handleSubmit: async () => { handleSubmit: async () => {
const _affinity = affinity const _affinity = affinity
.map(aff => ({ .map(aff => ({
conditional: aff['rule-instance-conditional'], ...aff,
placement: aff['rule-instance-placement'], value: aff.type === 'name' ? aff.name : aff.value
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']
})) }))
.map(({ conditional, placement, identity, key, pattern, value }) => { .map(({ conditional, placement, identity, key, pattern, value }) => {
const type = constantCase( 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 Title from '@components/create-instance/title';
import Description from '@components/description'; import Description from '@components/description';
import { addMetadata as validateMetadata } from '@state/validators';
const FORM_NAME_CREATE = 'CREATE-INSTANCE-METADATA-ADD'; const FORM_NAME_CREATE = 'CREATE-INSTANCE-METADATA-ADD';
const FORM_NAME_EDIT = i => `CREATE-INSTANCE-METADATA-EDIT-${i}`; const FORM_NAME_EDIT = i => `CREATE-INSTANCE-METADATA-EDIT-${i}`;
@ -179,19 +180,7 @@ export default compose(
shouldAsyncValidate: ({ trigger }) => { shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit'; return trigger === 'submit';
}, },
asyncValidate: async ({ name = '', value = '' }) => { handleAsyncValidate: validateMetadata,
const isNameInvalid = name.length === 0;
const isValueInvalid = value.length === 0;
if (!isNameInvalid && !isValueInvalid) {
return;
}
throw {
name: isNameInvalid,
value: isValueInvalid
};
},
handleAddMetadata: value => { handleAddMetadata: value => {
const toggleToClosed = set({ const toggleToClosed = set({
name: `create-instance-metadata-add-open`, name: `create-instance-metadata-add-open`,

View File

@ -7,18 +7,15 @@ import { change } from 'redux-form';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import intercept from 'apr-intercept'; import intercept from 'apr-intercept';
import get from 'lodash.get'; import get from 'lodash.get';
import punycode from 'punycode';
import { NameIcon, H3, Button } from 'joyent-ui-toolkit'; 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 GetInstance from '@graphql/get-instance-small.gql'; import { instanceName as validateName } from '@state/validators';
import GetRandomName from '@graphql/get-random-name.gql';
import createClient from '@state/apollo-client'; import createClient from '@state/apollo-client';
import parseError from '@state/parse-error'; import GetRandomName from '@graphql/get-random-name.gql';
import { fieldError } from '@root/constants';
const FORM_NAME = 'create-instance-name'; const FORM_NAME = 'create-instance-name';
@ -28,7 +25,7 @@ const NameContainer = ({
name, name,
placeholderName, placeholderName,
randomizing, randomizing,
handleAsyncValidation, handleAsyncValidate,
shouldAsyncValidate, shouldAsyncValidate,
handleNext, handleNext,
handleRandomize, handleRandomize,
@ -54,7 +51,7 @@ const NameContainer = ({
destroyOnUnmount={false} destroyOnUnmount={false}
forceUnregisterOnUnmount={true} forceUnregisterOnUnmount={true}
onSubmit={handleNext} onSubmit={handleNext}
asyncValidate={handleAsyncValidation} asyncValidate={handleAsyncValidate}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
> >
{props => {props =>
@ -127,49 +124,10 @@ export default compose(
handleEdit: () => { handleEdit: () => {
history.push(`/~create/name${history.location.search}`); history.push(`/~create/name${history.location.search}`);
}, },
shouldAsyncValidate: ({ trigger }) => trigger === 'change', shouldAsyncValidate: ({ trigger }) => {
handleAsyncValidation: async ({ name }) => { return trigger === 'change';
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`
};
}
}, },
handleAsyncValidate: validateName,
handleRandomize: async () => { handleRandomize: async () => {
dispatch( dispatch(
set({ name: 'create-instance-name-randomizing', value: true }) set({ name: 'create-instance-name-randomizing', value: true })

View File

@ -20,7 +20,7 @@ import {
import Title from '@components/create-instance/title'; import Title from '@components/create-instance/title';
import Description from '@components/description'; import Description from '@components/description';
import Tag from '@components/tags'; 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_CREATE = 'CREATE-INSTANCE-TAGS-ADD';
const FORM_NAME_EDIT = i => `CREATE-INSTANCE-TAGS-EDIT-${i}`; const FORM_NAME_EDIT = i => `CREATE-INSTANCE-TAGS-EDIT-${i}`;
@ -87,9 +87,9 @@ export const Tags = ({
form={FORM_NAME_CREATE} form={FORM_NAME_CREATE}
destroyOnUnmount={false} destroyOnUnmount={false}
forceUnregisterOnUnmount={true} forceUnregisterOnUnmount={true}
onSubmit={handleAddTag}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidate} asyncValidate={handleAsyncValidate}
onSubmit={handleAddTag}
> >
{props => {props =>
expanded && addOpen ? ( expanded && addOpen ? (
@ -151,20 +151,10 @@ export default compose(
dispatch(set({ name: 'create-instance-tags-proceeded', value: true })); dispatch(set({ name: 'create-instance-tags-proceeded', value: true }));
return history.push(`/~create/tags${history.location.search}`); return history.push(`/~create/tags${history.location.search}`);
}, },
shouldAsyncValidate: ({ trigger }) => trigger === 'submit', shouldAsyncValidate: ({ trigger }) => {
handleAsyncValidate: async ({ name = '', value = '' }) => { return trigger === 'submit';
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
};
}, },
handleAsyncValidate: validateTag,
handleAddTag: value => { handleAddTag: value => {
const toggleToClosed = set({ const toggleToClosed = set({
name: `create-instance-tags-add-open`, 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 UpdateTags from '@graphql/update-tags.gql';
import GetTags from '@graphql/list-tags.gql'; import GetTags from '@graphql/list-tags.gql';
import parseError from '@state/parse-error'; import parseError from '@state/parse-error';
import { fieldError } from '@root/constants'; import { addCnsService as validateServiceName } from '@state/validators';
const FORM_NAME = 'cns-new-service'; const FORM_NAME = 'cns-new-service';
@ -258,18 +258,10 @@ export default compose(
return refetch(); return refetch();
}, },
shouldAsyncValidate: ({ trigger }) => trigger === 'change', shouldAsyncValidate: ({ trigger }) => {
handleAsyncValidate: async ({ name }) => { return trigger === 'change';
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
if (isNameValid) {
return;
}
throw {
name: fieldError
};
}, },
handleAsyncValidate: validateServiceName,
handleRemoveService: async (name, services) => { handleRemoveService: async (name, services) => {
const value = services.filter(svc => name !== svc); 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 parseError from '@state/parse-error';
import ToolbarForm from '@components/instances/toolbar'; import ToolbarForm from '@components/instances/toolbar';
import Confirm from '@state/confirm'; import Confirm from '@state/confirm';
import { addMetadata as validateMetadata } from '@state/validators';
import { import {
AddForm as MetadataAddForm, AddForm as MetadataAddForm,
@ -43,14 +44,14 @@ export const Metadata = ({
addOpen, addOpen,
loading, loading,
error, error,
shouldAsyncValidate,
handleAsyncValidate,
handleToggleAddOpen, handleToggleAddOpen,
handleUpdateExpanded, handleUpdateExpanded,
handleCancel, handleCancel,
handleCreate, handleCreate,
handleUpdate, handleUpdate,
handleRemove, handleRemove
shouldAsyncValidate,
asyncValidate
}) => { }) => {
const _loading = !(loading && !metadata.length) ? null : <StatusLoader />; const _loading = !(loading && !metadata.length) ? null : <StatusLoader />;
@ -59,7 +60,7 @@ export const Metadata = ({
form={ADD_FORM_NAME} form={ADD_FORM_NAME}
onSubmit={handleCreate} onSubmit={handleCreate}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={asyncValidate} asyncValidate={handleAsyncValidate}
> >
{props => ( {props => (
<MetadataAddForm <MetadataAddForm
@ -94,9 +95,9 @@ export const Metadata = ({
key={form} key={form}
initialValues={initialValues} initialValues={initialValues}
destroyOnUnmount={false} destroyOnUnmount={false}
onSubmit={handleUpdate}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={asyncValidate} asyncValidate={handleAsyncValidate}
onSubmit={handleUpdate}
> >
{props => ( {props => (
<MetadataEditForm <MetadataEditForm
@ -239,19 +240,7 @@ export default compose(
shouldAsyncValidate: ({ trigger }) => { shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit'; return trigger === 'submit';
}, },
asyncValidate: async ({ name = '', value = '' }) => { handleAsyncValidate: validateMetadata,
const isNameInvalid = name.length === 0;
const isValueInvalid = value.length === 0;
if (!isNameInvalid && !isValueInvalid) {
return;
}
throw {
name: isNameInvalid,
value: isValueInvalid
};
},
handleCreate: async ({ name, value }) => { handleCreate: async ({ name, value }) => {
// call mutation // call mutation
const [err] = await intercept( 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 CreateSnapshotMutation from '@graphql/create-snapshot.gql';
import ToolbarForm from '@components/instances/toolbar'; import ToolbarForm from '@components/instances/toolbar';
import SnapshotsListActions from '@components/instances/footer'; import SnapshotsListActions from '@components/instances/footer';
import { addSnapshot as validateSnapshot } from '@state/validators';
import parseError from '@state/parse-error'; import parseError from '@state/parse-error';
import Confirm from '@state/confirm'; import Confirm from '@state/confirm';
import { fieldError } from '@root/constants';
const MENU_FORM_NAME = 'snapshot-list-menu'; const MENU_FORM_NAME = 'snapshot-list-menu';
const TABLE_FORM_NAME = 'snapshot-list-table'; const TABLE_FORM_NAME = 'snapshot-list-table';
@ -89,14 +89,12 @@ const Snapshots = ({
asyncValidate={handleAsyncValidate} asyncValidate={handleAsyncValidate}
onSubmit={handleCreateSnapshot} onSubmit={handleCreateSnapshot}
> >
{props => { {props => (
return ( <SnapshotAddForm
<SnapshotAddForm {...props}
{...props} onCancel={() => toggleCreateSnapshotOpen(false)}
onCancel={() => toggleCreateSnapshotOpen(false)} />
/> )}
);
}}
</ReduxForm> </ReduxForm>
</Margin> </Margin>
) : null; ) : null;
@ -252,11 +250,29 @@ export default compose(
}; };
}, },
(dispatch, ownProps) => { (dispatch, ownProps) => {
const { instance, createSnapshot, refetch } = ownProps; const { instance, snapshots, createSnapshot, refetch } = ownProps;
return { 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) => { handleSortBy: (newSortBy, sortOrder) => {
dispatch([ return dispatch([
set({ set({
name: `snapshots-list-sort-order`, name: `snapshots-list-sort-order`,
value: sortOrder === 'desc' ? 'asc' : 'desc' value: sortOrder === 'desc' ? 'asc' : 'desc'
@ -267,25 +283,14 @@ export default compose(
}) })
]); ]);
}, },
shouldAsyncValidate: ({ trigger }) => trigger === 'change', toggleCreateSnapshotOpen: value => {
handleAsyncValidate: async ({ name }) => { return dispatch(
const isNameValid = /^[a-zA-Z_.-]{1,16}$/.test(name);
if (isNameValid) {
return;
}
throw {
name: fieldError
};
},
toggleCreateSnapshotOpen: value =>
dispatch(
set({ set({
name: `snapshots-create-open`, name: `snapshots-create-open`,
value value
}) })
), );
},
toggleSelectAll: ({ selected = [], snapshots = [] }) => () => { toggleSelectAll: ({ selected = [], snapshots = [] }) => () => {
const same = selected.length === snapshots.length; const same = selected.length === snapshots.length;
const hasSelected = selected.length > 0; const hasSelected = selected.length > 0;
@ -311,7 +316,6 @@ export default compose(
); );
} }
}, },
handleCreateSnapshot: async ({ name }) => { handleCreateSnapshot: async ({ name }) => {
const [err] = await intercept( const [err] = await intercept(
createSnapshot({ createSnapshot({
@ -342,7 +346,6 @@ export default compose(
}) })
); );
}, },
handleAction: async ({ name, selected = [] }) => { handleAction: async ({ name, selected = [] }) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if ( if (

View File

@ -31,8 +31,8 @@ import GetTags from '@graphql/list-tags.gql';
import UpdateTags from '@graphql/update-tags.gql'; import UpdateTags from '@graphql/update-tags.gql';
import DeleteTag from '@graphql/delete-tag.gql'; import DeleteTag from '@graphql/delete-tag.gql';
import parseError from '@state/parse-error'; import parseError from '@state/parse-error';
import { addTag as validateTag } from '@state/validators';
import Confirm from '@state/confirm'; import Confirm from '@state/confirm';
import { fieldError } from '@root/constants';
const MENU_FORM_NAME = 'instance-tags-list-menu'; const MENU_FORM_NAME = 'instance-tags-list-menu';
const ADD_FORM_NAME = 'instance-tags-add-new'; const ADD_FORM_NAME = 'instance-tags-add-new';
@ -58,10 +58,10 @@ export const Tags = ({
const _add = addOpen ? ( const _add = addOpen ? (
<ReduxForm <ReduxForm
form={ADD_FORM_NAME} form={ADD_FORM_NAME}
onSubmit={handleCreate}
onCancel={() => handleToggleAddOpen(false)}
shouldAsyncValidate={shouldAsyncValidate} shouldAsyncValidate={shouldAsyncValidate}
asyncValidate={handleAsyncValidate} asyncValidate={handleAsyncValidate}
onSubmit={handleCreate}
onCancel={() => handleToggleAddOpen(false)}
> >
{TagsAddForm} {TagsAddForm}
</ReduxForm> </ReduxForm>
@ -208,26 +208,16 @@ export default compose(
}, },
(dispatch, ownProps) => { (dispatch, ownProps) => {
return { return {
shouldAsyncValidate: ({ trigger }) => {
return trigger === 'submit';
},
handleAsyncValidate: validateTag,
handleToggleAddOpen: value => { handleToggleAddOpen: value => {
return dispatch(set({ name: `add-tags-open`, value })); return dispatch(set({ name: `add-tags-open`, value }));
}, },
handleToggleEditing: value => { handleToggleEditing: value => {
return dispatch(set({ name: `editing-tag`, 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 }) => { handleEdit: async ({ name, value }, _, { form, initialValues }) => {
const { instance, deleteTag, updateTags, refetch } = ownProps; 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:lib": "echo 0",
"build:bundle": "NODE_ENV=production redrun build", "build:bundle": "NODE_ENV=production redrun build",
"prepublish": "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", "lint:ci": "NODE_ENV=test eslint . --ext .js --ext .md",
"test": "echo 0", "test": "echo 0",
"test:ci": "redrun test", "test:ci": "redrun test",

View File

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

View File

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

View File

@ -2319,6 +2319,10 @@ case-sensitive-paths-webpack-plugin@2.1.1:
version "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" 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: caseless@~0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 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" inherits "^2.0.1"
readable-stream "^2.0.4" 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: follow-redirects@^1.2.3:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" 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" version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" 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: lodash.reverse@^4.0.1:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.reverse/-/lodash.reverse-4.0.1.tgz#1f2afedace2e16e660f3aa7c59d3300a6f25d13c" 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" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" 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" version "4.17.5"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" 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" loose-envify "^1.3.1"
object-assign "^4.1.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: proxy-addr@~2.0.2:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" 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" version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" 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" version "2.1.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
@ -10943,6 +10959,10 @@ symbol@^0.2.1:
version "0.2.3" version "0.2.3"
resolved "https://registry.yarnpkg.com/symbol/-/symbol-0.2.3.tgz#3b9873b8a901e47c6efe21526a3ac372ef28bbc7" 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: table@^4.0.1:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc"
@ -11197,6 +11217,10 @@ topo@3.x.x:
dependencies: dependencies:
hoek "5.x.x" 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: toposort@^1.0.0:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.6.tgz#c31748e55d210effc00fdcdc7d6e68d7d7bb9cec" 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" media-typer "0.3.0"
mime-types "~2.1.18" 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: typedarray@^0.0.6:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@ -12282,6 +12310,18 @@ yauzl@2.4.1:
dependencies: dependencies:
fd-slicer "~1.0.1" 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: zen-observable-ts@^0.8.6:
version "0.8.8" version "0.8.8"
resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.8.tgz#1a586dc204fa5632a88057f879500e0d2ba06869" resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.8.tgz#1a586dc204fa5632a88057f879500e0d2ba06869"