diff --git a/services/frontend/src/components/Modal/Network/Create.tsx b/services/frontend/src/components/Modal/Network/Create.tsx new file mode 100644 index 0000000..455e673 --- /dev/null +++ b/services/frontend/src/components/Modal/Network/Create.tsx @@ -0,0 +1,132 @@ +import { useState } from "react"; +import { Formik } from "formik"; +import * as yup from "yup"; +import { + topLevelNetworkConfigInitialValues, + networkConfigCanvasInitialValues +} from "../../../utils"; +import General from "./General"; +import IPam from "./IPam"; +import Labels from "./Labels"; +import { CallbackFunction } from "../../../types"; + +interface INetworkCreateProps { + onCreateNetwork: CallbackFunction; +} + +const NetworkCreate = (props: INetworkCreateProps) => { + const { onCreateNetwork } = props; + const [openTab, setOpenTab] = useState("General"); + const handleCreate = (values: any, formik: any) => { + onCreateNetwork(values); + formik.resetForm(); + }; + const validationSchema = yup.object({ + canvasConfig: yup.object({ + node_name: yup + .string() + .max(256, "network name should be 256 characters or less") + .required("network name is required") + }), + networkConfig: 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: "Ipam", + href: "#", + current: false, + hidden: false + }, + { + name: "Labels", + href: "#", + current: false, + hidden: false + } + ]; + const classNames = (...classes: string[]) => { + return classes.filter(Boolean).join(" "); + }; + + return ( + { + handleCreate(values, formik); + }} + validationSchema={validationSchema} + > + {(formik) => ( + <> +
+
+ +
+
+ +
+ {openTab === "General" && } + {openTab === "IPam" && } + {openTab === "Labels" && } +
+ +
+ +
+ + )} +
+ ); +}; + +export default NetworkCreate; diff --git a/services/frontend/src/components/Modal/Network/Edit.tsx b/services/frontend/src/components/Modal/Network/Edit.tsx new file mode 100644 index 0000000..ede89a5 --- /dev/null +++ b/services/frontend/src/components/Modal/Network/Edit.tsx @@ -0,0 +1,143 @@ +import { useState } from "react"; +import { Formik } from "formik"; +import * as yup from "yup"; +import { TrashIcon } from "@heroicons/react/outline"; +import General from "./General"; +import IPam from "./IPam"; +import Labels from "./Labels"; +import { + CallbackFunction, + ICanvasConfig, + INetworkTopLevel +} from "../../../types"; + +interface INetworkEditProps { + onUpdateNetwork: CallbackFunction; + onDeleteNetwork: CallbackFunction; + network: any; +} + +const NetworkEdit = (props: INetworkEditProps) => { + const { onUpdateNetwork, onDeleteNetwork, network } = props; + const [openTab, setOpenTab] = useState("General"); + const handleUpdate = (values: any) => { + const updated = { ...network }; + updated.canvasConfig = values.canvasConfig; + updated.networkConfig = values.networkConfig; + onUpdateNetwork(updated); + }; + const validationSchema = yup.object({ + canvasConfig: yup.object({ + node_name: yup + .string() + .max(256, "network name should be 256 characters or less") + .required("network name is required") + }), + networkConfig: 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: "Ipam", + href: "#", + current: false, + hidden: false + }, + { + name: "Labels", + href: "#", + current: false, + hidden: false + } + ]; + const classNames = (...classes: string[]) => { + return classes.filter(Boolean).join(" "); + }; + + return ( + { + handleUpdate(values); + }} + validationSchema={validationSchema} + > + {(formik) => ( + <> +
+
+ +
+
+ +
+ {openTab === "General" && } + {openTab === "IPam" && } + {openTab === "Labels" && } +
+ +
+ + + +
+ + )} +
+ ); +}; + +export default NetworkEdit; diff --git a/services/frontend/src/components/Modal/Network/General.tsx b/services/frontend/src/components/Modal/Network/General.tsx index 7d006fd..b2997a5 100644 --- a/services/frontend/src/components/Modal/Network/General.tsx +++ b/services/frontend/src/components/Modal/Network/General.tsx @@ -3,7 +3,8 @@ import TextField from "../../global/FormElements/InputField"; const General = () => { return ( <> - + + ); }; diff --git a/services/frontend/src/components/Modal/Network/index.tsx b/services/frontend/src/components/Modal/Network/index.tsx index de0e46a..43fb0ea 100644 --- a/services/frontend/src/components/Modal/Network/index.tsx +++ b/services/frontend/src/components/Modal/Network/index.tsx @@ -1,51 +1,50 @@ import { useState } from "react"; -import { Formik } from "formik"; -import * as yup from "yup"; import { XIcon } from "@heroicons/react/outline"; -import General from "./General"; -import IPam from "./IPam"; -import Labels from "./Labels"; -import { topLevelNetworkConfigInitialValues } from "../../../utils"; +import NetworkCreate from "./Create"; import { CallbackFunction } from "../../../types"; +import NetworkEdit from "./Edit"; +import { attachUUID } from "../../../utils"; interface IModalNetworkProps { + networks: Record; + onCreateNetwork: CallbackFunction; + onUpdateNetwork: CallbackFunction; + onDeleteNetwork: CallbackFunction; onHide: CallbackFunction; } const ModalNetwork = (props: IModalNetworkProps) => { - const { onHide } = props; - const [openTab, setOpenTab] = useState("General"); - const handleCreate = (values: any, formik: any) => { - formik.resetForm(); + const { + networks, + onCreateNetwork, + onUpdateNetwork, + onDeleteNetwork, + onHide + } = props; + const [selectedNetwork, setSelectedNetwork] = useState(); + const handleCreate = (values: any) => { + const uniqueKey = attachUUID(values.key); + const network = { + ...values, + key: uniqueKey + }; + onCreateNetwork(network); + setSelectedNetwork(network); }; - const validationSchema = 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: "Ipam", - href: "#", - current: false, - hidden: false - }, - { - name: "Labels", - href: "#", - current: false, - hidden: false - } - ]; - const classNames = (...classes: string[]) => { - return classes.filter(Boolean).join(" "); + const handleUpdate = (values: any) => { + onUpdateNetwork(values); + setSelectedNetwork(values); + }; + const handleDelete = () => { + onDeleteNetwork(selectedNetwork.key); + setSelectedNetwork(null); + }; + const handleNew = () => { + setSelectedNetwork(null); + }; + const onNetworkSelect = (e: any) => { + const networkUuid = e.target.value; + setSelectedNetwork(networks[networkUuid]); }; return ( @@ -69,70 +68,46 @@ const ModalNetwork = (props: IModalNetworkProps) => { - { - handleCreate(values, formik); - }} - validationSchema={validationSchema} - > - {(formik) => ( - <> - + {networks && Object.keys(networks).length > 0 && ( +
+ + + {selectedNetwork && ( + + )} +
+ )} -
- {openTab === "General" && } - {openTab === "IPam" && } - {openTab === "Labels" && } -
+ {!selectedNetwork && ( + + )} -
- -
- - )} -
+ {selectedNetwork && ( + + )} diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index 1f48fb6..1a07042 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -8,7 +8,8 @@ import { IServiceNodeItem, IVolumeNodeItem, IServiceNodePosition, - IProject + IProject, + INetworkTopLevel } from "../../types"; import eventBus from "../../events/eventBus"; import { useMutation } from "react-query"; @@ -18,7 +19,7 @@ import { createProject } from "../../hooks/useProject"; import useWindowDimensions from "../../hooks/useWindowDimensions"; -import { flattenGraphData } from "../../utils/generators"; +import { generatePayload } from "../../utils/generators"; import { nodeLibraries } from "../../utils/data/libraries"; import { getClientNodeItem, @@ -46,6 +47,7 @@ export default function Project() { const stateNodesRef = useRef>(); const stateConnectionsRef = useRef<[[string, string]] | []>(); + const stateNetworksRef = useRef({}); const [generatedCode, setGeneratedCode] = useState(); const [formattedCode, setFormattedCode] = useState(""); @@ -67,6 +69,7 @@ export default function Project() { const [copyText, setCopyText] = useState("Copy"); const [nodes, setNodes] = useState({}); const [connections, setConnections] = useState<[[string, string]] | []>([]); + const [networks, setNetworks] = useState>({}); const [projectName, setProjectName] = useState("Untitled"); const [canvasPosition, setCanvasPosition] = useState({ top: 0, @@ -87,6 +90,7 @@ export default function Project() { stateNodesRef.current = nodes; stateConnectionsRef.current = connections; + stateNetworksRef.current = networks; const handleNameChange = (e: any) => { setProjectName(e.target.value); @@ -109,7 +113,8 @@ export default function Project() { canvas: { position: canvasPosition, nodes: nodes, - connections: connections + connections: connections, + networks: networks } } }; @@ -152,6 +157,7 @@ export default function Project() { setProjectName(data.name); setNodes(clientNodeItems); setConnections(canvasData.canvas.connections); + setNetworks(canvasData.canvas.networks); setCanvasPosition(canvasData.canvas.position); }, [data]); @@ -166,7 +172,8 @@ export default function Project() { const debouncedOnGraphUpdate = useMemo( () => debounce((graphData) => { - const flatData = flattenGraphData(graphData); + graphData.networks = stateNetworksRef.current; + const flatData = generatePayload(graphData); generateHttp(flatData) .then(checkHttpStatus) .then((data) => { @@ -220,6 +227,26 @@ export default function Project() { setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem }); }; + const onCreateNetwork = (values: any) => { + setNetworks({ ...networks, [values.key]: values }); + }; + + const onUpdateNetwork = (values: any) => { + setNetworks({ ...networks, [values.key]: values }); + }; + + const onDeleteNetwork = (uuid: string) => { + const _networks = Object.keys(networks).reduce((ret: any, key) => { + if (networks[key].key !== uuid) { + ret[key] = networks[key]; + } + + return ret; + }, {}); + + setNetworks({ ..._networks }); + }; + const onUpdateEndpoint = (nodeItem: IServiceNodeItem) => { setNodes({ ...nodes, [nodeItem.key]: nodeItem }); }; @@ -283,7 +310,13 @@ export default function Project() { return ( <> {showNetworksModal ? ( - setShowNetworksModal(false)} /> + setShowNetworksModal(false)} + onCreateNetwork={(values: any) => onCreateNetwork(values)} + onUpdateNetwork={(values: any) => onUpdateNetwork(values)} + onDeleteNetwork={(uuid: string) => onDeleteNetwork(uuid)} + /> ) : null} {showVolumesModal ? ( diff --git a/services/frontend/src/index.css b/services/frontend/src/index.css index 8e65800..dc397bd 100644 --- a/services/frontend/src/index.css +++ b/services/frontend/src/index.css @@ -4,6 +4,8 @@ body { margin: 0; + height: 100%; + overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; @@ -110,6 +112,9 @@ path, .btn-util { @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-indigo-500; } + .btn-util-red { + @apply inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-red-700 bg-red-100 hover:bg-red-200 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-red-500; + } .btn-util-selected { @apply text-white bg-indigo-500 hover:bg-indigo-500 focus:ring-indigo-500; } diff --git a/services/frontend/src/types/index.ts b/services/frontend/src/types/index.ts index 35cb56b..0734986 100644 --- a/services/frontend/src/types/index.ts +++ b/services/frontend/src/types/index.ts @@ -339,6 +339,7 @@ export interface IProjectPayload { }; nodes: any; connections: any; + networks: any; }; }; } @@ -346,7 +347,7 @@ export interface IProjectPayload { export interface IGeneratePayload { data: { version: number; - networks: Partial[]; + networks: Record>; services: Record>; volumes: Record>; }; diff --git a/services/frontend/src/utils/generators.ts b/services/frontend/src/utils/generators.ts index 546547e..9816850 100644 --- a/services/frontend/src/utils/generators.ts +++ b/services/frontend/src/utils/generators.ts @@ -1,11 +1,12 @@ import { IGeneratePayload } from "../types"; -export const flattenGraphData = (graphData: any): IGeneratePayload => { - const nodes = graphData["nodes"]; +export const generatePayload = (data: any): IGeneratePayload => { + const nodes = data["nodes"]; + const networks = data["networks"] || {}; const base: IGeneratePayload = { data: { version: 3, - networks: [], + networks: {}, services: {}, volumes: {} } @@ -23,5 +24,10 @@ export const flattenGraphData = (graphData: any): IGeneratePayload => { } }); + Object.keys(networks).forEach((key) => { + base.data.networks[networks[key].canvasConfig.node_name] = + networks[key].networkConfig; + }); + return base; }; diff --git a/services/frontend/src/utils/index.ts b/services/frontend/src/utils/index.ts index 19d0bc6..718bbe6 100644 --- a/services/frontend/src/utils/index.ts +++ b/services/frontend/src/utils/index.ts @@ -209,6 +209,12 @@ export const volumeConfigCanvasInitialValues = (): ICanvasConfig => { }; }; +export const networkConfigCanvasInitialValues = (): ICanvasConfig => { + return { + node_name: "unnamed" + }; +}; + export const serviceConfigCanvasInitialValues = (): ICanvasConfig => { return { node_name: "unnamed",