diff --git a/services/frontend/package.json b/services/frontend/package.json index da60b62..a8f4ad1 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -48,7 +48,8 @@ "typescript": "^4.5.5", "uuid": "^8.3.2", "web-vitals": "^2.1.4", - "yaml": "^1.10.2" + "yaml": "^1.10.2", + "yup": "^0.32.11" }, "scripts": { "start": "react-scripts start", diff --git a/services/frontend/src/components/Canvas/ServiceNode.tsx b/services/frontend/src/components/Canvas/ServiceNode.tsx index 48e4a0b..cbd3d71 100644 --- a/services/frontend/src/components/Canvas/ServiceNode.tsx +++ b/services/frontend/src/components/Canvas/ServiceNode.tsx @@ -1,4 +1,6 @@ import { useEffect, useState } from "react"; +import { ServerIcon } from "@heroicons/react/outline"; +import { truncateStr } from "../../utils"; import { IServiceNodeItem, CallbackFunction } from "../../types"; import eventBus from "../../events/eventBus"; import { Popover } from "./Popover"; @@ -52,14 +54,21 @@ export default function ServiceNode(props: INodeProps) { }} > )} -
+
<> -
- {node.canvasConfig.service_name} -
-
- {node.serviceConfig.container_name} -
+ {node.canvasConfig.service_name && ( +
+ {truncateStr(node.canvasConfig.service_name, 12)} +
+ )} + + {node.serviceConfig.container_name && ( +
+ {truncateStr(node.serviceConfig.container_name, 20)} +
+ )} + +
diff --git a/services/frontend/src/components/Canvas/VolumeNode.tsx b/services/frontend/src/components/Canvas/VolumeNode.tsx index b067d74..759f2dd 100644 --- a/services/frontend/src/components/Canvas/VolumeNode.tsx +++ b/services/frontend/src/components/Canvas/VolumeNode.tsx @@ -1,4 +1,6 @@ import { useEffect, useState } from "react"; +import { DatabaseIcon } from "@heroicons/react/outline"; +import { truncateStr } from "../../utils"; import { IVolumeNodeItem, CallbackFunction } from "../../types"; import eventBus from "../../events/eventBus"; import { Popover } from "./Popover"; @@ -52,11 +54,15 @@ export default function VolumeNode(props: INodeProps) { }} > )} -
+
<> -
- {node.volumeConfig.name} -
+ {node.volumeConfig.name && ( +
+ {truncateStr(node.volumeConfig.name, 20)} +
+ )} + +
diff --git a/services/frontend/src/components/Modal/Network/General.tsx b/services/frontend/src/components/Modal/Network/General.tsx index 00639c8..7d006fd 100644 --- a/services/frontend/src/components/Modal/Network/General.tsx +++ b/services/frontend/src/components/Modal/Network/General.tsx @@ -1,32 +1,11 @@ -const General = (props: any) => { - const { formik } = props; +import TextField from "../../global/FormElements/InputField"; +const General = () => { return ( <> -
-
-
- -
- -
-
-
-
+ ); }; + export default General; diff --git a/services/frontend/src/components/Modal/Network/IPam.tsx b/services/frontend/src/components/Modal/Network/IPam.tsx index 05b471f..2ea3f71 100644 --- a/services/frontend/src/components/Modal/Network/IPam.tsx +++ b/services/frontend/src/components/Modal/Network/IPam.tsx @@ -1,6 +1,5 @@ -const IPam = (props: any) => { - const { formik } = props; - +const IPam = () => { return <>; }; + export default IPam; diff --git a/services/frontend/src/components/Modal/Network/Labels.tsx b/services/frontend/src/components/Modal/Network/Labels.tsx index 9838a34..12d9e27 100644 --- a/services/frontend/src/components/Modal/Network/Labels.tsx +++ b/services/frontend/src/components/Modal/Network/Labels.tsx @@ -1,6 +1,5 @@ -const Labels = (props: any) => { - const { formik } = props; - +const Labels = () => { return <>; }; + export default Labels; diff --git a/services/frontend/src/components/Modal/Network/index.tsx b/services/frontend/src/components/Modal/Network/index.tsx index c65ccf4..de0e46a 100644 --- a/services/frontend/src/components/Modal/Network/index.tsx +++ b/services/frontend/src/components/Modal/Network/index.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { useFormik } from "formik"; +import { Formik } from "formik"; +import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; import General from "./General"; import IPam from "./IPam"; @@ -14,12 +15,14 @@ interface IModalNetworkProps { const ModalNetwork = (props: IModalNetworkProps) => { const { onHide } = props; const [openTab, setOpenTab] = useState("General"); - - const formik = useFormik({ - initialValues: { - ...topLevelNetworkConfigInitialValues() - }, - onSubmit: () => undefined + const handleCreate = (values: any, formik: any) => { + formik.resetForm(); + }; + const validationSchema = yup.object({ + name: yup + .string() + .max(256, "name should be 256 characters or less") + .required("name is required") }); const tabs = [ { @@ -66,54 +69,70 @@ const ModalNetwork = (props: IModalNetworkProps) => { -
-
-
- -
-
+ { + handleCreate(values, formik); + }} + validationSchema={validationSchema} + > + {(formik) => ( + <> +
+
+ +
+
-
-
- {openTab === "General" && } - {openTab === "IPam" && } - {openTab === "Labels" && } - -
-
+
+ {openTab === "General" && } + {openTab === "IPam" && } + {openTab === "Labels" && } +
-
- -
+
+ +
+ + )} + diff --git a/services/frontend/src/components/Modal/Service/Create.tsx b/services/frontend/src/components/Modal/Service/Create.tsx index dfe9f53..aba5516 100644 --- a/services/frontend/src/components/Modal/Service/Create.tsx +++ b/services/frontend/src/components/Modal/Service/Create.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { useFormik } from "formik"; +import { Formik } from "formik"; +import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; import General from "./General"; import Environment from "./Environment"; @@ -16,24 +17,23 @@ interface IModalServiceProps { const ModalServiceCreate = (props: IModalServiceProps) => { const { onHide, onAddEndpoint } = props; const [openTab, setOpenTab] = useState("General"); - - const formik = useFormik({ - initialValues: { - canvasConfig: { - ...serviceConfigCanvasInitialValues() - }, - serviceConfig: { - container_name: "", - labels: [], - environmentVariables: [] - }, - key: "service", - type: "SERVICE", - inputs: ["op_source"], - outputs: [], - config: {} - }, - onSubmit: () => undefined + const handleCreate = (values: any, formik: any) => { + onAddEndpoint(values); + formik.resetForm(); + }; + const validationSchema = yup.object({ + canvasConfig: yup.object({ + service_name: yup + .string() + .max(256, "service name should be 256 characters or less") + .required("service name is required") + }), + serviceConfig: yup.object({ + container_name: yup + .string() + .max(256, "container name should be 256 characters or less") + .required("container name is required") + }) }); const tabs = [ { @@ -86,56 +86,77 @@ const ModalServiceCreate = (props: IModalServiceProps) => { -
-
-
- -
-
+ { + handleCreate(values, formik); + }} + validationSchema={validationSchema} + > + {(formik) => ( + <> +
+
+ +
+
-
-
- {openTab === "General" && } - {openTab === "Environment" && } - {openTab === "Volumes" && } - {openTab === "Labels" && } - -
-
+
+ {openTab === "General" && } + {openTab === "Environment" && } + {openTab === "Volumes" && } + {openTab === "Labels" && } +
-
- -
+
+ +
+ + )} + diff --git a/services/frontend/src/components/Modal/Service/Edit.tsx b/services/frontend/src/components/Modal/Service/Edit.tsx index 478712a..baa8699 100644 --- a/services/frontend/src/components/Modal/Service/Edit.tsx +++ b/services/frontend/src/components/Modal/Service/Edit.tsx @@ -1,16 +1,16 @@ import { useState, useEffect } from "react"; -import { useFormik } from "formik"; +import { Formik } from "formik"; +import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; import General from "./General"; import Environment from "./Environment"; import Volumes from "./Volumes"; import Labels from "./Labels"; -import { serviceConfigCanvasInitialValues } from "../../../utils"; import { CallbackFunction, ICanvasConfig, - IServiceNodeItem, - IService + IService, + IServiceNodeItem } from "../../../types"; interface IModalServiceProps { @@ -23,18 +23,26 @@ const ModalServiceEdit = (props: IModalServiceProps) => { const { node, onHide, onUpdateEndpoint } = props; const [openTab, setOpenTab] = useState("General"); const [selectedNode, setSelectedNode] = useState(); - const formik = useFormik({ - initialValues: { - canvasConfig: { - ...serviceConfigCanvasInitialValues() - }, - serviceConfig: { - container_name: "", - labels: [], - environmentVariables: [] - } - }, - onSubmit: () => undefined + + const handleUpdate = (values: any) => { + const updated = { ...selectedNode }; + updated.canvasConfig = values.canvasConfig; + updated.serviceConfig = values.serviceConfig; + onUpdateEndpoint(updated); + }; + const validationSchema = yup.object({ + canvasConfig: yup.object({ + service_name: yup + .string() + .max(256, "service name should be 256 characters or less") + .required("service name is required") + }), + serviceConfig: yup.object({ + container_name: yup + .string() + .max(256, "container name should be 256 characters or less") + .required("container name is required") + }) }); const tabs = [ { @@ -72,25 +80,6 @@ const ModalServiceEdit = (props: IModalServiceProps) => { } }, [node]); - useEffect(() => { - formik.resetForm(); - - if (selectedNode) { - formik.initialValues.canvasConfig = { - ...selectedNode.canvasConfig - } as ICanvasConfig; - formik.initialValues.serviceConfig = { - ...selectedNode.serviceConfig - } as any; // as IService; - } - }, [selectedNode]); - - useEffect(() => { - return () => { - formik.resetForm(); - }; - }, []); - return (
@@ -112,58 +101,76 @@ const ModalServiceEdit = (props: IModalServiceProps) => {
-
-
- -
- -
-
- {openTab === "General" && } - {openTab === "Environment" && } - {openTab === "Volumes" && } - {openTab === "Labels" && } - -
-
- -
- -
+ {(formik) => ( + <> +
+
+ +
+
+ +
+ {openTab === "General" && } + {openTab === "Environment" && } + {openTab === "Volumes" && } + {openTab === "Labels" && } +
+ +
+ +
+ + )} + + )}
diff --git a/services/frontend/src/components/Modal/Service/Environment.tsx b/services/frontend/src/components/Modal/Service/Environment.tsx index 5dc33f5..7123d1d 100644 --- a/services/frontend/src/components/Modal/Service/Environment.tsx +++ b/services/frontend/src/components/Modal/Service/Environment.tsx @@ -1,8 +1,9 @@ +import { useCallback } from "react"; import { PlusIcon } from "@heroicons/react/outline"; -import Record from "../../Record"; -import { Button } from "@mui/joy"; import { styled } from "@mui/joy"; -import { useCallback } from "react"; +import { useFormikContext } from "formik"; +import Record from "../../Record"; +import { IService } from "../../../types"; const Root = styled("div")` display: flex; @@ -12,87 +13,80 @@ const Root = styled("div")` 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)}; + row-gap: ${({ theme }: { theme: any }) => theme.spacing(1)}; `; -const EnvironmentVariables = (props: any) => { - const { formik } = props; - - const { environmentVariables } = formik.values.serviceConfig; +const Environment = () => { + const formik = useFormikContext<{ + serviceConfig: IService; + }>(); + const environment = (formik.values.serviceConfig.environment as []) || []; const handleNewEnvironmentVariable = useCallback(() => { - formik.setFieldValue( - `serviceConfig.environmentVariables[${environmentVariables.length}]`, - { - key: "", - value: "" - } - ); + formik.setFieldValue(`serviceConfig.environment[${environment.length}]`, { + key: "", + value: "" + }); }, [formik]); const handleRemoveEnvironmentVariable = useCallback( (index: number) => { - const newEnvironmentVariables = environmentVariables.filter( + const newEnvironmentVariables = environment.filter( (_: unknown, currentIndex: number) => currentIndex != index ); formik.setFieldValue( - `serviceConfig.environmentVariables`, + `serviceConfig.environment`, newEnvironmentVariables ); }, [formik] ); - const emptyEnvironmentVariables = environmentVariables.length === 0; + const emptyEnvironmentVariables = + environment && environment.length === 0 ? true : false; return ( - - {!emptyEnvironmentVariables && ( - - {environmentVariables.map((_: unknown, index: number) => ( - - ))} - - )} - {emptyEnvironmentVariables && ( -

- The service does not have any environment variables. -
- Click "+ New Variable" to add a new environment variable. -

- )} - - + - - New Variable - -
+ {!emptyEnvironmentVariables && ( + + {environment.map((_: unknown, index: number) => ( + + ))} + + )} + + {emptyEnvironmentVariables && ( +

+ add environment variables +

+ )} + + +
+ +
+ ); }; -export default EnvironmentVariables; +export default Environment; diff --git a/services/frontend/src/components/Modal/Service/General.tsx b/services/frontend/src/components/Modal/Service/General.tsx index 3ae0c59..40b26e4 100644 --- a/services/frontend/src/components/Modal/Service/General.tsx +++ b/services/frontend/src/components/Modal/Service/General.tsx @@ -1,56 +1,12 @@ -const General = (props: any) => { - const { formik } = props; +import TextField from "../../global/FormElements/InputField"; +const General = () => { return ( <> -
-
-
- -
- -
-
-
-
- -
-
-
- -
- -
-
-
-
+ + ); }; + export default General; diff --git a/services/frontend/src/components/Modal/Service/Labels.tsx b/services/frontend/src/components/Modal/Service/Labels.tsx index 95e571c..7ae6d92 100644 --- a/services/frontend/src/components/Modal/Service/Labels.tsx +++ b/services/frontend/src/components/Modal/Service/Labels.tsx @@ -1,8 +1,9 @@ +import { useCallback } from "react"; import { PlusIcon } from "@heroicons/react/outline"; -import Record from "../../Record"; -import { Button } from "@mui/joy"; import { styled } from "@mui/joy"; -import { useCallback } from "react"; +import { useFormikContext } from "formik"; +import Record from "../../Record"; +import { IService } from "../../../types"; const Root = styled("div")` display: flex; @@ -15,15 +16,11 @@ const Records = styled("div")` row-gap: ${({ theme }) => theme.spacing(1)}; `; -const AddButton = styled(Button)` - width: 120px; - margin-top: ${({ theme }) => theme.spacing(2)}; -`; - -const Labels = (props: any) => { - const { formik } = props; - - const { labels } = formik.values.serviceConfig; +const Labels = () => { + const formik = useFormikContext<{ + serviceConfig: IService; + }>(); + const labels = (formik.values.serviceConfig.labels as []) || []; const handleNewLabel = useCallback(() => { formik.setFieldValue(`serviceConfig.labels[${labels.length}]`, { @@ -42,45 +39,48 @@ const Labels = (props: any) => { [formik] ); - const emptyLabels = labels.length === 0; + const emptyLabels = labels && labels.length === 0 ? true : false; return ( - - {!emptyLabels && ( - - {labels.map((_: unknown, index: number) => ( - - ))} - - )} - {emptyLabels && ( -

- The service does not have any labels. -
- Click "+ New Label" to add a new label. -

- )} + <> + + {!emptyLabels && ( + + {labels.map((_: unknown, index: number) => ( + + ))} + + )} + {emptyLabels && ( +

+ add labels +

+ )} +
- - - New Label - -
+
+ +
+ ); }; + export default Labels; diff --git a/services/frontend/src/components/Modal/Service/Volumes.tsx b/services/frontend/src/components/Modal/Service/Volumes.tsx index 8ed7f94..054bed2 100644 --- a/services/frontend/src/components/Modal/Service/Volumes.tsx +++ b/services/frontend/src/components/Modal/Service/Volumes.tsx @@ -1,6 +1,5 @@ -const Volumes = (props: any) => { - const { formik } = props; - +const Volumes = () => { return <>; }; + export default Volumes; diff --git a/services/frontend/src/components/Modal/Volume/Create.tsx b/services/frontend/src/components/Modal/Volume/Create.tsx index cde3c62..6d8ef0c 100644 --- a/services/frontend/src/components/Modal/Volume/Create.tsx +++ b/services/frontend/src/components/Modal/Volume/Create.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; -import { useFormik } from "formik"; +import { Formik } from "formik"; +import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; import General from "./General"; import Labels from "./Labels"; @@ -14,19 +15,17 @@ interface IModalVolumeCreate { const ModalVolumeCreate = (props: IModalVolumeCreate) => { const { onHide, onAddEndpoint } = props; const [openTab, setOpenTab] = useState("General"); - - const formik = useFormik({ - initialValues: { - volumeConfig: { - ...topLevelVolumeConfigInitialValues() - }, - key: "volume", - type: "VOLUME", - inputs: [], - outputs: [], - config: {} - }, - onSubmit: () => undefined + const handleCreate = (values: any, formik: any) => { + onAddEndpoint(values); + formik.resetForm(); + }; + const validationSchema = yup.object({ + volumeConfig: yup.object({ + name: yup + .string() + .max(256, "name should be 256 characters or less") + .required("name is required") + }) }); const tabs = [ { @@ -67,54 +66,71 @@ const ModalVolumeCreate = (props: IModalVolumeCreate) => { -
- + { + handleCreate(values, formik); + }} + validationSchema={validationSchema} + > + {(formik) => ( + <> + -
-
- {openTab === "General" && } - {openTab === "Labels" && } - -
-
+
+ {openTab === "General" && } + {openTab === "Labels" && } +
-
- -
+
+ +
+ + )} + diff --git a/services/frontend/src/components/Modal/Volume/Edit.tsx b/services/frontend/src/components/Modal/Volume/Edit.tsx index 06e452d..2424560 100644 --- a/services/frontend/src/components/Modal/Volume/Edit.tsx +++ b/services/frontend/src/components/Modal/Volume/Edit.tsx @@ -1,14 +1,10 @@ import { useEffect, useState } from "react"; -import { useFormik } from "formik"; +import { Formik } from "formik"; +import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; import General from "./General"; import Labels from "./Labels"; -import { topLevelVolumeConfigInitialValues } from "../../../utils"; -import { - CallbackFunction, - IVolumeNodeItem, - IVolumeTopLevel -} from "../../../types"; +import { CallbackFunction, IVolumeNodeItem } from "../../../types"; interface IModalVolumeEdit { node: IVolumeNodeItem; @@ -20,14 +16,18 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => { const { node, onHide, onUpdateEndpoint } = props; const [openTab, setOpenTab] = useState("General"); const [selectedNode, setSelectedNode] = useState(); - - const formik = useFormik({ - initialValues: { - volumeConfig: { - ...topLevelVolumeConfigInitialValues() - } - }, - onSubmit: () => undefined + const handleUpdate = (values: any) => { + const updated = { ...selectedNode }; + updated.volumeConfig = values.volumeConfig; + onUpdateEndpoint(updated); + }; + const validationSchema = yup.object({ + volumeConfig: yup.object({ + name: yup + .string() + .max(256, "name should be 256 characters or less") + .required("name is required") + }) }); const tabs = [ { @@ -53,22 +53,6 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => { } }, [node]); - useEffect(() => { - formik.resetForm(); - - if (selectedNode) { - formik.initialValues.volumeConfig = { - ...selectedNode.volumeConfig - } as IVolumeTopLevel; - } - }, [selectedNode]); - - useEffect(() => { - return () => { - formik.resetForm(); - }; - }, []); - return (
@@ -92,55 +76,71 @@ const ModalVolumeEdit = (props: IModalVolumeEdit) => {
-
- - -
-
- {openTab === "General" && } - {openTab === "Labels" && } - -
-
- -
- -
+ {(formik) => ( + <> + + +
+ {openTab === "General" && } + {openTab === "Labels" && } +
+ +
+ +
+ + )} + + )}
diff --git a/services/frontend/src/components/Modal/Volume/General.tsx b/services/frontend/src/components/Modal/Volume/General.tsx index 1e18ea2..01d817a 100644 --- a/services/frontend/src/components/Modal/Volume/General.tsx +++ b/services/frontend/src/components/Modal/Volume/General.tsx @@ -1,32 +1,11 @@ -const General = (props: any) => { - const { formik } = props; +import TextField from "../../global/FormElements/InputField"; +const General = () => { return ( <> -
-
-
- -
- -
-
-
-
+ ); }; + export default General; diff --git a/services/frontend/src/components/Modal/Volume/Labels.tsx b/services/frontend/src/components/Modal/Volume/Labels.tsx index 9838a34..710b9e5 100644 --- a/services/frontend/src/components/Modal/Volume/Labels.tsx +++ b/services/frontend/src/components/Modal/Volume/Labels.tsx @@ -1,6 +1,4 @@ -const Labels = (props: any) => { - const { formik } = props; - +const Labels = () => { return <>; }; export default Labels; diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index ec7b9a2..19ffb9f 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -245,9 +245,9 @@ export default function Project() { if (existingIndex !== -1) { _connections.splice(existingIndex, 1); + setConnections(_connections); + stateConnectionsRef.current = _connections; } - - setConnections(_connections); }; const onConnectionAttached = (data: any) => { diff --git a/services/frontend/src/components/global/FormElements/InputField.tsx b/services/frontend/src/components/global/FormElements/InputField.tsx new file mode 100644 index 0000000..f3d0480 --- /dev/null +++ b/services/frontend/src/components/global/FormElements/InputField.tsx @@ -0,0 +1,50 @@ +import _ from "lodash"; +import { useFormikContext } from "formik"; + +interface Props { + name: string; + help?: string; + [key: string]: any; +} + +const TextField = (props: Props) => { + const { label, name, help, ...otherProps } = props; + const formik = useFormikContext(); + const error = _.get(formik.touched, name) && _.get(formik.errors, name); + + return ( +
+
+
+ +
+ + { +
+ {error &&
{error}
} + {!error && help} +
+ } +
+
+
+
+ ); +}; + +export default TextField;