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, isLoaded} = 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 [appLoaded, setAppLoaded] = useState(false);
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 (openapi?.id === "HTTP") {
selectedAppData.name = "HTTP"
}
if (selectedAppData !== undefined && selectedAppData !== null && Object.getOwnPropertyNames(selectedAppData).length > 0) {
HandleAppAuthentication(selectedAppData?.name)
}
}, [selectedAppData, openapi])
useEffect(() => {
getAppData(appid)
if (appid !== undefined && appid !== null && appid.length !== 0) {
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");
if (appname === "HTTP" || appname === "http") {
navigate("/apis")
return
}
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 API data for '${appname}' (1). Contact support@shuffler.io if this persists.`, {
"autoClose": 10000,
})
setTimeout(()=>{
navigate("/search?tab=apps");
},3000)
}
} else {
toast.error(`Failed to get API data for '${appname}' (2). Contact support@shuffler.io if this persists.`, {
"autoClose": 10000,
})
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) {
toast.warning("No app ID loaded. Showing default API testing window. ")
setOpenapi({
"id": "HTTP",
"servers": [
{"url": "https://shuffler.io"},
],
"info": {
"title": "HTTP",
"x-logo": theme.palette?.defaultImage,
},
"paths": {
"/api/v1/workflows/usecases": {
"get": {
"summary": "Custom Action",
}
}
}
})
setAppLoaded(true)
//setTimeout(() => {
// navigate("/search?tab=apps")
//}, 3000)
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) {
if (responseJson.openapi === undefined || responseJson.openapi === null) {
toast.warning("Loaded App, but no API found. Redirecting back to app..")
navigate(`/apps/${appid}`)
} else {
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);
setAppLoaded(true);
};
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
}
console.log("APPNAME: ", appname, openapi.id)
if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") {
setAppAuthentication(data)
setSelectedAuthentication({})
return
}
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(``);
};
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 and we can't run the API. Please contact support@shuffler.io if this persists.");
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 (