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"; import Dropzone from "./Dropzone.jsx"; const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenApi = false, prefillOpenApiData = "" }) => { 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() useEffect(() => { if (open && (startOpenApi || (prefillOpenApiData && prefillOpenApiData.length > 0))) { if (prefillOpenApiData && prefillOpenApiData.length > 0) { setOpenApiData(prefillOpenApiData) setIsDropzone(true) } setOpenApiModal(true) } }, [open, startOpenApi, prefillOpenApiData]) // 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: "30px 20px 20px", margin: 12, 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(to right, #ff8544 0%, #ec517c 50%, #9c5af2 100%) 1" : "none", transition: 'all 0.2s ease-in-out', paddingBottom: isCloud ? 0 : 175, } 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 === true) { setAppValidation(responseJson?.id) navigate(`/apps/new?id=${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: theme.palette.DialogStyle.border, minWidth: '500px', fontFamily: theme?.typography?.fontFamily, backgroundColor: theme.palette.DialogStyle.backgroundColor, zIndex: 1000, '& .MuiDialogContent-root': { backgroundColor: theme.palette.DialogStyle.backgroundColor, padding: '24px', fontFamily: theme?.typography?.fontFamily, }, '& .MuiDialogTitle-root': { backgroundColor: theme.palette.DialogStyle.backgroundColor, padding: '24px', fontFamily: theme?.typography?.fontFamily, }, '& .MuiDialogActions-root': { backgroundColor: theme.palette.DialogStyle.backgroundColor, 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 { setOpenApi("") setOpenApiError("") setAppValidation("") setValidation(false) onClose() }} sx={{ color: theme.palette.text.primary, '&:hover': { bgcolor: theme.palette.hoverColor }, }} >
{ 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) setGenerateAppModal(false) setOpenApi("") setOpenApiError("") setAppValidation("") setValidation(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
upload.current.click()} >
Drag & drop your OpenAPI (YAML/JSON) anywhere or click to browse files
{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) setOpenApi("") setOpenApiError("") setAppValidation("") setValidation(false) }} sx={{ color: theme.palette.text.primary, '&:hover': { bgcolor: theme.palette.hoverColor } }} > 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