Fixed debouncecallback ref
This commit is contained in:
@@ -365,6 +365,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shuffle/shuffle-shared v0.9.30 h1:3CYvNyD7sTxdxoZjTVrtaDqFvSWQRKAFGaga6rPGf8A=
|
||||
github.com/shuffle/shuffle-shared v0.9.30/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw=
|
||||
github.com/shuffle/shuffle-shared v0.9.31 h1:BMoDO4Sgz4+I12aHhc3cWHiCuAQ827XIxajPqCJ9cXA=
|
||||
github.com/shuffle/shuffle-shared v0.9.31/go.mod h1:vfI2QDGphZGrcwuUPQ1yI/Hgc8aseFro5+2k36irfkQ=
|
||||
github.com/shuffle/singul v0.0.16 h1:dW+0Mln9R1aUJ0fjikpWcxbjoQWqJHxe4kSxh2tQN5E=
|
||||
github.com/shuffle/singul v0.0.16/go.mod h1:LYkp320A6gsoPlYbXUM+WvEPUVAuutlSsqnVKyRy4gs=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
|
||||
@@ -0,0 +1,891 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx'
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import { isMobile } from "react-device-detect"
|
||||
import Markdown from "react-markdown";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
import { toast } from "react-toastify";
|
||||
import { v4 as uuidv4} from "uuid";
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
DragIndicator as DragIndicatorIcon,
|
||||
Close as CloseIcon,
|
||||
LockOpen as LockOpenIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
|
||||
const AuthenticationModal = (props) => {
|
||||
const {
|
||||
globalUrl,
|
||||
userdata,
|
||||
|
||||
selectedAppData,
|
||||
getAppAuthentication,
|
||||
appAuthentication,
|
||||
setSelectedAction,
|
||||
|
||||
selectedMeta,
|
||||
setSelectedMeta,
|
||||
|
||||
setAppAuthentication,
|
||||
} = props;
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
|
||||
const [selectedAuthentication, setSelectedAuthentication] = React.useState({});
|
||||
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
|
||||
const [authenticationType, setAuthenticationType] = React.useState({})
|
||||
|
||||
const [appid, setAppId] = useState("")
|
||||
|
||||
//const [appAuthentication, setAppAuthentication] = useState([]);
|
||||
|
||||
const { themeMode, supportEmail, brandColor } = useContext(Context)
|
||||
const theme = getTheme(themeMode, brandColor)
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedAppData === undefined || selectedAppData === null || Object.getOwnPropertyNames(selectedAppData).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
|
||||
setAuthenticationType({
|
||||
type: "",
|
||||
})
|
||||
|
||||
selectedAppData.authentication = {
|
||||
type: "",
|
||||
required: false,
|
||||
}
|
||||
} else {
|
||||
setAuthenticationType(
|
||||
selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? {
|
||||
type: selectedAppData.authentication.type,
|
||||
redirect_uri: selectedAppData.authentication.redirect_uri,
|
||||
refresh_uri: selectedAppData.authentication.refresh_uri,
|
||||
token_uri: selectedAppData.authentication.token_uri,
|
||||
scope: selectedAppData.authentication.scope,
|
||||
client_id: selectedAppData.authentication.client_id,
|
||||
client_secret: selectedAppData.authentication.client_secret,
|
||||
grant_type: selectedAppData.authentication.grant_type,
|
||||
} : {
|
||||
type: "",
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [selectedAppData])
|
||||
|
||||
if (selectedAppData === undefined || selectedAppData === null || Object.getOwnPropertyNames(selectedAppData).length === 0) {
|
||||
console.log("No app data for authentication modal");
|
||||
return null
|
||||
}
|
||||
|
||||
if (authenticationModalOpen === false) {
|
||||
return (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
minWidth: 275,
|
||||
maxWidth: 275,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
setAppId(selectedAppData?.id)
|
||||
setAuthenticationModalOpen(true)
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAppData?.name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
|
||||
src={selectedAppData?.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
|
||||
Authenticate {selectedAppData?.name?.replaceAll("_", " ", -1)}
|
||||
</Typography>
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function Heading(props) {
|
||||
const element = React.createElement(
|
||||
`h${props.level}`,
|
||||
{ style: { marginTop: 40 } },
|
||||
props.children
|
||||
);
|
||||
return (
|
||||
<Typography>
|
||||
{props.level !== 1 ? (
|
||||
<Divider
|
||||
style={{
|
||||
width: "90%",
|
||||
marginTop: 40,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{element}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const UpdateAppAuthentication = (data) => {
|
||||
if (data === undefined || data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
|
||||
if (filteredData.length === 0) {
|
||||
setAppAuthentication([]);
|
||||
setSelectedAuthentication({});
|
||||
} else {
|
||||
setAppAuthentication(filteredData);
|
||||
setSelectedAuthentication(filteredData[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const HandleAppAuthentication = () => {
|
||||
|
||||
const url = `${globalUrl}/api/v1/apps/authentication`;
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
}).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
}).then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
UpdateAppAuthentication(responseJson.data);
|
||||
} else {
|
||||
toast.error("Failed to get app authentication data");
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error("error for app is :", error);
|
||||
});
|
||||
}
|
||||
|
||||
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 ||
|
||||
selectedApp.authentication.parameters === null ||
|
||||
selectedApp.authentication.parameters === undefined ||
|
||||
selectedApp.authentication.parameters.length === 0
|
||||
) {
|
||||
return (
|
||||
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
|
||||
<Typography variant="h4" id="draggable-dialog-title" style={{ cursor: "move", }}>
|
||||
{selectedApp.name} does not require authentication
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
authenticationOption.app.actions = [];
|
||||
|
||||
for (let paramkey in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] === undefined
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] = "";
|
||||
}
|
||||
}
|
||||
|
||||
const setNewAppAuth = (appAuthData, refresh) => {
|
||||
setSelectedAuthentication(appAuthData);
|
||||
var headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
headers["Org-Id"] = userdata?.active_org?.id
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||
method: "PUT",
|
||||
headers: headers,
|
||||
body: JSON.stringify(appAuthData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for setting app auth :O!");
|
||||
|
||||
if (response.status === 400) {
|
||||
toast.error("Failed setting new auth. Please try again", {
|
||||
"autoClose": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success) {
|
||||
toast.error("Error: " + responseJson.reason, {
|
||||
"autoClose": false,
|
||||
})
|
||||
|
||||
} else {
|
||||
HandleAppAuthentication()
|
||||
setAuthenticationModalOpen(false)
|
||||
getAppAuthentication()
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("New auth error: ", error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitCheck = () => {
|
||||
if (authenticationOption.label.length === 0) {
|
||||
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||||
}
|
||||
for (let paramkey in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
].length === 0
|
||||
) {
|
||||
if (
|
||||
selectedApp.authentication.parameters[paramkey].value !== undefined &&
|
||||
selectedApp.authentication.parameters[paramkey].value !== null &&
|
||||
selectedApp.authentication.parameters[paramkey].value.length > 0
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] = selectedApp.authentication.parameters[paramkey].value;
|
||||
} else {
|
||||
if (
|
||||
selectedApp.authentication.parameters[paramkey].schema.type === "bool"
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] = "false";
|
||||
} else {
|
||||
toast(
|
||||
"Field " +
|
||||
selectedApp.authentication.parameters[paramkey].name +
|
||||
" can't be empty"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||||
var newFields = [];
|
||||
for (let authkey in newAuthOption.fields) {
|
||||
const value = newAuthOption.fields[authkey];
|
||||
newFields.push({
|
||||
"key": authkey,
|
||||
"value": value,
|
||||
});
|
||||
}
|
||||
|
||||
newAuthOption.fields = newFields
|
||||
setNewAppAuth(newAuthOption)
|
||||
}
|
||||
|
||||
if (authenticationOption.label === null || authenticationOption.label === undefined) {
|
||||
authenticationOption.label = selectedApp.name + " authentication";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle id="draggable-dialog-title" style={{ cursor: "move", }}>
|
||||
<div style={{ color: theme.palette.textColor }}>
|
||||
Authentication for {selectedApp.name.replaceAll("_", " ", -1)}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://shuffler.io/docs/apps#authentication"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
What is app authentication?
|
||||
</a>
|
||||
<div />
|
||||
These are required fields for authenticating with {selectedApp.name}
|
||||
<div style={{ marginTop: 15 }} />
|
||||
<b>Label for you to remember</b>
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Auth july 2020"}
|
||||
defaultValue={`Auth for ${selectedApp.name}`}
|
||||
onChange={(event) => {
|
||||
authenticationOption.label = event.target.value;
|
||||
}}
|
||||
/>
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
backgroundColor: "rgb(91, 96, 100)",
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
{selectedApp.authentication.parameters.map((data, index) => {
|
||||
if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") {
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div key={index} style={{ marginTop: 10 }}>
|
||||
<LockOpenIcon style={{ marginRight: 10 }} />
|
||||
<b>{data.name}</b>
|
||||
|
||||
{data.schema !== undefined &&
|
||||
data.schema !== null &&
|
||||
data.schema.type === "bool" ? (
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
defaultValue={"false"}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
authenticationOption.fields[data.name] = e.target.value;
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: theme.palette.textColor,
|
||||
height: 50,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
key={"false"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: theme.palette.textColor,
|
||||
}}
|
||||
value={"false"}
|
||||
>
|
||||
false
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
key={"true"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: theme.palette.textColor,
|
||||
}}
|
||||
value={"true"}
|
||||
>
|
||||
true
|
||||
</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
type={
|
||||
data.example !== undefined && data.example.includes("***")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
color="primary"
|
||||
defaultValue={
|
||||
data.value !== undefined && data.value !== null
|
||||
? data.value
|
||||
: ""
|
||||
}
|
||||
placeholder={data.example}
|
||||
onChange={(event) => {
|
||||
authenticationOption.fields[data.name] =
|
||||
event.target.value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
color="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{}}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption);
|
||||
handleSubmitCheck()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const authenticationModal = authenticationModalOpen ? (
|
||||
<Dialog
|
||||
PaperComponent={PaperComponent}
|
||||
aria-labelledby="draggable-dialog-title"
|
||||
hideBackdrop={true}
|
||||
disableEnforceFocus={true}
|
||||
disableBackdropClick={true}
|
||||
style={{ pointerEvents: "none" }}
|
||||
open={authenticationModalOpen}
|
||||
onClose={() => {setSelectedMeta(undefined)}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
color: theme.palette.textColor,
|
||||
minWidth: 1100,
|
||||
minHeight: 800,
|
||||
maxHeight: 800,
|
||||
padding: 15,
|
||||
overflow: "hidden",
|
||||
zIndex: 10012,
|
||||
border: theme.palette.defaultBorder,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
right: 75,
|
||||
height: 50,
|
||||
width: 50,
|
||||
}}
|
||||
>
|
||||
{ selectedAppData.reference_info === undefined ||
|
||||
selectedAppData.reference_info === null ||
|
||||
selectedAppData.reference_info.github_url === undefined ||
|
||||
selectedAppData.reference_info.github_url === null ||
|
||||
selectedAppData.reference_info.github_url.length === 0 ? (
|
||||
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={"https://github.com/shuffle/python-apps"}
|
||||
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||
>
|
||||
<img
|
||||
alt={`Documentation image for ${selectedAppData.name}`}
|
||||
src={selectedAppData.large_image}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
border: "2px solid rgba(255,255,255,0.6)",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
maxHeight: 30,
|
||||
maxWidth: 30,
|
||||
overflow: "hidden",
|
||||
fontSize: 8,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={selectedAppData.reference_info.github_url}
|
||||
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||
>
|
||||
<img
|
||||
alt={`Documentation image for ${selectedAppData.name}`}
|
||||
src={selectedAppData.large_image}
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
border: "2px solid rgba(255,255,255,0.6)",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
maxWidth: 30,
|
||||
maxHeight: 30,
|
||||
overflow: "hidden",
|
||||
fontSize: 8,
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={`Move window`}
|
||||
placement="left"
|
||||
>
|
||||
<IconButton
|
||||
id="draggable-dialog-title"
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 14,
|
||||
right: 50,
|
||||
color: "grey",
|
||||
|
||||
cursor: "move",
|
||||
}}
|
||||
>
|
||||
<DragIndicatorIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 14,
|
||||
right: 18,
|
||||
color: "grey",
|
||||
}}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div
|
||||
style={{
|
||||
flex: 2,
|
||||
padding: 0,
|
||||
minHeight: isMobile ? "90%" : 800,
|
||||
maxHeight: isMobile ? "90%" : 800,
|
||||
overflowY: "auto",
|
||||
overflowX: isMobile ? "auto" : "hidden",
|
||||
}}
|
||||
>
|
||||
{authenticationType?.type === "oauth2" || authenticationType?.type === "oauth2-app" ?
|
||||
<AuthenticationOauth2
|
||||
selectedApp={selectedAppData}
|
||||
selectedAction={{
|
||||
"app_name": selectedAppData.name,
|
||||
"app_id": selectedAppData.id,
|
||||
"app_version": selectedAppData.version,
|
||||
"large_image": selectedAppData.large_image,
|
||||
}}
|
||||
authenticationType={authenticationType}
|
||||
getAppAuthentication={getAppAuthentication}
|
||||
appAuthentication={appAuthentication}
|
||||
setAuthenticationModalOpen={setAuthenticationModalOpen}
|
||||
isCloud={isCloud}
|
||||
org_id={userdata?.active_org?.id}
|
||||
setSelectedAction={setSelectedAction}
|
||||
/>
|
||||
:
|
||||
<AuthenticationData app={selectedAppData} />
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 3,
|
||||
borderLeft: `1px solid ${theme.palette.inputColor}`,
|
||||
padding: "70px 30px 30px 30px",
|
||||
maxHeight: 630,
|
||||
minHeight: 630,
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
>
|
||||
{selectedAppData?.documentation === undefined ||
|
||||
selectedAppData?.documentation === null ||
|
||||
selectedAppData?.documentation.length === 0 ? (
|
||||
<span
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
<div style={{textAlign: "left", }}>
|
||||
<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%",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
{selectedAppData?.description}
|
||||
</Markdown>
|
||||
</div>
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 25,
|
||||
marginBottom: 25,
|
||||
backgroundColor: "rgba(255,255,255,0.6)",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" style={{marginBottom: 25, }}>
|
||||
There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
toast.success("Opening remote Github documentation link. Thanks for contributing!")
|
||||
|
||||
setTimeout(() => {
|
||||
window.open(`https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${selectedAppData.name.toLowerCase()}.md`, "_blank")
|
||||
}, 2500)
|
||||
}}
|
||||
>
|
||||
<EditIcon /> Create Docs
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Typography variant="body1" style={{ marginTop: 25 }}>
|
||||
Want to help the making of, or improve this app?{" "}
|
||||
<br />
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://discord.gg/B2CBzUm"
|
||||
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||
>
|
||||
Join the community on Discord!
|
||||
</a>
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={{ marginTop: 50 }}>
|
||||
Want to help change this app directly?
|
||||
</Typography>
|
||||
{selectedAppData.reference_info === undefined ||
|
||||
selectedAppData.reference_info === null ||
|
||||
selectedAppData.reference_info.github_url === undefined ||
|
||||
selectedAppData.reference_info.github_url === null ||
|
||||
selectedAppData.reference_info.github_url.length === 0 ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 25 }}>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={"https://github.com/shuffle/python-apps"}
|
||||
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||
>
|
||||
Check it out on Github!
|
||||
</a>
|
||||
</Typography>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 25 }}>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={selectedAppData.reference_info.github_url}
|
||||
style={{ textDecoration: "none", color: "#f86a3e" }}
|
||||
>
|
||||
Check it out on Github!
|
||||
</a>
|
||||
</Typography>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<div>
|
||||
{selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ?
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 3, display: "flex", vAlign: "center", position: "sticky", top: 50, }}>
|
||||
{isMobile ? null : (
|
||||
<Typography style={{ display: "inline", marginTop: 6 }}>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={selectedMeta.link}
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
<Button style={{ color: theme.palette.textColor, }} variant="outlined" color="secondary">
|
||||
<EditIcon /> Edit
|
||||
</Button>
|
||||
</a>
|
||||
</Typography>
|
||||
)}
|
||||
{isMobile ? null : (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: 1,
|
||||
backgroundColor: theme.palette.textColor,
|
||||
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 ||
|
||||
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}
|
||||
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>
|
||||
: null}
|
||||
|
||||
<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%",
|
||||
}}
|
||||
>
|
||||
{selectedAppData.documentation}
|
||||
</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
) : null;
|
||||
|
||||
return authenticationModal
|
||||
}
|
||||
|
||||
export default AuthenticationModal;
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Search as SearchIcon } from '@mui/icons-material';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback.js';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback.jsx';
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useContext, memo, useMemo } from 'react'
|
||||
import {getTheme} from '../theme.jsx';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -14,19 +15,104 @@ import {
|
||||
|
||||
GridlineSeries,
|
||||
Gridline,
|
||||
TooltipArea,
|
||||
ChartTooltip,
|
||||
TooltipTemplate,
|
||||
TooltipArea,
|
||||
} from 'reaviz';
|
||||
|
||||
export const LoadStats = (globalUrl, cachekey) => {
|
||||
if (globalUrl === undefined) {
|
||||
console.log("Error: Global URL is undefined")
|
||||
return
|
||||
}
|
||||
|
||||
if (cachekey === undefined) {
|
||||
console.log("Error: Cachekey is undefined")
|
||||
return
|
||||
}
|
||||
|
||||
var basedata = {
|
||||
"key": cachekey,
|
||||
"total": 0,
|
||||
"available_keys": [],
|
||||
"labels": [],
|
||||
"datasets": [
|
||||
{
|
||||
"label": "",
|
||||
"data": [],
|
||||
"backgroundColor": [],
|
||||
"barThickness": 15,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
|
||||
//cachekey = cachekey.replace(" ", "_", -1)
|
||||
const url = `${globalUrl}/api/v1/stats/${cachekey}`
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((resp) => {
|
||||
return resp.json()
|
||||
}).then((respJson) => {
|
||||
const selectedIndex = 0
|
||||
|
||||
//console.log("Stats response: ", respJson)
|
||||
|
||||
if (respJson.success === true) {
|
||||
for (let entryKey in respJson.entries) {
|
||||
const entry = respJson.entries[entryKey]
|
||||
basedata.labels.push(entry.date)
|
||||
|
||||
basedata.datasets[0].data.push(entry.value)
|
||||
basedata.datasets[0].backgroundColor.push(entry.value > 0 ? "rgba(255,255,255,0.4)" : "red")
|
||||
}
|
||||
|
||||
basedata.available_keys = respJson.available_keys
|
||||
basedata.total = respJson.total
|
||||
|
||||
return basedata
|
||||
} else {
|
||||
console.log("Failed to get stats")
|
||||
return basedata
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
toast("Failed to get stats")
|
||||
return basedata
|
||||
})
|
||||
}
|
||||
|
||||
const LineChartWrapper = (props) => {
|
||||
const {keys, inputname, height, width, border} = props
|
||||
const {keys, inputname, height, width, border, color} = props
|
||||
|
||||
const [hovered, setHovered] = useState("");
|
||||
const {themeMode} = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
|
||||
var inputdata = keys.data === undefined ? keys : keys.data
|
||||
// Correct format:
|
||||
/* keys={[
|
||||
{
|
||||
key: "2025-07-23T00:00:09.409718Z",
|
||||
data: 24,
|
||||
},
|
||||
{
|
||||
key: "2025-07-24T00:00:09.409718Z",
|
||||
data: 50,
|
||||
},
|
||||
{
|
||||
key: "2025-07-25T00:00:09.409718Z",
|
||||
data: 75,
|
||||
},
|
||||
{
|
||||
key: "2025-07-26T00:00:09.409718Z",
|
||||
data: 42,
|
||||
},
|
||||
]}
|
||||
*/
|
||||
|
||||
var inputdata = keys?.data === undefined ? keys : keys.data
|
||||
|
||||
var newname = inputname === undefined || inputname === null ? "" : inputname.trim().replaceAll("_", " ")
|
||||
newname = newname.charAt(0).toUpperCase() + newname.slice(1)
|
||||
@@ -35,7 +121,6 @@ const LineChartWrapper = (props) => {
|
||||
|
||||
var tmpdata = inputdata?.datasets[0]
|
||||
if (tmpdata?.data !== undefined && tmpdata?.data !== null && tmpdata?.data.length > 0 && inputdata?.labels?.length === tmpdata?.data?.length) {
|
||||
console.log("Fix it!")
|
||||
var newarray = []
|
||||
for (var key in tmpdata.data) {
|
||||
var entry = {
|
||||
@@ -48,26 +133,25 @@ const LineChartWrapper = (props) => {
|
||||
|
||||
inputdata = newarray
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (inputdata === undefined || inputdata === null) {
|
||||
return (
|
||||
return null /*(
|
||||
<Typography>
|
||||
Invalid linegraph data format
|
||||
</Typography>
|
||||
)
|
||||
)*/
|
||||
}
|
||||
|
||||
var defaultStyle = {
|
||||
color: "white",
|
||||
padding: 30,
|
||||
padding: "5px 5px 10px 5px",
|
||||
marginTop: 15,
|
||||
overflow: "hidden",
|
||||
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
//backgroundColor: theme.palette.platformColor,
|
||||
}
|
||||
|
||||
if (border === false) {
|
||||
@@ -76,11 +160,64 @@ const LineChartWrapper = (props) => {
|
||||
defaultStyle.backgroundColor = "transparent"
|
||||
}
|
||||
|
||||
// Check if it's a list or not
|
||||
if (!Array.isArray(inputdata) || inputdata.length === 0 || (inputdata.length > 0 && (inputdata[0].key === undefined && inputdata[0].data === undefined))) {
|
||||
console.log("Invalid graph data format: ", inputdata)
|
||||
console.log("Expected format: [{key: 'label1', data: 10}, {key: 'label2', data: 20}]")
|
||||
//inputdata = inputdata?.datasets[0]?.data
|
||||
return null
|
||||
}
|
||||
|
||||
//console.log("FORMAT: ", inputdata)
|
||||
|
||||
const tooltip = <TooltipArea
|
||||
tooltip={
|
||||
<ChartTooltip
|
||||
placement={"top"}
|
||||
followCursor={true}
|
||||
content={(data) => (
|
||||
<div style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
border: theme.palette.defaultBorder,
|
||||
color: theme.palette.text.primary,
|
||||
padding: 6,
|
||||
maxWidth: 240,
|
||||
}}>
|
||||
<Typography variant="body2" style={{fontWeight: 600}}>{data?.x ?? ''}</Typography>
|
||||
<Typography variant="body2">{data?.y ?? ''}</Typography>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>;
|
||||
|
||||
const selectedColor = color === undefined || color === null || color === "" ? "" : color
|
||||
const barseries = color === undefined || color === null || color === "" ?
|
||||
<BarSeries
|
||||
bar={
|
||||
<Bar
|
||||
/>
|
||||
}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
:
|
||||
<BarSeries
|
||||
colorScheme={[selectedColor]}
|
||||
bar={
|
||||
<Bar
|
||||
/>
|
||||
}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
|
||||
return (
|
||||
<div style={defaultStyle}>
|
||||
<Typography variant="h6" style={{marginBotton: 30, }}>
|
||||
{newname}
|
||||
</Typography>
|
||||
{newname !== "" &&
|
||||
<Typography variant="h6" style={{paddingBottom: 25, }}>
|
||||
{newname}
|
||||
</Typography>
|
||||
}
|
||||
|
||||
<BarChart
|
||||
style={{marginTop: 100, }}
|
||||
@@ -89,11 +226,8 @@ const LineChartWrapper = (props) => {
|
||||
data={inputdata}
|
||||
|
||||
series={
|
||||
<BarSeries
|
||||
bar={
|
||||
<Bar />
|
||||
}
|
||||
/>
|
||||
barseries
|
||||
|
||||
}
|
||||
gridlines={
|
||||
<GridlineSeries line={<Gridline direction="all" />} />
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useRef, useEffect, useCallback } from "react";
|
||||
|
||||
export const useDebouncedCallback = (callback, delay = 300) => {
|
||||
const timeoutRef = useRef(null);
|
||||
const savedCallback = useRef(callback);
|
||||
|
||||
useEffect(() => {
|
||||
savedCallback.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useEffect(() => () => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
}, []);
|
||||
|
||||
return useCallback((...args) => {
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
savedCallback.current(...args);
|
||||
}, delay);
|
||||
}, [delay]);
|
||||
};
|
||||
|
||||
export default useDebouncedCallback;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import useDebouncedCallback from "../utils/useDebouncedCallback.js";
|
||||
import useDebouncedCallback from "../utils/useDebouncedCallback.jsx";
|
||||
import {
|
||||
Zoom,
|
||||
Fade,
|
||||
|
||||
Reference in New Issue
Block a user