Files
shuffle-cracked/frontend/src/components/RuntimeDebugger.jsx
T
2026-02-10 18:35:35 +05:30

1286 lines
38 KiB
React

import React, { useState, useEffect, useContext } from 'react';
import {
TextField,
Link,
Button,
ButtonGroup,
CircularProgress,
Select,
MenuList,
MenuItem,
FormControl,
InputLabel,
Autocomplete,
Tooltip,
Typography,
IconButton,
Switch,
} from '@mui/material';
import { Context } from '../context/ContextApi.jsx';
import { toast } from "react-toastify"
import { makeStyles } from "@mui/styles";
import {getTheme} from '../theme.jsx';
import dayjs from 'dayjs';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import Pagination from '@mui/material/Pagination';
import { triggers as alltriggers } from "../views/AngularWorkflow.jsx"
import {
DatePicker,
DateTimePicker,
LocalizationProvider,
} from '@mui/x-date-pickers'
import {
OpenInNew as OpenInNewIcon,
PlayArrow as PlayArrowIcon,
Insights as InsightsIcon,
Replay as ReplayIcon,
EditNote as EditNoteIcon,
AccountTree as AccountTreeIcon,
Cached as CachedIcon,
FilterAltOff as FilterAltOffIcon,
Send as SendIcon,
} from '@mui/icons-material';
import { DataGrid } from '@mui/x-data-grid'
import {
Search as SearchIcon,
} from "@mui/icons-material";
import {
InputAdornment,
} from "@mui/material"
import ClearIcon from '@mui/icons-material/Clear';
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const RuntimeDebugger = (props) => {
const { userdata, globalUrl, } = props
const classes = useStyles();
const [workflowId, setWorkflowId] = useState("")
const [status, setStatus] = useState("")
const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("")
const [totalCount, setTotalCount] = useState(0)
const {themeMode, supportEmail} = useContext(Context)
const theme = getTheme(themeMode)
const [workflow, setWorkflow] = useState({})
const [ignoreOrg, setIgnoreOrg] = useState(false)
const [searchLoading, setSearchLoading] = useState(false)
const [rowCursor, setCursor] = useState("")
const [rowsPerPage, setRowsPerPage] = useState(20)
const [maxExecutionCount, setMaxExecutionCount] = useState(50)
const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
const [suborgWorkflowRuns, setSuborgWorkflowRuns] = useState(false)
const [paginationModel, setPaginationModel] = useState({
page: 0,
pageSize: 10,
})
const [openWorkflowMenu, setOpenWorkflowMenu] = useState(false)
const [workflows, setWorkflows] = useState([
{"id": "", "name": "All Workflows",}
])
if (document != undefined) {
document.title = "Workflow Run Debugger"
}
// Shitty workflow search on purpose :)
const handleWorkflowUsageCount = (workflows) => {
if (workflows === undefined || workflows === null || workflows.length === 0) {
return
}
setTotalCount(0)
var starttime = ""
var endtime = ""
var count = 0
try {
starttime = startTime === undefined || startTime === null || startTime === "" ? "" : new Date(startTime).toISOString()
endtime = endTime === undefined || endTime === null || endTime == "" ? "" : new Date(endTime).toISOString()
} catch (e) {
toast("Invalid date format", { type: "error" })
}
var maxworkflows = 5
//console.log("Looking for MAX this amount of workflows: ", maxworkflows)
for (let key in workflows) {
if (key > maxworkflows) {
break
}
const workflowId = workflows[key].id
if (workflowId === undefined || workflowId === null || workflowId === "") {
continue
}
// Fetch the data for the workflow
var url = `${globalUrl}/api/v1/workflows/${workflowId}/executions/count`
if (starttime !== "") {
url += `?start_time=${starttime}`
}
if (endtime !== "") {
if (starttime !== "") {
url += `&end_time=${endtime}`
} else {
url += `?end_time=${endtime}`
}
}
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json()
})
.then((data) => {
if (data.success) {
if (data.count !== undefined && data.count !== null) {
count += data.count
}
} else {
console.log("Failed to get workflow usage count: ", data)
}
})
.catch((error) => {
console.error("Error:", error);
})
}
setTimeout(() => {
setTotalCount(count)
}, maxworkflows*300)
}
const submitSearch = (workflowId, status, startTime, endTime, cursor, limit, suborg_runs) => {
handleWorkflowUsageCount(workflows)
//setResultRows([])
setSearchLoading(true)
const fetchData = {
workflow_id: workflowId,
cursor: cursor,
limit: limit,
status: status.toUpperCase(),
start_time: startTime,
end_time: endTime,
ignore_org: ignoreOrg,
suborg_runs: suborg_runs,
}
fetch(`${globalUrl}/api/v1/workflows/search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(fetchData),
})
.then((response) => response.json())
.then((data) => {
setSearchLoading(false)
if (data.success) {
if (data.runs !== undefined && data.runs !== null && data.runs.length > 0) {
for (let key in data.runs) {
data.runs[key].id = data.runs[key].execution_id
// make started_at field into ISO string
//data.runs[key].endTimestamp = data.runs[key].ended_at.toISOString().slice(0, 19).replace('T', ' ')
const startTimestamp = new Date(data.runs[key].started_at*1000)
data.runs[key].startTimestamp = startTimestamp.toISOString().slice(0, 19).replace('T', ' ')
const endTimestamp = new Date(data.runs[key].completed_at*1000)
data.runs[key].endTimestamp = endTimestamp.toISOString().slice(0, 19).replace('T', ' ')
if (data.runs[key].completed_at === 0 || data.runs[key].completed_at === null) {
data.runs[key].endTimestamp = ""
}
if (data.runs[key]?.workflow_id === data.runs[key]?.execution_id && data.runs[key].type === "AGENT") {
data.runs[key].workflow.name = "Agent Execution"
}
}
// Add 20 empty rows to the end of the resultRows array
// This is to make sure that the scrollbar is always visible
setResultRows(data.runs)
} else {
toast("No results found. Keeping old runs")
}
} else {
toast("Failed to search for runs. Please try again.")
}
})
.catch((error) => {
setSearchLoading(false)
console.error("Error:", error);
toast("Failed to search for runs. Please try again (2)")
})
}
const getAvailableWorkflows = (workflowId) => {
fetch(globalUrl + "/api/v1/workflows", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson !== undefined) {
var foundWorkflows = [{"id": "", "name": "All Workflows",}]
foundWorkflows.push(...responseJson)
setWorkflows(foundWorkflows)
if (workflowId !== undefined && workflowId !== null && workflowId !== "" && workflowId.length === 36) {
for (var key in responseJson) {
if (responseJson[key].id === workflowId) {
setWorkflow(responseJson[key])
break
}
}
}
}
})
.catch((error) => {
console.log("Error getting workflows: " + error);
})
}
useEffect(() => {
// Find workflow_id in url query
const urlParams = new URLSearchParams(window.location.search);
const workflowId = urlParams.get('workflow_id');
if (workflowId !== undefined && workflowId !== null && workflowId !== "" && workflowId.length === 36) {
setWorkflowId(workflowId)
// find the workflow in workflows and select it
for (let key in workflows) {
if (workflows[key].id === workflowId) {
setWorkflow(workflows[key])
break
}
}
}
getAvailableWorkflows(workflowId)
const foundStatus = urlParams.get('status');
if (foundStatus !== undefined && foundStatus !== null && foundStatus !== "") {
setStatus(foundStatus)
}
}, [])
const forceContinue = (execution) => {
console.log(`FORCE CONTINUE execution ${execution.execution_id} for workflow ${execution.workflow.id}`)
fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/executions/${execution.execution_id}/rerun`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
if (data.reason !== undefined && data.reason !== null && data.reason !== "") {
toast("Successful response: " + data.reason)
} else {
toast("Successfully forced continue")
}
} else {
if (data.reason !== undefined && data.reason !== null && data.reason !== "") {
toast(`Failed to force continue: ${data.reason}`)
} else {
toast("Failed to force continue")
}
}
})
.catch((error) => {
console.error("Error:", error);
toast(`Failed to force continue: ${error}`)
})
}
const imageSize = 30
const timenowUnix = Math.floor(Date.now() / 1000)
const columns = [
{
field: 'execution_source',
headerName: 'Source',
width: 75,
renderCell: (params) => {
var foundSource = <PlayArrowIcon style={{color: theme.palette.primary.main, height: imageSize, width: imageSize, }} />
var source = params.row.execution_source
if (source === "schedule") {
foundSource = <img src={alltriggers[1].large_image} alt="schedule" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
} else if (source === "webhook") {
foundSource = <img src={alltriggers[0].large_image} alt="webhook" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
} else if (source === "subflow" || source.length === 36) {
foundSource = <img src={alltriggers[3].large_image} alt="subflow" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
source = "subflow"
} else if (source === "rerun" || source.length === 36) {
foundSource = <ReplayIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
source = "rerun of a previous run"
} else if (source === "form") {
foundSource = <EditNoteIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
} else if (source === "single_action" || source == "single_api" || source === "direct_api") {
foundSource = <SendIcon color="secondary" style={{height: imageSize-5, }} />
source = "Single API call"
} else if (params?.row?.type === "AGENT") {
foundSource = <img src={theme.palette.singulBlackWhite} alt="agent" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
} else {
source = "manual"
}
var imageSource = "";
if (params?.row?.org?.id?.length > 0) {
if (params?.row?.org?.image?.length > 0){
imageSource = params?.row?.org?.image
}else {
imageSource = "/images/no_image.png"
}
} else {
if (userdata?.active_org?.image?.length > 0){
imageSource = userdata?.active_org?.image
}else {
imageSource = "/images/no_image.png"
}
}
return (
<span style={{}} onClick={() => {
//setStatus(params.row.status)
}}>
{userdata?.active_org?.creator_org?.length === 0 && suborgWorkflowRuns ? (
<img src={imageSource} alt={source} style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
) : null}
<Tooltip title={source} placement="top">
{foundSource}
</Tooltip>
</span>
)
},
},
{
field: 'status',
headerName: 'Status',
width: 100,
renderCell: (params) => (
<span style={{cursor: "pointer", }} onClick={() => {
setStatus(params.row.status)
}}>
{params.row.status}
</span>
),
},
{
field: 'workflow.name',
headerName: 'Workflow Name',
width: 250,
renderCell: (params) => (
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", cursor: "pointer", }} onClick={() => {
setWorkflowId(params.row.workflow.id)
for (let key in workflows) {
if (workflows[key].id === params.row.workflow.id) {
setWorkflow(workflows[key])
break
}
}
}}>
<span>{params.row.workflow.name}</span>
{params?.row?.org?.id?.length > 0 &&
<Tooltip title={(
<div style={{display: "flex", }}>
<img src={params.row.org.image || "/images/no_image.png"} alt={params.row.org.name} style={{height: 24, width: 24, borderRadius: 12, }} />
<Typography variant="body2" style={{marginLeft: 5, }}>{params.row.org.name}</Typography>
</div>
)} placement="top" arrow>
<AccountTreeIcon style={{color: "#B0B0B0", height: "24px", width: "24px", marginLeft: 10}} />
</Tooltip>
}
</div>
),
},
{
field: 'workflow results',
headerName: 'Results',
width: 75,
renderCell: (params) => {
//console.log("PARAMS: ", params)
var extraItems = 0
if (params.row.workflow.triggers !== null) {
for (let key in params.row.workflow.triggers) {
if (params.row.workflow.triggers[key].app_name === "User Input" || params.row.workflow.triggers[key].app_name === "Shuffle Workflow") {
extraItems += 1
}
}
}
const actionLength = params.row.workflow.actions === null ? 0 : params.row.workflow.actions.length+extraItems
const resultLength = params.row.results === null ? 0 : params.row.results.length
//console.log("actionLength: ", actionLength, " resultLength: ", resultLength)
return (
<span style={{}} onClick={() => {
}}>
{resultLength} / {actionLength}
</span>
)
},
},
{
field: 'finished',
headerName: 'Finished',
width: 75,
renderCell: (params) => {
var foundItems = 0
var foundSkipped = 0
if (params.row.results !== null && params.row.results !== undefined) {
for (let key in params.row.results) {
if (params.row.results[key].status === "SUCCESS") {
foundItems += 1
}
if (params.row.results[key].status === "SKIPPED") {
foundSkipped += 1
}
}
}
var foundError = ""
if (foundItems + foundSkipped < params.row.workflow.actions.length && params.row.status === "FINISHED") {
foundError = "Workflow is done, but all nodes are not finished. This most likely indicates a problem with the workflow"
}
return (
<Tooltip title={foundError} placement="top">
<span style={{backgroundColor: foundError.length > 0 ? "rgba(244,0,0,0.6)" : "inherit"}} onClick={() => {
}}>
{foundItems}
</span>
</Tooltip>
)
},
},
{
field: 'skipped',
headerName: 'Skipped',
width: 75,
renderCell: (params) => {
var foundItems = 0
if (params.row.results !== null && params.row.results !== undefined) {
for (let key in params.row.results) {
if (params.row.results[key].status === "SKIPPED") {
foundItems += 1
}
}
}
return (
<span style={{}} onClick={() => {
}}>
{foundItems}
</span>
)
},
},
{ field: 'startTimestamp', headerName: 'Start time (UTC)', width: 160,
renderCell: (params) => {
const comparisonTimestamp = params.row.completed_at === 0 ? timenowUnix : params.row.completed_at
const hasError = comparisonTimestamp-params.row.started_at > 300
return (
<Tooltip title={hasError ? "More than 5 minutes from start to finish" : ""} placement="top">
<span style={{cursor: "pointer", backgroundColor: !hasError ? "inherit" : "rgba(244,0,0,0.4)",}} onClick={() => {
console.log("Zoom in on end timestamp is this one: ", params.row.endTimestamp)
//setEndTimestamp(params.row.endTimestamp)
// Make a new Date() from params.row.startTimestamp and set it in the endTime
const newEndTime = new Date(params.row.startTimestamp)
if (newEndTime !== null && newEndTime !== undefined && newEndTime !== "" && newEndTime !== "Invalid Date") {
// Translate newEndTime to UTC no matter what timezone we are in. Based it on local()
// Plus 1 minute to make sure it comes in
setEndTime(dayjs(newEndTime.setMinutes(newEndTime.getMinutes()+1)))
// Use dayjs to translate it into something useful
// Remove 5 minutes from it and set startTime
//newEndTime.setMinutes(newEndTime.getMinutes()-5)
//setStartTime(dayjs(newEndTime))
}
}}>
{params.row.startTimestamp}
</span>
</Tooltip>
)
}
},
{ field: 'endTimestamp', headerName: 'End time (UTC)', width: 160, },
{
field: 'id',
headerName: 'Explore',
width: 120,
renderCell: (params) => {
const parsedResult = params.row.result === null || params.row.result === undefined || params.row.result === "" ? "" : params.row.result
var errorReason = ""
var hasError = parsedResult !== null && parsedResult !== undefined && parsedResult !== "" ? parsedResult.includes("{%") && parsedResult.includes("%}") : false
if (hasError) {
errorReason = "Liquid parsing error"
}
// if success: false
// if node == FAILURE or ABORTED
if (parsedResult.includes(`\"success\": false`)) {
errorReason = "success: false in last result"
hasError = true
}
if (!hasError && parsedResult.includes(`\"status\":`)) {
// Look for any status that is 300 or higher
const statusSplit = parsedResult.split(`\"status\":`)
if (statusSplit.length > 1) {
var foundStatus = statusSplit[1].trim()
// Check if pattern is \d,
if (foundStatus.includes(",")) {
const foundStatusSplit = foundStatus.split(",")
if (foundStatusSplit.length > 1) {
foundStatus = foundStatusSplit[0].trim()
// Check if it's a number
}
} else {
foundStatus = ""
}
if (!isNaN(foundStatus) && foundStatus >= 300) {
errorReason = "Status code: "+foundStatus
hasError = true
}
}
}
if (!hasError) {
// Find last node that isn't skipped and check status
var lastresult = {}
for (var key in params.row.results) {
const result = params.row.results[key]
if (result.status === "SKIPPED") {
continue
}
if (result.completed_at === undefined || result.completed_at === null) {
continue
}
if (result.completed_at >= lastresult.completed_at) {
lastresult = result
}
}
if (lastresult.id !== undefined && lastresult.status !== "SUCCESS" && lastresult.status !== "SKIPPED") {
errorReason = "Bad status for last node: "+lastresult.status
hasError = true
}
}
if (!hasError && params.row.notifications_created !== null && params.row.notifications_created !== undefined && params.row.notifications_created !== 0) {
hasError = true
errorReason = "Generated notifications: "+params.row.notifications_created
}
return (
<div style={{display: "flex", }}>
<Tooltip arrow placement="left" title={
params.row.type === "AGENT" ?
"See agent result"
:
<Typography variant="body2" style={{whiteSpace: "pre-line", padding: 10, }}>
Workflow result: {errorReason}<br/><br/>
{params.row.result !== null && params.row.result !== undefined && params.row.result !== "" ?
params.row.result
:
null
}
</Typography>
}>
<span style={{backgroundColor: !hasError ? "inherit" : "rgba(244,0,0,0.45)", display: "flex", }}>
{params.row.type === "AGENT" ?
<Link href={`/agents?execution_id=${params.row.execution_id}&authorization=${params.row.authorization}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
</Link>
:
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
</Link>
}
</span>
</Tooltip>
<Tooltip arrow title={`Force continue workflow. Only workflows for workflows in EXECUTING state. This is NOT a rerun, but way for Shuffle to figure out the next steps automatically. If the execution doesn't finish even after trying this, please contact ${supportEmail}`}>
<IconButton
style={{marginLeft: 5, }}
disabled={params.row.status !== "EXECUTING"}
onClick={() => {
forceContinue(params.row)
}}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip arrow title="Explore workflow run logs">
<IconButton
style={{marginLeft: 5, }}
onClick={() => {
window.open(`${globalUrl}/api/v1/workflows/search/${params.row.id}`, "_blank")
}}
disabled={userdata.region_url !== "https://shuffler.io"}
>
<InsightsIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
)
}
},
]
useEffect(() => {
setPaginationModel(prev => ({
...prev,
pageSize: rowsPerPage
}))
}, [rowsPerPage])
useEffect(() => {
// Check if the user is currently focusing a texxtfield or not
// If they are, don't submit the search
if (document.activeElement.tagName === "INPUT") {
return
}
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}, [workflowId, status, startTime, endTime])
const textfieldStyle = {
marginTop: 7,
marginLeft: 5,
}
const handleStartTimeChange = (date) => {
setStartTime(date)
}
const handleEndTimeChange = (date) => {
setEndTime(date)
}
const abortExecution = (workflowId, executionId) => {
fetch(`${globalUrl}/api/v1/workflows/${workflowId}/executions/${executionId}/abort`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}
)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for ABORT EXECUTION :O!");
}
return response.json();
})
.catch((error) => {
toast.error("Error aborting execution: "+error.toString())
});
};
const handleWorkflowSelectionUpdate = (e, isUserinput) => {
if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) {
console.log("Returning as there's no id")
return null
}
console.log("DATA: ", e.target.value)
setWorkflow(e.target.value)
setWorkflowId(e.target.value.id)
setSuborgWorkflowRuns(false)
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, maxExecutionCount, false)
}
const executeWorkflow = (execution) => {
const data = {
execution_argument: execution.execution_argument,
start: execution.start,
execution_source: "rerun",
};
fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/execute?start=${execution.start}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(data),
}
)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Error executing workflow: "+responseJson.error)
} else {
console.log("Executed workflow: ", responseJson)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}
})
.catch((error) => {
toast("Failed to execute workflow: "+error.toString())
});
}
const [searchQuery, setSearchQuery] = useState("");
const [filteredRows, setFilteredRows] = useState([]);
const [defaultRows, setDefaultRows] = useState([]);
const handleQueryChange = (e) => {
const query = e.target.value.toLowerCase();
setSearchQuery(query);
}
useEffect(() => {
setDefaultRows(resultRows);
}, [resultRows]);
useEffect(() => {
if (searchQuery.trim() === "") {
setFilteredRows(defaultRows);
} else {
const filterRow = defaultRows.filter((data) => {
//Filter workflow base on the workflow status
if (data.status.toLowerCase().includes(searchQuery)) {
return true;
}
//Filter workflow base on workflow name
if (data.workflow.name.toLowerCase().includes(searchQuery)) {
return true;
}
//Filter the workflow base on execution argument
if (data.execution_argument && data.execution_argument.length > 0) {
if (data.execution_argument.toLowerCase().includes(searchQuery)) {
return true;
}
}
//Filter the workflow base on the result
if (data.results && data.results.length > 0) {
for (let result of data.results) {
if (result && result.result && result.result.toLowerCase().includes(searchQuery)) {
return true;
}
}
}
return false;
});
setFilteredRows(filterRow);
}
}, [searchQuery, defaultRows]);
return (
<div style={{minWidth: 1150, maxWidth: 1150, margin: "auto", }}>
<div style={{display: "flex", paddingTop: 50, }}>
<div style={{display: 'flex', flexDirection: 'column'}}>
<div style={{display: "flex", width: "100%", }}>
<Typography variant="h3" style={{flex: 3, whiteSpace: "nowrap" }}>
Workflow Run Debugger {totalCount !== 0 ? ` (~${totalCount})` : ""}
</Typography>
{selectedWorkflowExecutions.length > 0 ?
<ButtonGroup>
<Tooltip title="Reruns ALL selected workflows. This will make a new execution for them, and not continue the existing.">
<Button
variant="outlined"
color="secondary"
style={{maxHeight: 40, marginTop: 25, marginLeft: 20, }}
onClick={() => {
for (var i = 0; i < selectedWorkflowExecutions.length; i++) {
const selected = selectedWorkflowExecutions[i]
executeWorkflow(selected)
}
toast("Reran "+selectedWorkflowExecutions.length+" workflow run!")
setSelectedWorkflowExecutions([])
}}
>
Rerun Selected ({selectedWorkflowExecutions.length})
</Button>
</Tooltip>
<Tooltip title="Aborts ALL selected workflows in EXECUTING state">
<Button
variant="contained"
color="primary"
style={{maxHeight: 40, marginTop: 25, }}
onClick={() => {
toast("Attempting to abort "+selectedWorkflowExecutions.length+" workflow runs...")
var aborted = 0
for (var i = 0; i < selectedWorkflowExecutions.length; i++) {
const selected = selectedWorkflowExecutions[i]
if (selected.status === "EXECUTING") {
abortExecution(selected.workflow.id, selected.execution_id)
aborted += 1
}
}
if (aborted === 0) {
toast("No workflows were aborted as they are not executing.")
} else {
toast("Aborted "+aborted+" workflows.")
// Research
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
setSelectedWorkflowExecutions([])
}
}}
>
Abort Selected ({selectedWorkflowExecutions.length})
</Button>
</Tooltip>
</ButtonGroup>
: null}
{userdata.support === true ?
<Button
variant={ignoreOrg ? "contained" : "outlined"}
color="secondary"
style={{marginLeft: 20, maxHeight: 40, marginTop: 25, }}
onClick={() => {
setIgnoreOrg(!ignoreOrg)
}}
>
{ignoreOrg ? "Ignoring Org" : "Ignore Org (Support Only)"}
</Button>
: null}
</div>
<div style={{display: "flex", justifyContent: "space-between", width: "100%"}}>
<div style={{position: 'relative', right: 10, marginBottom: 10}}>
<TextField
fullWidth
value={searchQuery}
style={{
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
marginTop: 20,
marginLeft: 10,
marginRight: 12,
width: 643,
height: 51,
borderRadius: 4,
fontSize: 16,
}}
InputProps={{
style: {
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
fontSize: "1em",
height: 51,
width: 643,
borderRadius: 4,
},
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ marginLeft: 5}} />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{searchQuery.length > 0 && (
<ClearIcon
style={{
color: "white",
cursor: "pointer",
marginRight: 10
}}
onClick={() => setSearchQuery('')}
/>
)}
</InputAdornment>
),
}}
onChange={(e)=>{handleQueryChange(e)}}
color="primary"
placeholder="Filter by Workflow Name, Status, Execution Argument, Results"
id="shuffle_search_field"
/>
<Tooltip title="Set the maximum number of workflow executions to retrieve in search results" placement="top">
<FormControl style={{ minWidth: 120, marginTop: 20 }}>
<InputLabel id="max-execution-count-label" style={{ fontSize: '0.875rem' }}>Max Results</InputLabel>
<Select
labelId="max-execution-count-label"
id="max-execution-count-select"
value={maxExecutionCount}
label="Max Results"
size="small"
onChange={(e) => {
setMaxExecutionCount(e.target.value)
submitSearch(workflowId, status, startTime, endTime, rowCursor, e.target.value, suborgWorkflowRuns)
}}
style={{
height: 50,
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
}}
>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={25}>25</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={200}>200</MenuItem>
<MenuItem value={500}>500</MenuItem>
</Select>
</FormControl>
</Tooltip>
</div>
{userdata?.active_org?.creator_org?.length === 0 ? (
<div style={{display: "flex", margin: 'auto',marginTop: 20,justifyContent: 'center', alignItems: 'center', }}>
<Switch
checked={suborgWorkflowRuns}
onChange={() => {
setSuborgWorkflowRuns(!suborgWorkflowRuns);
//set selected workflow to all workflows when switching between suborg and all workflows
setWorkflowId("")
setWorkflow({"id": "", "name": "All Workflows"})
setStatus("")
setStartTime("")
setEndTime("")
setSearchQuery("")
setMaxExecutionCount(50)
submitSearch("", "", "", "", rowCursor, 50, !suborgWorkflowRuns)}
}
color="secondary"
/>
<Typography variant="body2">Show workflow runs from suborgs</Typography>
</div>
) : null}
</div>
</div>
</div>
<form onSubmit={(e) => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}} style={{display: "flex", justifyContent: "center", alignItems: "center", }}>
<FormControl fullWidth style={{marginTop: 5, }}>
<InputLabel id="status-label">Status</InputLabel>
<Select
labelId="status-label"
id="status-label-select"
value={status}
label="Status"
onChange={(event) => {
console.log("Changed: ", event.target)
setStatus(event.target.value)
}}
>
<MenuItem value="">No selection</MenuItem>
<MenuItem value="FINISHED">FINISHED</MenuItem>
<MenuItem value="EXECUTING">EXECUTING</MenuItem>
<MenuItem value="WAITING">WAITING</MenuItem>
<MenuItem value="ABORTED">ABORTED</MenuItem>
</Select>
</FormControl>
{/*
<TextField
label="Workflow ID"
fullWidth
value={workflowId}
onChange={(e) => {
setWorkflowId(e.target.value)
}}
style={textfieldStyle}
/>
*/}
<Autocomplete
id="workflow_search"
value={workflow}
open={openWorkflowMenu}
onOpen={()=>{setOpenWorkflowMenu(true)}}
onClose={() => {setOpenWorkflowMenu(false)}}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
},
}}
getOptionLabel={(option) => {
if (option === undefined || option === null ||
option.name === undefined || option.name === null
) {
return "No Workflow Selected";
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
return newname;
}}
options={[{
"name": "Agent Runs",
"id": "AGENT",
}].concat(workflows)}
fullWidth
style={{
backgroundColor: theme.palette.backgroundColor,
height: 50,
borderRadius: 4,
marginLeft: 5,
marginBottom: 3,
}}
onChange={(event, newValue) => {
console.log("Found value: ", newValue)
var parsedinput = { target: { value: newValue } }
// For variables
if (typeof newValue === 'string' && newValue.startsWith("$")) {
parsedinput = {
target: {
value: {
"name": newValue,
"id": newValue,
"actions": [],
"triggers": [],
}
}
}
}
handleWorkflowSelectionUpdate(parsedinput)
}}
renderOption={(props, data, state) => {
if (data.id === workflow.id) {
data = workflow;
}
return (
<Tooltip arrow placement="left" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
: null}
<Typography>
Choose {data.name}
</Typography>
</span>
}>
<MenuItem
style={{
backgroundColor: theme.palette.backgroundColor,
color: data.id === workflow.id ? "red" : theme.palette.textColor,
}}
sx={{
"&:hover": {
backgroundColor: theme.palette.hoverColor,
},
}}
value={data}
onClick={(e) => {
var parsedinput = { target: { value: data } }
setOpenWorkflowMenu(false)
handleWorkflowSelectionUpdate(parsedinput)
}}
>
{data.name}
</MenuItem>
</Tooltip>
)
}}
renderInput={(params) => {
return (
<TextField
style={{
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
borderRadius: 4,
}}
inputProps={{
style: {
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.textColor,
},
}}
{...params}
label="Workflow"
variant="outlined"
/>
);
}}
/>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DateTimePicker
sx={{
marginLeft: 1,
minHeight: 50,
maxHeight: 50,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search from"
format="YYYY-MM-DD HH:mm:ss"
value={startTime}
onChange={handleStartTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
<DateTimePicker
sx={{
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
minHeight: 50,
maxHeight: 50,
}}
ampm={false}
label="Search until"
format="YYYY-MM-DD HH:mm:ss"
value={endTime}
onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
<Tooltip title="Clear all filters and search parameters">
<Button
style={{ marginLeft: 10, minHeight: 60, marginTop: 10, backgroundColor: theme.palette.backgroundColor, border: "1px solid #424242", boxShadow: 'none', borderRadius: 4, width: 81, height: 51, marginRight: 15 }}
onClick={() => {
setWorkflowId("")
setWorkflow({"id": "", "name": "All Workflows"})
setStatus("")
setStartTime("")
setEndTime("")
setSearchQuery("")
setSuborgWorkflowRuns(false)
setMaxExecutionCount(50)
submitSearch("", "", "", "", rowCursor, 50, false)
}}
>
<FilterAltOffIcon />
</Button>
</Tooltip>
<Button
variant="outlined"
color="primary"
onClick={() => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}}
disabled={searchLoading}
style={{height: 50, minWidth: 100, marginTop: 15, }}
>
{searchLoading ? <CircularProgress size={30} /> : "Search"}
</Button>
</form>
<div style={{height: 700, padding: "10px 0px 10px 0px", }}>
<DataGrid
rows={filteredRows}
columns={columns}
paginationModel={paginationModel}
pageSizeOptions={[10, 20, 50, 75, 100]}
checkboxSelection
disableSelectionOnClick
onPaginationModelChange={(newPaginationModel) => {
setPaginationModel(newPaginationModel)
setRowsPerPage(newPaginationModel.pageSize)
// No API call needed - this is just for client-side pagination
}}
selectionModel={selectedWorkflowExecutions.map((workflow) => workflow.id)}
onSelectionModelChange={(newSelection) => {
//console.log("newSelection: ", newSelection)
//setSelectedWorkflowExecutionsIndexes(newSelection)
var found = []
for (var i = 0; i < newSelection.length; i++) {
// Find the workflow in the resultRows
var selected = resultRows.find((workflow) => {
return workflow.id === newSelection[i]
})
if (selected === undefined || selected === null) {
continue
}
found.push(selected)
}
setSelectedWorkflowExecutions(found)
}}
// Track which items are selected
/>
</div>
</div>
)
}
export default RuntimeDebugger;