Files
shuffle-cracked/frontend/src/views/AppExplorer.jsx
T
2025-05-27 23:31:19 +02:00

4655 lines
145 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useContext } from "react";
import {getTheme} from "../theme.jsx";
import ReactGA from "react-ga4";
import Markdown from "react-markdown";
import algoliasearch from "algoliasearch/lite";
import ReactJson from "react-json-view-ssr";
//import { useAlert
import { ToastContainer, toast } from "react-toastify"
import { makeStyles, createStyles } from "@mui/styles";
import { useParams, useNavigate, Link } from "react-router-dom";
import {
Autocomplete,
Box,
Zoom,
Card,
CardActionArea,
Fade,
Tabs,
Tab,
CircularProgress,
DialogActions,
Dialog,
DialogTitle,
DialogContent,
Tooltip,
IconButton,
Menu,
Paper,
Button,
Typography,
Divider,
MenuItem,
Avatar,
TextField,
Breadcrumbs,
Checkbox,
Chip,
Select,
} from "@mui/material";
import {
Business as BusinessIcon,
Edit as EditIcon,
CloudDownload as CloudDownloadIcon,
Warning as WarningIcon,
VerifiedUser as VerifiedUserIcon,
Close as CloseIcon,
LockOpen as LockOpenIcon,
PlayArrow as PlayArrowIcon,
GetApp as GetAppIcon,
Apps as AppsIcon,
Description as DescriptionIcon,
ShowChart as ShowChartIcon,
Person as PersonIcon,
Polyline as PolylineIcon,
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material";
import ForkRightIcon from '@mui/icons-material/ForkRight';
import Alert from "@mui/material/Alert";
import { Context } from "../context/ContextApi.jsx";
import {
SearchBox,
StaticRefinementList,
RefinementList,
InstantSearch,
connectSearchBox,
connectHits,
Index,
} from "react-instantsearch-dom";
import AppStats from "../components/AppStats.jsx";
import ParsedAction from "../components/ParsedAction.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import { base64_decode, appCategories } from "../views/AppCreator.jsx";
import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
import AuthenticationWindow from "../components/AuthenticationWindow.jsx";
import { CodeHandler, Img, OuterLink, CopyToClipboard, } from "../views/Docs.jsx";
import { useStyles, } from "../components/ParsedAction.jsx";
import { sortByKey } from "../views/AngularWorkflow.jsx";
import { v4 as uuidv4 } from "uuid";
import aa from "search-insights";
// AppTypes:
// 0 = OpenAPI (VALID)
// 1 = Normal app (Python)
// 2 = OpenAPI (Invalid)
const searchClient = algoliasearch(
"JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e"
)
const AppExplorer = (props) => {
const {
globalUrl,
userdata,
setUserData,
checkLogin,
isLoaded,
selectedApp,
serverside,
isMobile,
isLoggedIn,
selectedDoc,
secondApp,
} = props;
//const alert = useAlert();
const classes = useStyles()
let navigate = useNavigate()
const { leftSideBarOpenByClick, themeMode, supportEmail } = useContext(Context);
const theme = getTheme(themeMode);
const surfaceColor = "#27292D";
const inputColor = theme.palette.textFieldStyle.backgroundColor;
const chipStyle = {
marginTop: 5,
backgroundColor: theme.palette.chipStyle.backgroundColor,
height: 30,
marginRight: 5,
paddingLeft: 5,
paddingRight: 5,
cursor: "pointer",
borderColor: theme.palette.chipStyle.borderColor,
color: theme.palette.chipStyle.color,
};
const actionListStyle = {
paddingLeft: 10,
paddingRight: 10,
paddingTop: 10,
marginTop: 5,
backgroundColor: inputColor,
display: "flex",
color: theme.palette.text.primary,
maxWidth: 350,
minWidth: 350,
maxHeight: 54,
overflow: "hidden",
};
const boxStyle = {
color: theme.palette.text.primary,
flex: "3",
margin: 10,
paddingLeft: 30,
paddingRight: 30,
paddingBottom: 30,
paddingTop: 30,
display: "flex",
flexDirection: "column",
position: "relative",
maxHeight: 180,
overflow: "hidden",
};
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const params = useParams();
//var props = JSON.parse(JSON.stringify(defaultprops))
//props.match = {}
//params = params
const bodyDivStyle = {
margin: "auto",
maxWidth: isMobile ? "100%" : 1350,
scrollX: "hidden",
overflowX: "hidden",
};
var upload = "";
const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"];
const authenticationOptions = [
"No authentication",
"API key",
"Bearer auth",
"Basic auth",
];
const apikeySelection = ["Header", "Query"];
const [app, setApp] = useState({});
const [openapi, setOpenapi] = useState({});
const [name, setName] = useState("");
const [appId, setAppId] = useState("");
const [contact, setContact] = useState("");
const [file] = useState("");
const [fileBase64, setFileBase64] = useState("");
const [isAppLoaded, setIsAppLoaded] = useState(false);
const [, setDescription] = useState("");
const [, setBaseUrl] = useState("");
const [, setAuthenticationRequired] = useState(false);
const [, setAuthenticationOption] = useState(authenticationOptions[0]);
const [newWorkflowTags, setNewWorkflowTags] = React.useState([]);
const [, setParameterName] = useState("");
const [, setParameterLocation] = useState(
apikeySelection.length > 0 ? apikeySelection[0] : ""
);
const [, setUrlPath] = useState("");
const [urlPathQueries, setUrlPathQueries] = useState([]);
const [, setBasedata] = React.useState({});
const [actions, setActions] = useState([]);
const [errorCode] = useState("");
const [reloadUrl, setReloadUrl] = React.useState(
serverside === true ? "" : window.location.href
);
const [relatedWorkflows, setRelatedWorkflows] = useState(0);
const [relatedApps, setRelatedApps] = useState(0);
const [appAuthentication, setAppAuthentication] = React.useState([]);
const [authLoaded, setAuthLoaded] = useState(false);
const baseResult = "The execution result will show up here";
const [anchorElAvatar, setAnchorElAvatar] = React.useState(null);
const [executionResult, setExecutionResult] = useState({
valid: false,
result: baseResult,
});
const [executing, setExecuting] = useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const [creatorProfile, setCreatorProfile] = React.useState({});
const [selectedTab, setSelectedTab] = React.useState(0);
const defaultDocs = `\n\n## No Shuffle-specific app documentation is available yet.\n\n## Need more information about the app? [Contact us](/contact) and [Join the Community](https://discord.gg/B2CBzUm) and find others using this app.`
const [sharingConfiguration, setSharingConfiguration] = React.useState("you");
const [appdata, setAppData] = React.useState({});
const [appDocumentation, setAppDocumentation] = useState(defaultDocs)
const [secondaryApp, setSecondaryApp] = useState({});
const [firstRequest, setFirstRequest] = useState(true);
const [publishModalOpen, setPublishModalOpen] = React.useState(false);
const [categories, setCategories] = useState(appCategories)
const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]);
const [update, setUpdate] = useState("");
const [triggers, setTriggers] = useState([])
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined)
const [selectedValidationAction, setSelectedValidationAction] = React.useState({})
const [selectedMeta, setSelectedMeta] = React.useState({
link: "https://github.com/Shuffle/openapi-apps/new/master/docs",
read_time: 1,
})
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
// FIXME: This is used, as useEffect() creates an issue with apps not loading at all
var to_be_copied = "";
// 0 = VALID OpenAPI, 1 = Python, 2 = INVALID OpenAPI
const [appType, setAppType] = React.useState(0);
function handleClick(event) {
setAnchorEl(event.currentTarget);
}
function handleClose() {
setAnchorEl(null);
}
const loadOrganization = (orgId) => {
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status === 401) {
} else {
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
} else {
setSelectedOrganization(responseJson)
}
})
.catch((error) => {
console.log("Error in fetching organization: ", error)
})
}
useEffect(() => {
if (selectedApp !== undefined && selectedApp !== null && Object.getOwnPropertyNames(selectedApp).length > 0) {
//console.log("Firstrequest!!!")
} else {
if (serverside) {
console.log("Not getting app because serverside.");
} else {
if (params.appid.length === 32 || params.appid.length === 36) {
handleEditApp(params.appid);
runAlgoliaAppSearch(params.appid, false, true);
} else {
runAlgoliaAppSearch(params.appid);
//handleEditApp()
}
}
//parseIncomingOpenapiData(YAML.parse(data))
}
if (serverside !== true) {
const urlSearchParams = new URLSearchParams(window.location.search);
const queries = Object.fromEntries(urlSearchParams.entries());
const foundTab = queries["tab"];
console.log("PROPS: ", queries);
if (params.integrationid !== undefined) {
console.log(
"Should search for connection integration with ",
params.integrationid
);
setSelectedTab(3);
runAlgoliaAppSearch(params.integrationid, false);
} else if (foundTab !== null && foundTab !== undefined) {
if (foundTab === "stats") {
setSelectedTab(2);
} else if (foundTab === "run") {
setSelectedTab(1);
} else if (foundTab === "docs" || foundTab === "documentation") {
setSelectedTab(0);
}
} else {
//setSelectedTab(1);
}
}
}, []);
if (serverside === false && firstRequest && isLoggedIn === true && selectedOrganization === undefined && userdata !== undefined && userdata.active_org !== undefined && userdata.active_org !== null && userdata.active_org.id !== undefined && userdata.active_org.id !== null) {
loadOrganization(userdata.active_org.id)
}
var activateButton = (
<Link to={`/apps/new?id=${appId}`} style={{ textDecoration: "none" }}>
<Button
variant="contained"
component="label"
color="primary"
onClick={() => {
ReactGA.event({
category: "Appexplorer",
action: "app_build",
label: params.appid,
});
toast("INTERESTED!");
}}
style={{
padding: 15,
marginTop: 15,
borderRadius: 25,
height: 50,
margin: "15px 0px 15px 0px",
fontSize: 14,
color: "white",
backgroundImage: buttonBackground,
width: "200px",
}}
>
I'm interested
</Button>
</Link>
);
const Heading = (props) => {
const element = React.createElement(`h${props.level}`,{ style: { marginTop: props.level === 1 ? 20 : 50 } },props.children);
const [hover, setHover] = useState(false);
var extraInfo = "";
if (props.level === 1) {
extraInfo = (
<div
style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
padding: 15,
borderRadius: theme.palette?.textFieldStyle.borderRadius,
marginBottom: 30,
display: "flex",
}}
>
<div
style={{
flex: 3,
display: "flex",
vAlign: "center",
position: "sticky",
top: 50,
}}
>
{isMobile === true ? null : (
<Typography style={{ display: "inline", marginTop: 6 }}>
<a
rel="noopener noreferrer"
target="_blank"
href={selectedMeta.link}
style={{ textDecoration: "none", color: "#f85a3e" }}
onClick={() => {
ReactGA.event({
category: "Appexplorer",
action: "github_docs_edit_click",
label: params.appid,
});
}}
>
<Button style={{}} variant="outlined">
<EditIcon /> &nbsp;&nbsp;Edit
</Button>
</a>
</Typography>
)}
{isMobile === true ? null : (
<div
style={{
height: "100%",
width: 1,
backgroundColor: "white",
marginLeft: 50,
marginRight: 50,
}}
/>
)}
<Typography style={{ display: "inline", marginTop: 11 }}>
{selectedMeta.read_time} minute
{selectedMeta.read_time === 1 ? "" : "s"} to read
</Typography>
</div>
<div style={{ flex: 2 }}>
{isMobile === true ||
selectedMeta.contributors === undefined ||
selectedMeta.contributors === null ? (
""
) : (
<div style={{ margin: 10, height: "100%", display: "inline" }}>
{selectedMeta.contributors.slice(0, 7).map((data, index) => {
return (
<a
key={index}
rel="noopener noreferrer"
target="_blank"
href={data.url}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<Tooltip title={data.url} placement="bottom">
<img
alt={data.url}
src={data.image}
style={{
marginTop: 5,
marginRight: 10,
height: 40,
borderRadius: 40,
}}
/>
</Tooltip>
</a>
);
})}
</div>
)}
</div>
</div>
);
}
// Still inside Heading
// Find appCategory in appCategories
const appCategory = newWorkflowCategories.length === 0 ? "" : (newWorkflowCategories[0]).toLowerCase();
const foundCategory = appCategories.find((category) => {
if (category.name.toLowerCase() === appCategory) {
return category
}
})
const bgColor = foundCategory === undefined ? "" : foundCategory.color
const getAllLabels = () => {
const actionLabels = actions.filter((input_action) => {
return input_action.action_label !== undefined && input_action.action_label !== null && input_action.action_label !== "No Label"
})
var labels = []
actionLabels.forEach((action) => {
labels.push(action.action_label)
})
return labels
}
const allLabels = getAllLabels()
const findRelevantAction = (action_label) => {
const foundAction = actions.find((input_action) => {
if (input_action.action_label.toLowerCase() === action_label.toLowerCase()) {
return input_action
}
})
if (foundAction !== undefined) {
setCurrentAction(foundAction)
setCurrentActionMethod(foundAction.method);
setSelectedTab(1);
} else {
console.log("Could not find action with label: ", action_label)
}
}
// Parses out extra category info and such for the app
const extraAppInfo = props.level === 1 ?
<div style={{marginBottom: 15, }}>
{/*
<div style={{display: "flex" }}>
<Typography style={{width: 100, }}>
Category:
</Typography>
<Chip
style={{
backgroundColor: bgColor,
color: "white",
borderRadius: 5,
minWidth: 80,
marginRight: 10,
marginTop: 2,
cursor: "pointer",
fontSize: 14,
}}
label={appCategory}
/>
</div>
*/}
{triggers.length === 0 ? null :
<div style={{display: "flex", marginTop: 10, }}>
{triggers.map((trigger, index) => {
return (
<Chip
key={index}
onClick={() => {
console.log("Clicked: ", trigger.name)
}}
style={{
cursor: "pointer",
backgroundColor: theme.palette.chipStyle.backgroundColor,
borderColor: theme.palette.chipStyle.borderColor,
color: theme.palette.chipStyle.color,
borderRadius: 40,
minWidth: 80,
marginRight: 10,
marginTop: 2,
fontSize: 14,
}}
avatar={<Avatar alt={trigger.name} src={trigger.large_image} />}
label={trigger.name}
/>
)
})}
</div>
}
{serverside === false && foundCategory !== undefined && foundCategory !== null && foundCategory.action_labels.length > 0 ?
<div style={{display: "flex", marginTop: 10, }}>
{foundCategory.action_labels.slice(0,5).map((action_label, index) => {
const included = allLabels.includes(action_label)
const iconInfo = GetIconInfo({ name: action_label });
const useIcon = iconInfo.originalIcon;
return (
<Chip
key={index}
onClick={() => {
findRelevantAction(action_label)
}}
disabled={included === false}
style={{
cursor: included ? "pointer" : "default",
color: theme.palette.chipStyle.color,
borderColor: theme.palette.chipStyle.borderColor,
backgroundColor: theme.palette.chipStyle.backgroundColor,
borderRadius: 40,
minWidth: 80,
marginRight: 10,
marginTop: 2,
fontSize: 14,
textDecoration: included ? "none" : "line-through",
}}
avatar={useIcon}
label={action_label}
/>
)
})}
</div>
: null}
</div>
: null
return (
<Typography
onMouseOver={() => {
setHover(true);
}}
>
{props.level !== 1 ? (
<Divider
style={{
width: "90%",
marginTop: 40,
backgroundColor: theme.palette.inputColor,
}}
/>
) : null}
{element}
{extraAppInfo}
{extraInfo}
</Typography>
);
};
const [, setCurrentActionMethod] = useState(actionNonBodyRequest[0]);
// Selectedaction = Shuffle style action
// Currentaction = OpenAPI style
const [selectedAction, setSelectedAction] = useState({});
const [currentAction, setCurrentAction] = useState({
name: "",
description: "",
url: "",
headers: "",
paths: [],
queries: [],
body: "",
errors: [],
method: actionNonBodyRequest[0],
});
if (params.appid === "new") {
return null;
}
const WorkflowHits = ({ hits }) => {
//console.log("WORKFLOWS: ", hits)
setRelatedWorkflows(hits.length);
return hits.length;
};
const AppHits = ({ hits }) => {
if (hits.length >= 1) {
setRelatedApps(hits.length - 1);
return hits.length - 1;
} else {
setRelatedApps(0);
return 0;
}
};
const getUserProfile = (username) => {
if (serverside === true) {
return;
}
fetch(`${globalUrl}/api/v1/users/creators/${username}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
setCreatorProfile(responseJson);
}
})
.catch((error) => {
console.log(error);
});
};
const SearchBox = ({ currentRefinement, refine, isSearchStalled }) => {
return null;
};
const CustomWorkflowHits = connectHits(WorkflowHits);
const CustomAppHits = connectHits(AppHits);
const CustomSearchBox = connectSearchBox(SearchBox);
const HandleJsonCopy = (base, copy, base_node_name) => {
console.log("COPY: ", copy);
var newitem = JSON.parse(base);
to_be_copied = "$" + base_node_name;
for (var key in copy.namespace) {
if (copy.namespace[key].includes("Results for")) {
continue;
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.namespace[key]];
if (!isNaN(copy.namespace[key])) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.namespace[key];
}
}
}
};
const handleReactJsonClipboard = (copy) => {
console.log("COPY: ", copy);
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
navigator.clipboard.writeText(JSON.stringify(copy));
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Copied data");
}
};
const activateApp = (action) => {
if (serverside === true) {
return
}
const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId)
var url = appExists ? `${globalUrl}/api/v1/apps/${appId}/deactivate` : `${globalUrl}/api/v1/apps/${appId}/activate`
if (action !== undefined && action !== null) {
url = `${globalUrl}/api/v1/apps/${appId}/${action}`
}
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Failed to activate");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (action === undefined || action === null) {
if (responseJson.reason !== undefined) {
toast("Failed to activate the app: "+responseJson.reason);
} else {
toast("Failed to activate the app");
}
} else {
if (responseJson.reason !== undefined) {
toast("Failed to perform action: "+responseJson.reason);
} else {
toast(`Failed to perform action. Please try again or contact ${supportEmail}`);
}
}
} else {
if (checkLogin !== undefined && checkLogin !== null) {
checkLogin()
}
if (action === undefined || action === null) {
if (appExists) {
toast("App deactivated for your organization! Existing workflows with the app will continue to work.")
} else {
toast("App activated for your organization!")
}
} else {
}
}
})
.catch((error) => {
toast(error.toString());
});
};
const handleEditApp = (appid) => {
if (serverside === true) {
return;
}
setAppId(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) {
//console.log("App doesn't exist or isn't available to you.")
//toast("Something went wrong - this app is not available to you. Redirecting you back to search.")
ReactGA.event({
category: "appexplorer",
action: `app_not_found`,
label: appid,
});
} else {
ReactGA.event({
category: "appexplorer",
action: `app_found`,
label: appid,
});
}
return response.json();
})
.then((responseJson) => {
if (
responseJson.success === false ||
responseJson.success === undefined
) {
toast("Failed to get the app")
setIsAppLoaded(true)
setTimeout(() => {
navigate("/search")
}, 1000);
return;
} else {
parseIncomingOpenapiData(responseJson);
}
})
.catch((error) => {
toast("Error in app fetch: " + error.toString());
});
};
const parseIncomingAppdata = (data, openapiExists) => {
document.title = data.name + " App - OpenAPI and API";
setExecutionResult({
valid: false,
result: baseResult,
});
setName((data.name.charAt(0).toUpperCase() + data.name.substring(1)).replaceAll("_"," "));
setDescription(data.description);
setFileBase64(data.large_image);
setContact(data.contact_info);
if (data.categories !== undefined && data.categories !== null) {
setNewWorkflowCategories(data.categories);
}
setAppType(1);
if (data.owner !== undefined && data.owner !== null) {
getUserProfile(data.owner);
//console.log("DATA: ", data)
}
setAppData(data)
if (data.reference_info.triggers !== undefined && data.reference_info.triggers !== null && data.reference_info.triggers.length > 0) {
var parsedtriggers = []
for (var key in data.reference_info.triggers) {
const curtrigger = data.reference_info.triggers[key]
const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger.toLowerCase())
if (foundTrigger !== undefined && foundTrigger !== null) {
parsedtriggers.push(foundTrigger)
}
}
setTriggers(parsedtriggers)
}
var newactions = [];
if (!openapiExists) {
console.log("Skipping openapi");
for (var key in data.actions) {
const action = data.actions[key];
newactions.push({
name: action.name,
description: action.description,
url: "",
headers: "",
paths: [],
queries: [],
body: "",
errors: [],
method: "CUSTOM",
});
}
}
if (newactions.length > 0) {
setCurrentAction(newactions[0]);
if (data.actions !== undefined) {
//var methodName = `${data.method}_${data.name}`.toLowerCase()
//if (data.name.toLowerCase().startsWith(data.method.toLowerCase())) {
// methodName = data.name.toLowerCase()
//}
//var newselectedaction = data.actions.find(item => item.name.toLowerCase() === methodName)
//if (newselectedaction === undefined || newselectedaction === null) {
// toast(`Name ${methodName} not found. Please contact us.`)
// return
//}
//var newselectedaction = data.actions.find(item => item.name.toLowerCase() === )
const newselectedaction = data.actions[0];
newselectedaction.app_id = data.id;
newselectedaction.app_name = data.name;
newselectedaction.app_version = data.app_version;
newselectedaction.authentication = selectedAction.authentication;
newselectedaction.authentication_id = selectedAction.authentication_id;
newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication;
if (
data.authentication.required &&
newselectedaction.authentication_id !== undefined &&
newselectedaction.authentication_id !== null &&
newselectedaction.authentication_id.length === 0
) {
const tmpParams = selectedAction.parameters;
selectedAction.parameters = [];
for (let paramkey in data.authentication.parameters) {
var item = data.authentication.parameters[paramkey];
console.log("PARAM1: ", item)
item.configuration = true;
const found = selectedAction.parameters.find((param) => param.name === item.name);
if (found === null || found === undefined) {
selectedAction.parameters.push(item);
}
}
for (let paramkey in tmpParams) {
var item = tmpParams[paramkey];
console.log("PARAM2: ", item)
//item.configuration = true
const found = selectedAction.parameters.find((param) => param.name === item.name);
if (found === null || found === undefined) {
selectedAction.parameters.push(item);
}
}
}
setSelectedAction(newselectedaction);
}
const firstActions = newactions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
//console.log("First actions: ", firstActions)
const secondActions = newactions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
const newActions = firstActions.concat(secondActions)
setActions(newActions);
}
setIsAppLoaded(true);
};
// 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 appexists = false;
var nameExists = false;
var parsedapp = {};
if (data.app !== undefined && data.app !== null) {
// Should basically always be true if openapi exists too
var parsedBaseapp = ""
try {
parsedBaseapp = base64_decode(data.app)
} catch (e) {
console.log("Failed JSON parsing: ", e)
parsedBaseapp = data
}
parsedapp = JSON.parse(parsedBaseapp)
parsedapp.name = parsedapp.name.replaceAll("_", " ");
setAppDocumentation("# "+parsedapp.name+defaultDocs);
setApp(parsedapp);
setSharingConfiguration(parsedapp.sharing === true ? "public" : "you")
appexists =
parsedapp.name !== undefined &&
parsedapp.name !== null &&
parsedapp.name.length !== 0;
if (appexists) {
getAppDocs(parsedapp.name, "python", parsedapp.app_version);
}
if (data.openapi === undefined || data.openapi === null) {
console.log("Parsed app: ", parsedapp)
parseIncomingAppdata(parsedapp, false);
} else {
parseIncomingAppdata(parsedapp, true);
}
}
if (data.openapi === undefined || data.openapi === null) {
return;
}
var parsedDecoded = ""
try {
parsedDecoded = base64_decode(data.openapi)
} catch (e) {
console.log("Failed JSON parsing: ", e)
parsedDecoded = data
}
setAppType(0);
parsedapp = JSON.parse(parsedDecoded);
data = parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body);
setOpenapi(data);
getAppDocs(data.info.title, "openapi", data.app_version);
setBasedata(data);
if (!appexists) {
setName(
(
data.info.title.charAt(0).toUpperCase() + data.info.title.substring(1)
).replaceAll("_", " ")
);
setDescription(data.info.description);
console.log("Found name: ", data.info.title);
}
if (serverside !== true) {
var doctitle = "Shuffle App for " + data.info.title
if (!data.info.title.toLowerCase().includes("api")) {
doctitle += " API"
}
document.title = doctitle
}
if (data.info !== null && data.info !== undefined) {
if (data.info["x-logo"] !== undefined) {
setFileBase64(data.info["x-logo"]);
}
if (data.info.contact !== undefined) {
setContact(data.info.contact);
}
if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) {
setNewWorkflowCategories(data.info["x-categories"]);
}
}
if (data.tags !== undefined && data.tags.length > 0) {
for (var key in data.tags) {
newWorkflowTags.push(data.tags[key].name);
}
setNewWorkflowTags(newWorkflowTags);
}
var securitySchemes = data?.components?.securityDefinitions;
if (securitySchemes === undefined) {
securitySchemes = data?.securitySchemes;
}
if (securitySchemes === undefined) {
securitySchemes = data?.components?.securitySchemes;
}
const allowedfunctions = [
"GET",
"CONNECT",
"HEAD",
"DELETE",
"POST",
"PATCH",
"PUT",
];
// FIXME - headers?
var newActions = [];
var wordlist = {};
if (data.paths !== null && data.paths !== undefined) {
for (let [path, pathvalue] of Object.entries(data.paths)) {
if (path === "tmp0") {
setAppType(2);
}
for (let [method, methodvalue] of Object.entries(pathvalue)) {
if (methodvalue === null) {
toast("Skipped method " + method);
continue;
}
if (!allowedfunctions.includes(method.toUpperCase())) {
continue;
}
var tmpname = methodvalue.summary;
if (
methodvalue.operationId !== undefined &&
methodvalue.operationId !== null &&
methodvalue.operationId.length > 0
) {
tmpname = methodvalue.operationId;
}
var newaction = {
name: tmpname,
description: methodvalue.description,
url: path,
method: method.toUpperCase(),
headers: "",
queries: [],
paths: [],
body: "",
errors: [],
example_response: "",
action_label: "No Label",
required_bodyfields: [],
}
// Related to Label Management
if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) {
// Check if there are commas in it then loop and find the correct one
// Should ignore 'No Label' and 'No label'
var correctlabel = ""
const labels = methodvalue["x-label"].split(",")
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, '')
//label = label.replace("_", " ", -1)
//label = label.charAt(0).toUpperCase() + label.slice(1)
correctlabel = label
break
}
// FIX: Map labels only if they're actually in the category list
newaction.action_label = correctlabel
}
if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) {
newaction.required_bodyfields = methodvalue["x-required-fields"]
}
for (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);
// 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) {
newaction.body = parameter.example;
}
} else if (parameter.in === "header") {
newaction.headers += `${parameter.name}=${parameter.example}\n`;
}
}
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] of Object.entries(data.paths)) {
const newpathsplit = newpath.split("/");
for (key in newpathsplit) {
const pathitem = newpathsplit[key].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 (key in urlsplit) {
var subpath = urlsplit[key];
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");
}
}
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 (key in found) {
const item = found[key].slice(1, found[key].length - 1);
const foundVar = data.servers[0].variables[item];
if (foundVar["default"] !== undefined) {
firstUrl = firstUrl.replaceAll(
found[key],
foundVar["default"]
);
}
}
}
}
if (firstUrl.endsWith("/")) {
setBaseUrl(firstUrl.slice(0, firstUrl.length - 1));
} else {
setBaseUrl(firstUrl);
}
}
}
}
// FIXME: Have multiple authentication options?
if (securitySchemes !== undefined) {
for (const [, value] of Object.entries(securitySchemes)) {
if (value.scheme === "bearer") {
setAuthenticationOption("Bearer auth");
setAuthenticationRequired(true);
break;
} else if (value.type === "apiKey") {
setAuthenticationOption("API key");
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)
setParameterName(value.name);
setAuthenticationRequired(true);
break;
} else if (value.scheme === "basic") {
setAuthenticationOption("Basic auth");
setAuthenticationRequired(true);
break;
}
}
}
const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
//console.log("First actions: ", firstActions)
const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
newActions = firstActions.concat(secondActions)
setActions(newActions);
setIsAppLoaded(true);
if (newActions.length > 0) {
setCurrentAction(newActions[0]);
setCurrentActionMethod(newActions[0].method);
setUrlPathQueries(newActions[0].queries);
setUrlPath(newActions[0].url);
//setActionsModalOpen(true)
}
};
const getAppDocs = (appname, location, version) => {
if (serverside === true) {
return;
}
fetch(
`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`,
{
headers: {
Accept: "application/json",
},
credentials: "include",
}
)
.then((response) => {
if (response.status === 200) {
//toast("Successfully GOT app "+appId)
} else {
//toast("Failed getting app");
}
return response.json();
})
.then((responseJson) => {
var setMeta = false
if (responseJson.success === true) {
if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) {
if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) {
const imgRegex = /<img.*?src="(.*?)"/g;
const newdata = responseJson.reason.replace(imgRegex, '![]($1)');
setAppDocumentation(newdata)
const urlSearchParams = new URLSearchParams(
window.location.search
);
const queries = Object.fromEntries(urlSearchParams.entries());
const foundTab = queries["tab"];
if (foundTab !== null && foundTab !== undefined) {
} else {
if (params.integrationid === undefined) {
setSelectedTab(0);
}
}
if (responseJson.meta !== undefined) {
setSelectedMeta(responseJson.meta);
setMeta = true
}
}
}
} else {
}
if (!setMeta) {
setSelectedMeta({
"name": appname,
"contributors": [],
"edited": "",
"read_time": 1,
"link": `https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${appname.toLowerCase().replaceAll(" ", "_").replaceAll("%20", "_")}.md`,
})
}
})
.catch((error) => {
toast("Error in doc loading: " + error.toString());
});
};
const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => {
const index = searchClient.initIndex("appsearch");
console.log("Running appsearch for: ", appname);
index
.search(appname)
.then(({ hits }) => {
const appsearchname = appname.replaceAll("_", " ").toLowerCase();
var found = false
if (hits !== undefined && hits !== null && hits.length === 1) {
found = true
if (isOriginal !== false) {
handleEditApp(hits[0].objectID)
} else {
setSecondaryApp(hits[0])
}
}
for (var key in hits) {
const hit = hits[key];
if (hit["name"] === null || hit["name"] === undefined) {
continue;
}
if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) {
/*
if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) {
var parsedtriggers = []
for (var key in hit.triggers) {
const curtrigger = hit.triggers[key].toLowerCase()
const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger)
if (foundTrigger !== undefined && foundTrigger !== null) {
parsedtriggers.push(foundTrigger)
}
}
setTriggers(parsedtriggers)
}
*/
if (triggerOnly === true) {
} else {
if (isOriginal !== false) {
found = true
handleEditApp(hit.objectID);
} else {
console.log("Found second app: ", hit);
hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1);
setSecondaryApp(hit);
}
}
break;
}
}
if (!found) {
if (hits.length > 0) {
if (isOriginal !== false) {
handleEditApp(hits[0].objectID)
} else {
setSecondaryApp(hits[0]);
}
return
}
//navigate("/search?message=App not found&q=" + appname + "&tab=apps")
//toast("App not found. Please contact support@shuffler.io if you believe this is an error.")
return
}
})
.catch((err) => {
console.log(err);
});
};
if (serverside === true && firstRequest) {
setFirstRequest(false);
if (
selectedApp !== undefined &&
selectedApp !== null &&
Object.getOwnPropertyNames(selectedApp).length > 0
) {
parseIncomingOpenapiData(selectedApp);
}
if (
selectedDoc !== undefined &&
selectedDoc !== null &&
Object.getOwnPropertyNames(selectedDoc).length > 0
) {
setAppDocumentation(selectedDoc.reason);
setSelectedTab(0);
}
if (
secondApp !== undefined &&
secondApp !== null &&
Object.getOwnPropertyNames(secondApp).length > 0
) {
setSelectedTab(3);
setSecondaryApp(secondApp);
}
}
//, [])
if (serverside !== true && window.location.href !== reloadUrl) {
setReloadUrl(window.location.href);
setAppDocumentation(defaultDocs);
setTriggers([])
//handleEditApp(params.appid)
if (params.appid.length === 32 || params.appid.length === 36) {
handleEditApp(params.appid);
runAlgoliaAppSearch(params.appid, false, true);
} else {
runAlgoliaAppSearch(params.appid);
}
}
const loopQueries =
urlPathQueries === undefined ||
urlPathQueries === null ||
urlPathQueries.length === 0 ? null : (
<div>
<Divider
style={{
marginBottom: "10px",
marginTop: "30px",
height: "1px",
width: "100%",
backgroundColor: "grey",
}}
/>
Queries
{urlPathQueries.map((data, index) => {
return (
<Paper key={index} style={actionListStyle}>
<div style={{ marginLeft: "5px", width: "100%" }}>
<TextField
required
fullWidth={true}
defaultValue={data.name}
placeholder={"Query name"}
onBlur={(e) => {
//urlPathQueries[index].name = e.target.value
//setUrlPathQueries(urlPathQueries)
}}
InputProps={{
style: {
color: "white",
},
}}
/>
</div>
</Paper>
);
})}
<Divider
style={{
marginBottom: "10px",
marginTop: "30px",
height: "1px",
width: "100%",
backgroundColor: "grey",
}}
/>
</div>
);
const executeSingleAction = (appid, thisaction) => {
if (serverside === true) {
return;
}
if (isCloud) {
thisaction.environment = "Cloud"
} else {
thisaction.environment = "Shuffle"
}
setExecutionResult({
valid: false,
result: baseResult,
});
setExecuting(true);
fetch(globalUrl + "/api/v1/apps/" + appid + "/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(thisaction),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
//console.log("RESPONSE: ", responseJson)
if (
responseJson.success === true &&
responseJson.result !== null &&
responseJson.result !== undefined &&
responseJson.result.length > 0
) {
const result = responseJson.result.slice(0, 50) + "...";
//toast("SUCCESS: "+result)
const validate = validateJson(responseJson.result);
setExecutionResult(validate);
} else if (
responseJson.success === false &&
responseJson.reason !== undefined &&
responseJson.reason !== null
) {
toast(responseJson.reason);
setExecutionResult({ valid: false, result: responseJson.reason });
} else if (responseJson.success === true) {
setExecutionResult({
valid: false,
result:
"Couldn't finish execution. Please fill all the required fields, and retry the execution.",
});
} else {
setExecutionResult({
valid: false,
result:
"Couldn't finish execution OR no result was returned (2). Please fill all the required fields, and validate the execution.",
});
}
setExecuting(false);
})
.catch((error) => {
toast("Execution error: " + error.toString());
setExecuting(false);
});
};
const MethodWrapper = (props) => {
const { data } = props;
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";
}
return (
<Chip
style={{
backgroundColor: bgColor,
color: "white",
borderRadius: theme.palette?.borderRadius,
minWidth: 80,
marginRight: 10,
marginTop: 2,
cursor: "pointer",
fontSize: 14,
}}
label={data.method}
/>
);
};
const parseName = (name, length) => {
if (name === undefined || name === null) {
return ""
}
var parsedName = name.charAt(0).toUpperCase() + name.slice(1);
parsedName = parsedName.replaceAll("_", " ");
if (
length !== undefined &&
length !== null &&
length > 3 &&
length < parsedName.length
) {
parsedName = parsedName.slice(0, length) + "..";
}
return parsedName;
};
const SubAction = (props) => {
const { data, selected, hovered, index } = props;
const [hoveredItem, setHoveredItem] = useState(true);
var urlPath = data.url !== undefined && data.url !== null && data.url.length > 0 ? data.url : ""
var wrappedStyle = JSON.parse(JSON.stringify(actionListStyle));
wrappedStyle.backgroundColor = selected || hovered ? theme.palette.platformColor : theme.palette.inputColor;
wrappedStyle.paddingBottom = urlPath.length > 0 ? 0 : 10
wrappedStyle.border = selected || hovered ? "1px solid rgba(255,255,255,0.3)" : ""
var methodName = `${data.method}_${data.name}`;
if ((data.name !== undefined && data.name !== null && data.method !== undefined && data.method !== null ) && (data.method.toLowerCase() === "custom" || data.name.toLowerCase().startsWith(data.method.toLowerCase()))) {
methodName = data.name;
}
const invalid_keys = [".", "(", ")", "'", ",", "[", "]"];
methodName = methodName.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_");
for (var key in invalid_keys) {
methodName = methodName.replaceAll(invalid_keys[key], "");
}
const parsedName = parseName(data.name, 35);
if (parsedName.length === 0) {
return null
}
const actionLabels = foundCategory !== undefined && foundCategory !== null && foundCategory.name !== "Other" && foundCategory.action_labels.length > 0 ? ["No Label"].concat(foundCategory.action_labels) : []
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";
}
//console.log("Category: ", foundCategory, actionLabels)
return (
<Paper
style={wrappedStyle}
onMouseOver={() => {
setHoveredItem(true);
}}
onMouseOut={() => {
setHoveredItem(false);
}}
>
<Tooltip
title={data.description === null || data.description === undefined ? urlPath : <Typography variant="body2"> {parsedName} <br/><br/>{data.method} {urlPath}<br/><br />{data.description}
</Typography>
}
placement="left"
>
<div
style={{
marginLeft: 10,
width: "100%",
cursor: "pointer",
maxWidth: 725,
overflowX: "hidden",
overflowY: "hidden",
}}
onClick={() => {
if (app.actions !== undefined) {
var newselectedaction = app.actions.find((item) => item.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "") === methodName)
if (newselectedaction === undefined || newselectedaction === null) {
newselectedaction = app.actions.find((item) => item.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", "") === data.name.toLowerCase().replaceAll(" ", "_").replaceAll(".", "").replaceAll("(", "").replaceAll(")", ""))
if (newselectedaction === undefined || newselectedaction === null) {
for (var key in app.actions) {
console.log(methodName, app.actions[key].name.toLowerCase().replaceAll(" ", "_"));
}
toast(`Name ${methodName} not found. Please contact us.`);
return;
}
}
newselectedaction.app_id = app.id;
newselectedaction.app_name = app.name;
newselectedaction.app_version = app.app_version;
newselectedaction.authentication = selectedAction.authentication;
newselectedaction.authentication_id = selectedAction.authentication_id;
newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication;
if (
app.authentication.required &&
newselectedaction.authentication_id !== undefined &&
newselectedaction.authentication_id !== null &&
newselectedaction.authentication_id.length === 0
) {
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(newselectedaction);
}
setCurrentAction(data);
setCurrentActionMethod(data.method);
setUrlPathQueries(data.queries);
setUrlPath(data.url);
/*
if (selectedTab !== 1) {
setSelectedTab(1);
}
*/
}}
>
<div style={{ display: "flex", marginBottom: 5 }}>
<MethodWrapper data={data} />
<span style={{maxWidth: 175, minWidth: 175, overflow: "hidden", marginRight: 5, }}>
<Typography
variant="body1"
style={{
marginTop: urlPath.length > 0 ? "auto" : 3,
marginBottom: "auto",
textAlign: "left",
overflow: "hidden",
maxHeight: 27,
maxWidth: 175,
}}
>
{parsedName}
</Typography>
{urlPath.length > 0 ?
<Typography variant="body2" color="textSecondary">
{urlPath}
</Typography>
: null}
</span>
{actionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ?
<Select
fullWidth
onChange={(e) => {
console.log("Should change: ", e.target.value, " Index: ", index)
const foundIndex = actions.findIndex((action) => action.name === data.name)
console.log("Found index: ", foundIndex)
if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
actions[foundIndex].action_label = e.target.value
setActions(actions)
setUpdate(Math.random())
// Should send recommendations to the owner
if (creatorProfile.github_username !== undefined && creatorProfile.github_username !== null && creatorProfile.github_username.length > 0) {
const labelData = {
"app_id": app.id,
"action_name": data.name,
"label": e.target.value
}
// Should send recommendations to the owner
var url = `${globalUrl}/api/v1/apps/label`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify(labelData),
credentials: "include",
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason)
}
})
.catch((error) => {
console.log("Error: ", error)
});
}
}
}}
value={data.action_label.toLowerCase().replaceAll(" ", "_")}
style={{
border: data.action_label === undefined || data.action_label === "No Label" ? "" : `2px solid ${bgColor}`,
borderRadius: theme.shape.borderRadius,
backgroundColor: inputColor,
paddingLeft: 10,
color: "white",
height: 30,
maxWidth: 35,
marginLeft: 10,
marginRight: 10,
overflow: "hidden",
}}
inputProps={{
name: "Method",
id: "method-option",
}}
>
{actionLabels.map((label, labelindex) => {
//label = label.toLowerCase().replaceAll(" ", "_")
if (label === undefined || label === null) {
return null
}
return (
<MenuItem
key={labelindex}
value={label.toLowerCase().replaceAll(" ", "_")}
style={{
}}
>
{label}
</MenuItem>
)
})}
<Divider />
<MenuItem
key={"app_validation"}
value={"app_validation"}
style={{
}}
>
App Validation
</MenuItem>
</Select>
: null}
</div>
</div>
</Tooltip>
</Paper>
);
};
const foundCategory = newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 ? categories.find((x) => x.name === newWorkflowCategories[0]) : undefined
const LoopActions = (props) => {
const { actions } = props;
//const [activeActions] = useState(actions === undefined ? [] : actions);
if (actions === undefined || actions === null || actions.length === 0) {
return null;
}
return (
<div>
{actions.map((data, index) => {
if (data.action_label === undefined || data.action_label === null || data.action_label.length === 0) {
data.action_label = "No Label"
}
return (
<SubAction
key={index}
data={data}
selected={currentAction.name === data.name}
index={index}
/>
);
})}
</div>
);
//</Scrollbars>
};
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.required &&
app.authentication.parameters !== undefined &&
app.authentication.parameters !== null
);
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: "white",
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("Action: ", selectedAction);
};
//{selectedAction.authentication !== undefined && selectedAction.authentication.length > 0 ?
const getAppAuthentication = () => {
if (serverside === true) {
return;
}
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 app auth :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success && responseJson.data !== undefined && responseJson.data !== null && responseJson.data.length !== 0) {
var newauth = [];
//console.log("Got auth. Trying to map to selectedAction appname: ", selectedAction)
var authUpdate = false;
const appname = selectedAction.app_name.toLowerCase().replaceAll(" ", "_")
selectedAction.authentication = []
for (var key in responseJson.data) {
if (responseJson.data[key].defined === false) {
continue;
}
if (responseJson.data[key].active === false) {
continue;
}
newauth.push(responseJson.data[key]);
if (responseJson.data[key].app === undefined || responseJson.data[key].app === null) {
continue
}
if (responseJson.data[key].app.name.toLowerCase().replaceAll(" ", "_") === appname) {
console.log("Found matching app name: ", responseJson.data[key].app.name)
selectedAction.authentication.push(responseJson.data[key])
selectedAction.authentication_id = responseJson.data[key].id
selectedAction.selectedAuthentication = responseJson.data[key]
authUpdate = true;
}
}
console.log("New auth: ", newauth)
if (authUpdate === true) {
setSelectedAction(selectedAction)
}
//setUpdate(Math.random())
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) => {
if (serverside === true) {
return;
}
//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("Auth error: ", error.toString());
});
};
const AuthenticationData = (props) => {
const selectedApp = props.app;
const [authenticationOption, setAuthenticationOptions] = React.useState({
app: JSON.parse(JSON.stringify(selectedApp)),
fields: {},
label: "",
usage: [
{
workflow_id: workflow.id,
},
],
id: uuidv4(),
active: true,
});
if (selectedApp.authentication === undefined) {
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 = () => {
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;
}
}
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: "white"}} value={data}>
*/
};
if (
authenticationOption.label === null ||
authenticationOption.label === undefined
) {
authenticationOption.label = selectedApp.name + " authentication";
}
return (
<div>
<DialogContent>
<a
target="_blank"
rel="norefferer"
href="https://shuffler.io/docs/apps#authentication"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
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: "white",
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 !== undefined &&
selectedApp.authentication.parameters !== null
? 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: "white",
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>
);
})
: null}
</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 authenticationModal = authenticationModalOpen ? (
<Dialog
open={authenticationModalOpen}
onClose={() => {
//setAuthenticationModalOpen(false)
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: 600,
padding: 15,
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 14,
color: "grey",
}}
onClick={() => {
setAuthenticationModalOpen(false);
}}
>
<CloseIcon />
</IconButton>
<DialogTitle>
<div style={{ color: "white" }}>
Authentication for {selectedApp.name}
</div>
</DialogTitle>
{/*<AuthenticationData app={app} />*/}
{app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ?
<AuthenticationOauth2
selectedApp={app}
selectedAction={{
"app_name": app.name,
"app_id": app.id,
"app_version": app.version,
"large_image": app.large_image,
}}
authenticationType={app.authentication}
isCloud={true}
authButtonOnly={true}
getAppAuthentication={getAppAuthentication}
isLoggedIn={isLoggedIn}
/>
:
<AuthenticationWindow
selectedApp={app}
globalUrl={globalUrl}
getAppAuthentication={getAppAuthentication}
appAuthentication={appAuthentication}
authenticationModalOpen={authenticationModalOpen}
setAuthenticationModalOpen={setAuthenticationModalOpen}
isLoggedIn={isLoggedIn}
/>
}
</Dialog>
) : null;
const selectedNameChange = (event) => {
if (event.target === undefined || event.target === null || event.target.value === undefined || event.target.value === null || event.target.value.length === 0) {
return
}
//console.log("OLDNAME: ", selectedAction.name)
event.target.value = event.target.value.replaceAll("(", "");
event.target.value = event.target.value.replaceAll(")", "");
event.target.value = event.target.value.replaceAll("$", "");
event.target.value = event.target.value.replaceAll("#", "");
event.target.value = event.target.value.replaceAll(".", "");
event.target.value = event.target.value.replaceAll(",", "");
event.target.value = event.target.value.replaceAll(" ", "_");
selectedAction.label = event.target.value;
setSelectedAction(selectedAction);
};
const actionStyling = {
width: "100%",
};
//backgroundColor: "#1F2023",
const appApiViewStyle = {
display: "flex",
flexDirection: "column",
color: "white",
minHeight: "100%",
zIndex: 1000,
resize: "vertical",
overflowY: "auto",
overflowX: "hidden",
maxHeight: 680,
paddingRight: 7,
};
// maxWidth: 420,
return (
<span>
{authenticationModal}
{selectedAction.id !== undefined ? (
<ParsedAction
selectedAction={selectedAction}
workflow={workflow}
setWorkflow={setWorkflow}
setSelectedAction={setSelectedAction}
setUpdate={setUpdate}
selectedApp={app}
setAuthenticationModalOpen={setAuthenticationModalOpen}
selectedNameChange={selectedNameChange}
appApiViewStyle={appApiViewStyle}
globalUrl={globalUrl}
requiresAuthentication={requiresAuthentication}
hideExtraTypes={true}
setNewSelectedAction={undefined}
environments={[]}
selectedActionEnvironment={undefined}
setSelectedActionEnvironment={undefined}
showEnvironment={false}
rightsidebarStyle={actionStyling}
setCodeModalOpen={undefined}
setLastSaved={undefined}
setVariablesModalOpen={undefined}
setCurrentView={undefined}
setSelectedTrigger={undefined}
setSelectedEdge={undefined}
setSelectedApp={setSelectedApp}
getParents={undefined}
setSelectedResult={undefined}
cy={undefined}
workflowExecutions={[]}
/>
) : null}
</span>
);
};
const SelectedActionView = (props) => {
const { action } = props;
//console.log("Parsedaction: ", selectedAction)
const parsedName = parseName(action.name);
const splitHeaders = action.headers === undefined || action.headers === null ? [] : action.headers.split("\n");
return (
<div style={{ minWidth: 400, maxWidth: 400, marginLeft: 10, marginRight: 20, }}>
<div style={{ display: "flex", marginTop: 10 }}>
<MethodWrapper data={action} />
<Typography variant="body1" style={{ marginTop: 5 }}>
{parsedName}
</Typography>
</div>
<ParsedActionHandler />
</div>
);
};
const AppDetails = (props) => {
const { title, inputTitle } = props
const [details, setDetails] = useState("")
return (
<span>
<TextField
fullWidth
margin="dense"
id="outlined-basic"
label={title}
variant="outlined"
defaultValue={inputTitle}
style={{
minWidth: 600,
maxWidth: 600,
marginRight: 10,
}}
// endornament for submit
onChange={(event) => {
setDetails(event.target.value)
}}
onBlur={(event) => {
submitAppDetails(title.toLowerCase().replaceAll(" ", "_"), details)
}}
/>
</span>
)
}
const imageStyle = {
borderRadius: theme.palette?.borderRadius,
border: "1px solid rgba(255,255,255,0.6)",
minWidth: 100,
maxWidth: 100,
minhHight: 100,
maxHeight: 100,
};
const textStyle = {
marginLeft: 15,
marginTop: 15,
};
const submitAppDetails = (field, value) => {
console.log("To submit. Skipping if value is empty: ", field, value)
if (value === undefined || value === null || (value.length === 0 && field !== "triggers")) {
return
}
//toast("Submitting details for field", field)
const data = {
field: field,
value: value,
app_id: app.id,
}
fetch(globalUrl + "/api/v1/apps/label", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason === undefined) {
toast("Failed to submit details for field ", field, " because ", responseJson.reason)
} else {
toast("Failed to submit details for field ", field, " because ", responseJson.reason)
}
} else {
toast("Successfully submitted details for field ", field)
}
})
.catch((error) => {
console.log("Error: ", error)
toast("Error submitting details for field ", field)
})
}
const editTrigger = (triggerName) => {
console.log("Editing trigger: ", triggerName)
var found = false
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].name === triggerName) {
found = true
break
}
}
var newtriggers = JSON.parse(JSON.stringify(triggers))
if (!found) {
// Find the trigger with the same name in imported workflowTriggers
var importedTrigger = undefined
for (var i = 0; i < workflowTriggers.length; i++) {
if (workflowTriggers[i].name.toLowerCase() === triggerName.toLowerCase()) {
newtriggers.push(workflowTriggers[i])
break
}
}
} else {
// Remove
newtriggers = newtriggers.filter((trigger) => trigger.name !== triggerName)
}
console.log("Triggers: ", newtriggers)
setTriggers(newtriggers)
var parsedtriggers = []
for (var i = 0; i < newtriggers.length; i++) {
parsedtriggers.push(newtriggers[i].name)
}
submitAppDetails("triggers", parsedtriggers.join(","))
}
const removeAppFromSearchEngine = (appID) => {
toast(`Removing app ${appId} from search engine`)
const field = "public"
const data = {
field: field,
value: "false",
app_id: appID,
}
fetch(globalUrl + "/api/v1/apps/label", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason === undefined) {
toast.error("Failed removing app from search engine.")
} else {
toast.error("Failed to remove app from search engine because: " + responseJson.reason)
}
} else {
toast.info("Successfully unpublished app. It can still be accessed by direct link.")
}
})
.catch((error) => {
console.log("Error: ", error)
toast.info("Error when removing app from search engine.")
})
}
const deduplicateByName = (array) => {
const uniqueNames = {};
return array.filter(item => {
if (!item?.hasOwnProperty('name') || !item?.name?.length) {
return true
}
if (!uniqueNames[item.name]) {
uniqueNames[item.name] = true
return true
}
return false
})
}
const ActionSelectOption = (actionprops) => {
const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops;
const [hover, setHover] = React.useState(false);
return (
<Tooltip
color="secondary"
title={newActiondescription}
placement="left"
>
<div style={{
cursor: "pointer",
padding: 8,
paddingLeft: 14,
paddingBottom: 4,
backgroundColor: hover ? theme.palette.surfaceColor : theme.palette.inputColor,
}} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
onClick={(event) => {
console.log("Clicked on action: ", option)
if (option !== undefined && option !== null) {
setSelectedValidationAction(option)
const labelData = {
"app_id": app.id,
"action_name": option.name,
"label": "app_validation",
}
// Should send recommendations to the owner
var url = `${globalUrl}/api/v1/apps/label`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify(labelData),
credentials: "include",
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason)
}
})
.catch((error) => {
console.log("Error: ", error)
})
// Update the app itself?
/*
setNewSelectedAction({
target: {
value: option.name
}
});
*/
}
}}
>
<div style={{ display: "flex", marginBottom: 0,}}>
<span
style={{
marginRight: 10,
marginTop: "auto",
marginBottom: 0,
}}
>
{useIcon}
</span>
<span style={{marginBottom: 0, marginTop: 3, }}>{newActionname}</span>
</div>
{extraDescription.length > 0 ?
<Typography variant="body2" color="textSecondary" style={{marginTop: 0, overflow: "hidden", whiteSpace: "nowrap", display: "block",}}>
{extraDescription}
</Typography>
: null}
</div>
</Tooltip>
)
}
const userRoles = ["you", "public"];
const updateAppField = (app_id, fieldname, fieldvalue) => {
const data = {};
data[fieldname] = fieldvalue;
console.log("DATA: ", data);
fetch(globalUrl + "/api/v1/apps/" + app_id, {
method: "PATCH",
headers: {
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
//setAppSearchLoading(false)
return response.json();
})
.then((responseJson) => {
//console.log(responseJson)
//toast(responseJson)
if (responseJson.success) {
toast("Successfully updated app configuration");
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
toast("Error: "+responseJson.reason);
} else {
toast("Error updating app configuration. Are you the owner of this app?");
}
}
})
.catch((error) => {
toast(error.toString());
});
};
const sortByCategoryLabel = (a, b) => {
const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0
const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0
// Sort by existence and length of "category_label"
if (aHasCategoryLabel && !bHasCategoryLabel) {
return -1
} else if (!aHasCategoryLabel && bHasCategoryLabel) {
return 1
} else {
return 0
}
}
const getDownloadUrl = () => {
console.log("APP: ", app)
const appEnding = app?.public === true ? app?.app_version : app?.id
return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip`
}
const renderedActionOptions = deduplicateByName((
actions === undefined || actions === null ? [] :
actions.filter((a) =>
a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(actions, "label"))
).sort(sortByCategoryLabel))
const actionView =
actions === undefined || actions === null ? null : (
<div style={{ color: "white" }}>
<div style={{ display: "flex" }}>
{isMobile || appType === 0 || appType === 2 ? null : (
<span style={{maxWidth: 350, minWidth: 350, marginRight: 10, marginLeft: 10, marginTop: 10, }}>
<div
style={{
maxHeight: 800,
overflowY: "auto",
overflowX: "hidden",
}}
>
<LoopActions actions={actions} />
</div>
</span>
)}
<Paper
style={{
flex: 6,
margin: 10,
padding: 30,
backgroundColor: boxStyle.backgroundColor,
color: theme.palette.text.primary,
textAlign: "left",
paddingBottom: 50,
overflow: "hidden",
margin: "auto",
}}
>
<Tabs
color="secondary"
indicatorColor="primary"
textColor="secondary"
variant="fullWidth"
value={selectedTab}
onChange={(event, newValue) => {
if (newValue === 1 && appType === 0 || appType === 2) {
window.open(`/apis/${app.id}`, "_blank")
} else {
setSelectedTab(newValue)
}
}}
style={{ marginBottom: 0, marginLeft: 0, marginRight: 0, minWidth: 800, maxWidth: 800, margin: "auto", }}
aria-label="disabled tabs example"
>
<Tab style={{marginLeft: 0, color: theme.palette.text.primary }} icon={<DescriptionIcon />} label="Docs" />
<Tab icon={appType === 0 || appType === 2 ? <OpenInNewIcon /> : <AppsIcon />} label={appType === 0 || appType === 2 ? "Explore the API" : "Try it out"} />
<Tab icon={<ShowChartIcon />} style={{color: theme.palette.text.primary}} label="Stats" />
<Tab icon={<PolylineIcon />} disabled style={{color: theme.palette.text.secondary}} label="Integrations" />
<Tab icon={<PersonIcon />} disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary}} label="Creator" value={4} />
</Tabs>
<div style={{ marginTop: 25 }}>
{selectedTab === 1 && app.skipped_build == false ? (
<div style={{ display: "flex", marginLeft: 25, }}>
<div style={{ }}>
<SelectedActionView action={currentAction} />
</div>
<div style={{ marginLeft: 25, maxWidth: 350 }}>
{currentAction.description !== undefined && currentAction.description !== null && currentAction.description !== "" ?
<div
style={{
maxHeight: 175,
overflowX: "hidden",
overflowY: "auto",
marginBottom: 25,
}}
>
<Typography variant="h6" style={{ flex: 10 }}>
Action Description
</Typography>
<Typography
variant="body1"
color="textSecondary"
style={{ maxWidth: 350, minWidth: 350 }}
>
{currentAction.description}
</Typography>
</div>
: app.description !== undefined &&
app.description !== null &&
app.description.length > 0 ?
<div
style={{
maxHeight: 175,
overflowX: "hidden",
overflowY: "auto",
marginBottom: 25,
}}
>
<Typography variant="h6" style={{ flex: 10 }}>
App Description
</Typography>
<Typography
variant="body1"
color="textSecondary"
style={{ maxWidth: 350, minWidth: 350 }}
>
{app.description}
</Typography>
</div>
: null}
<div style={{ display: "flex" }}>
<Typography variant="h6" style={{ flex: 10 }}>
Result
</Typography>
<IconButton
disabled={executing || appType === 2}
color="primary"
style={{
border: `1px solid ${theme.palette.primary.main}`,
}}
variant="contained"
onClick={() => {
executeSingleAction(
selectedAction.app_id,
selectedAction
);
}}
>
<Tooltip title="Run Action" placement="top">
<PlayArrowIcon />
</Tooltip>
</IconButton>
</div>
{executing ? (
<div style={{ textAlign: "center", width: "100%" }}>
{serverside === true ? null :
<CircularProgress
style={{
marginTop: 25,
height: 35,
width: 35,
marginLeft: "auto",
marginRight: "auto",
}}
/>
}
</div>
) : (
<div>
{executionResult.valid ? (
<ReactJson
src={executionResult.result}
theme={theme.palette.jsonTheme}
style={theme.palette.reactJsonStyle}
collapsed={false}
displayDataTypes={false}
enableClipboard={(copy) => {
handleReactJsonClipboard(copy);
}}
onSelect={(select) => {
HandleJsonCopy(
executionResult.result,
select,
"exec"
);
console.log("SELECTED!: ", select);
}}
name={"Result"}
/>
) : (
<Typography variant="body2">
{executionResult.result}
</Typography>
)}
</div>
)}
</div>
</div>
) : selectedTab === 2 && app.id !== undefined ? (
<div>
<Typography variant="h4">
Use the App onprem (hybrid)
</Typography>
<Typography variant="body2" color="textSecondary" style={{marginTop: 5, }}>
Due to using docker containers with privately uploaded containers, we had to use a custom registry. Use the command below to download the image to the server if it fails to run.
It will authenticate and authorize you, before redirecting to a Signed URL on https://storage.googleapis.com
<b>&nbsp;Now also works for ARM containers!</b>
</Typography>
<div
style={{
marginTop: 10,
padding: 15,
minWidth: "50%",
maxWidth: "100%",
backgroundColor: theme.palette.inputColor,
overflowY: "auto",
// Have it inline
borderRadius: theme.palette?.borderRadius,
}}
>
<code
style={{
// Wrap if larger than X
whiteSpace: "pre",
overflow: "auto",
marginRight: 40,
}}
>
{getDownloadUrl()}
</code>
</div>
<CopyToClipboard
text={getDownloadUrl()}
/>
<div style={{marginTop: 50, }}>
<AppStats
globalUrl={globalUrl}
appId={app.id}
{...props}
/>
</div>
</div>
) : selectedTab === 0 ? (
<div
style={{ minWidth: "100%", maxWidth: "100%", margin: "auto" }}
>
<Markdown
components={{
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
id="markdown_wrapper"
escapeHtml={false}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{appDocumentation}
</Markdown>
</div>
) : selectedTab === 3 && secondaryApp.objectID !== undefined && app.name !== undefined ? (
<div
style={{
maxWidth: "100%",
minWidth: "100%",
margin: "auto",
marginTop: 50,
}}
>
<Typography
variant="h1"
style={{ textAlign: "center", fontSize: 22 }}
>
Connect {app.name.replaceAll("_", " ")} and{" "}
{secondaryApp.name.replaceAll("_", " ")}
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{ textAlign: "center" }}
>
Using Shuffle, you can connect{" "}
{app.name.replaceAll("_", " ")} and{" "}
{secondaryApp.name.replaceAll("_", " ")} with no code.
</Typography>
<div style={{ display: "flex", marginTop: 25 }}>
<div
style={{
flex: 2,
display: "flex",
maxHeight: 100,
minHeight: 100,
border: "1px solid rgba(255,255,255,0.3",
marginRight: 10,
borderRadius: theme.palette?.borderRadius,
}}
>
<img
src={app.large_image}
alt={app.name}
id="logo"
style={imageStyle}
/>
<Typography variant="h6" style={textStyle}>
{app.name.replaceAll("_", " ")}
</Typography>
</div>
<div
style={{
flex: 2,
display: "flex",
maxHeight: 100,
minHeight: 100,
border: "1px solid rgba(255,255,255,0.3",
marginleft: 10,
borderRadius: theme.palette?.borderRadius,
}}
>
<img
src={secondaryApp.image_url}
alt={secondaryApp.name}
id="logo"
style={imageStyle}
/>
<Typography variant="h6" style={textStyle}>
{secondaryApp.name.replaceAll("_", " ")}
</Typography>
</div>
</div>
<div style={{ textAlign: "center", marginTop: 25 }}>
<Link
rel="noopener noreferrer"
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}`}
style={{ textDecoration: "none" }}
>
<Button
variant="contained"
onClick={() => {
const allapps = [app.name, secondaryApp.name].sort()
ReactGA.event({
category: "Appexplorer",
action: "app_connect_click",
label: `${allapps[0]}_and_${allapps[1]}`,
})
}}
style={{
padding: 15,
marginTop: 15,
borderRadius: 25,
height: 50,
margin: "15px 0px 15px 0px",
fontSize: 14,
color: "white",
backgroundImage: buttonBackground,
marginRight: 10,
}}
>
Connect {app.name.replaceAll("_", " ")} and{" "}
{secondaryApp.name.replaceAll("_", " ")}
</Button>
</Link>
</div>
<Divider style={{ marginTop: 50, marginBottom: 50 }} />
<div
style={{
minWidth: "100%",
maxWidth: "100%",
margin: "auto",
}}
>
<Markdown
components={{
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
id="markdown_wrapper"
escapeHtml={false}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{appDocumentation}
</Markdown>
</div>
</div>
) :
selectedTab === 4 ?
<div>
<Typography variant="h4">
App Details
</Typography>
<Typography variant="body2" style={{marginTop: 20, marginBottom: 10, }} color="textSecondary">
Add more details about your app here. This is to help both the Shuffle team, and the public get easier access to this information. Data from these will be used to track app "completeness" for recommendation systems.
</Typography>
<Divider style={{marginTop: 10, marginBottom: 10, }}/>
<Typography variant="h6" style={{marginTop: 20, marginBottom: 10, }}>
Validation Action
</Typography>
<Typography variant="body2" style={{marginBottom: 20, }} color="textSecondary">
The validation action is the action that is used to validate the app. This is used both when a user wants to validate their auth, as well as when Shuffle runs automatic tests of the app. It is recommended that the action should be a GET request. Validation is decided based on whether the action is ran successfully in a workflow.
</Typography>
<Autocomplete
id="action_search"
value={selectedValidationAction}
classes={{ inputRoot: classes.inputRoot }}
groupBy={(option) => {
// Most popular
// Is categorized
// Uncategorized
return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions";
}}
renderGroup={(params) => {
return (
<li key={params.key}>
<Typography variant="body1" style={{textAlign: "center", marginLeft: 10, marginTop: 25, marginBottom: 10, }}>{params.group}</Typography>
<Typography variant="body2">{params.children}</Typography>
</li>
)
}}
options={renderedActionOptions}
ListboxProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
},
}}
filterOptions={(options, { inputValue }) => {
const lowercaseValue = inputValue === null ? "" : inputValue.toLowerCase()
options = options.filter((x) => {
if (x.name === undefined || x.name === null) {
x.name = ""
}
if (x.description === undefined || x.description === null) {
x.description = ""
}
if (x.method !== "GET") {
return null
}
return x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)
})
return options
}}
getOptionLabel={(option) => {
if (option === undefined || option === null || option.name === undefined || option.name === null ) {
return null;
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
fullWidth
style={{
backgroundColor: theme.palette.inputColor,
height: 50,
borderRadius: theme.palette?.borderRadius,
}}
onChange={(event, newValue) => {
// Workaround with event lol
console.log("Changed to: ", event, newValue)
toast("Changed validation action")
if (newValue !== undefined && newValue !== null) {
/*
setNewSelectedAction({
target: {
value: newValue.name
}
})
*/
}
}}
renderOption={(props, option, state) => {
var newActionname = option.name;
if (option.label !== undefined && option.label !== null && option.label.length > 0) {
newActionname = option.label;
}
var newActiondescription = option.description;
//console.log("DESC: ", newActiondescription)
if (option.description === undefined || option.description === null) {
newActiondescription = "Description: No description defined for this action"
} else {
newActiondescription = "Description: "+newActiondescription
}
const iconInfo = GetIconInfo({ name: option.name });
const useIcon = iconInfo.originalIcon;
if (newActionname === undefined || newActionname === null) {
newActionname = "No name"
option.name = "No name"
option.label = "No name"
}
newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " ");
var method = ""
var extraDescription = ""
if (option.name.includes("get_")) {
method = "GET"
} else if (option.name.includes("post_")) {
method = "POST"
} else if (option.name.includes("put_")) {
method = "PUT"
} else if (option.name.includes("patch_")) {
method = "PATCH"
} else if (option.name.includes("delete_")) {
method = "DELETE"
} else if (option.name.includes("options_")) {
method = "OPTIONS"
} else if (option.name.includes("connect_")) {
method = "CONNECT"
}
// FIXME: Should it require a base URL?
if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) {
var extraUrl = ""
const descSplit = option.description.split("\n")
// Last line of descSplit
if (descSplit.length > 0) {
extraUrl = descSplit[descSplit.length-1]
}
if (extraUrl.length > 0) {
if (extraUrl.includes(" ")) {
extraUrl = extraUrl.split(" ")[0]
}
if (extraUrl.includes("#")) {
extraUrl = extraUrl.split("#")[0]
}
extraDescription = `${method} ${extraUrl}`
} else {
//console.log("No url found. Check again :)")
}
}
return (
<ActionSelectOption
{...props}
option={option}
newActiondescription={newActiondescription}
useIcon={useIcon}
newActionname={newActionname}
extraDescription={extraDescription}
/>
);
}}
renderInput={(params) => {
if (params.inputProps?.value) {
const prefixes = ["Post", "Put", "Patch"];
for (let prefix of prefixes) {
if (params.inputProps.value.startsWith(prefix)) {
let newValue = params.inputProps.value.replace(prefix + " ", "");
if (newValue.length > 1) {
newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1);
}
// Set the new value without mutating inputProps
params = { ...params, inputProps: { ...params.inputProps, value: newValue } };
break;
}
}
// Check if it starts with "Get List" and method is "Get"
if (params.inputProps.value.startsWith("Get List")) {
console.log("Get List")
}
}
const actionDescription = ""
const isIntegration = false
return (
<Tooltip title={actionDescription}
placement="right"
open={false}
PopperProps={{
sx: {
'& .MuiTooltip-tooltip': {
backgroundColor: 'transparent',
boxShadow: 'none',
},
'& .MuiTooltip-arrow': {
color: 'transparent',
},
},
}}
>
<TextField
{...params}
data-lpignore="true"
autocomplete="off"
dataLPIgnore="true"
autoComplete="off"
color="primary"
id="checkbox-search"
variant="body1"
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
}}
label={"Select Validation Action"}
variant="outlined"
name={`disable_autocomplete_${Math.random()}`}
/>
</Tooltip>
)
}}
/>
<Divider style={{marginTop: 30, marginBottom: 10, }}/>
<Typography variant="h6" style={{marginTop: 20, marginBottom: 10, }}>
Triggers
</Typography>
<div style={{display: "flex", }}>
<Typography variant="body1" color="textSecondary" style={{marginTop: 7, }}>
Schedule:
</Typography>
<Checkbox
checked={triggers === undefined ? false : triggers.find(trigger => trigger.name === "Schedule") !== undefined}
label="Schedule"
onChange={(e) => {
editTrigger("Schedule")
}}
/>
<Typography variant="body1" color="textSecondary" style={{marginTop: 7, marginLeft: 20, }}>
Webhook:
</Typography>
<Checkbox
checked={triggers === undefined ? false : triggers.find(trigger => trigger.name === "Webhook") !== undefined}
label="Webhook"
onChange={() => {
editTrigger("Webhook")
}}
/>
</div>
{triggers === undefined || triggers === null || triggers.find(trigger => trigger.name === "Webhook") === undefined ? null :
<TextField
label="Webhook Example response"
variant="outlined"
disabled
fullWidth
multiline
style={{
maxHeight: 200,
}}
onBlur={(e) => {
submitAppDetails("extra_value", e.target.value)
}}
/>
}
<Divider />
<Typography variant="h6" style={{marginTop: 20, marginBottom: 10, }}>
External info
</Typography>
<AppDetails
title="External Documentation URL"
inputTitle={app.reference_info.documentation_url === undefined ? "" : app.reference_info.documentation_url}
/>
<AppDetails
title="Blogpost"
inputTitle={app.blogpost === undefined ? "" : app.blogpost}
/>
<AppDetails
title="Video"
inputTitle={app.video === undefined ? "" : app.video}
/>
<Typography variant="h6" style={{marginTop: 20, marginBottom: 10, }}>
Partner Details
</Typography>
<AppDetails
title="Partner Website"
inputTitle={app.company_url === undefined ? "" : app.company_url}
/>
<AppDetails
title="Partner Contacts"
inputTitle={app.reference_info.partner_contacts === undefined ? "" : app.reference_info.partner_contacts}
/>
<Divider style={{marginTop: 20, }} />
<Typography variant="h6" style={{marginTop: 20, marginBottom: 10, }}>
Public Status
</Typography>
<Button
style={{marginTop: 10, }}
variant="outlined"
onClick={() => {
removeAppFromSearchEngine(app.id)
}}
>
Unpublish App from Search Engine
</Button>
</div>
:
(
<div>
<Typography
variant="h1"
color="textSecondary"
style={{
margin: "auto",
textAlign: "center",
fontSize: 22,
marginTop: 20,
paddingTop: 20,
maxWidth: 550,
minWidth: 550,
}}
>
This app is currently in Beta, but is usable. Interested in using this app? Click the button below or contact us.
<div style={{ marginTop: 10 }}>
{activateButton}
</div>
</Typography>
{app.description !== undefined &&
app.description !== null &&
app.description !== "" ?
<span style={{textAlign: "center", }}>
<Typography variant="h6" style={{ marginTop: 50, }}>
More about the app
</Typography>
<Typography
variant="body1"
color="textSecondary"
style={{ maxWidth: 550, minWidth: 550, margin: "auto", marginTop: 10, }}
>
{app.description}
</Typography>
</span>
: null}
</div>
)}
</div>
</Paper>
</div>
</div>
);
// Random names for type & autoComplete. Didn't research :^)
const imageData = file.length > 0 ? file : fileBase64;
const height = 100;
const imageInfo = (
<img
src={imageData}
alt=""
id="logo"
style={{
borderRadius: theme.palette?.borderRadius,
border: "1px solid rgba(255,255,255,0.6)",
maxWidth: height,
maxHeight: height,
minWidth: height,
minHeight: height,
objectFit: "contain",
}}
/>
);
const publishModal = publishModalOpen ? (
<Dialog
open={publishModalOpen}
onClose={() => {
setPublishModalOpen(false);
}}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: '500px',
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}
>
<DialogTitle style={{ marginBottom: 0, padding: 50, paddingBottom: 0, }}>
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.9)" }}>
Are you sure you want to PUBLISH this app?
</div>
</DialogTitle>
<DialogContent
style={{ color: "rgba(255,255,255,0.65)", textAlign: "center", padding: 50, }}
>
<div>
<Typography variant="body1" style={{ marginBottom: 20 }}>
Before publishing, make sure to sanitize the App for anything you don't want public.
</Typography>
<Typography variant="body1" style={{ marginBottom: 20 }}>
The published App is yours, and you can always change your public Apps after they are released.
</Typography>
</div>
<Button
variant="contained"
color="primary"
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, }}
onClick={() => {
updateAppField(selectedApp.id, "sharing", true);
setPublishModalOpen(false);
}}
>
Yes
</Button>
<Button
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: "#ff8544", marginLeft: 5 }}
onClick={() => {
setPublishModalOpen(false);
}}
color="primary"
>
No
</Button>
</DialogContent>
</Dialog>
) : null;
const landingpageDataBrowser = (
<div
style={{
paddingTop: isMobile ? 0 : 25,
paddingBottom: 100,
color: "white",
}}
>
{publishModal}
<div style={{ display: "flex", position: "relative" }}>
{isMobile ? null : (
<Breadcrumbs
aria-label="breadcrumb"
separator=""
style={{ color: theme.palette.text.primary, marginLeft: 15, flex: 100 }}
>
<Link
to="/search"
style={{ textDecoration: "none", color: "inherit" }}
>
<h2 style={{ color: theme.palette.text.primary }}>
<AppsIcon style={{ marginRight: 10 }} />
Apps
</h2>
</Link>
<Link
to={`/apps/${params.appid}`}
style={{ textDecoration: "none", color: "inherit" }}
>
<h2>{name}</h2>
</Link>
</Breadcrumbs>
)}
<div
style={{ position: "absolute", top: 40, right: 10, display: "flex", gap: "10px",}}
>
{app.documentation_download_url !== undefined &&
app.documentation_download_url !== null &&
app.documentation_download_url.length > 0 ? (
<IconButton
color="primary"
style={{ marginRight: 20 }}
variant="contained"
onClick={() => {
const data = openapi;
let linkElement = document.createElement("a");
linkElement.setAttribute("target", "_blank");
linkElement.setAttribute(
"href",
app.documentation_download_url
);
linkElement.setAttribute(
"download",
app.documentation_download_url
);
linkElement.click();
}}
>
<Tooltip title="Download Documentation" placement="top">
<DescriptionIcon color="secondary" />
</Tooltip>
</IconButton>
) : null}
{appType === 0 || appType === 2 ? (
<IconButton
style={{ marginRight: 20 }}
onClick={() => {
const data = openapi;
let exportFileDefaultName = name + ".json";
let dataStr = JSON.stringify(data);
let dataUri =
"data:application/json;charset=utf-8," +
encodeURIComponent(dataStr);
let linkElement = document.createElement("a");
linkElement.setAttribute("href", dataUri);
linkElement.setAttribute("download", exportFileDefaultName);
linkElement.click();
const tmpurl = new URL(window.location.href);
const searchParams = tmpurl.searchParams;
const queryID = searchParams.get("queryID");
if (queryID !== undefined && queryID !== null) {
aa("init", {
appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e",
});
const timestamp = new Date().getTime();
aa("sendEvents", [
{
eventType: "conversion",
eventName: "Public App Downloaded",
index: "appsearch",
objectIDs: [app.id],
timestamp: timestamp,
queryID: queryID,
userToken:
userdata === undefined ||
userdata === null ||
userdata.id === undefined
? "unauthenticated"
: userdata.id,
},
]);
} else {
console.log("No query to handle when downloading");
}
}}
>
<Tooltip title="Download OpenAPI" placement="top">
<CloudDownloadIcon color={theme.palette.text.primary} />
</Tooltip>
</IconButton>
) : null}
{selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.id !== undefined && userdata.support === true ?
// Iconbutton for authentication with just an icon. Link to /apps/authentication?app_id=app.id
<IconButton
color="secondary"
variant="contained"
style={{marginRight: 20, }}
onClick={() => {
window.open(`/appauth?app_id=${app.id}&auth=${selectedOrganization.org_auth.token}`, "_blank")
}}
>
<Tooltip title="Public Authentication link for the current Organization. Times out every 24 hours." placement="top">
<LockOpenIcon color={theme.palette.text.primary}/>
</Tooltip>
</IconButton>
: null}
{isMobile || userdata?.active_apps === undefined || userdata?.active_apps === null || !userdata?.active_apps?.includes(appId) ? null :
<Button
variant={userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) ? "outlined": "contained"}
component="label"
color="primary"
onClick={() => {
if (!isLoggedIn) {
//navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
toast("You must be logged in to activate apps! Go to /login first.")
return;
}
toast.info("Sending download job to relevant runtime locations")
activateApp("distribute")
}}
style={{ height: 40, marginTop: 5 }}
>
Push Image
</Button>
}
{isMobile ? null : (
<Button
variant={userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) ? "outlined": "contained"}
component="label"
color="primary"
onClick={() => {
if (!isLoggedIn) {
//navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
toast("You must be logged in to activate apps! Go to /login first.")
return;
}
const tmpurl = new URL(window.location.href);
const searchParams = tmpurl.searchParams;
const queryID = searchParams.get("queryID");
if (queryID !== undefined && queryID !== null) {
aa("init", {
appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e",
});
const timestamp = new Date().getTime();
aa("sendEvents", [
{
eventType: "conversion",
eventName: "Public App Activated",
index: "appsearch",
objectIDs: [app.id],
timestamp: timestamp,
queryID: queryID,
userToken:
userdata === undefined ||
userdata === null ||
userdata.id === undefined
? "unauthenticated"
: userdata.id,
},
]);
} else {
console.log("No query to handle when activating");
}
activateApp();
}}
style={{ height: 40, marginTop: 5 }}
>
{userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) ?
"Deactivate App"
:
"Activate App"
}
</Button>
)}
{appType === 1 ? null :
userdata?.support || userdata?.id === app?.owner || (userdata?.admin === "true" && userdata?.active_org?.id === app?.reference_org) || app?.contributors?.includes(userdata?.id) || creatorProfile?.self === true ?
<Tooltip title="Edit App" placement="top">
<Button
variant="outlined"
component="label"
color="secondary"
onClick={() => {
navigate(`/apps/edit/${appId}`)
}}
style={{ height: 40, marginTop: 5, marginLeft: 5, }}
>
<EditIcon style={{marginRight: 10, }}/> Edit
</Button>
</Tooltip>
: isMobile || !isLoggedIn ? null :
<Tooltip title="Fork this app to edit it for yourself" placement="top">
<Button
variant="outlined"
component="label"
color="secondary"
onClick={() => {
navigate(`/apps/new?id=${appId}`)
}}
style={{ height: 40, marginTop: 5, marginLeft: 5, }}
>
<ForkRightIcon style={{marginRight: 10, }}/> Fork
</Button>
</Tooltip>
}
{appType === 0 || appType === 2 ? (
<div style={{display: "flex", alignItems: 'center',gap: 10 }}>
<a
rel="noopener noreferrer"
href={`/apis/${app.id}`}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e", }}
>
<Button
variant="outlined"
color="secondary"
style={{ height: 40, marginTop: 5, marginLeft: 5, }}
>
<OpenInNewIcon style={{marginRight: 5, }}/>
Explore API
</Button>
</a>
<Select
value={sharingConfiguration}
disabled={!isCloud}
onChange={(event) => {
if ((appdata.owner !== userdata.id) && (!userdata.support)) {
toast("You're not the owner of this app. You can't update it.");
return;
}
setSharingConfiguration(event.target.value);
if (event.target.value === "you") {
toast("Changing sharing to " + event.target.value);
updateAppField(appdata.id, "sharing", false);
} else if (
event.target.value === "everyone" ||
event.target.value === "public"
) {
if (!isCloud) {
setPublishModalOpen(true)
} else {
updateAppField(appdata.id, "sharing", true);
}
} else {
console.log(
"Can't handle value for sharing: ",
event.target.value
);
}
}}
style={{
width: 150,
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.text.primary,
height: 40,
marginTop: 5
}}
SelectDisplayProps={{
style: {
marginLeft: 10,
display: 'flex'
},
}}
>
{userRoles.map((data) => {
return (
<MenuItem
key={data}
sx={{ backgroundColor: inputColor, color: theme.palette.text.primary, display: 'flex', "&:hover": { backgroundColor: theme.palette.hoverColor } }}
value={data}
>
{data}
</MenuItem>
);
})}
</Select>
</div>
) : (
<a
rel="noopener noreferrer"
href={"https://shuffler.io/docs/app_creation#python---building-apps-manually"}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e" }}
>
<img
src={"/images/frameworks/python.jpeg"}
alt="Python integration for Shuffle"
id="logo"
style={{
marginTop: 5,
marginLeft: 15,
height: 35,
borderRadius: theme.palette?.borderRadius,
}}
/>
</a>
)}
</div>
</div>
<div
style={{ display: "flex", flexDirection: isMobile ? "column" : "row" }}
>
<Paper style={boxStyle}>
<div
style={{
color: "white",
flex: 1,
display: "flex",
flexDirection: "row",
}}
>
<div
style={{
flex: 1,
margin: 10,
cursor: "pointer",
backgroundColor: inputColor,
maxWidth: height,
maxHeight: height,
}}
onClick={() => {
upload.click();
}}
>
{imageInfo}
</div>
<div style={{ marginLeft: 15, color: "white" }}>
<div style={{display: "flex", marginTop: 5, }}>
<Typography variant="h6" style={{ }}>
{name}
</Typography>
<span style={{ marginTop: "auto", marginBottom: "auto" }}>
{(app.public || appType === 0) && app.skipped_build == false ? (
<Tooltip title="This app is verified" placement="top">
<VerifiedUserIcon style={{marginLeft: 10, }} />
</Tooltip>
) : (
<Tooltip
title="This app may not work, and can't be tested yet"
placement="top"
>
<WarningIcon />
</Tooltip>
)}
</span>
{/* Handles category changing, but looks like shit. Should probably work as a suggestion */}
{/*
<Select
onChange={(e) => {
console.log("Changed: ", e.target.value)
setNewWorkflowCategories([e.target.value]);
}}
value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]}
style={{
flexAlign: "right",
color: "white",
height: 40,
width: 150,
borderRadius: 20,
marginLeft: 200,
}}
>
{categories.map((category, categoryIndex) => {
if (category === undefined || category === null || category === "") {
return null
}
return (
<MenuItem
key={categoryIndex}
value={category}
style={{
}}
>
{category.name}
</MenuItem>
)
})}
</Select>
*/}
</div>
<div style={{ marginTop: 5 }}>
<Typography variant="body1" color="textSecondary">
Version - {app?.app_version}
</Typography>
</div>
<div style={{ marginTop: 5 }} />
{Object.getOwnPropertyNames(creatorProfile).length !== 0 &&
creatorProfile.github_avatar !== undefined &&
creatorProfile.github_avatar !== null ? (
<div style={{ display: "flex" }}>
<IconButton
color="primary"
style={{ padding: 0, marginRight: 10 }}
aria-controls="simple-menu"
aria-haspopup="true"
onClick={(event) => {
setAnchorElAvatar(event.currentTarget);
}}
>
<Link
to={`/creators/${creatorProfile.github_username}`}
style={{ textDecoration: "none", color: "#f86a3e" }}
>
<Avatar
style={{ height: 30, width: 30 }}
alt={contact.name}
src={creatorProfile.github_avatar}
/>
</Link>
</IconButton>
<Typography
variant="body1"
color="textSecondary"
style={{ color: "" }}
>
Shared by{" "}
<Link
to={`/creators/${creatorProfile.github_username}`}
style={{ textDecoration: "none", color: "#f86a3e" }}
>
{creatorProfile.github_username}
</Link>
<Button
style={{
borderRadius: 25,
fontSize: 11,
color: theme.palette.text.primary,
backgroundColor: theme.palette.platformColor,
border: "1px solid #ddf4e1",
textTransform: "none",
marginLeft: 4,
padding: 1,
}}
onClick={() => {
ReactGA.event({
category: "hiring_button",
action: `${creatorProfile.github_username}_app_click`,
label: name,
});
if (window.drift !== undefined) {
window.drift.api.startInteraction({
interactionId: 342345,
});
} else {
console.log(
"Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ",
window.drift
);
toast(
`Thanks for showing interest in getting help from ${creatorProfile.github_username}`
);
}
}}
>
Get help
</Button>
{contact.name !== undefined &&
contact.name !== null &&
!contact.name.includes("frikky") &&
contact.name.length > 0 &&
contact.name.toLowerCase() !==
creatorProfile.github_username.toLowerCase() &&
!(
contact.name.toLowerCase().includes("anon") &&
creatorProfile.github_username.length > 0
) ? (
<span style={{ marginLeft: 15 }}>
{" "}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Created by {contact.name}
</span>
) : (
""
)}
</Typography>
</div>
) : contact.name !== undefined &&
contact.name !== null &&
contact.name.length > 0 ? (
<Typography
variant="body1"
color="textSecondary"
style={{ color: "" }}
>
Created by {contact.name}
</Typography>
) : null}
<div
style={{
marginTop: 5,
minHeight: 30,
maxHeight: 30,
overflow: "hidden",
}}
>
{newWorkflowTags.map((tag, index) => {
return (
<Chip
key={index}
style={chipStyle}
variant="outlined"
label={tag}
color="primary"
/>
);
})}
</div>
</div>
</div>
</Paper>
{isMobile || serverside ? null : (
<Card
style={{
flex: 1,
margin: 10,
backgroundColor: boxStyle.backgroundColor,
color: "white",
textAlign: "center",
}}
sx={{
"&:hover": {
backgroundColor: theme.palette.hoverColor,
},
}}
>
<CardActionArea
component={Link}
to={"/search?tab=workflows&q=" + name.split(" ")[0]}
style={{ height: "100%" }}
>
<div style={{ padding: "50px 0" }}>
<Typography variant="h4">
{relatedWorkflows !== 0 ? (
relatedWorkflows
) : (
<InstantSearch
searchClient={searchClient}
indexName="workflows"
>
<CustomSearchBox defaultRefinement={name.split(" ")[0]} />
<CustomWorkflowHits />
</InstantSearch>
)}
</Typography>
<Typography
variant="h6"
color="textSecondary"
style={{ marginTop: 5 }}
>
Workflows
</Typography>
</div>
</CardActionArea>
</Card>
)}
{app.video !== undefined &&
app.video !== null &&
app.video.includes("http") ? (
<div style={{ margin: 10 }}>
{app.video.includes("loom.com/share") &&
app.video.split("/").length > 4 ? (
<div>
<iframe
src={`https://www.loom.com/embed/${app.video.split("/")[4]}`}
frameBorder={false}
webkitallowFullscreen={true}
mozallowFullscreen={true}
allowFullScreen={true}
style={{
marginTop: 15,
top: 0,
left: 0,
maxWidth: 230,
minWidth: 230,
}}
/>
</div>
) : app.video.includes("youtube.com") &&
app.video.split("/").length > 3 &&
app.video.includes("v=") ? (
<div>
<iframe
src={`https://www.youtube.com/embed/${new URL(
app.video
).searchParams.get("v")}`}
frameBorder={false}
webkitallowFullscreen={true}
mozallowFullscreen={true}
allowFullScreen={true}
style={{
top: 0,
left: 0,
maxWidth: 230,
minWidth: 230,
}}
/>
</div>
) : (
<Typography variant="body1">{app.video}</Typography>
)}
</div>
) : isMobile || serverside ? null : (
<Card
style={{
flex: 1,
margin: 10,
backgroundColor: boxStyle.backgroundColor,
color: "white",
textAlign: "center",
}}
sx={{
"&:hover": {
backgroundColor: theme.palette.hoverColor,
},
}}
>
<CardActionArea
component={Link}
to={"/search?tab=apps&q=" + name.split(" ")[0]}
style={{
height: "100%",
}}
>
<div style={{ padding: "50px 0" }}>
<Typography variant="h4">
{relatedApps !== 0 ? (
relatedApps
) : (
<InstantSearch
searchClient={searchClient}
indexName="appsearch"
>
<CustomSearchBox defaultRefinement={name.split(" ")[0]} />
<CustomAppHits />
</InstantSearch>
)}
</Typography>
<Typography
variant="h6"
color="textSecondary"
style={{ marginTop: 5 }}
>
Related apps
</Typography>
</div>
</CardActionArea>
</Card>
)}
</div>
<div>
<div style={{}}>{actionView}</div>
{errorCode.length > 0 ? `Error: ${errorCode}` : null}
</div>
</div>
);
const loadedCheck =
(isLoaded && isAppLoaded) || serverside === true ? (
<div>
{serverside === true ? (
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
) : (
<Fade
in={true}
timeout={1000}
style={{ transitionDelay: `${150}ms` }}
>
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
</Fade>
)}
<TextField
id="copy_element_shuffle"
value={to_be_copied}
style={{ display: "none" }}
/>
</div>
) : (
<div>
{serverside === true ? null :
<div style={{ margin: "auto", textAlign: "center", width: 250 }}>
<CircularProgress
style={{
marginTop: "27vh",
minHeight: 35,
maxHeight: 35,
minWidth: 35,
maxWidth: 35,
marginLeft: "auto",
marginRight: "auto",
}}
/>
<Typography variant="body1" color="textSecondary">
Loading App
</Typography>
</div>
}
</div>
);
return <div style={{ marginLeft: !isLoggedIn || !isLoaded ? 0 : leftSideBarOpenByClick ? 280 : 80, transition: 'margin-left 0.3s ease'}}>{loadedCheck}</div>;
};
export default AppExplorer;