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="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="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"> <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 <button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none" className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide} onClick={onHide}
@ -145,7 +145,7 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
formik.submitForm(); formik.submitForm();
}} }}
> >
Update Save
</button> </button>
</div> </div>
</> </>

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

@ -1,6 +1,7 @@
import type { IEditServiceForm, IServiceNodeItem } from "../../../types"; import type { IEditServiceForm, IServiceNodeItem } from "../../../types";
import * as yup from "yup"; import * as yup from "yup";
import lodash from "lodash"; import lodash from "lodash";
import { checkArray } from "../../../utils/forms";
const initialValues: IEditServiceForm = { const initialValues: IEditServiceForm = {
imageName: "", imageName: "",
@ -96,15 +97,6 @@ export const getInitialValues = (node?: IServiceNodeItem): IEditServiceForm => {
labels labels
} = serviceConfig; } = 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 environment0: string[] = checkArray(environment, "environment");
const volumes0: string[] = checkArray(volumes, "volumes"); const volumes0: string[] = checkArray(volumes, "volumes");
const ports0: string[] = checkArray(ports, "ports"); 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 { Formik } from "formik";
import * as yup from "yup";
import { XIcon } from "@heroicons/react/outline"; import { XIcon } from "@heroicons/react/outline";
import {
getFinalValues,
getInitialValues,
tabs,
validationSchema
} from "./form-utils";
import General from "./General"; import General from "./General";
import Labels from "./Labels"; import Labels from "./Labels";
import {
topLevelVolumeConfigInitialValues,
volumeConfigCanvasInitialValues
} from "../../../utils";
import { CallbackFunction } from "../../../types"; import { CallbackFunction } from "../../../types";
interface IModalVolumeCreate { interface ICreateVolumeModalProps {
onHide: CallbackFunction; onHide: CallbackFunction;
onAddEndpoint: CallbackFunction; onAddEndpoint: CallbackFunction;
} }
const ModalVolumeCreate = (props: IModalVolumeCreate) => { const classNames = (...classes: string[]) => classes.filter(Boolean).join(" ");
const CreateVolumeModal = (props: ICreateVolumeModalProps) => {
const { onHide, onAddEndpoint } = props; const { onHide, onAddEndpoint } = props;
const [openTab, setOpenTab] = useState("General"); 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(); formik.resetForm();
}; }, []);
const validationSchema = yup.object({
canvasConfig: yup.object({ const initialValues = useMemo(() => getInitialValues(), []);
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(" ");
};
return ( return (
<div className="fixed z-50 inset-0 overflow-y-auto"> <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="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="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"> <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 <button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none" className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide} onClick={onHide}
@ -76,23 +52,9 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
</div> </div>
<Formik <Formik
initialValues={{ initialValues={initialValues}
canvasConfig: {
...volumeConfigCanvasInitialValues()
},
volumeConfig: {
...topLevelVolumeConfigInitialValues()
},
key: "volume",
type: "VOLUME",
inputs: [],
outputs: [],
config: {}
}}
enableReinitialize={true} enableReinitialize={true}
onSubmit={(values, formik) => { onSubmit={handleCreate}
handleCreate(values, formik);
}}
validationSchema={validationSchema} validationSchema={validationSchema}
> >
{(formik) => ( {(formik) => (
@ -133,9 +95,7 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => {
<button <button
className="btn-util" className="btn-util"
type="button" type="button"
onClick={() => { onClick={formik.submitForm}
formik.submitForm();
}}
> >
Add Add
</button> </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 { Formik } from "formik";
import * as yup from "yup";
import { XIcon } from "@heroicons/react/outline"; import { XIcon } from "@heroicons/react/outline";
import General from "./General"; import General from "./General";
import Labels from "./Labels"; import Labels from "./Labels";
import type { CallbackFunction, IVolumeNodeItem } from "../../../types";
import { import {
CallbackFunction, getFinalValues,
ICanvasConfig, getInitialValues,
IVolumeNodeItem, tabs,
IVolumeTopLevel validationSchema
} from "../../../types"; } from "./form-utils";
interface IModalVolumeEdit { interface IEditVolumeModal {
node: IVolumeNodeItem; node: IVolumeNodeItem;
onHide: CallbackFunction; onHide: CallbackFunction;
onUpdateEndpoint: 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 { node, onHide, onUpdateEndpoint } = props;
const [openTab, setOpenTab] = useState("General"); const [openTab, setOpenTab] = useState("General");
const [selectedNode, setSelectedNode] = useState<IVolumeNodeItem>(); 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(() => { useEffect(() => {
if (node) { if (node) {
@ -65,6 +32,15 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
} }
}, [node]); }, [node]);
const handleUpdate = (values: any) => {
onUpdateEndpoint(getFinalValues(values, selectedNode));
};
const initialValues = useMemo(
() => getInitialValues(selectedNode),
[selectedNode]
);
return ( return (
<div className="fixed z-50 inset-0 overflow-y-auto"> <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"> <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="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="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"> <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"> <h3 className="text-sm font-semibold">Edit top level volumes</h3>
Update top level volumes
</h3>
<button <button
className="p-1 ml-auto text-black float-right outline-none focus:outline-none" className="p-1 ml-auto text-black float-right outline-none focus:outline-none"
onClick={onHide} onClick={onHide}
@ -90,18 +64,9 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
{selectedNode && ( {selectedNode && (
<Formik <Formik
initialValues={{ initialValues={initialValues}
canvasConfig: {
...selectedNode.canvasConfig
} as ICanvasConfig,
volumeConfig: {
...selectedNode.volumeConfig
} as IVolumeTopLevel
}}
enableReinitialize={true} enableReinitialize={true}
onSubmit={(values) => { onSubmit={handleUpdate}
handleUpdate(values);
}}
validationSchema={validationSchema} validationSchema={validationSchema}
> >
{(formik) => ( {(formik) => (
@ -145,11 +110,9 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
<button <button
className="btn-util" className="btn-util"
type="button" type="button"
onClick={() => { onClick={formik.submitForm}
formik.submitForm();
}}
> >
Update Save
</button> </button>
</div> </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, IServiceNodeItem,
IVolumeNodeItem, IVolumeNodeItem,
IServiceNodePosition, IServiceNodePosition,
IProject, IProject
INetworkTopLevel
} from "../../types"; } from "../../types";
import eventBus from "../../events/eventBus"; import eventBus from "../../events/eventBus";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
@ -37,8 +36,8 @@ import ModalConfirmDelete from "../Modal/ConfirmDelete";
import ModalServiceCreate from "../Modal/Service/Create"; import ModalServiceCreate from "../Modal/Service/Create";
import ModalServiceEdit from "../Modal/Service/Edit"; import ModalServiceEdit from "../Modal/Service/Edit";
import ModalNetwork from "../Modal/Network"; import ModalNetwork from "../Modal/Network";
import ModalVolumeCreate from "../Modal/Volume/Create"; import CreateVolumeModal from "../Modal/volume/CreateVolumeModal";
import ModalVolumeEdit from "../Modal/Volume/Edit"; import EditVolumeModal from "../Modal/volume/EditVolumeModal";
import CodeEditor from "../CodeEditor"; import CodeEditor from "../CodeEditor";
export default function Project() { export default function Project() {
@ -329,7 +328,7 @@ export default function Project() {
) : null} ) : null}
{showVolumesModal ? ( {showVolumesModal ? (
<ModalVolumeCreate <CreateVolumeModal
onHide={() => setShowVolumesModal(false)} onHide={() => setShowVolumesModal(false)}
onAddEndpoint={(values: any) => onAddEndpoint(values)} onAddEndpoint={(values: any) => onAddEndpoint(values)}
/> />
@ -361,7 +360,7 @@ export default function Project() {
) : null} ) : null}
{volumeToEdit ? ( {volumeToEdit ? (
<ModalVolumeEdit <EditVolumeModal
node={volumeToEdit} node={volumeToEdit}
onHide={() => setVolumeToEdit(null)} onHide={() => setVolumeToEdit(null)}
onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)} onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)}

@ -377,3 +377,12 @@ export interface IEditServiceForm {
value: string; 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