feat: support k8s in ui, some component separation

pull/118/head
corpulent 3 years ago
parent 1f27a0541b
commit 5b826f6a27

@ -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<string>();
const manifestRef = useRef<number>();
const [language, setLanguage] = useState("yaml");
const [version, setVersion] = useState("3");
const [copyText, setCopyText] = useState("Copy");
const [generatedCode, setGeneratedCode] = useState<string>("");
const [formattedCode, setFormattedCode] = useState<string>("");
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 (
<>
<div
className={`absolute top-0 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`}
>
<select
id="version"
onChange={versionChange}
value={version}
className="input-util w-min pr-8"
>
<option value="1">v 1</option>
<option value="2">v 2</option>
<option value="3">v 3</option>
</select>
<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>
<div
className={`absolute top-10 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`}
>
<ManifestSelect setManifest={setManifest} />
</div>
<CodeEditor
data={formattedCode}
language={language}
onChange={() => {
return;
}}
disabled={true}
lineWrapping={false}
height={height - 64}
/>
</>
);
};
export default CodeBox;

@ -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 (
<>
<div className="px-4 py-3 border-b border-gray-200">
<form
className="flex flex-col space-y-2 md:space-y-0 md:flex-row md:justify-between items-center"
autoComplete="off"
>
<input
className={`
bg-gray-100
appearance-none
w-full
md:w-1/2
lg:w-1/3
block
text-gray-700
border
border-gray-100
dark:bg-gray-900
dark:text-white
dark:border-gray-900
rounded
py-2
px-3
leading-tight
focus:outline-none
focus:border-indigo-400
focus:ring-0
`}
type="text"
placeholder="Project name"
autoComplete="off"
id="name"
name="name"
onChange={handleNameChange}
value={projectName}
/>
<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">
{isAuthenticated && (
<VisibilitySwitch
isVisible={visibility}
onToggle={() => {
setVisibility(!visibility);
}}
/>
)}
<button
onClick={() => handleSave()}
type="button"
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">
{isLoading && <Spinner className="w-4 h-4 text-green-300" />}
{isLoading && <Spinner className="w-4 h-4 text-green-300" />}
<span>Save</span>
</div>
</button>
</div>
</form>
</div>
</>
);
};
export default ManifestSelect;

@ -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 (
<>
<button
style={selected === 1 ? styles.selected : styles.default}
type="button"
onClick={() => {
setManifest(1);
setSelected(1);
}}
>
<K8sLogo />
</button>
<button
style={selected === 0 ? styles.selected : styles.default}
type="button"
onClick={() => {
setManifest(0);
setSelected(0);
}}
>
<DcLogo />
</button>
</>
);
};
export default ManifestSelect;

@ -1,25 +1,22 @@
import { useEffect, useState, useRef, useMemo } from "react"; import { useEffect, useState, useRef } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { debounce, Dictionary, omit } from "lodash"; import { Dictionary, omit } from "lodash";
import YAML from "yaml"; import _ from "lodash";
import { GlobeAltIcon, CubeIcon, FolderAddIcon } from "@heroicons/react/solid"; import { GlobeAltIcon, CubeIcon, FolderAddIcon } from "@heroicons/react/solid";
import { import {
IProjectPayload,
IServiceNodeItem, IServiceNodeItem,
IVolumeNodeItem, IVolumeNodeItem,
IServiceNodePosition, IServiceNodePosition,
IProject, IProject,
IEditServiceFormDependsOn IProjectPayload,
} from "../../types"; } from "../../types";
import eventBus from "../../events/eventBus"; import eventBus from "../../events/eventBus";
import { useMutation } from "react-query";
import { import {
createProject,
useProject, useProject,
useUpdateProject, useUpdateProject
createProject
} from "../../hooks/useProject"; } from "../../hooks/useProject";
import useWindowDimensions from "../../hooks/useWindowDimensions"; import useWindowDimensions from "../../hooks/useWindowDimensions";
import { generatePayload } from "../../utils/generators";
import { nodeLibraries } from "../../utils/data/libraries"; import { nodeLibraries } from "../../utils/data/libraries";
import { import {
getClientNodeItem, getClientNodeItem,
@ -28,8 +25,6 @@ import {
getClientNodesAndConnections, getClientNodesAndConnections,
getMatchingSetIndex getMatchingSetIndex
} from "../../utils"; } from "../../utils";
import { checkHttpStatus } from "../../services/helpers";
import { generateHttp } from "../../services/generate";
import { Canvas } from "../Canvas"; import { Canvas } from "../Canvas";
import Spinner from "../global/Spinner"; import Spinner from "../global/Spinner";
import ModalConfirmDelete from "../modals/ConfirmDelete"; 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 ModalNetwork from "../modals/docker-compose/network";
import CreateVolumeModal from "../modals/docker-compose/volume/CreateVolumeModal"; import CreateVolumeModal from "../modals/docker-compose/volume/CreateVolumeModal";
import EditVolumeModal from "../modals/docker-compose/volume/EditVolumeModal"; import EditVolumeModal from "../modals/docker-compose/volume/EditVolumeModal";
import CodeEditor from "../CodeEditor";
import { useTitle } from "../../hooks"; import { useTitle } from "../../hooks";
import VisibilitySwitch from "../global/VisibilitySwitch"; import CodeBox from "./CodeBox";
import _ from "lodash"; import Header from "./Header";
import { useMutation } from "react-query";
interface IProjectProps { interface IProjectProps {
isAuthenticated: boolean; isAuthenticated: boolean;
@ -56,10 +51,8 @@ export default function Project(props: IProjectProps) {
useRef<Dictionary<IServiceNodeItem | IVolumeNodeItem>>(); useRef<Dictionary<IServiceNodeItem | IVolumeNodeItem>>();
const stateConnectionsRef = useRef<[[string, string]] | []>(); const stateConnectionsRef = useRef<[[string, string]] | []>();
const stateNetworksRef = useRef({}); const stateNetworksRef = useRef({});
const stateProjectRef = useRef();
const [isVisible, setIsVisible] = useState(false);
const [generatedCode, setGeneratedCode] = useState<string>();
const [formattedCode, setFormattedCode] = useState<string>("");
const [showModalCreateService, setShowModalCreateService] = useState(false); const [showModalCreateService, setShowModalCreateService] = useState(false);
const [showVolumesModal, setShowVolumesModal] = useState(false); const [showVolumesModal, setShowVolumesModal] = useState(false);
const [showNetworksModal, setShowNetworksModal] = useState(false); const [showNetworksModal, setShowNetworksModal] = useState(false);
@ -74,14 +67,9 @@ export default function Project(props: IProjectProps) {
const [volumeToDelete, setVolumeToDelete] = useState<IVolumeNodeItem | null>( const [volumeToDelete, setVolumeToDelete] = useState<IVolumeNodeItem | null>(
null null
); );
const [language, setLanguage] = useState("yaml");
const [version, setVersion] = useState("3");
const [copyText, setCopyText] = useState("Copy");
const [nodes, setNodes] = useState<Record<string, any>>({}); const [nodes, setNodes] = useState<Record<string, any>>({});
const [connections, setConnections] = useState<[[string, string]] | []>([]); const [connections, setConnections] = useState<[[string, string]] | []>([]);
const [networks, setNetworks] = useState<Record<string, any>>({}); const [networks, setNetworks] = useState<Record<string, any>>({});
const [projectName, setProjectName] = useState("Untitled");
const [canvasPosition, setCanvasPosition] = useState({ const [canvasPosition, setCanvasPosition] = useState({
top: 0, top: 0,
left: 0, left: 0,
@ -109,10 +97,7 @@ export default function Project(props: IProjectProps) {
stateNodesRef.current = nodes; stateNodesRef.current = nodes;
stateConnectionsRef.current = connections; stateConnectionsRef.current = connections;
stateNetworksRef.current = networks; stateNetworksRef.current = networks;
stateProjectRef.current = data;
const handleNameChange = (e: any) => {
setProjectName(e.target.value);
};
const onNodeUpdate = (positionData: IServiceNodePosition) => { const onNodeUpdate = (positionData: IServiceNodePosition) => {
if (stateNodesRef.current) { 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(() => { useEffect(() => {
if (!data) { if (!data) {
return; return;
@ -167,42 +122,41 @@ export default function Project(props: IProjectProps) {
nodesAsList, nodesAsList,
nodeLibraries nodeLibraries
); );
setProjectName(data.name);
setIsVisible(Boolean(data.visibility));
setNodes(clientNodeItems); setNodes(clientNodeItems);
setConnections(canvasData.canvas.connections); setConnections(canvasData.canvas.connections);
setNetworks(canvasData.canvas.networks); setNetworks(canvasData.canvas.networks);
setCanvasPosition(canvasData.canvas.position); setCanvasPosition(canvasData.canvas.position);
}, [data]); }, [data]);
const debouncedOnGraphUpdate = useMemo( const onSave = (partial: any) => {
() => const base: IProjectPayload = {
debounce((payload) => { name: data?.name ?? "",
generateHttp(JSON.stringify(payload)) visibility: data?.visibility ?? 0,
.then(checkHttpStatus) data: {
.then((data) => { canvas: {
if (data["code"].length) { position: canvasPosition,
for (let i = 0; i < data["code"].length; ++i) { nodes: nodes,
data["code"][i] = data["code"][i].replace(/(\r\n|\n|\r)/gm, ""); connections: connections,
} networks: networks
}
}
};
const code = data["code"].join("\n"); const payload = { ...base, ...partial };
setGeneratedCode(code);
} if (uuid) {
}) updateProjectMutation.mutate(payload);
.catch(() => undefined) } else {
.finally(() => undefined); createProjectMutation.mutate(payload);
}, 600), }
[] };
);
const onGraphUpdate = (graphData: any) => { const onGraphUpdate = (graphData: any) => {
const data = { ...graphData }; const data = { ...graphData };
data.version = version;
data.networks = stateNetworksRef.current; data.networks = stateNetworksRef.current;
const payload = generatePayload(data); eventBus.dispatch("FETCH_CODE", {
debouncedOnGraphUpdate(payload); message: data
});
}; };
const onCanvasUpdate = (updatedCanvasPosition: any) => { const onCanvasUpdate = (updatedCanvasPosition: any) => {
@ -404,32 +358,6 @@ export default function Project(props: IProjectProps) {
eventBus.dispatch("NODE_DELETED", { message: { node: node } }); 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 (!isFetching) {
if (!error) { if (!error) {
return ( return (
@ -495,70 +423,15 @@ export default function Project(props: IProjectProps) {
) : null} ) : null}
<div className="md:pl-16 flex flex-col flex-1"> <div className="md:pl-16 flex flex-col flex-1">
<div className="px-4 py-3 border-b border-gray-200"> <Header
<form onSave={onSave}
className="flex flex-col space-y-2 md:space-y-0 md:flex-row md:justify-between items-center" isLoading={
autoComplete="off" updateProjectMutation.isLoading ||
> createProjectMutation.isLoading
<input }
className={` projectData={data}
bg-gray-100 isAuthenticated={isAuthenticated}
appearance-none />
w-full
md:w-1/2
lg:w-1/3
block
text-gray-700
border
border-gray-100
dark:bg-gray-900
dark:text-white
dark:border-gray-900
rounded
py-2
px-3
leading-tight
focus:outline-none
focus:border-indigo-400
focus:ring-0
`}
type="text"
placeholder="Project name"
autoComplete="off"
id="name"
name="name"
onChange={handleNameChange}
value={projectName}
/>
<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">
{isAuthenticated && (
<VisibilitySwitch
isVisible={isVisible}
onToggle={() => {
setIsVisible(!isVisible);
}}
/>
)}
<button
onClick={() => onSave()}
type="button"
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">
{updateProjectMutation.isLoading && (
<Spinner className="w-4 h-4 text-green-300" />
)}
{createProjectMutation.isLoading && (
<Spinner className="w-4 h-4 text-green-300" />
)}
<span>Save</span>
</div>
</button>
</div>
</form>
</div>
<div className="flex flex-grow relative"> <div className="flex flex-grow relative">
<div <div
@ -631,51 +504,7 @@ export default function Project(props: IProjectProps) {
</div> </div>
<div className="group code-column w-1/2 md:w-1/3 absolute top-0 right-0 sm:relative z-40 md:z-30"> <div className="group code-column w-1/2 md:w-1/3 absolute top-0 right-0 sm:relative z-40 md:z-30">
<div <CodeBox />
className={`absolute top-0 left-0 right-0 z-10 flex justify-end p-1 space-x-2 group-hover:visible invisible`}
>
<select
id="version"
onChange={versionChange}
value={version}
className="input-util w-min pr-8"
>
<option value="1">v 1</option>
<option value="2">v 2</option>
<option value="3">v 3</option>
</select>
<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={() => {
return;
}}
disabled={true}
lineWrapping={false}
height={height - 64}
/>
</div> </div>
</div> </div>
</div> </div>

@ -433,13 +433,13 @@ export const useJsPlumb = (
useEffect(() => { useEffect(() => {
eventBus.on("GENERATE", () => { eventBus.on("GENERATE", () => {
if (!instance) return; if (!instanceRef.current) return;
if (stateRef.current) { if (stateRef.current) {
onGraphUpdate({ onGraphUpdate({
nodes: stateRef.current, nodes: stateRef.current,
connections: getConnections( connections: getConnections(
instance.getConnections({}, true) as Connection[] instanceRef.current.getConnections({}, true) as Connection[]
) )
}); });
} }

@ -1,7 +1,16 @@
import { API_SERVER_URL } from "../constants"; import { API_SERVER_URL } from "../constants";
export const generateHttp = (data: string) => { export const generateHttp = (data: string, manifest: number) => {
return fetch(`${API_SERVER_URL}/generate/`, { let endpoint = `${API_SERVER_URL}/generate/`;
if (manifest === 0) {
endpoint += "docker-compose";
}
if (manifest === 1) {
endpoint += "kubernetes";
}
return fetch(endpoint, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"

Loading…
Cancel
Save