From 5b826f6a273931c2eac5dce4ea51c1380645a77d Mon Sep 17 00:00:00 2001 From: corpulent Date: Tue, 6 Sep 2022 16:08:23 +0300 Subject: [PATCH] feat: support k8s in ui, some component separation --- .../src/components/Project/CodeBox.tsx | 156 +++++++++++ .../src/components/Project/Header.tsx | 106 +++++++ .../src/components/Project/ManifestSelect.tsx | 51 ++++ .../frontend/src/components/Project/index.tsx | 261 +++--------------- .../frontend/src/components/useJsPlumb.ts | 4 +- services/frontend/src/services/generate.ts | 13 +- 6 files changed, 371 insertions(+), 220 deletions(-) create mode 100644 services/frontend/src/components/Project/CodeBox.tsx create mode 100644 services/frontend/src/components/Project/Header.tsx create mode 100644 services/frontend/src/components/Project/ManifestSelect.tsx diff --git a/services/frontend/src/components/Project/CodeBox.tsx b/services/frontend/src/components/Project/CodeBox.tsx new file mode 100644 index 0000000..8e7e633 --- /dev/null +++ b/services/frontend/src/components/Project/CodeBox.tsx @@ -0,0 +1,156 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import YAML from "yaml"; +import { debounce } from "lodash"; +import { generatePayload } from "../../utils/generators"; +import { checkHttpStatus } from "../../services/helpers"; +import { generateHttp } from "../../services/generate"; +import { toaster } from "../../utils"; +import eventBus from "../../events/eventBus"; +import ManifestSelect from "./ManifestSelect"; +import CodeEditor from "../CodeEditor"; +import useWindowDimensions from "../../hooks/useWindowDimensions"; + +const CodeBox = () => { + const versionRef = useRef(); + const manifestRef = useRef(); + const [language, setLanguage] = useState("yaml"); + const [version, setVersion] = useState("3"); + const [copyText, setCopyText] = useState("Copy"); + const [generatedCode, setGeneratedCode] = useState(""); + const [formattedCode, setFormattedCode] = useState(""); + const [manifest, setManifest] = useState(0); + const { height } = useWindowDimensions(); + + versionRef.current = version; + manifestRef.current = manifest; + + const getCode = (payload: any, manifest: number) => { + generateHttp(JSON.stringify(payload), manifest) + .then(checkHttpStatus) + .then((data) => { + if (data["code"]) { + setGeneratedCode(data["code"]); + } else { + setGeneratedCode(""); + } + + if (data["error"].length) { + setGeneratedCode(""); + toaster(`error ${data["error"]}`, "error"); + } + }) + .catch(() => undefined) + .finally(() => undefined); + }; + + const debouncedOnGraphUpdate = useMemo( + () => + debounce((payload, manifest) => { + getCode(payload, manifest); + }, 600), + [] + ); + + const versionChange = (e: any) => { + setVersion(e.target.value); + }; + + const copy = () => { + navigator.clipboard.writeText(formattedCode); + setCopyText("Copied"); + + setTimeout(() => { + setCopyText("Copy"); + }, 300); + }; + + useEffect(() => { + if (language === "json") { + setFormattedCode( + JSON.stringify(YAML.parseAllDocuments(generatedCode), null, 2) + ); + } + + if (language === "yaml") { + setFormattedCode(generatedCode); + } + }, [language, generatedCode]); + + useEffect(() => { + eventBus.dispatch("GENERATE", { + message: { + id: "" + } + }); + }, [version, manifest]); + + useEffect(() => { + eventBus.on("FETCH_CODE", (data) => { + const graphData = data.detail.message; + graphData.version = versionRef.current; + debouncedOnGraphUpdate(generatePayload(graphData), manifestRef.current); + }); + + return () => { + eventBus.remove("FETCH_CODE", () => undefined); + }; + }, []); + + return ( + <> +
+ + + + + +
+ +
+ +
+ + { + return; + }} + disabled={true} + lineWrapping={false} + height={height - 64} + /> + + ); +}; + +export default CodeBox; diff --git a/services/frontend/src/components/Project/Header.tsx b/services/frontend/src/components/Project/Header.tsx new file mode 100644 index 0000000..00b19cc --- /dev/null +++ b/services/frontend/src/components/Project/Header.tsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react"; +import { CallbackFunction, IProject } from "../../types"; +import Spinner from "../global/Spinner"; +import VisibilitySwitch from "../global/VisibilitySwitch"; + +interface IManifestSelectProps { + onSave: CallbackFunction; + isLoading: boolean; + projectData: IProject; + isAuthenticated: boolean; +} + +const ManifestSelect = (props: IManifestSelectProps) => { + const { onSave, isLoading, projectData, isAuthenticated } = props; + const [visibility, setVisibility] = useState(false); + const [projectName, setProjectName] = useState("Untitled"); + + const handleNameChange = (e: any) => { + setProjectName(e.target.value); + }; + + const handleSave = () => { + const data: any = { + name: projectName, + visibility: +visibility + }; + + onSave(data); + }; + + useEffect(() => { + if (!projectData) { + return; + } + + setProjectName(projectData.name); + setVisibility(Boolean(projectData.visibility)); + }, [projectData]); + + return ( + <> +
+
+ + +
+ {isAuthenticated && ( + { + setVisibility(!visibility); + }} + /> + )} + + +
+
+
+ + ); +}; + +export default ManifestSelect; diff --git a/services/frontend/src/components/Project/ManifestSelect.tsx b/services/frontend/src/components/Project/ManifestSelect.tsx new file mode 100644 index 0000000..6508085 --- /dev/null +++ b/services/frontend/src/components/Project/ManifestSelect.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import DcLogo from "../global/dc-logo"; +import K8sLogo from "../global/k8s-logo"; + +const styles = { + default: { + filter: "grayscale(100%)", + opacity: "90%" + }, + selected: { + filter: "grayscale(0)", + opacity: "100%" + } +}; + +interface IManifestSelectProps { + setManifest: any; +} + +const ManifestSelect = (props: IManifestSelectProps) => { + const { setManifest } = props; + const [selected, setSelected] = useState(0); + + return ( + <> + + + + + ); +}; + +export default ManifestSelect; diff --git a/services/frontend/src/components/Project/index.tsx b/services/frontend/src/components/Project/index.tsx index 37e3130..cc7bc99 100644 --- a/services/frontend/src/components/Project/index.tsx +++ b/services/frontend/src/components/Project/index.tsx @@ -1,25 +1,22 @@ -import { useEffect, useState, useRef, useMemo } from "react"; +import { useEffect, useState, useRef } from "react"; import { useParams } from "react-router-dom"; -import { debounce, Dictionary, omit } from "lodash"; -import YAML from "yaml"; +import { Dictionary, omit } from "lodash"; +import _ from "lodash"; import { GlobeAltIcon, CubeIcon, FolderAddIcon } from "@heroicons/react/solid"; import { - IProjectPayload, IServiceNodeItem, IVolumeNodeItem, IServiceNodePosition, IProject, - IEditServiceFormDependsOn + IProjectPayload, } from "../../types"; import eventBus from "../../events/eventBus"; -import { useMutation } from "react-query"; import { + createProject, useProject, - useUpdateProject, - createProject + useUpdateProject } from "../../hooks/useProject"; import useWindowDimensions from "../../hooks/useWindowDimensions"; -import { generatePayload } from "../../utils/generators"; import { nodeLibraries } from "../../utils/data/libraries"; import { getClientNodeItem, @@ -28,8 +25,6 @@ import { 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"; @@ -38,10 +33,10 @@ 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"; -import _ from "lodash"; +import CodeBox from "./CodeBox"; +import Header from "./Header"; +import { useMutation } from "react-query"; interface IProjectProps { isAuthenticated: boolean; @@ -56,10 +51,8 @@ export default function Project(props: IProjectProps) { useRef>(); const stateConnectionsRef = useRef<[[string, string]] | []>(); const stateNetworksRef = useRef({}); + const stateProjectRef = 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); @@ -74,14 +67,9 @@ export default function Project(props: IProjectProps) { 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("Untitled"); - const [canvasPosition, setCanvasPosition] = useState({ top: 0, left: 0, @@ -109,10 +97,7 @@ export default function Project(props: IProjectProps) { stateNodesRef.current = nodes; stateConnectionsRef.current = connections; stateNetworksRef.current = networks; - - const handleNameChange = (e: any) => { - setProjectName(e.target.value); - }; + stateProjectRef.current = data; const onNodeUpdate = (positionData: IServiceNodePosition) => { if (stateNodesRef.current) { @@ -124,36 +109,6 @@ export default function Project(props: IProjectProps) { } }; - const onSave = () => { - const payload: IProjectPayload = { - name: projectName, - visibility: +isVisible, - 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; @@ -167,42 +122,41 @@ export default function Project(props: IProjectProps) { 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 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 onSave = (partial: any) => { + const base: IProjectPayload = { + name: data?.name ?? "", + visibility: data?.visibility ?? 0, + data: { + canvas: { + position: canvasPosition, + nodes: nodes, + connections: connections, + networks: networks + } + } + }; - const code = data["code"].join("\n"); - setGeneratedCode(code); - } - }) - .catch(() => undefined) - .finally(() => undefined); - }, 600), - [] - ); + const payload = { ...base, ...partial }; + + if (uuid) { + updateProjectMutation.mutate(payload); + } else { + createProjectMutation.mutate(payload); + } + }; const onGraphUpdate = (graphData: any) => { const data = { ...graphData }; - data.version = version; data.networks = stateNetworksRef.current; - const payload = generatePayload(data); - debouncedOnGraphUpdate(payload); + eventBus.dispatch("FETCH_CODE", { + message: data + }); }; const onCanvasUpdate = (updatedCanvasPosition: any) => { @@ -404,32 +358,6 @@ export default function Project(props: IProjectProps) { 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 ( @@ -495,70 +423,15 @@ export default function Project(props: IProjectProps) { ) : null}
-
-
- - -
- {isAuthenticated && ( - { - setIsVisible(!isVisible); - }} - /> - )} - - -
-
-
+
-
- - - - - -
- - { - return; - }} - disabled={true} - lineWrapping={false} - height={height - 64} - /> +
diff --git a/services/frontend/src/components/useJsPlumb.ts b/services/frontend/src/components/useJsPlumb.ts index d622486..295a4b5 100644 --- a/services/frontend/src/components/useJsPlumb.ts +++ b/services/frontend/src/components/useJsPlumb.ts @@ -433,13 +433,13 @@ export const useJsPlumb = ( useEffect(() => { eventBus.on("GENERATE", () => { - if (!instance) return; + if (!instanceRef.current) return; if (stateRef.current) { onGraphUpdate({ nodes: stateRef.current, connections: getConnections( - instance.getConnections({}, true) as Connection[] + instanceRef.current.getConnections({}, true) as Connection[] ) }); } diff --git a/services/frontend/src/services/generate.ts b/services/frontend/src/services/generate.ts index c6d3901..09b6b29 100644 --- a/services/frontend/src/services/generate.ts +++ b/services/frontend/src/services/generate.ts @@ -1,7 +1,16 @@ import { API_SERVER_URL } from "../constants"; -export const generateHttp = (data: string) => { - return fetch(`${API_SERVER_URL}/generate/`, { +export const generateHttp = (data: string, manifest: number) => { + let endpoint = `${API_SERVER_URL}/generate/`; + if (manifest === 0) { + endpoint += "docker-compose"; + } + + if (manifest === 1) { + endpoint += "kubernetes"; + } + + return fetch(endpoint, { method: "POST", headers: { "Content-Type": "application/json"