3098 lines
112 KiB
React
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>
|
|
);
|
|
});
|