Merge pull request #76 from nuxxapp/feat/volume-forms

Update volume forms state shape
pull/79/head
Artem Golub 3 years ago committed by GitHub
commit 4080943699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -80,7 +80,7 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
<h3 className="text-sm font-semibold">Update service</h3>
<h3 className="text-sm font-semibold">Edit service</h3>
<button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide}
@ -145,7 +145,7 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
formik.submitForm();
}}
>
Update
Save
</button>
</div>
</>

@ -3,7 +3,7 @@ import { PlusIcon } from "@heroicons/react/outline";
import { Button, styled } from "@mui/joy";
import { useFormikContext } from "formik";
import Record from "../../Record";
import { IEditServiceForm, IService } from "../../../types";
import { IEditServiceForm } from "../../../types";
const Root = styled("div")`
display: flex;

@ -1,6 +1,7 @@
import type { IEditServiceForm, IServiceNodeItem } from "../../../types";
import * as yup from "yup";
import lodash from "lodash";
import { checkArray } from "../../../utils/forms";
const initialValues: IEditServiceForm = {
imageName: "",
@ -96,15 +97,6 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => {
labels
} = serviceConfig;
const checkArray = <T>(array: any, name: string): T => {
if (!Array.isArray(array)) {
throw new Error(
`Looks like we encountered a bug. The current implementation expects "${name}" to be an array.`
);
}
return array as unknown as T;
};
const environment0: string[] = checkArray(environment, "environment");
const volumes0: string[] = checkArray(volumes, "volumes");
const ports0: string[] = checkArray(ports, "ports");

@ -1,12 +0,0 @@
import TextField from "../../global/FormElements/TextField";
const General = () => {
return (
<>
<TextField label="Volume name" name="canvasConfig.node_name" />
<TextField label="Name" name="volumeConfig.name" />
</>
);
};
export default General;

@ -1,4 +0,0 @@
const Labels = () => {
return <></>;
};
export default Labels;

@ -1,58 +1,34 @@
import { useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { Formik } from "formik";
import * as yup from "yup";
import { XIcon } from "@heroicons/react/outline";
import {
getFinalValues,
getInitialValues,
tabs,
validationSchema
} from "./form-utils";
import General from "./General";
import Labels from "./Labels";
import {
topLevelVolumeConfigInitialValues,
volumeConfigCanvasInitialValues
} from "../../../utils";
import { CallbackFunction } from "../../../types";
interface IModalVolumeCreate {
interface ICreateVolumeModalProps {
onHide: CallbackFunction;
onAddEndpoint: CallbackFunction;
}
const ModalVolumeCreate = (props: IModalVolumeCreate) => {
const classNames = (...classes: string[]) => classes.filter(Boolean).join(" ");
const CreateVolumeModal = (props: ICreateVolumeModalProps) => {
const { onHide, onAddEndpoint } = props;
const [openTab, setOpenTab] = useState("General");
const handleCreate = (values: any, formik: any) => {
onAddEndpoint(values);
const handleCreate = useCallback((values: any, formik: any) => {
onAddEndpoint(getFinalValues(values));
formik.resetForm();
};
const validationSchema = yup.object({
canvasConfig: yup.object({
node_name: yup
.string()
.max(256, "volume name should be 256 characters or less")
.required("volume name is required")
}),
volumeConfig: yup.object({
name: yup
.string()
.max(256, "name should be 256 characters or less")
.required("name is required")
})
});
const tabs = [
{
name: "General",
href: "#",
current: true,
hidden: false
},
{
name: "Labels",
href: "#",
current: false,
hidden: false
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
}, []);
const initialValues = useMemo(() => getInitialValues(), []);
return (
<div className="fixed z-50 inset-0 overflow-y-auto">
@ -64,7 +40,7 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
<h3 className="text-sm font-semibold">Create top level volume</h3>
<h3 className="text-sm font-semibold">Add top level volume</h3>
<button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide}
@ -76,23 +52,9 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
</div>
<Formik
initialValues={{
canvasConfig: {
...volumeConfigCanvasInitialValues()
},
volumeConfig: {
...topLevelVolumeConfigInitialValues()
},
key: "volume",
type: "VOLUME",
inputs: [],
outputs: [],
config: {}
}}
initialValues={initialValues}
enableReinitialize={true}
onSubmit={(values, formik) => {
handleCreate(values, formik);
}}
onSubmit={handleCreate}
validationSchema={validationSchema}
>
{(formik) => (
@ -133,9 +95,7 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
<button
className="btn-util"
type="button"
onClick={() => {
formik.submitForm();
}}
onClick={formik.submitForm}
>
Add
</button>
@ -150,4 +110,4 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
);
};
export default ModalVolumeCreate;
export default CreateVolumeModal;

@ -1,63 +1,30 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Formik } from "formik";
import * as yup from "yup";
import { XIcon } from "@heroicons/react/outline";
import General from "./General";
import Labels from "./Labels";
import type { CallbackFunction, IVolumeNodeItem } from "../../../types";
import {
CallbackFunction,
ICanvasConfig,
IVolumeNodeItem,
IVolumeTopLevel
} from "../../../types";
getFinalValues,
getInitialValues,
tabs,
validationSchema
} from "./form-utils";
interface IModalVolumeEdit {
interface IEditVolumeModal {
node: IVolumeNodeItem;
onHide: CallbackFunction;
onUpdateEndpoint: CallbackFunction;
}
const ModalVolumeEdit = (props: IModalVolumeEdit) => {
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
const EditVolumeModal = (props: IEditVolumeModal) => {
const { node, onHide, onUpdateEndpoint } = props;
const [openTab, setOpenTab] = useState("General");
const [selectedNode, setSelectedNode] = useState<IVolumeNodeItem>();
const handleUpdate = (values: any) => {
const updated = { ...selectedNode };
updated.canvasConfig = values.canvasConfig;
updated.volumeConfig = values.volumeConfig;
onUpdateEndpoint(updated);
};
const validationSchema = yup.object({
canvasConfig: yup.object({
node_name: yup
.string()
.max(256, "volume name should be 256 characters or less")
.required("volume name is required")
}),
volumeConfig: yup.object({
name: yup
.string()
.max(256, "name should be 256 characters or less")
.required("name is required")
})
});
const tabs = [
{
name: "General",
href: "#",
current: true,
hidden: false
},
{
name: "Labels",
href: "#",
current: false,
hidden: false
}
];
const classNames = (...classes: string[]) => {
return classes.filter(Boolean).join(" ");
};
useEffect(() => {
if (node) {
@ -65,6 +32,15 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
}
}, [node]);
const handleUpdate = (values: any) => {
onUpdateEndpoint(getFinalValues(values, selectedNode));
};
const initialValues = useMemo(
() => getInitialValues(selectedNode),
[selectedNode]
);
return (
<div className="fixed z-50 inset-0 overflow-y-auto">
<div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 outline-none focus:outline-none">
@ -75,9 +51,7 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
<div className="relative w-auto my-6 mx-auto max-w-5xl z-50">
<div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
<div className="flex items-center justify-between px-4 py-3 border-b border-solid border-blueGray-200 rounded-t">
<h3 className="text-sm font-semibold">
Update top level volumes
</h3>
<h3 className="text-sm font-semibold">Edit top level volumes</h3>
<button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide}
@ -90,18 +64,9 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
{selectedNode && (
<Formik
initialValues={{
canvasConfig: {
...selectedNode.canvasConfig
} as ICanvasConfig,
volumeConfig: {
...selectedNode.volumeConfig
} as IVolumeTopLevel
}}
initialValues={initialValues}
enableReinitialize={true}
onSubmit={(values) => {
handleUpdate(values);
}}
onSubmit={handleUpdate}
validationSchema={validationSchema}
>
{(formik) => (
@ -145,11 +110,9 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
<button
className="btn-util"
type="button"
onClick={() => {
formik.submitForm();
}}
onClick={formik.submitForm}
>
Update
Save
</button>
</div>
</>
@ -163,4 +126,4 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
);
};
export default ModalVolumeEdit;
export default EditVolumeModal;

@ -0,0 +1,20 @@
import { styled } from "@mui/joy";
import TextField from "../../global/FormElements/TextField";
const Root = styled("div")`
display: flex;
flex-direction: column;
row-gap: ${({ theme }) => theme.spacing(1)};
`;
const General = () => {
return (
<Root>
<TextField label="Entry name" name="entryName" />
<TextField label="Volume name" name="volumeName" />
</Root>
);
};
export default General;

@ -0,0 +1,98 @@
import { useCallback } from "react";
import { PlusIcon } from "@heroicons/react/outline";
import { Button, styled } from "@mui/joy";
import { useFormikContext } from "formik";
import Record from "../../Record";
import { IEditServiceForm } from "../../../types";
const Root = styled("div")`
display: flex;
flex-direction: column;
align-items: center;
`;
const Records = styled("div")`
display: flex;
flex-direction: column;
row-gap: ${({ theme }) => theme.spacing(1)};
`;
const AddButton = styled(Button)`
width: 140px;
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const Description = styled("p")`
margin-top: ${({ theme }) => theme.spacing(2)};
text-align: center;
color: #7a7a7a;
font-size: 14px;
`;
const Labels = () => {
const formik = useFormikContext<IEditServiceForm>();
const { labels } = formik.values;
const handleNewLabel = useCallback(() => {
formik.setFieldValue(`labels[${labels.length}]`, {
key: "",
value: ""
});
}, [formik]);
const handleRemoveLabel = useCallback(
(index: number) => {
const newLabels = labels.filter(
(_: unknown, currentIndex: number) => currentIndex != index
);
formik.setFieldValue(`labels`, newLabels);
},
[formik]
);
const emptyLabels = labels && labels.length === 0 ? true : false;
return (
<Root>
{!emptyLabels && (
<Records>
{labels.map((_: unknown, index: number) => (
<Record
key={index}
index={index}
fields={[
{
name: `labels[${index}].key`,
placeholder: "Key",
required: true,
type: "text"
},
{
name: `labels[${index}].value`,
placeholder: "Value",
required: true,
type: "text"
}
]}
onRemove={handleRemoveLabel}
/>
))}
</Records>
)}
{emptyLabels && (
<Description>
This volume does not have any labels.
<br />
Click "+ New label" to add a new label.
</Description>
)}
<AddButton size="sm" variant="plain" onClick={handleNewLabel}>
<PlusIcon className="h-4 w-4 mr-2" />
New label
</AddButton>
</Root>
);
};
export default Labels;

@ -0,0 +1,95 @@
import lodash from "lodash";
import * as yup from "yup";
import { IEditVolumeForm, IVolumeNodeItem } from "../../../types";
import { checkArray } from "../../../utils/forms";
export const validationSchema = yup.object({
entryName: yup
.string()
.max(256, "Entry name should be 256 characters or less")
.required("Entry name is required"),
volumeName: yup
.string()
.max(256, "Volume name should be 256 characters or less")
.required("Volume name is required"),
labels: yup.array(
yup.object({
key: yup.string().required("Key is required"),
value: yup.string().required("Value is required")
})
)
});
const initialValues: IEditVolumeForm = {
entryName: "unknown",
volumeName: "unknown",
labels: []
};
export const getInitialValues = (node?: IVolumeNodeItem): IEditVolumeForm => {
if (!node) {
return {
...initialValues
};
}
const { canvasConfig, volumeConfig } = node;
const { node_name = "" } = canvasConfig;
const { name = "", labels } = volumeConfig;
const labels0: string[] = checkArray(labels, "labels");
return {
...initialValues,
entryName: node_name,
volumeName: name,
labels: labels0.map((label) => {
const [key, value] = label.split(":");
return {
key,
value
};
})
};
};
export const getFinalValues = (
values: IEditVolumeForm,
previous?: IVolumeNodeItem
): IVolumeNodeItem => {
const { labels } = values;
return lodash.merge(
lodash.cloneDeep(previous) || {
key: "volume",
type: "VOLUME",
inputs: [],
outputs: [],
config: {}
},
{
canvasConfig: {
node_name: values.entryName
},
volumeConfig: {
name: values.volumeName,
labels: labels.map((label) => `${label.key}:${label.value}`)
}
}
) as any;
};
export const tabs = [
{
name: "General",
href: "#",
current: true,
hidden: false
},
{
name: "Labels",
href: "#",
current: false,
hidden: false
}
];

@ -9,8 +9,7 @@ import {
IServiceNodeItem,
IVolumeNodeItem,
IServiceNodePosition,
IProject,
INetworkTopLevel
IProject
} from "../../types";
import eventBus from "../../events/eventBus";
import { useMutation } from "react-query";
@ -37,8 +36,8 @@ import ModalConfirmDelete from "../Modal/ConfirmDelete";
import ModalServiceCreate from "../Modal/Service/Create";
import ModalServiceEdit from "../Modal/Service/Edit";
import ModalNetwork from "../Modal/Network";
import ModalVolumeCreate from "../Modal/Volume/Create";
import ModalVolumeEdit from "../Modal/Volume/Edit";
import CreateVolumeModal from "../Modal/volume/CreateVolumeModal";
import EditVolumeModal from "../Modal/volume/EditVolumeModal";
import CodeEditor from "../CodeEditor";
export default function Project() {
@ -329,7 +328,7 @@ export default function Project() {
) : null}
{showVolumesModal ? (
<ModalVolumeCreate
<CreateVolumeModal
onHide={() => setShowVolumesModal(false)}
onAddEndpoint={(values: any) => onAddEndpoint(values)}
/>
@ -361,7 +360,7 @@ export default function Project() {
) : null}
{volumeToEdit ? (
<ModalVolumeEdit
<EditVolumeModal
node={volumeToEdit}
onHide={() => setVolumeToEdit(null)}
onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)}

@ -377,3 +377,12 @@ export interface IEditServiceForm {
value: string;
}[];
}
export interface IEditVolumeForm {
entryName: string;
volumeName: string;
labels: {
key: string;
value: string;
}[];
}

@ -0,0 +1,8 @@
export const checkArray = <T>(array: any, name: string): T => {
if (!Array.isArray(array)) {
throw new Error(
`Looks like we encountered a bug. The current implementation expects "${name}" to be an array.`
);
}
return array as unknown as T;
};
Loading…
Cancel
Save