718 lines
19 KiB
React
718 lines
19 KiB
React
import React, { useState, } from "react";
|
|
import { makeStyles, createStyles } from "@mui/styles";
|
|
import { toast } from "react-toastify"
|
|
|
|
import {
|
|
Tooltip,
|
|
Chip,
|
|
Typography,
|
|
IconButton,
|
|
|
|
Avatar,
|
|
AvatarGroup,
|
|
} from "@mui/material"
|
|
|
|
import {
|
|
ErrorOutline as ErrorOutlineIcon,
|
|
} from "@mui/icons-material"
|
|
|
|
import {
|
|
green,
|
|
yellow,
|
|
red,
|
|
grey,
|
|
} from "../views/AngularWorkflow.jsx"
|
|
|
|
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
|
|
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
|
import theme from "../theme.jsx";
|
|
const itemHeight = 24
|
|
|
|
export const getParentNodes = (workflow, action, count) => {
|
|
if (count === undefined) {
|
|
count = 0
|
|
}
|
|
|
|
// 50 levels of parent nodes
|
|
if (count > 50) {
|
|
return []
|
|
}
|
|
|
|
if (action === undefined || action === null) {
|
|
return []
|
|
}
|
|
|
|
if (workflow.actions === undefined || workflow.actions === null) {
|
|
workflow.actions = []
|
|
}
|
|
|
|
if (workflow.triggers === undefined || workflow.triggers === null) {
|
|
workflow.triggers = []
|
|
}
|
|
|
|
if (workflow.branches === undefined || workflow.branches === null) {
|
|
workflow.branches = []
|
|
}
|
|
|
|
var allkeys = [action.id];
|
|
var handled = [];
|
|
var results = [];
|
|
|
|
// maxiter = max amount of parent nodes to loop
|
|
// also handles breaks if there are issues
|
|
var iterations = 0;
|
|
var maxiter = 10;
|
|
while (true) {
|
|
for (let parentkey in allkeys) {
|
|
if (allkeys[parentkey] === undefined) {
|
|
continue
|
|
}
|
|
|
|
var currentnode = workflow.actions.find((element) => element.id === allkeys[parentkey])
|
|
if (currentnode === undefined) {
|
|
currentnode = workflow.triggers.find((element) => element.id === allkeys[parentkey])
|
|
|
|
if (currentnode === undefined) {
|
|
//console.log("Could not find parent node for: ", allkeys[parentkey])
|
|
continue
|
|
}
|
|
}
|
|
|
|
if (handled.includes(currentnode.id)) {
|
|
continue
|
|
} else {
|
|
handled.push(currentnode.id);
|
|
results.push(currentnode);
|
|
}
|
|
|
|
// Get the name / label here too?
|
|
if (currentnode.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
// FIXME: recursion
|
|
var incomingEdges = []
|
|
for (var branchkey in workflow.branches) {
|
|
const branch = workflow.branches[branchkey]
|
|
if (branch.destination_id !== currentnode.id) {
|
|
continue
|
|
}
|
|
|
|
// FIXME: Go up in the levels
|
|
// This somehow creates infinite recursion for now, so
|
|
// we are skipping it.
|
|
// This function is also not used for cytoscape recursion,
|
|
// so it doesn't matter much (yet)
|
|
/*
|
|
const parents = getParentNodes(workflow, {
|
|
id: branch.source_id,
|
|
}, count+1)
|
|
if (parents.length > 0) {
|
|
incomingEdges = incomingEdges.concat(parents)
|
|
}
|
|
*/
|
|
|
|
incomingEdges.push(branch)
|
|
}
|
|
|
|
for (let i = 0; i < incomingEdges.length; i++) {
|
|
var tmp = incomingEdges[i];
|
|
if (tmp.decorator === true) {
|
|
continue
|
|
}
|
|
|
|
if (!allkeys.includes(tmp.source_id)) {
|
|
allkeys.push(tmp.source_id)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (results.length === allkeys.length || iterations === maxiter) {
|
|
break
|
|
}
|
|
|
|
iterations += 1
|
|
}
|
|
|
|
// Remove on the end as we don't want to remove everything
|
|
results = results.filter((data) => data.id !== action.id)
|
|
results = results.filter((data) => data.type === "ACTION" || data.app_name === "Shuffle Workflow" || data.app_name === "User Input" || data.app_name === "shuffle-subflow")
|
|
results.push({ label: "Execution Argument", type: "INTERNAL" })
|
|
|
|
return results
|
|
}
|
|
|
|
const WorkflowValidationTimeline = (props) => {
|
|
const { globalUrl, userdata, workflow, originalWorkflow, apps, getParents, execution, showHoverColor, } = props
|
|
|
|
const [hovering, setHovering] = useState(false)
|
|
const [decidedColor, setDecidedColor] = useState(grey)
|
|
const [isClicked, setIsClicked] = useState(false)
|
|
|
|
const showMiddle = false
|
|
if (workflow === undefined || workflow === null) {
|
|
return null
|
|
}
|
|
|
|
if (workflow.validation === undefined || workflow.validation === null) {
|
|
return null
|
|
}
|
|
|
|
if (workflow.actions === undefined || workflow.actions === null) {
|
|
workflow.actions = []
|
|
}
|
|
|
|
if (workflow.triggers === undefined || workflow.triggers === null) {
|
|
workflow.triggers = []
|
|
}
|
|
|
|
if (workflow.branches === undefined || workflow.branches === null) {
|
|
workflow.branches = []
|
|
}
|
|
|
|
var results = []
|
|
if (execution !== undefined) {
|
|
results = execution.results
|
|
}
|
|
|
|
// 1. Find startnode
|
|
// 2. Map childnodes from it
|
|
var startnodeId = workflow.start
|
|
|
|
if (execution !== undefined && execution !== null) {
|
|
startnodeId = execution.start
|
|
}
|
|
|
|
// Find parent of startnodeId and if it's a webhook
|
|
var relevantactions = []
|
|
for (var key in workflow.branches) {
|
|
const branch = workflow.branches[key]
|
|
if (branch.destination_id !== startnodeId) {
|
|
continue
|
|
}
|
|
|
|
for (var triggerkey in workflow.triggers) {
|
|
const trigger = workflow.triggers[triggerkey]
|
|
if (trigger.trigger_type !== "WEBHOOK") {
|
|
continue
|
|
}
|
|
|
|
if (trigger.id === branch.source_id) {
|
|
trigger.order = -1
|
|
relevantactions.push(trigger)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var key in workflow.actions) {
|
|
const action = workflow.actions[key]
|
|
if (action.id === startnodeId) {
|
|
action.order = 0
|
|
relevantactions.push(action)
|
|
continue
|
|
}
|
|
|
|
var parents = []
|
|
if (getParents !== undefined) {
|
|
parents = getParents(action)
|
|
} else {
|
|
parents = getParentNodes(workflow, action)
|
|
}
|
|
|
|
if (action.app_name === "Integration Framework" || action.app_name === "Integration") {
|
|
for (var paramkey in action.parameters) {
|
|
const param = action.parameters[paramkey]
|
|
if (param.name === "app_name") {
|
|
action.app_name = param.value.charAt(0).toUpperCase() + param.value.slice(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (parents !== undefined && parents !== null && parents.length > 0) {
|
|
const parentfound = parents.find((element) => element.id === startnodeId)
|
|
if (parentfound !== undefined) {
|
|
|
|
// FIXME: add order here based on how many steps away from the startnode
|
|
// This just has the parent count
|
|
action.order = parents.length
|
|
|
|
relevantactions.push(action)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (getParents === undefined) {
|
|
var newactions = []
|
|
for (var key in workflow.triggers) {
|
|
const trigger = workflow.triggers[key]
|
|
if (trigger.trigger_type !== "SUBFLOW" && trigger.trigger_type !== "USERINPUT") {
|
|
continue
|
|
}
|
|
|
|
for (var branchkey in workflow.branches) {
|
|
const branch = workflow.branches[branchkey]
|
|
|
|
// Checking for OUTBOUND branches from it.
|
|
// This will mean it's NOT the last node and is easy to visualize
|
|
if (branch.source_id !== trigger.id) {
|
|
continue
|
|
}
|
|
|
|
trigger.order = 2
|
|
}
|
|
|
|
|
|
// Just in case (:
|
|
if (workflow.actions.find((element) => element.id === trigger.id) === undefined) {
|
|
newactions.push(trigger)
|
|
//workflow.actions.push(trigger)
|
|
}
|
|
}
|
|
|
|
relevantactions.push(...newactions)
|
|
}
|
|
|
|
if (relevantactions.length <= 1) {
|
|
return null
|
|
}
|
|
|
|
// Sort according to how many parents a node has. MAY be wrong~
|
|
relevantactions.sort((a, b) => {
|
|
if (a.order === undefined) {
|
|
return 1
|
|
}
|
|
|
|
if (b.order === undefined) {
|
|
return -1
|
|
}
|
|
|
|
return a.order - b.order
|
|
})
|
|
|
|
// FIXME: Add other relevant items as well from subflows (?)
|
|
var nodecolor = grey
|
|
var branchcolor = grey
|
|
var skipped = false
|
|
|
|
var previousTools = false
|
|
var scheduleNotStarted = false
|
|
|
|
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.validation_ran === false) {
|
|
//console.log("Validation didn't run or get set for workflow. Why?")
|
|
return null
|
|
}
|
|
|
|
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null && workflow.validation.errors.length > 0) {
|
|
var newErrors = []
|
|
for (var key in workflow.validation.errors) {
|
|
const error = workflow.validation.errors[key]
|
|
if (error.type === "SCHEDULE") {
|
|
scheduleNotStarted = true
|
|
continue
|
|
}
|
|
|
|
newErrors.push(error)
|
|
}
|
|
|
|
workflow.validation.errors = newErrors
|
|
}
|
|
|
|
// Use this variable to control visualization
|
|
//const showMiddle = false
|
|
// border: workflow.validation.valid ? `2px solid ${green}` : "1px solid rgba(255,255,255,0.4)",
|
|
var middleError = ""
|
|
var startBranchColor = ""
|
|
var middleBranchColor = ""
|
|
|
|
|
|
const showHoverForClick = showHoverColor === true ? true : false
|
|
return (
|
|
<div
|
|
style={{
|
|
padding: "10px 5px 10px 5px",
|
|
borderRadius: theme.palette?.borderRadius,
|
|
|
|
border: hovering === true && showHoverForClick === true ? `1px solid ${decidedColor}` : "1px solid rgba(255,255,255,0.0)",
|
|
cursor: hovering === true && showHoverForClick === true ? "pointer" : "default",
|
|
}}
|
|
onMouseEnter={() => {
|
|
if (isClicked === false) {
|
|
setHovering(true)
|
|
}
|
|
}}
|
|
onMouseLeave={() => {
|
|
if (isClicked === false) {
|
|
setHovering(false)
|
|
}
|
|
}}
|
|
onClick={() => {
|
|
if (showHoverForClick === true) {
|
|
setIsClicked(true)
|
|
}
|
|
}}
|
|
>
|
|
|
|
{isClicked === false ? null :
|
|
<WorkflowTemplatePopup2
|
|
globalUrl={globalUrl}
|
|
userdata={userdata}
|
|
|
|
isModalOpenDefault={isClicked}
|
|
workflowBuilt={true}
|
|
setIsClicked={setIsClicked}
|
|
inputWorkflowId={workflow.id}
|
|
/>
|
|
}
|
|
|
|
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
|
|
|
|
{scheduleNotStarted === true ?
|
|
null
|
|
: null}
|
|
|
|
{relevantactions.map((action, index) => {
|
|
action.result = {}
|
|
if (results !== undefined) {
|
|
const foundResult = results.find((element) => element.action.id === action.id)
|
|
if (foundResult !== undefined) {
|
|
action.result = foundResult
|
|
|
|
action.status = foundResult.status
|
|
} else {
|
|
action.status = "SUCCESS"
|
|
}
|
|
}
|
|
|
|
const lastitem = index === relevantactions.length - 1
|
|
|
|
if (!lastitem) {
|
|
if (action.app_name === "Shuffle Tools") {
|
|
if (action.status === "SUCCESS") {
|
|
branchcolor = red
|
|
|
|
// Check action.result for the actual status
|
|
const validate = validateJson(action.result.result)
|
|
if (validate.valid) {
|
|
if (validate.result.success === true) {
|
|
nodecolor = green
|
|
branchcolor = green
|
|
} else {
|
|
// Green by default
|
|
nodecolor = green
|
|
branchcolor = green
|
|
}
|
|
} else {
|
|
nodecolor = green
|
|
branchcolor = green
|
|
}
|
|
} else if (action.status === "SKIPPED") {
|
|
branchcolor = grey
|
|
} else {
|
|
// FIXME: How do we handle this?
|
|
if (action.status === undefined) {
|
|
nodecolor = grey
|
|
branchcolor = grey
|
|
} else {
|
|
nodecolor = red
|
|
branchcolor = red
|
|
}
|
|
}
|
|
|
|
previousTools = true
|
|
|
|
if (startnodeId !== action.id) {
|
|
//return null
|
|
}
|
|
} else {
|
|
if (action.status === "SUCCESS") {
|
|
nodecolor = green
|
|
} else if (action.status === "SKIPPED") {
|
|
nodecolor = grey
|
|
} else {
|
|
if (action.status === undefined) {
|
|
nodecolor = green
|
|
} else {
|
|
nodecolor = red
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
nodecolor = grey
|
|
}
|
|
|
|
if (action.status === "SKIPPED") {
|
|
skipped = true
|
|
}
|
|
|
|
var image = ""
|
|
if (action.large_image !== undefined && action.large_image !== null && action.large_image !== "") {
|
|
image = action.large_image
|
|
} else {
|
|
if (originalWorkflow !== undefined) {
|
|
for (var key in originalWorkflow.actions) {
|
|
if (originalWorkflow.actions[key].id === action.id) {
|
|
image = originalWorkflow.actions[key].large_image
|
|
break
|
|
}
|
|
}
|
|
|
|
if (image === "") {
|
|
for (var key in originalWorkflow.triggers) {
|
|
if (originalWorkflow.triggers[key].id === action.id) {
|
|
image = originalWorkflow.triggers[key].large_image
|
|
break
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
var founderror = ""
|
|
//console.log("WORKFLOW VALIDATION: ", workflow.validation)
|
|
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null) {
|
|
const foundError = workflow.validation.errors.find((element) => element.action_id === action.id)
|
|
if (foundError !== undefined) {
|
|
founderror = foundError.error
|
|
nodecolor = red
|
|
branchcolor = red
|
|
} else {
|
|
//console.log("NO ERROR: ", action.id)
|
|
}
|
|
}
|
|
|
|
var appgroup = []
|
|
if (action.app_name === "shuffle-subflow") {
|
|
if (action.status === "SUCCESS") {
|
|
nodecolor = green
|
|
branchcolor = green
|
|
}
|
|
|
|
if (workflow.validation.subflow_apps !== undefined && workflow.validation.subflow_apps !== null && workflow.validation.subflow_apps.length > 0) {
|
|
nodecolor = red
|
|
branchcolor = red
|
|
|
|
for (var subflowkey in workflow.validation.subflow_apps) {
|
|
const subflowApp = workflow.validation.subflow_apps[subflowkey]
|
|
founderror += "- " + subflowApp.error+"\n"
|
|
|
|
if (subflowApp.error === action.id) {
|
|
appgroup.push(subflowApp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!showMiddle && relevantactions.length > 2 && index > 0 && index === relevantactions.length - 2) {
|
|
if (founderror.length > 0) {
|
|
middleError += action.label+": "+founderror+"\n\n"
|
|
|
|
middleBranchColor = branchcolor
|
|
}
|
|
|
|
if (index === relevantactions.length-2 && relevantactions.length > 2) {
|
|
|
|
const selectedIcon = middleError.length > 0 ?
|
|
<Tooltip
|
|
title={
|
|
<Typography variant="body1" style={{margin: 5, whiteSpace: "pre-line", }}>
|
|
{middleError}
|
|
</Typography>
|
|
}
|
|
inputProps={{
|
|
paperProps: {
|
|
backgroundColor: "red",
|
|
}
|
|
}}
|
|
>
|
|
<IconButton style={{width: 30, height: 30, backgroundColor: "rgba(255,255,255,0.0)", borderRadius: 30, marginTop: 2, }}>
|
|
<ErrorOutlineIcon style={{color: "red", }} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
: null
|
|
|
|
return selectedIcon
|
|
} else {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Returns for anything non-middle
|
|
if (relevantactions.length > 2 && index >= 1 && index < relevantactions.length - 2) {
|
|
if (founderror.length > 0) {
|
|
middleError += action.label+": "+founderror+"\n\n"
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
if (skipped && !lastitem) {
|
|
nodecolor = grey
|
|
branchcolor = grey
|
|
}
|
|
|
|
if (previousTools) {
|
|
previousTools = false
|
|
} else {
|
|
branchcolor = nodecolor
|
|
}
|
|
|
|
if (action.trigger_type === "WEBHOOK") {
|
|
nodecolor = green
|
|
branchcolor = green
|
|
}
|
|
|
|
var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3
|
|
if (nodecolor === green) {
|
|
branchcolor = green
|
|
} else if (nodecolor === yellow) {
|
|
branchcolor = yellow
|
|
} else if (nodecolor === red) {
|
|
branchcolor = red
|
|
}
|
|
|
|
if (index === 0) {
|
|
startBranchColor = branchcolor
|
|
} else if (index !== 0 && index !== relevantactions.length - 1) {
|
|
// FIXME: This doesn't work yet
|
|
middleBranchColor = branchcolor
|
|
}
|
|
|
|
if (lastitem) {
|
|
if (middleError.length === 0) {
|
|
branchcolor = startBranchColor
|
|
} else {
|
|
//branchcolor = middleBranchColor
|
|
}
|
|
|
|
if (founderror === "") {
|
|
nodecolor = green
|
|
}
|
|
}
|
|
|
|
// FIXME: This could mean the workflow hasn't ran yet
|
|
if (workflow.validation.valid === false && (workflow.validation.errors === undefined || workflow.validation.errors === null || workflow.validation.errors.length == 0) && (workflow.validation.subflow_apps === undefined || workflow.validation.subflow_apps === null || workflow.validation.subflow_apps.length == 0)) {
|
|
nodecolor = grey
|
|
branchcolor = grey
|
|
}
|
|
|
|
const branchTooltip = branchcolor === yellow ? "Check nodes for errors" : ""
|
|
const appname = action.app_name.replaceAll('_', ' ').slice(0, 16)
|
|
|
|
const chipBackground = nodecolor === green ? "rgba(2,203,112, 0.2)" : nodecolor === grey ? "#494949": "rgba(245,52,52,0.8)"
|
|
const chipColor = nodecolor === green ? "#02cb70" : nodecolor === grey ? "#CDCDCD" : "white"
|
|
|
|
//const ballcolor = lastitem ? nodecolor : branchcolor
|
|
const ballcolor = branchcolor
|
|
const ballsize = 8
|
|
const topMargin = 20
|
|
|
|
console.log("CHIP: ", index, chipColor, chipBackground)
|
|
|
|
const chipStyle = {
|
|
height: 40,
|
|
minWidth: 125,
|
|
maxWidth: 125,
|
|
borderRadius: 50,
|
|
color: chipColor,
|
|
backgroundColor: chipBackground,
|
|
|
|
//border: `2px solid ${chipBackground}`,
|
|
//backgroundColor: "rgba(0,0,0,0.0)",
|
|
}
|
|
|
|
if (image === "") {
|
|
console.log("MISSING IMAGE: ", appname, image, action)
|
|
}
|
|
|
|
if (decidedColor === grey && nodecolor === green) {
|
|
setDecidedColor(red)
|
|
}
|
|
|
|
if (decidedColor !== red && nodecolor === red) {
|
|
setDecidedColor(red)
|
|
}
|
|
|
|
return (
|
|
<div style={{display: "flex", flex: flex, justifyContent: "right", }}>
|
|
{lastitem ?
|
|
<Tooltip title={branchTooltip}>
|
|
<div style={{display: "flex", width: "100%", position: "relative",}}>
|
|
<div style={{marginLeft: 0, marginRight: 0, marginTop: topMargin, height: 3, width: "100%", backgroundColor: branchcolor, }} />
|
|
<div style={{position: "absolute", right: -3, top: topMargin-2.5, backgroundColor: ballcolor, width: ballsize, height: ballsize, borderRadius: 10, }}/>
|
|
</div>
|
|
</Tooltip>
|
|
: null}
|
|
|
|
{appgroup.length > 0 ?
|
|
<AvatarGroup max={4} style={{height: itemHeight+8, }}>
|
|
{appgroup.map((subflowApp, subflowIndex) => {
|
|
var appimage = ""
|
|
if (apps !== undefined && apps !== null && apps.length > 0) {
|
|
for (var key in apps) {
|
|
const app = apps[key]
|
|
if (app.name === subflowApp.app_name) {
|
|
appimage = apps[key].large_image
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Tooltip title={`App: ${subflowApp.app_name} - subflow app`}>
|
|
<Avatar
|
|
style={{
|
|
border: `4px solid ${nodecolor}`,
|
|
}}
|
|
src={appimage}
|
|
/>
|
|
</Tooltip>
|
|
)
|
|
})}
|
|
</AvatarGroup>
|
|
:
|
|
<Tooltip title={
|
|
<Typography variant="body1" style={{margin: 5, color: "white", }}>
|
|
{founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`}
|
|
</Typography>
|
|
} placement="top">
|
|
<Chip label={`${appname}`} style={chipStyle} icon={
|
|
<Avatar
|
|
variant="round"
|
|
sx={{ backgroundColor: nodecolor, width: itemHeight, height: itemHeight, borderRadius: 50, border: `1px solid ${nodecolor}`,}}
|
|
>
|
|
{image !== "" ?
|
|
<img
|
|
src={image}
|
|
style={{
|
|
width: itemHeight,
|
|
height: itemHeight,
|
|
}}
|
|
/>
|
|
: null}
|
|
</Avatar>
|
|
} />
|
|
|
|
</Tooltip>
|
|
}
|
|
|
|
{lastitem ? null :
|
|
<Tooltip title={branchTooltip}>
|
|
<div style={{display: "flex", width: "100%", position: "relative",}}>
|
|
<div style={{position: "absolute", left: -3, top: topMargin-2.5, backgroundColor: ballcolor, color: ballcolor, width: ballsize, height: ballsize, borderRadius: 10, }}/>
|
|
<div style={{marginLeft: 0, marginRight: 0, marginTop: 20, height: 3, width: "100%", backgroundColor: branchcolor, }} />
|
|
</div>
|
|
</Tooltip>
|
|
}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default WorkflowValidationTimeline
|