|
|
|
|
@ -1,136 +1,286 @@
|
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
|
import { useEffect, useState, useRef, useMemo } from "react";
|
|
|
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
|
|
|
import { IProjectPayload } from "../../types";
|
|
|
|
|
import { nodes, connections, position, updateProjectName } from "../../reducers";
|
|
|
|
|
import Spinner from "../Spinner";
|
|
|
|
|
import { Canvas } from "../Canvas";
|
|
|
|
|
import { debounce, Dictionary, omit } from 'lodash';
|
|
|
|
|
import YAML from "yaml";
|
|
|
|
|
import { PlusIcon } from "@heroicons/react/solid";
|
|
|
|
|
import { IProjectPayload, IClientNodeItem, IServiceNodePosition } from "../../types";
|
|
|
|
|
import eventBus from "../../events/eventBus";
|
|
|
|
|
import { useProject, useUpdateProject } from "../../hooks/useProject";
|
|
|
|
|
import useWindowDimensions from "../../hooks/useWindowDimensions";
|
|
|
|
|
import { getClientNodesAndConnections } from "../../utils";
|
|
|
|
|
import { projectHttpGet, projectHttpUpdate, projectHttpCreate } from "../../services/project";
|
|
|
|
|
import { checkHttpStatus } from "../../services/helpers";
|
|
|
|
|
import { projectHttpCreate } from "../../services/project";
|
|
|
|
|
import { flattenGraphData } 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 "../Spinner";
|
|
|
|
|
import ModalConfirmDelete from "../Modal/Service/ConfirmDelete";
|
|
|
|
|
import ModalServiceCreate from "../Modal/Service/Create";
|
|
|
|
|
import ModalServiceEdit from "../Modal/Service/Edit";
|
|
|
|
|
import CodeEditor from "../CodeEditor";
|
|
|
|
|
|
|
|
|
|
interface IProjectProps {
|
|
|
|
|
dispatch: any;
|
|
|
|
|
state: any;
|
|
|
|
|
}
|
|
|
|
|
interface IProjectProps {}
|
|
|
|
|
|
|
|
|
|
export default function Project(props: IProjectProps) {
|
|
|
|
|
const { uuid } = useParams<{ uuid?: string }>();
|
|
|
|
|
const { dispatch, state } = props;
|
|
|
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
|
const [projectName, setProjectName] = useState("");
|
|
|
|
|
const { height, width } = useWindowDimensions();
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
const { uuid } = useParams<{ uuid: string }>();
|
|
|
|
|
const { height } = useWindowDimensions();
|
|
|
|
|
const { data, error, isFetching } = useProject(uuid);
|
|
|
|
|
const mutation = useUpdateProject(uuid);
|
|
|
|
|
const stateNodesRef = useRef<Dictionary<IClientNodeItem>>();
|
|
|
|
|
const stateConnectionsRef = useRef<[[string, string]] | []>();
|
|
|
|
|
|
|
|
|
|
const handleNameChange = (e: any) => {
|
|
|
|
|
setProjectName(e.target.value);
|
|
|
|
|
dispatch(updateProjectName(e.target.value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateProject = (uuid: string, payload: IProjectPayload) => {
|
|
|
|
|
projectHttpUpdate(uuid, JSON.stringify(payload))
|
|
|
|
|
.then(checkHttpStatus)
|
|
|
|
|
.then(data => {
|
|
|
|
|
const [generatedCode, setGeneratedCode] = useState<string>();
|
|
|
|
|
const [formattedCode, setFormattedCode] = useState<string>("");
|
|
|
|
|
const [showModalCreateService, setShowModalCreateService] = useState(false);
|
|
|
|
|
const [showVolumesModal, setShowVolumesModal] = useState(false);
|
|
|
|
|
const [showNetworksModal, setShowNetworksModal] = useState(false);
|
|
|
|
|
const [nodeForEdit, setNodeForEdit] = useState<IClientNodeItem | null>(null);
|
|
|
|
|
const [nodeForDelete, setNodeForDelete] = useState<IClientNodeItem | null>(null);
|
|
|
|
|
const [language, setLanguage] = useState("yaml");
|
|
|
|
|
const [copyText, setCopyText] = useState("Copy");
|
|
|
|
|
const [nodes, setNodes] = useState({});
|
|
|
|
|
const [connections, setConnections] = useState<[[string, string]] | []>([]);
|
|
|
|
|
const [projectName, setProjectName] = useState("Untitled");
|
|
|
|
|
const [canvasPosition, setCanvasPosition] = useState({top: 0, left: 0, scale: 1});
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
|
|
|
|
stateNodesRef.current = nodes;
|
|
|
|
|
stateConnectionsRef.current = connections;
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
});
|
|
|
|
|
const handleNameChange = (e: any) => {
|
|
|
|
|
setProjectName(e.target.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const createProject = (payload: IProjectPayload) => {
|
|
|
|
|
projectHttpCreate(JSON.stringify(payload))
|
|
|
|
|
.then(checkHttpStatus)
|
|
|
|
|
.then(data => {
|
|
|
|
|
navigate(`/projects/${data.uuid}`)
|
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
|
|
|
|
navigate(`/projects/${data.uuid}`);
|
|
|
|
|
})
|
|
|
|
|
.catch(err => {})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
setSaving(false);
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onNodeUpdate = (positionData: IServiceNodePosition) => {
|
|
|
|
|
if (stateNodesRef.current) {
|
|
|
|
|
const node = { ...stateNodesRef.current[positionData.key], ...positionData };
|
|
|
|
|
setNodes({ ...stateNodesRef.current, [positionData.key]: node });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onSave = () => {
|
|
|
|
|
setSaving(true);
|
|
|
|
|
const payload: IProjectPayload = {
|
|
|
|
|
name: state.projectName,
|
|
|
|
|
name: projectName,
|
|
|
|
|
data: {
|
|
|
|
|
canvas: {
|
|
|
|
|
position: state.canvasPosition,
|
|
|
|
|
nodes: state.nodes,
|
|
|
|
|
connections: state.connections
|
|
|
|
|
position: canvasPosition,
|
|
|
|
|
nodes: nodes,
|
|
|
|
|
connections: connections
|
|
|
|
|
},
|
|
|
|
|
configs: [],
|
|
|
|
|
networks: [],
|
|
|
|
|
secrets: [],
|
|
|
|
|
services: state.nodes,
|
|
|
|
|
services: nodes,
|
|
|
|
|
version: 3,
|
|
|
|
|
volumes: [],
|
|
|
|
|
volumes: []
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (uuid) {
|
|
|
|
|
updateProject(uuid, payload);
|
|
|
|
|
mutation.mutate(payload);
|
|
|
|
|
} else {
|
|
|
|
|
createProject(payload);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const setViewHeight = () => {
|
|
|
|
|
let vh = window.innerHeight * 0.01;
|
|
|
|
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
|
|
|
};
|
|
|
|
|
document.documentElement.style.setProperty("--vh", `${vh}px`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const copy = () => {
|
|
|
|
|
navigator.clipboard.writeText(formattedCode);
|
|
|
|
|
setCopyText("Copied");
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setCopyText("Copy");
|
|
|
|
|
}, 300);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (uuid) {
|
|
|
|
|
projectHttpGet(uuid)
|
|
|
|
|
.then(checkHttpStatus)
|
|
|
|
|
.then(data => {
|
|
|
|
|
const projectData = JSON.parse(data.data);
|
|
|
|
|
const nodesAsList = Object.keys(projectData.canvas.nodes).map((k) => {
|
|
|
|
|
return projectData.canvas.nodes[k];
|
|
|
|
|
});
|
|
|
|
|
if (!data) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(data);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
dispatch(updateProjectName(data.name));
|
|
|
|
|
dispatch(nodes(clientNodeItems));
|
|
|
|
|
dispatch(connections(projectData.canvas.connections));
|
|
|
|
|
dispatch(position(projectData.canvas.position));
|
|
|
|
|
setNodes(clientNodeItems);
|
|
|
|
|
setConnections(canvasData.canvas.connections);
|
|
|
|
|
setCanvasPosition(canvasData.canvas.position);
|
|
|
|
|
}, [data]);
|
|
|
|
|
|
|
|
|
|
const debouncedOnCodeChange = useMemo(() => debounce((code: string) => {
|
|
|
|
|
//formik.setFieldValue("code", e, false);
|
|
|
|
|
}, 700), []);
|
|
|
|
|
|
|
|
|
|
const debouncedOnGraphUpdate = useMemo(() => debounce((graphData) => {
|
|
|
|
|
const flatData = flattenGraphData(graphData);
|
|
|
|
|
generateHttp(flatData)
|
|
|
|
|
.then(checkHttpStatus)
|
|
|
|
|
.then(data => {
|
|
|
|
|
if (data['code'].length) {
|
|
|
|
|
for (var 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(err => {
|
|
|
|
|
if (err.status === 404) {
|
|
|
|
|
window.location.replace("/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
//setFetching(false);
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}, [uuid, dispatch]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
//setProjectName(state.projectName);
|
|
|
|
|
}, [state.projectName]);
|
|
|
|
|
});
|
|
|
|
|
}, 450), []);
|
|
|
|
|
|
|
|
|
|
const onCodeUpdate = (code: string) => {
|
|
|
|
|
debouncedOnCodeChange(code);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onGraphUpdate = (graphData: any) => {
|
|
|
|
|
debouncedOnGraphUpdate(graphData);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onCanvasUpdate = (updatedCanvasPosition: any) => {
|
|
|
|
|
setCanvasPosition({...canvasPosition, ...updatedCanvasPosition});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.addEventListener("resize", () => {
|
|
|
|
|
const handler = () => {
|
|
|
|
|
setViewHeight();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener("resize", handler);
|
|
|
|
|
setViewHeight();
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("resize", handler);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const onAddEndpoint = (values: any) => {
|
|
|
|
|
let sections = flattenLibraries(nodeLibraries);
|
|
|
|
|
let clientNodeItem = getClientNodeItem(values, ensure(sections.find((l) => l.Type === values.type)));
|
|
|
|
|
clientNodeItem.position = { left: 60, top: 30 };
|
|
|
|
|
setNodes({ ...nodes, [clientNodeItem.key]: clientNodeItem });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onUpdateEndpoint = (nodeItem: IClientNodeItem) => {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: IClientNodeItem) => {
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
|
|
|
|
}, [nodeForEdit]);
|
|
|
|
|
|
|
|
|
|
if (!isFetching) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
{showModalCreateService
|
|
|
|
|
? <ModalServiceCreate
|
|
|
|
|
onHide={() => setShowModalCreateService(false)}
|
|
|
|
|
onAddEndpoint={(values: any) => onAddEndpoint(values)}
|
|
|
|
|
/>
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{nodeForEdit
|
|
|
|
|
? <ModalServiceEdit
|
|
|
|
|
node={nodeForEdit}
|
|
|
|
|
onHide={() => setNodeForEdit(null)}
|
|
|
|
|
onUpdateEndpoint={(values: any) => onUpdateEndpoint(values)}
|
|
|
|
|
/>
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{nodeForDelete
|
|
|
|
|
? <ModalConfirmDelete
|
|
|
|
|
onHide={() => setNodeForDelete(null)}
|
|
|
|
|
onConfirm={() => {
|
|
|
|
|
onRemoveEndpoint(nodeForDelete);
|
|
|
|
|
setNodeForDelete(null);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
<div className="px-4 py-3 border-b border-gray-200">
|
|
|
|
|
<form className="flex flex-col space-y-2 md:flex-row md:justify-between items-center" autoComplete="off">
|
|
|
|
|
<form
|
|
|
|
|
className="flex flex-col space-y-2 md:flex-row md:justify-between items-center"
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
className={`
|
|
|
|
|
bg-gray-100
|
|
|
|
|
@ -165,7 +315,7 @@ export default function Project(props: IProjectProps) {
|
|
|
|
|
<div className="flex flex-col space-y-2 w-full justify-end mb-4 md:flex-row md:space-y-0 md:space-x-2 md:mb-0">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
window.location.replace("/");
|
|
|
|
|
window.location.replace("/projects/new")
|
|
|
|
|
}}
|
|
|
|
|
type="button"
|
|
|
|
|
className="btn-util text-black bg-gray-200 hover:bg-gray-300 sm:w-auto"
|
|
|
|
|
@ -181,9 +331,7 @@ export default function Project(props: IProjectProps) {
|
|
|
|
|
className="btn-util text-white bg-green-600 hover:bg-green-700 sm:w-auto"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex justify-center items-center space-x-2 mx-auto">
|
|
|
|
|
{saving &&
|
|
|
|
|
<Spinner className="w-4 h-4 text-green-300" />
|
|
|
|
|
}
|
|
|
|
|
{mutation.isLoading && <Spinner className="w-4 h-4 text-green-300" />}
|
|
|
|
|
<span>Save</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
@ -192,12 +340,61 @@ export default function Project(props: IProjectProps) {
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-grow relative flex-col md:flex-row">
|
|
|
|
|
<div className="w-full overflow-hidden md:w-2/3 z-40" style={{ height: height }}>
|
|
|
|
|
<div className="relative h-full">
|
|
|
|
|
<div className="absolute top-0 right-0 z-40">
|
|
|
|
|
<div className="flex space-x-2 p-2">
|
|
|
|
|
<button className="flex space-x-1 btn-util" type="button" onClick={() => setShowModalCreateService(true)}>
|
|
|
|
|
<PlusIcon className="w-3" />
|
|
|
|
|
<span>Service</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn-util" type="button" onClick={() => setShowVolumesModal(true)}>
|
|
|
|
|
Volumes
|
|
|
|
|
</button>
|
|
|
|
|
<button className="btn-util" type="button" onClick={() => setShowNetworksModal(true)}>
|
|
|
|
|
Networks
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Canvas
|
|
|
|
|
state={state}
|
|
|
|
|
dispatch={dispatch}
|
|
|
|
|
height={(height - 64)}
|
|
|
|
|
nodes={nodes}
|
|
|
|
|
connections={connections}
|
|
|
|
|
canvasPosition={canvasPosition}
|
|
|
|
|
onNodeUpdate={(node: IServiceNodePosition) => onNodeUpdate(node)}
|
|
|
|
|
onGraphUpdate={(graphData: any) => onGraphUpdate(graphData)}
|
|
|
|
|
onCanvasUpdate={(canvasData: any) => onCanvasUpdate(canvasData)}
|
|
|
|
|
onConnectionAttached={(connectionData: any) => onConnectionAttached(connectionData)}
|
|
|
|
|
onConnectionDetached={(connectionData: any) => onConnectionDetached(connectionData)}
|
|
|
|
|
setNodeForEdit={(node: IClientNodeItem) => setNodeForEdit(node)}
|
|
|
|
|
setNodeForDelete={(node: IClientNodeItem) => setNodeForDelete(node)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="relative group code-column w-full md:w-1/3">
|
|
|
|
|
<div className={`absolute top-0 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`}>
|
|
|
|
|
<button className={`btn-util ${language === "json" ? `btn-util-selected` : ``}`} onClick={() => setLanguage('json')}>json</button>
|
|
|
|
|
<button className={`btn-util ${language === "yaml" ? `btn-util-selected` : ``}`} onClick={() => setLanguage('yaml')}>yaml</button>
|
|
|
|
|
<button className="btn-util" type="button" onClick={copy}>{copyText}</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<CodeEditor
|
|
|
|
|
data={formattedCode}
|
|
|
|
|
language={language}
|
|
|
|
|
onChange={(e: any) => { onCodeUpdate(e) }}
|
|
|
|
|
disabled={false}
|
|
|
|
|
lineWrapping={false}
|
|
|
|
|
height={height - 64}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center items-stretch min-h-screen align-middle">
|
|
|
|
|
<Spinner className="w-4 h-4 m-auto dark:text-blue-400 text-blue-600"></Spinner>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|