From 163ae9c3b74f4b25d6abd98c08719c941f80d385 Mon Sep 17 00:00:00 2001 From: Frikky Date: Thu, 5 Dec 2024 07:44:47 +0100 Subject: [PATCH] Synced app creation modal --- frontend/src/components/AppCreationModal.jsx | 777 +++++++++++++++++++ 1 file changed, 777 insertions(+) create mode 100644 frontend/src/components/AppCreationModal.jsx diff --git a/frontend/src/components/AppCreationModal.jsx b/frontend/src/components/AppCreationModal.jsx new file mode 100644 index 00000000..7b650fbd --- /dev/null +++ b/frontend/src/components/AppCreationModal.jsx @@ -0,0 +1,777 @@ +import React, { useState, useRef, useEffect } from 'react' +import { useNavigate, Link } from 'react-router-dom'; +import { + Dialog, + DialogTitle, + DialogContent, + Typography, + Paper, + Button, + FormControl, + TextField, + DialogActions, + IconButton, + CircularProgress +} from '@mui/material' + +// Icons +import CloseIcon from '@mui/icons-material/Close' +import PublishIcon from '@mui/icons-material/Publish' +import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh' +import CreateIcon from '@mui/icons-material/Create' +import { toast } from 'react-toastify' +import YAML from "yaml"; + + +const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => { + const [openApiModal, setOpenApiModal] = useState(false) + const [generateAppModal, setGenerateAppModal] = useState(false) + const [openApi, setOpenApi] = useState("") + const [openApiData, setOpenApiData] = useState("") + const [openApiError, setOpenApiError] = useState("") + const [validation, setValidation] = useState(false) + const [appValidation, setAppValidation] = useState("") + const [isDropzone, setIsDropzone] = useState(false) + + const navigate = useNavigate() + const upload = useRef() + + // Style for the create options + const AppCreateButton = ({ text, func, icon }) => { + const [hover, setHover] = React.useState(false) + const makeFancy = text?.includes("Generate") + + const parsedStyle = { + flex: 1, + padding: 20, + margin: 12, + paddingTop: 30, + backgroundColor: hover && !makeFancy ? theme.palette.surfaceColor : "transparent", + cursor: hover ? "pointer" : "default", + textAlign: "center", + minHeight: 180, + maxHeight: 180, + borderRadius: 8, + border: makeFancy + ? "1px solid transparent" + : hover + ? "" + : "1px solid rgba(255,255,255,0.3)", + borderImage: makeFancy ? "linear-gradient(45deg, red, orange, yellow, green, blue, indigo, violet) 1" : "none", + transition: 'all 0.2s ease-in-out', + } + + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + onClick={func} + style={parsedStyle} + > + {icon} + {text} + + ) + } + + + const uploadFile = (e) => { + const isDropzone = + e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; + const files = isDropzone ? e.dataTransfer.files : e.target.files; + + const reader = new FileReader(); + + try { + reader.addEventListener("load", (e) => { + const content = e.target.result; + setOpenApiData(content); + setIsDropzone(isDropzone); + setOpenApiModal(true); + }); + } catch (e) { + console.log("Error in dropzone: ", e); + } + + try { + reader.readAsText(files[0]); + } catch (error) { + toast("Failed to read file"); + } + }; + + useEffect(() => { + if (openApiData.length > 0) { + setOpenApiError(""); + validateOpenApi(openApiData); + } + }, [openApiData]); + + + // useEffect(() => { + // console.log("APPVALID: ", appValidation) + // redirectOpenApi() + // }, [appValidation]) + + useEffect(() => { + if (appValidation && isDropzone) { + redirectOpenApi(); + setIsDropzone(false); + } + }, [appValidation, isDropzone]); + + const validateDocumentationUrl = () => { + setValidation(true); + + // curl https://doc-to-openapi-stbuwivzoq-nw.a.run.app/doc_to_openapi -d '{"url": "https://gitlab.com/rhab/PyOTRS/-/raw/main/pyotrs/lib.py?ref_type=heads"}' -H "Content-Type: application/json" + const urldata = { + "url": openApi, + } + + //fetch("http://localhost:8080/doc_to_openapi", { + //fetch("https://doc-to-openapi-stbuwivzoq-nw.a.run.app/doc_to_openapi", { + fetch("https://doc-to-openapi-stbuwivzoq-nw.a.run.app/api/v1/doc_to_openapi", { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(urldata), + }) + .then((response) => { + setValidation(false); + if (response.status !== 200) { + toast("Error in generation: " + response.status); + setOpenApiError("Error in generation - bad status: " + response.status); + return response.text(); + } + + return response.json(); + }) + .then((responseJson) => { + // Check if openapi or swagger in string of the json + var parsedtext = responseJson + try { + parsedtext = JSON.stringify(responseJson); + if (parsedtext.indexOf("openapi") === -1 && parsedtext.indexOf("swagger") === -1) { + setValidation(false) + setOpenApiError("Error in generation: " + parsedtext) + + return + } + } catch (e) { + setValidation(false); + setOpenApiError("Error in generation (2): " + e.toString()); + return; + } + + console.log("Validating response!"); + validateOpenApi(parsedtext) + }) + .catch((error) => { + setValidation(false); + toast(error.toString()); + setOpenApiError(error.toString()); + }); + } + + const validateRemote = () => { + setValidation(true); + + fetch(globalUrl + "/api/v1/get_openapi_uri", { + method: "POST", + headers: { + Accept: "application/json", + }, + body: JSON.stringify(openApi), + credentials: "include", + }) + .then((response) => { + setValidation(false); + if (response.status !== 200) { + return response.json(); + } + + return response.text(); + }) + .then((responseJson) => { + if (typeof responseJson !== "string" && !responseJson.success) { + console.log(responseJson.reason); + if (responseJson.reason !== undefined) { + setOpenApiError(responseJson.reason); + } else { + setOpenApiError("Undefined issue with OpenAPI validation"); + } + return; + } + + console.log("Validating response!"); + validateOpenApi(responseJson); + }) + .catch((error) => { + toast(error.toString()); + setOpenApiError(error.toString()); + }); + }; + + const escapeApiData = (apidata) => { + //console.log(apidata) + try { + return JSON.stringify(JSON.parse(apidata)); + } catch (error) { + console.log("JSON DECODE ERROR - TRY YAML"); + } + + try { + const parsed = YAML.parse(YAML.stringify(apidata)); + //const parsed = YAML.parse(apidata)) + console.log(YAML.stringify(parsed)) + return YAML.stringify(parsed); + } catch (error) { + console.log("YAML DECODE ERROR - TRY SOMETHING ELSE?: " + error); + setOpenApiError("Local error: " + error.toString()); + } + + return ""; + }; + + + const validateOpenApi = (openApidata) => { + var newApidata = escapeApiData(openApidata); + if (newApidata === "") { + // Used to return here + newApidata = openApidata; + return; + } + + //console.log(newApidata) + + setValidation(true); + fetch(globalUrl + "/api/v1/validate_openapi", { + method: "POST", + headers: { + Accept: "application/json", + }, + body: openApidata, + credentials: "include", + }) + .then((response) => { + + setValidation(false); + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success) { + setAppValidation(responseJson.id); + } else { + if (responseJson.reason !== undefined) { + setOpenApiError(responseJson.reason); + } + toast("An error occurred in the response"); + } + }) + .catch((error) => { + setValidation(false); + toast(error.toString()); + setOpenApiError(error.toString()); + }); + }; + + const redirectOpenApi = () => { + if (appValidation === undefined || appValidation === null || appValidation.length === 0) { + return + } + + toast.success("Successfully validated OpenAPI. Redirecting to app creation. Remember to save the app to be able to use it.", { + // Disable autoclose + autoClose: 10000, + }) + navigate(`/apps/new?id=${appValidation}`) + } + + + + + // const validateOpenApi = (openApidata) => { + + // var newApidata = escapeApiData(openApidata); + // if (newApidata === "") { + // // Used to return here + // newApidata = openApidata; + // return; + // } + // setValidation(true) + // const url = `${globalUrl}/api/v1/verify_openapi` + + // fetch(url, { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Accept: "application/json", + // }, + // body: JSON.stringify(openApidata), + // credentials: "include", + // }) + // .then((response) => response.json()) + // .then((responseJson) => { + // if (responseJson.success === false) { + // setOpenApiError(responseJson.reason) + // setValidation(false) + // setAppValidation("") + // } else { + // setAppValidation(responseJson.id) + // setValidation(false) + // } + // }) + // .catch((error) => { + // setOpenApiError("Failed loading: " + error.toString()) + // setValidation(false) + // setAppValidation("") + // }) + // } + + + + + + // Validation and redirect functions + + + + const circularLoader = validation ? : null + const errorText = openApiError?.length > 0 ?
Error: {openApiError}
: null + + // Common dialog styles + const dialogStyle = { + borderRadius: 2, + border: "1px solid #494949", + minWidth: '500px', + fontFamily: theme?.typography?.fontFamily, + backgroundColor: "#1A1A1A", + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: "#1A1A1A", + padding: '24px', + fontFamily: theme?.typography?.fontFamily, + }, + '& .MuiDialogTitle-root': { + backgroundColor: "#1A1A1A", + padding: '24px', + fontFamily: theme?.typography?.fontFamily, + }, + '& .MuiDialogActions-root': { + backgroundColor: "#1A1A1A", + padding: '16px 24px', + fontFamily: theme?.typography?.fontFamily, + }, + '& .MuiTypography-root': { + fontFamily: theme?.typography?.fontFamily, + }, + '& .MuiButton-root': { + fontFamily: theme?.typography?.fontFamily, + }, + transition: 'all 0.2s ease-in-out', + }; + + return ( + <> + {/* Main App Creation Modal */} + + + + Create New App + + + + + + +
+ { + setOpenApiModal(true) + onClose() + }} + icon={} + /> + { + setGenerateAppModal(true) + onClose() + }} + icon={} + /> +
+ +
+
+ + {/* OpenAPI Modal */} + { + setOpenApiModal(false) + setGenerateAppModal(false) + }} + PaperProps={{ + sx: { + ...dialogStyle, + bgcolor: '#1A1A1A', + maxWidth: '800px', + width: '100%' + } + }} + > + + + Create New App from Open API or Swagger + + setOpenApiModal(false)} + sx={{ + color: 'rgba(255,255,255,0.7)', + '&:hover': { bgcolor: 'rgba(255,255,255,0.1)' } + }} + > + + + + +
+ + Paste in the URI for the OpenAPI or find out + + + How to find URI for openAPI? + +
+ + +
+ setOpenApi(e.target.value)} + /> + +
+ + + Must point to a version 2 or 3 OpenAPI specification. + + + + Or upload a YAML or JSON specification + + + + + + {errorText} +
+ + {circularLoader} + + +
+ + {/* Generate App Modal */} + { + setGenerateAppModal(false) + setOpenApiModal(false) + }} + PaperProps={{ + sx: { + ...dialogStyle, + bgcolor: '#1A1A1A', + maxWidth: '610px', + width: '100%', + } + }} + > + + + Generate an app based on documentation (beta) + + { + setGenerateAppModal(false) + setOpenApiModal(false) + }} + sx={{ + color: 'rgba(255,255,255,0.7)', + '&:hover': { bgcolor: 'rgba(255,255,255,0.1)' } + }} + > + + + + + + Paste in a URL, and we will make it into an app for you. + {isCloud ? "" : " Uses Shuffle Cloud (https://shuffler.io) for processing (for now)."} + + +
+ { + setOpenApi(e.target.value); + }} /> +
+ + + Should be a documentation page containing an API. + + {errorText} +
+ + {circularLoader} + { + !validation && + + This may take multiple minutes based on the size of the documentation. + + } + + +
+ + ) +} + +export default AppCreationModal