Build with missing files
This commit is contained in:
@@ -45,6 +45,11 @@ import AlertTemplate from "./components/AlertTemplate";
|
|||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import RuntimeDebugger from "./components/RuntimeDebugger.jsx"
|
import RuntimeDebugger from "./components/RuntimeDebugger.jsx"
|
||||||
|
|
||||||
|
import MFASetUp from './components/MFASetUP.jsx';
|
||||||
|
import ApiExplorerWrapper from './views/ApiExplorerWrapper.jsx';
|
||||||
|
import LeftSideBar from './components/LeftSideBar.jsx';
|
||||||
|
import CodeWorkflow from './views/CodeWorkflow.jsx';
|
||||||
|
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
import { ToastContainer, toast } from 'react-toastify';
|
||||||
import 'react-toastify/dist/ReactToastify.css';
|
import 'react-toastify/dist/ReactToastify.css';
|
||||||
|
|
||||||
@@ -204,6 +209,11 @@ const App = (message, props) => {
|
|||||||
{curpath.includes("/workflows") && curpath.includes("/run") ?
|
{curpath.includes("/workflows") && curpath.includes("/run") ?
|
||||||
<div style={{ height: 60, }} />
|
<div style={{ height: 60, }} />
|
||||||
:
|
:
|
||||||
|
isLoggedIn ?
|
||||||
|
<div style={{ position: 'fixed', top: 32, left: 10, zIndex: 100000 }}>
|
||||||
|
<LeftSideBar userdata={userdata} globalUrl={globalUrl} serverside={false} notifications={notifications} />
|
||||||
|
</div>
|
||||||
|
:
|
||||||
<Header
|
<Header
|
||||||
billingInfo={{}}
|
billingInfo={{}}
|
||||||
|
|
||||||
@@ -454,6 +464,7 @@ const App = (message, props) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route exact path="/apis/:appid" element={<ApiExplorerWrapper serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} isMobile={false} selectedApp={undefined} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} checkLogin={checkLogin} {...props} />} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/detections/sigma"
|
path="/detections/sigma"
|
||||||
@@ -525,8 +536,14 @@ const App = (message, props) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route exact path="/workflows/:key/code" element={<CodeWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||||
<Route exact path="/workflows/:key/run" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
|
<Route exact path="/workflows/:key/run" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
|
||||||
<Route exact path="/workflows/:key/execute" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
|
<Route exact path="/workflows/:key/execute" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
|
||||||
|
|
||||||
|
<Route exact path="/forms" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||||
|
<Route exact path="/forms/:key/run" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||||
|
<Route exact path="/forms/:key" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/docs/:key"
|
path="/docs/:key"
|
||||||
@@ -582,6 +599,7 @@ const App = (message, props) => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route exact path="/login/:key/mfa-setup" element={<MFASetUp setCookie={setCookie} serverside={false} mainColor={theme.palette.backgroundColor} userdata={userdata} stripeKey={undefined} globalUrl={globalUrl} inputColor={theme.palette.inputColor} isLoaded={isLoaded} {...props} />} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path="/login_sso"
|
path="/login_sso"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
@@ -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 can’t 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;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,466 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { toast } from 'react-toastify';
|
||||||
|
import { Button, Box, Typography, Paper, Toolbar, Divider, CircularProgress } from '@mui/material';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PlayArrow as PlayArrowIcon,
|
||||||
|
Save as SaveIcon,
|
||||||
|
History as HistoryIcon
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
import ExecutionPanel from '../components/ExecutionPanel.jsx';
|
||||||
|
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||||
|
|
||||||
|
const CodeWorkflow = (defaultprops) => {
|
||||||
|
const { serverside, userdata, globalUrl, isLoaded, isLoggedIn, surfaceColor, inputColor, ...props } = defaultprops;
|
||||||
|
|
||||||
|
const [workflow, setWorkflow] = useState({});
|
||||||
|
|
||||||
|
const [showExecutions, setShowExecutions] = useState(false);
|
||||||
|
// In CodeWorkflow, add this state
|
||||||
|
const [panelHeight, setPanelHeight] = useState(400);
|
||||||
|
const [executions, setExecutions] = useState([]);
|
||||||
|
const [currentExecution, setCurrentExecution] = useState(null);
|
||||||
|
const [mainAction, setMainAction] = useState(null);
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const [apiKey, setApiKey] = useState("");
|
||||||
|
|
||||||
|
const [editorData, setEditorData] = React.useState({
|
||||||
|
"name": "",
|
||||||
|
"value": "",
|
||||||
|
"field_number": -1,
|
||||||
|
"actionlist": [],
|
||||||
|
"field_id": "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const getSettings = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${globalUrl}/api/v1/getsettings`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for getsettings :O!");
|
||||||
|
if (response.status >= 500) {
|
||||||
|
toast("Something went wrong while loading the settings. Please reload.")
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await response.json();
|
||||||
|
setApiKey(responseJson.apikey);
|
||||||
|
console.log("API Key: ", responseJson.apikey);
|
||||||
|
|
||||||
|
return responseJson.apikey;
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Get settings error: ", error.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Calculate editor height based on execution panel visibility
|
||||||
|
const getEditorHeight = () => {
|
||||||
|
return `calc(100vh - ${showExecutions ? `${panelHeight + 40}px` : '40px'})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = window.location.pathname;
|
||||||
|
const workflowId = url.split("/")[2];
|
||||||
|
|
||||||
|
const getWorkflow = async (workflow_id, sourcenode) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for workflows :O!");
|
||||||
|
if (response.status >= 500) {
|
||||||
|
toast("Something went wrong while loading the workflow. Please reload.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseJson = await response.json();
|
||||||
|
setWorkflow(responseJson);
|
||||||
|
|
||||||
|
for (let i = 0; i < responseJson.actions.length; i++) {
|
||||||
|
if (responseJson.actions[i].app_id === "3e320a20966d33c9b7e6790b2705f0bf") {
|
||||||
|
console.log("Setting code to: ", responseJson.actions[i].parameters[0].value);
|
||||||
|
setCode(responseJson.actions[i].parameters[0].value);
|
||||||
|
setMainAction(responseJson.actions[i]);
|
||||||
|
|
||||||
|
if (responseJson.actions[i].parameters[0].value.length === 0) {
|
||||||
|
// fetch API key of the user
|
||||||
|
const result = await getSettings();
|
||||||
|
|
||||||
|
console.log("accessible result: ", result);
|
||||||
|
|
||||||
|
// await setCode(`
|
||||||
|
// from shufflepy import Shuffle
|
||||||
|
|
||||||
|
// shuffle = Shuffle(
|
||||||
|
// "${result}",
|
||||||
|
// url='https://shuffler.io',
|
||||||
|
// )
|
||||||
|
// `
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Get workflows error: ", error.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getWorkflow(workflowId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// In CodeWorkflow component, add this effect:
|
||||||
|
useEffect(() => {
|
||||||
|
if (workflow.id) {
|
||||||
|
// Check URL for execution_id parameter
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const executionId = urlParams.get('execution_id');
|
||||||
|
if (executionId) {
|
||||||
|
setShowExecutions(true); // Show the panel if execution_id is present
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [workflow]);
|
||||||
|
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const [testResult, setTestResult] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const saveLatestWorkflow = () => {
|
||||||
|
let newWorkflow = workflow;
|
||||||
|
|
||||||
|
console.log("Workflow actions: ", newWorkflow.actions);
|
||||||
|
|
||||||
|
// find the latest "Shuffle tools fork" node
|
||||||
|
for (let i = 0; i < newWorkflow.actions.length; i++) {
|
||||||
|
console.log("Actios: ", newWorkflow.actions[i]);
|
||||||
|
if (newWorkflow.actions[i].app_id === "3e320a20966d33c9b7e6790b2705f0bf") {
|
||||||
|
// update the code
|
||||||
|
console.log("Updating code: ", code);
|
||||||
|
newWorkflow.actions[i].parameters[0].value = code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`${globalUrl}/api/v1/workflows/${workflow.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify(newWorkflow),
|
||||||
|
}).then((response) => {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log("Status not 200 for workflows :O!");
|
||||||
|
toast("Something went wrong while saving the workflow. Please try again.", { type: "error" });
|
||||||
|
} else if (response.status === 200) {
|
||||||
|
toast("Workflow saved successfully!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getParents = async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"label": "Execution Argument",
|
||||||
|
"type": "INTERNAL"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRunCode = async () => {
|
||||||
|
saveLatestWorkflow();
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
let start_node = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < workflow.actions.length; i++) {
|
||||||
|
if (workflow.actions[i].isStartNode) {
|
||||||
|
start_node = workflow.actions[i].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${globalUrl}/api/v1/workflows/${workflow.id}/execute`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
credentials: "include",
|
||||||
|
body: JSON.stringify({
|
||||||
|
"start": start_node,
|
||||||
|
"execution_arguments": "",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const responseJson = await response.json();
|
||||||
|
const newExecution = {
|
||||||
|
execution_id: responseJson.execution_id,
|
||||||
|
authorization: responseJson.authorization,
|
||||||
|
status: 'EXECUTING',
|
||||||
|
started_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
setShowExecutions(true);
|
||||||
|
setCurrentExecution(newExecution);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error: ", error);
|
||||||
|
setError(error.toString());
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleSaveCode = async () => {
|
||||||
|
saveLatestWorkflow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorDidMount = (editor, monaco) => {
|
||||||
|
if (monaco.languages && monaco.languages.python) {
|
||||||
|
if (monaco.languages.python.pythonDefaults) {
|
||||||
|
monaco.languages.python.pythonDefaults.setCompilerOptions({
|
||||||
|
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||||
|
allowNonTsExtensions: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monaco.languages.setLanguageConfiguration('python', {
|
||||||
|
autoClosingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" }
|
||||||
|
],
|
||||||
|
surroundingPairs: [
|
||||||
|
{ open: '{', close: '}' },
|
||||||
|
{ open: '[', close: ']' },
|
||||||
|
{ open: '(', close: ')' },
|
||||||
|
{ open: '"', close: '"' },
|
||||||
|
{ open: "'", close: "'" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditorChange = (value) => {
|
||||||
|
setCode(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100vh', // Set to full viewport height
|
||||||
|
backgroundColor: '#252526'
|
||||||
|
}}>
|
||||||
|
{/* IDE-like toolbar */}
|
||||||
|
<Toolbar
|
||||||
|
variant="dense"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#333333',
|
||||||
|
borderBottom: '1px solid #454545',
|
||||||
|
minHeight: '40px',
|
||||||
|
px: 1,
|
||||||
|
flex: '0 0 auto' // Prevent toolbar from flexing
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
sx={{
|
||||||
|
color: '#CCCCCC',
|
||||||
|
marginRight: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{workflow.name || 'Untitled Workflow'}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleRunCode}
|
||||||
|
disabled={loading}
|
||||||
|
startIcon={<PlayArrowIcon />}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: '#0098FF',
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: '#0076CE'
|
||||||
|
},
|
||||||
|
textTransform: 'none',
|
||||||
|
minWidth: '80px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Running...' : 'Run'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<HistoryIcon />}
|
||||||
|
onClick={() => setShowExecutions(true)}
|
||||||
|
sx={{
|
||||||
|
color: '#CCCCCC',
|
||||||
|
borderColor: '#454545',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#666666',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||||
|
},
|
||||||
|
textTransform: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Show Executions
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleSaveCode}
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
sx={{
|
||||||
|
color: '#CCCCCC',
|
||||||
|
borderColor: '#454545',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: '#666666',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.1)'
|
||||||
|
},
|
||||||
|
textTransform: 'none',
|
||||||
|
marginLeft: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
|
||||||
|
{/* Editor container */}
|
||||||
|
<Box sx={{
|
||||||
|
height: getEditorHeight(),
|
||||||
|
// flex: '1 1 auto', // Allow editor to grow and shrink
|
||||||
|
// minHeight: 0, // Important for flex child
|
||||||
|
transition: 'height 0.2s ease-in-out',
|
||||||
|
position: 'relative' // Needed for Editor's absolute positioning
|
||||||
|
}}>
|
||||||
|
{workflow && mainAction ? (
|
||||||
|
<ShuffleCodeEditor
|
||||||
|
expansionModalOpen={true}
|
||||||
|
setExpansionModalOpen={true}
|
||||||
|
isCloud={true}
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
workflowExecutions={executions}
|
||||||
|
getParents={getParents}
|
||||||
|
selectedAction={mainAction}
|
||||||
|
aiSubmit={() => { }}
|
||||||
|
toolsAppId={mainAction.app_id}
|
||||||
|
codedata={code}
|
||||||
|
setcodedata={setCode}
|
||||||
|
parameterName={editorData.name}
|
||||||
|
fieldCount={editorData.field_number}
|
||||||
|
actionlist={editorData.actionlist}
|
||||||
|
fieldname={editorData.field_id}
|
||||||
|
changeActionParameterCodeMirror={() => { }}
|
||||||
|
activeDialog={() => { }}
|
||||||
|
setActiveDialog={() => { }}
|
||||||
|
fullScreenMode={true}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <Editor
|
||||||
|
height="100%" // Take full height of container
|
||||||
|
language="python"
|
||||||
|
theme="vs-dark"
|
||||||
|
value={code}
|
||||||
|
onChange={handleEditorChange}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 14,
|
||||||
|
lineHeight: 24,
|
||||||
|
fontFamily: "'JetBrains Mono', Consolas, monospace",
|
||||||
|
fontLigatures: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
contextmenu: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
snippetSuggestions: 'top',
|
||||||
|
quickSuggestions: {
|
||||||
|
other: true,
|
||||||
|
comments: false,
|
||||||
|
strings: false
|
||||||
|
},
|
||||||
|
intellisenseOptions: {
|
||||||
|
includeAutoCompletionsFromLanguageFile: true,
|
||||||
|
maxCompletionItems: 10
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Create an input element called "copy_element_shuffle" that is not visible */}
|
||||||
|
<input type="text" id="copy_element_shuffle" style={{ position: "absolute", left: "-9999px" }} />
|
||||||
|
|
||||||
|
{/* Results Panel */}
|
||||||
|
{(error || testResult) && (
|
||||||
|
<Box sx={{
|
||||||
|
height: '200px',
|
||||||
|
backgroundColor: '#1E1E1E',
|
||||||
|
borderTop: '1px solid #454545',
|
||||||
|
overflow: 'auto',
|
||||||
|
flex: '0 0 auto' // Prevent panel from flexing
|
||||||
|
}}>
|
||||||
|
{error && (
|
||||||
|
<Box sx={{ p: 2, color: '#f44336' }}>
|
||||||
|
<Typography variant="subtitle2">Error:</Typography>
|
||||||
|
<pre style={{ margin: 0 }}>{error}</pre>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{testResult && (
|
||||||
|
<Box sx={{ p: 2, color: '#4caf50' }}>
|
||||||
|
<Typography variant="subtitle2">Test Result:</Typography>
|
||||||
|
<pre style={{ margin: 0 }}>{JSON.stringify(testResult, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* In CodeWorkflow component */}
|
||||||
|
{showExecutions && (
|
||||||
|
<ExecutionPanel
|
||||||
|
workflow={workflow}
|
||||||
|
globalUrl={globalUrl}
|
||||||
|
onClose={() => setShowExecutions(false)}
|
||||||
|
currentExecution={currentExecution}
|
||||||
|
height={panelHeight}
|
||||||
|
onHeightChange={setPanelHeight} // Add this prop to handle height updates
|
||||||
|
mainAction={mainAction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeWorkflow;
|
||||||
Reference in New Issue
Block a user