6459 lines
203 KiB
React
Executable File
6459 lines
203 KiB
React
Executable File
import React, { useState, useEffect, useContext } from "react";
|
||
import { makeStyles } from "@mui/styles";
|
||
import { BrowserView, MobileView } from "react-device-detect";
|
||
import { getTheme } from '../theme.jsx';
|
||
|
||
import {
|
||
Paper,
|
||
Typography,
|
||
FormControlLabel,
|
||
Button,
|
||
ButtonGroup,
|
||
Divider,
|
||
Select,
|
||
MenuItem,
|
||
FormControl,
|
||
Switch,
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
TextField,
|
||
Tooltip,
|
||
Breadcrumbs,
|
||
Drawer,
|
||
CircularProgress,
|
||
Chip,
|
||
IconButton,
|
||
} from "@mui/material";
|
||
|
||
import {
|
||
Publish as PublishIcon,
|
||
LockOpen as LockOpenIcon,
|
||
FileCopy as FileCopyIcon,
|
||
Delete as DeleteIcon,
|
||
Remove as RemoveIcon,
|
||
Add as AddIcon,
|
||
CheckCircle as CheckCircleIcon,
|
||
AttachFile as AttachFileIcon,
|
||
Apps as AppsIcon,
|
||
ErrorOutline as ErrorOutlineIcon,
|
||
AddAPhoto as AddAPhotoIcon,
|
||
AddAPhotoOutlined as AddAPhotoOutlinedIcon,
|
||
ZoomInOutlined as ZoomInOutlinedIcon,
|
||
ZoomOutOutlined as ZoomOutOutlinedIcon,
|
||
Loop as LoopIcon,
|
||
AddPhotoAlternate as AddPhotoAlternateIcon,
|
||
CallMerge as CallMergeIcon,
|
||
CloudDownload as CloudDownloadIcon,
|
||
OpenInNew as OpenInNewIcon,
|
||
} from "@mui/icons-material";
|
||
|
||
import { v4 as uuidv4 } from "uuid";
|
||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||
import YAML from "yaml";
|
||
import { MuiChipsInput } from "mui-chips-input";
|
||
//import { useAlert
|
||
import { ToastContainer, toast } from "react-toastify"
|
||
import { split } from "shellwords";
|
||
|
||
import AvatarEditor from "react-avatar-editor";
|
||
import { Context } from "../context/ContextApi.jsx";
|
||
|
||
const surfaceColor = "#27292D";
|
||
const inputColor = "#383B40";
|
||
|
||
const bodyDivStyle = {
|
||
margin: "auto",
|
||
width: "900px",
|
||
zoom: 0.8,
|
||
};
|
||
|
||
|
||
|
||
const dividerStyle = {
|
||
marginBottom: "10px",
|
||
marginTop: "10px",
|
||
height: "1px",
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
};
|
||
|
||
const appIconStyle = {
|
||
marginLeft: "5px",
|
||
};
|
||
|
||
export const useStyles = makeStyles({
|
||
notchedOutline: {
|
||
borderColor: "#f85a3e !important",
|
||
},
|
||
});
|
||
|
||
const rewrite = (args) => {
|
||
return args.reduce(function (args, a) {
|
||
if (0 === a.indexOf("-X")) {
|
||
args.push("-X");
|
||
args.push(a.slice(2));
|
||
} else {
|
||
args.push(a);
|
||
}
|
||
|
||
return args;
|
||
}, []);
|
||
};
|
||
|
||
const parseField = (s) => {
|
||
return s.split(/: (.+)/);
|
||
};
|
||
|
||
const isURL = (s) => {
|
||
return /^https?:\/\//.test(s);
|
||
};
|
||
|
||
// Parses CURL to a real request
|
||
const parseCurl = (s) => {
|
||
//console.log("CURL: ", s)
|
||
|
||
if (0 != s.indexOf("curl ")) {
|
||
console.log("Not curl start");
|
||
return "";
|
||
}
|
||
|
||
try {
|
||
var args = rewrite(split(s));
|
||
} catch (e) {
|
||
return s;
|
||
}
|
||
|
||
var out = { method: "GET", header: {} };
|
||
var state = "";
|
||
|
||
args.forEach(function (arg) {
|
||
switch (true) {
|
||
case isURL(arg):
|
||
out.url = arg;
|
||
break;
|
||
|
||
case arg === "-A" || arg === "--user-agent":
|
||
state = "user-agent";
|
||
break;
|
||
|
||
case arg === "-H" || arg === "--header":
|
||
state = "header";
|
||
break;
|
||
|
||
case arg === "-d" || arg === "--data" || arg === "--data-ascii":
|
||
state = "data";
|
||
break;
|
||
|
||
case arg === "-u" || arg === "--user":
|
||
state = "user";
|
||
break;
|
||
|
||
case arg === "-I" || arg === "--head":
|
||
out.method = "HEAD";
|
||
break;
|
||
|
||
case arg === "-X" || arg === "--request":
|
||
state = "method";
|
||
break;
|
||
|
||
case arg === "-b" || arg === "--cookie":
|
||
state = "cookie";
|
||
break;
|
||
|
||
case arg === "--compressed":
|
||
out.header["Accept-Encoding"] =
|
||
out.header["Accept-Encoding"] || "deflate, gzip";
|
||
break;
|
||
|
||
case !!arg:
|
||
switch (state) {
|
||
case "header":
|
||
var field = parseField(arg);
|
||
out.header[field[0]] = field[1];
|
||
state = "";
|
||
break;
|
||
case "user-agent":
|
||
out.header["User-Agent"] = arg;
|
||
state = "";
|
||
break;
|
||
case "data":
|
||
if (out.method === "GET" || out.method === "HEAD")
|
||
out.method = "POST";
|
||
|
||
out.header["Content-Type"] = out.header["Content-Type"] || "application/x-www-form-urlencoded";
|
||
|
||
out.body = out.body ? out.body + "&" + arg : arg;
|
||
state = "";
|
||
break;
|
||
case "user":
|
||
out.header["Authorization"] = "Basic " + btoa(arg);
|
||
state = "";
|
||
break;
|
||
case "method":
|
||
out.method = arg;
|
||
state = "";
|
||
break;
|
||
case "cookie":
|
||
out.header["Set-Cookie"] = arg;
|
||
state = "";
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
});
|
||
|
||
return out;
|
||
};
|
||
|
||
// Basically CRUD for each category + special
|
||
// These are already tracked in the shuffle/shuffle-shared/blobs.go file
|
||
// as backend should be used for managing this long-term
|
||
export const appCategories = [
|
||
{
|
||
"name": "Communication",
|
||
"color": "#FFC107",
|
||
"icon": "communication",
|
||
"action_labels": ["List Messages", "Send Message", "Get Message", "Search messages", "List Attachments", "Get Attachment", "Get Contact"],
|
||
},
|
||
{
|
||
"name": "SIEM",
|
||
"color": "#FFC107",
|
||
"icon": "siem",
|
||
"action_labels": ["Search", "List Alerts", "Close Alert", "Get Alert", "Create detection", "Add to lookup list", "Isolate endpoint",],
|
||
}, {
|
||
"name": "Eradication",
|
||
"color": "#FFC107",
|
||
"icon": "eradication",
|
||
"action_labels": ["List Alerts", "Close Alert", "Get Alert", "Create detection", "Block hash", "Search Hosts", "Isolate host", "Unisolate host", "Trigger host scan",],
|
||
}, {
|
||
"name": "Cases",
|
||
"color": "#FFC107",
|
||
"icon": "cases",
|
||
"action_labels": ["List tickets", "Get ticket", "Create ticket", "Close ticket", "Add comment", "Update ticket", "Search tickets"],
|
||
}, {
|
||
"name": "Assets",
|
||
"color": "#FFC107",
|
||
"icon": "assets",
|
||
"action_labels": ["List Assets", "Get Asset", "Search Assets", "Search Users", "Search endpoints", "Search vulnerabilities"],
|
||
}, {
|
||
"name": "Intel",
|
||
"color": "#FFC107",
|
||
"icon": "intel",
|
||
"action_labels": ["Get IOC", "Search IOC", "Create IOC", "Update IOC", "Delete IOC",],
|
||
}, {
|
||
"name": "IAM",
|
||
"color": "#FFC107",
|
||
"icon": "iam",
|
||
"action_labels": ["Reset Password", "Enable user", "Disable user", "Get Identity", "Get Asset", "Search Identity", "Get KMS Key",],
|
||
}, {
|
||
"name": "Network",
|
||
"color": "#FFC107",
|
||
"icon": "network",
|
||
"action_labels": ["Get Rules", "Allow IP", "Block IP",],
|
||
},
|
||
{
|
||
"name": "AI",
|
||
"color": "#FFC107",
|
||
"icon": "AI",
|
||
"action_labels": ["Answer Question", "Run Action", "Run LLM",],
|
||
},
|
||
{
|
||
"name": "Other",
|
||
"color": "#FFC107",
|
||
"icon": "other",
|
||
"action_labels": ["Update Info", "Get Info", "Get Status", "Get Version", "Get Health", "Get Config", "Get Configs", "Get Configs by type", "Get Configs by name", "Run script"],
|
||
},
|
||
]
|
||
|
||
export const base64_decode = (str) => {
|
||
return decodeURIComponent(
|
||
atob(str).split("").map(function (c) {
|
||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||
}).join("")
|
||
);
|
||
};
|
||
|
||
// Loops through properties to find the actual JSON output to use
|
||
const getJsonObject = (properties) => {
|
||
// Loop inside the JSON object and get the value of each key
|
||
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") {
|
||
//console.log("ARRAY!!")
|
||
jsonObject[key] = [jsonret];
|
||
} else {
|
||
jsonObject[key] = jsonret;
|
||
}
|
||
} else {
|
||
if (property.hasOwnProperty("properties")) {
|
||
const jsonret = getJsonObject(property.properties);
|
||
if (property.type === "array") {
|
||
//console.log("ARRAY2!!")
|
||
jsonObject[key] = [jsonret];
|
||
} else {
|
||
jsonObject[key] = jsonret;
|
||
}
|
||
} else {
|
||
//console.log("No items or properties found: ", property);
|
||
}
|
||
}
|
||
|
||
} 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 {
|
||
console.log("Unknown type: ", property);
|
||
}
|
||
} else {
|
||
console.log("No example or enum found: ", property);
|
||
}
|
||
}
|
||
}
|
||
|
||
return jsonObject
|
||
}
|
||
|
||
// Should be different if logged in :|
|
||
const AppCreator = (defaultprops) => {
|
||
const { globalUrl, isLoaded } = defaultprops;
|
||
const classes = useStyles();
|
||
//const alert = useAlert();
|
||
|
||
const params = useParams();
|
||
var props = JSON.parse(JSON.stringify(defaultprops))
|
||
props.match = {}
|
||
props.match.params = params
|
||
|
||
var upload = "";
|
||
let navigate = useNavigate();
|
||
|
||
const increaseAmount = 50;
|
||
const actionNonBodyRequest = ["GET", "HEAD", "CONNECT"];
|
||
const actionBodyRequest = ["POST", "PUT", "PATCH", "DELETE"];
|
||
const authenticationOptions = [
|
||
"No authentication",
|
||
"API key",
|
||
"Bearer auth",
|
||
"Basic auth",
|
||
"JWT",
|
||
"Oauth2",
|
||
];
|
||
const apikeySelection = ["Header", "Query"];
|
||
|
||
const [name, setName] = useState("");
|
||
const [contact, setContact] = useState("");
|
||
const [file, setFile] = useState("");
|
||
const [fileBase64, setFileBase64] = useState("");
|
||
const [isAppLoaded, setIsAppLoaded] = useState(false);
|
||
const [isEditing, setIsEditing] = useState(false);
|
||
const [description, setDescription] = useState("");
|
||
const [baseUrl, setBaseUrl] = useState("");
|
||
const [authenticationRequired, setAuthenticationRequired] = useState(false);
|
||
const [authenticationOption, setAuthenticationOption] = useState(
|
||
authenticationOptions[0]
|
||
);
|
||
const [newWorkflowTags, setNewWorkflowTags] = React.useState([]);
|
||
const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]);
|
||
const [parameterName, setParameterName] = useState("");
|
||
const [parameterLocation, setParameterLocation] = useState(
|
||
apikeySelection.length > 0 ? apikeySelection[0] : ""
|
||
);
|
||
const [refreshUrl, setRefreshUrl] = useState("");
|
||
const [projectCategories, setProjectCategories] = useState([]);
|
||
const [selectedCategory, setSelectedCategory] = useState("");
|
||
|
||
const [update, setUpdate] = useState("");
|
||
const [urlPathParameters] = useState([]);
|
||
const [basedata, setBasedata] = React.useState({});
|
||
const [actions, setActions] = useState([]);
|
||
const [filteredActions, setFilteredActions] = useState([]);
|
||
const [errorCode, setErrorCode] = useState("");
|
||
const [appBuilding, setAppBuilding] = useState(false);
|
||
const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false);
|
||
const [actionAmount, setActionAmount] = useState(increaseAmount);
|
||
const [newAppGroup, setNewAppGroup] = useState("")
|
||
|
||
const { themeMode, supportEmail, brandColor } = useContext(Context);
|
||
const theme = getTheme(themeMode, brandColor);
|
||
|
||
const [oauth2Scopes, setOauth2Scopes] = useState([]);
|
||
const [oauth2Type, setOauth2Type] = useState("delegated");
|
||
|
||
const actionListStyle = {
|
||
paddingLeft: "10px",
|
||
paddingRight: "10px",
|
||
paddingBottom: "10px",
|
||
paddingTop: "10px",
|
||
marginTop: "5px",
|
||
display: "flex",
|
||
color: theme.palette.text.primary,
|
||
position: "relative",
|
||
|
||
backgroundColor: theme.palette.platformColor,
|
||
};
|
||
|
||
const boxStyle = {
|
||
color: theme.palette.text.primary,
|
||
flex: "1",
|
||
marginLeft: "10px",
|
||
marginRight: "10px",
|
||
paddingLeft: "30px",
|
||
paddingRight: "30px",
|
||
paddingBottom: "30px",
|
||
paddingTop: "30px",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
|
||
backgroundColor: theme.palette.platformColor,
|
||
};
|
||
|
||
//client_credentials
|
||
const [oauth2GrantType, setOauth2GrantType] = useState("");
|
||
const defaultAuth = {
|
||
name: "",
|
||
type: "header",
|
||
example: "",
|
||
};
|
||
|
||
const [extraAuth, setExtraAuth] = useState([]);
|
||
const [app, setApp] = useState({});
|
||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||
const [selectedAction, setSelectedAction] = useState({});
|
||
const [authLoaded, setAuthLoaded] = useState(false);
|
||
|
||
// From 2023: Example to handle action labels
|
||
// Goal: Make this dynamically load from the backend
|
||
// and make categories + labels modifyable.
|
||
// Categories are the main categories in the App Framework
|
||
const [categories, setCategories] = useState(appCategories)
|
||
|
||
|
||
|
||
const redirectOpenApi = () => {
|
||
navigate(`/apps/new?id=${appValidation}`)
|
||
}
|
||
|
||
const newUpload = React.useRef(null);
|
||
const [openApiError, setOpenApiError] = React.useState("");
|
||
const [validation, setValidation] = React.useState("");
|
||
const [appValidation, setAppValidation] = React.useState("");
|
||
const [openApi, setOpenApi] = React.useState("");
|
||
const [openApiData, setOpenApiData] = React.useState("");
|
||
const [openApiModal, setOpenApiModal] = React.useState(false);
|
||
|
||
const [appDownloadData, setAppDownloadData] = React.useState("");
|
||
|
||
useEffect(() => {
|
||
console.log("In useEffect for openApiData: ", openApiData)
|
||
}, [openApiData]);
|
||
|
||
const uploadFile = (e) => {
|
||
console.log("In uploadFile")
|
||
const isDropzone =
|
||
e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
|
||
const files = isDropzone ? e.dataTransfer.files : e.target.files;
|
||
const reader = new FileReader();
|
||
|
||
try {
|
||
reader.addEventListener("load", (e) => {
|
||
const content = e.target.result;
|
||
console.log("set openapi data! ", content)
|
||
setOpenApiData(content);
|
||
setOpenApiModal(true);
|
||
});
|
||
} catch (e) {
|
||
console.log("Error in dropzone: ", e);
|
||
}
|
||
|
||
try {
|
||
reader.readAsText(files[0]);
|
||
} catch (error) {
|
||
toast("Failed to read file");
|
||
}
|
||
}
|
||
|
||
const escapeApiData = (apidata) => {
|
||
//console.log(apidata)
|
||
try {
|
||
return JSON.stringify(JSON.parse(apidata));
|
||
} catch (error) {
|
||
console.log("JSON DECODE ERROR - TRY YAML");
|
||
}
|
||
|
||
try {
|
||
const parsed = YAML.parse(YAML.stringify(apidata));
|
||
//const parsed = YAML.parse(apidata))
|
||
return YAML.stringify(parsed);
|
||
} catch (error) {
|
||
console.log("YAML DECODE ERROR - TRY SOMETHING ELSE?: " + error);
|
||
setOpenApiError("Local error: " + error.toString());
|
||
}
|
||
|
||
return "";
|
||
}
|
||
|
||
const validateOpenApi = (openApidata) => {
|
||
var newApidata = escapeApiData(openApidata);
|
||
if (newApidata === "") {
|
||
// Used to return here
|
||
newApidata = openApidata;
|
||
return;
|
||
}
|
||
|
||
//console.log(newApidata)
|
||
|
||
setValidation(true);
|
||
fetch(globalUrl + "/api/v1/validate_openapi", {
|
||
method: "POST",
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
body: openApidata,
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
setValidation(false);
|
||
return response.json();
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.success) {
|
||
setAppValidation(responseJson.id);
|
||
} else {
|
||
if (responseJson.reason !== undefined) {
|
||
setOpenApiError(responseJson.reason);
|
||
}
|
||
toast("An error occurred in the response");
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
setValidation(false);
|
||
toast(error.toString());
|
||
setOpenApiError(error.toString());
|
||
});
|
||
};
|
||
|
||
|
||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||
|
||
useEffect(() => {
|
||
if (window.location.pathname.includes("apps/edit")) {
|
||
setIsEditing(true);
|
||
handleEditApp(props.match.params.appid);
|
||
} else {
|
||
checkQuery();
|
||
}
|
||
}, []);
|
||
|
||
const handleEditApp = (appid) => {
|
||
fetch(globalUrl + "/api/v1/apps/" + appid + "/config", {
|
||
method: "GET",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Accept: "application/json",
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
window.location.pathname = "/apps";
|
||
}
|
||
|
||
return response.json();
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson.success === false) {
|
||
toast("Failed to get the app");
|
||
setIsAppLoaded(true);
|
||
window.location.pathname = "/search";
|
||
} else {
|
||
parseIncomingOpenapiData(responseJson);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
toast(error.toString());
|
||
});
|
||
};
|
||
|
||
// Checks if there is an ID in the query, and gets it if it doesn't exist.
|
||
const checkQuery = () => {
|
||
var urlParams = new URLSearchParams(window.location.search);
|
||
if (!urlParams.has("id")) {
|
||
setActionAmount(0);
|
||
|
||
setIsAppLoaded(true);
|
||
return;
|
||
}
|
||
|
||
//handleEditApp(urlParams.get("id"))
|
||
|
||
// THIS has to stay due to ID may not exist as normal app yet
|
||
fetch(globalUrl + "/api/v1/get_openapi/" + urlParams.get("id"), {
|
||
method: "GET",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Accept: "application/json",
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
throw new Error("NOT 200 :O");
|
||
}
|
||
|
||
return response.json();
|
||
})
|
||
.then((responseJson) => {
|
||
setIsAppLoaded(true);
|
||
if (!responseJson.success) {
|
||
toast("Failed to get app config. Do you have access?");
|
||
} else {
|
||
parseIncomingOpenapiData(responseJson);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.log("Error: ", error.toString());
|
||
toast(error.toString());
|
||
});
|
||
};
|
||
|
||
const setFileFromb64 = () => {
|
||
//const img = document.getElementById('logo')
|
||
//var canvas = document.createElement('canvas')
|
||
//var ctx = canvas.getContext('2d')
|
||
//img.onload = function() {
|
||
// console.log("LOADED?")
|
||
// ctx.drawImage(img, 0, 0)
|
||
// const canvasUrl = canvas.toDataURL()
|
||
// console.log(canvasUrl)
|
||
// setFileBase64(canvasUrl)
|
||
//}
|
||
};
|
||
|
||
const handleGetRef = (parameter, data) => {
|
||
try {
|
||
if (parameter === null || parameter["$ref"] === undefined) {
|
||
//console.log("$ref not found in getref for: ", parameter)
|
||
return parameter;
|
||
}
|
||
} catch (e) {
|
||
console.log("Failed getting $ref of ", parameter);
|
||
return parameter;
|
||
}
|
||
|
||
const paramsplit = parameter["$ref"].split("/");
|
||
if (paramsplit[0] !== "#") {
|
||
console.log("Bad param: ", paramsplit);
|
||
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;
|
||
};
|
||
|
||
|
||
|
||
// Sets the data up as it should be at later points
|
||
// This is the data FROM the database, not what's being saved
|
||
const parseIncomingOpenapiData = (data) => {
|
||
|
||
var parsedDecoded = ""
|
||
try {
|
||
const decoded = base64_decode(data.openapi)
|
||
parsedDecoded = decoded
|
||
} catch (e) {
|
||
console.log("Failed JSON parsing: ", e)
|
||
parsedDecoded = data
|
||
}
|
||
|
||
if (data.openapi === null) {
|
||
toast("Failed to load OpenAPI for app. Please contact support if this persists.")
|
||
setIsAppLoaded(true);
|
||
return
|
||
}
|
||
|
||
//console.log("Decoded: ", parsedDecoded)
|
||
const parsedapp =
|
||
data.openapi === undefined || data.openapi === null
|
||
? data
|
||
: JSON.parse(parsedDecoded);
|
||
|
||
data = parsedapp.body === undefined ? parsedapp : parsedapp.body;
|
||
|
||
var jsonvalid = false;
|
||
var tmpvalue = "";
|
||
try {
|
||
data = JSON.parse(data);
|
||
jsonvalid = true;
|
||
} catch (e) {
|
||
console.log("Error JSON: ", e);
|
||
}
|
||
|
||
if (!jsonvalid) {
|
||
try {
|
||
data = YAML.parse(data);
|
||
jsonvalid = true;
|
||
} catch (e) {
|
||
console.log("Error YAML: ", e);
|
||
}
|
||
}
|
||
|
||
if (!jsonvalid) {
|
||
toast("OpenAPI data is invalid.");
|
||
return;
|
||
}
|
||
|
||
setBasedata(data);
|
||
console.log("Loaded Info: ", data)
|
||
|
||
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)
|
||
}
|
||
|
||
if (data.info.title.length > 29) {
|
||
setName(data.info.title.slice(0, 29));
|
||
} else {
|
||
// Check if fork=true in URL
|
||
if (window.location.search.includes("fork=true")) {
|
||
setName(data.info.title + " Fork");
|
||
} else {
|
||
setName(data.info.title)
|
||
}
|
||
}
|
||
}
|
||
|
||
setDescription(data.info.description);
|
||
document.title = "Apps - " + data.info.title;
|
||
|
||
if (data.info["x-logo"] !== undefined) {
|
||
if (data.info["x-logo"].url !== undefined) {
|
||
//console.log("PARSED LOGO: ", data.info["x-logo"].url);
|
||
setFileBase64(data.info["x-logo"].url);
|
||
} else {
|
||
setFileBase64(data.info["x-logo"]);
|
||
}
|
||
//console.log("");
|
||
//console.log("");
|
||
//console.log("LOGO: ", data.info["x-logo"]);
|
||
//console.log("");
|
||
//console.log("");
|
||
}
|
||
|
||
if (data.info.contact !== undefined) {
|
||
setContact(data.info.contact);
|
||
}
|
||
|
||
if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) {
|
||
if (typeof data.info["x-categories"] === "array") {
|
||
} else {
|
||
}
|
||
setNewWorkflowCategories(data.info["x-categories"]);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log("Failed setting info: ", e)
|
||
}
|
||
|
||
console.log("Tags: ", data.tags)
|
||
try {
|
||
if (data.tags !== undefined && data.tags.length > 0) {
|
||
var newtags = [];
|
||
for (let tagkey in data.tags) {
|
||
if (data.tags[tagkey].name.length > 50) {
|
||
console.log("Skipping tag because it's too long: ",data.tags[tagkey].name.length);
|
||
|
||
continue;
|
||
}
|
||
|
||
newtags.push(data.tags[tagkey].name);
|
||
}
|
||
|
||
if (newtags.length > 10) {
|
||
newtags = newtags.slice(0, 9);
|
||
}
|
||
|
||
setNewWorkflowTags(newtags);
|
||
}
|
||
} catch (e) {
|
||
console.log("Failed to parse tags: ", e)
|
||
}
|
||
|
||
// This is annoying (:
|
||
console.log("Security schemes 1: ", data.securitySchemes)
|
||
|
||
// 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) {
|
||
console.log("Failed to parse security schemes: ", e)
|
||
}
|
||
|
||
console.log("Security schemes 2: ", securitySchemes)
|
||
|
||
const allowedfunctions = [
|
||
"GET",
|
||
"CONNECT",
|
||
"HEAD",
|
||
"DELETE",
|
||
"POST",
|
||
"PATCH",
|
||
"PUT",
|
||
];
|
||
|
||
|
||
var newActions = [];
|
||
var wordlist = {};
|
||
var all_categories = [];
|
||
var parentUrl = ""
|
||
|
||
console.log("Paths: ", data.paths)
|
||
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) {
|
||
toast("Skipped method (null)" + method);
|
||
continue;
|
||
}
|
||
|
||
if (!allowedfunctions.includes(method.toUpperCase())) {
|
||
// Typical YAML issue
|
||
if (method !== "parameters") {
|
||
console.log("Invalid method: ", method, "data: ", methodvalue);
|
||
//toast("Skipped method (not allowed): " + method);
|
||
}
|
||
continue;
|
||
}
|
||
|
||
//console.log("METHOD: ", methodvalue)
|
||
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) {
|
||
const labels = typeof methodvalue["x-label"] === 'object' || Array.isArray(methodvalue["x-label"]) ? methodvalue["x-label"] : (typeof methodvalue["x-label"] === 'string' ? methodvalue["x-label"].split(",") : [])
|
||
|
||
var correctlabels = []
|
||
console.log("LABEL1: ", methodvalue["x-label"])
|
||
for (let labelkey in labels) {
|
||
var label = labels[labelkey]?.trim()
|
||
if (label?.toLowerCase() === "no label") {
|
||
continue
|
||
}
|
||
|
||
// Remove quotes and escapes
|
||
label = label?.replace(/['"]+/g, '')
|
||
label = label?.replace(/\\/g, '')
|
||
|
||
// Uppercase first
|
||
if (label === undefined || label === null || label?.length === 0 || label?.toLowerCase().replaceAll(" ", "_") === "no_label") {
|
||
label = "No Label"
|
||
}
|
||
|
||
label = label?.replace("_", " ")
|
||
label = label?.charAt(0).toUpperCase() + label?.slice(1)
|
||
|
||
correctlabels?.push(label)
|
||
}
|
||
|
||
console.log("LABEL: ", correctlabels)
|
||
|
||
// FIX: Map labels only if they're actually in the category list
|
||
//newaction.action_label = methodvalue["x-label"]
|
||
newaction.action_label = correctlabels
|
||
}
|
||
|
||
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
|
||
const newurl = newaction.url.replaceAll(new RegExp(regex, 'g'), "")
|
||
newaction.url = newurl
|
||
}
|
||
|
||
// Finding category
|
||
if (path.includes("/")) {
|
||
const pathsplit = path.split("/");
|
||
var categoryindex = -1;
|
||
// 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") {
|
||
//console.log("FILE DOWNLOAD Method: ", path, method, methodvalue)
|
||
}
|
||
|
||
|
||
// Typescript? I think not ;)
|
||
if (methodvalue["requestBody"] !== undefined) {
|
||
if (methodvalue["requestBody"]["$ref"] !== undefined && methodvalue["requestBody"]["$ref"] !== null) {
|
||
// Handle ref
|
||
//
|
||
console.log("Ref: ", methodvalue["requestBody"]["$ref"])
|
||
const parameter = handleGetRef({ $ref: methodvalue["requestBody"]["$ref"]}, data);
|
||
console.log("PARAM: ", parameter)
|
||
if (parameter.content !== undefined && parameter.content !== null) {
|
||
methodvalue["requestBody"]["content"] = parameter.content
|
||
console.log("Set content!")
|
||
}
|
||
}
|
||
|
||
if (methodvalue["requestBody"]["content"] !== undefined) {
|
||
// Handle content - XML or JSON
|
||
//
|
||
if (
|
||
methodvalue["requestBody"]["content"]["application/json"] !==
|
||
undefined
|
||
) {
|
||
//newaction["headers"] = ""
|
||
//"Content-Type=application/json\nAccept=application/json";
|
||
if (
|
||
methodvalue["requestBody"]["content"]["application/json"]["schema"] !== undefined && methodvalue["requestBody"]["content"]["application/json"]["schema"] !== null
|
||
) {
|
||
//console.log("Schema: ", methodvalue["requestBody"]["content"]["application/json"]["schema"])
|
||
|
||
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"])
|
||
//console.log("JSON OBJECT: ", jsonObject)
|
||
if (jsonObject !== undefined && jsonObject !== null) {
|
||
try {
|
||
newaction["body"] = JSON.stringify(jsonObject, null, 2)
|
||
} catch (e) {
|
||
console.log("JSON object parse error: ", e)
|
||
}
|
||
}
|
||
|
||
|
||
//newaction["body"] = JSON.stringify(jsonObject, null, 2);
|
||
|
||
var tmpobject = {};
|
||
for (let prop of methodvalue["requestBody"]["content"]["application/json"]["schema"]["properties"]) {
|
||
tmpobject[prop] = `\$\{${prop}\}`;
|
||
}
|
||
|
||
//console.log("Data: ", data)
|
||
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 = {};
|
||
// Can handle default, required, description and type
|
||
for (let propkey in retRef.properties) {
|
||
console.log("replace: ", propkey)
|
||
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||
newbody[parsedkey] = "${" + parsedkey + "}";
|
||
}
|
||
|
||
newaction["body"] = JSON.stringify(newbody, null, 2);
|
||
}
|
||
} catch (e) {
|
||
console.log("RequestBody json error: ", e, path)
|
||
}
|
||
}
|
||
} else if (
|
||
methodvalue["requestBody"]["content"]["application/xml"] !==
|
||
undefined
|
||
) {
|
||
console.log("METHOD XML: ", methodvalue);
|
||
//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
|
||
) {
|
||
var tmpobject = {};
|
||
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}\}`;
|
||
}
|
||
|
||
//console.log("OBJ XML: ", tmpobject)
|
||
//newaction["body"] = XML.stringify(tmpobject, null, 2)
|
||
}
|
||
} catch (e) {
|
||
console.log("RequestBody xml error: ", e, path)
|
||
}
|
||
}
|
||
} 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) {
|
||
//console.log("FIELDNAME: ", fieldname);
|
||
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")) {
|
||
console.log("Found subkey field for file: ", path, method, methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"])
|
||
newaction.file_field = subkey
|
||
break
|
||
}
|
||
}
|
||
|
||
if (newaction.file_field === undefined || newaction.file_field === null || newaction.file_field.length === 0) {
|
||
console.log("No file fieldname found: ", methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"]["properties"])
|
||
}
|
||
}
|
||
} else {
|
||
console.log("No type found: ", methodvalue["requestBody"]["content"]["multipart/form-data"]["schema"])
|
||
}
|
||
} catch (e) {
|
||
console.log("Multipart/form error: ", e, path)
|
||
}
|
||
}
|
||
} else {
|
||
var schemas = [];
|
||
const content = methodvalue["requestBody"]["content"];
|
||
if (content !== undefined && content !== null) {
|
||
//console.log("CONTENT: ", content)
|
||
for (const [subkey, subvalue] of Object.entries(content)) {
|
||
if (subvalue["schema"] !== undefined && subvalue["schema"] !== null) {
|
||
console.log("SCHEMA: ", subvalue["schema"])
|
||
if (subvalue["schema"]["$ref"] !== undefined && subvalue["schema"]["$ref"] !== null) {
|
||
|
||
console.log("SCHEMA FOUND REF!")
|
||
if (!schemas.includes(subvalue["schema"]["$ref"])) {
|
||
schemas.push(subvalue["schema"]["$ref"]);
|
||
}
|
||
}
|
||
} else {
|
||
if (subvalue["example"] !== undefined && subvalue["example"] !== null) {
|
||
newaction["body"] = subvalue["example"]
|
||
} else {
|
||
console.log("ERROR: couldn't find schema for ", subvalue, method, path);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
try {
|
||
if (schemas.length === 1) {
|
||
const parameter = handleGetRef({ $ref: schemas[0] }, data);
|
||
|
||
console.log("Reading type from parameter: ", parameter)
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
|
||
var newbody = {};
|
||
for (let propkey in parameter.properties) {
|
||
console.log("propkey2: ", propkey)
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||
if (parameter.properties[propkey].type === undefined) {
|
||
console.log(
|
||
"Skipping (4): ",
|
||
parameter.properties[propkey]
|
||
);
|
||
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 {
|
||
console.log(
|
||
"CANT HANDLE JSON TYPE (4)",
|
||
parameter.properties[propkey].type,
|
||
parameter.properties[propkey],
|
||
path
|
||
);
|
||
newbody[parsedkey] = [];
|
||
}
|
||
}
|
||
|
||
newaction["body"] = JSON.stringify(newbody, null, 2);
|
||
} else {
|
||
console.log(
|
||
"CANT HANDLE PARAM: (4) ",
|
||
parameter.properties,
|
||
path
|
||
);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.log("Param Error: ", e, path)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
) {
|
||
//console.log("JSON Output: ", selectedExample["content"]["application/json"]["schema"])
|
||
|
||
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) {
|
||
console.log("JSON object output parse error: ", e)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (selectedExample["content"]["application/json"]["schema"]["$ref"] !== undefined) {
|
||
//console.log("REF EXAMPLE: ", selectedExample["content"]["application/json"]["schema"])
|
||
const parameter = handleGetRef(
|
||
selectedExample["content"]["application/json"][
|
||
"schema"
|
||
],
|
||
data
|
||
);
|
||
|
||
//console.log("Reading parameter type 2", parameter)
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {};
|
||
for (let propkey in parameter.properties) {
|
||
//console.log("propkey3: ", propkey)
|
||
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||
if (parameter.properties[propkey].type === undefined) {
|
||
console.log(
|
||
"Skipping (1): ",
|
||
parameter.properties[propkey]
|
||
);
|
||
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"
|
||
)
|
||
) {
|
||
//console.log("Added empty array. Base is: ", parameter.properties[propkey].type)
|
||
|
||
//const parameter = handleGetRef(selectedExample["content"]["application/json"]["schema"], data)
|
||
newbody[parsedkey] = [];
|
||
} else {
|
||
console.log("CANT HANDLE JSON TYPE ", parameter.properties[propkey].type,parameter.properties[propkey]
|
||
);
|
||
newbody[parsedkey] = [];
|
||
}
|
||
}
|
||
newaction.example_response = JSON.stringify(
|
||
newbody,
|
||
null,
|
||
2
|
||
);
|
||
} else {
|
||
console.log(
|
||
"CANT HANDLE PARAM: (1) ",
|
||
parameter.properties
|
||
);
|
||
}
|
||
} else {
|
||
// Just selecting the first one. bleh.
|
||
if (
|
||
selectedExample["content"]["application/json"][
|
||
"schema"
|
||
]["allOf"] !== undefined
|
||
) {
|
||
//console.log("ALLOF: ", selectedExample["content"]["application/json"]["schema"]["allOf"])
|
||
//console.log("BAD EXAMPLE: (SKIP ALLOF) ", selectedExample["content"]["application/json"]["schema"]["allOf"])
|
||
var selectedComponent =
|
||
selectedExample["content"]["application/json"][
|
||
"schema"
|
||
]["allOf"];
|
||
if (selectedComponent.length >= 1) {
|
||
selectedComponent = selectedComponent[0];
|
||
|
||
const parameter = handleGetRef(
|
||
selectedComponent,
|
||
data
|
||
);
|
||
|
||
console.log("Reading parameter type 3!")
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {};
|
||
for (let propkey in parameter.properties) {
|
||
console.log("propkey4: ", propkey)
|
||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||
if (
|
||
parameter.properties[propkey].type ===
|
||
undefined
|
||
) {
|
||
console.log(
|
||
"Skipping (2): ",
|
||
parameter.properties[propkey]
|
||
);
|
||
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 {
|
||
console.log(
|
||
"CANT HANDLE JSON TYPE (2) ",
|
||
parameter.properties[propkey].type
|
||
);
|
||
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
|
||
console.log(
|
||
"CANT HANDLE PARAM: (3) ",
|
||
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
|
||
);
|
||
|
||
console.log("Reading type 3: ", parameter)
|
||
if (parameter.properties !== undefined && parameter["type"] === "object") {
|
||
var newbody = {};
|
||
for (let propkey in parameter.properties) {
|
||
console.log("propkey5: ", propkey)
|
||
const parsedkey = propkey
|
||
.replaceAll(" ", "_")
|
||
.toLowerCase();
|
||
if (
|
||
parameter.properties[propkey].type ===
|
||
undefined
|
||
) {
|
||
console.log(
|
||
"Skipping (3): ",
|
||
parameter.properties[propkey]
|
||
);
|
||
continue;
|
||
}
|
||
|
||
if (
|
||
parameter.properties[propkey].type ===
|
||
"string"
|
||
) {
|
||
if (
|
||
parameter.properties[propkey]
|
||
.description !== undefined
|
||
) {
|
||
newbody[parsedkey] =
|
||
parameter.properties[propkey].description;
|
||
} else {
|
||
newbody[parsedkey] = "";
|
||
}
|
||
console.log(parameter.properties[propkey]);
|
||
} else if (
|
||
parameter.properties[propkey].type.includes(
|
||
"int"
|
||
)
|
||
) {
|
||
newbody[parsedkey] = 0;
|
||
} else {
|
||
console.log(
|
||
"CANT HANDLE JSON TYPE (3) ",
|
||
parameter.properties[propkey].type
|
||
);
|
||
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
|
||
console.log(
|
||
"CANT HANDLE PARAM: (3) ",
|
||
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 {
|
||
console.log(
|
||
"WARNING: don't know how to handle this param: ",
|
||
parameter
|
||
);
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
//console.log("WORDLIST: ", wordlist)
|
||
|
||
// 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
|
||
);
|
||
|
||
//console.log("SEARCH: ", searchactions);
|
||
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("/")) {
|
||
setBaseUrl(firstUrl.slice(0, firstUrl.length - 1));
|
||
parentUrl = firstUrl.slice(0, firstUrl.length - 1)
|
||
} else {
|
||
setBaseUrl(firstUrl)
|
||
parentUrl = firstUrl
|
||
}
|
||
}
|
||
}
|
||
|
||
if (securitySchemes !== undefined) {
|
||
|
||
//console.log("SECURITY: ", securitySchemes)
|
||
var newauth = [];
|
||
try {
|
||
var optionset = false
|
||
for (const [key, value] of Object.entries(securitySchemes)) {
|
||
//console.log("AUTH: ", key, value);
|
||
|
||
if (key === "jwt") {
|
||
setAuthenticationOption("JWT");
|
||
setAuthenticationRequired(true);
|
||
|
||
if (
|
||
value.in !== undefined &&
|
||
value.in !== null &&
|
||
value.in.length > 0
|
||
) {
|
||
setParameterName(value.in);
|
||
optionset = true
|
||
}
|
||
|
||
} else if (value.scheme === "bearer") {
|
||
setAuthenticationOption("Bearer auth");
|
||
setAuthenticationRequired(true);
|
||
optionset = true
|
||
|
||
} else if (key === "ApiKeyAuth" || key === "Token" || ((value.in === "header" || value.in === "query") && value.name !== undefined)) {
|
||
//if (optionset === false) {
|
||
// optionset = true
|
||
//}
|
||
|
||
if (optionset === false) {
|
||
optionset = true
|
||
value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1)
|
||
|
||
setParameterLocation(value.in);
|
||
if (!apikeySelection.includes(value.in)) {
|
||
console.log("APIKEY SELECT: ", apikeySelection);
|
||
toast("Might be error in setting up API key authentication");
|
||
}
|
||
|
||
console.log("PARAM NAME: ", value.name);
|
||
setAuthenticationOption("API key");
|
||
setParameterName(value.name);
|
||
setAuthenticationRequired(true);
|
||
|
||
newauth.push({
|
||
"name": key,
|
||
"type": value.in.toLowerCase(),
|
||
"in": value.in.toLowerCase(),
|
||
"example": "",
|
||
})
|
||
} else {
|
||
newauth.push({
|
||
"name": key,
|
||
"type": value.in.toLowerCase(),
|
||
"in": value.in.toLowerCase(),
|
||
"example": "",
|
||
})
|
||
}
|
||
|
||
if (value.description !== undefined && value.description !== null && value.description.length > 0) {
|
||
// Don't want a real description - just the ones we're replacing with
|
||
if ((value.description.split(" ").length - 1) <= 2) {
|
||
setRefreshUrl(value.description)
|
||
}
|
||
}
|
||
|
||
} else if (value.scheme === "basic") {
|
||
setAuthenticationOption("Basic auth");
|
||
setAuthenticationRequired(true);
|
||
optionset = true
|
||
|
||
} else if (value.scheme === "oauth2") {
|
||
setAuthenticationOption("Oauth2");
|
||
setAuthenticationRequired(true);
|
||
optionset = true
|
||
|
||
} else if (value.type === "oauth2" || key === "Oauth2" || key === "Oauth2c" || (key !== undefined && key !== null && key.toLowerCase().includes("oauth2"))) {
|
||
//toast("Can't handle Oauth2 auth yet.")
|
||
setAuthenticationOption("Oauth2");
|
||
setAuthenticationRequired(true);
|
||
optionset = true
|
||
|
||
//console.log("FLOW-1: ", value)
|
||
const flowkey = value.flow === undefined ? "flows" : "flow";
|
||
//console.log("FLOW: ", value[flowkey])
|
||
|
||
|
||
// Doesn't seem to be used for now
|
||
const basekey = value[flowkey].authorizationCode !== undefined ? "authorizationCode" : "implicit";
|
||
|
||
// Kind of fucked up, but it works for now?
|
||
if (value["x-grant-type"] !== undefined && value["x-grant-type"] !== null && value["x-grant-type"].length !== 0) {
|
||
setOauth2GrantType(value["x-grant-type"])
|
||
}
|
||
|
||
//console.log("FLOW2: ", value[flowkey][basekey])
|
||
if (value[flowkey] !== undefined && value[flowkey][basekey] !== undefined
|
||
) {
|
||
var newparamname = parameterName
|
||
if (value[flowkey][basekey].authorizationUrl !== undefined && value[flowkey][basekey].authorizationUrl !== null && value[flowkey][basekey].authorizationUrl.length !== 0 && parameterName.length === 0) {
|
||
setParameterName(value[flowkey][basekey].authorizationUrl);
|
||
} else {
|
||
setOauth2Type("application")
|
||
|
||
}
|
||
|
||
var tokenUrl = "";
|
||
if (value[flowkey][basekey].tokenUrl !== undefined) {
|
||
setParameterLocation(value[flowkey][basekey].tokenUrl);
|
||
tokenUrl = value[flowkey][basekey].tokenUrl;
|
||
} else {
|
||
setParameterLocation("");
|
||
}
|
||
|
||
if (value[flowkey][basekey].refreshUrl !== undefined) {
|
||
setRefreshUrl(value[flowkey][basekey].refreshUrl);
|
||
} else if (tokenUrl.length > 0) {
|
||
setRefreshUrl(tokenUrl);
|
||
}
|
||
|
||
if (
|
||
value[flowkey][basekey].scopes !== undefined &&
|
||
value[flowkey][basekey].scopes !== null
|
||
) {
|
||
if (value[flowkey][basekey].scopes.length > 0) {
|
||
setOauth2Scopes(value[flowkey][basekey].scopes);
|
||
} else {
|
||
var newscopes = [];
|
||
for (let [scopekey, scopevalue] of Object.entries(value[flowkey][basekey].scopes)) {
|
||
if (scopekey.startsWith("http")) {
|
||
const scopekeysplit = scopekey.split("/");
|
||
if (scopekeysplit.length < 5) {
|
||
console.log("Skipping scope: ", scopekey);
|
||
toast("Skipping scope: " + scopekey);
|
||
continue;
|
||
}
|
||
|
||
//console.log("Checking scope for: ", scopekey, scopekeysplit.length)
|
||
}
|
||
|
||
newscopes.push(scopekey);
|
||
}
|
||
|
||
setOauth2Scopes(newscopes);
|
||
}
|
||
}
|
||
} else {
|
||
console.log(
|
||
"Bad flowkey and basekey for oauth2: ",
|
||
flowkey,
|
||
basekey
|
||
);
|
||
}
|
||
} else {
|
||
toast("Couldn't handle AUTH type: ", key);
|
||
//newauth.push({
|
||
// "name": key,
|
||
// "type": value.in,
|
||
// "example": "",
|
||
//})
|
||
}
|
||
}
|
||
} catch (e) {
|
||
toast("Failed to handle auth")
|
||
console.log("Error: ", e)
|
||
}
|
||
|
||
if (newauth.length > 0) {
|
||
newauth = newauth.filter(data => data.name != "ApiKeyAuth")
|
||
setExtraAuth(newauth);
|
||
}
|
||
}
|
||
|
||
console.log("PARent: ", parentUrl)
|
||
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("/")
|
||
}
|
||
|
||
console.log("Prefix: ", prefixCheck)
|
||
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)
|
||
}
|
||
|
||
console.log("Action: ", newActions[actionKey].url)
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log("Actions: ", newActions.length, " BaseURL: ", 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 {
|
||
//console.log("NOT skipping duplicate action: ", action.url, ". Should merge contents")
|
||
newActions2.push(action)
|
||
}
|
||
}
|
||
|
||
//console.log("Actions: ", newActions.length, " Actions2: ", newActions2.length)
|
||
newActions = newActions2
|
||
if (newActions.length > increaseAmount - 1) {
|
||
setActionAmount(increaseAmount);
|
||
} else {
|
||
setActionAmount(newActions.length);
|
||
}
|
||
|
||
//const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
|
||
if (newActions.length > 1000 && isCloud) {
|
||
toast("Cut down actions from " + newActions.length + " to 999 because of limit");
|
||
newActions = newActions.slice(0, 999);
|
||
}
|
||
|
||
setProjectCategories(all_categories);
|
||
|
||
// Rearrange them by which has action_label
|
||
const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label[0] !== "No Label")
|
||
console.log("First actions: ", firstActions)
|
||
const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label[0] === "No Label")
|
||
newActions = firstActions.concat(secondActions)
|
||
setActions(newActions);
|
||
//data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label
|
||
|
||
|
||
setFilteredActions(newActions);
|
||
setIsAppLoaded(true);
|
||
};
|
||
|
||
// Saving the app that's been configured.
|
||
// Save SAVE app
|
||
const submitApp = () => {
|
||
toast.info("Uploading and building app " + name + ". This may take a minute or two.");
|
||
setAppBuilding(true);
|
||
setErrorCode("");
|
||
|
||
// Format the information
|
||
const splitBase = baseUrl.split("/");
|
||
const host = splitBase[2];
|
||
const schemes = [splitBase[0]];
|
||
const basePath = "/" + splitBase.slice(3).join("/");
|
||
|
||
const newBaseUrl = baseUrl.replaceAll("http//", "http://").replaceAll("https//", "https://")
|
||
|
||
const data = {
|
||
openapi: "3.0.0",
|
||
info: {
|
||
title: name,
|
||
description: description,
|
||
version: "1.0",
|
||
"x-logo": fileBase64,
|
||
},
|
||
servers: [{ url: newBaseUrl }],
|
||
host: host,
|
||
basePath: basePath,
|
||
schemes: schemes,
|
||
paths: {},
|
||
editing: isEditing,
|
||
components: {
|
||
securitySchemes: {},
|
||
},
|
||
id: props.match.params.appid,
|
||
};
|
||
|
||
if (isEditing === false) {
|
||
var urlParams = new URLSearchParams(window.location.search);
|
||
if (urlParams !== undefined && urlParams !== null && urlParams.has("id")) {
|
||
data.id = urlParams.get("id")
|
||
}
|
||
|
||
//id: props.match.params.appid,
|
||
}
|
||
|
||
if (basedata.info !== undefined && basedata.info.contact !== undefined) {
|
||
data.info["contact"] = basedata.info.contact;
|
||
} else if (contact === "") {
|
||
data.info["contact"] = {
|
||
name: "@Anonymous Shuffle User",
|
||
url: "https://twitter.com/shuffleio",
|
||
email: "support@shuffler.io",
|
||
};
|
||
} else {
|
||
data.info["contact"] = contact;
|
||
}
|
||
|
||
if (newWorkflowTags.length > 0) {
|
||
var newtags = [];
|
||
for (let tagkey in newWorkflowTags) {
|
||
newtags.push({ name: newWorkflowTags[tagkey] });
|
||
}
|
||
|
||
data["tags"] = newtags;
|
||
}
|
||
|
||
if (newWorkflowCategories.length > 0) {
|
||
data["info"]["x-categories"] = newWorkflowCategories;
|
||
}
|
||
|
||
// Handles actions
|
||
var handledPaths = []
|
||
for (let actionkey in actions) {
|
||
var item = JSON.parse(JSON.stringify(actions[actionkey]))
|
||
if (item.errors.length > 0) {
|
||
//toast("Saving with error in action " + item.name);
|
||
}
|
||
|
||
if (item.name === undefined && item.description !== undefined) {
|
||
item.name = item.description;
|
||
}
|
||
|
||
|
||
// Basic way to allow multiple of the same path
|
||
var pathjoin = item.url+"_"+item.method.toLowerCase()
|
||
if (handledPaths.includes(pathjoin)) {
|
||
|
||
// Max 1000 of same. Will it be ok for graphql longterm?
|
||
const baseurl = item.url
|
||
for (let i = 0; i < 1000; i++) {
|
||
item.url = baseurl+"_shuffle_replace_"+i
|
||
|
||
pathjoin = item.url+"_"+item.method.toLowerCase()
|
||
if (handledPaths.includes(pathjoin)) {
|
||
continue
|
||
}
|
||
|
||
break
|
||
}
|
||
}
|
||
|
||
handledPaths.push(pathjoin)
|
||
|
||
if (data.paths[item.url] === null || data.paths[item.url] === undefined) {
|
||
data.paths[item.url] = {};
|
||
}
|
||
|
||
const regex = /[A-Za-z0-9 _]/g;
|
||
if (item.name === undefined) {
|
||
console.log("Skipping action ", item);
|
||
continue;
|
||
}
|
||
|
||
const found = item.name.match(regex);
|
||
if (found !== null) {
|
||
item.name = found.join("");
|
||
}
|
||
|
||
// Workaround for proper responses. No default as JSON for now
|
||
data.paths[item.url][item.method.toLowerCase()] = {
|
||
responses: {
|
||
default: {
|
||
description: "default",
|
||
content: {
|
||
"text/plain": {
|
||
schema: {
|
||
type: "string",
|
||
example: "",
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
summary: item.name,
|
||
operationId: item.name.split(" ").join("_"),
|
||
description: item.description,
|
||
parameters: [],
|
||
requestBody: {
|
||
content: {},
|
||
},
|
||
};
|
||
|
||
if (item.action_label !== undefined && item.action_label !== "" && item.action_label[0] !== "No Label") {
|
||
data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label
|
||
}
|
||
|
||
if (item.required_bodyfields !== undefined && item.required_bodyfields !== null && item.required_bodyfields.length > 0) {
|
||
console.log("Required bodyfields: ", item.required_bodyfields)
|
||
data.paths[item.url][item.method.toLowerCase()]["x-required-fields"] = item.required_bodyfields
|
||
}
|
||
|
||
//console.log("ACTION: ", item)
|
||
|
||
if (item.example_response !== undefined && item.example_response !== null && item.example_response.length > 0) {
|
||
|
||
if (item["example_response"] === "shuffle_file_download") {
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.type = "string"
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"]["content"]["text/plain"].schema.format = "binary"
|
||
|
||
/*
|
||
schema:
|
||
type: object
|
||
properties:
|
||
username:
|
||
type: string
|
||
avatar: # <-- image embedded into JSON
|
||
type: string
|
||
format: byte
|
||
description: Base64-encoded contents of the avatar image
|
||
*/
|
||
|
||
} else {
|
||
// FIXME: Shallow copy of the string
|
||
var showResult = Object.assign("", item.example_response).trim();
|
||
showResult = showResult.split(" None").join(' "None"');
|
||
showResult = showResult.split("'").join('"');
|
||
showResult = showResult.split(" False").join(" false");
|
||
showResult = showResult.split(" True").join(" true");
|
||
|
||
var jsonvalid = true;
|
||
try {
|
||
const tmp = String(JSON.parse(showResult));
|
||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||
jsonvalid = false;
|
||
}
|
||
} catch (e) {
|
||
jsonvalid = false;
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"][
|
||
"content"
|
||
]["text/plain"].schema.type = "string";
|
||
if (jsonvalid) {
|
||
// FIXME: Add a JSON parser here - don't run it as a string.
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"][
|
||
"content"
|
||
]["text/plain"].schema.example = showResult;
|
||
} else {
|
||
data.paths[item.url][item.method.toLowerCase()].responses["default"][
|
||
"content"
|
||
]["text/plain"].schema.example = item.example_response;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (item.queries.length > 0) {
|
||
var skipped = false;
|
||
var querynames = []
|
||
for (let querykey in item.queries) {
|
||
let queryitem = item.queries[querykey];
|
||
|
||
if (queryitem === undefined || queryitem === null || queryitem.name === undefined || queryitem.name === null || queryitem.name === "") {
|
||
continue
|
||
}
|
||
|
||
// A fix for duplicate items
|
||
if (querynames.includes(queryitem.name.toLowerCase())) {
|
||
continue
|
||
}
|
||
|
||
querynames.push(queryitem.name.toLowerCase())
|
||
if (queryitem.name.toLowerCase() == "url") {
|
||
console.log(item.name + " uses a bad query: url");
|
||
continue;
|
||
//skipped = true
|
||
//break
|
||
}
|
||
|
||
if (queryitem.name.toLowerCase() == "file_id") {
|
||
item.queries[querykey].name = "fileid"
|
||
continue;
|
||
//skipped = true
|
||
//break
|
||
}
|
||
|
||
if (
|
||
queryitem.name.toLowerCase() == "url" ||
|
||
queryitem.name.toLowerCase() == "body" ||
|
||
queryitem.name.toLowerCase() == "self" ||
|
||
queryitem.name.toLowerCase() == "query" ||
|
||
queryitem.name.toLowerCase() == "ssl_verify" ||
|
||
queryitem.name.toLowerCase() == "queries" ||
|
||
queryitem.name.toLowerCase() == "headers" ||
|
||
queryitem.name.toLowerCase() == "list" ||
|
||
queryitem.name.toLowerCase() == "dict" ||
|
||
queryitem.name.toLowerCase() == "str" ||
|
||
queryitem.name.toLowerCase() == "int" ||
|
||
queryitem.name.toLowerCase() == "access_token") {
|
||
/*
|
||
|
||
queryitem.name.includes("[") ||
|
||
queryitem.name.includes("]") ||
|
||
queryitem.name.includes("{") ||
|
||
queryitem.name.includes("}") ||
|
||
queryitem.name.includes("(") ||
|
||
queryitem.name.includes(")") ||
|
||
queryitem.name.includes("!") ||
|
||
queryitem.name.includes("@") ||
|
||
queryitem.name.includes("#") ||
|
||
queryitem.name.includes("$") ||
|
||
queryitem.name.includes("%") ||
|
||
queryitem.name.includes("^") ||
|
||
queryitem.name.includes("&") ||
|
||
queryitem.name.includes(":") ||
|
||
queryitem.name.includes(";") ||
|
||
queryitem.name.includes("<") ||
|
||
queryitem.name.includes(">") ||
|
||
queryitem.name.includes('"') ||
|
||
queryitem.name.includes("'")
|
||
) {
|
||
*/
|
||
console.log(item.name + " error: uses a bad query - not adding: ",queryitem.name)
|
||
|
||
|
||
// Find a replacement for the invalid ones first.
|
||
|
||
continue;
|
||
}
|
||
|
||
var newitem = {
|
||
in: "query",
|
||
name: queryitem.name,
|
||
description: "Generated by shuffler.io OpenAPI",
|
||
required: queryitem.required,
|
||
schema: {
|
||
type: "string",
|
||
},
|
||
};
|
||
|
||
if (queryitem.example !== undefined) {
|
||
newitem.example = queryitem.example
|
||
}
|
||
|
||
if (queryitem.description !== undefined) {
|
||
newitem.description = queryitem.description;
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||
newitem
|
||
);
|
||
//console.log(queryitem)
|
||
}
|
||
|
||
// Bad code as it doesn't allow for "anything".
|
||
if (skipped) {
|
||
toast(
|
||
"Bad configuration of " +
|
||
item.name +
|
||
". Skipping because queries are invalid."
|
||
);
|
||
continue;
|
||
}
|
||
}
|
||
//data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem)
|
||
|
||
if (item.paths.length > 0) {
|
||
for (let querykey in item.paths) {
|
||
let queryitem = item.paths[querykey];
|
||
|
||
if (queryitem.toLowerCase() == "url") {
|
||
queryitem = "action_url";
|
||
}
|
||
|
||
if (queryitem.toLowerCase() == "apikey") {
|
||
queryitem = "action_apikey";
|
||
}
|
||
|
||
newitem = {
|
||
in: "path",
|
||
name: queryitem,
|
||
description: "Generated by shuffler.io OpenAPI",
|
||
required: true,
|
||
schema: {
|
||
type: "string",
|
||
},
|
||
};
|
||
|
||
if (queryitem.description !== undefined) {
|
||
newitem.description = queryitem.description;
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem);
|
||
//console.log(queryitem)
|
||
}
|
||
} else {
|
||
// Always goes here if they didn't click anything :/
|
||
const values = getCurrentPaths(item.url);
|
||
const paths = values[0];
|
||
|
||
for (let querykey in paths) {
|
||
let queryitem = paths[querykey];
|
||
newitem = {
|
||
in: "path",
|
||
name: queryitem,
|
||
description: "Generated by shuffler.io OpenAPI",
|
||
required: false,
|
||
schema: {
|
||
type: "string",
|
||
},
|
||
};
|
||
|
||
if (queryitem.description !== undefined) {
|
||
newitem.description = queryitem.description;
|
||
}
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||
newitem
|
||
);
|
||
//console.log(queryitem)
|
||
}
|
||
}
|
||
|
||
const methodname = item.method.toLowerCase()
|
||
if (methodname === "post" || methodname === "put" || methodname === "patch" || methodname === "delete") {
|
||
if (
|
||
item.body !== undefined &&
|
||
item.body !== null &&
|
||
item.body.length > 0
|
||
) {
|
||
//console.log("GOT BODY: ", item.url, item.method, item.body)
|
||
|
||
// Replacing dollarsign insertions that aren't escaped
|
||
// This is to stop it from messing with systems in Shuffle.
|
||
// This MAY cause it to be a little weird in other systems however,
|
||
// but it's the only way we can properly support e.g. GraphQL
|
||
// with good examples
|
||
var newbody = ""
|
||
for (let bodykey in item.body) {
|
||
if (item.body[bodykey] === "$") {
|
||
if (bodykey > 0) {
|
||
|
||
const newkey = parseInt(bodykey, 10)
|
||
if (item.body[newkey-1] !== "\\") {
|
||
if (item.body[newkey+1] !== "\{") {
|
||
newbody += "\\"
|
||
}
|
||
}
|
||
|
||
newbody += item.body[bodykey]
|
||
} else {
|
||
newbody += "\\"
|
||
newbody += item.body[bodykey]
|
||
}
|
||
|
||
} else {
|
||
newbody += item.body[bodykey]
|
||
}
|
||
}
|
||
|
||
//console.log("New body: ", newbody)
|
||
if (newbody !== item.body) {
|
||
item.body = newbody
|
||
}
|
||
|
||
//var pathjoin = item.url+"_"+item.method.toLowerCase()
|
||
|
||
const required = false;
|
||
newitem = {
|
||
in: "body",
|
||
name: "body",
|
||
multiline: true,
|
||
description: "Generated by shuffler.io OpenAPI",
|
||
required: required,
|
||
example: item.body,
|
||
schema: {
|
||
type: "string",
|
||
},
|
||
};
|
||
|
||
// FIXME - add application/json if JSON example?
|
||
data.paths[item.url][item.method.toLowerCase()]["requestBody"] = {
|
||
description: "Generated by Shuffler.io",
|
||
required: required,
|
||
content: {
|
||
example: {
|
||
example: item.body,
|
||
},
|
||
},
|
||
};
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||
newitem
|
||
);
|
||
} else if (actionBodyRequest.includes(item.method.toUpperCase())) {
|
||
// Appending an empty field
|
||
const required = false;
|
||
newitem = {
|
||
in: "body",
|
||
name: "body",
|
||
multiline: true,
|
||
description: "Generated by shuffler.io OpenAPI",
|
||
required: required,
|
||
example: "",
|
||
schema: {
|
||
type: "string",
|
||
},
|
||
};
|
||
|
||
// FIXME - add application/json if JSON example?
|
||
data.paths[item.url][item.method.toLowerCase()]["requestBody"] = {
|
||
description: "Generated by Shuffler.io",
|
||
required: required,
|
||
content: {
|
||
example: {
|
||
example: "",
|
||
},
|
||
},
|
||
};
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||
newitem
|
||
);
|
||
} else {
|
||
//console.log("Nothing to append?")
|
||
}
|
||
}
|
||
|
||
// https://swagger.io/docs/specification/describing-request-body/file-upload/
|
||
if (item.file_field !== undefined && item.file_field !== null && item.file_field.length > 0) {
|
||
data.paths[item.url][item.method.toLowerCase()]["requestBody"][
|
||
"content"
|
||
]["multipart/form-data"] = {
|
||
schema: {
|
||
type: "object",
|
||
properties: {
|
||
fieldname: {
|
||
type: "string",
|
||
value: item.file_field,
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
//console.log(data.paths[item.url][item.method.toLowerCase()]["requestBody"]["content"]["multipart/form-data"])
|
||
}
|
||
|
||
if (item.headers.length > 0) {
|
||
const required = false;
|
||
|
||
const headersSplit = item.headers.split("\n");
|
||
for (let headerkey in headersSplit) {
|
||
const header = headersSplit[headerkey];
|
||
|
||
var innerkey = ""
|
||
var value = "";
|
||
if (header.length > 0 && header.includes("= ")) {
|
||
const headersplit = header.split("= ");
|
||
innerkey = headersplit[0];
|
||
value = headersplit[1];
|
||
} else if (header.length > 0 && header.includes(" =")) {
|
||
const headersplit = header.split(" =");
|
||
innerkey = headersplit[0];
|
||
value = headersplit[1];
|
||
} else if (header.length > 0 && header.includes("=")) {
|
||
const headersplit = header.split("=");
|
||
innerkey = headersplit[0];
|
||
value = headersplit[1];
|
||
} else if (header.length > 0 && header.includes(": ")) {
|
||
const headersplit = header.split(": ");
|
||
innerkey = headersplit[0];
|
||
value = headersplit[1];
|
||
} else if (header.length > 0 && header.includes(" :")) {
|
||
const headersplit = header.split(" :");
|
||
innerkey = headersplit[0];
|
||
value = headersplit[1];
|
||
} else if (header.length > 0 && header.includes(":")) {
|
||
const headersplit = header.split(":");
|
||
innerkey = headersplit[0];
|
||
value = headersplit[1];
|
||
} else {
|
||
continue;
|
||
}
|
||
|
||
if (innerkey.length > 0 && value.length > 0) {
|
||
newitem = {
|
||
in: "header",
|
||
name: innerkey,
|
||
multiline: false,
|
||
description: "Header generated by shuffler.io OpenAPI",
|
||
required: false,
|
||
example: value,
|
||
schema: {
|
||
type: "string",
|
||
},
|
||
};
|
||
|
||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||
newitem
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (authenticationOption === "API key") {
|
||
if (parameterName.length === 0) {
|
||
toast("A field name for the APIkey must be defined");
|
||
setAppBuilding(false);
|
||
return;
|
||
}
|
||
|
||
console.log("Paramname: ", parameterName)
|
||
var newparamName = parameterName.replaceAll('"', "");
|
||
newparamName = newparamName.replaceAll("'", "");
|
||
|
||
data.components.securitySchemes["ApiKeyAuth"] = {
|
||
type: "apiKey",
|
||
in: parameterLocation.toLowerCase(),
|
||
name: newparamName,
|
||
description: refreshUrl,
|
||
}
|
||
|
||
console.log("Full auth component: ", data.components.securitySchemes["ApiKeyAuth"])
|
||
|
||
} else if (authenticationOption === "Bearer auth") {
|
||
data.components.securitySchemes["BearerAuth"] = {
|
||
type: "http",
|
||
scheme: "bearer",
|
||
bearerFormat: "UUID",
|
||
};
|
||
} else if (authenticationOption === "JWT") {
|
||
data.components.securitySchemes["jwt"] = {
|
||
type: "http",
|
||
scheme: "bearer",
|
||
bearerFormat: "JWT",
|
||
in: parameterName,
|
||
};
|
||
|
||
console.log("SECURITYSCHEMES: ", data.components);
|
||
} else if (authenticationOption === "Basic auth") {
|
||
data.components.securitySchemes["BasicAuth"] = {
|
||
type: "http",
|
||
scheme: "basic",
|
||
};
|
||
} else if (authenticationOption === "Oauth2") {
|
||
console.log("oauth2: ", parameterName)
|
||
var newparamName = parameterName.replaceAll('"', "");
|
||
newparamName = newparamName.replaceAll("'", "");
|
||
|
||
// FIXME - this is a hack to get around the fact that the oauth2
|
||
// flow is not properly defined
|
||
if (oauth2Type === "application") {
|
||
newparamName = ""
|
||
}
|
||
|
||
//parameterName, parameterValue, revocationUrl
|
||
data.components.securitySchemes["Oauth2"] = {
|
||
type: "oauth2",
|
||
description: "Oauth2.0 authorizationCode authentication",
|
||
flow: {
|
||
authorizationCode: {
|
||
authorizationUrl: newparamName,
|
||
tokenUrl: parameterLocation,
|
||
refreshUrl: refreshUrl,
|
||
scopes:
|
||
oauth2Scopes === undefined || oauth2Scopes === null
|
||
? []
|
||
: oauth2Scopes,
|
||
},
|
||
},
|
||
};
|
||
|
||
|
||
//if (value[flowkey][basekey]["x-grant-type"] !== undefined && value[flowkey][basekey]["x-grant-type"] !== null && value[flowkey][basekey]["x-grant-type"].length !== 0) {
|
||
if (oauth2GrantType.length > 0) {
|
||
data.components.securitySchemes["Oauth2"]["x-grant-type"] = oauth2GrantType;
|
||
}
|
||
|
||
console.log("SECURITYSCHEMES: ", data.components);
|
||
}
|
||
|
||
if (setExtraAuth.length > 0) {
|
||
const invalidfieldnames = ["apikey", "username", "password", "username_basic", "password_basic", "url", "access_token"]
|
||
for (let authkey in extraAuth) {
|
||
const curauth = extraAuth[authkey];
|
||
if (curauth.name.length === 0 || curauth.name.toLowerCase() == "url") {
|
||
toast("Can't add extra auth with empty name or Name URL");
|
||
setAppBuilding(false);
|
||
return;
|
||
}
|
||
|
||
// Additional comparisonchecks
|
||
if (authenticationOption !== "No authentication" && authenticationOption !== "") {
|
||
if (invalidfieldnames.includes(curauth.name.toLowerCase())) {
|
||
toast.warn("Skipping extra auth field: " + curauth.name + ". This is not valid.")
|
||
continue
|
||
}
|
||
}
|
||
|
||
data.components.securitySchemes[curauth.name] = {
|
||
type: "apiKey",
|
||
in: curauth.type,
|
||
name: curauth.name,
|
||
};
|
||
}
|
||
}
|
||
|
||
setAppDownloadData(JSON.stringify(data, null, 4))
|
||
|
||
fetch(globalUrl + "/api/v1/verify_openapi", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Accept: "application/json",
|
||
},
|
||
body: JSON.stringify(data, null, 4),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
setAppBuilding(false)
|
||
if (response.status === 403) {
|
||
|
||
|
||
var urlParams = new URLSearchParams(window.location.search)
|
||
if (urlParams.has("id")) {
|
||
toast.error(`Please log in to build this app. If this error persists, please contact ${supportEmail}`)
|
||
} else {
|
||
toast.error("Failed to save the app as you are not the owner. Redirecting you to the forking page. If this does not load, please download and re-import the app.", {
|
||
autoClose: 10000
|
||
})
|
||
if (props.match.params.appid !== undefined && props.match.params.appid !== null && props.match.params.appid.length > 0) {
|
||
setTimeout(() => {
|
||
window.open(`/apps/new?id=${props.match.params.appid}`, "_blank")
|
||
}, 2500)
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
if (response.status !== 200) {
|
||
setErrorCode("An error occurred during validation")
|
||
//throw new Error("NOT 200 :O")
|
||
}
|
||
|
||
return response.json();
|
||
})
|
||
.then((responseJson) => {
|
||
if (responseJson?.success !== true) {
|
||
if (responseJson?.extra !== undefined && responseJson?.extra !== null) {
|
||
toast("Failed building: " + responseJson.extra);
|
||
} else if (responseJson?.reason !== undefined) {
|
||
setErrorCode(responseJson.reason);
|
||
|
||
if (responseJson?.details !== undefined && responseJson?.details !== null) {
|
||
toast.error("Failed to build - contact support@shuffler.io:\n\n" + responseJson.details, {
|
||
autoClose: 60000
|
||
})
|
||
} else {
|
||
toast.error("Failed to build: \n\n" + responseJson?.reason, {
|
||
autoClose: 10000
|
||
})
|
||
}
|
||
} else {
|
||
}
|
||
} else {
|
||
toast.success("Successfully built openapi app! Added job to rebuild it in your hybrid runtime locations (Orborus).");
|
||
if (window.location.pathname.includes("/new")) {
|
||
if (responseJson.id !== undefined && responseJson.id !== null) {
|
||
window.location = `/apps/edit/${responseJson.id}`;
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
setAppBuilding(false);
|
||
setErrorCode(error.toString());
|
||
toast.error(error.toString());
|
||
});
|
||
};
|
||
|
||
const bearerAuth =
|
||
authenticationOption === "Bearer auth" ? (
|
||
<div style={{ color: theme.palette.text.primary }}>
|
||
<h4>
|
||
<a
|
||
target="_blank"
|
||
href="https://swagger.io/docs/specification/authentication/bearer-authentication/"
|
||
style={{ textDecoriation: "none", color: theme.palette.linkColor }}
|
||
>
|
||
Bearer auth
|
||
</a>
|
||
</h4>
|
||
Users will be required to submit their API as the header "Authorization:
|
||
Bearer APIKEY"
|
||
</div>
|
||
) : null;
|
||
|
||
// Basicauth
|
||
const basicAuth =
|
||
authenticationOption === "Basic auth" ? (
|
||
<div style={{ color: theme.palette.text.primary }}>
|
||
<h4>
|
||
<a
|
||
target="_blank"
|
||
href="https://swagger.io/docs/specification/authentication/basic-authentication/"
|
||
style={{ textDecoriation: "none", color: theme.palette.linkColor }}
|
||
>
|
||
Basic authentication
|
||
</a>
|
||
</h4>
|
||
Users will be required to submit a valid username and password before
|
||
using the API
|
||
</div>
|
||
) : null;
|
||
|
||
const testAction = (index) => {
|
||
console.log("Should test action at index " + index);
|
||
console.log(actions[index]);
|
||
};
|
||
|
||
|
||
|
||
const duplicateAction = (index) => {
|
||
var newAction = JSON.parse(JSON.stringify(actions[index]));
|
||
newAction.name = newAction.name + "_copy";
|
||
newAction.errors.push("Can't have the same name");
|
||
|
||
actions.push(newAction);
|
||
|
||
if (actions.length > actionAmount) {
|
||
setActionAmount(actions.length);
|
||
}
|
||
|
||
setActions(actions);
|
||
setFilteredActions(actions);
|
||
setUpdate(Math.random());
|
||
};
|
||
|
||
const deleteAction = (index) => {
|
||
actions.splice(index, 1);
|
||
//setCurrentAction({
|
||
// name: "",
|
||
// description: "",
|
||
// url: "",
|
||
// file_field: "",
|
||
// headers: "",
|
||
// paths: [],
|
||
// queries: [],
|
||
// body: "",
|
||
// errors: [],
|
||
// method: actionNonBodyRequest[0],
|
||
//});
|
||
|
||
setActions(actions);
|
||
setFilteredActions(actions);
|
||
setActionAmount(actionAmount - 1);
|
||
setUpdate(Math.random());
|
||
};
|
||
|
||
//console.log("Option: ", authenticationOption)
|
||
//console.log("Location: ", parameterLocation)
|
||
//console.log("Name: ", parameterName)
|
||
//const extraKeys =
|
||
const extraKeys = authenticationOption === "Oauth2" ? null :
|
||
<div style={{ marginTop: 50, marginRight: 25, }}>
|
||
<div style={{ display: "flex" }}>
|
||
<Typography variant="body1">Extra authentication</Typography>
|
||
{extraAuth.length === 0 ? (
|
||
<Button
|
||
color="primary"
|
||
style={{ maxWidth: 50, marginLeft: 15 }}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
console.log("ADD NEW!");
|
||
extraAuth.push(defaultAuth);
|
||
setExtraAuth(extraAuth);
|
||
setUpdate(Math.random());
|
||
}}
|
||
>
|
||
<AddIcon style={{}} />
|
||
</Button>
|
||
) : (
|
||
<span style={{ width: 50 }} />
|
||
)}
|
||
</div>
|
||
|
||
{extraAuth.map((value, index) => {
|
||
return (
|
||
<span
|
||
key={index}
|
||
style={{ display: "flex", height: 50, marginTop: 5 }}
|
||
>
|
||
<TextField
|
||
required
|
||
style={{
|
||
height: 50,
|
||
flex: 2,
|
||
marginTop: 0,
|
||
marginBottom: 0,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
marginRight: 5,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="Name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={extraAuth[index].name}
|
||
helperText={
|
||
<Typography variant="caption" style={{ color: theme.palette.error.main, marginTop: 10, marginBottom: 10, }}>
|
||
{extraAuth[index]?.name?.toLowerCase() === "url"
|
||
|| extraAuth[index]?.name?.toLowerCase() === "apikey"
|
||
? `ERROR: Invalid key: ${extraAuth[index].name}. ` : ""}
|
||
</Typography>
|
||
}
|
||
onChange={(e) => {
|
||
extraAuth[index].name = e.target.value;
|
||
setExtraAuth(extraAuth);
|
||
}}
|
||
onBlur={(e) => {
|
||
// Forcerender
|
||
setUpdate(Math.random());
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
minHeight: 50,
|
||
marginLeft: 5,
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
/>
|
||
<TextField
|
||
required
|
||
style={{
|
||
height: 50,
|
||
marginTop: 0,
|
||
marginBottom: 0,
|
||
flex: 2,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
marginRight: 5,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="Example - input an example for the user"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={extraAuth[index].example}
|
||
onChange={(e) => {
|
||
extraAuth[index].example = e.target.value;
|
||
setExtraAuth(extraAuth);
|
||
}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
minHeight: 50,
|
||
marginLeft: 5,
|
||
maxWidth: "95%",
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
/>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
extraAuth[index].type = e.target.value;
|
||
setUpdate(Math.random());
|
||
setExtraAuth(extraAuth);
|
||
}}
|
||
value={extraAuth[index].type}
|
||
style={{
|
||
flex: 1,
|
||
backgroundColor: theme.palette.backgroundColor,
|
||
paddingLeft: "10px",
|
||
color: theme.palette.text.primary,
|
||
height: 50,
|
||
borderRadius: theme.shape.borderRadius,
|
||
}}
|
||
inputProps={{
|
||
name: "age",
|
||
id: "outlined-age-simple",
|
||
}}
|
||
>
|
||
<MenuItem
|
||
key={index}
|
||
style={{ backgroundColor: theme.palette.backgroundColor, color: theme.palette.text.primary }}
|
||
value={"header"}
|
||
>
|
||
Header
|
||
</MenuItem>
|
||
<MenuItem
|
||
key={index}
|
||
style={{ backgroundColor: theme.palette.backgroundColor, color: theme.palette.text.primary }}
|
||
value={"query"}
|
||
>
|
||
Query
|
||
</MenuItem>
|
||
</Select>
|
||
<ButtonGroup style={{ display: "flex", width: 100 }}>
|
||
{index === extraAuth.length - 1 ? (
|
||
<Button
|
||
color="primary"
|
||
style={{}}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
extraAuth.push(defaultAuth);
|
||
setExtraAuth(extraAuth);
|
||
setUpdate(Math.random());
|
||
}}
|
||
>
|
||
<AddIcon style={{}} />
|
||
</Button>
|
||
) : (
|
||
<span style={{}} />
|
||
)}
|
||
<Button
|
||
color="primary"
|
||
style={{}}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
const tmpAuth = extraAuth.filter(
|
||
(item) =>
|
||
item.type === value.type && item.name !== value.name
|
||
);
|
||
setExtraAuth(tmpAuth);
|
||
console.log(tmpAuth);
|
||
setUpdate(Math.random());
|
||
}}
|
||
>
|
||
<RemoveIcon style={{}} />
|
||
</Button>
|
||
</ButtonGroup>
|
||
</span>
|
||
);
|
||
})}
|
||
</div>
|
||
//);
|
||
|
||
const jwtAuth =
|
||
authenticationOption === "JWT" ? (
|
||
<div style={{ color: theme.palette.text.primary, marginTop: 20 }}>
|
||
<Typography variant="body1">JWT authentication</Typography>
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
Authentication path
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{ margin: 0, flex: "1", backgroundColor: inputColor }}
|
||
fullWidth={true}
|
||
placeholder="/security/user/authenticate"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={parameterName}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Must start with / and be a valid path
|
||
</span>
|
||
}
|
||
onBlur={(e) => setParameterName(e.target.value)}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
Optional: Authentication queries
|
||
</Typography>
|
||
{/*
|
||
<TextField
|
||
style={{ margin: 0, flex: "1", backgroundColor: inputColor }}
|
||
fullWidth={true}
|
||
placeholder="grant_type=client_credentials&scope=connect.api.read"
|
||
id=""
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={parameterName}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Must use 'key=value&key=value' format
|
||
</span>
|
||
}
|
||
onBlur={(e) => {
|
||
//setParameterName(e.target.value)
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
*/}
|
||
</div>
|
||
) : null;
|
||
|
||
const oauth2Auth =
|
||
authenticationOption === "Oauth2" ? (
|
||
<div style={{ color: theme.palette.text.primary, marginTop: 20 }}>
|
||
<Typography variant="body1">Oauth2 authentication</Typography>
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
{oauth2Type === "delegated" ?
|
||
"Find the Authorization URL, Token URL and scopes in question for the API. Ensure your app in the service uses redirect url https://shuffler.io/set_authentication"
|
||
:
|
||
"Find the Token URL and scopes in question for the API"
|
||
}
|
||
</Typography>
|
||
|
||
{oauth2Type === "delegated" ?
|
||
<span>
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
Authorization URL for Oauth2
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{ margin: 0, flex: "1", backgroundColor: inputColor }}
|
||
fullWidth={true}
|
||
placeholder="https://.../oauth2/authorize"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={parameterName}
|
||
onChange={(e) => setParameterName(e.target.value)}
|
||
onBlur={(event) => {
|
||
var tmpstring = event.target.value.trim();
|
||
|
||
if (
|
||
tmpstring.length > 4 &&
|
||
!tmpstring.startsWith("http") &&
|
||
!tmpstring.startsWith("ftp")
|
||
) {
|
||
toast("Auth URL must start with http(s)://");
|
||
}
|
||
|
||
if (tmpstring.includes("?")) {
|
||
var newtmp = tmpstring.split("?")
|
||
if (tmpstring.length > 1) {
|
||
tmpstring = newtmp[0]
|
||
}
|
||
}
|
||
|
||
setParameterName(tmpstring)
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
</span>
|
||
: null}
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
Token URL for Oauth2
|
||
</Typography>
|
||
<TextField
|
||
required
|
||
style={{ margin: 0, flex: "1", backgroundColor: inputColor }}
|
||
fullWidth={true}
|
||
placeholder="https://.../oauth2/token"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={parameterLocation}
|
||
onChange={(e) => {
|
||
setParameterLocation(e.target.value)
|
||
}}
|
||
onBlur={(event) => {
|
||
var tmpstring = event.target.value.trim();
|
||
|
||
if (
|
||
tmpstring.length > 4 &&
|
||
!tmpstring.startsWith("http") &&
|
||
!tmpstring.startsWith("ftp")
|
||
) {
|
||
toast("Token URL must start with http(s)://");
|
||
}
|
||
|
||
if (tmpstring.includes("?")) {
|
||
var newtmp = tmpstring.split("?")
|
||
if (tmpstring.length > 1) {
|
||
tmpstring = newtmp[0]
|
||
}
|
||
}
|
||
|
||
setParameterLocation(tmpstring)
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
{oauth2Type === "delegated" ?
|
||
<span>
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
Refresh-token URL for Oauth2 (Optional)
|
||
</Typography>
|
||
<TextField
|
||
style={{
|
||
margin: 0, flex: "1", backgroundColor: inputColor,
|
||
border: refreshUrl.length > 0 && (!refreshUrl.startsWith("http") || refreshUrl.includes("//shuffler.")) ? "2px solid red" : "inherit",
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="The URL to retrieve refresh-tokens at"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={refreshUrl}
|
||
helperText={!refreshUrl.startsWith("http") || refreshUrl.includes("//shuffler.")? "Must start with http(s):// and can not contain shuffler.io" : ""}
|
||
onChange={(e) => setRefreshUrl(e.target.value)}
|
||
onBlur={(event) => {
|
||
var tmpstring = event.target.value.trim();
|
||
|
||
if (
|
||
tmpstring.length > 4 &&
|
||
!tmpstring.startsWith("http") &&
|
||
!tmpstring.startsWith("ftp")
|
||
) {
|
||
toast("Refresh URL must start with http(s)://");
|
||
}
|
||
|
||
if (tmpstring.includes("?")) {
|
||
var newtmp = tmpstring.split("?")
|
||
if (tmpstring.length > 1) {
|
||
tmpstring = newtmp[0]
|
||
}
|
||
}
|
||
|
||
setRefreshUrl(tmpstring)
|
||
}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
</span>
|
||
: null}
|
||
<Typography
|
||
variant="body2"
|
||
color="textSecondary"
|
||
style={{ marginTop: 10 }}
|
||
>
|
||
Scopes for Oauth2
|
||
</Typography>
|
||
<MuiChipsInput
|
||
required
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
maxHeight: 160,
|
||
},
|
||
}}
|
||
style={{ minHeight: 160, maxHeight: 160, overflowX: "hidden", overflowY: "auto" }}
|
||
placeholder="Available Oauth2 Scopes"
|
||
color="primary"
|
||
fullWidth
|
||
value={oauth2Scopes}
|
||
onChange={(chips) => {
|
||
setOauth2Scopes(chips)
|
||
setUpdate(Math.random())
|
||
}}
|
||
onBlur={(e) => {
|
||
var newchips = oauth2Scopes
|
||
newchips.push(e.target.value)
|
||
setOauth2Scopes(newchips)
|
||
setUpdate(Math.random())
|
||
}}
|
||
/>
|
||
</div>
|
||
) : null;
|
||
|
||
const apiKey =
|
||
authenticationOption === "API key" ? (
|
||
<div style={{ color: theme.palette.text.primary, marginTop: 20 }}>
|
||
<Typography variant="body1">API key authentication</Typography>
|
||
<Typography variant="body2" color="textSecondary">
|
||
<b>Do NOT put your actual API-key.</b> Add the name of the field used for authentication, e.g. "X-APIKEY".
|
||
</Typography>
|
||
<div style={{display: "flex", marginTop: 10, }}>
|
||
<div style={{flex: 4,}}>
|
||
Key
|
||
<TextField
|
||
required
|
||
style={{ marginTop: 0, backgroundColor: inputColor }}
|
||
fullWidth={true}
|
||
placeholder="The Key to use as the header/query - NOT your actual API-key"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={parameterName}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Can't be empty or contain any of the following: !#$%&'^"+-._~|]+$:=
|
||
</span>
|
||
}
|
||
onChange={(e) => {
|
||
setParameterName(e.target.value);
|
||
}}
|
||
onBlur={(event) => {
|
||
var tmpstring = event.target.value.trim()
|
||
|
||
// Check if tmpstring has any of the illegal characters in it
|
||
}}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{marginLeft: 5, flex: 1,}}>
|
||
Field type
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setParameterLocation(e.target.value);
|
||
}}
|
||
value={parameterLocation}
|
||
style={{
|
||
borderRadius: theme.shape.borderRadius,
|
||
backgroundColor: inputColor,
|
||
paddingLeft: 10,
|
||
color: theme.palette.text.primary,
|
||
height: 57,
|
||
}}
|
||
inputProps={{
|
||
name: "age",
|
||
id: "outlined-age-simple",
|
||
}}
|
||
>
|
||
{apikeySelection.map((data, index) => {
|
||
if (data === undefined) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<MenuItem
|
||
key={index}
|
||
style={{ backgroundColor: inputColor, color: theme.palette.text.primary }}
|
||
value={data}
|
||
>
|
||
{data}
|
||
</MenuItem>
|
||
);
|
||
})}
|
||
</Select>
|
||
</div>
|
||
<div style={{marginLeft: 5, flex: 1,}}>
|
||
Prefix
|
||
<TextField
|
||
style={{ marginTop: 0, flex: "1", backgroundColor: inputColor }}
|
||
fullWidth={true}
|
||
placeholder="Token, SSWS..."
|
||
type="name"
|
||
id="standard-notrequired"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={refreshUrl}
|
||
onChange={(e) => {
|
||
// Just reusing this state
|
||
setRefreshUrl(e.target.value);
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : null;
|
||
|
||
|
||
|
||
const getCurrentPaths = (urlPath) => {
|
||
var paths = [];
|
||
var queries = [];
|
||
|
||
if (urlPath.includes("{") && urlPath.includes("}")) {
|
||
var tmpWord = "";
|
||
var record = false;
|
||
|
||
var query = false;
|
||
for (var key in urlPath) {
|
||
if (urlPath[key] === "?") {
|
||
query = true;
|
||
}
|
||
|
||
if (urlPath[key] === "}") {
|
||
if (tmpWord === parameterName) {
|
||
tmpWord = "";
|
||
record = false;
|
||
continue;
|
||
} else if (query) {
|
||
queries.push(tmpWord);
|
||
} else {
|
||
paths.push(tmpWord);
|
||
}
|
||
|
||
tmpWord = "";
|
||
record = false;
|
||
}
|
||
|
||
if (record) {
|
||
tmpWord += urlPath[key];
|
||
}
|
||
|
||
//if (urlPath[key] === "{" && urlPath[key-1] === "/") {
|
||
if (urlPath[key] === "{") {
|
||
record = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (urlPath.includes("<") && urlPath.includes(">")) {
|
||
var tmpWord = "";
|
||
var record = false;
|
||
|
||
var query = false;
|
||
for (var key in urlPath) {
|
||
if (urlPath[key] === "?") {
|
||
query = true;
|
||
}
|
||
|
||
if (urlPath[key] === ">") {
|
||
if (tmpWord === parameterName) {
|
||
tmpWord = "";
|
||
record = false;
|
||
continue;
|
||
} else if (query) {
|
||
queries.push(tmpWord);
|
||
} else {
|
||
paths.push(tmpWord);
|
||
}
|
||
|
||
tmpWord = "";
|
||
record = false;
|
||
}
|
||
|
||
if (record) {
|
||
tmpWord += urlPath[key];
|
||
}
|
||
|
||
//if (urlPath[key] === "{" && urlPath[key-1] === "/") {
|
||
if (urlPath[key] === "<") {
|
||
record = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return [paths, queries];
|
||
};
|
||
|
||
const foundCategory = newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 ? categories.find((x) => x.name === newWorkflowCategories[0]) : undefined
|
||
const actionLabels = foundCategory !== undefined && foundCategory !== null && foundCategory.action_labels.length > 0 ? ["No Label"].concat(foundCategory.action_labels) : []
|
||
|
||
const ActionPaper = (props) => {
|
||
const { data, index } = props
|
||
|
||
const [updater, setUpdater] = useState("tmp");
|
||
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
||
const [urlPath, setUrlPath] = useState("");
|
||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
||
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
||
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||
const [currentAction, setCurrentAction] = useState({
|
||
name: "",
|
||
file_field: "",
|
||
description: "",
|
||
url: "",
|
||
headers: "",
|
||
paths: [],
|
||
queries: [],
|
||
body: "",
|
||
errors: [],
|
||
example_response: "",
|
||
method: actionNonBodyRequest[0],
|
||
action_label: ["No Label"],
|
||
required_bodyfields: [],
|
||
});
|
||
|
||
//useEffect(() => {
|
||
// console.log("Queries: ", urlPathQueries)
|
||
//}, [urlPathQueries])
|
||
|
||
const findBodyParams = (body) => {
|
||
const regex = /\${(\w+)}/g;
|
||
const found = body.match(regex);
|
||
if (found === null) {
|
||
setExtraBodyFields([]);
|
||
} else {
|
||
setExtraBodyFields(found);
|
||
}
|
||
};
|
||
|
||
const UrlPathParameters = () => {
|
||
const values = getCurrentPaths(urlPath);
|
||
const paths = values[0];
|
||
const queries = values[1];
|
||
|
||
if (currentAction.paths !== paths && urlPath.length > 0) {
|
||
//console.log("IN PATHS SETTER: !", paths)
|
||
setActionField("paths", paths);
|
||
}
|
||
|
||
var tmpQueries = [];
|
||
|
||
// No overlapping of names
|
||
for (var key in queries) {
|
||
const tmpquery = queries[key];
|
||
const found = tmpQueries.find((query) => query.name === tmpquery);
|
||
if (found === undefined) {
|
||
tmpQueries.push({ name: queries[key], required: true });
|
||
}
|
||
}
|
||
|
||
// FIXME: Frontend isn't updating..
|
||
if (tmpQueries.length > 0 && JSON.stringify(tmpQueries) !== JSON.stringify(urlPathQueries)) {
|
||
setUrlPathQueries(tmpQueries);
|
||
}
|
||
|
||
return paths.length > 0 ? (
|
||
<div>Required parameters: {paths.join(", ")}</div>
|
||
) : null;
|
||
};
|
||
|
||
const HandleIndividualChip = (props) => {
|
||
const { chipData, index } = props;
|
||
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
|
||
|
||
const parsedChip = chipData.startsWith("${") && chipData.endsWith("}") ? chipData.substring(2, chipData.length - 1) : chipData
|
||
|
||
return (
|
||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||
<Chip
|
||
style={{
|
||
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||
height: 30,
|
||
margin: 3,
|
||
paddingLeft: 5,
|
||
paddingRight: 5,
|
||
cursor: "pointer",
|
||
borderColor: theme.palette.chipStyle.borderColor,
|
||
color: theme.palette.chipStyle.color,
|
||
}}
|
||
label={parsedChip}
|
||
onClick={() => {
|
||
if (chipRequired) {
|
||
currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1)
|
||
} else {
|
||
currentAction["required_bodyfields"].push(chipData)
|
||
}
|
||
|
||
setCurrentAction(currentAction);
|
||
setChipRequired(!chipRequired);
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
);
|
||
};
|
||
|
||
const setActionField = (field, value) => {
|
||
currentAction[field] = value
|
||
setCurrentAction(currentAction)
|
||
|
||
//setUrlPathQueries(currentAction.queries)
|
||
};
|
||
|
||
const addPathQuery = () => {
|
||
urlPathQueries.push({ name: "", required: true, example: "", });
|
||
if (updater === "addupdater") {
|
||
setUpdater("updater");
|
||
} else {
|
||
setUpdater("addupdater");
|
||
}
|
||
|
||
setUrlPathQueries(urlPathQueries);
|
||
};
|
||
|
||
const flipRequired = (index) => {
|
||
urlPathQueries[index].required = !urlPathQueries[index].required;
|
||
if (updater === "flipupdater") {
|
||
setUpdater("updater");
|
||
} else {
|
||
setUpdater("flipupdater");
|
||
}
|
||
|
||
setUrlPathQueries(urlPathQueries);
|
||
};
|
||
|
||
const deletePathQuery = (index) => {
|
||
console.log("Should delete index: ", index)
|
||
var tmpqueries = JSON.parse(JSON.stringify(urlPathQueries))
|
||
tmpqueries.splice(index, 1)
|
||
|
||
console.log("Queries: ", tmpqueries)
|
||
setUrlPathQueries(tmpqueries);
|
||
|
||
if (updater === "deleteupdater") {
|
||
setUpdater("updater");
|
||
} else {
|
||
setUpdater("deleteupdater");
|
||
}
|
||
};
|
||
|
||
const loopQueries = urlPathQueries.length === 0 ? null : (
|
||
<div>
|
||
<Divider
|
||
style={{
|
||
marginBottom: "10px",
|
||
marginTop: "30px",
|
||
height: "1px",
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}}
|
||
/>
|
||
Queries
|
||
{urlPathQueries.map((query, queryIndex) => {
|
||
const requiredColor = query.required === true ? "green" : "red";
|
||
//const required = data.required === true ? <div style={{color: "green", cursor: "pointer"}}>{data.required.toString()}</div> : <div onClick={() => {flipRequired(index)}} style={{display: "inline", color: "red", cursor: "pointer"}}>{data.required.toString()}</div>
|
||
return (
|
||
<Paper key={queryIndex} style={actionListStyle}>
|
||
<div style={{ marginLeft: "5px", width: "100%" }}>
|
||
<div style={{display: "flex"}}>
|
||
<TextField
|
||
required
|
||
fullWidth={true}
|
||
defaultValue={query.name}
|
||
placeholder={"Query name (key)"}
|
||
label={"Query Key"}
|
||
onBlur={(e) => {
|
||
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "")
|
||
setUrlPathQueries(urlPathQueries)
|
||
}}
|
||
style={{flex: 3}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
<TextField
|
||
fullWidth={true}
|
||
defaultValue={query.example}
|
||
placeholder={"Default value"}
|
||
label={"Example"}
|
||
onBlur={(e) => {
|
||
// E.g. for Jira -> JQL -> requires = in param
|
||
urlPathQueries[queryIndex].example = e.target.value.replaceAll("=","=")
|
||
setUrlPathQueries(urlPathQueries)
|
||
}}
|
||
style={{flex: 2}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
<div
|
||
style={{ cursor: "pointer" }}
|
||
onClick={() => {
|
||
flipRequired(queryIndex);
|
||
}}
|
||
>
|
||
Required:{" "}
|
||
<div style={{ display: "inline", color: requiredColor }}>
|
||
{query.required.toString()}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{ float: "right", color: "#f85a3e", cursor: "pointer" }}
|
||
onClick={() => {
|
||
deletePathQuery(queryIndex);
|
||
}}
|
||
>
|
||
<DeleteIcon />
|
||
</div>
|
||
</Paper>
|
||
);
|
||
})}
|
||
<Divider
|
||
style={{
|
||
marginBottom: "10px",
|
||
marginTop: "30px",
|
||
height: "1px",
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
const SetExtraBodyField = (props) => {
|
||
const { extraBodyFields } = props;
|
||
|
||
if (extraBodyFields === undefined || extraBodyFields === null) {
|
||
return null;
|
||
}
|
||
|
||
//const parsedlist = extraBodyFields.join(", ")
|
||
//console.log("LIST: ", parsedlist)
|
||
|
||
return (
|
||
<span>
|
||
{extraBodyFields.map((data, index) => {
|
||
return <HandleIndividualChip key={index} chipData={data} />;
|
||
})}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
const bodyInfo = actionBodyRequest.includes(currentActionMethod) ? (
|
||
<div style={{ marginTop: 10 }}>
|
||
<b>Request Body</b>:{" "}
|
||
{extraBodyFields.length > 0 ? (
|
||
<Typography style={{ display: "inline-block" }}>
|
||
<SetExtraBodyField extraBodyFields={extraBodyFields} />
|
||
</Typography>
|
||
) : (
|
||
<Typography style={{ display: "inline-block" }}>
|
||
{`Add variables with \$\{ variable_name }`}
|
||
</Typography>
|
||
)}
|
||
<TextField
|
||
required
|
||
style={{ flex: "1", marginRight: "15px", backgroundColor: theme.palette.textFieldStyle.backgroundColor }}
|
||
fullWidth={true}
|
||
placeholder={
|
||
'{\n\t"example": "${example}",\n\t"apikey": "${apikey}",\n\t"search": "1.2.3.5"\n}'
|
||
}
|
||
margin="normal"
|
||
variant="outlined"
|
||
multiline
|
||
minRows="5"
|
||
defaultValue={currentAction["body"]}
|
||
onChange={(e) => {
|
||
setActionField("body", e.target.value);
|
||
findBodyParams(e.target.value);
|
||
}}
|
||
key={currentAction}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Shows an example body to the user. ${} creates variables.
|
||
</span>
|
||
}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
},
|
||
}}
|
||
/>
|
||
<div></div>
|
||
</div>
|
||
) : null;
|
||
|
||
const exampleResponse = fileDownloadEnabled ? null : (
|
||
<div style={{}}>
|
||
<b>Example success response</b>
|
||
<TextField
|
||
required
|
||
style={{ flex: "1", marginRight: "15px", backgroundColor: theme.palette.textFieldStyle.backgroundColor }}
|
||
fullWidth={true}
|
||
placeholder={
|
||
'{\n\t"email": "testing@test.com",\n\t"firstname": "testing"\n}'
|
||
}
|
||
margin="normal"
|
||
variant="outlined"
|
||
multiline
|
||
minRows="2"
|
||
defaultValue={currentAction["example_response"]}
|
||
onChange={(e) => setActionField("example_response", e.target.value)}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Helps with autocompletion and understanding of the endpoint
|
||
</span>
|
||
}
|
||
key={currentAction}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
const addActionToView = (errors) => {
|
||
currentAction.errors = errors;
|
||
currentAction.queries = urlPathQueries;
|
||
setUrlPathQueries([]);
|
||
|
||
const actionIndex = actions.findIndex((data) => data.name === currentAction.name);
|
||
|
||
if (actionIndex < 0) {
|
||
actions.push(currentAction);
|
||
} else {
|
||
actions[actionIndex] = currentAction;
|
||
}
|
||
|
||
if (actions.length > actionAmount) {
|
||
setActionAmount(actions.length);
|
||
}
|
||
|
||
setActions(actions);
|
||
setFilteredActions(actions);
|
||
};
|
||
|
||
const getActionErrors = () => {
|
||
var errormessage = [];
|
||
if (currentAction.name === undefined || currentAction.name.length === 0) {
|
||
errormessage.push("Name can't be empty");
|
||
}
|
||
|
||
// Url verification
|
||
//if (currentAction.url.length === 0) {
|
||
// errormessage.push("URL path can't be empty.")
|
||
if (!currentAction.url.startsWith("/") && baseUrl.length > 0 && currentAction.url.length > 0) {
|
||
errormessage.push("URL must start with /");
|
||
}
|
||
|
||
const check = urlPathQueries.findIndex((data) => data.name.length === 0);
|
||
if (check >= 0) {
|
||
errormessage.push("All queries must have a value");
|
||
}
|
||
|
||
return errormessage;
|
||
};
|
||
|
||
|
||
const getBackgroundColor = (data) => {
|
||
var bgColor = "#61afee";
|
||
if (data === "POST") {
|
||
bgColor = "#49cc90";
|
||
} else if (data === "PUT") {
|
||
bgColor = "#fca130";
|
||
} else if (data === "PATCH") {
|
||
bgColor = "#50e3c2";
|
||
} else if (data === "DELETE") {
|
||
bgColor = "#f93e3e";
|
||
} else if (data === "HEAD") {
|
||
bgColor = "#9012fe";
|
||
}
|
||
|
||
return bgColor;
|
||
}
|
||
|
||
|
||
const newActionModal = (
|
||
<Drawer
|
||
anchor={"right"}
|
||
open={actionsModalOpen}
|
||
fullWidth
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: theme.palette.drawer.backgroundColor,
|
||
color: theme.palette.text.primary,
|
||
minWidth: 700,
|
||
maxWidth: 700,
|
||
},
|
||
}}
|
||
onClose={() => {
|
||
console.log("Closing modal");
|
||
|
||
console.log(currentAction);
|
||
const errors = getActionErrors();
|
||
addActionToView(errors);
|
||
setActionsModalOpen(false);
|
||
setUrlPathQueries([]);
|
||
setUrlPath("");
|
||
setFileUploadEnabled(false);
|
||
}}
|
||
>
|
||
<FormControl style={{ backgroundColor: theme.palette.drawer.backgroundColor, color: theme.palette.text.primary }}>
|
||
<DialogTitle style={{marginTop: 45, }}>
|
||
<div style={{ color: theme.palette.text.primary }}>New action</div>
|
||
</DialogTitle>
|
||
<DialogContent style={{paddingBottom: 100, }}>
|
||
<a
|
||
target="_blank"
|
||
href="https://shuffler.io/docs/app_creation#actions"
|
||
style={{ textDecoration: "none", color: theme.palette.linkColor }}
|
||
>
|
||
Learn more about actions
|
||
</a>
|
||
<div style={{ marginTop: "15px" }} />
|
||
Name
|
||
<TextField
|
||
required
|
||
style={{
|
||
flex: "1",
|
||
marginTop: 5,
|
||
marginRight: 15,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="Name"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={currentAction["name"]}
|
||
onChange={(e) => {
|
||
var trimmed = e.target.value.trim();
|
||
setActionField("name", trimmed)
|
||
}}
|
||
onBlur={(e) => {
|
||
// Fix basic issues in frontend. Python functions run a-zA-Z0-9_
|
||
const regex = /[A-Za-z0-9 _]/g;
|
||
const found = e.target.value.match(regex);
|
||
if (found !== null) {
|
||
setActionField("name", found.join(""));
|
||
}
|
||
|
||
// Look through all actions and see if there is one with the same name
|
||
if (currentAction.url === "" && actions !== undefined && actions !== null && actions.length > 0) {
|
||
for (var i = 0; i < actions.length; i++) {
|
||
if (actions[i].name.toLowerCase() === e.target.value.toLowerCase()) {
|
||
//toast("Action with name " + e.target.value + " already exists. If you keep this, it will be overwritten.")
|
||
break
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
}}
|
||
key={currentAction}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
},
|
||
}}
|
||
/>
|
||
<div style={{ marginTop: 10 }} />
|
||
Description
|
||
<TextField
|
||
required
|
||
style={{
|
||
flex: "1",
|
||
marginTop: 5,
|
||
marginRight: "15px",
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="Description"
|
||
type="description"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={currentAction["description"]}
|
||
onChange={(e) => setActionField("description", e.target.value)}
|
||
InputProps={{
|
||
style: {
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
},
|
||
}}
|
||
/>
|
||
<Divider
|
||
style={{
|
||
marginBottom: "10px",
|
||
marginTop: "30px",
|
||
height: "1px",
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}}
|
||
/>
|
||
<h2>Request</h2>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setActionField("method", e.target.value);
|
||
setCurrentActionMethod(e.target.value);
|
||
}}
|
||
value={currentActionMethod}
|
||
style={{
|
||
paddingLeft: "10px",
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
height: "50px",
|
||
}}
|
||
inputProps={{
|
||
name: "Method",
|
||
id: "method-option",
|
||
style: {
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
},
|
||
}}
|
||
>
|
||
|
||
// Add actionBodyRequest to actionNonBodyRequest
|
||
{actionNonBodyRequest.concat(actionBodyRequest).map((data, index) => {
|
||
const backgroundColor = getBackgroundColor(data);
|
||
return (
|
||
<MenuItem
|
||
key={index}
|
||
style={{}}
|
||
value={data}
|
||
>
|
||
<Chip
|
||
style={{
|
||
color: theme.palette.text.primary,
|
||
borderRadius: theme.shape.borderRadius,
|
||
minWidth: 80,
|
||
marginRight: 10,
|
||
marginTop: 2,
|
||
cursor: "pointer",
|
||
fontSize: 14,
|
||
fontWeight: "bold",
|
||
backgroundColor: backgroundColor,
|
||
}}
|
||
label={data}
|
||
/>
|
||
</MenuItem>
|
||
);
|
||
})}
|
||
</Select>
|
||
<div style={{ marginTop: "15px" }} />
|
||
URL path / Curl statement
|
||
<TextField
|
||
required
|
||
style={{
|
||
flex: "1",
|
||
marginRight: "15px",
|
||
marginTop: "5px",
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="URL path"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={urlPath}
|
||
onChange={(e) => {
|
||
setActionField("url", e.target.value);
|
||
setUrlPath(e.target.value);
|
||
}}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
The path to use. Must start with /. Use {"{variablename}"} to
|
||
have path variables
|
||
</span>
|
||
}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
input: classes.input,
|
||
},
|
||
style: {
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
},
|
||
}}
|
||
onBlur={(event) => {
|
||
var parsedurl = event.target.value;
|
||
//console.log("URL: ", parsedurl)
|
||
if (parsedurl.includes(" ")) {
|
||
parsedurl = parsedurl.replaceAll(" ", " ");
|
||
}
|
||
|
||
if (parsedurl.includes(" ")) {
|
||
parsedurl = parsedurl.replaceAll(" ", " ");
|
||
}
|
||
|
||
if (parsedurl.includes("[") && parsedurl.includes("]")) {
|
||
//console.log("REPLACE1")
|
||
parsedurl = parsedurl.replaceAll("[", "{");
|
||
parsedurl = parsedurl.replaceAll("]", "}");
|
||
}
|
||
|
||
if (parsedurl.includes("<") && parsedurl.includes(">")) {
|
||
//console.log("REPLACE2")
|
||
parsedurl = parsedurl.replaceAll("<", "{");
|
||
parsedurl = parsedurl.replaceAll(">", "}");
|
||
}
|
||
|
||
//console.log("URL2: ", parsedurl)
|
||
if (
|
||
parsedurl.startsWith("PUT ") ||
|
||
parsedurl.startsWith("GET ") ||
|
||
parsedurl.startsWith("POST ") ||
|
||
parsedurl.startsWith("DELETE ") ||
|
||
parsedurl.startsWith("PATCH ") ||
|
||
parsedurl.startsWith("CONNECT ")
|
||
) {
|
||
const tmp = parsedurl.split(" ");
|
||
|
||
if (tmp.length > 1) {
|
||
parsedurl = tmp[1].trim();
|
||
setActionField("url", parsedurl);
|
||
|
||
setCurrentActionMethod(tmp[0].toUpperCase());
|
||
setActionField("method", tmp[0].toUpperCase());
|
||
}
|
||
|
||
console.log("URL3: ", parsedurl);
|
||
|
||
//setUpdate(Math.random());
|
||
} else if (parsedurl.startsWith("curl")) {
|
||
console.log("URL4: ", parsedurl);
|
||
|
||
const request = parseCurl(event.target.value);
|
||
if (
|
||
request !== event.target.value &&
|
||
request.method !== undefined &&
|
||
request.method !== null
|
||
) {
|
||
if (request.method.toUpperCase() !== currentAction.Method) {
|
||
setCurrentActionMethod(request.method.toUpperCase());
|
||
setActionField("method", request.method.toUpperCase());
|
||
}
|
||
|
||
if (request.header !== undefined && request.header !== null) {
|
||
var headers = [];
|
||
for (let [key, value] of Object.entries(request.header)) {
|
||
if (value === undefined) {
|
||
if (key.includes(":")) {
|
||
const keysplit = key.split(":")
|
||
key = keysplit[0].trim()
|
||
value = keysplit[1].trim()
|
||
|
||
} else if (key.includes("=")) {
|
||
const keysplit = key.split("=")
|
||
key = keysplit[0].trim()
|
||
value = keysplit[1].trim()
|
||
|
||
} else {
|
||
toast("Removed key: ", key)
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (
|
||
parameterName !== undefined &&
|
||
key.toLowerCase() === parameterName.toLowerCase()
|
||
) {
|
||
continue;
|
||
}
|
||
|
||
if (key === "Authorization") {
|
||
continue;
|
||
}
|
||
|
||
headers += key + "=" + value + "\n";
|
||
}
|
||
|
||
try {
|
||
setActionField("headers", headers.trim());
|
||
} catch (e) {
|
||
console.log("Failed to parse header: ", e)
|
||
}
|
||
}
|
||
|
||
if (request.body !== undefined && request.body !== null) {
|
||
setActionField("body", request.body);
|
||
}
|
||
|
||
// Parse URL
|
||
if (request.url !== undefined) {
|
||
parsedurl = request.url;
|
||
}
|
||
}
|
||
|
||
console.log("PARSED: ", parsedurl);
|
||
if (parsedurl !== undefined) {
|
||
if (parsedurl.includes("<") && parsedurl.includes(">")) {
|
||
parsedurl = parsedurl.split("<").join("{");
|
||
parsedurl = parsedurl.split(">").join("}");
|
||
}
|
||
|
||
if (
|
||
parsedurl.startsWith("http") ||
|
||
parsedurl.startsWith("ftp")
|
||
) {
|
||
if (
|
||
parsedurl !== undefined &&
|
||
parsedurl.includes(parameterName)
|
||
) {
|
||
// Remove <> etc.
|
||
//
|
||
|
||
console.log("IT HAS THE PARAM NAME!");
|
||
const newurl = new URL(encodeURI(parsedurl));
|
||
newurl.searchParams.delete(parameterName);
|
||
parsedurl = decodeURI(newurl.href);
|
||
}
|
||
|
||
// Remove the base URL itself
|
||
if (
|
||
parsedurl !== undefined &&
|
||
baseUrl !== undefined &&
|
||
baseUrl.length > 0 &&
|
||
parsedurl.includes(baseUrl)
|
||
) {
|
||
parsedurl = parsedurl.replace(baseUrl, "");
|
||
}
|
||
|
||
// Check URL query && headers
|
||
//setActionField("url", parsedurl)
|
||
}
|
||
}
|
||
}
|
||
|
||
if (baseUrl !== undefined && baseUrl !== null && parsedurl.startsWith(baseUrl)) {
|
||
parsedurl = parsedurl.replaceAll(baseUrl, "");
|
||
}
|
||
|
||
if (parsedurl.includes("?")) {
|
||
const parsedurlsplit = parsedurl.split("?")
|
||
parsedurl = parsedurlsplit[0]
|
||
|
||
//var newqueries = selectedAction.queries === undefined || selectedAction.queries === null ? [] : selectedAction.queries
|
||
|
||
const datasplit = parsedurlsplit[1].split("&")
|
||
for (var key in datasplit) {
|
||
//console.log("Data: ", datasplit[key])
|
||
var actualkey = datasplit[key]
|
||
var example = ""
|
||
if (datasplit[key].includes("=")) {
|
||
actualkey = datasplit[key].split("=")[0]
|
||
example = datasplit[key].split("=")[1]
|
||
}
|
||
|
||
const foundPath = urlPathQueries.find(data => data.name === actualkey)
|
||
if (foundPath === null || foundPath === undefined) {
|
||
urlPathQueries.push({ name: actualkey, example: example, required: true })
|
||
}
|
||
}
|
||
}
|
||
|
||
// Found that dashes in the URL doesn't work
|
||
//parsedurl = parsedurl.replace("-", "_")
|
||
//console.log("Actions: ", actions)
|
||
|
||
if (baseUrl.length === 0 && parsedurl.includes("http")) {
|
||
try {
|
||
const newurl = new URL(encodeURI(parsedurl))
|
||
newurl.searchParams.delete(parameterName)
|
||
console.log("New url: ", newurl)
|
||
parsedurl = newurl.pathname
|
||
setBaseUrl(newurl.origin)
|
||
} catch (e) {
|
||
console.log("Failed to parse URL: ", e)
|
||
}
|
||
}
|
||
|
||
if (event.target.value !== parsedurl) {
|
||
setUrlPath(parsedurl);
|
||
setActionField("url", parsedurl);
|
||
}
|
||
//console.log("URL: ", request.url)
|
||
}}
|
||
/>
|
||
<UrlPathParameters />
|
||
{loopQueries}
|
||
<Button
|
||
color="primary"
|
||
style={{
|
||
marginTop: "5px",
|
||
marginBottom: "10px",
|
||
borderRadius: "0px",
|
||
}}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
addPathQuery();
|
||
}}
|
||
>
|
||
New query
|
||
</Button>
|
||
{currentActionMethod === "POST" ? (
|
||
<Button
|
||
color="primary"
|
||
variant={fileUploadEnabled ? "contained" : "outlined"}
|
||
style={{
|
||
marginLeft: 10,
|
||
marginTop: "5px",
|
||
marginBottom: "10px",
|
||
borderRadius: "0px",
|
||
}}
|
||
onClick={() => {
|
||
setFileUploadEnabled(!fileUploadEnabled);
|
||
if (
|
||
fileUploadEnabled &&
|
||
currentAction["file_field"].length > 0
|
||
) {
|
||
setActionField("file_field", "");
|
||
}
|
||
//setUpdate(Math.random());
|
||
}}
|
||
>
|
||
Enable Fileupload
|
||
</Button>
|
||
) : null}
|
||
{/*currentActionMethod === "GET" ? (
|
||
<Button
|
||
color="primary"
|
||
variant={fileDownloadEnabled ? "contained" : "outlined"}
|
||
style={{
|
||
marginLeft: 10,
|
||
marginTop: "5px",
|
||
marginBottom: "10px",
|
||
borderRadius: "0px",
|
||
}}
|
||
onClick={() => {
|
||
setFileDownloadEnabled(!fileDownloadEnabled);
|
||
if (fileDownloadEnabled) {
|
||
setActionField("example_response", "");
|
||
} else {
|
||
setActionField("example_response", "shuffle_file_download");
|
||
}
|
||
//setUpdate(Math.random());
|
||
}}
|
||
>
|
||
Download as file
|
||
</Button>
|
||
) : null*/}
|
||
{fileUploadEnabled ? (
|
||
<TextField
|
||
required
|
||
style={{
|
||
backgroundColor: inputColor,
|
||
display: "inline-block",
|
||
marginLeft: 10,
|
||
maxWidth: 210,
|
||
marginTop: 7,
|
||
}}
|
||
placeholder={"file"}
|
||
margin="normal"
|
||
variant="outlined"
|
||
id="standard-required"
|
||
defaultValue={currentAction["file_field"]}
|
||
onChange={(e) => setActionField("file_field", e.target.value)}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
The File field to interact with
|
||
</span>
|
||
}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
) : null}
|
||
<div />
|
||
{fileUploadEnabled ? null :
|
||
<span>
|
||
<b>Headers</b>
|
||
<TextField
|
||
required
|
||
style={{
|
||
flex: "1",
|
||
marginRight: "15px",
|
||
marginTop: "5px",
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder={
|
||
"Accept: application/json\r\nContent-Type: application/json"
|
||
}
|
||
margin="normal"
|
||
variant="outlined"
|
||
id="standard-required"
|
||
defaultValue={currentAction["headers"]}
|
||
multiline
|
||
minRows="2"
|
||
onChange={(e) => setActionField("headers", e.target.value)}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Headers that are part of the request. Default: EMPTY
|
||
</span>
|
||
}
|
||
InputProps={{
|
||
style: {
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
color: theme.palette.textFieldStyle.color,
|
||
},
|
||
}}
|
||
/>
|
||
</span>
|
||
}
|
||
{bodyInfo}
|
||
<Divider
|
||
style={{
|
||
backgroundColor: theme.palette.defaultBorder,
|
||
marginTop: 15,
|
||
marginBottom: 15,
|
||
}}
|
||
/>
|
||
{exampleResponse}
|
||
</DialogContent>
|
||
<div style={{position: "fixed", backgroundColor: theme.palette.drawer.backgroundColor, bottom: 0, width: "100%", padding: 25, borderTop: theme.palette.defaultBorder, }}>
|
||
<Button
|
||
color="primary"
|
||
variant={urlPath.length > 0 ? "contained" : "outlined"}
|
||
style={{ }}
|
||
onClick={() => {
|
||
console.log(currentAction);
|
||
|
||
const errors = getActionErrors();
|
||
addActionToView(errors);
|
||
setActionsModalOpen(false);
|
||
setUrlPathQueries([]);
|
||
setUrlPath("");
|
||
setFileUploadEnabled(false);
|
||
}}
|
||
>
|
||
Submit
|
||
</Button>
|
||
{/*
|
||
<Button
|
||
style={{ marginLeft: 10, }}
|
||
onClick={() => {
|
||
setActionsModalOpen(false);
|
||
}}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
*/}
|
||
</div>
|
||
</FormControl>
|
||
</Drawer>
|
||
);
|
||
|
||
|
||
var error =
|
||
data.errors.length > 0 ? (
|
||
<Tooltip
|
||
color="primary"
|
||
title={data.errors.join("\n")}
|
||
placement="bottom"
|
||
>
|
||
<ErrorOutlineIcon />
|
||
</Tooltip>
|
||
) : null
|
||
{/*
|
||
<Tooltip
|
||
color="secondary"
|
||
title={data.errors.join("\n")}
|
||
placement="bottom"
|
||
>
|
||
<CheckCircleIcon style={{ marginTop: 6 }} />
|
||
</Tooltip>
|
||
*/}
|
||
|
||
var bgColor = "#61afee";
|
||
if (data.method === "POST") {
|
||
bgColor = "#49cc90";
|
||
} else if (data.method === "PUT") {
|
||
bgColor = "#fca130";
|
||
} else if (data.method === "PATCH") {
|
||
bgColor = "#50e3c2";
|
||
} else if (data.method === "DELETE") {
|
||
bgColor = "#f93e3e";
|
||
} else if (data.method === "HEAD") {
|
||
bgColor = "#9012fe";
|
||
}
|
||
|
||
const url = data.url;
|
||
const hasFile = (data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download"
|
||
|
||
if (data?.action_label[0] !== 'No Label') {
|
||
//console.log("Action label: ", data?.name, data?.action_label)
|
||
}
|
||
|
||
var currentActionLabels = JSON.parse(JSON.stringify(actionLabels))
|
||
for (var labelKey in data?.action_label) {
|
||
const label = data?.action_label[labelKey]
|
||
if (label.toLowerCase().replaceAll(" ", "_") === "no_label") {
|
||
continue
|
||
}
|
||
|
||
var found = false
|
||
for (var acatKey in currentActionLabels) {
|
||
const acat = currentActionLabels[acatKey]
|
||
if (acat === label) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!found) {
|
||
//console.log(`Not found: '${label}' in ${currentActionLabels}`)
|
||
currentActionLabels.push(data?.action_label[labelKey])
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Paper key={index} style={actionListStyle}>
|
||
{newActionModal}
|
||
|
||
{error}
|
||
<Tooltip title="Edit action" placement="bottom">
|
||
<div
|
||
id={data.name}
|
||
style={{
|
||
marginLeft: "5px",
|
||
width: "100%",
|
||
cursor: "pointer",
|
||
maxWidth: 725,
|
||
overflowX: "hidden",
|
||
}}
|
||
onClick={() => {
|
||
//console.log("Data: ", data)
|
||
if (hasFile) {
|
||
//setActionField("headers", "")
|
||
//console.log("It has a file: ", data["file_field"])
|
||
|
||
setFileUploadEnabled(true);
|
||
data.headers = ""
|
||
} else {
|
||
console.log("No file")
|
||
}
|
||
|
||
setCurrentAction(data);
|
||
setCurrentActionMethod(data.method);
|
||
|
||
console.log("QUERIES: ", data.queries)
|
||
setUrlPathQueries(data.queries);
|
||
setUrlPath(data.url);
|
||
setActionsModalOpen(true);
|
||
|
||
if (data["body"] !== undefined && data["body"] !== null && data["body"].length > 0) {
|
||
findBodyParams(data["body"]);
|
||
} else {
|
||
console.log("No body param")
|
||
}
|
||
|
||
}}
|
||
>
|
||
<div style={{ display: "flex" }}>
|
||
<Chip
|
||
style={{
|
||
backgroundColor: bgColor,
|
||
color: theme.palette.text.primary,
|
||
borderRadius: theme.shape.borderRadius,
|
||
minWidth: 80,
|
||
marginRight: 10,
|
||
marginTop: 2,
|
||
cursor: "pointer",
|
||
fontSize: 14,
|
||
}}
|
||
label={data.method}
|
||
/>
|
||
<span
|
||
style={{
|
||
fontSize: 16,
|
||
marginTop: "auto",
|
||
marginBottom: "auto",
|
||
}}
|
||
>
|
||
{hasFile ? (
|
||
<AttachFileIcon style={{ height: 20, width: 20 }} />
|
||
) : null}{" "}
|
||
{url} - {data.name}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Tooltip>
|
||
{/*
|
||
<Tooltip title="Test action" placement="bottom">
|
||
<div style={{color: "#f85a3e", cursor: "pointer", marginRight: "10px", }} onClick={() => {testAction(index)}}>
|
||
Test
|
||
</div>
|
||
</Tooltip>
|
||
*/}
|
||
|
||
{/* From 2023: Example of handling action labels */}
|
||
{currentActionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ?
|
||
<Select
|
||
multiple={true}
|
||
fullWidth
|
||
onChange={(e) => {
|
||
if (e.target.value === undefined || e.target.value === null) {
|
||
return
|
||
}
|
||
|
||
if (e.target.value.length === 0) {
|
||
e.target.value = ["No Label"]
|
||
} else if (e.target.value.length >= 1) {
|
||
var foundother = false
|
||
for (var labelkey in e.target.value) {
|
||
const label = e.target.value[labelkey]
|
||
if (label !== "no_label" && label !== "No_Label" && label !== "No Label") {
|
||
foundother = true
|
||
break
|
||
}
|
||
}
|
||
|
||
if (foundother) {
|
||
e.target.value = e.target.value.filter(label => label !== "no_label" && label !== "No_Label" && label !== "No Label")
|
||
|
||
} else {
|
||
e.target.value = ["No Label"]
|
||
}
|
||
|
||
}
|
||
|
||
const foundActionIndex = actions.findIndex((action) => action.name === data.name)
|
||
if (foundActionIndex !== undefined && foundActionIndex !== null && foundActionIndex >= 0) {
|
||
actions[foundActionIndex].action_label = e.target.value
|
||
setActions(actions)
|
||
setUpdate(Math.random())
|
||
} else {
|
||
console.log("Could not find action: ", data.name)
|
||
}
|
||
}}
|
||
value={data?.action_label}
|
||
style={{
|
||
border: data.action_label === undefined || data.action_label[0] === "No Label" ? "" : `2px solid ${bgColor}`,
|
||
borderRadius: theme.shape.borderRadius,
|
||
backgroundColor: theme.palette.backgroundColor,
|
||
paddingLeft: 10,
|
||
color: theme.palette.text.primary,
|
||
height: 30,
|
||
maxWidth: 35,
|
||
marginLeft: 10,
|
||
marginRight: 10,
|
||
overflow: "hidden",
|
||
}}
|
||
inputProps={{
|
||
name: "Method",
|
||
id: "method-option",
|
||
}}
|
||
onClick={(e) => {
|
||
console.log("Current labels: ", data?.action_label)
|
||
}}
|
||
>
|
||
{currentActionLabels.map((label, labelindex) => {
|
||
return (
|
||
<MenuItem
|
||
key={labelindex}
|
||
value={label}
|
||
style={{
|
||
}}
|
||
>
|
||
{label}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
: null}
|
||
|
||
<Tooltip
|
||
title="Duplicate action"
|
||
placement="bottom"
|
||
style={{ minWidth: 60 }}
|
||
>
|
||
<div
|
||
style={{
|
||
color: "#f85a3e",
|
||
cursor: "pointer",
|
||
marginRight: 15,
|
||
}}
|
||
onClick={() => {
|
||
duplicateAction(index);
|
||
}}
|
||
>
|
||
<FileCopyIcon color="secondary" />
|
||
</div>
|
||
</Tooltip>
|
||
<Tooltip
|
||
title="Delete action"
|
||
placement="bottom"
|
||
style={{ minWidth: 60 }}
|
||
>
|
||
<div
|
||
style={{ color: "#f85a3e", cursor: "pointer" }}
|
||
onClick={() => {
|
||
deleteAction(index);
|
||
}}
|
||
>
|
||
<DeleteIcon color="secondary" />
|
||
</div>
|
||
</Tooltip>
|
||
|
||
|
||
{ index === filteredActions.length - 1 || index === actionAmount - 1 ?
|
||
<Button
|
||
color="primary"
|
||
style={{ borderRadius: 0, position: "absolute", top: 70, }}
|
||
variant={actions.length === 0 ? "contained" : "outlined"}
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
|
||
setCurrentActionMethod(actionNonBodyRequest[0]);
|
||
setCurrentAction({
|
||
name: "",
|
||
description: "",
|
||
url: "",
|
||
file_field: "",
|
||
headers: "",
|
||
queries: [],
|
||
paths: [],
|
||
body: "",
|
||
errors: [],
|
||
method: actionNonBodyRequest[0],
|
||
action_label: ["No Label"],
|
||
required_bodyfields: [],
|
||
});
|
||
setActionsModalOpen(true);
|
||
}}
|
||
>
|
||
New action
|
||
</Button>
|
||
: null}
|
||
</Paper>
|
||
)
|
||
}
|
||
|
||
const LoopActions = (props) => {
|
||
const { filteredActions } = props;
|
||
|
||
//console.log("Actions: ", filteredActions)
|
||
if (filteredActions === null || filteredActions === undefined || filteredActions.length === 0) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
{filteredActions.slice(0, actionAmount).map((data, index) => {
|
||
//console.log("Found action: ", data)
|
||
return (
|
||
<ActionPaper key={index} index={index} data={data} />
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
|
||
const tagView = (
|
||
<div style={{ color: theme.palette.text.primary }}>
|
||
{/*
|
||
<ChipInput
|
||
style={{marginTop: 10}}
|
||
InputProps={{
|
||
style:{
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
placeholder="Categories"
|
||
color="primary"
|
||
fullWidth
|
||
value={newWorkflowCategories}
|
||
onAdd={(chip) => {
|
||
newWorkflowCategories.push(chip)
|
||
setNewWorkflowCategories(newWorkflowCategories)
|
||
setUpdate("added "+chip)
|
||
}}
|
||
onDelete={(chip, index) => {
|
||
newWorkflowCategories.splice(index, 1)
|
||
setNewWorkflowCategories(newWorkflowCategories)
|
||
setUpdate("delete "+chip)
|
||
}}
|
||
/>
|
||
*/}
|
||
<div style={{display: "flex", }}>
|
||
<div style={{flex: 2, }}>
|
||
<h4>Choose a Category</h4>
|
||
<Select
|
||
fullWidth
|
||
SelectDisplayProps={{
|
||
style: {
|
||
marginLeft: 10,
|
||
},
|
||
}}
|
||
onChange={(e) => {
|
||
setNewWorkflowCategories([e.target.value]);
|
||
setUpdate("added " + e.target.value);
|
||
}}
|
||
value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]}
|
||
|
||
style={{ backgroundColor: theme.palette.backgroundColor, color: theme.palette.text.primary, height: "50px" }}
|
||
>
|
||
{categories.map((data, index) => {
|
||
if (data === undefined || data === null || data === "" || data === undefined || data === null || data === "") {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<MenuItem
|
||
key={index}
|
||
sx={{ backgroundColor: theme.palette.backgroundColor, color: theme.palette.text.primary, "&:hover" : { backgroundColor: theme.palette.hoverColor} }}
|
||
value={data.name}
|
||
>
|
||
{data.name}
|
||
</MenuItem>
|
||
)
|
||
})}
|
||
</Select>
|
||
</div>
|
||
{/*
|
||
<div style={{flex: 1, marginLeft: 25, }}>
|
||
<Tooltip title="Helps when searching for apps. E.g. Google or Microsoft are their own groups." placement="top">
|
||
<h4 style={{marginBottom: 0, }}>Group</h4>
|
||
</Tooltip>
|
||
<TextField
|
||
style={{
|
||
}}
|
||
fullWidth
|
||
placeholder="Group"
|
||
type="text"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
defaultValue={newAppGroup}
|
||
onChange={(e) => {
|
||
setNewAppGroup(e.target.value)
|
||
setUpdate("group added "+e.target.value)
|
||
}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
/>
|
||
</div>
|
||
*/}
|
||
</div>
|
||
<h4>Tags</h4>
|
||
<MuiChipsInput
|
||
style={{ marginTop: 10 }}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
},
|
||
}}
|
||
placeholder="Tags"
|
||
color="primary"
|
||
fullWidth
|
||
value={newWorkflowTags}
|
||
onChange={(chips) => {
|
||
setNewWorkflowTags(chips)
|
||
setUpdate("added "+chips)
|
||
}}
|
||
|
||
/>
|
||
</div>
|
||
);
|
||
|
||
const ParsedActionHandler = () => {
|
||
const passedOrg = { id: "", name: "" };
|
||
const owner = "";
|
||
const passedTags = ["single test"];
|
||
|
||
const [, setUpdate] = useState();
|
||
const [authenticationModalOpen, setAuthenticationModalOpen] =
|
||
useState(false);
|
||
const [selectedApp, setSelectedApp] = useState({
|
||
versions: [
|
||
{
|
||
id: selectedAction.app_id,
|
||
version: selectedAction.app_version,
|
||
},
|
||
],
|
||
loop_versions: [selectedAction.app_version],
|
||
id: selectedAction.app_id,
|
||
name: selectedAction.app_name,
|
||
version: selectedAction.app_version,
|
||
});
|
||
|
||
const [requiresAuthentication, setRequiresAuthentication] = useState(
|
||
app.authentication !== null &&
|
||
app.authentication !== undefined &&
|
||
app.authentication.required &&
|
||
app.authentication.parameters !== undefined &&
|
||
app.authentication.parameters !== null
|
||
? true
|
||
: false
|
||
);
|
||
const [workflow, setWorkflow] = useState({
|
||
name: "",
|
||
description: "",
|
||
actions: [selectedAction],
|
||
start: selectedAction.id,
|
||
tags: passedTags,
|
||
execution_org: passedOrg,
|
||
org_id: passedOrg.id,
|
||
id: uuidv4(),
|
||
isValid: true,
|
||
owner: owner,
|
||
created: Date.now(),
|
||
});
|
||
|
||
const EndpointData = () => {
|
||
const [tmpVar, setTmpVar] = React.useState("");
|
||
|
||
return (
|
||
<div>
|
||
The API endpoint to use (URL) - predefined in the app
|
||
<TextField
|
||
style={{
|
||
backgroundColor: inputColor,
|
||
borderRadius: theme.palette?.borderRadius,
|
||
}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type="text"
|
||
color="primary"
|
||
disabled={true}
|
||
placeholder="Bearer token"
|
||
defaultValue={selectedApp.link}
|
||
onChange={(event) => {
|
||
setTmpVar(event.target.value);
|
||
}}
|
||
onBlur={() => {
|
||
selectedApp.link = tmpVar;
|
||
console.log("LINK: ", selectedApp.link);
|
||
setSelectedApp(selectedApp);
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const setAppActionAuthentication = (newauth) => {
|
||
if (app.authentication.required) {
|
||
var findAuthId = "";
|
||
if (
|
||
selectedAction.authentication_id !== null &&
|
||
selectedAction.authentication_id !== undefined &&
|
||
selectedAction.authentication_id.length > 0
|
||
) {
|
||
findAuthId = selectedAction.authentication_id;
|
||
}
|
||
|
||
var baseAuthOptions = [];
|
||
for (var key in newauth) {
|
||
var item = newauth[key];
|
||
|
||
const newfields = {};
|
||
for (var filterkey in item.fields) {
|
||
newfields[item.fields[filterkey].key] =
|
||
item.fields[filterkey].value;
|
||
}
|
||
|
||
item.fields = newfields;
|
||
if (item.app.name === app.name) {
|
||
baseAuthOptions.push(item);
|
||
|
||
if (item.id === findAuthId) {
|
||
selectedAction.selectedAuthentication = item;
|
||
}
|
||
}
|
||
}
|
||
|
||
selectedAction.authentication = baseAuthOptions;
|
||
//console.log("Authentication: ", authenticationOptions)
|
||
if (
|
||
selectedAction.selectedAuthentication === null ||
|
||
selectedAction.selectedAuthentication === undefined ||
|
||
selectedAction.selectedAuthentication.length === ""
|
||
) {
|
||
selectedAction.selectedAuthentication = {};
|
||
}
|
||
} else {
|
||
selectedAction.authentication = [];
|
||
selectedAction.authentication_id = "";
|
||
selectedAction.selectedAuthentication = {};
|
||
}
|
||
|
||
setSelectedAction(selectedAction);
|
||
console.log(selectedAction);
|
||
};
|
||
|
||
const getAppAuthentication = () => {
|
||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||
method: "GET",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Accept: "application/json",
|
||
},
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for apps :O!");
|
||
}
|
||
|
||
return response.json();
|
||
})
|
||
.then((responseJson) => {
|
||
if (
|
||
responseJson.success &&
|
||
responseJson.data !== undefined &&
|
||
responseJson.data !== null &&
|
||
responseJson.data.length !== 0
|
||
) {
|
||
var newauth = [];
|
||
for (var key in responseJson.data) {
|
||
if (responseJson.data[key].defined === false) {
|
||
continue;
|
||
}
|
||
|
||
newauth.push(responseJson.data[key]);
|
||
}
|
||
|
||
setAppAuthentication(newauth);
|
||
setAppActionAuthentication(newauth);
|
||
} else {
|
||
if (app.authentication.required) {
|
||
const tmpParams = selectedAction.parameters;
|
||
selectedAction.parameters = [];
|
||
|
||
for (var paramkey in app.authentication.parameters) {
|
||
var item = app.authentication.parameters[paramkey];
|
||
item.configuration = true;
|
||
|
||
const found = selectedAction.parameters.find(
|
||
(param) => param.name === item.name
|
||
);
|
||
if (found === null || found === undefined) {
|
||
selectedAction.parameters.push(item);
|
||
}
|
||
}
|
||
|
||
for (var paramkey in tmpParams) {
|
||
var item = tmpParams[paramkey];
|
||
//item.configuration = true
|
||
|
||
const found = selectedAction.parameters.find(
|
||
(param) => param.name === item.name
|
||
);
|
||
if (found === null || found === undefined) {
|
||
selectedAction.parameters.push(item);
|
||
}
|
||
}
|
||
|
||
setSelectedAction(selectedAction);
|
||
}
|
||
|
||
//toast("Failed getting authentications")
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
toast("Auth loading error: " + error.toString());
|
||
});
|
||
};
|
||
|
||
if (!authLoaded && appAuthentication.length === 0 && selectedAction.id !== undefined) {
|
||
setAuthLoaded(true);
|
||
getAppAuthentication();
|
||
}
|
||
|
||
/*
|
||
else if (selectedAction.id === undefined && currentAction.name !== undefined && currentAction.name !== null && currentAction.name.length > 0) {
|
||
var methodName = `${currentAction.method}_${currentAction.name}`;
|
||
if (currentAction.method.toLowerCase() === "custom" ||
|
||
currentAction.name.toLowerCase().startsWith(currentAction.method.toLowerCase())) {
|
||
methodName = currentAction.name;
|
||
}
|
||
|
||
methodName = methodName.toLowerCase().replaceAll(" ", "_");
|
||
if (app.actions !== null && app.actions !== undefined) {
|
||
var newselectedaction = app.actions.find(
|
||
(item) => item.name.toLowerCase().replaceAll(" ", "_") === methodName
|
||
);
|
||
|
||
if (newselectedaction !== undefined && newselectedaction !== null) {
|
||
newselectedaction.app_id = app.id;
|
||
newselectedaction.app_name = app.name;
|
||
newselectedaction.app_version = app.app_version;
|
||
newselectedaction.authentication = [];
|
||
newselectedaction.authentication_id = "";
|
||
newselectedaction.selectedAuthentication = {};
|
||
setSelectedAction(newselectedaction);
|
||
}
|
||
}
|
||
}
|
||
*/
|
||
|
||
const setNewAppAuth = (appAuthData) => {
|
||
//console.log("DAta: ", appAuthData)
|
||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||
method: "PUT",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Accept: "application/json",
|
||
},
|
||
body: JSON.stringify(appAuthData),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
if (response.status !== 200) {
|
||
console.log("Status not 200 for setting app auth :O!");
|
||
}
|
||
|
||
return response.json();
|
||
})
|
||
.then((responseJson) => {
|
||
if (!responseJson.success) {
|
||
toast("Failed to set app auth: " + responseJson.reason);
|
||
} else {
|
||
getAppAuthentication(true);
|
||
setAuthenticationModalOpen(false);
|
||
|
||
// Needs a refresh with the new authentication..
|
||
//toast("Successfully saved new app auth")
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
toast(error.toString());
|
||
});
|
||
};
|
||
|
||
const AuthenticationData = (props) => {
|
||
const selectedApp = props.app;
|
||
console.log("APP: ", selectedApp);
|
||
|
||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||
fields: {},
|
||
label: "",
|
||
usage: [
|
||
{
|
||
workflow_id: workflow.id,
|
||
},
|
||
],
|
||
id: uuidv4(),
|
||
active: true,
|
||
});
|
||
|
||
if (selectedApp.authentication === undefined) {
|
||
return null;
|
||
}
|
||
|
||
if (
|
||
selectedApp.authentication.parameters === null ||
|
||
selectedApp.authentication.parameters === undefined ||
|
||
selectedApp.authentication.parameters.length === 0
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
authenticationOption.app.actions = [];
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (
|
||
authenticationOption.fields[
|
||
selectedApp.authentication.parameters[key].name
|
||
] === undefined
|
||
) {
|
||
authenticationOption.fields[
|
||
selectedApp.authentication.parameters[key].name
|
||
] = "";
|
||
}
|
||
}
|
||
|
||
const handleSubmitCheck = () => {
|
||
//console.log("NEW AUTH: ", authenticationOption);
|
||
if (authenticationOption.label.length === 0) {
|
||
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||
//toast("Label can't be empty")
|
||
//return
|
||
}
|
||
|
||
for (var key in selectedApp.authentication.parameters) {
|
||
if (
|
||
authenticationOption.fields[
|
||
selectedApp.authentication.parameters[key].name
|
||
].length === 0
|
||
) {
|
||
toast(
|
||
"Field " +
|
||
selectedApp.authentication.parameters[key].name +
|
||
" can't be empty"
|
||
);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
console.log("Action: ", selectedAction);
|
||
selectedAction.authentication_id = authenticationOption.id;
|
||
selectedAction.selectedAuthentication = authenticationOption;
|
||
if (
|
||
selectedAction.authentication === undefined ||
|
||
selectedAction.authentication === null
|
||
) {
|
||
selectedAction.authentication = [authenticationOption];
|
||
} else {
|
||
selectedAction.authentication.push(authenticationOption);
|
||
}
|
||
|
||
setSelectedAction(selectedAction);
|
||
|
||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||
var newFields = [];
|
||
for (const key in newAuthOption.fields) {
|
||
const value = newAuthOption.fields[key];
|
||
newFields.push({
|
||
key: key,
|
||
value: value,
|
||
});
|
||
}
|
||
|
||
console.log("FIELDS: ", newFields);
|
||
newAuthOption.fields = newFields;
|
||
setNewAppAuth(newAuthOption);
|
||
//appAuthentication.push(newAuthOption)
|
||
//setAppAuthentication(appAuthentication)
|
||
//
|
||
|
||
setUpdate(authenticationOption.id);
|
||
|
||
/*
|
||
{selectedAction.authentication.map(data => (
|
||
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: theme.palette.text.primary}} value={data}>
|
||
*/
|
||
};
|
||
|
||
if (
|
||
authenticationOption.label === null ||
|
||
authenticationOption.label === undefined
|
||
) {
|
||
authenticationOption.label = selectedApp.name + " authentication";
|
||
}
|
||
|
||
console.log("PRE RETURN");
|
||
return (
|
||
<div>
|
||
<DialogContent>
|
||
<a
|
||
target="_blank"
|
||
rel="norefferer"
|
||
href="https://shuffler.io/docs/app_creation#authentication"
|
||
style={{ textDecoration: "none", color: theme.palette.linkColor }}
|
||
>
|
||
What is this?
|
||
</a>
|
||
<div />
|
||
These are required fields for authenticating with {selectedApp.name}
|
||
<div style={{ marginTop: 15 }} />
|
||
<b>Name - what is this used for?</b>
|
||
<TextField
|
||
style={{
|
||
backgroundColor: inputColor,
|
||
borderRadius: theme.palette?.borderRadius,
|
||
}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
color="primary"
|
||
placeholder={"Auth july 2020"}
|
||
defaultValue={`Auth for ${selectedApp.name}`}
|
||
onChange={(event) => {
|
||
authenticationOption.label = event.target.value;
|
||
}}
|
||
/>
|
||
{selectedApp.link.length > 0 ? (
|
||
<div style={{ marginTop: 15 }}>
|
||
<EndpointData />
|
||
</div>
|
||
) : null}
|
||
<Divider
|
||
style={{
|
||
marginTop: 15,
|
||
marginBottom: 15,
|
||
backgroundColor: "rgb(91, 96, 100)",
|
||
}}
|
||
/>
|
||
<div style={{}} />
|
||
{selectedApp.authentication.parameters.map((data, index) => {
|
||
return (
|
||
<div key={index} style={{ marginTop: 10 }}>
|
||
<LockOpenIcon style={{ marginRight: 10 }} />
|
||
<b>{data.name}</b>
|
||
<TextField
|
||
style={{
|
||
backgroundColor: inputColor,
|
||
borderRadius: theme.palette?.borderRadius,
|
||
}}
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
marginLeft: "5px",
|
||
maxWidth: "95%",
|
||
height: 50,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
fullWidth
|
||
type={
|
||
data.example !== undefined && data.example.includes("***")
|
||
? "password"
|
||
: "text"
|
||
}
|
||
color="primary"
|
||
placeholder={data.example}
|
||
onChange={(event) => {
|
||
authenticationOption.fields[data.name] =
|
||
event.target.value;
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
})}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{ borderRadius: "0px" }}
|
||
onClick={() => {
|
||
setAuthenticationModalOpen(false);
|
||
}}
|
||
color="primary"
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
style={{ borderRadius: "0px" }}
|
||
onClick={() => {
|
||
handleSubmitCheck();
|
||
}}
|
||
color="primary"
|
||
>
|
||
Submit
|
||
</Button>
|
||
</DialogActions>
|
||
</div>
|
||
);
|
||
};
|
||
};
|
||
|
||
const actionView = (
|
||
<div style={{ color: theme.palette.text.primary, position: "relative" }}>
|
||
<div style={{ position: "absolute", right: 0, top: 0 }}>
|
||
{actionAmount > 0 && actionAmount < filteredActions.length ? (
|
||
<Button
|
||
color="primary"
|
||
style={{ float: "right", borderRadius: 0, textAlign: "center" }}
|
||
variant="outlined"
|
||
onClick={() => {
|
||
setActionAmount(filteredActions.length);
|
||
/*
|
||
if (actionAmount+increaseAmount > filteredActions.length) {
|
||
setActionAmount(filteredActions.length)
|
||
} else {
|
||
setActionAmount(actionAmount+increaseAmount)
|
||
}
|
||
*/
|
||
}}
|
||
>
|
||
See all actions
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
<Typography variant="h6">
|
||
Actions{" "}
|
||
{actionAmount > 0 ? (
|
||
<span>
|
||
({actionAmount} / {filteredActions.length})
|
||
</span>
|
||
) : null}
|
||
</Typography>
|
||
<Typography variant="body2" style={{ marginTop: 10, marginBottom: 10 }}>
|
||
Actions are the tasks performed by an app - usually single URL paths for
|
||
REST API's.
|
||
</Typography>
|
||
|
||
{projectCategories !== undefined &&
|
||
projectCategories !== null &&
|
||
projectCategories.length > 1 ? (
|
||
<div style={{ marginTop: 10, marginBottom: 10 }}>
|
||
{projectCategories.map((tag, index) => {
|
||
const newname = tag.charAt(0).toUpperCase() + tag.slice(1);
|
||
|
||
//var regex = /_shuffle_replace_\d/i;
|
||
////console.log("NEW: ",
|
||
//newname = newname.replaceAll(regex, "")
|
||
//console.log("Replaced: ", newname)
|
||
|
||
return (
|
||
<Chip
|
||
key={index}
|
||
style={{
|
||
backgroundColor:
|
||
tag === selectedCategory ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||
height: 30,
|
||
margin: 3,
|
||
paddingLeft: 5,
|
||
paddingRight: 5,
|
||
cursor: "pointer",
|
||
borderColor: theme.palette.chipStyle.borderColor,
|
||
color: theme.palette.chipStyle.color,
|
||
}}
|
||
label={newname}
|
||
onClick={() => {
|
||
if (selectedCategory === tag) {
|
||
setFilteredActions(actions);
|
||
setSelectedCategory("");
|
||
setActionAmount(50);
|
||
return;
|
||
}
|
||
|
||
const foundActions = actions.filter(
|
||
(data) => data.category === tag
|
||
);
|
||
setFilteredActions(foundActions);
|
||
setSelectedCategory(tag);
|
||
if (actionAmount > foundActions.length) {
|
||
setActionAmount(foundActions.length);
|
||
}
|
||
|
||
//{filteredActions.slice(0,actionAmount).map((data, index) => {
|
||
|
||
//console.log("Found: ", foundActions)
|
||
//{filteredActions.slice(0,actionAmount).map((data, index) => {
|
||
//{actions.slice(0,actionAmount).map((data, index) => {
|
||
}}
|
||
variant={selectedCategory === tag ? "contained " : "outlined"}
|
||
color={selectedCategory === tag ? "primary" : "secondary"}
|
||
/>
|
||
);
|
||
})}
|
||
</div>
|
||
) : null}
|
||
<div>
|
||
<LoopActions filteredActions={filteredActions} />
|
||
{ actions.length === 0 || filteredActions.length === 0 ?
|
||
<Button
|
||
color="primary"
|
||
style={{ borderRadius: 0, marginTop: 15, marginBottom: 10, }}
|
||
variant={actions.length === 0 ? "contained" : "outlined"}
|
||
onClick={(e) => {
|
||
actions.push({
|
||
name: "Change Me",
|
||
description: "",
|
||
url: "",
|
||
file_field: "",
|
||
headers: "",
|
||
queries: [],
|
||
paths: [],
|
||
body: "",
|
||
errors: [],
|
||
method: actionNonBodyRequest[0],
|
||
action_label: ["No Label"],
|
||
required_bodyfields: [],
|
||
});
|
||
|
||
console.log("Added: ", actions)
|
||
setActions(actions)
|
||
setFilteredActions(actions)
|
||
setActionAmount(50);
|
||
setUpdate(Math.random());
|
||
}}
|
||
>
|
||
New action
|
||
</Button>
|
||
: null}
|
||
{/*
|
||
<div style={{ display: "flex" }}>
|
||
<Button
|
||
color="primary"
|
||
style={{ marginTop: "20px", borderRadius: "0px" }}
|
||
variant={actions.length === 0 ? "contained" : "outlined"}
|
||
onClick={() => {
|
||
//actions.push({
|
||
// name: "Change name",
|
||
// description: "",
|
||
// url: "",
|
||
// file_field: "",
|
||
// headers: "",
|
||
// queries: [],
|
||
// paths: [],
|
||
// body: "",
|
||
// errors: [],
|
||
// method: actionNonBodyRequest[0],
|
||
// action_label: ["No Label"],
|
||
// required_bodyfields: [],
|
||
//})
|
||
//setActions(actions)
|
||
//setFilteredActions(actions)
|
||
//setUpdate(Math.random());
|
||
|
||
//const foundPaper = document.getElementById("Change name");
|
||
//if (foundPaper !== null) {
|
||
// console.log("Found: ", foundPaper)
|
||
//} else {
|
||
// console.log("Not found")
|
||
//}
|
||
|
||
// Find the item and click if possible
|
||
|
||
//setCurrentActionMethod(actionNonBodyRequest[0]);
|
||
//setCurrentAction({
|
||
// name: "",
|
||
// description: "",
|
||
// url: "",
|
||
// file_field: "",
|
||
// headers: "",
|
||
// queries: [],
|
||
// paths: [],
|
||
// body: "",
|
||
// errors: [],
|
||
// method: actionNonBodyRequest[0],
|
||
//});
|
||
//setActionsModalOpen(true);
|
||
}}
|
||
>
|
||
New action
|
||
</Button>
|
||
{actionAmount} {actions.length}
|
||
</div>
|
||
*/}
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
const testView = (
|
||
<div style={{ color: theme.palette.text.primary }}>
|
||
<h2>Test</h2>
|
||
Test an action to see whether it performs in an expected way.
|
||
<a
|
||
target="_blank"
|
||
href="https://shuffler.io/docs/app_creation#testing"
|
||
style={{ textDecoration: "none", color: theme.palette.linkColor }}
|
||
>
|
||
TBD: Click here to learn more about testing
|
||
</a>
|
||
.<div>Test :)</div>
|
||
</div>
|
||
);
|
||
|
||
var image = "";
|
||
const editHeaderImage = (event) => {
|
||
const file = event.target.value;
|
||
const actualFile = event.target.files[0];
|
||
const fileObject = URL.createObjectURL(actualFile);
|
||
setFile(fileObject);
|
||
};
|
||
|
||
if (file !== "") {
|
||
const img = document.getElementById("logo");
|
||
var canvas = document.createElement("canvas");
|
||
canvas.width = 174;
|
||
canvas.height = 174;
|
||
var ctx = canvas.getContext("2d");
|
||
|
||
if (img)
|
||
img.onload = function () {
|
||
// img, x, y, width, height
|
||
//ctx.drawImage(img, 174, 174)
|
||
//console.log("IMG natural: ", img.naturalWidth, img.naturalHeight)
|
||
//ctx.drawImage(img, 0, 0, 174, 174)
|
||
ctx.drawImage(
|
||
img,
|
||
0,
|
||
0,
|
||
img.width,
|
||
img.height,
|
||
0,
|
||
0,
|
||
canvas.width,
|
||
canvas.height
|
||
);
|
||
|
||
try {
|
||
const canvasUrl = canvas.toDataURL();
|
||
if (canvasUrl !== fileBase64) {
|
||
//console.log("SET URL TO: ", canvasUrl)
|
||
setFileBase64(canvasUrl);
|
||
}
|
||
} catch (e) {
|
||
toast("Failed to parse canvasurl!");
|
||
}
|
||
};
|
||
|
||
//console.log(img.width)
|
||
//console.log(img.width)
|
||
//canvas.width = img.width
|
||
//canvas.height = img.height
|
||
}
|
||
|
||
const [imageUploadError, setImageUploadError] = useState("");
|
||
const [openImageModal, setOpenImageModal] = useState("");
|
||
const [scale, setScale] = useState(1);
|
||
const [rotate, setRotatation] = useState(0);
|
||
const [disableImageUpload, setDisableImageUpload] = useState(true);
|
||
|
||
let imageData = fileBase64;
|
||
let croppedData = file.length > 0 ? file : fileBase64;
|
||
|
||
const imageInfo = (
|
||
<img
|
||
crossOrigin="anonymous"
|
||
src={imageData}
|
||
id="logo"
|
||
style={{
|
||
maxWidth: 174,
|
||
maxHeight: 174,
|
||
minWidth: 174,
|
||
minHeight: 174,
|
||
objectFit: "contain",
|
||
borderRadius: theme.palette?.borderRadius,
|
||
}}
|
||
/>
|
||
);
|
||
|
||
const alternateImg = (
|
||
<AddPhotoAlternateIcon
|
||
style={{
|
||
width: 100,
|
||
height: 100,
|
||
flex: "1",
|
||
display: "flex",
|
||
flexDirection: "row",
|
||
margin: "auto",
|
||
marginTop: 30,
|
||
marginLeft: 40,
|
||
borderRadius: theme.palette?.borderRadius,
|
||
}}
|
||
onClick={() => {
|
||
upload.click();
|
||
}}
|
||
/>
|
||
);
|
||
|
||
const zoomIn = () => {
|
||
console.log("ZOOOMING IN");
|
||
setScale(scale + 0.1);
|
||
};
|
||
|
||
const zoomOut = () => {
|
||
setScale(scale - 0.1);
|
||
};
|
||
const rotatation = () => {
|
||
setRotatation(rotate + 10);
|
||
};
|
||
|
||
const onPositionChange = () => {
|
||
setDisableImageUpload(false);
|
||
};
|
||
|
||
const onCancelSaveAppIcon = () => {
|
||
setFile("");
|
||
setOpenImageModal(false);
|
||
setImageUploadError("");
|
||
};
|
||
|
||
let editor;
|
||
const setEditorRef = (imgEditor) => {
|
||
editor = imgEditor;
|
||
};
|
||
|
||
const onSaveAppIcon = () => {
|
||
if (editor) {
|
||
try {
|
||
setFile("");
|
||
const canvas = editor.getImageScaledToCanvas();
|
||
setFileBase64(canvas.toDataURL());
|
||
setOpenImageModal(false);
|
||
setDisableImageUpload(true);
|
||
} catch (e) {
|
||
toast("Failed to set image. Replace it if this persists.");
|
||
}
|
||
}
|
||
};
|
||
|
||
const errorText =
|
||
imageUploadError.length > 0 ? (
|
||
<div style={{ marginTop: 10 }}>Error: {imageUploadError}</div>
|
||
) : null;
|
||
|
||
const imageUploadModalView = openImageModal ? (
|
||
<Dialog
|
||
open={openImageModal}
|
||
onClose={onCancelSaveAppIcon}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: theme.palette.text.primary,
|
||
minWidth: "300px",
|
||
minHeight: "300px",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle>
|
||
<div style={{ color: "rgba(255,255,255,0.9)" }}>Upload App Icon</div>
|
||
</DialogTitle>
|
||
{errorText}
|
||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||
<AvatarEditor
|
||
ref={setEditorRef}
|
||
image={croppedData}
|
||
width={174}
|
||
height={174}
|
||
border={50}
|
||
color={[0, 0, 0, 0.6]} // RGBA
|
||
scale={scale}
|
||
rotate={rotate}
|
||
onImageChange={onPositionChange}
|
||
onLoadSuccess={() => setRotatation(0)}
|
||
/>
|
||
<Divider style={dividerStyle} />
|
||
<Tooltip title={"New Icon"}>
|
||
<Button
|
||
variant="outlined"
|
||
component="label"
|
||
color="primary"
|
||
style={appIconStyle}
|
||
>
|
||
<AddAPhotoOutlinedIcon
|
||
onClick={() => {
|
||
upload.click();
|
||
}}
|
||
color="primary"
|
||
/>
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title={"Zoom In"}>
|
||
<Button
|
||
variant="outlined"
|
||
component="label"
|
||
color="primary"
|
||
style={appIconStyle}
|
||
>
|
||
<ZoomInOutlinedIcon onClick={zoomIn} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title={"Zoom out"}>
|
||
<Button
|
||
variant="outlined"
|
||
component="label"
|
||
color="primary"
|
||
style={appIconStyle}
|
||
>
|
||
<ZoomOutOutlinedIcon onClick={zoomOut} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title={"Rotate"}>
|
||
<Button
|
||
variant="outlined"
|
||
component="label"
|
||
color="primary"
|
||
style={appIconStyle}
|
||
>
|
||
<LoopIcon onClick={rotatation} color="primary" />
|
||
</Button>
|
||
</Tooltip>
|
||
<Divider style={dividerStyle} />
|
||
</DialogContent>
|
||
<DialogActions>
|
||
<Button
|
||
style={{ borderRadius: "0px" }}
|
||
onClick={onCancelSaveAppIcon}
|
||
color="primary"
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
style={{ borderRadius: "0px" }}
|
||
disabled={disableImageUpload}
|
||
onClick={() => {
|
||
onSaveAppIcon();
|
||
}}
|
||
color="primary"
|
||
>
|
||
Continue
|
||
</Button>
|
||
</DialogActions>
|
||
{/*<ParsedActionHandler />*/}
|
||
</FormControl>
|
||
</Dialog>
|
||
) : null;
|
||
|
||
const validateRemote = () => {
|
||
setValidation(true);
|
||
|
||
fetch(globalUrl + "/api/v1/get_openapi_uri", {
|
||
method: "POST",
|
||
headers: {
|
||
Accept: "application/json",
|
||
},
|
||
body: JSON.stringify(openApi),
|
||
credentials: "include",
|
||
})
|
||
.then((response) => {
|
||
setValidation(false);
|
||
if (response.status !== 200) {
|
||
return response.json();
|
||
}
|
||
|
||
return response.text();
|
||
})
|
||
.then((responseJson) => {
|
||
if (typeof responseJson !== "string" && !responseJson.success) {
|
||
console.log(responseJson.reason);
|
||
if (responseJson.reason !== undefined) {
|
||
setOpenApiError(responseJson.reason);
|
||
} else {
|
||
setOpenApiError("Undefined issue with OpenAPI validation");
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log("Validating response!");
|
||
validateOpenApi(responseJson);
|
||
})
|
||
.catch((error) => {
|
||
toast(error.toString());
|
||
setOpenApiError(error.toString());
|
||
});
|
||
}
|
||
|
||
const circularLoader = validation ? (
|
||
<CircularProgress color="primary" />
|
||
) : null;
|
||
|
||
const newApimodalView = openApiModal ?
|
||
<Dialog
|
||
open={openApiModal}
|
||
onClose={() => {
|
||
setOpenApiModal(false)
|
||
}}
|
||
PaperProps={{
|
||
style: {
|
||
backgroundColor: surfaceColor,
|
||
color: theme.palette.text.primary,
|
||
minWidth: "800px",
|
||
minHeight: "320px",
|
||
},
|
||
}}
|
||
>
|
||
<FormControl>
|
||
<DialogTitle>
|
||
<div style={{ color: "rgba(255,255,255,0.9)" }}>
|
||
Merge with another OpenAPI document. You will get to choose Actions before they are merged.
|
||
</div>
|
||
</DialogTitle>
|
||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||
Paste in the URI for the OpenAPI
|
||
<TextField
|
||
style={{ backgroundColor: inputColor }}
|
||
variant="outlined"
|
||
margin="normal"
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.text.primary,
|
||
height: "50px",
|
||
fontSize: "1em",
|
||
},
|
||
endAdornment: (
|
||
<Button
|
||
style={{
|
||
borderRadius: "0px",
|
||
marginTop: "0px",
|
||
height: "50px",
|
||
}}
|
||
variant="contained"
|
||
disabled={openApi.length === 0 || appValidation.length > 0}
|
||
color="primary"
|
||
onClick={() => {
|
||
setOpenApiError("");
|
||
validateRemote();
|
||
}}
|
||
>
|
||
Validate
|
||
</Button>
|
||
),
|
||
}}
|
||
onChange={(e) => {
|
||
setOpenApi(e.target.value);
|
||
}}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Must point to a version 2 or 3 OpenAPI specification.
|
||
</span>
|
||
}
|
||
placeholder="OpenAPI URI"
|
||
fullWidth
|
||
/>
|
||
{/*
|
||
<div style={{marginTop: "15px"}}/>
|
||
Example:
|
||
<div />
|
||
https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/uber.json
|
||
*/}
|
||
<p>Or upload a YAML/JSON specification</p>
|
||
<input
|
||
hidden
|
||
type="file"
|
||
ref={newUpload}
|
||
accept="application/JSON,application/YAML,application/yaml,text/yaml,text/x-yaml,application/x-yaml,application/vnd.yaml,.yml,.yaml"
|
||
multiple={false}
|
||
onChange={uploadFile}
|
||
/>
|
||
<Button
|
||
variant="contained"
|
||
color="primary"
|
||
onClick={() => newUpload.current.click()}
|
||
>
|
||
Upload
|
||
</Button>
|
||
{errorText}
|
||
</DialogContent>
|
||
<DialogActions>
|
||
{circularLoader}
|
||
<Button
|
||
style={{ borderRadius: "0px" }}
|
||
onClick={() => {
|
||
setOpenApiModal(false);
|
||
setAppValidation("");
|
||
setOpenApiError("");
|
||
setOpenApi("");
|
||
setOpenApiData("");
|
||
}}
|
||
color="primary"
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
style={{ borderRadius: "0px" }}
|
||
disabled={appValidation.length === 0}
|
||
onClick={() => {
|
||
redirectOpenApi();
|
||
}}
|
||
color="primary"
|
||
>
|
||
Continue
|
||
</Button>
|
||
</DialogActions>
|
||
</FormControl>
|
||
</Dialog>
|
||
: null
|
||
|
||
// Random names for type & autoComplete. Didn't research :^)
|
||
const landingpageDataBrowser = (
|
||
<div style={{ paddingBottom: 100, color: theme.palette.text.primary, }}>
|
||
<Breadcrumbs
|
||
aria-label="breadcrumb"
|
||
separator="›"
|
||
style={{ color: theme.palette.text.primary }}
|
||
>
|
||
<Link to="/apps" style={{ textDecoration: "none", color: "inherit" }}>
|
||
<h2 style={{ color: theme.palette.textColor }}>
|
||
<AppsIcon style={{ marginRight: 10 }} />
|
||
Apps
|
||
</h2>
|
||
</Link>
|
||
<h2>
|
||
<Link to={props?.match?.params?.appid !== undefined && props?.match?.params?.appid !== null && props?.match?.params?.appid.length > 0 ? `/apps/${props?.match?.params?.appid}` : "/apps" } style={{ textDecoration: "none", color: theme.palette.text.primary }}>
|
||
{name}{" "}
|
||
</Link>
|
||
{actions === null ||
|
||
actions === undefined ||
|
||
actions.length === 0 ? null : (
|
||
<span>({actions.length})</span>
|
||
)}
|
||
</h2>
|
||
</Breadcrumbs>
|
||
{imageUploadModalView}
|
||
<input
|
||
hidden
|
||
type="file"
|
||
ref={(ref) => (upload = ref)}
|
||
onChange={editHeaderImage}
|
||
/>
|
||
<div style={boxStyle}>
|
||
<div style={{display: "flex", }}>
|
||
<div style={{flex: 1, }}>
|
||
<h2 style={{ marginBottom: "10px", color: theme.palette.text.primary }}>
|
||
General information
|
||
</h2>
|
||
</div>
|
||
<div style={{flex: 1, itemAlign: "right", textAlign: "right", marginRight: 10, }}>
|
||
<Tooltip title="Try the API" placement="bottom">
|
||
<Button
|
||
variant="outlined"
|
||
color="secondary"
|
||
onClick={() => {
|
||
var urlParams = new URLSearchParams(window.location.search)
|
||
if (urlParams.has("id")) {
|
||
window.open(`/apis/${urlParams.get("id")}`, "_blank")
|
||
} else if (props.match.params.appid !== undefined && props.match.params.appid !== null && props.match.params.appid.length > 0) {
|
||
window.open(`/apis/${props.match.params.appid}`, "_blank")
|
||
} else {
|
||
toast.error("Build the app first.")
|
||
}
|
||
|
||
}}
|
||
>
|
||
<OpenInNewIcon style={{marginRight: 5, }}/>
|
||
Try the API
|
||
</Button>
|
||
</Tooltip>
|
||
<Tooltip title="Manage forks and Merge with another API (coming soon)" placement="bottom">
|
||
<IconButton
|
||
onClick={() => {
|
||
//setOpenApiModal(true)
|
||
toast.info("Action merging & fork management coming soon")
|
||
}}
|
||
disabled={true}
|
||
style={{marginLeft: 10, }}
|
||
>
|
||
<CallMergeIcon
|
||
style={{}}
|
||
/>
|
||
</IconButton>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
<a
|
||
target="_blank"
|
||
href="https://shuffler.io/docs/app_creation#app-creator-instructions"
|
||
style={{ textDecoration: "none", color: theme.palette.linkColor }}
|
||
>
|
||
Click to learn more about app creation
|
||
</a>
|
||
<div
|
||
style={{
|
||
color: theme.palette.text.primary,
|
||
flex: "1",
|
||
display: "flex",
|
||
flexDirection: "row",
|
||
}}
|
||
>
|
||
<Tooltip title="Click to edit the app's image" placement="bottom">
|
||
<div
|
||
style={{
|
||
flex: "1",
|
||
margin: 10,
|
||
border: "1px solid #f85a3e",
|
||
borderRadius: theme.palette?.borderRadius,
|
||
cursor: "pointer",
|
||
backgroundColor: inputColor,
|
||
maxWidth: 174,
|
||
maxHeight: 174,
|
||
}}
|
||
onClick={() => {
|
||
setOpenImageModal(true);
|
||
}}
|
||
>
|
||
{imageData ? imageInfo : alternateImg}
|
||
<input
|
||
hidden
|
||
type="file"
|
||
ref={(ref) => (upload = ref)}
|
||
onChange={editHeaderImage}
|
||
/>
|
||
</div>
|
||
</Tooltip>
|
||
<div style={{ flex: "3", color: theme.palette.text.primary, marginLeft: 20, }}>
|
||
<div style={{ marginTop: "10px" }} />
|
||
Name
|
||
<TextField
|
||
required
|
||
style={{
|
||
flex: "1",
|
||
marginTop: "5px",
|
||
marginRight: "15px",
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
}}
|
||
fullWidth={true}
|
||
placeholder="Name"
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={name}
|
||
onChange={(e) => {
|
||
const invalid = ["#", ":", "."];
|
||
for (var key in invalid) {
|
||
if (e.target.value.includes(invalid[key])) {
|
||
toast("Can't use " + invalid[key] + " in name");
|
||
setName(e.target.value.replaceAll(".", "").replaceAll("#", "").replaceAll(":", "").replaceAll(",", ""))
|
||
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (e.target.value.length > 29) {
|
||
toast("Choose a shorter name (max 29).");
|
||
setName(e.target.value.slice(0,28))
|
||
return;
|
||
}
|
||
|
||
//e.target.value.trim()
|
||
|
||
setName(e.target.value);
|
||
}}
|
||
onBlur={(e) => {
|
||
setName(e.target.value.trim());
|
||
}}
|
||
color="primary"
|
||
InputProps={{
|
||
style: {
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
height: "50px",
|
||
fontSize: "1em",
|
||
},
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
}}
|
||
/>
|
||
<div style={{ marginTop: 10, }} />
|
||
Description
|
||
<TextField
|
||
required
|
||
style={{
|
||
marginTop: 5,
|
||
marginRight: 15,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
maxHeight: 250,
|
||
overflowY: "auto"
|
||
}}
|
||
fullWidth={true}
|
||
type="name"
|
||
id="outlined-with-placeholder"
|
||
inputProps={{
|
||
style: {
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
}
|
||
}}
|
||
margin="normal"
|
||
multiline
|
||
variant="outlined"
|
||
placeholder="A description for the service"
|
||
defaultValue={description}
|
||
//onChange={(e) => setDescription(e.target.value)}
|
||
onBlur={(e) => setDescription(e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<Divider
|
||
style={{
|
||
marginBottom: 10,
|
||
marginTop: 30,
|
||
height: 1,
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}}
|
||
/>
|
||
<Typography
|
||
variant="h6"
|
||
style={{ marginTop: 10, marginBottom: 10, color: theme.palette.text.primary }}
|
||
>
|
||
API information
|
||
</Typography>
|
||
<Typography variant="body1">
|
||
Base URL - used as suggestion to the user
|
||
</Typography>
|
||
<TextField
|
||
color="primary"
|
||
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: "5px" }}
|
||
InputProps={{
|
||
classes: {
|
||
notchedOutline: classes.notchedOutline,
|
||
},
|
||
style: {
|
||
height: "50px",
|
||
color: theme.palette.textFieldStyle.color,
|
||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||
fontSize: "1em",
|
||
},
|
||
}}
|
||
required
|
||
fullWidth={true}
|
||
type="name"
|
||
id="standard-required"
|
||
margin="normal"
|
||
variant="outlined"
|
||
value={baseUrl}
|
||
helperText={
|
||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||
Must start with http(s):// and can NOT end with /.{" "}
|
||
</span>
|
||
}
|
||
placeholder="https://api.example.com"
|
||
onChange={(e) => setBaseUrl(e.target.value)}
|
||
onBlur={(event) => {
|
||
var tmpstring = event.target.value.trim();
|
||
if (tmpstring.endsWith("/")) {
|
||
tmpstring = tmpstring.slice(0, -1);
|
||
}
|
||
|
||
if (
|
||
tmpstring.length > 4 &&
|
||
!tmpstring.startsWith("http") &&
|
||
!tmpstring.startsWith("ftp")
|
||
) {
|
||
toast("URL must start with http(s)://");
|
||
}
|
||
|
||
if (tmpstring.includes("?")) {
|
||
var newtmp = tmpstring.split("?")
|
||
if (tmpstring.length > 1) {
|
||
tmpstring = newtmp[0]
|
||
}
|
||
}
|
||
|
||
setBaseUrl(tmpstring);
|
||
}}
|
||
/>
|
||
<div style={{padding: 25, border: "2px solid rgba(255,255,255,0.7)", borderRadius: theme.palette?.borderRadius, }}>
|
||
<span style={{display: "flex", }}>
|
||
<FormControl style={{ }} variant="outlined">
|
||
<Typography variant="h6">Authentication</Typography>
|
||
<a
|
||
target="_blank"
|
||
href="https://shuffler.io/docs/app_creation#authentication"
|
||
style={{ textDecoration: "none", color: theme.palette.linkColor }}
|
||
>
|
||
Learn more about app authentication
|
||
</a>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setAuthenticationOption(e.target.value);
|
||
if (e.target.value === "No authentication") {
|
||
setAuthenticationRequired(false);
|
||
} else {
|
||
setAuthenticationRequired(true);
|
||
}
|
||
|
||
if (e.target.value === "Oauth2") {
|
||
if (parameterLocation === "Header") {
|
||
setParameterLocation("")
|
||
}
|
||
|
||
setExtraAuth([])
|
||
}
|
||
}}
|
||
value={authenticationOption}
|
||
style={{
|
||
backgroundColor: theme.palette.backgroundColor,
|
||
color: theme.palette.text.primary,
|
||
height: "50px",
|
||
}}
|
||
>
|
||
{authenticationOptions.map((data, index) => (
|
||
<MenuItem
|
||
key={index}
|
||
sx={{ backgroundColor: theme.palette.backgroundColor, color: theme.palette.text.primary, "&:hover": {backgroundColor: theme.palette.backgroundColor} }}
|
||
value={data}
|
||
>
|
||
{data}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
{authenticationOption === "Oauth2" ?
|
||
<FormControl style={{ marginLeft: 310, maxWidth: 240, }} variant="outlined">
|
||
{/*
|
||
<Typography variant="body2">
|
||
- Delegated: The user will get a popup for access their personal data.
|
||
- Application: Permissions are set by the app creator in the 3rd party platform.
|
||
</Typography>
|
||
*/}
|
||
<div style={{display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "space-between", }}>
|
||
<Typography variant="body2" style={{ marginTop: 10, marginRight: 10, }} color="textSecondary">Oauth2 type</Typography>
|
||
<Select
|
||
fullWidth
|
||
onChange={(e) => {
|
||
setOauth2Type(e.target.value)
|
||
|
||
if (e.target.value === "application" && oauth2GrantType === "") {
|
||
setOauth2GrantType("client_credentials")
|
||
setParameterName("")
|
||
}
|
||
|
||
if (e.target.value === "delegated") {
|
||
setOauth2GrantType("")
|
||
}
|
||
}}
|
||
value={oauth2Type}
|
||
style={{
|
||
backgroundColor: inputColor,
|
||
color: theme.palette.text.primary,
|
||
height: "50px",
|
||
}}
|
||
>
|
||
{["delegated", "application"].map((data, index) => (
|
||
<MenuItem
|
||
key={index}
|
||
style={{ backgroundColor: inputColor, color: theme.palette.text.primary }}
|
||
value={data}
|
||
>
|
||
{data}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
|
||
{oauth2Type === "application" ?
|
||
<div style={{display: "flex", }}>
|
||
<Typography variant="body2" style={{ marginTop: 10, marginRight: 10, }} color="textSecondary">Grant Type</Typography>
|
||
<Select
|
||
fullWidth
|
||
label="Grant Type"
|
||
onChange={(e) => {
|
||
setOauth2GrantType(e.target.value);
|
||
}}
|
||
value={oauth2GrantType}
|
||
style={{
|
||
backgroundColor: inputColor,
|
||
color: theme.palette.text.primary,
|
||
height: "50px",
|
||
}}
|
||
>
|
||
{["client_credentials", "password"].map((data, index) => (
|
||
<MenuItem
|
||
key={index}
|
||
style={{ backgroundColor: inputColor, color: theme.palette.text.primary }}
|
||
value={data}
|
||
>
|
||
{data}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</div>
|
||
: null}
|
||
</FormControl>
|
||
: null}
|
||
</span>
|
||
|
||
|
||
<div style={{marginTop: 15 }} />
|
||
{basicAuth}
|
||
{bearerAuth}
|
||
{apiKey}
|
||
{oauth2Auth}
|
||
{jwtAuth}
|
||
{extraKeys}
|
||
</div>
|
||
|
||
{/*authenticationOption === "No authentication" ? null :
|
||
<FormControlLabel
|
||
style={{color: theme.palette.text.primary, marginBottom: 0, marginTop: 20}}
|
||
label=<div style={{color: theme.palette.text.primary}}>Authentication required (default true)</div>
|
||
control={<Switch checked={authenticationRequired} onChange={() => {
|
||
setAuthenticationRequired(!authenticationRequired)
|
||
}} />}
|
||
/>*/}
|
||
<Divider
|
||
style={{
|
||
marginBottom: "10px",
|
||
marginTop: "30px",
|
||
height: "1px",
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}}
|
||
/>
|
||
<div style={{ marginTop: "25px" }}>{actionView}</div>
|
||
|
||
<Divider
|
||
style={{
|
||
marginBottom: 10,
|
||
marginTop: 70,
|
||
height: 1,
|
||
width: "100%",
|
||
backgroundColor: "grey",
|
||
}}
|
||
/>
|
||
<div style={{ marginTop: "25px" }}>{tagView}</div>
|
||
{/*
|
||
{/*
|
||
<Divider style={{marginBottom: "10px", marginTop: "30px", height: "1px", width: "100%", backgroundColor: "grey"}}/>
|
||
{testView}
|
||
*/}
|
||
|
||
<div style={{height: isCloud ? 50 : 80, padding: 15, display: "flex", marginTop: 35, position: "fixed", bottom: 0, left: 0, width: "100%", backgroundColor: theme.palette?.backgroundColor, borderTop: "1px solid rgba(255,255,255,0.3)",}}>
|
||
<div style={{width: 450, margin: "auto", display: "flex", textAlign: "center", }}>
|
||
{appDownloadData.length > 0 ?
|
||
<Tooltip title="Download the OpenAPI specification for the App" placement="bottom">
|
||
<IconButton
|
||
style={{marginRight: 25, }}
|
||
onClick={() => {
|
||
toast(`Downloading OpenAPI JSON data for for ${name}`)
|
||
// Download as file
|
||
var blob = new Blob([appDownloadData], {
|
||
type: "application/octet-stream",
|
||
});
|
||
|
||
var url = URL.createObjectURL(blob);
|
||
var link = document.createElement("a");
|
||
link.setAttribute("href", url);
|
||
link.setAttribute("download", `${name}.json`);
|
||
var event = document.createEvent("MouseEvents");
|
||
event.initMouseEvent(
|
||
"click",
|
||
true,
|
||
true,
|
||
window,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
false,
|
||
false,
|
||
false,
|
||
false,
|
||
0,
|
||
null
|
||
);
|
||
link.dispatchEvent(event);
|
||
}}
|
||
>
|
||
<CloudDownloadIcon />
|
||
</IconButton>
|
||
</Tooltip>
|
||
: null}
|
||
<Button
|
||
disabled={appBuilding}
|
||
color="primary"
|
||
variant="contained"
|
||
style={{ height: 50, flex: 1, minWidth: 400, maxWidth: 400, }}
|
||
onClick={() => {
|
||
submitApp();
|
||
}}
|
||
>
|
||
{appBuilding ? <CircularProgress /> : "Save API"}
|
||
</Button>
|
||
{appDownloadData.length > 0 ?
|
||
<div style={{width: 50, }}/>
|
||
: null}
|
||
</div>
|
||
</div>
|
||
|
||
<Typography style={{ marginTop: 25, textAlign: "center", }}>
|
||
{errorCode.length > 0 ? `Upload Error: ${errorCode}` : null}
|
||
</Typography>
|
||
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
const loadedCheck =
|
||
isLoaded && isAppLoaded ? (
|
||
<div>
|
||
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
|
||
{newApimodalView}
|
||
</div>
|
||
) : (
|
||
<div></div>
|
||
);
|
||
|
||
return <div>{loadedCheck}</div>;
|
||
};
|
||
export default AppCreator;
|