Files
shuffle-cracked/frontend/src/components/ApiExplorer.jsx
T

3098 lines
112 KiB
React

import ReactJson from "react-json-view-ssr";
import React, { useState, useEffect, useRef, useCallback, memo, useContext} from "react";
import { toast } from "react-toastify";
import {
Search as SearchIcon,
} from "@mui/icons-material";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-json";
import "ace-builds/src-noconflict/theme-gruvbox";
import {
Paper,
Button,
Box,
MenuItem,
TextField,
Tabs,
Tab,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
Select,
Typography,
TableRow,
InputAdornment,
Divider,
LinearProgress,
Tooltip,
} from "@mui/material";
import throttle from "lodash/throttle";
import {getTheme} from "../theme.jsx";
import { validateJson, collapseField, } from "../views/Workflows2.jsx";
import DeleteIcon from "@mui/icons-material/Delete";
import { Context } from "../context/ContextApi.jsx";
function CustomTabPanel(props) {
const { children, value, index, ...other } = props;
const {themeMode} = useContext(Context);
const theme = getTheme(themeMode)
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Box sx={{ p: 3, padding: 0, backgroundColor: theme.palette.backgroundColor}}>
{children}
</Box>
)}
</div>
);
}
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, isLoggedIn, isLoaded }) => {
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 {themeMode} = useContext(Context);
const theme = getTheme(themeMode)
const [firstSendDone, setFirstSendDone] = useState(false)
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 (
<div
style={{
display: "flex",
flexDirection: 'row',
height: (isLoggedIn && isLoaded) ? "auto" : "calc(100vh - 80px)",
}}
>
<ActionsList info={info} openapi={openapi} isLoggedIn={isLoggedIn} isLoaded={isLoaded} userdata={userdata} actions={actions} filteredActions={filteredActions} setFilteredActions={setFilteredActions} selectedActionIndex={selectedActionIndex} setSelectedActionIndex={setSelectedActionIndex} setExampleBody={setExampleBody }/>
<ActionResponseAndRequest isLoaded={isLoaded} isLoggedIn={isLoggedIn} ConfigurationTab={ConfigurationTab} selectedAppData={selectedAppData} HandleApiExecution={HandleApiExecution} userdata={userdata} info={info} filteredActions={filteredActions} setFilteredActions={setFilteredActions} actions={actions} selectedActionIndex={selectedActionIndex} ExampleBody={ExampleBody} setExampleBody={setExampleBody} openapi={openapi} serverurl={serverurl} globalUrl={globalUrl} setSelectedActionIndex={setSelectedActionIndex}/>
</div>
);
});
export default ApiExplorer;
const ActionResponseAndRequest = memo(({ isLoggedIn, isLoaded, 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 (
<div ref={loadedAction} style={{ width: "100%", overflowY:'auto', justifyContent: 'center', alignItems: "center", paddingTop: 20}}>
<div style={{ display: "flex", flexDirection: "column", gap: "10px", paddingTop: 40, height: "calc(100vh - 80px)"}}>
{filteredActions.map((action, index) => (
<div key={index} style={{ width: "100%", boxSizing: "border-box", borderBottom: index === filteredActions.length - 1 ? "none" : "1px solid #494949", paddingBottom: index === filteredActions.length - 1 ? 250 : 30 , paddingTop: index !== 0 ? 30 : 0 }}>
<Action
id={action.name.replace(/ /g, "-").replace(/_/g, "-")}
action={action}
index={index}
serverurl={serverurl}
setApiResponse={setApiResponse}
setExampleBody={setExampleBody}
globalUrl={globalUrl}
setSelectedActionIndex={setSelectedActionIndex}
selectedActionIndex={selectedActionIndex}
info={info}
HandleApiExecution={HandleApiExecution}
selectedAppData={selectedAppData}
userdata={userdata}
ConfigurationTab={ConfigurationTab}
/>
</div>
))}
</div>
<ActionResponse isLoaded={isLoaded} isLoggedIn={isLoggedIn} apiResponse={apiResponse} ExampleBody={ExampleBody}/>
</div>
)})
const ActionsList = memo(({
actions,
selectedActionIndex,
setSelectedActionIndex,
setExampleBody,
setFilteredActions,
filteredActions,
userdata,
info,
openapi,
isLoggedIn,
isLoaded
}) => {
const [searchQuery, setSearchQuery] = useState("");
const [visibleActions, setVisibleActions] = useState([]);
const {themeMode} = useContext(Context);
const theme = getTheme(themeMode);
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?.toLowerCase().replaceAll("_", " ");
setSearchQuery(query)
if (query.length === 0) {
setVisibleActions(actions)
} else {
setVisibleActions(
actions?.filter((action) =>
action?.name?.toLowerCase()?.replaceAll("_", " ")?.includes(searchQuery)
)
)
}
}
return (
<div style={{ maxWidth: '350px', width: "25%", overflow: 'hidden', marginLeft: !(isLoggedIn || isLoaded) ? 5 : 0}}>
<div style={{ borderBottom: '1px solid #494949', paddingTop: 10, paddingBottom: 10}}>
<div>
{info?.title ? (
<a
href={`/apps/${openapi?.id}`}
style={{ textDecoration: 'none', }}
target="_blank"
rel="noreferrer"
>
<Tooltip title="Go to app" placement="right">
<div style={{ display: "flex", alignItems: "center" }}>
<img
src={openapi?.info["x-logo"]?.length > 0 ? openapi?.info["x-logo"] : theme?.palette?.defaultImage}
width={48}
height={48}
alt="app logo"
style={{ marginLeft: 20, borderRadius: 8 }}
/>
<Typography style={{ fontSize: 24, fontWeight: 'bold', marginLeft: 20, overflow: 'hidden', color: theme.palette.textColor
}}>
{info.title}
</Typography>
</div>
</Tooltip>
</a>
) : (
<Typography style={{ fontSize: 24, fontWeight: 'bold', marginLeft: 20, overflow: 'hidden', color: theme.palette.textColor
}}>
Api Explorer
</Typography>
)}
</div>
</div>
<TextField
value={searchQuery}
placeholder="Search endpoints"
onChange={handleSearch}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
style: { height: "100%", marginTop: 10, width: '90%', },
}}
sx={{
marginLeft: 2,
width:'100%',
"& .MuiOutlinedInput-root fieldset": {
border: "1px solid rgba(73, 73, 73, 1)",
},
}}
/>
<div
style={{
marginLeft: 20,
marginTop: 15,
backgroundColor: theme.palette.backgroundColor,
overflowY: "auto",
height: (isLoaded && isLoggedIn) ? "calc(100vh - 190px)" : "calc(100vh - 260px)",
paddingRight: 5,
}}
>
{visibleActions.length > 0 ? (
visibleActions.map((action, actionIndex) => (
<Button
id={`action-list-${actionIndex}`}
key={`action-list-${actionIndex}`}
sx={{
padding: "15px 5px",
color: "none",
fontWeight: "normal",
textTransform: "none",
backgroundColor:
selectedActionIndex === actionIndex
? theme.palette.hoverColor
: "transparent",
border: "none",
justifyContent: "flex-start",
boxShadow: "none",
pointerEvents: "auto",
marginBottom: 0.625,
width: "100%",
display: "flex",
alignItems: "center",
textWrap: "nowrap",
textOverflow: "ellipsis",
"&:hover": {
backgroundColor: theme.palette.hoverColor,
},
}}
onClick={() => handleActionClick(actionIndex, action)}
>
<span
style={{
color: RequestMethods.find(
(method) => method.value === action.method
)?.color,
fontWeight: "bold",
minWidth: 70,
textAlign: "start",
}}
>
{action.method}
</span>
<span
style={{
color: theme.palette.textColor,
textOverflow: "ellipsis",
overflow: "hidden",
}}
>
{action?.name?.replaceAll("_", " ")}
</span>
</Button>
))
) : (
<div style={{ padding: "15px", color: theme.palette.textColor, textAlign: "center" }}>
No actions found
</div>
)}
</div>
</div>
);
});
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 {themeMode} = useContext(Context);
const theme = getTheme(themeMode);
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]);
const actionname = action?.name.charAt(0).toUpperCase() + action?.name.slice(1).replaceAll("_", " ")
return (
<div
ref={actionRef}
id={action.name.replace(/ /g, "-").replace(/_/g, "-")}
data-index={index}
key={action.name.replace(/ /g, "-").replace(/_/g, "-")}
style={{
marginTop: 50,
display: "flex",
flexDirection: "row",
minHeight: 400,
justifyContent: 'center'
}}
onClick={() => setIsUserInteracting(true)}
>
<div
style={{
display: "flex",
flexDirection: "column",
maxWidth: 1024,
width: "100%",
}}
>
<Typography
style={{
fontSize: 24,
fontWeight: 700,
marginLeft: 40,
marginBottom: 5,
color: theme.palette.textColor
}}
>
{actionname}
</Typography>
<div
style={{
border: "1px solid rgba(73, 73, 73, 1)",
display: "flex",
minWidth: 694,
borderRadius: 6,
marginLeft: "40px",
marginTop: 2,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
height: 51,
alignItems: 'center',
}}
>
<Select
value={selectedMethod}
onChange={(e) => setSelectedMethod(e.target.value)}
sx={{
minWidth: 130,
height: "100%",
backgroundColor: "transparent",
"& .MuiSelect-select": {
color: RequestMethods.find((method) => method.value === selectedMethod)
?.color || theme.palette.textFieldStyle.backgroundColor,
},
}}
MenuProps={{
PaperProps: {
sx: {
padding: 0,
margin: 0,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
},
},
MenuListProps: {
sx: {
padding: 0,
margin: 0,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
},
},
}}
>
{RequestMethods.map((method, methodIndex) => (
<MenuItem
key={`${method.value}-${methodIndex}`}
value={method.value}
sx={{
color: method.color,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
border: "none",
marginBottom: 0.25,
"&:hover": {
backgroundColor: method.color,
color: theme.palette.textFieldStyle.color,
},
"&.Mui-selected": {
backgroundColor: method.color,
color: theme.palette.textFieldStyle.color,
border: "none",
"&:hover": {
backgroundColor: method.color,
color: theme.palette.textFieldStyle.color,
},
},
"&.Mui-focusVisible": {
outline: "none",
},
}}
>
{method.value}
</MenuItem>
))}
</Select>
<TextField
fullWidth
ref={inputRef}
value={actionUrl}
inputProps={{
style: {
margin: "auto",
border: "none",
color: theme.palette.textFieldStyle.color,
display: 'flex',
height: '100%',
alignItems: 'center',
},
}}
sx={{
"& .MuiOutlinedInput-root": {
"& fieldset": {
border: "none",
},
"&:hover fieldset": {
border: "none",
},
"&.Mui-focused fieldset": {
border: "none",
},
margin: "auto",
},
margin: "auto",
}}
onKeyDown={(e) => {
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:", newUrl, error);
toast("The URL is not a valid one. Please check and try again.")
}
} else {
//toast("The URL needs to start with http:// or https://")
}
}}
/>
<Button
variant="contained"
sx={{
width: "107px",
height: 35,
borderRadius: 200,
padding: "16px 24px 16px 24px",
backgroundColor: "rgba(2, 203, 112, 0.2)",
color: "rgba(2, 203, 112, 1)",
cursor: "pointer",
textTransform: "none",
"&:hover": {
backgroundColor: "rgba(2, 203, 112, 0.1)",
},
marginRight: "5px"
}}
disabled={disableExecuteButton}
onClick={() => {
/*
if (!firstSendDone) {
setFirstSendDone(true)
setCurTab(2)
}
*/
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)
}
}}
>
Send
</Button>
</div>
{showResponseLoader? (
<LinearProgress
variant="indeterminate"
sx={{
backgroundColor: "#252525",
"& .MuiLinearProgress-bar": {
backgroundColor: "#ff4500",
},
minWidth: "694px",
maxWidth: "1024px",
marginLeft: "40px",
height: '1px',
borderRadius: '8px',
}}
/>
) : null}
<Paper
style={{
minWidth: 696,
marginTop: 16,
marginLeft: "40px",
position: "sticky",
boxShadow: "none",
}}
>
<div
style={{
display: "flex",
justifyContent: "center",
background: theme.palette.backgroundColor,
}}
>
<Tabs
value={curTab}
onChange={(event, newValue) =>
handleChangeTab(index, newValue)
}
aria-label="basic tabs example"
>
<Tab
label=<span
style={{ textTransform: "none", fontSize: 16 }}
>
Headers
</span>
{...a11yProps(0)}
/>
{shouldShowBodyTab && (
<Tab
label=<span
style={{ textTransform: "none", fontSize: 16 }}
>
Body
</span>
{...a11yProps(1)}
/>
)}
<Tab
label=<span
style={{ textTransform: "none", fontSize: 16 }}
>
Params
</span>
{...a11yProps(shouldShowBodyTab ? 2 : 1)}
/>
{ConfigurationTab ? (
<Tab
label=<span
style={{ textTransform: "none", fontSize: 16 }}
>
Configuration
</span>
{...a11yProps(shouldShowBodyTab ? 3 : 2)}
/>
) : null}
</Tabs>
</div>
<CustomTabPanel
value={curTab}
index={0}
>
<TableContainer
style={{
minWidth: 696,
backgroundColor: theme.palette.platformColor,
borderRadius: 6,
border: "1px solid rgba(73, 73, 73, 1)",
}}
>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
fontSize: 16
}}
>
Key
</TableCell>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
fontSize: 16
}}
>
Value
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{RequestHeader.map((row, rowIndex) => (
<TableRow key={rowIndex} sx={{ height: 60 }}>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
padding: "0px 4px",
}}
>
<TextField
fullWidth
variant="outlined"
type="text"
id={`header-key-${index}-${rowIndex}`}
value={row.key}
onChange={(e) =>
handleInputChange(
rowIndex,
"key",
e.target.value
)
}
inputProps={{
style: {
backgroundColor: theme.palette.platformColor,
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()
}
}}
/>
</TableCell>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
padding: "0px 4px",
}}
>
<TextField
fullWidth
type="text"
value={row.value}
id={`header-value-${index}-${rowIndex}`}
onChange={(e) =>
handleInputChange(
rowIndex,
"value",
e.target.value
)
}
inputProps={{
endAdornment: (
<InputAdornment position="end">
<DeleteIcon sx={{ color: "red" }} />
</InputAdornment>
),
style: {
backgroundColor: theme.palette.platformColor,
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()
}}
}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Button
onClick={() => addRow()}
variant="contained"
color="primary"
style={{
width: 127,
height: 35,
borderRadius: 200,
padding: "16px 24px",
marginTop: 26,
textTransform: "none",
backgroundColor: "rgba(255, 132, 68, 0.2)",
color: "rgba(255, 132, 68, 1)",
}}
>
Add Row
</Button>
</CustomTabPanel>
{shouldShowBodyTab && (
<CustomTabPanel
value={curTab}
index={1}
>
<AceEditor
id="shuffle-codeeditor-api-Explorer"
name="shuffle-codeeditor-api-Explorer"
mode="json"
ref={editorRef}
value={RequestBody}
theme="gruvbox"
width="100%"
fontSize={16}
setOptions={{ useWorker: false }}
onChange={handleAceEditorChange}
highlightActiveLine={false}
style={{
wordBreak: "break-word",
marginTop: 0,
paddingBottom: 10,
overflowY: "auto",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
backgroundColor: "#212121",
color: "white",
justifyContent: "center",
minHeight: `${AceEditorHeight}px`,
maxHeight: `${AceEditorHeight}px`,
border: "1px solid rgb(73, 73, 73)",
borderRadius: 6,
}}
/>
</CustomTabPanel>
)}
<CustomTabPanel
value={curTab}
index={shouldShowBodyTab ? 2 : 1}
>
<TableContainer
style={{
minWidth: 696,
backgroundColor: theme.palette.platformColor,
borderRadius: 6,
border: "1px solid rgba(73, 73, 73, 1)",
}}
>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
fontSize: 16
}}
>
Key
</TableCell>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
fontSize: 16
}}
>
Value
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{RequestParams.map((row, rowIndex) => (
<TableRow key={rowIndex} sx={{ height: 60 }}>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
padding: "0px 4px",
}}
>
<TextField
fullWidth
type="text"
inputProps={{
style: {
backgroundColor: theme.palette.platformColor ,
padding: "4px 8px",
},
}}
id={`param-key-${index}-${rowIndex}`}
sx={{
"& .MuiOutlinedInput-root": {
"& fieldset": {
border: "none",
},
"&:hover fieldset": {
border: "none",
},
"&.Mui-focused fieldset": {
border: "none",
},
},
}}
value={row.key}
onChange={(e) =>
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();
}
}}
/>
</TableCell>
<TableCell
style={{
border: "1px solid rgba(73, 73, 73, 1)",
padding: "0px 4px",
}}
>
<TextField
fullWidth
type="text"
id={`param-value-${index}-${rowIndex}`}
value={row.value}
inputProps={{
style: {
backgroundColor: theme.palette.platformColor,
padding: "4px 8px",
},
}}
sx={{
"& .MuiOutlinedInput-root": {
"& fieldset": {
border: "none",
},
"&:hover fieldset": {
border: "none",
},
"&.Mui-focused fieldset": {
border: "none",
},
},
}}
onChange={(e) =>
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()
}
}
}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Button
onClick={addRequestParamsRow}
variant="contained"
color="primary"
style={{
width: 127,
height: 35,
borderRadius: 200,
padding: "16px 24px 16px 24px",
marginTop: 26,
textTransform: "none",
backgroundColor: "rgba(255, 132, 68, 0.2)",
color: "rgba(255, 132, 68, 1)",
}}
>
Add Row
</Button>
</CustomTabPanel>
{(ConfigurationTab && ((shouldShowBodyTab && curTab === 3 ) || (!shouldShowBodyTab && curTab === 2)))? (
<ConfigurationTab
setCurTab={setCurTab}
shouldShowBodyTab={shouldShowBodyTab}
/>
) : null}
</Paper>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
width: 354,
marginLeft: 10,
marginRight: 5,
}}
>
<div
key={index}
style={{
display: "flex",
flexDirection: "column",
paddingTop: index === 0 ? "20px" : "40px",
minWidth: 200,
marginLeft: 10
}}
>
<span style={{ fontSize: 16, fontWeight: 600, color: theme.palette.textColor }}>
{action.name.replaceAll("_", " ")}
</span>
<p style={{color: theme.palette.textColor}}>
{action.description
? action.description
: ""}
</p>
</div>
</div>
</div>
);
})
const ActionResponse = memo(({ apiResponse, ExampleBody, isLoggedIn, isLoaded }) => {
const [height, setHeight] = useState("14vh")
const [responseTabIndex, setResponseTabIndex] = useState(0)
const [oldResponse, setOldResponse] = useState(apiResponse)
const [highlight, setHighlight] = useState(false)
const {themeMode } = useContext(Context)
const theme = getTheme(themeMode)
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 (
<ApiResponseWrapper isLoggedIn={isLoggedIn} isLoaded={isLoaded}>
<div
style={{
width: '100%',
height: height,
backgroundColor: theme.palette.backgroundColor,
display: 'flex',
flexDirection: 'column',
borderTop: '1px solid rgba(255,255,255,0.2)',
maxHeight: "80vh",
transition: "height 0.3s ease",
}}
>
{highlight === true ?
<LinearProgress fullWidth color="secondary" style={{height: 1, }}/>
: null
}
<div
style={{
overflow: 'auto',
cursor: 'row-resize',
}}
onMouseDown={initResize}
>
<Tabs
value={responseTabIndex}
onChange={(e, newValue) => setResponseTabIndex(newValue)}
>
<Tab
label={<span style={{ textTransform: 'none', fontSize: 16 }}>Response</span>}
{...a11yProps(0)}
/>
<Tab
label={<span style={{ textTransform: 'none', fontSize: 16 }}>Example Response</span>}
disabled={ExampleBody === undefined || ExampleBody === null || ExampleBody === ""}
{...a11yProps(1)}
/>
<Tab
label={<span style={{ textTransform: 'none', fontSize: 16 }}>History</span>}
disabled={true}
{...a11yProps(2)}
/>
</Tabs>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
<CustomTabPanel
value={responseTabIndex}
index={0}
>
<ResponseTabWrapper apiResponse={apiResponse} />
</CustomTabPanel>
<CustomTabPanel
value={responseTabIndex}
index={1}
>
<ReactJson
src={formData(ExampleBody)}
theme={theme.palette.jsonTheme}
style={{...theme.palette.reactJsonStyle, border: "none"}}
collapsed={false}
iconStyle={theme.palette.jsonIconStyle}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
enableClipboard={handleReactJsonClipboard}
displayDataTypes={false}
name={false}
/>
</CustomTabPanel>
</div>
</div>
</ApiResponseWrapper>
);
});
const ResponseTabWrapper = memo(({ apiResponse }) => {
const {themeMode } = useContext(Context)
const theme = getTheme(themeMode)
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(
<ReactJson
src={apiResponse}
theme={theme.palette.jsonTheme}
style={{...theme.palette.reactJsonStyle, border: "none"}}
shouldCollapse={(jsonField) => {
return collapseField(jsonField)
}}
iconStyle={theme.palette.jsonIconStyle}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
enableClipboard={handleReactJsonClipboard}
displayDataTypes={false}
name={false}
/>
)})
const PaddingWrapper = memo(({ isLoggedIn, isLoaded, children }) => {
const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context);
const theme = getTheme(themeMode)
return (
<div
style={{
width: (isLoggedIn && isLoaded)
? leftSideBarOpenByClick
? windowWidth >= 1920 ? "calc(100% - 630px)" : "calc(100% - 570px)"
: windowWidth >= 1920 ? "calc(100vw - 460px)": "calc(100% - 410px)"
: windowWidth >= 1920 ? "calc(100% - 370px)" : "calc(100% - 320px)",
backgroundColor: theme.palette.backgroundColor,
position: "fixed",
bottom: 0,
right: 0,
display: "flex",
flexDirection: "column",
borderTop: "1px solid #212121",
transition: "width 0.3s ease",
minHeight: "10%",
}}
>
{children}
</div>
);
});
const ApiResponseWrapper = memo(({ children, isLoaded, isLoggedIn }) => {
return (
<PaddingWrapper isLoggedIn={isLoggedIn} isLoaded={isLoaded}>
{children}
</PaddingWrapper>
);
});