Build with missing files

This commit is contained in:
Frikky
2024-11-30 15:44:19 +01:00
parent d81f9d79b8
commit a9b4f5bb6f
6 changed files with 6065 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+546
View File
@@ -0,0 +1,546 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Typography, IconButton, CircularProgress, Tooltip } from '@mui/material';
import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as PauseIcon } from '@mui/icons-material';
import theme from '../theme.jsx';
import ReactJson from "react-json-view-ssr";
import { toast } from 'react-toastify';
import { validateJson } from "../views/Workflows.jsx";
// import HandleJsonCopy from "./ShuffleCodeEditor1";
const STATUS_CONFIG = {
EXECUTING: {
color: '#64B5F6',
icon: () => <CircularProgress size={16} thickness={4} sx={{ color: '#64B5F6' }} />,
label: 'Executing'
},
SUCCESS: {
color: '#4CAF50',
icon: () => <CheckCircle sx={{ color: '#4CAF50', fontSize: 16 }} />,
label: 'Success'
},
FINISHED: {
color: '#4CAF50',
icon: () => <CheckCircle sx={{ color: '#4CAF50', fontSize: 16 }} />,
label: 'Finished'
},
ABORTED: {
color: '#F44336',
icon: () => <Error sx={{ color: '#F44336', fontSize: 16 }} />,
label: 'Aborted'
}
};
let to_be_copied = ""
const handleReactJsonClipboard = (copy) => {
toast("Copied JSON path to clipboard, NOT Path")
};
const HandleJsonCopy = (base, copy, base_node_name) => {
if (typeof copy.name === "string") {
copy.name = copy.name.replaceAll(" ", "_");
}
//lol
if (typeof base === 'object' || typeof base === 'dict') {
base = JSON.stringify(base)
}
if (base_node_name === "execution_argument" || base_node_name === "Execution Argument") {
base_node_name = "exec"
}
console.log("COPY: ", base_node_name, copy);
//var newitem = JSON.parse(base);
var newitem = validateJson(base).result
to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_");
for (let copykey in copy.namespace) {
if (copy.namespace[copykey].includes("Results for")) {
continue;
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.namespace[copykey]];
if (!isNaN(copy.namespace[copykey])) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.namespace[copykey];
}
}
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.name];
if (!isNaN(copy.name)) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.name;
}
}
to_be_copied.replaceAll(" ", "_");
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
console.log("NAVIGATOR: ", navigator);
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(to_be_copied);
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices *
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Copied JSON path to clipboard.")
console.log("COPYING!");
} else {
console.log("Couldn't find element ", elementName);
}
}
const ExecuteWorkflow = async (executionData, globalUrl) => {
try {
const workflowData = executionData.workflow;
// Execute workflow with original parameters
await fetch(`${globalUrl}/api/v1/workflows/${workflowData.id}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include',
body: workflowData
}).then(response => {
window.location.href = `/workflows/${workflowData.id}/code?execution_id=` + response.json().execution_id;
});
} catch (error) {
console.error('Error re-executing workflow:', error);
}
};
const ExecutionsList = ({ executions, onSelectExecution, activeExecutionId }) => {
return (
<Box>
{executions.map((execution) => {
const status = STATUS_CONFIG[execution.status] || STATUS_CONFIG.ABORTED;
return (
<Box
key={execution.execution_id}
onClick={() => onSelectExecution(execution)}
sx={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
py: 1,
px: 2,
borderBottom: '1px solid #2A2A2A',
backgroundColor: activeExecutionId === execution.execution_id ?
'rgba(255,255,255,0.05)' : 'transparent',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.05)'
}
}}
>
{status.icon()}
<Box sx={{ ml: 2, flex: 1, overflow: 'hidden' }}>
<Typography variant="body2" sx={{
color: '#E0E0E0',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{new Date(execution.started_at * 1000).toLocaleString()}
</Typography>
<Typography variant="caption" sx={{
color: status.color
}}>
{status.label}
</Typography>
</Box>
</Box>
);
})}
</Box>
);
};
const ExecutionDetail = ({ execution: initialExecution, onBack, globalUrl, onExecutionUpdate, selectedAction, executeWorkflow }) => {
const [execution, setExecution] = useState(initialExecution);
const [status, setStatus] = useState(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING);
const [validResult, setValidResult] = useState("{}")
const abortExecution = async () => {
try {
await fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/executions/${execution.execution_id}/abort`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => {
if (response.ok) {
const updatedExecution = {
...execution,
status: "ABORTED",
};
setExecution(updatedExecution);
onExecutionUpdate(updatedExecution);
}
});
} catch (error) {
console.log("Abort error:", error);
}
};
useEffect(() => {
setStatus(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING);
}, [execution]);
const pollExecutionStatus = useCallback(async () => {
try {
const response = await fetch(`${globalUrl}/api/v1/streams/results`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
execution_id: execution.execution_id,
authorization: execution.authorization,
}),
});
if (response.ok) {
const data = await response.json();
const currentStatus = data.results?.[0]?.status || 'EXECUTING';
const updatedExecution = {
...execution,
...data,
status: currentStatus
};
setExecution(updatedExecution);
onExecutionUpdate(updatedExecution);
}
} catch (error) {
console.error('Polling error:', error);
}
}, [execution, globalUrl, onExecutionUpdate]);
useEffect(() => {
let pollTimeout;
if (execution.status === 'EXECUTING') {
pollTimeout = setTimeout(() => pollExecutionStatus(), 3000);
}
if (execution?.results?.length === 1) {
setValidResult(JSON.parse(execution?.results[0]?.result || "{}"))
}
return () => clearTimeout(pollTimeout);
}, [execution.status, pollExecutionStatus]);
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{
display: 'flex',
alignItems: 'center',
p: 2,
borderBottom: '1px solid #454545'
}}>
<IconButton onClick={onBack} size="small" sx={{ mr: 2 }}>
<ArrowBack fontSize="small" />
</IconButton>
<Typography variant="subtitle2" sx={{ color: '#E0E0E0', mr: 2 }}>
Execution Details
</Typography>
{status.icon()}
<Typography variant="body2" sx={{ ml: 1, color: status.color }}>
{status.label}
</Typography>
<Box sx={{ ml: 'auto' }}>
{execution.status === "EXECUTING" && (
<Tooltip title="Abort workflow" placement="top">
<IconButton
size="small"
onClick={abortExecution}
sx={{ color: theme.palette.error.main }}
>
<PauseIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
<Tooltip title="Rerun workflow (uses same startnode as the original)" placement="top">
<IconButton
size="small"
onClick={() => {
ExecuteWorkflow(
execution,
globalUrl
);
}}
sx={{ color: theme.palette.primary.main }}
>
<CachedIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
<Box sx={{ mb: 3 }}>
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
Started at
</Typography>
<Typography variant="body2">
{new Date(execution.started_at * 1000).toLocaleString()}
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
Execution ID
</Typography>
<Typography variant="body2" sx={{
backgroundColor: '#2A2A2A',
p: 1,
borderRadius: 1,
fontFamily: 'monospace'
}}>
{execution.execution_id}
</Typography>
</Box>
<Box>
<Box>
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
Result
</Typography>
<Box sx={{
backgroundColor: '#2A2A2A',
borderRadius: 1,
overflow: 'auto',
maxHeight: '200px'
}}>
<pre style={{
margin: 0,
padding: '1rem',
fontSize: '12px',
color: status.color,
fontFamily: "'JetBrains Mono', monospace"
}}>
{execution?.status === 'EXECUTING' ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 2 }}>
<CircularProgress size={24} />
<Typography sx={{ ml: 2, color: '#888' }}>Executing...</Typography>
</Box>
) : (
execution?.results?.length === 1 ?
<ReactJson
src={validResult}
theme={theme.palette.jsonTheme}
style={{
borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`,
padding: 10,
maxHeight: 450,
minheight: 450,
overflow: "auto",
minWidth: 450,
maxWidth: "100%",
zIndex: 1200
}}
enableClipboard={(copy) => {
handleReactJsonClipboard(copy);
}}
collapsed={false}
displayDataTypes={false}
onSelect={(select) => {
var basename = "exec"
if (selectedAction !== undefined && selectedAction !== null && Object.keys(selectedAction).length !== 0) {
basename = selectedAction.label.toLowerCase().replaceAll(" ", "_")
}
HandleJsonCopy(validResult, select, basename)
}}
name={"JSON autocompletion"}
/> :
<ReactJson
src={{}}
theme={theme.palette.jsonTheme}
style={{
borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`,
padding: 10,
maxHeight: 450,
minheight: 450,
overflow: "auto",
minWidth: 450,
maxWidth: "100%",
zIndex: 1200
}}
collapsed={false}
enableClipboard={(copy) => { }}
displayDataTypes={false}
name={"JSON autocompletion"}
/>
)}
</pre>
</Box>
</Box>
</Box>
</Box>
</Box>
);
};
const ExecutionPanel = ({
workflow,
globalUrl,
onClose,
currentExecution,
mainAction
}) => {
const [executions, setExecutions] = useState([]);
const [selectedExecution, setSelectedExecution] = useState(null);
const [loading, setLoading] = useState(true);
const handleExecutionUpdate = useCallback((updatedExecution) => {
setExecutions(prevExecutions => {
const updatedExecutions = [...prevExecutions];
const index = updatedExecutions.findIndex(
e => e.execution_id === updatedExecution.execution_id
);
if (index !== -1) {
updatedExecutions[index] = updatedExecution;
}
return updatedExecutions;
});
}, []);
const fetchExecutions = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(`${globalUrl}/api/v2/workflows/${workflow.id}/executions`, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setExecutions(data.executions);
const urlParams = new URLSearchParams(window.location.search);
const executionId = urlParams.get('execution_id');
if (executionId) {
const execution = data.executions.find(e => e.execution_id === executionId);
if (execution) {
setSelectedExecution(execution);
}
}
}
} catch (error) {
console.error('Failed to fetch executions:', error);
} finally {
setLoading(false);
}
}, [workflow.id, globalUrl]);
useEffect(() => {
fetchExecutions();
}, [fetchExecutions]);
useEffect(() => {
if (currentExecution?.execution_id) {
setExecutions(prev => {
const existingIndex = prev.findIndex(e => e.execution_id === currentExecution.execution_id);
if (existingIndex === -1) {
return [currentExecution, ...prev];
}
const updated = [...prev];
updated[existingIndex] = currentExecution;
return updated;
});
setSelectedExecution(currentExecution);
}
}, [currentExecution]);
return (
<Box
sx={{
position: 'relative',
height: "fit-content",
backgroundColor: '#1E1E1E',
borderTop: '1px solid #454545',
overflow: 'hidden',
color: '#CCCCCC',
fontFamily: "'JetBrains Mono', monospace",
display: 'flex',
flexDirection: 'column',
}}
>
{loading && !executions.length ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<CircularProgress size={24} />
</Box>
) : selectedExecution ? (
<ExecutionDetail
execution={selectedExecution}
onBack={() => {
setSelectedExecution(null);
const url = new URL(window.location);
url.searchParams.delete('execution_id');
window.history.pushState({}, '', url);
fetchExecutions();
}}
globalUrl={globalUrl}
selecteAction={mainAction}
onExecutionUpdate={handleExecutionUpdate}
/>
) : (
<>
<Box sx={{
p: 2,
borderBottom: '1px solid #454545',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Typography variant="subtitle2" sx={{ color: '#E0E0E0' }}>
Execution History
</Typography>
<IconButton size="small" onClick={onClose} sx={{ color: '#888' }}>
<Close fontSize="small" />
</IconButton>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<ExecutionsList
executions={executions}
onSelectExecution={(execution) => {
setSelectedExecution(execution);
const url = new URL(window.location);
url.searchParams.set('execution_id', execution.execution_id);
window.history.pushState({}, '', url);
}}
activeExecutionId={currentExecution?.execution_id}
/>
</Box>
</>
)}
</Box>
);
};
export default ExecutionPanel;
+197
View File
@@ -0,0 +1,197 @@
import React, { useEffect, useState } from "react";
import { Paper, Typography, Box, CircularProgress, TextField, Button } from "@mui/material";
import { toast } from "react-toastify";
const MFASetup = ({ isLoaded, globalUrl, setCookie }) => {
const [image2FA, setImage2FA] = useState("");
const [secret2FA, setSecret2FA] = useState("");
const [mfaCode, setMfaCode] = useState("");
const [code, setCode] = useState(null);
useEffect(() => {
handleGet2FACode();
}, []);
useEffect(() => {
if (isLoaded) {
const code = window.location.pathname.split("/")[2];
setMfaCode(code);
}
}, [isLoaded]);
const handleGet2FACode = () => {
if (mfaCode === "") {
return;
}
fetch(`${globalUrl}/api/v1/users/${mfaCode}/get2fa`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status === 404) {
toast("User not found. Redirecting to login page in 3 seconds...");
setTimeout(() => {
window.location.pathname = "/login";
return;
}, 3000);
}
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
setImage2FA(responseJson.reason);
setSecret2FA(responseJson.extra);
}
})
.catch((error) => {
toast(error.toString());
});
};
useEffect(() => {
if (mfaCode) {
handleGet2FACode();
}
}, [mfaCode]);
const handleVerify2FA = (mfaCode, code, changeMFAActive) => {
const data = {
code: code,
changeMFAActive: changeMFAActive,
};
toast("Verifying 2fa code. Please wait...");
fetch(`${globalUrl}/api/v1/users/${mfaCode}/set2fa`, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status === 500) {
toast("Wrong code sent. Please try again.");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
toast.success("Successfully setup 2fa. Redirecting in 3 seconds...");
for (var key in responseJson["cookies"]) {
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" });
}
const tmpView = new URLSearchParams(window.location.search).get("view");
if (tmpView !== undefined && tmpView !== null) {
var newUrl = `/${tmpView}`;
if (tmpView.startsWith("/")) {
newUrl = `${tmpView}`;
}
window.location.pathname = newUrl;
return;
}
if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) {
const welcome = responseJson.tutorials.find((element) => element.name === "welcome");
if (welcome === undefined || welcome === null) {
setTimeout(() => {
window.location.pathname = "/welcome";
}, 3000);
}
}
setTimeout(() => {
window.location.pathname = "/workflows";
}, 3000);
} else {
toast("Failed to setup 2fa. Please try again.");
}
})
.catch((error) => {
console.error("Error:", error);
});
};
return (
<div style={{ margin: "50px auto", width: "500px" }}>
<Paper elevation={3} style={{ padding: "30px", backgroundColor: "#212121" }}>
<Typography variant="h5" style={{ color: "white", marginBottom: 10, textAlign: "center" }}>
Multi-Factor Authentication Setup
</Typography>
<div style={{ marginTop: 15 }}>
<QRCodeSection secret2FA={secret2FA} image2FA={image2FA} />
</div>
<Typography variant="body1" style={{ color: "white", marginTop: 15 }}>
Enter the code from your authenticator app below.
</Typography>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="code"
label="Code"
name="code"
autoComplete="code"
autoFocus
onChange={(e) => setCode(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter" && code !== null && code !== "" && code.length === 6) {
handleVerify2FA(mfaCode, code, true);
}
}}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
onClick={() => handleVerify2FA(mfaCode, code, true)}
style={{
backgroundColor: code === null || code === "" || code.length !== 6 ? "gray" : "#f86743",
marginTop: 10,
color: "#fff",
cursor: code === null || code === "" || code.length !== 6 ? "" : "pointer",
}}
disabled={code === null || code === "" || code.length !== 6}
>
Submit
</Button>
</Paper>
</div>
);
};
const QRCodeSection = ({ secret2FA, image2FA }) => {
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", width: "100%" }}>
{secret2FA && image2FA ? (
<div style={{ textAlign: "center" }}>
<Typography variant="body2" style={{ color: "white", textAlign: "justify", marginBottom: 10, fontSize: 16 }}>
Scan the image below with the two-factor authentication app on your phone. If you cant use a QR code, use the code {secret2FA} instead.
</Typography>
<img alt="2FA QR code" src={image2FA} style={{ maxHeight: 200, maxWidth: 200, }} />
</div>
) : (
<CircularProgress style={{ margin: "15px auto" }} />
)}
</div>
);
};
export default MFASetup;