From a77f0042f553547cc30df8bbcee456f491abd772 Mon Sep 17 00:00:00 2001 From: corpulent Date: Sun, 21 Aug 2022 18:41:59 +0300 Subject: [PATCH] fix: new routes for k8s and dc projects --- services/frontend/src/App.tsx | 29 +- .../Project/docker-compose/index.tsx | 705 ++++++++++++++++++ .../Project/{ => kubernetes}/index.tsx | 43 +- .../src/components/Projects/PreviewBlock.tsx | 10 +- 4 files changed, 759 insertions(+), 28 deletions(-) create mode 100644 services/frontend/src/components/Project/docker-compose/index.tsx rename services/frontend/src/components/Project/{ => kubernetes}/index.tsx (94%) diff --git a/services/frontend/src/App.tsx b/services/frontend/src/App.tsx index b7e04ea..8a14675 100644 --- a/services/frontend/src/App.tsx +++ b/services/frontend/src/App.tsx @@ -14,7 +14,8 @@ import { refresh, self } from "./services/auth"; import SideBar from "./components/global/SideBar"; import Projects from "./components/Projects"; -import Project from "./components/Project"; +import KubernetesProject from "./components/Project/kubernetes"; +import DockerComposeProject from "./components/Project/docker-compose"; import Profile from "./components/Profile"; import Signup from "./components/Auth/Signup"; import Login from "./components/Auth/Login"; @@ -107,13 +108,31 @@ export default function App() { } + path="/projects/kubernetes/:uuid" + element={ + + } + /> + + + } /> } + path="/projects/kubernetes/new" + element={ + + } + /> + + + } /> (); + const { height } = useWindowDimensions(); + const { data, error, isFetching } = useProject(uuid); + const stateNodesRef = + useRef>(); + const stateConnectionsRef = useRef<[[string, string]] | []>(); + const stateNetworksRef = useRef({}); + + const [isVisible, setIsVisible] = useState(false); + const [generatedCode, setGeneratedCode] = useState(); + const [formattedCode, setFormattedCode] = useState(""); + const [showModalCreateService, setShowModalCreateService] = useState(false); + const [showVolumesModal, setShowVolumesModal] = useState(false); + const [showNetworksModal, setShowNetworksModal] = useState(false); + const [serviceToEdit, setServiceToEdit] = useState( + null + ); + const [serviceToDelete, setServiceToDelete] = + useState(null); + const [volumeToEdit, setVolumeToEdit] = useState( + null + ); + const [volumeToDelete, setVolumeToDelete] = useState( + null + ); + const [language, setLanguage] = useState("yaml"); + const [version, setVersion] = useState("3"); + const [copyText, setCopyText] = useState("Copy"); + const [nodes, setNodes] = useState>({}); + const [connections, setConnections] = useState<[[string, string]] | []>([]); + const [networks, setNetworks] = useState>({}); + const [projectName, setProjectName] = useState( + () => + randomWords({ + wordsPerString: 2, + exactly: 1, + separator: "-" + } as any)[0] + ); + + const [canvasPosition, setCanvasPosition] = useState({ + top: 0, + left: 0, + scale: 1 + }); + const updateProjectMutation = useUpdateProject(uuid); + const createProjectMutation = useMutation( + (payload: IProjectPayload) => { + return createProject(payload); + }, + { + onSuccess: (project: IProject) => { + window.location.replace(`/projects/${project.uuid}`); + } + } + ); + + useTitle( + [ + isFetching ? "" : data ? data.name : "New project", + "Container Toolkit" + ].join(" | ") + ); + + stateNodesRef.current = nodes; + stateConnectionsRef.current = connections; + stateNetworksRef.current = networks; + + const handleNameChange = (e: any) => { + setProjectName(e.target.value); + }; + + const onNodeUpdate = (positionData: IServiceNodePosition) => { + if (stateNodesRef.current) { + const node = { + ...stateNodesRef.current[positionData.key], + ...positionData + }; + setNodes({ ...stateNodesRef.current, [positionData.key]: node }); + } + }; + + const onSave = () => { + const payload: IProjectPayload = { + name: projectName, + visibility: +isVisible, + project_type: 1, + data: { + canvas: { + position: canvasPosition, + nodes: nodes, + connections: connections, + networks: networks + } + } + }; + + if (uuid) { + updateProjectMutation.mutate(payload); + } else { + createProjectMutation.mutate(payload); + } + }; + + const copy = () => { + navigator.clipboard.writeText(formattedCode); + setCopyText("Copied"); + + setTimeout(() => { + setCopyText("Copy"); + }, 300); + }; + + useEffect(() => { + if (!data) { + return; + } + + const canvasData = JSON.parse(data.data); + const nodesAsList = Object.keys(canvasData.canvas.nodes).map( + (k) => canvasData.canvas.nodes[k] + ); + const clientNodeItems = getClientNodesAndConnections( + nodesAsList, + nodeLibraries + ); + + setProjectName(data.name); + setIsVisible(Boolean(data.visibility)); + setNodes(clientNodeItems); + setConnections(canvasData.canvas.connections); + setNetworks(canvasData.canvas.networks); + setCanvasPosition(canvasData.canvas.position); + }, [data]); + + const debouncedOnCodeChange = useMemo( + () => + debounce((code: string) => { + //formik.setFieldValue("code", e, false); + }, 700), + [] + ); + + const debouncedOnGraphUpdate = useMemo( + () => + debounce((payload) => { + generateHttp(JSON.stringify(payload)) + .then(checkHttpStatus) + .then((data) => { + if (data["code"].length) { + for (let i = 0; i < data["code"].length; ++i) { + data["code"][i] = data["code"][i].replace(/(\r\n|\n|\r)/gm, ""); + } + + const code = data["code"].join("\n"); + setGeneratedCode(code); + } + }) + .catch(() => undefined) + .finally(() => undefined); + }, 600), + [] + ); + + const onCodeUpdate = (code: string) => { + debouncedOnCodeChange(code); + }; + + const onGraphUpdate = (graphData: any) => { + const data = { ...graphData }; + data.version = version; + data.networks = stateNetworksRef.current; + const payload = generatePayload(data); + debouncedOnGraphUpdate(payload); + }; + + const onCanvasUpdate = (updatedCanvasPosition: any) => { + setCanvasPosition({ ...canvasPosition, ...updatedCanvasPosition }); + }; + + const onAddEndpoint = (values: any) => { + const sections = flattenLibraries(nodeLibraries); + const clientNodeItem = getClientNodeItem( + values, + ensure(sections.find((l) => l.type === values.type)) + ); + clientNodeItem.position = { + left: 60 - canvasPosition.left, + top: 30 - canvasPosition.top + }; + setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem }); + + if (clientNodeItem.type === "VOLUME") { + setVolumeToEdit(clientNodeItem as unknown as IVolumeNodeItem); + } + + if (clientNodeItem.type === "SERVICE") { + setServiceToEdit(clientNodeItem as unknown as IServiceNodeItem); + } + }; + + 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) => { + const key = nodeItem.key; + + if (connections.length) { + const _connections = [...connections]; + + _connections.forEach((conn: any) => { + if (key === conn[0]) { + const filtered = connections.filter((conn: any) => { + return key !== conn[0]; + }) as any; + + setConnections(filtered); + stateConnectionsRef.current = filtered; + } + }); + } + + if ( + nodeItem.serviceConfig?.depends_on && + Array.isArray(nodeItem.serviceConfig.depends_on) + ) { + nodeItem.serviceConfig.depends_on.forEach((dep: string) => { + const depObject = Object.keys(nodes).find((key: string) => { + const node = nodes[key]; + if (node.canvasConfig.node_name === dep) { + return node; + } + }); + + if (depObject) { + onConnectionAttached([key, depObject]); + } + }); + } + + setNodes({ ...nodes, [nodeItem.key]: nodeItem }); + }; + + const onConnectionDetached = (data: any) => { + if ( + !stateConnectionsRef.current || + stateConnectionsRef.current.length <= 0 + ) { + return; + } + + if (stateNodesRef.current) { + const sourceNode = { + ...stateNodesRef.current[data[0]] + } as IServiceNodeItem; + const targetNode = stateNodesRef.current[data[1]]; + const targetServiceName = targetNode.canvasConfig.node_name; + const sourceDependsOn = sourceNode.serviceConfig.depends_on as string[]; + + if (sourceDependsOn && sourceDependsOn.length) { + if (targetServiceName) { + const filtered = sourceDependsOn.filter( + (nodeName: string) => nodeName !== targetServiceName + ); + + if (filtered.length) { + sourceNode.serviceConfig.depends_on = filtered; + } else { + delete sourceNode.serviceConfig.depends_on; + } + } + } + } + + const _connections: [[string, string]] = [ + ...stateConnectionsRef.current + ] as any; + const existingIndex = getMatchingSetIndex(_connections, data); + + if (existingIndex !== -1) { + _connections.splice(existingIndex, 1); + setConnections(_connections); + stateConnectionsRef.current = _connections; + } + }; + + const onConnectionAttached = (data: any) => { + if (stateNodesRef.current) { + const sourceNode = { + ...stateNodesRef.current[data[0]] + } as IServiceNodeItem; + const targetNode = stateNodesRef.current[data[1]]; + const targetServiceName = targetNode.canvasConfig.node_name; + let sourceDependsOn = sourceNode.serviceConfig.depends_on as string[]; + + if (sourceDependsOn && sourceDependsOn.length) { + if (targetServiceName) { + if (!sourceDependsOn.includes(targetServiceName)) { + sourceDependsOn.push(targetServiceName); + } + } + } else { + if (targetServiceName) { + sourceDependsOn = [targetServiceName]; + } + } + + sourceNode.serviceConfig.depends_on = sourceDependsOn; + } + + if (stateConnectionsRef.current && stateConnectionsRef.current.length > 0) { + const _connections: [[string, string]] = [ + ...stateConnectionsRef.current + ] as any; + const existingIndex = getMatchingSetIndex(_connections, data); + if (existingIndex === -1) { + _connections.push(data); + } + setConnections(_connections); + } else { + setConnections([data]); + } + }; + + const onRemoveEndpoint = (node: IServiceNodeItem | IVolumeNodeItem) => { + setNodes({ ...omit(nodes, node.key) }); + eventBus.dispatch("NODE_DELETED", { message: { node: node } }); + }; + + const versionChange = (e: any) => { + setVersion(e.target.value); + }; + + useEffect(() => { + if (!generatedCode) { + return; + } + + if (language === "json") { + setFormattedCode(JSON.stringify(YAML.parse(generatedCode), null, 2)); + } + + if (language === "yaml") { + setFormattedCode(generatedCode); + } + }, [language, generatedCode]); + + useEffect(() => { + eventBus.dispatch("GENERATE", { + message: { + id: "" + } + }); + }, [version]); + + if (!isFetching) { + if (!error) { + return ( + <> + {showNetworksModal ? ( + setShowNetworksModal(false)} + onCreateNetwork={(values: any) => onCreateNetwork(values)} + onUpdateNetwork={(values: any) => onUpdateNetwork(values)} + onDeleteNetwork={(uuid: string) => onDeleteNetwork(uuid)} + /> + ) : null} + + {showVolumesModal ? ( + setShowVolumesModal(false)} + onAddEndpoint={(values: any) => onAddEndpoint(values)} + /> + ) : null} + + {showModalCreateService ? ( + setShowModalCreateService(false)} + onAddEndpoint={(values: any) => onAddEndpoint(values)} + /> + ) : null} + + {serviceToEdit ? ( + setServiceToEdit(null)} + onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)} + /> + ) : null} + + {serviceToDelete ? ( + setServiceToDelete(null)} + onConfirm={() => { + onRemoveEndpoint(serviceToDelete); + setServiceToDelete(null); + }} + /> + ) : null} + + {volumeToEdit ? ( + setVolumeToEdit(null)} + onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)} + /> + ) : null} + + {volumeToDelete ? ( + setServiceToDelete(null)} + onConfirm={() => { + onRemoveEndpoint(volumeToDelete); + setVolumeToDelete(null); + }} + /> + ) : null} + +
+
+
+ + +
+ {isAuthenticated && ( + { + setIsVisible(!isVisible); + }} + /> + )} + + +
+
+
+ +
+
+
+
+
+ + + + + +
+
+ + + onNodeUpdate(node) + } + onGraphUpdate={(graphData: any) => onGraphUpdate(graphData)} + onCanvasUpdate={(canvasData: any) => + onCanvasUpdate(canvasData) + } + onConnectionAttached={(connectionData: any) => + onConnectionAttached(connectionData) + } + onConnectionDetached={(connectionData: any) => + onConnectionDetached(connectionData) + } + setServiceToEdit={(node: IServiceNodeItem) => + setServiceToEdit(node) + } + setServiceToDelete={(node: IServiceNodeItem) => + setServiceToDelete(node) + } + setVolumeToEdit={(node: IVolumeNodeItem) => + setVolumeToEdit(node) + } + setVolumeToDelete={(node: IVolumeNodeItem) => + setVolumeToDelete(node) + } + /> +
+
+ +
+
+ + + + + +
+ + { + onCodeUpdate(e); + }} + disabled={true} + lineWrapping={false} + height={height - 64} + /> +
+
+
+ + ); + } + + if (error) { + return ( +
+

+ {(error as any)?.response.status === 404 ? <>404 : <>Oops...} +

+

+ Either this project does not exist, it is private or the link is + wrong. +

+
+ ); + } + } + + return ( +
+ +
+ ); +} diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/kubernetes/index.tsx similarity index 94% rename from services/frontend/src/components/Project/index.tsx rename to services/frontend/src/components/Project/kubernetes/index.tsx index 01328d2..8fbb37d 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/kubernetes/index.tsx @@ -9,43 +9,43 @@ import { IVolumeNodeItem, IServiceNodePosition, IProject -} from "../../types"; -import eventBus from "../../events/eventBus"; +} from "../../../types"; +import eventBus from "../../../events/eventBus"; import { useMutation } from "react-query"; import { useProject, useUpdateProject, createProject -} from "../../hooks/useProject"; -import useWindowDimensions from "../../hooks/useWindowDimensions"; -import { generatePayload } from "../../utils/generators"; -import { nodeLibraries } from "../../utils/data/libraries"; +} from "../../../hooks/useProject"; +import useWindowDimensions from "../../../hooks/useWindowDimensions"; +import { generatePayload } from "../../../utils/generators"; +import { nodeLibraries } from "../../../utils/data/libraries"; import { getClientNodeItem, flattenLibraries, ensure, getClientNodesAndConnections, getMatchingSetIndex -} from "../../utils"; -import { checkHttpStatus } from "../../services/helpers"; -import { generateHttp } from "../../services/generate"; -import { Canvas } from "../Canvas"; -import Spinner from "../global/Spinner"; -import ModalConfirmDelete from "../modals/ConfirmDelete"; -import CreateServiceModal from "../modals/docker-compose/service/Create"; -import ModalServiceEdit from "../modals/docker-compose/service/Edit"; -import ModalNetwork from "../modals/docker-compose/network"; -import CreateVolumeModal from "../modals/docker-compose/volume/CreateVolumeModal"; -import EditVolumeModal from "../modals/docker-compose/volume/EditVolumeModal"; -import CodeEditor from "../CodeEditor"; -import { useTitle } from "../../hooks"; -import VisibilitySwitch from "../global/VisibilitySwitch"; +} from "../../../utils"; +import { checkHttpStatus } from "../../../services/helpers"; +import { generateHttp } from "../../../services/generate"; +import { Canvas } from "../../Canvas"; +import Spinner from "../../global/Spinner"; +import ModalConfirmDelete from "../../modals/ConfirmDelete"; +import CreateServiceModal from "../../modals/docker-compose/service/Create"; +import ModalServiceEdit from "../../modals/docker-compose/service/Edit"; +import ModalNetwork from "../../modals/docker-compose/network"; +import CreateVolumeModal from "../../modals/docker-compose/volume/CreateVolumeModal"; +import EditVolumeModal from "../../modals/docker-compose/volume/EditVolumeModal"; +import CodeEditor from "../../CodeEditor"; +import { useTitle } from "../../../hooks"; +import VisibilitySwitch from "../../global/VisibilitySwitch"; interface IProjectProps { isAuthenticated: boolean; } -export default function Project(props: IProjectProps) { +export default function KubernetesProject(props: IProjectProps) { const { isAuthenticated } = props; const { uuid } = useParams<{ uuid: string }>(); const { height } = useWindowDimensions(); @@ -126,6 +126,7 @@ export default function Project(props: IProjectProps) { const payload: IProjectPayload = { name: projectName, visibility: +isVisible, + project_type: 1, data: { canvas: { position: canvasPosition, diff --git a/services/frontend/src/components/Projects/PreviewBlock.tsx b/services/frontend/src/components/Projects/PreviewBlock.tsx index 9840361..1458e1c 100644 --- a/services/frontend/src/components/Projects/PreviewBlock.tsx +++ b/services/frontend/src/components/Projects/PreviewBlock.tsx @@ -25,8 +25,14 @@ const PreviewBlock = (props: IPreviewBlockProps) => { setIsHovering(false); }; - const handleClick = () => { - navigate(`/projects/${project.uuid}`); + const handleClick = (e: any) => { + if (project.project_type === 0) { + navigate(`/projects/docker-compose/${project.uuid}`); + } + + if (project.project_type === 1) { + navigate(`/projects/kubernetes/${project.uuid}`); + } }; const onDelete = (e: any) => {