import { useEffect, useState, useRef, useMemo } from "react"; import { useParams } from "react-router-dom"; import { debounce, Dictionary, omit } from "lodash"; import YAML from "yaml"; import { GlobeAltIcon, CubeIcon, FolderAddIcon } from "@heroicons/react/solid"; import randomWords from "random-words"; import { IProjectPayload, IServiceNodeItem, IVolumeNodeItem, IServiceNodePosition, IProject } 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"; 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 "../Modal/ConfirmDelete"; import ModalServiceCreate from "../Modal/Service/Create"; import ModalServiceEdit from "../Modal/Service/Edit"; import ModalNetwork from "../Modal/Network"; import CreateVolumeModal from "../Modal/volume/CreateVolumeModal"; import EditVolumeModal from "../Modal/volume/EditVolumeModal"; import CodeEditor from "../CodeEditor"; export default function Project() { const { uuid } = useParams<{ uuid: string }>(); const { height } = useWindowDimensions(); const { data, error, isFetching } = useProject(uuid); const stateNodesRef = useRef>(); const stateConnectionsRef = useRef<[[string, string]] | []>(); const stateNetworksRef = useRef({}); 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 [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}`); } } ); 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, data: { canvas: { position: canvasPosition, nodes: nodes, connections: connections, networks: networks } } }; if (uuid) { updateProjectMutation.mutate(payload); } else { createProjectMutation.mutate(payload); } }; const setViewHeight = () => { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--vh", `${vh}px`); }; 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); 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((graphData) => { graphData.networks = stateNetworksRef.current; const flatData = generatePayload(graphData); generateHttp(JSON.stringify(flatData)) .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) => { debouncedOnGraphUpdate(graphData); }; const onCanvasUpdate = (updatedCanvasPosition: any) => { setCanvasPosition({ ...canvasPosition, ...updatedCanvasPosition }); }; useEffect(() => { const handler = () => { setViewHeight(); }; window.addEventListener("resize", handler); setViewHeight(); return () => { window.removeEventListener("resize", handler); }; }, []); 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 }); }; 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 }); }; const onConnectionDetached = (data: any) => { if ( !stateConnectionsRef.current || stateConnectionsRef.current.length <= 0 ) { return; } 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 (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 } }); }; useEffect(() => { if (!generatedCode) { return; } if (language === "json") { setFormattedCode(JSON.stringify(YAML.parse(generatedCode), null, 2)); } if (language === "yaml") { setFormattedCode(generatedCode); } }, [language, generatedCode]); 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}
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} />
); } else { return <>Something went wrong; } } return (
); }