diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 17bcab31..e2c7a41d 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -45,6 +45,11 @@ import AlertTemplate from "./components/AlertTemplate"; import { isMobile } from "react-device-detect"; import RuntimeDebugger from "./components/RuntimeDebugger.jsx" +import MFASetUp from './components/MFASetUP.jsx'; +import ApiExplorerWrapper from './views/ApiExplorerWrapper.jsx'; +import LeftSideBar from './components/LeftSideBar.jsx'; +import CodeWorkflow from './views/CodeWorkflow.jsx'; + import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; @@ -204,6 +209,11 @@ const App = (message, props) => { {curpath.includes("/workflows") && curpath.includes("/run") ?
: + isLoggedIn ? +
+ +
+ :
{ /> } /> + } /> { /> } /> + } /> } /> } /> + + } /> + } /> + } /> + { /> } /> + } />
+ ); +} + +function a11yProps(index) { + return { + id: `simple-tab-${index}`, + "aria-controls": `simple-tabpanel-${index}`, + }; +} + +const RequestMethods = [ + { + value: "GET", + color: "#61afee", + }, + { + value: "POST", + color: "#49cc90", + }, + { + value: "DELETE", + color: "#f93e3e", + }, + { + value: "PUT", + color: "#fca130", + }, + { + value: "PATCH", + color: "#50e3c2", + }, + { + value: "CONNECT", + color: "#ff69b4", + }, + { + value: "HEAD", + color: "#9012fe", + }, +]; + +const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, selectedAppData, ConfigurationTab }) => { + const [actions, setActions] = useState([]); + const [info, setInfo] = useState({}); + const [serverurl, setServerUrl] = useState(""); + const [selectedActionIndex, setSelectedActionIndex] = useState(0); + const [ExampleBody, setExampleBody] = useState({}); + const [filteredActions, setFilteredActions] = useState([]); + + const getJsonObject = (properties) => { + + let jsonObject = {}; + for (let key in properties) { + const property = properties[key]; + + let subloop = false; + if (property.hasOwnProperty("type")) { + if (property.type === "object" || property.type === "array") { + subloop = true; + } + } + + if (subloop) { + if ( + property.hasOwnProperty("items") && + property.items.hasOwnProperty("properties") + ) { + const jsonret = getJsonObject(property.items.properties); + if (property.type === "array") { + jsonObject[key] = [jsonret]; + } else { + jsonObject[key] = jsonret; + } + } else { + if (property.hasOwnProperty("properties")) { + const jsonret = getJsonObject(property.properties); + if (property.type === "array") { + jsonObject[key] = [jsonret]; + } else { + jsonObject[key] = jsonret; + } + } else { + } + } + } else { + if (property.hasOwnProperty("example")) { + jsonObject[key] = property.example; + } else if ( + property.hasOwnProperty("enum") && + property.enum.length > 0 + ) { + jsonObject[key] = property.enum[0]; + } else if (property.hasOwnProperty("default")) { + jsonObject[key] = property.default; + } else if (property.hasOwnProperty("maximum")) { + jsonObject[key] = property.maximum; + } else if (property.hasOwnProperty("minimum")) { + jsonObject[key] = property.minimum; + } else if (property.hasOwnProperty("type")) { + if (property.type === "integer" || property.type === "number") { + jsonObject[key] = 0; + } else if (property.type === "boolean") { + jsonObject[key] = false; + } else if (property.type === "string") { + jsonObject[key] = ""; + } else { + } + } else { + } + } + } + + return jsonObject; + }; + + const handleGetRef = (parameter, data) => { + try { + if (parameter === null || parameter["$ref"] === undefined) { + return parameter; + } + } catch (e) { + return parameter; + } + + const paramsplit = parameter["$ref"].split("/"); + if (paramsplit[0] !== "#") { + return parameter; + } + + var newitem = data; + for (let paramkey in paramsplit) { + var tmpparam = paramsplit[paramkey]; + if (tmpparam === "#") { + continue; + } + + if (newitem[tmpparam] === undefined) { + return parameter; + } + + newitem = newitem[tmpparam]; + } + return newitem; + }; + + useEffect(() => { + if (openapi !== undefined && openapi !== null) { + parseIncomingOpenapiData(openapi); + } + }, [openapi]); + + const parseIncomingOpenapiData = useCallback((data) => { + if (data.info !== null && data.info !== undefined) { + setInfo(data.info); + } + + try { + if (data.info !== null && data.info !== undefined) { + if (data.info.title !== undefined && data.info.title !== null) { + if (data.info.title.endsWith(" API")) { + data.info.title = data.info.title.substring( + 0, + data.info.title.length - 4 + ); + } else if (data.info.title.endsWith("API")) { + data.info.title = data.info.title.substring( + 0, + data.info.title.length - 3 + ); + } + } + + document.title = data.info.title + " Rest API" + + if ( + data.info["x-catefies"] !== undefined && + data.info["x-categories"].length > 0 + ) { + if (Array.isArray(data.info["x-categories"])) { + } else { + } + } + } + } catch (e) {} + + try { + if (data.tags !== undefined && data.tags.length > 0) { + var newtags = []; + for (let tagkey in data.tags) { + if (data.tags[tagkey]?.name.length > 50) { + continue; + } + + newtags.push(data.tags[tagkey]?.name); + } + + if (newtags.length > 10) { + newtags = newtags.slice(0, 9); + } + } + } catch (e) {} + + // This is annoying (: + // Weird generator problems to be handle + var securitySchemes = undefined; + try { + if (data.securitySchemes !== undefined) { + securitySchemes = data.securitySchemes; + if (securitySchemes === undefined) { + securitySchemes = data.securityDefinitions; + } + } + + if (securitySchemes === undefined && data.components !== undefined) { + securitySchemes = data.components.securitySchemes; + if (securitySchemes === undefined) { + securitySchemes = data.components.securityDefinitions; + } + } + } catch (e) {} + + const allowedfunctions = [ + "GET", + "CONNECT", + "HEAD", + "DELETE", + "POST", + "PATCH", + "PUT", + ]; + + var newActions = []; + var wordlist = {}; + var all_categories = []; + var parentUrl = ""; + + if (data.paths !== null && data.paths !== undefined) { + for (let [path, pathvalue] of Object.entries(data.paths)) { + for (let [method, methodvalue] of Object.entries(pathvalue)) { + if (methodvalue === null) { + continue; + } + + if (!allowedfunctions.includes(method.toUpperCase())) { + // Typical YAML issue + if (method !== "parameters") { + //toast("Skipped method (not allowed): " + method); + } + continue; + } + + var tmpname = methodvalue.summary; + if ( + methodvalue.operationId !== undefined && + methodvalue.operationId !== null && + methodvalue.operationId.length > 0 && + (tmpname === undefined || tmpname.length === 0) + ) { + tmpname = methodvalue.operationId; + } + + if (tmpname !== undefined && tmpname !== null) { + tmpname = tmpname.replaceAll(".", " "); + } + + if ( + (tmpname === undefined || tmpname === null) && + methodvalue.description !== undefined && + methodvalue.description !== null && + methodvalue.description.length > 0 + ) { + tmpname = methodvalue.description + .replaceAll(".", " ") + .replaceAll("_", " "); + } + + var newaction = { + name: tmpname, + description: methodvalue.description, + url: path, + file_field: "", + method: method.toUpperCase(), + headers: "", + queries: [], + paths: [], + body: "", + errors: [], + example_response: "", + action_label: "No Label", + required_bodyfields: [], + }; + + if ( + methodvalue["x-label"] !== undefined && + methodvalue["x-label"] !== null + ) { + // FIX: Map labels only if they're actually in the category list + newaction.action_label = methodvalue["x-label"]; + } + + if ( + methodvalue["x-required-fields"] !== undefined && + methodvalue["x-required-fields"] !== null + ) { + newaction.required_bodyfields = methodvalue["x-required-fields"]; + } + + if ( + newaction.url !== undefined && + newaction.url !== null && + newaction.url.includes("_shuffle_replace_") + ) { + //const regex = /_shuffle_replace_\d/i; + const regex = /_shuffle_replace_\d+/i; + + newaction.url = newaction.url.replaceAll( + new RegExp(regex, "g"), + "" + ); + } + + // Finding category + if (path.includes("/")) { + const pathsplit = path.split("/"); + // Stupid way of finding a category/grouping + for (let splitkey in pathsplit) { + if (pathsplit[splitkey].includes("_shuffle_replace_")) { + //const regex = /_shuffle_replace_\d/i; + const regex = /_shuffle_replace_\d+/i; + pathsplit[splitkey] = pathsplit[splitkey].replaceAll( + new RegExp(regex, "g"), + "" + ); + } + + if ( + pathsplit[splitkey].length > 0 && + pathsplit[splitkey] !== "v1" && + pathsplit[splitkey] !== "v2" && + pathsplit[splitkey] !== "api" && + pathsplit[splitkey] !== "1.0" && + pathsplit[splitkey] !== "apis" + ) { + newaction["category"] = pathsplit[splitkey]; + if (!all_categories.includes(pathsplit[splitkey])) { + all_categories.push(pathsplit[splitkey]); + } + break; + } + } + } + + if (path === "/files/{file_id}/content") { + } + + // Typescript? I think not ;) + if (methodvalue["requestBody"] !== undefined) { + if ( + methodvalue["requestBody"]["$ref"] !== undefined && + methodvalue["requestBody"]["$ref"] !== null + ) { + // Handle ref + const parameter = handleGetRef( + { $ref: methodvalue["requestBody"]["$ref"] }, + data + ); + if ( + parameter.content !== undefined && + parameter.content !== null + ) { + methodvalue["requestBody"]["content"] = parameter.content; + } + } + + if (methodvalue["requestBody"]["content"] !== undefined) { + // Handle content - XML or JSON + // + if ( + methodvalue["requestBody"]["content"]["application/json"] !== + undefined + ) { + if ( + methodvalue["requestBody"]["content"]["application/json"][ + "schema" + ] !== undefined && + methodvalue["requestBody"]["content"]["application/json"][ + "schema" + ] !== null + ) { + try { + if ( + methodvalue["requestBody"]["content"]["application/json"][ + "schema" + ]["properties"] !== undefined + ) { + // Read out properties from a JSON object + const jsonObject = getJsonObject( + methodvalue["requestBody"]["content"][ + "application/json" + ]["schema"]["properties"] + ); + if (jsonObject !== undefined && jsonObject !== null) { + try { + newaction["body"] = JSON.stringify( + jsonObject, + null, + 2 + ); + } catch (e) {} + } + + //newaction["body"] = JSON.stringify(jsonObject, null, 2); + + var tmpobject = {}; + for (let prop of methodvalue["requestBody"]["content"][ + "application/json" + ]["schema"]["properties"]) { + tmpobject[prop] = `\$\{${prop}\}`; + } + for (let subkey in methodvalue["requestBody"]["content"][ + "application/json" + ]["schema"]["required"]) { + const tmpitem = + methodvalue["requestBody"]["content"][ + "application/json" + ]["schema"]["required"][subkey]; + tmpobject[tmpitem] = `\$\{${tmpitem}\}`; + } + + newaction["body"] = JSON.stringify(tmpobject, null, 2); + } else if ( + methodvalue["requestBody"]["content"]["application/json"][ + "schema" + ]["$ref"] !== undefined && + methodvalue["requestBody"]["content"]["application/json"][ + "schema" + ]["$ref"] !== null + ) { + const retRef = handleGetRef( + methodvalue["requestBody"]["content"][ + "application/json" + ]["schema"], + data + ); + var newbody = {}; + for (let propkey in retRef.properties) { + const parsedkey = propkey + .replaceAll(" ", "_") + .toLowerCase(); + newbody[parsedkey] = "${" + parsedkey + "}"; + } + + newaction["body"] = JSON.stringify(newbody, null, 2); + } + } catch (e) {} + } + } else if ( + methodvalue["requestBody"]["content"]["application/xml"] !== + undefined + ) { + //newaction["headers"] = "" + //"Content-Type=application/xml\nAccept=application/xml"; + if ( + methodvalue["requestBody"]["content"]["application/xml"][ + "schema" + ] !== undefined && + methodvalue["requestBody"]["content"]["application/xml"][ + "schema" + ] !== null + ) { + try { + if ( + methodvalue["requestBody"]["content"]["application/xml"][ + "schema" + ]["properties"] !== undefined + ) { + for (let [prop, propvalue] of Object.entries( + methodvalue["requestBody"]["content"][ + "application/xml" + ]["schema"]["properties"] + )) { + tmpobject[prop] = `\$\{${prop}\}`; + } + + for (let [subkey, subkeyval] in Object.entries( + methodvalue["requestBody"]["content"][ + "application/xml" + ]["schema"]["required"] + )) { + const tmpitem = + methodvalue["requestBody"]["content"][ + "application/xml" + ]["schema"]["required"][subkey]; + tmpobject[tmpitem] = `\$\{${tmpitem}\}`; + } + + //newaction["body"] = XML.stringify(tmpobject, null, 2) + } + } catch (e) {} + } + } else { + if ( + methodvalue["requestBody"]["content"]["example"] !== undefined + ) { + if ( + methodvalue["requestBody"]["content"]["example"][ + "example" + ] !== undefined + ) { + newaction["body"] = + methodvalue["requestBody"]["content"]["example"][ + "example" + ]; + } + } + + if ( + methodvalue["requestBody"]["content"][ + "multipart/form-data" + ] !== undefined + ) { + if ( + methodvalue["requestBody"]["content"][ + "multipart/form-data" + ]["schema"] !== undefined && + methodvalue["requestBody"]["content"][ + "multipart/form-data" + ]["schema"] !== null + ) { + try { + if ( + methodvalue["requestBody"]["content"][ + "multipart/form-data" + ]["schema"]["type"] === "object" + ) { + const fieldname = + methodvalue["requestBody"]["content"][ + "multipart/form-data" + ]["schema"]["properties"]["fieldname"]; + + if (fieldname !== undefined) { + newaction.file_field = fieldname["value"]; + } else { + for (const [subkey, subvalue] of Object.entries( + methodvalue["requestBody"]["content"][ + "multipart/form-data" + ]["schema"]["properties"] + )) { + if (subkey.includes("file")) { + newaction.file_field = subkey; + break; + } + } + + if ( + newaction.file_field === undefined || + newaction.file_field === null || + newaction.file_field.length === 0 + ) { + } + } + } else { + } + } catch (e) {} + } + } else { + var schemas = []; + const content = methodvalue["requestBody"]["content"]; + if (content !== undefined && content !== null) { + for (const [subkey, subvalue] of Object.entries(content)) { + if ( + subvalue["schema"] !== undefined && + subvalue["schema"] !== null + ) { + if ( + subvalue["schema"]["$ref"] !== undefined && + subvalue["schema"]["$ref"] !== null + ) { + if (!schemas.includes(subvalue["schema"]["$ref"])) { + schemas.push(subvalue["schema"]["$ref"]); + } + } + } else { + if ( + subvalue["example"] !== undefined && + subvalue["example"] !== null + ) { + newaction["body"] = subvalue["example"]; + } else { + } + } + } + } + + try { + if (schemas.length === 1) { + const parameter = handleGetRef( + { $ref: schemas[0] }, + data + ); + if ( + parameter.properties !== undefined && + parameter["type"] === "object" + ) { + var newbody = {}; + for (let propkey in parameter.properties) { + const parsedkey = propkey + .replaceAll(" ", "_") + .toLowerCase(); + if ( + parameter.properties[propkey].type === undefined + ) { + continue; + } + + if (parameter.properties[propkey].type === "string") { + if ( + parameter.properties[propkey].description !== + undefined + ) { + newbody[parsedkey] = + parameter.properties[propkey].description; + } else { + newbody[parsedkey] = ""; + } + } else if ( + parameter.properties[propkey].type.includes( + "int" + ) || + parameter.properties[propkey].type.includes( + "uint64" + ) + ) { + newbody[parsedkey] = 0; + } else if ( + parameter.properties[propkey].type.includes( + "boolean" + ) + ) { + newbody[parsedkey] = false; + } else if ( + parameter.properties[propkey].type.includes("array") + ) { + newbody[parsedkey] = []; + } else { + newbody[parsedkey] = []; + } + } + + newaction["body"] = JSON.stringify(newbody, null, 2); + } else { + } + } + } catch (e) {} + } + } + } + } + + if ( + methodvalue.responses !== undefined && + methodvalue.responses !== null + ) { + if (methodvalue.responses.default !== undefined) { + if (methodvalue.responses.default.content !== undefined) { + if ( + methodvalue.responses.default.content["text/plain"] !== + undefined + ) { + if ( + methodvalue.responses.default.content["text/plain"][ + "schema" + ] !== undefined + ) { + if ( + methodvalue.responses.default.content["text/plain"][ + "schema" + ]["example"] !== undefined + ) { + newaction.example_response = + methodvalue.responses.default.content["text/plain"][ + "schema" + ]["example"]; + } + + if ( + methodvalue.responses.default.content["text/plain"][ + "schema" + ]["format"] === "binary" && + methodvalue.responses.default.content["text/plain"][ + "schema" + ]["type"] === "string" + ) { + newaction.example_response = "shuffle_file_download"; + } + } + } + } + } else { + var selectedReturn = ""; + if (methodvalue.responses["200"] !== undefined) { + selectedReturn = "200"; + } else if (methodvalue.responses["201"] !== undefined) { + selectedReturn = "201"; + } + + // Parsing examples. This should be standardized lol + if (methodvalue.responses[selectedReturn] !== undefined) { + const selectedExample = methodvalue.responses[selectedReturn]; + if (selectedExample["content"] !== undefined) { + if ( + selectedExample["content"]["application/json"] !== undefined + ) { + if ( + selectedExample["content"]["application/json"][ + "schema" + ] !== undefined && + selectedExample["content"]["application/json"][ + "schema" + ] !== null + ) { + if ( + selectedExample["content"]["application/json"][ + "schema" + ]["properties"] !== undefined && + selectedExample["content"]["application/json"][ + "schema" + ]["properties"] !== null + ) { + const jsonObject = getJsonObject( + selectedExample["content"]["application/json"][ + "schema" + ]["properties"] + ); + if (jsonObject !== undefined && jsonObject !== null) { + try { + newaction.example_response = JSON.stringify( + jsonObject, + null, + 2 + ); + } catch (e) {} + } + } + + if ( + selectedExample["content"]["application/json"][ + "schema" + ]["$ref"] !== undefined + ) { + const parameter = handleGetRef( + selectedExample["content"]["application/json"][ + "schema" + ], + data + ); + if ( + parameter.properties !== undefined && + parameter["type"] === "object" + ) { + var newbody = {}; + for (let propkey in parameter.properties) { + const parsedkey = propkey + .replaceAll(" ", "_") + .toLowerCase(); + if ( + parameter.properties[propkey].type === undefined + ) { + continue; + } + + if ( + parameter.properties[propkey].type === "string" + ) { + if ( + parameter.properties[propkey].description !== + undefined + ) { + newbody[parsedkey] = + parameter.properties[propkey].description; + } else { + newbody[parsedkey] = ""; + } + } else if ( + parameter.properties[propkey].type.includes("int") + ) { + newbody[parsedkey] = 0; + } else if ( + parameter.properties[propkey].type.includes( + "boolean" + ) + ) { + newbody[parsedkey] = false; + } else if ( + parameter.properties[propkey].type.includes( + "array" + ) + ) { + //const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"], data) + newbody[parsedkey] = []; + } else { + newbody[parsedkey] = []; + } + } + newaction.example_response = JSON.stringify( + newbody, + null, + 2 + ); + } else { + } + } else { + // Just selecting the first one. bleh. + if ( + selectedExample["content"]["application/json"][ + "schema" + ]["allOf"] !== undefined + ) { + var selectedComponent = + selectedExample["content"]["application/json"][ + "schema" + ]["allOf"]; + if (selectedComponent.length >= 1) { + selectedComponent = selectedComponent[0]; + + const parameter = handleGetRef( + selectedComponent, + data + ); + if ( + parameter.properties !== undefined && + parameter["type"] === "object" + ) { + var newbody = {}; + for (let propkey in parameter.properties) { + const parsedkey = propkey + .replaceAll(" ", "_") + .toLowerCase(); + if ( + parameter.properties[propkey].type === + undefined + ) { + continue; + } + + if ( + parameter.properties[propkey].type === + "string" + ) { + if ( + parameter.properties[propkey] + .description !== undefined + ) { + newbody[parsedkey] = + parameter.properties[propkey].description; + } else { + newbody[parsedkey] = ""; + } + } else if ( + parameter.properties[propkey].type.includes( + "int" + ) + ) { + newbody[parsedkey] = 0; + } else if ( + parameter.properties[propkey].type.includes( + "boolean" + ) + ) { + newbody[parsedkey] = false; + } else { + newbody[parsedkey] = []; + } + } + + newaction.example_response = JSON.stringify( + newbody, + null, + 2 + ); + //newaction.example_response = JSON.stringify(parameter.properties, null, 2) + } else { + //newaction.example_response = parameter.properties + } + } else { + } + } else if ( + selectedExample["content"]["application/json"][ + "schema" + ]["properties"] !== undefined + ) { + if ( + selectedExample["content"]["application/json"][ + "schema" + ]["properties"]["data"] !== undefined + ) { + const parameter = handleGetRef( + selectedExample["content"]["application/json"][ + "schema" + ]["properties"]["data"], + data + ); + if ( + parameter.properties !== undefined && + parameter["type"] === "object" + ) { + var newbody = {}; + for (let propkey in parameter.properties) { + const parsedkey = propkey + .replaceAll(" ", "_") + .toLowerCase(); + if ( + parameter.properties[propkey].type === + undefined + ) { + continue; + } + + if ( + parameter.properties[propkey].type === + "string" + ) { + if ( + parameter.properties[propkey] + .description !== undefined + ) { + newbody[parsedkey] = + parameter.properties[propkey].description; + } else { + newbody[parsedkey] = ""; + } + } else if ( + parameter.properties[propkey].type.includes( + "int" + ) + ) { + newbody[parsedkey] = 0; + } else { + newbody[parsedkey] = []; + } + } + + newaction.example_response = JSON.stringify( + newbody, + null, + 2 + ); + //newaction.example_response = JSON.stringify(parameter.properties, null, 2) + } else { + //newaction.example_response = parameter.properties + } + } + } + } + } + } + } + } + } + } + + for (let paramkey in methodvalue.parameters) { + const parameter = handleGetRef( + methodvalue.parameters[paramkey], + data + ); + + if (parameter.in === "query") { + var tmpaction = { + description: parameter.description, + name: parameter?.name, + required: parameter.required, + in: "query", + }; + + if ( + parameter.example !== undefined && + parameter.example !== null + ) { + tmpaction.example = parameter.example; + } + + if (parameter.required === undefined) { + tmpaction.required = false; + } + + newaction.queries.push(tmpaction); + } else if (parameter.in === "path") { + // FIXME - parse this to the URL too + newaction.paths.push(parameter?.name); + + // FIXME: This doesn't follow OpenAPI3 exactly. + // https://swagger.io/docs/specification/describing-request-body/ + // https://swagger.io/docs/specification/describing-parameters/ + // Need to split the data. + } else if (parameter.in === "body") { + // FIXME: Add tracking for components + // E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml + if ( + parameter.example !== undefined && + parameter.example !== null + ) { + if ( + newaction.body === undefined || + newaction.body === null || + newaction.body.length < 5 + ) { + newaction.body = parameter.example; + } + } + } else if (parameter.in === "header") { + newaction.headers += `${parameter?.name}=${parameter.example}\n`; + } else { + } + } + + // Check if body is valid JSON. + if ( + newaction.body !== undefined && + newaction.body !== null && + newaction.body.length > 0 + ) { + // Trim starting / ending newlines, spaces and tabs + newaction.body = newaction.body.trim(); + } + + if (newaction?.name === "" || newaction?.name === undefined) { + // Find a unique part of the string + // FIXME: Looks for length between /, find the one where they differ + // Should find others with the same START to their path + // Make a list of reserved names? Aka things that show up only once + if (Object.getOwnPropertyNames(wordlist).length === 0) { + for (let [newpath, pathvalue] of Object.entries(data.paths)) { + const newpathsplit = newpath.split("/"); + + for (let splitkey in newpathsplit) { + const pathitem = newpathsplit[splitkey].toLowerCase(); + if (wordlist[pathitem] === undefined) { + wordlist[pathitem] = 1; + } else { + wordlist[pathitem] += 1; + } + } + } + } + + // Remove underscores and make it normal with upper case etc + const urlsplit = path.split("/"); + if (urlsplit.length > 0) { + var curname = ""; + for (let urlkey in urlsplit) { + var subpath = urlsplit[urlkey]; + if (wordlist[subpath] > 2 || subpath.length < 1) { + continue; + } + + curname = subpath; + break; + } + + // FIXME: If name exists, + // FIXME: Check if first part of parsedname is verb, otherwise use method + const parsedname = curname + .split("_") + .join(" ") + .split("-") + .join(" ") + .split("{") + .join(" ") + .split("}") + .join(" ") + .trim(); + if (parsedname.length === 0) { + newaction.errors.push("Missing name"); + } else { + const newname = + method.charAt(0).toUpperCase() + + method.slice(1) + + " " + + parsedname; + const searchactions = newActions.find( + (data) => data?.name === newname + ); + + if (searchactions !== undefined) { + newaction.errors.push("Missing name"); + } else { + newaction.name = newname; + } + } + } else { + newaction.errors.push("Missing name"); + } + } + + //newaction.action_label = "No Label" + newActions.push(newaction); + } + } + + if (data.servers !== undefined && data.servers.length > 0) { + var firstUrl = data.servers[0].url; + if ( + firstUrl.includes("{") && + firstUrl.includes("}") && + data.servers[0].variables !== undefined + ) { + const regex = /{\w+}/g; + const found = firstUrl.match(regex); + if (found !== null) { + for (let foundkey in found) { + const item = found[foundkey].slice(1, found[foundkey].length - 1); + const foundVar = data.servers[0].variables[item]; + if (foundVar["default"] !== undefined) { + firstUrl = firstUrl.replace( + found[foundkey], + foundVar["default"] + ); + } + } + } + } + + if (firstUrl.endsWith("/")) { + parentUrl = firstUrl.slice(0, firstUrl.length - 1); + } else { + parentUrl = firstUrl; + } + } + } + var prefixCheck = "/v1"; + if (parentUrl.includes("/")) { + const urlsplit = parentUrl.split("/"); + if (urlsplit.length > 2) { + // Skip if http:// in it too + prefixCheck = "/" + urlsplit.slice(3).join("/"); + } + + if ( + prefixCheck.length > 0 && + prefixCheck !== "/" && + prefixCheck.startsWith("/") + ) { + for (var actionKey in newActions) { + const action = newActions[actionKey]; + + if ( + action.url !== undefined && + action.url !== null && + action.url.startsWith(prefixCheck) + ) { + newActions[actionKey].url = action.url.slice( + prefixCheck.length, + action.url.length + ); + } + } + } + } + + setServerUrl(parentUrl); + var newActions2 = []; + // Remove with duplicate action URLs + for (var actionKey in newActions) { + const action = newActions[actionKey]; + if (action.url === undefined || action.url === null) { + continue; + } + + var found = false; + for (var actionKey2 in newActions2) { + const action2 = newActions2[actionKey2]; + if (action2.url === undefined || action2.url === null) { + continue; + } + + if (action.url === action2.url) { + found = true; + break; + } + } + + if (!found) { + newActions2.push(action); + } else { + newActions2.push(action); + } + } + + newActions = newActions2; + + // Rearrange them by which has action_label + const firstActions = newActions.filter( + (data) => + data.action_label !== undefined && + data.action_label !== null && + data.action_label !== "No Label" + ); + const secondActions = newActions.filter( + (data) => + data.action_label === undefined || + data.action_label === null || + data.action_label === "No Label" + ); + newActions = firstActions.concat(secondActions); + setActions(newActions); + setExampleBody(newActions[0]?.body); + }, [openapi]); + + return ( +
+ + + + +
+ ); +}); + +export default ApiExplorer; + + +const ActionResponseAndRequest = memo(({ConfigurationTab, selectedAppData,actions, info, HandleApiExecution, userdata, filteredActions, setFilteredActions, serverurl, globalUrl, setSelectedActionIndex, ExampleBody, setExampleBody, selectedActionIndex}) => { + const [apiResponse, setApiResponse] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const loadAction = 10; + const loadedAction = useRef(null); + + const loadMoreActions = useCallback(() => { + if (isLoading || filteredActions.length >= actions.length) return; + + setIsLoading(true); + + setFilteredActions((prevActions) => { + const newActions = actions.slice(prevActions.length, prevActions.length + loadAction); + setIsLoading(false); + return [...prevActions, ...newActions]; + }); + }, [isLoading, actions.length, filteredActions.length, loadAction]); + + // Scroll position reference + const scrollPosition = useRef(0); + + // Handle scroll event with debounce + const handleScroll = useCallback(() => { + const actionContainer = loadedAction.current; + if ( + actionContainer && + actionContainer.scrollTop + actionContainer.clientHeight >= actionContainer.scrollHeight - 10 + ) { + loadMoreActions(); + } + scrollPosition.current = actionContainer?.scrollTop || 0; + }, [loadMoreActions]); + + // Add scroll event listener on mount and remove on unmount + useEffect(() => { + const actionContainer = loadedAction.current; + if (actionContainer) { + actionContainer.addEventListener("scroll", handleScroll); + } + return () => { + if (actionContainer) { + actionContainer.removeEventListener("scroll", handleScroll); + } + }; + }, [handleScroll]); + + + // Restore scroll position when the component rerenders or new items are added + useEffect(() => { + const actionContainer = loadedAction.current; + if (actionContainer) { + actionContainer.scrollTop = scrollPosition.current; + } + }, [actions, filteredActions]); + + useEffect(() => { + if (actions?.length > 0 && filteredActions?.length === 0) { + setFilteredActions(actions.slice(0, loadAction)); + } + }, [actions?.length]); + + return ( + +
+
+ {filteredActions.map((action, index) => ( +
+ +
+ ))} +
+ + +
+ )}) + + +const ActionsList = memo(({ + actions, + selectedActionIndex, + setSelectedActionIndex, + setExampleBody, + setFilteredActions, + filteredActions, + userdata, + info, + openapi, +}) => { + + const [searchQuery, setSearchQuery] = useState(""); + const [visibleActions, setVisibleActions] = useState([]); + + + useEffect(() => { + if (visibleActions?.length === 0 && actions?.length > 0) { + setVisibleActions(actions) + } + }, [actions?.length]) + + const handleActionClick = (index, action) => { + const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-"); + setSelectedActionIndex(index); + setExampleBody(action.example_response); + + const actionIndex = actions.findIndex((act) => { + const id = act.name.replace(/ /g, "-").replace(/_/g, "-"); + return id === actionId; + }); + + if (actionIndex !== -1 && !filteredActions.some((act) => { + const id = act.name.replace(/ /g, "-").replace(/_/g, "-"); + return id === actionId; + })) { + const newActionToLoad = [ + ...filteredActions, + ...actions.slice(filteredActions.length, actionIndex + 1) + ]; + setFilteredActions(newActionToLoad); + } + + // Update URL hash and scroll to action + window.history.pushState(null, "", `#${actionId}`); + const actionElement = document.getElementById(actionId); + if (actionElement) { + actionElement.scrollIntoView({ behavior: "smooth", block: "start" }); + } +}; + + const handleSearch = (e) => { + const query = e.target.value; + setSearchQuery(query); + if (query.length === 0) { + setVisibleActions(actions); + } else { + setVisibleActions( + actions.filter((action) => + action.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + ); + } + }; + return ( +
+
+
+ {info?.title ? ( +
+ app logo + + {info.title} + +
+ ) : ( + + Api Explorer + + )} +
+
+ + + + ), + style: { height: "100%", marginTop: 10, width: '90%', }, + }} + sx={{ + marginLeft: 2, + width:'100%', + "& .MuiOutlinedInput-root fieldset": { + border: "1px solid rgba(73, 73, 73, 1)", + }, + }} + /> +
+ {visibleActions.length > 0 ? ( + visibleActions.map((action, actionIndex) => ( + + )) + ) : ( +
+ No actions found +
+ )} +
+
+ ); +}); + + + +const Action = memo(( + { + action, + index, + serverurl, + setApiResponse, + setExampleBody, + globalUrl, + info, + setSelectedActionIndex, + selectedActionIndex, + HandleApiExecution, + ConfigurationTab, + }, + ) => + { + const [RequestHeader, setRequestHeader] = useState([{ key: "Content-Type", value: "application/json" }]); + const [RequestBody, setRequestBody] = useState(action?.body); + const editorRef = useRef(null); + const [AceEditorHeight, setAceEditorHeight] = useState(275) + const [baseUrl, setBaseUrl] = useState(serverurl) + const [path, setPath] = useState(action?.url) + const inputRef = useRef(null); + const [shouldChageInputFocus, setShouldChangeInputFocus] = useState(true); + const [disableExecuteButton, setDisableExecuteButton] = useState(false); + const [showResponseLoader, setShowResponseLoader] = useState(false); + const [appAuthentication, setAppAuthentication] = useState([]) + const parseHeaders = (headersString) => { + if (headersString?.length > 0) { + const headersArray = headersString.split("\n"); + const parsedHeaders = headersArray + .map((header) => { + const [key, value] = header.split("="); // Split by '=' to get key-value pairs + + // Only proceed if both key and value exist, and neither is undefined + if (key && value) { + return { key: key.trim(), value: value.trim() }; + } + return null; // Return null if the header is invalid + }) + .filter(Boolean); // Filter out any null values + + setRequestHeader(parsedHeaders); + } + }; + + useEffect(() => { + if (action?.headers) { + parseHeaders(action.headers); + } + }, []); + + const [RequestParams, setRequestParams] = useState([ + { + key: "", + value: "", + }, + ]); + + const [curTab, setCurTab] = useState(0) + const [actionUrl, setActionUrl] = useState(action?.url) + + const [selectedMethod, setSelectedMethod] = useState(action?.method) + + const fix_url = (newUrl) => { + if (newUrl.includes("hhttp")) { + newUrl = newUrl.replace("hhttp", "http"); + } + + if (newUrl.includes("http:/") && !newUrl.includes("http://")) { + newUrl = newUrl.replace("http:/", "http://"); + } + if (newUrl.includes("https:/") && !newUrl.includes("https://")) { + newUrl = newUrl.replace("https:/", "https://"); + } + if (newUrl.includes("http:///")) { + newUrl = newUrl.replace("http:///", "http://"); + } + if (newUrl.includes("https:///")) { + newUrl = newUrl.replace("https:///", "https://"); + } + if (!newUrl.includes("http://") && !newUrl.includes("https://")) { + newUrl = `http://${newUrl}`; + } + return newUrl; + }; + + function isValidMethod(method) { + const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]; + method = method.toUpperCase(); + + if (validMethods.includes(method)) { + return method; + } else { + throw new Error(`Invalid HTTP method: ${method}`); + } + } + + function fixHeader(headers) { + if (Array.isArray(headers)) { + return headers.reduce((acc, header) => { + if (header.key.trim() !== "" || header.value.trim() !== "") { + acc[header.key.trim()] = header.value.trim(); + } + return acc; + }, {}); + } + + const parsedHeaders = {}; + + if (typeof headers === 'string' && headers) { + const splitHeaders = headers.split("\n"); + + splitHeaders.forEach(header => { + let splitItem; + if (header.includes(":")) { + splitItem = ":"; + } else if (header.includes("=")) { + splitItem = "="; + } else { + return; + } + + const splitHeader = header.split(splitItem); + if (splitHeader.length >= 2) { + const key = splitHeader[0].trim(); + const value = splitHeader.slice(1).join(splitItem).trim(); + parsedHeaders[key] = value; + } + }); + } + + return parsedHeaders; + } + + function fixParams(queries) { + if (Array.isArray(queries)) { + return queries + .filter(query => query.key.trim() !== "" || query.value.trim() !== "") + .map(query => ({ key: query.key.trim(), value: query.value.trim() })); + } + + const parsedQueries = []; + if (typeof queries === 'string') { + if (!queries.trim()) return parsedQueries; + const cleanedQueries = queries.trim().replace(/\s+/g, " "); + const splittedQueries = cleanedQueries.split("&"); + splittedQueries.forEach(query => { + if (!query.includes("=")) { + console.info("Skipping as there is no '=' in the query"); + return; + } + const [key, value] = query.split("="); + if (!key.trim() || !value.trim()) { + console.info("Skipping because either key or value is not present in query"); + return; + } + parsedQueries.push({ key: key.trim(), value: value.trim() }); + }); + } + + return parsedQueries; + } + + async function prepareResponse(response) { + try { + const parsedHeaders = {}; + response.headers.forEach((value, key) => { + parsedHeaders[key] = value; + }); + + const cookies = {}; + if (response.headers.has("set-cookie")) { + const cookieHeader = response.headers.get("set-cookie").split(";"); + cookieHeader.forEach(cookie => { + const [key, value] = cookie.split("="); + if (key && value) { + cookies[key.trim()] = value.trim(); + } + }); + } + + const textData = await response.text(); + + let parsedBody; + try { + parsedBody = JSON.parse(textData); + } catch (error) { + console.error("Error parsing JSON response:", error); + parsedBody = textData; + } + + return { + success: true, + status: response.status, + url: response.url, + body: parsedBody, + headers: parsedHeaders, + cookies: cookies, + }; + } catch (error) { + console.error("Error preparing response:", error); + return { + success: false, + status: response?.status, + error: error.message, + }; + } + } + + const handleRequestWithCustomAction = async (selectedMethod, baseUrl, path, RequestHeader, RequestBody, RequestParams, info, action) => { + + if((HandleApiExecution !== undefined || HandleApiExecution !== null) && typeof HandleApiExecution === 'function'){ + + try { + if (baseUrl.length === 0) { + setBaseUrl(serverurl) + } + if (path.length === 0) { + setPath(action.url) + } + + const apiResponse = await HandleApiExecution( + selectedMethod, baseUrl, path, RequestHeader, RequestBody, RequestParams, info, action, setCurTab, + ) + + const response = { + "action name" : action.name.replaceAll("_", " "), + ...apiResponse + }; + + if (typeof response.result === "string") { + try { + response.result = JSON.parse(response.result); + } catch (parseError) { + console.error("Error parsing result:", parseError); + toast.error("Error parsing response result."); + } + } + + setDisableExecuteButton(false); + setApiResponse(response); // Set the response with the action name added + setShowResponseLoader(false); + + return apiResponse; + + } catch (error) { + console.error("Error during HandleApiExecution:", error); + toast.error(`Error: ${error.message}`); + return { error: error.message }; + } + } else{ + + const newUrl = fix_url(baseUrl); + let validMethod; + try { + validMethod = isValidMethod(selectedMethod); + } catch (error) { + console.error(error); + toast.error(error.message); + return { error: error.message }; + } + try { + + if (path && !path.startsWith('/')) { + path = '/' + path; + } + + const finalUrl = newUrl + path; + const newHeader = fixHeader(RequestHeader); + const newParams = fixParams(RequestParams); + + if (typeof RequestBody === 'object') { + try { + RequestBody = JSON.stringify(RequestBody); + } catch (error) { + console.error(`Error: ${error}`); + toast.error("Invalid JSON format for request body: ", error); + return { error: "Invalid JSON format for request body" }; + } + } + const queryString = new URLSearchParams(newParams.map(param => [param.key, param.value])).toString(); + const fullUrl = queryString ? `${finalUrl}?${queryString}` : finalUrl; + const response = await fetch(fullUrl, { + method: validMethod, + headers: newHeader, + body: validMethod !== 'GET' ? RequestBody : undefined, + }); + + const preparedResponse = await prepareResponse(response); + + setApiResponse(preparedResponse); + + return preparedResponse; + + } catch (error) { + console.error("Error:", error); + toast.error(`${error.message} Please ensure all fields are filled out correctly and try again.`); + return { error: error.message }; + } + } + }; + + const addRequestParamsRow = () => { + setRequestParams((prevRows) => { + const updatedRows = [...RequestParams, { key: "", value: "" }]; + return updatedRows; + }); + }; + + const handleRequestParamsChange = (rowIndex, field, value) => { + setRequestParams( + RequestParams.map((row, index) => { + return { + ...row, + [field]: index === rowIndex ? value : row[field], + }; + }) + ); + }; + + const addRow = () => { + setRequestHeader((prevRows) => { + const updatedRows = [...RequestHeader, { key: "", value: "" }]; + return updatedRows; + }); + }; + + const handleInputChange = (rowIndex, field, value) => { + setRequestHeader( + RequestHeader.map((row, i) => { + return { + ...row, + [field]: i === rowIndex ? value : row[field], + }; + }) + ); + }; + const handleChangeTab = (actionIndex, newValue) => { + setCurTab(newValue); + }; + + const shouldShowBodyTab = ![ + "GET", + "CONNECT", + "OPTIONS", + "TRACE", + "HEAD", + ].includes(selectedMethod); + + const extractParamsFromText = (text) => { + const params = []; + const queryString = text.split("?")[1]; + + if (queryString) { + const pairs = queryString.split("&"); + pairs.forEach((pair) => { + const [key, value] = pair.split("="); + if (key && value) { + params.push({ key, value }); + } + }); + } + + return params.length > 0 ? params : [{ key: "", value: "" }]; + }; + + const actionRef = useRef(null); + const scrollTimeoutRef = useRef(null); + const [isUserInteracting, setIsUserInteracting] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver( + throttle((entries) => { + if (!isUserInteracting) return; + let nextSelectedActionIndex = null; + + entries.forEach((entry) => { + if (entry.isIntersecting && selectedActionIndex !== index) { + nextSelectedActionIndex = index; + } + }); + + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + + if (nextSelectedActionIndex !== null) { + scrollTimeoutRef.current = setTimeout(() => { + if (selectedActionIndex !== nextSelectedActionIndex) { + setSelectedActionIndex(nextSelectedActionIndex); + const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-"); + window.history.pushState(null, "", `#${actionId}`); + setExampleBody(action?.example_response); + document.getElementById(`action-list-${nextSelectedActionIndex}`).scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, 300); + } + }, 200), + { threshold: 0.5 } + ); + + if (actionRef.current) { + observer.observe(actionRef.current); + } + + return () => { + if (actionRef.current) { + observer.unobserve(actionRef.current); + } + if (scrollTimeoutRef.current) { + clearTimeout(scrollTimeoutRef.current); + } + }; + }, [isUserInteracting]); + + const handleAceEditorChange = (value) => { + setRequestBody(value); + if (editorRef.current) { + const editor = editorRef.current.editor; + const lineHeight = editor.renderer.lineHeight; + const minHeight = 100; + const maxHeight = 300; + const session = editor.getSession(); + const screenLength = session.getScreenLength(); + const contentHeight = screenLength * lineHeight; + const padding = 20; + + // Calculate new height + let newHeight = Math.min( + Math.max( + minHeight, + contentHeight + padding + ), + maxHeight + ); + + if (newHeight !== AceEditorHeight) { + if (value.length < (editorRef.current._lastValue || '').length) { + if (contentHeight + padding < AceEditorHeight) { + setAceEditorHeight(newHeight); + } + } else { + if (contentHeight + padding > AceEditorHeight) { + setAceEditorHeight(newHeight); + } + } + } + editorRef.current._lastValue = value; + } + }; + + useEffect(() => { + if (editorRef.current) { + const editor = editorRef.current.editor; + editor.commands.addCommand({ + name: "executeOnCtrlEnter", + bindKey: { win: "Ctrl-Enter", mac: "Command-Enter" }, + exec: () => { + setShowResponseLoader(true); + setDisableExecuteButton(true); + handleRequestWithCustomAction( + selectedMethod, + baseUrl, + path, + RequestHeader, + RequestBody, + RequestParams, + info, + action + ); + }, + }); + } + }, [editorRef.current]); + + + return ( +
setIsUserInteracting(true)} + > +
+ + {action.name} + +
+ + { + if (e.key === "Enter") { + if (actionUrl.length === 0) { + toast.error("URL cannot be empty"); + return; + }else{ + setShowResponseLoader(true); + setDisableExecuteButton(true) + handleRequestWithCustomAction(selectedMethod, baseUrl, path, RequestHeader, RequestBody, RequestParams, info, action); + } + }} + } + onClick={(e) => { + const clickPosition = e.target.selectionStart; + const baseUrlLength = baseUrl.length; + if (shouldChageInputFocus) { + e.target.setSelectionRange(baseUrlLength + clickPosition, baseUrlLength + clickPosition); + setShouldChangeInputFocus(false); + } + }} + onFocus={(e) => { + const validParams = RequestParams.filter(param => param.key.trim().length > 0 && param.value.trim().length > 0); + const fullUrl = validParams.length > 0 ? `${baseUrl}${path}?${validParams.map(param => `${param.key}=${param.value}`).join("&")}` : `${baseUrl}${path}`; + if (fullUrl.length === 0) { + setActionUrl(serverurl + action?.url); + }else{ + setActionUrl(fullUrl); + } + if(!shouldChageInputFocus){ + setShouldChangeInputFocus(true); + } + }} + + onBlur={(e) => { + if(e.target.value.trim().length === 0) { + setActionUrl(path); + }else if(path.length === 0){ + setActionUrl(action?.url); + }else if(baseUrl.length === 0){ + setBaseUrl(serverurl) + setActionUrl(path) + }else{ + const validParams = RequestParams.filter(param => param.key.trim().length > 0 && param.value.trim().length > 0); + const validPath = validParams?.length > 0 ? `${path}?${validParams.map(param => `${param.key}=${param.value}`).join("&")}` : path; + setActionUrl(validPath) + } + setShouldChangeInputFocus(false); + }} + + onChange={(e) => { + const newUrl = e.target.value; + setActionUrl(newUrl); + const params = extractParamsFromText(newUrl); + setRequestParams(params); + + if (newUrl.startsWith("http://") || newUrl.startsWith("https://")) { + try { + const url = new URL(newUrl); + setBaseUrl(url.origin); + const newPath = decodeURIComponent(url.pathname);; + + setPath(newPath); + } catch (error) { + console.error("Invalid URL:", error); + } + } + }} + /> + + +
+ {showResponseLoader? ( + + ) : null} + +
+ + handleChangeTab(index, newValue) + } + aria-label="basic tabs example" + > + + Headers + + {...a11yProps(0)} + /> + {shouldShowBodyTab && ( + + Body + + {...a11yProps(1)} + /> + )} + + Params + + {...a11yProps(shouldShowBodyTab ? 2 : 1)} + /> + {ConfigurationTab ? ( + + Configuration + + {...a11yProps(shouldShowBodyTab ? 3 : 2)} + /> + ) : null} + +
+ + + + + + + Key + + + Value + + + + + {RequestHeader.map((row, rowIndex) => ( + + + + handleInputChange( + rowIndex, + "key", + e.target.value + ) + } + inputProps={{ + style: { + backgroundColor: "rgba(33, 33, 33, 1)", + padding: "4px 8px", + }, + }} + sx={{ + "& .MuiOutlinedInput-root": { + "& fieldset": { + border: "none", + }, + "&:hover fieldset": { + border: "none", + }, + "&.Mui-focused fieldset": { + border: "none", + }, + }, + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + document.getElementById( + `header-value-${index}-${rowIndex}` + ).focus(); + } + if (e.key === "Backspace" && row.key.length === 0 && rowIndex !== 0) { + setRequestHeader(RequestHeader.filter((header, i) => i !== rowIndex)); + document.getElementById( + `header-value-${index}-${rowIndex - 1}` + ).focus(); + e.preventDefault() + } + }} + /> + + + + handleInputChange( + rowIndex, + "value", + e.target.value + ) + } + inputProps={{ + endAdornment: ( + + + + ), + style: { + backgroundColor: "rgba(33, 33, 33, 1)", + padding: "4px 8px", + }, + }} + sx={{ + "& .MuiOutlinedInput-root": { + "& fieldset": { + border: "none", + }, + "&:hover fieldset": { + border: "none", + }, + "&.Mui-focused fieldset": { + border: "none", + }, + }, + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.ctrlKey) { + addRow(); + setTimeout(() => { + document.getElementById( + `header-key-${index}-${rowIndex + 1}` + ).focus(); + }, 0); + } + if (e.ctrlKey && e.key === "Enter") { + setShowResponseLoader(true); + setDisableExecuteButton(true) + handleRequestWithCustomAction(selectedMethod, baseUrl, path, RequestHeader, RequestBody, RequestParams, info, action); + } + if (e.key === "Backspace" && row.value.length === 0 && rowIndex !== 0) { + setRequestHeader(RequestHeader.filter((header, i) => i !== rowIndex)); + document.getElementById( + `header-value-${index}-${rowIndex - 1}` + ).focus(); + e.preventDefault() + }} + } + /> + + + ))} + +
+
+ +
+ {shouldShowBodyTab && ( + + + + )} + + + + + + + Key + + + Value + + + + + {RequestParams.map((row, rowIndex) => ( + + + + handleRequestParamsChange( + rowIndex, + "key", + e.target.value + ) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + document.getElementById( + `param-value-${index}-${rowIndex}` + ).focus(); + } + if (e.key === "Backspace" && row.value.length === 0 && rowIndex !== 0) { + setRequestParams(RequestParams.filter((param, i) => i !== rowIndex)); + document.getElementById( + `param-value-${index}-${rowIndex - 1}` + ).focus(); + } + }} + /> + + + + handleRequestParamsChange( + rowIndex, + "value", + e.target.value + ) + } + onKeyDown={(e) => { + if (e.key === "Enter" && !e.ctrlKey) { + addRequestParamsRow(); + setTimeout(() => { + document.getElementById( + `param-key-${index}-${rowIndex + 1}` + ).focus(); + }, 0); + } + if (e.ctrlKey && e.key === "Enter") { + setShowResponseLoader(true); + setDisableExecuteButton(true) + handleRequestWithCustomAction(selectedMethod, baseUrl, path, RequestHeader, RequestBody, RequestParams, info, action); + } + + if (e.key === "Backspace" && row.value.length === 0 && rowIndex !== 0) { + setRequestParams(RequestParams.filter((param, i) => i !== rowIndex)); + document.getElementById( + `param-value-${index}-${rowIndex - 1}` + ).focus(); + e.preventDefault() + } + } + } + /> + + + ))} + +
+
+ +
+ {(ConfigurationTab && ((shouldShowBodyTab && curTab === 3 ) || (!shouldShowBodyTab && curTab === 2)))? ( + + ) : null} +
+
+
+
+ + {action.name.replaceAll("_", " ")} + +

+ {action.description + ? action.description + : ""} +

+
+
+
+ ); +}) + +const ActionResponse = memo(({ apiResponse, ExampleBody, userdata }) => { + const [height, setHeight] = useState("14vh") + const [responseTabIndex, setResponseTabIndex] = useState(0) + const [oldResponse, setOldResponse] = useState(apiResponse) + const [highlight, setHighlight] = useState(false) + + const MIN_HEIGHT = 50 + + useEffect(() => { + var apiResp = apiResponse + var oldResp = oldResponse + try { + apiResp = JSON.stringify(apiResponse) + } catch (error) { + //console.error("Error parsing JSON response:", error); + } + + try { + oldResp = JSON.stringify(oldResponse) + } catch (error) { + //console.error("Error parsing JSON response:", error); + } + + if (apiResp === oldResp) { + return + } + + setOldResponse(apiResponse) + + //console.log("CHANGES MADE: ", apiResponse, oldResponse) + //toast("CHANGES!") + //console.log("HEIGHT: ", height) + + if (height === "14vh") { + setHeight("30vh") + } else { + // Check if height is less than 250px + var heightNum = 0 + try { + heightNum = parseInt(height.slice(0, -2)) + } catch (error) { + } + + if (heightNum < 350) { + setHeight("350px") + } + } + + setHighlight(true) + setTimeout(() => { + setHighlight(false) + }, 2000) + }, [apiResponse, ExampleBody]) + + const handleReactJsonClipboard = (copy) => { + const elementName = "copy_element_shuffle"; + let copyText = document.getElementById(elementName); + + if (copyText) { + if (copy.namespace && copy.name && copy.src) { + copy = copy.src; + } + + const clipboard = navigator.clipboard; + if (!clipboard) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + let stringified = JSON.stringify(copy); + if (stringified.startsWith('"') && stringified.endsWith('"')) { + stringified = stringified.slice(1, -1); + } + + navigator.clipboard.writeText(stringified); + toast("Copied value to clipboard, NOT json path."); + } else { + console.log("Failed to copy from " + elementName + ": ", copyText); + } + }; + + const stopResizing = () => { + window.removeEventListener("mousemove", startResizing); + window.removeEventListener("mouseup", stopResizing); + }; + + const initResize = (e) => { + e.preventDefault(); + window.addEventListener("mousemove", startResizing); + window.addEventListener("mouseup", stopResizing); + }; + + const startResizing = useCallback((e) => { + const newHeight = window.innerHeight - e.clientY; + if (newHeight >= MIN_HEIGHT) { + setHeight(`${newHeight}px`); + } + }, []); + + const formData = (exampleBody) => { + try { + return exampleBody ? JSON.parse(exampleBody) : {}; + } catch (error) { + console.error("Error parsing the example string:", error); + return {}; + } + }; + + useEffect(() => { + const handleResize = () => { + const newHeight = window.innerHeight * 0.1; + setHeight(`${newHeight}px`); + }; + + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + return ( + +
+ {highlight === true ? + + : null + } +
+ setResponseTabIndex(newValue)} + > + Response} + {...a11yProps(0)} + /> + Example Response} + disabled={ExampleBody === undefined || ExampleBody === null || ExampleBody === ""} + {...a11yProps(1)} + /> + History} + disabled={true} + {...a11yProps(2)} + /> + +
+
+ + + + + + + + +
+
+
+ ); +}); + +const ResponseTabWrapper = memo(({ apiResponse }) => { + const handleReactJsonClipboard = (copy) => { + const elementName = "copy_element_shuffle"; + let copyText = document.getElementById(elementName); + + if (copyText) { + if (copy.namespace && copy.name && copy.src) { + copy = copy.src; + } + + const clipboard = navigator.clipboard; + if (!clipboard) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + let stringified = JSON.stringify(copy); + if (stringified.startsWith('"') && stringified.endsWith('"')) { + stringified = stringified.slice(1, -1); + } + + navigator.clipboard.writeText(stringified); + toast("Copied value to clipboard, NOT json path."); + } else { + console.log("Failed to copy from " + elementName + ": ", copyText); + } + }; + + return( + { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + enableClipboard={handleReactJsonClipboard} + displayDataTypes={false} + name={false} + /> + )}) + +const PaddingWrapper = memo(({ userdata, children }) => { + const { leftSideBarOpenByClick, windowWidth } = useContext(Context); + return ( +
= 1920 ? "calc(100% - 630px)" : "calc(100% - 570px)" + : windowWidth >= 1920 ? "calc(100vw - 460px)": "calc(100% - 410px)" + : windowWidth >= 1920 ? "calc(100% - 370px)" : "calc(100% - 320px)", + backgroundColor: "#1a1a1a", + position: "fixed", + bottom: 0, + right: 0, + display: "flex", + flexDirection: "column", + borderTop: "1px solid #212121", + transition: "width 0.3s ease", + minHeight: "10%", + }} + > + {children} +
+ ); +}); + +const ApiResponseWrapper = memo(({ children, userdata }) => { + return ( + + {children} + + ); +}); diff --git a/frontend/src/components/ExecutionPanel.jsx b/frontend/src/components/ExecutionPanel.jsx new file mode 100644 index 00000000..e3712bdf --- /dev/null +++ b/frontend/src/components/ExecutionPanel.jsx @@ -0,0 +1,546 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Box, Typography, IconButton, CircularProgress, Tooltip } from '@mui/material'; +import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as PauseIcon } from '@mui/icons-material'; +import theme from '../theme.jsx'; +import ReactJson from "react-json-view-ssr"; +import { toast } from 'react-toastify'; +import { validateJson } from "../views/Workflows.jsx"; +// import HandleJsonCopy from "./ShuffleCodeEditor1"; + +const STATUS_CONFIG = { + EXECUTING: { + color: '#64B5F6', + icon: () => , + label: 'Executing' + }, + SUCCESS: { + color: '#4CAF50', + icon: () => , + label: 'Success' + }, + FINISHED: { + color: '#4CAF50', + icon: () => , + label: 'Finished' + }, + ABORTED: { + color: '#F44336', + icon: () => , + label: 'Aborted' + } +}; + +let to_be_copied = "" + +const handleReactJsonClipboard = (copy) => { + toast("Copied JSON path to clipboard, NOT Path") +}; + + +const HandleJsonCopy = (base, copy, base_node_name) => { + if (typeof copy.name === "string") { + copy.name = copy.name.replaceAll(" ", "_"); + } + + //lol + if (typeof base === 'object' || typeof base === 'dict') { + base = JSON.stringify(base) + } + + if (base_node_name === "execution_argument" || base_node_name === "Execution Argument") { + base_node_name = "exec" + } + + console.log("COPY: ", base_node_name, copy); + + //var newitem = JSON.parse(base); + var newitem = validateJson(base).result + to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_"); + for (let copykey in copy.namespace) { + if (copy.namespace[copykey].includes("Results for")) { + continue; + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.namespace[copykey]]; + if (!isNaN(copy.namespace[copykey])) { + to_be_copied += ".#"; + } else { + to_be_copied += "." + copy.namespace[copykey]; + } + } + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.name]; + if (!isNaN(copy.name)) { + to_be_copied += ".#"; + } else { + to_be_copied += "." + copy.name; + } + } + + to_be_copied.replaceAll(" ", "_"); + const elementName = "copy_element_shuffle"; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(to_be_copied); + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices * + + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied JSON path to clipboard.") + console.log("COPYING!"); + } else { + console.log("Couldn't find element ", elementName); + } +} + +const ExecuteWorkflow = async (executionData, globalUrl) => { + try { + const workflowData = executionData.workflow; + + // Execute workflow with original parameters + await fetch(`${globalUrl}/api/v1/workflows/${workflowData.id}/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + credentials: 'include', + body: workflowData + }).then(response => { + window.location.href = `/workflows/${workflowData.id}/code?execution_id=` + response.json().execution_id; + }); + + } catch (error) { + console.error('Error re-executing workflow:', error); + } +}; + +const ExecutionsList = ({ executions, onSelectExecution, activeExecutionId }) => { + return ( + + {executions.map((execution) => { + const status = STATUS_CONFIG[execution.status] || STATUS_CONFIG.ABORTED; + return ( + onSelectExecution(execution)} + sx={{ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + py: 1, + px: 2, + borderBottom: '1px solid #2A2A2A', + backgroundColor: activeExecutionId === execution.execution_id ? + 'rgba(255,255,255,0.05)' : 'transparent', + '&:hover': { + backgroundColor: 'rgba(255,255,255,0.05)' + } + }} + > + {status.icon()} + + + + {new Date(execution.started_at * 1000).toLocaleString()} + + + {status.label} + + + + ); + })} + + ); +}; + +const ExecutionDetail = ({ execution: initialExecution, onBack, globalUrl, onExecutionUpdate, selectedAction, executeWorkflow }) => { + const [execution, setExecution] = useState(initialExecution); + const [status, setStatus] = useState(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING); + const [validResult, setValidResult] = useState("{}") + + const abortExecution = async () => { + try { + await fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/executions/${execution.execution_id}/abort`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.ok) { + const updatedExecution = { + ...execution, + status: "ABORTED", + }; + setExecution(updatedExecution); + onExecutionUpdate(updatedExecution); + } + }); + + } catch (error) { + console.log("Abort error:", error); + } + }; + + useEffect(() => { + setStatus(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING); + }, [execution]); + + const pollExecutionStatus = useCallback(async () => { + try { + const response = await fetch(`${globalUrl}/api/v1/streams/results`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + execution_id: execution.execution_id, + authorization: execution.authorization, + }), + }); + + if (response.ok) { + const data = await response.json(); + const currentStatus = data.results?.[0]?.status || 'EXECUTING'; + + const updatedExecution = { + ...execution, + ...data, + status: currentStatus + }; + + setExecution(updatedExecution); + onExecutionUpdate(updatedExecution); + + } + } catch (error) { + console.error('Polling error:', error); + } + }, [execution, globalUrl, onExecutionUpdate]); + + useEffect(() => { + let pollTimeout; + if (execution.status === 'EXECUTING') { + pollTimeout = setTimeout(() => pollExecutionStatus(), 3000); + } + + if (execution?.results?.length === 1) { + setValidResult(JSON.parse(execution?.results[0]?.result || "{}")) + } + + return () => clearTimeout(pollTimeout); + }, [execution.status, pollExecutionStatus]); + + return ( + + + + + + + Execution Details + + {status.icon()} + + {status.label} + + + + {execution.status === "EXECUTING" && ( + + + + + + )} + + + { + ExecuteWorkflow( + execution, + globalUrl + ); + }} + sx={{ color: theme.palette.primary.main }} + > + + + + + + + + + + Started at + + + {new Date(execution.started_at * 1000).toLocaleString()} + + + + + + Execution ID + + + {execution.execution_id} + + + + + + + Result + + +
+                {execution?.status === 'EXECUTING' ? (
+                  
+                    
+                    Executing...
+                  
+                ) : (
+                  execution?.results?.length === 1 ?
+                     {
+                        handleReactJsonClipboard(copy);
+                      }}
+                      collapsed={false}
+                      displayDataTypes={false}
+                      onSelect={(select) => {
+                        var basename = "exec"
+                        if (selectedAction !== undefined && selectedAction !== null && Object.keys(selectedAction).length !== 0) {
+                          basename = selectedAction.label.toLowerCase().replaceAll(" ", "_")
+                        }
+                        HandleJsonCopy(validResult, select, basename)
+                      }}
+                      name={"JSON autocompletion"}
+                    /> :
+                     { }}
+                      displayDataTypes={false}
+                      name={"JSON autocompletion"}
+                    />
+                )}
+              
+
+
+
+
+
+ ); +}; +const ExecutionPanel = ({ + workflow, + globalUrl, + onClose, + currentExecution, + mainAction +}) => { + const [executions, setExecutions] = useState([]); + const [selectedExecution, setSelectedExecution] = useState(null); + const [loading, setLoading] = useState(true); + + const handleExecutionUpdate = useCallback((updatedExecution) => { + setExecutions(prevExecutions => { + const updatedExecutions = [...prevExecutions]; + const index = updatedExecutions.findIndex( + e => e.execution_id === updatedExecution.execution_id + ); + if (index !== -1) { + updatedExecutions[index] = updatedExecution; + } + return updatedExecutions; + }); + }, []); + + const fetchExecutions = useCallback(async () => { + setLoading(true); + try { + const response = await fetch(`${globalUrl}/api/v2/workflows/${workflow.id}/executions`, { + credentials: 'include', + }); + if (response.ok) { + const data = await response.json(); + setExecutions(data.executions); + + const urlParams = new URLSearchParams(window.location.search); + const executionId = urlParams.get('execution_id'); + if (executionId) { + const execution = data.executions.find(e => e.execution_id === executionId); + if (execution) { + setSelectedExecution(execution); + } + } + } + } catch (error) { + console.error('Failed to fetch executions:', error); + } finally { + setLoading(false); + } + }, [workflow.id, globalUrl]); + + useEffect(() => { + fetchExecutions(); + }, [fetchExecutions]); + + useEffect(() => { + if (currentExecution?.execution_id) { + setExecutions(prev => { + const existingIndex = prev.findIndex(e => e.execution_id === currentExecution.execution_id); + if (existingIndex === -1) { + return [currentExecution, ...prev]; + } + const updated = [...prev]; + updated[existingIndex] = currentExecution; + return updated; + }); + setSelectedExecution(currentExecution); + } + }, [currentExecution]); + + return ( + + {loading && !executions.length ? ( + + + + ) : selectedExecution ? ( + { + setSelectedExecution(null); + const url = new URL(window.location); + url.searchParams.delete('execution_id'); + window.history.pushState({}, '', url); + fetchExecutions(); + }} + globalUrl={globalUrl} + selecteAction={mainAction} + onExecutionUpdate={handleExecutionUpdate} + /> + ) : ( + <> + + + Execution History + + + + + + + { + setSelectedExecution(execution); + const url = new URL(window.location); + url.searchParams.set('execution_id', execution.execution_id); + window.history.pushState({}, '', url); + }} + activeExecutionId={currentExecution?.execution_id} + /> + + + )} + + ); +}; + + +export default ExecutionPanel; diff --git a/frontend/src/components/MFASetUP.jsx b/frontend/src/components/MFASetUP.jsx new file mode 100644 index 00000000..270a81a9 --- /dev/null +++ b/frontend/src/components/MFASetUP.jsx @@ -0,0 +1,197 @@ +import React, { useEffect, useState } from "react"; +import { Paper, Typography, Box, CircularProgress, TextField, Button } from "@mui/material"; +import { toast } from "react-toastify"; + +const MFASetup = ({ isLoaded, globalUrl, setCookie }) => { + const [image2FA, setImage2FA] = useState(""); + const [secret2FA, setSecret2FA] = useState(""); + const [mfaCode, setMfaCode] = useState(""); + const [code, setCode] = useState(null); + + useEffect(() => { + handleGet2FACode(); + }, []); + + useEffect(() => { + if (isLoaded) { + const code = window.location.pathname.split("/")[2]; + setMfaCode(code); + } + }, [isLoaded]); + + const handleGet2FACode = () => { + if (mfaCode === "") { + return; + } + + fetch(`${globalUrl}/api/v1/users/${mfaCode}/get2fa`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 404) { + toast("User not found. Redirecting to login page in 3 seconds..."); + setTimeout(() => { + window.location.pathname = "/login"; + return; + }, 3000); + } + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setImage2FA(responseJson.reason); + setSecret2FA(responseJson.extra); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + useEffect(() => { + if (mfaCode) { + handleGet2FACode(); + } + }, [mfaCode]); + + const handleVerify2FA = (mfaCode, code, changeMFAActive) => { + const data = { + code: code, + changeMFAActive: changeMFAActive, + }; + + toast("Verifying 2fa code. Please wait..."); + + fetch(`${globalUrl}/api/v1/users/${mfaCode}/set2fa`, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 500) { + toast("Wrong code sent. Please try again."); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + toast.success("Successfully setup 2fa. Redirecting in 3 seconds..."); + for (var key in responseJson["cookies"]) { + setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" }); + } + + const tmpView = new URLSearchParams(window.location.search).get("view"); + if (tmpView !== undefined && tmpView !== null) { + var newUrl = `/${tmpView}`; + if (tmpView.startsWith("/")) { + newUrl = `${tmpView}`; + } + window.location.pathname = newUrl; + return; + } + + if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) { + const welcome = responseJson.tutorials.find((element) => element.name === "welcome"); + if (welcome === undefined || welcome === null) { + setTimeout(() => { + window.location.pathname = "/welcome"; + }, 3000); + } + } + + setTimeout(() => { + window.location.pathname = "/workflows"; + }, 3000); + } else { + toast("Failed to setup 2fa. Please try again."); + } + }) + .catch((error) => { + console.error("Error:", error); + }); + }; + + return ( +
+ + + Multi-Factor Authentication Setup + +
+ +
+ + Enter the code from your authenticator app below. + + setCode(e.target.value)} + onKeyPress={(e) => { + if (e.key === "Enter" && code !== null && code !== "" && code.length === 6) { + handleVerify2FA(mfaCode, code, true); + } + }} + /> + + +
+
+ ); +}; + +const QRCodeSection = ({ secret2FA, image2FA }) => { + return ( +
+ {secret2FA && image2FA ? ( +
+ + Scan the image below with the two-factor authentication app on your phone. If you can’t use a QR code, use the code {secret2FA} instead. + + 2FA QR code +
+ ) : ( + + )} +
+ ); +}; + +export default MFASetup; diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx new file mode 100644 index 00000000..0b9143c4 --- /dev/null +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -0,0 +1,1787 @@ +import React, { memo, useCallback } from "react"; +import { useState, useEffect, useContext, Suspense } from "react"; +import { useNavigate, Link, useLocation } from "react-router-dom"; +import { toast } from "react-toastify"; +import { Context } from "../context/ContextApi.jsx"; +import { + Box, + IconButton, + MenuItem, + Select, + Skeleton, + Stack, + Collapse, + ListItem, + Typography, + Tab, + Button, + Dialog, + Tooltip, + DialogContent, + DialogTitle, + DialogActions, + TextField, + Divider, +} from "@mui/material"; + +import { validateJson, } from "../views/Workflows.jsx"; +import { isMobile } from "react-device-detect" +import theme from "../theme.jsx"; +import PaperComponent from "../components/PaperComponent.jsx"; +import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx' +import { v4 as uuidv4} from "uuid"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + DragIndicator as DragIndicatorIcon, + Close as CloseIcon, + Edit as EditIcon, + LockOpen as LockOpenIcon, + Delete as DeleteIcon, + CheckCircle as CheckCircleIcon, +} from "@mui/icons-material"; + +import Markdown from "react-markdown"; +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import algoliasearch from "algoliasearch/lite"; +import { green } from "../views/AngularWorkflow.jsx" + +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +) + +// Lazy loading of ApiExplorer component to reduce initial load time +const ApiExplorer = React.lazy(() => import("../components/ApiExplorer.jsx")); + + +const ApiExplorerWrapper = (props) => { + const { globalUrl, serverside, userdata, isLoggedIn} = props; + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" + const location = useLocation(); + const navigate = useNavigate(); + const [openapi, setOpenapi] = useState({}); + const [selectedAppData, setSelectedAppData] = useState({}) + const [selectedAuthentication, setSelectedAuthentication] = useState({}); + const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false) + const [authenticationType, setAuthenticationType] = React.useState(""); + const [appAuthentication, setAppAuthentication] = useState([]); + const [selectedMeta, setSelectedMeta] = useState(undefined); + const [selectedAction, setSelectedAction] = useState( + { + "app_name": selectedAppData.name, + "app_id": selectedAppData.id, + "app_version": selectedAppData.version, + "large_image": selectedAppData.large_image, + } + ) + const [authHighlighted, setAuthHighlighted] = useState(false) + const [locations, setLocations] = React.useState([]) + const [selectedLocation, setSelectedLocation] = React.useState("") + + const appid = location.pathname.split("/")[2]; + const base64_decode = (str) => { + return decodeURIComponent( + atob(str) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); + }; + + useEffect(() => { + if (selectedAppData !== undefined && selectedAppData !== null && Object.getOwnPropertyNames(selectedAppData).length > 0) { + HandleAppAuthentication(selectedAppData?.name) + } + }, [selectedAppData, openapi]) + + useEffect(() => { + if (appid !== undefined && appid !== null && appid.length !== 0) { + getAppData(appid) + HandleGetLocations() + } + + if (appAuthentication.length === 0 || selectedAuthentication.length === 0) { + HandleAppAuthentication() + } + }, [appid]); + + function Heading(props) { + const element = React.createElement( + `h${props.level}`, + { style: { marginTop: 40 } }, + props.children + ); + return ( + + {props.level !== 1 ? ( + + ) : null} + {element} + + ); + } + + const runAlgoliaAppSearch = (appname) => { + const index = searchClient.initIndex("appsearch"); + + console.log("Running appsearch for: ", appname); + + index + .search(appname) + .then(({ hits }) => { + + if (hits !== undefined && hits !== null && hits.length > 0) { + const appsearchname = appname.replaceAll("_", " ").toLowerCase() + var found = false + for (var key in hits) { + const hit = hits[key] + const newname = hit.name.replaceAll("_", " ").toLowerCase() + + if (newname?.includes(appsearchname)) { + found = true + getAppData(hit.objectID) + break + } + } + + if (!found) { + toast.error("Failed to get app data or App doesn't exist (1). Redirecting.."); + setTimeout(()=>{ + navigate("/search?tab=apps"); + },3000) + } + } else { + toast.error("Failed to get app data or App doesn't exist (2). Redirecting.."); + setTimeout(()=>{ + navigate("/search?tab=apps"); + },3000) + } + }) + .catch((err) => { + console.log(err); + }); + } + + // Fetch data when appid is available + const getAppData = useCallback((appid) => { + if (appid === undefined || appid === null || appid.length === 0) { + return + } + + if (appid.length !== 32) { + runAlgoliaAppSearch(appid) + return + } + + const url = `${globalUrl}/api/v1/apps/${appid}/config` + + fetch(url, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + toast.error("Failed to get app data or App doesn't exist (3). Redirecting.."); + setTimeout(()=>{ + navigate("/search?tab=apps"); + },3000) + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + handleDecodeOfOpenApiData(responseJson); + } else { + toast.error("Failed to get app data or App doesn't exist (4)"); + } + }) + .catch((error) => { + console.error("error for app is :", error); + }); + },[appid]); + + const handleDecodeOfOpenApiData = (data) => { + var appexists = false; + var parsedapp = {}; + + if (data.app !== undefined && data.app !== null) { + var parsedBaseapp = ""; + try { + parsedBaseapp = base64_decode(data.app); + } catch (e) { + parsedBaseapp = data; + } + + parsedapp = JSON.parse(parsedBaseapp); + parsedapp.name = parsedapp.name.replaceAll("_", " "); + + appexists = + parsedapp.name !== undefined && + parsedapp.name !== null && + parsedapp.name.length !== 0; + if(parsedapp?.id.length > 0){ + setSelectedAppData(parsedapp) + handleAppAuthenticationType(parsedapp) + const apptype = selectedAppData?.generated === false ? "python" : "openapi" + getAppDocs(parsedapp.name, apptype, parsedapp.version); + } + } + + if (data.openapi === undefined || data.openapi === null) { + return; + } + + var parsedDecoded = ""; + try { + parsedDecoded = base64_decode(data.openapi); + } catch (e) { + parsedDecoded = data; + } + + parsedapp = JSON.parse(parsedDecoded); + data = + parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body); + + setOpenapi(data); + }; + + const handleAppAuthenticationType = (selectedAppData) => { + + if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) { + setAuthenticationType({ + type: "", + }) + + selectedAppData.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? { + type: selectedAppData.authentication.type, + redirect_uri: selectedAppData.authentication.redirect_uri, + refresh_uri: selectedAppData.authentication.refresh_uri, + token_uri: selectedAppData.authentication.token_uri, + scope: selectedAppData.authentication.scope, + client_id: selectedAppData.authentication.client_id, + client_secret: selectedAppData.authentication.client_secret, + grant_type: selectedAppData.authentication.grant_type, + } : { + type: "", + } + ) + } + } + + const fix_url = (newUrl) => { + if (newUrl.includes("hhttp")) { + newUrl = newUrl.replace("hhttp", "http"); + } + + if (newUrl.includes("http:/") && !newUrl.includes("http://")) { + newUrl = newUrl.replace("http:/", "http://"); + } + if (newUrl.includes("https:/") && !newUrl.includes("https://")) { + newUrl = newUrl.replace("https:/", "https://"); + } + if (newUrl.includes("http:///")) { + newUrl = newUrl.replace("http:///", "http://"); + } + if (newUrl.includes("https:///")) { + newUrl = newUrl.replace("https:///", "https://"); + } + if (!newUrl.includes("http://") && !newUrl.includes("https://")) { + newUrl = `http://${newUrl}`; + } + return newUrl; + }; + + function isValidMethod(method) { + const validMethods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]; + method = method.toUpperCase(); + + if (validMethods.includes(method)) { + return method; + } else { + throw new Error(`Invalid HTTP method: ${method}`); + } + } + function fixHeader(headers) { + if (Array.isArray(headers)) { + return headers.reduce((acc, header) => { + if (header.key.trim() !== "" || header.value.trim() !== "") { + acc[header.key.trim()] = header.value.trim(); + } + return acc; + }, {}); + } + + const parsedHeaders = {}; + + if (typeof headers === 'string' && headers) { + const splitHeaders = headers.split("\n"); + + splitHeaders.forEach(header => { + let splitItem; + if (header.includes(":")) { + splitItem = ":"; + } else if (header.includes("=")) { + splitItem = "="; + } else { + return; + } + + const splitHeader = header.split(splitItem); + if (splitHeader.length >= 2) { + const key = splitHeader[0].trim(); + const value = splitHeader.slice(1).join(splitItem).trim(); + parsedHeaders[key] = value; + } + }); + } + + return parsedHeaders; + } + + function fixParams(queries) { + if (Array.isArray(queries)) { + return queries + .filter(query => query.key.trim() !== "" || query.value.trim() !== "") + .map(query => ({ key: query.key.trim(), value: query.value.trim() })); + } + + const parsedQueries = []; + if (typeof queries === 'string') { + if (!queries.trim()) return parsedQueries; + const cleanedQueries = queries.trim().replace(/\s+/g, " "); + const splittedQueries = cleanedQueries.split("&"); + splittedQueries.forEach(query => { + if (!query.includes("=")) { + console.info("Skipping as there is no '=' in the query"); + return; + } + const [key, value] = query.split("="); + if (!key.trim() || !value.trim()) { + console.info("Skipping because either key or value is not present in query"); + return; + } + parsedQueries.push({ key: key.trim(), value: value.trim() }); + }); + } + + return parsedQueries; + } + + const UpdateAppAuthentication = useCallback((data, appname) => { + if (data === undefined || data === null) { + return + } + + if (appname !== undefined && appname !== null && appname.length > 0) { + selectedAppData.name = appname + } + + const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid || appAuth?.app?.name?.replaceAll(" ", "_").toLowerCase() === selectedAppData?.name?.replaceAll(" ", "_").toLowerCase()); + if (filteredData.length === 0) { + setAppAuthentication([]) + setSelectedAuthentication({}) + } else { + setAppAuthentication(filteredData) + setSelectedAuthentication(filteredData[0]) + } + }, [appid]) + + const HandleGetLocations = () => { + const url = `${globalUrl}/api/v1/environments`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + return + } + + return response.json() + }).then((responseJson) => { + if (responseJson.success !== false) { + setLocations(responseJson) + } + }).catch((error) => { + console.error("Error loading locations:", error); + }) + } + + const HandleAppAuthentication = useCallback((appname) =>{ + + const url = `${globalUrl}/api/v1/apps/authentication`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson.success === true) { + UpdateAppAuthentication(responseJson.data, appname) + } else { + toast.error("Failed to get app authentication data"); + } + }).catch((error) => { + console.error("error for app is :", error); + }); + }) + + const HandleApiExecution = useCallback(async (selectedMethod, url, path, RequestHeader, RequestBody, RequestParams, info, action, setCurTab, executionLocation) => { + + let validMethod; + try { + validMethod = isValidMethod(selectedMethod); + } catch (error) { + console.error(error); + toast.error(error.message); + return { error: error.message }; + } + + const headers = {}; + RequestHeader.forEach((header) => { + if (header.key.length > 0 && header.value.length > 0) { + headers[header.key] = header.value; + } + }); + + const formatArrayToString = (array) => { + return array + .map(item => (item.key.trim().length > 0 && item.value.trim().length > 0 ? `${item.key}=${item.value}` : "")) + .filter(str => str.length > 0) + .join("\n"); + }; + + var appid = ""; + + if (selectedAppData?.id?.length > 0) { + appid = selectedAppData?.id; + }else if (openapi?.id?.length > 0) { + appid = openapi?.id; + }else{ + toast.error("App id is missing. Please try again."); + return; + } + + const fullUrl = `${globalUrl}/api/v1/apps/${appid}/run`; + + var actionData = { + name: "custom_action", + app_name: info?.title, + app_version: info?.version, + app_id: appid, + authentication_id: selectedAuthentication?.id?.length > 0 ? selectedAuthentication?.id : "", + auth_not_required: false, + environment: isCloud ? "cloud" : "Shuffle", + node_type: "action", + parameters: [{ name: "url", value: fix_url(url)}], + } + + if (selectedLocation?.length > 0 && selectedLocation?.toLowerCase() !== "default") { + actionData.environment = selectedLocation + + // Find the env + for (var envkey in locations) { + const env = locations[envkey] + if (env.Name !== selectedLocation) { + continue + } + + if (env.Type === "cloud" || (env.running_ip !== undefined && env.running_ip !== null && env.running_ip.length > 0)) { + } else { + toast.warn(`Location ${env.Name} is not running and may not work as expected`) + } + break + } + } + + + const body = RequestBody; + const header = formatArrayToString(RequestHeader); + const param = fixParams(RequestParams); + + var hasBody = false + if (body.length > 0 && body !== "{}" && validMethod !== "GET" && validMethod !== "HEAD" && validMethod !== "OPTIONS" && validMethod !== "CONNECT" && validMethod !== "TRACE") { + hasBody = true + actionData.parameters.push({ + name: "body", + value: body, + }); + } + + if (header.length > 0) { + actionData.parameters.push({ + name: "headers", + value: header, + }); + } + + if (param.length > 0) { + const paramsString = new URLSearchParams(param.map(param => [param.key, param.value])).toString(); + actionData.parameters.push({ + name: "queries", + value: paramsString, + }); + } + if ( validMethod.length > 0) { + actionData.parameters.push({ + name: "method", + value: validMethod, + }); + } + + if (path.length > 0) { + actionData.parameters.push({ + name: "path", + value: path, + }); + } + + const options = { + method: "POST", + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(actionData), + credentials: 'include', + }; + + try { + const response = await fetch(fullUrl, options) + const data = await response.json() + + if (data.success === false) { + if (data.reason !== undefined && data.reason !== null && data.reason.length > 0) { + + if (data.reason.includes("authenticate")) { + toast.error("Authenticate the app first or add authentication headers"); + + setAuthHighlighted(true) + if (setCurTab !== undefined) { + if (hasBody) { + setCurTab(3) + } else { + setCurTab(2) + } + } + } + } + } else { + if (data.result !== undefined && data.result !== null && data.result.length > 0) { + const validate = validateJson(data.result) + if (validate.valid === true) { + if (validate.result.status === 401 || validate.result.status === 403) { + setAuthHighlighted(true) + + toast.info("You need to authenticate the app first, either with an API-key directly in the headers or with the Shuffle auth system") + + if (setCurTab !== undefined) { + if (hasBody) { + setCurTab(3) + } else { + setCurTab(2) + } + } + } else if (validate.result.status === 404) { + toast.error("Page not found. Please try a different URL.") + } else if (validate.result.error !== undefined && validate.result.error !== null && validate.result.error.length > 0) { + if (validate.result.error.toLowerCase().includes("max retries")) { + toast.error("Are you sure the URL is correct? It seems like the server is not responding.") + } + } + } + + if (data.result.includes("custom_action doesn't exist")) { + // No timeout error + toast.info("This API is being rebuilt due to missing functionality. Please wait a minute or two, then try again. If this persists, please report to support@shuffler.io", { + "autoClose": 90000, + }) + } else if (data.result.includes("authentication") && data.result.includes("Oauth2")) { + toast.error("Oauth2 apps require authentication") + + setAuthHighlighted(true) + if (setCurTab !== undefined) { + if (hasBody) { + setCurTab(3) + } else { + setCurTab(2) + } + } + } + + if (validate.valid === true) { + return validate.result + } + } + } + + return data + + } catch (error) { + console.error("Error during API execution:", error); + toast.error(`${error.message} Please ensure all fields are filled out correctly and try again.`); + return { error: error.message }; + } + + },[selectedAuthentication, selectedAppData, openapi]); + + + const AuthenticationList = () => { + const [openId, setOpenId] = useState(null); + const name = selectedAuthentication?.app?.name?.length > 0 ? selectedAuthentication?.label : "No Selection"; + const [authenticationName, setAuthenticationName] = useState(name) + + const toggleScope = (id, event) => { + event.stopPropagation(); + setOpenId((prevOpenId) => (prevOpenId === id ? null : id)); + }; + + return ( + + ); + }; + + const skeletonLoader = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + + const AuthenticationData = (props) => { + const selectedApp = props.app; + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + // workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if ( + selectedApp.authentication === undefined || + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return ( + + + {selectedApp.name} does not require authentication + + + ); + } + + authenticationOption.app.actions = []; + + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = ""; + } + } + + const setNewAppAuth = (appAuthData, refresh) => { + setSelectedAuthentication(appAuthData); + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + headers["Org-Id"] = userdata?.active_org?.id + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: headers, + body: JSON.stringify(appAuthData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + + if (response.status === 400) { + toast.error("Failed setting new auth. Please try again", { + "autoClose": true, + }) + } + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast.error("Error: " + responseJson.reason, { + "autoClose": false, + }) + + } else { + HandleAppAuthentication() + setAuthenticationModalOpen(false) + } + }) + .catch((error) => { + console.log("New auth error: ", error.toString()); + }); + }; + + const handleSubmitCheck = () => { + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + } + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[paramkey].value !== undefined && + selectedApp.authentication.parameters[paramkey].value !== null && + selectedApp.authentication.parameters[paramkey].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = selectedApp.authentication.parameters[paramkey].value; + } else { + if ( + selectedApp.authentication.parameters[paramkey].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = "false"; + } else { + toast( + "Field " + + selectedApp.authentication.parameters[paramkey].name + + " can't be empty" + ); + return; + } + } + } + } + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (let authkey in newAuthOption.fields) { + const value = newAuthOption.fields[authkey]; + newFields.push({ + "key": authkey, + "value": value, + }); + } + + newAuthOption.fields = newFields + setNewAppAuth(newAuthOption) + } + + if (authenticationOption.label === null || authenticationOption.label === undefined) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
+ +
+ Authentication for {selectedApp.name.replaceAll("_", " ", -1)} +
+
+ + + What is app authentication? + +
+ These are required fields for authenticating with {selectedApp.name} +
+ Label for you to remember + { + authenticationOption.label = event.target.value; + }} + /> + +
+ {selectedApp.authentication.parameters.map((data, index) => { + if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { + } + + + return ( +
+ + {data.name} + + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> + )} +
+ ); + })} + + + + + +
+ ); + }; + + const getAppDocs = (appname, location, version) => { + fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + //toast("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) { + setSelectedMeta(responseJson.meta) + } + + if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { + if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { + // Translate into markdown ![]() + const imgRegex = / ({ + ...prevState, + documentation: newdata, + })); + } + } + } + + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const authenticationModal = authenticationModalOpen ? ( + {setSelectedMeta(undefined)}} + PaperProps={{ + style: { + pointerEvents: "auto", + color: "white", + minWidth: 1100, + minHeight: 700, + maxHeight: 700, + padding: 15, + overflow: "hidden", + zIndex: 10012, + border: theme.palette.defaultBorder, + }, + }} + > +
+ { selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + {`Documentation + + ) : ( + + {`Documentation + + )} +
+ + + + + + { + setAuthenticationModalOpen(false); + }} + > + + +
+
+ {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? + + : + + } +
+
+ {selectedAppData.documentation === undefined || + selectedAppData.documentation === null || + selectedAppData.documentation.length === 0 ? ( + +
+ + {selectedAppData?.description} + +
+ + +
+ + There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! + + +
+ + + Want to help the making of, or improve this app?{" "} +
+ + Join the community on Discord! + +
+ + + Want to help change this app directly? + + {selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + + Check it out on Github! + + + + ) : ( + + + + Check it out on Github! + + + + )} +
+ ) : ( +
+ {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
+
+ {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
+ )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
+
+ {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
+ {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
+ )} +
+
+ : null} + + + {selectedAppData.documentation} + +
+ )} +
+
+
+) : null; + + const ConfigurationTab = memo((props) => { + return ( +
+
+
+ {isLoggedIn === true ? + + : + + + + } + + {appAuthentication?.length > 0 ? +
+ + or use + + +
+ +
+
+ : null} +
+
+ + {locations !== undefined && locations !== null && locations.length > 0 ? +
+ + Runtime location + + +
+ : null} +
+ )}); + + return ( + + + {authenticationModal} + + + + ); +}; + +export default ApiExplorerWrapper; + + +const Wrapper = ({children, userdata})=>{ + + const { leftSideBarOpenByClick } = useContext(Context); + + return( + +
+ {children} +
+ ) +} diff --git a/frontend/src/views/CodeWorkflow.jsx b/frontend/src/views/CodeWorkflow.jsx new file mode 100644 index 00000000..6959fe7d --- /dev/null +++ b/frontend/src/views/CodeWorkflow.jsx @@ -0,0 +1,466 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { toast } from 'react-toastify'; +import { Button, Box, Typography, Paper, Toolbar, Divider, CircularProgress } from '@mui/material'; + +import { + PlayArrow as PlayArrowIcon, + Save as SaveIcon, + History as HistoryIcon +} from '@mui/icons-material'; + +import ExecutionPanel from '../components/ExecutionPanel.jsx'; +import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; + +const CodeWorkflow = (defaultprops) => { + const { serverside, userdata, globalUrl, isLoaded, isLoggedIn, surfaceColor, inputColor, ...props } = defaultprops; + + const [workflow, setWorkflow] = useState({}); + + const [showExecutions, setShowExecutions] = useState(false); + // In CodeWorkflow, add this state + const [panelHeight, setPanelHeight] = useState(400); + const [executions, setExecutions] = useState([]); + const [currentExecution, setCurrentExecution] = useState(null); + const [mainAction, setMainAction] = useState(null); + const editorRef = useRef(null); + const [apiKey, setApiKey] = useState(""); + + const [editorData, setEditorData] = React.useState({ + "name": "", + "value": "", + "field_number": -1, + "actionlist": [], + "field_id": "", + }) + + const getSettings = async () => { + try { + const response = await fetch(`${globalUrl}/api/v1/getsettings`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }); + + if (response.status !== 200) { + console.log("Status not 200 for getsettings :O!"); + if (response.status >= 500) { + toast("Something went wrong while loading the settings. Please reload.") + } + return null; + } + + const responseJson = await response.json(); + setApiKey(responseJson.apikey); + console.log("API Key: ", responseJson.apikey); + + return responseJson.apikey; + } catch (error) { + console.log("Get settings error: ", error.toString()); + return null; + } + } + // Calculate editor height based on execution panel visibility + const getEditorHeight = () => { + return `calc(100vh - ${showExecutions ? `${panelHeight + 40}px` : '40px'})`; + }; + + let url = window.location.pathname; + const workflowId = url.split("/")[2]; + + const getWorkflow = async (workflow_id, sourcenode) => { + try { + const response = await fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }); + + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + if (response.status >= 500) { + toast("Something went wrong while loading the workflow. Please reload.") + } + } + + const responseJson = await response.json(); + setWorkflow(responseJson); + + for (let i = 0; i < responseJson.actions.length; i++) { + if (responseJson.actions[i].app_id === "3e320a20966d33c9b7e6790b2705f0bf") { + console.log("Setting code to: ", responseJson.actions[i].parameters[0].value); + setCode(responseJson.actions[i].parameters[0].value); + setMainAction(responseJson.actions[i]); + + if (responseJson.actions[i].parameters[0].value.length === 0) { + // fetch API key of the user + const result = await getSettings(); + + console.log("accessible result: ", result); + + // await setCode(` + // from shufflepy import Shuffle + + // shuffle = Shuffle( + // "${result}", + // url='https://shuffler.io', + // ) + // ` + // ); + } + break; + } + } + } catch (error) { + console.log("Get workflows error: ", error.toString()); + } + }; + + useEffect(() => { + getWorkflow(workflowId); + }, []); + + // In CodeWorkflow component, add this effect: + useEffect(() => { + if (workflow.id) { + // Check URL for execution_id parameter + const urlParams = new URLSearchParams(window.location.search); + const executionId = urlParams.get('execution_id'); + if (executionId) { + setShowExecutions(true); // Show the panel if execution_id is present + } + } + }, [workflow]); + + const [code, setCode] = useState(""); + const [testResult, setTestResult] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const saveLatestWorkflow = () => { + let newWorkflow = workflow; + + console.log("Workflow actions: ", newWorkflow.actions); + + // find the latest "Shuffle tools fork" node + for (let i = 0; i < newWorkflow.actions.length; i++) { + console.log("Actios: ", newWorkflow.actions[i]); + if (newWorkflow.actions[i].app_id === "3e320a20966d33c9b7e6790b2705f0bf") { + // update the code + console.log("Updating code: ", code); + newWorkflow.actions[i].parameters[0].value = code; + break; + } + } + + fetch(`${globalUrl}/api/v1/workflows/${workflow.id}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(newWorkflow), + }).then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + toast("Something went wrong while saving the workflow. Please try again.", { type: "error" }); + } else if (response.status === 200) { + toast("Workflow saved successfully!"); + } + }); + }; + + const getParents = async () => { + return [ + { + "label": "Execution Argument", + "type": "INTERNAL" + } + ] + } + + const handleRunCode = async () => { + saveLatestWorkflow(); + setLoading(true); + setError(""); + + let start_node = ""; + + for (let i = 0; i < workflow.actions.length; i++) { + if (workflow.actions[i].isStartNode) { + start_node = workflow.actions[i].id; + } + } + + try { + const response = await fetch(`${globalUrl}/api/v1/workflows/${workflow.id}/execute`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify({ + "start": start_node, + "execution_arguments": "", + }), + }); + + if (response.ok) { + const responseJson = await response.json(); + const newExecution = { + execution_id: responseJson.execution_id, + authorization: responseJson.authorization, + status: 'EXECUTING', + started_at: new Date().toISOString() + }; + setShowExecutions(true); + setCurrentExecution(newExecution); + } + + } catch (error) { + console.log("Error: ", error); + setError(error.toString()); + } finally { + setLoading(false); + } + }; + + + const handleSaveCode = async () => { + saveLatestWorkflow(); + }; + + const handleEditorDidMount = (editor, monaco) => { + if (monaco.languages && monaco.languages.python) { + if (monaco.languages.python.pythonDefaults) { + monaco.languages.python.pythonDefaults.setCompilerOptions({ + target: monaco.languages.typescript.ScriptTarget.ES2020, + allowNonTsExtensions: true + }); + } + } + + monaco.languages.setLanguageConfiguration('python', { + autoClosingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" } + ], + surroundingPairs: [ + { open: '{', close: '}' }, + { open: '[', close: ']' }, + { open: '(', close: ')' }, + { open: '"', close: '"' }, + { open: "'", close: "'" } + ] + }); + }; + + const handleEditorChange = (value) => { + setCode(value); + }; + + return ( + + {/* IDE-like toolbar */} + + + {workflow.name || 'Untitled Workflow'} + + + + + + + + + {/* Editor container */} + + {workflow && mainAction ? ( + { }} + toolsAppId={mainAction.app_id} + codedata={code} + setcodedata={setCode} + parameterName={editorData.name} + fieldCount={editorData.field_number} + actionlist={editorData.actionlist} + fieldname={editorData.field_id} + changeActionParameterCodeMirror={() => { }} + activeDialog={() => { }} + setActiveDialog={() => { }} + fullScreenMode={true} + /> + ) : ( + + + + )} + + {/* */} + + + {/* Create an input element called "copy_element_shuffle" that is not visible */} + + + {/* Results Panel */} + {(error || testResult) && ( + + {error && ( + + Error: +
{error}
+
+ )} + {testResult && ( + + Test Result: +
{JSON.stringify(testResult, null, 2)}
+
+ )} +
+ )} + + {/* In CodeWorkflow component */} + {showExecutions && ( + setShowExecutions(false)} + currentExecution={currentExecution} + height={panelHeight} + onHeightChange={setPanelHeight} // Add this prop to handle height updates + mainAction={mainAction} + /> + )} + +
+ ); +}; + +export default CodeWorkflow;