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}
) }