import React, {useState, useEffect} from 'react'; import { makeStyles } from '@material-ui/styles'; import {BrowserView, MobileView} from "react-device-detect"; import Paper from '@material-ui/core/Paper'; import Button from '@material-ui/core/Button'; import Divider from '@material-ui/core/Divider'; import Select from '@material-ui/core/Select'; import MenuItem from '@material-ui/core/MenuItem'; import FormControl from '@material-ui/core/FormControl'; import Dialog from '@material-ui/core/Dialog'; import DialogTitle from '@material-ui/core/DialogTitle'; import DialogContent from '@material-ui/core/DialogContent'; import DialogActions from '@material-ui/core/DialogActions'; import TextField from '@material-ui/core/TextField'; import Tooltip from '@material-ui/core/Tooltip'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; import ErrorOutline from '@material-ui/icons/ErrorOutline'; import { useAlert } from "react-alert"; const surfaceColor = "#27292D" const inputColor = "#383B40" const bodyDivStyle = { margin: "auto", width: "900px", } const actionListStyle = { paddingLeft: "10px", paddingRight: "10px", paddingBottom: "10px", paddingTop: "10px", marginTop: "5px", backgroundColor: inputColor, display: "flex", color: "white", } const boxStyle = { flex: "1", marginLeft: "10px", marginRight: "10px", paddingLeft: "30px", paddingRight: "30px", paddingBottom: "30px", paddingTop: "30px", display: "flex", flexDirection: "column", backgroundColor: surfaceColor, } const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important" }, }); // Should be different if logged in :| const AppCreator = (props) => { const { globalUrl, isLoaded } = props; const classes = useStyles(); const alert = useAlert() var upload = "" const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"] const actionBodyRequest = ["POST", "PUT", "PATCH",] const authenticationOptions = ["No authentication", "API key", "Bearer auth", "Basic auth", ] 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 [updater, setUpdater] = useState("tmp") const [baseUrl, setBaseUrl] = useState(""); const [actionsModalOpen, setActionsModalOpen] = useState(false); const [authenticationOption, setAuthenticationOption] = useState(authenticationOptions[0]); const [parameterName, setParameterName] = useState(""); const [parameterLocation, setParameterLocation] = useState(apikeySelection.length > 0 ? apikeySelection[0] : ""); const [urlPath, setUrlPath] = useState(""); //const [urlPathQueries, setUrlPathQueries] = useState([{"name": "test", "required": false}]); const [urlPathQueries, setUrlPathQueries] = useState([]); const [urlPathParameters, ] = useState([]); const [firstrequest, setFirstrequest] = React.useState(true) const [, setBasedata] = React.useState({}) const [actions, setActions] = useState([]) const [errorCode, setErrorCode] = useState("") //const [actions, setActions] = useState([{ // "name": "Get workflows", // "description": "Get workflows", // "url": "/workflows", // "headers": "", // "queries": [], // "paths": [], // "body": "", // "errors": ["wutface", "WOAH"], // "method": actionNonBodyRequest[0], //}, { // "name": "Get workflow", // "description": "Get workflow", // "url": "/workflows/{id}", // "headers": "", // "queries": [], // "paths": ["id"], // "body": "", // "errors": ["wutface", "WOAH"], // "method": actionNonBodyRequest[0], //}, // //]) const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0]) const [currentAction, setCurrentAction] = useState({ "name": "", "description": "", "url": "", "headers": "", "paths": [], "queries": [], "body": "", "errors": [], "method": actionNonBodyRequest[0], }); useEffect(() => { if (firstrequest) { setFirstrequest(false) if (window.location.pathname.includes("apps/edit")) { setIsEditing(true) handleEditApp() } else { checkQuery() } } }) const handleEditApp = () => { fetch(globalUrl+"/api/v1/apps/"+props.match.params.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) => { setIsAppLoaded(true) if (!responseJson.success) { alert.error("Failed to get the app") } else { const data = JSON.parse(responseJson.body) console.log("LOADED IMAGE: ", data.image) setFileBase64(data.image) parseOpenapiData(data) } }) .catch(error => { alert.error(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")) { setIsAppLoaded(true) return } 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) { alert.error("Failed to verify") } else { const data = JSON.parse(responseJson.body) parseOpenapiData(data) } }) .catch(error => { alert.error(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) //} } // Sets the data up as it should be at later points const parseOpenapiData = (data) => { setBasedata(data) setName(data.info.title) setDescription(data.info.description) document.title = "Apps - "+data.info.title if (data.info.contact != undefined) { setContact(data.info.contact) } if (data.servers !== undefined && data.servers.length > 0) { setBaseUrl(data.servers[0].url) } console.log(data) // This is annoying (: var securitySchemes = data.components.securityDefinitions if (securitySchemes === undefined) { securitySchemes = data.securitySchemes } if (securitySchemes === undefined) { securitySchemes = data.components.securitySchemes } if (securitySchemes !== undefined) { console.log("Am I in here?") for (const [key, value] of Object.entries(securitySchemes)) { if (value.scheme === "bearer") { setAuthenticationOption("Bearer auth") break } else if (value.type === "apiKey") { setAuthenticationOption("API key") setParameterName(value.name) value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1); setParameterLocation(value.in) if (!apikeySelection.includes(value.in)) { console.log("APIKEY SELECT: ", apikeySelection) alert.error("Might be error in setting up API key authentication") } break } else if (value.scheme === "basic") { setAuthenticationOption("Basic auth") break } } } // FIXME - headers? var newActions = [] for (let [path, pathvalue] of Object.entries(data.paths)) { for (let [method, methodvalue] of Object.entries(pathvalue)) { var newaction = { "name": methodvalue.summary, "description": methodvalue.description, "url": path, "method": method.toUpperCase(), "headers": "", "queries": [], "paths": [], "body": "", "errors": [], } //console.log(`${path}: ${method}`); //console.log(methodvalue) for (var key in methodvalue.parameters) { const parameter = methodvalue.parameters[key] if (parameter.in === "query") { var tmpaction = { "description": parameter.description, "name": parameter.name, "required": parameter.required, "in": "query", } 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) } } newActions.push(newaction) } } console.log(newActions) setActions(newActions) } const submitApp = () => { alert.info("Uploading private app " + name) setErrorCode("") // Format the information const splitBase = baseUrl.split("/") const host = splitBase[2] const schemes = [splitBase[0]] const basePath = "/"+(splitBase.slice(3, )).join("/") const data = { "swagger": "3.0", "info": { "title": name, "description": description, "version": "1.0", }, "servers": [{"url": baseUrl}], "host": host, "basePath": basePath, "schemes": schemes, "paths": {}, "editing": isEditing, "components": { "securitySchemes": {}, }, "image": fileBase64, "id": props.match.params.appid, "securityDefinitions": {}, } if (contact === "") { data.info["contact"] = { "name": "@frikkylikeme", "url": "https://twitter.com/frikkylikeme", "email": "frikky@shuffler.io", } } else { data.info["contact"] = contact } console.log("LOADED IMAGE: ", data.image) for (var key in actions) { const item = actions[key] if (item.errors.length > 0) { alert.error("Saving with error in action "+item.name) } if (item.name === undefined && item.description !== undefined) { item.name = item.description } if (data.paths[item.url] === null || data.paths[item.url] === undefined) { data.paths[item.url] = {} } data.paths[item.url][item.method.toLowerCase()] = { "responses": { "default": { "description": "default", "schema": {} } }, "summary": item.name, "description": item.description, "parameters": [] } if (item.queries.length > 0) { for (var querykey in item.queries) { const queryitem = item.queries[querykey] var newitem = { "in": "query", "name": queryitem.name, "description": "Generated by shuffler.io OpenAPI", "required": queryitem.required, "schema": { "type": "string", }, } if (queryitem.description !== undefined) { newitem.description = queryitem.description } data.paths[item.url][item.method.toLowerCase()].parameters.push(newitem) //console.log(queryitem) } } if (item.paths.length > 0) { for (querykey in item.paths) { const queryitem = item.paths[querykey] 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) } } } if (authenticationOption === "API key") { data.components.securitySchemes["ApiKeyAuth"] = { "type": "apiKey", "in": parameterLocation.toLowerCase(), "name": parameterName, } } else if (authenticationOption === "Bearer auth") { data.components.securitySchemes["BearerAuth"] = { "type": "http", "scheme": "bearer", "bearerFormat": "UUID", } } else if (authenticationOption === "Basic auth") { data.components.securitySchemes["BasicAuth"] = { "type": "http", "scheme": "basic", } } fetch(globalUrl+"/api/v1/verify_openapi", { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify(data), credentials: "include", }) .then((response) => { //if (response.status !== 200) { // setErrorCode("An error occurred during validation") // throw new Error("NOT 200 :O") //} return response.json() }) .then((responseJson) => { if (!responseJson.success) { setErrorCode(responseJson.reason) alert.error("Failed to verify: ") } else { // Return? alert.success("Successfully uploaded openapi") //window.location = "/apps" } }) .catch(error => { setErrorCode(error.toString()) alert.error(error.toString()) }); } const bearerAuth = authenticationOption === "Bearer auth" ?

Bearer auth

Users will be required to submit their API as the header "Authorization: Bearer APIKEY"
: null // Basicauth const basicAuth = authenticationOption === "Basic auth" ?

Basic authentication

Users will be required to submit a valid username and password before using the API
: null // API key //const verifyBaseUrl = () => { // if (baseUrl.startsWith("http://") || baseUrl.startsWith("https://")) { // return true // } // if (baseUrl.endsWith("/")) { // return true // } // // return false //} //const verifyApiParameter = () => { // const notAllowed = ["!","#","$","%","&","'","^","+","-",".","_","~","|","]","+","$",] // for (var key in notAllowed) { // if (parameterName.includes(notAllowed[key])) { // return false // } // } // return true //} const testAction = (index) => { console.log("Should test action at index "+index) console.log(actions[index]) } const addPathQuery = () => { urlPathQueries.push({"name": "", "required": true}) 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) => { urlPathQueries.splice(index, 1) if (updater === "deleteupdater") { setUpdater("updater") } else { setUpdater("deleteupdater") } setUrlPathQueries(urlPathQueries) } const deleteAction = (index) => { actions.splice(index, 1) setCurrentAction({ "name": "", "description": "", "url": "", "headers": "", "paths": [], "queries": [], "body": "", "errors": [], "method": actionNonBodyRequest[0], }) setActions(actions) } console.log("Option: ", authenticationOption) console.log("Location: ", parameterLocation) console.log("Name: ", parameterName) const apiKey = authenticationOption === "API key" ?

API key

Can't be empty. Can't contain any of the following characters: !#$%&'^+-._~|]+$
} onChange={e => setParameterName(e.target.value)} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style:{ color: "white", }, }} /> Field type : null const loopQueries = urlPathQueries.length === 0 ? null :
Queries {urlPathQueries.map((data, index) => { const requiredColor = data.required === true ? "green" : "red" //const required = data.required === true ?
{data.required.toString()}
:
{flipRequired(index)}} style={{display: "inline", color: "red", cursor: "pointer"}}>{data.required.toString()}
return (
{flipRequired(index)}}> Required:
{data.required.toString()}
Click required switch
} onBlur={(e) => { urlPathQueries[index].name = e.target.value setUrlPathQueries(urlPathQueries) }} InputProps={{ style:{ color: "white", }, }} />
{deletePathQuery(index)}}> Delete
) })} const loopActions = actions.length === 0 ? null :
{actions.map((data, index) => { var error = // "ERROR: "+data.errors.join("\n") if (data.errors.length > 0) { error = } const url = baseUrl+data.url return ( {error}
{ setCurrentAction(data) setCurrentActionMethod(data.method) setUrlPathQueries(data.queries) setUrlPath(data.url) setActionsModalOpen(true) }}> {data.method} - {url} - {data.name}
{testAction(index)}}> Test
{deleteAction(index)}}> Delete
) })}
const setActionField = (field, value) => { currentAction[field] = value setCurrentAction(currentAction) } const bodyInfo = actionBodyRequest.includes(currentActionMethod) ?
Body setActionField("body", e.target.value)} key={currentAction} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style:{ color: "white", }, }} />
: null const addActionToView = (errors) => { currentAction.errors = errors currentAction.queries = urlPathQueries setUrlPathQueries([]) console.log(actions) console.log(currentAction.name) const actionIndex = actions.findIndex(data => data.name === currentAction.name) console.log(actionIndex) if (actionIndex < 0) { actions.push(currentAction) } else { actions[actionIndex] = currentAction } setActions(actions) } const getActionErrors = () => { var errormessage = [] if (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.") } else if (!currentAction.url.startsWith("/")) { 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") } console.log(urlPathParameters) // const [urlPathParameters, setUrlPathParameters] = useState([]); return errormessage } const UrlPathParameters = () => { if (urlPath.includes("{") && urlPath.includes("}")) { var values = [] var tmpWord = "" var record = false for (var key in urlPath) { if (urlPath[key] === "}") { values.push(tmpWord) tmpWord = "" record = false } if (record) { tmpWord += urlPath[key] } if (urlPath[key] === "{" && urlPath[key-1] === "/") { record = true } } if (!currentAction.paths === values) { currentAction.paths = values setCurrentAction(currentAction) } return (
Required parameters: {values.join(", ")}
) } return null } const newActionModal = { setUrlPath("") setCurrentAction({ "name": "", "description": "", "url": "", "headers": "", "paths": [], "queries": [], "body": "", "errors": [], "method": actionNonBodyRequest[0], }) setCurrentActionMethod(apikeySelection[0]) setUrlPathQueries([]) setActionsModalOpen(false) }} >
New action
Learn more about app creation
Name setActionField("name", e.target.value)} key={currentAction} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style:{ color: "white", }, }} />
Description setActionField("description", e.target.value)} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style:{ color: "white", }, }} />

Request

URL path { setActionField("url", e.target.value) setUrlPath(e.target.value) console.log(e.target.value) }} helperText={
The path to use. Must start with /. Add {"{variable}"} to have path variables
} InputProps={{ classes: { notchedOutline: classes.notchedOutline, input: classes.input, }, style:{ color: "white", }, }} /> {loopQueries}
Headers setActionField("headers", e.target.value)} helperText={
Headers that are part of the request
} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style:{ color: "white", }, }} /> {bodyInfo}
const actionView =

Actions

Actions are the tasks performed by an app. Read more about actions and apps here.
{loopActions}
const testView =

Test

Test an action to see whether it performs in an expected way.  Click here to learn more about testing.
Test :)
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') var ctx = canvas.getContext('2d') img.onload = function() { // img, x, y, width, height ctx.drawImage(img, 0, 0) const canvasUrl = canvas.toDataURL() console.log(canvasUrl) if (canvasUrl !== fileBase64) { setFileBase64(canvasUrl) } } //console.log(img.width) //console.log(img.width) //canvas.width = img.width //canvas.height = img.height } //const imageInfo = file.length === 0 ? //
// Upload logo //
: // const imageData = file.length > 0 ? file : fileBase64 const imageInfo = // Random names for type & autoComplete. Didn't research :^) const landingpageDataBrowser =

General information

Click here to learn more about app creation
{upload.click()}}> upload = ref} onChange={editHeaderImage} /> {imageInfo}
Name setName(e.target.value)} color="primary" InputProps={{ style:{ color: "white", height: "50px", fontSize: "1em", }, classes: { notchedOutline: classes.notchedOutline, }, }} />
Description setDescription(e.target.value)} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style:{ color: "white", }, }} />

API information

Base URL - leave empty if user changeable Must start with http(s):// and CANT end with /.
} placeholder="https://api.example.com" onChange={e => setBaseUrl(e.target.value)} />
Authentication
{basicAuth} {bearerAuth} {apiKey}
{actionView}
{testView} {errorCode}
const loadedCheck = isLoaded && isAppLoaded && !firstrequest ?
{landingpageDataBrowser}
{newActionModal}
:
return(
{loadedCheck}
) } export default AppCreator;