feat: improved validation of attrs
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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`,
|
||||
|
@ -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 }));
|
||||
},
|
||||
|
67
packages/my-joy-images/src/state/validators.js
Normal 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 });
|
@ -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",
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@ -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"
|
||||
>
|
||||
|
@ -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
|
||||
>
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@ -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",
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const fieldError =
|
||||
'Please enter only letters, numbers, periods (.), underscores (_), and hyphens (-).';
|
@ -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}
|
||||
form={FORM_NAME_EDIT}
|
||||
initialValues={exitingRule}
|
||||
destroyOnUnmount={false}
|
||||
forceUnregisterOnUnmount={true}
|
||||
forceUnregisterOnUnmount={false}
|
||||
shouldAsyncValidate={shouldAsyncValidate}
|
||||
asyncValidation={handleAsyncValidation}
|
||||
onSubmit={newValue => handleUpdateAffinityRule(index, newValue)}
|
||||
asyncValidation={handleAsyncValidate}
|
||||
onSubmit={handleUpdateAffinityRule}
|
||||
>
|
||||
{props => (
|
||||
{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>
|
||||
)}
|
||||
) : 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 })
|
||||
]);
|
||||
}
|
||||
}))
|
||||
|
@ -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)
|
||||
})
|
||||
]);
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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`,
|
||||
|
@ -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 })
|
||||
|
@ -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`,
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 9.1 KiB |
@ -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);
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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 (
|
||||
{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 (
|
||||
|
@ -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;
|
||||
|
||||
|
6
packages/my-joy-instances/src/graphql/get-snapshot.gql
Normal file
@ -0,0 +1,6 @@
|
||||
query snapshot($machine: ID!, $name: String!) {
|
||||
snapshot(machine: $machine, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
112
packages/my-joy-instances/src/state/validators.js
Normal 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;
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -40,6 +40,10 @@ const StyledLabel = Label.extend`
|
||||
${is('small')`
|
||||
width: ${remcalc(120)};
|
||||
`};
|
||||
|
||||
${is('absolute')`
|
||||
position: absolute;
|
||||
`};
|
||||
`;
|
||||
|
||||
const Meta = props => {
|
||||
|
44
yarn.lock
@ -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"
|
||||
|