Sync + Added dashboard for stats API
This commit is contained in:
+557
-78
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import AuthenticationModal from "../components/AuthenticationModal.jsx";
|
||||
import { useNavigate, Link, useLocation } from "react-router-dom";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import { toast } from "react-toastify"
|
||||
@@ -21,12 +22,16 @@ import {
|
||||
|
||||
import {
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Check as CheckIcon,
|
||||
HourglassDisabled as HourglassDisabledIcon,
|
||||
RestartAlt as RestartAltIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
Send as SendIcon,
|
||||
Error as ErrorIcon,
|
||||
Close as CloseIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Refresh as RefreshIcon,
|
||||
} from '@mui/icons-material'
|
||||
|
||||
import {
|
||||
@@ -43,21 +48,26 @@ const AgentUI = (props) => {
|
||||
const [data, setData] = useState({})
|
||||
const [openIndexes, setOpenIndexes] = useState([])
|
||||
const [disableButtons, setDisableButtons] = useState(false)
|
||||
const [apps, setApps] = useState([])
|
||||
const [appAuth, setAppAuth] = useState([])
|
||||
|
||||
const [originalStartTime, setOriginalStartTime] = useState(0)
|
||||
const [latestEndTime, setLatestEndTime] = useState(0)
|
||||
const [showAgentStarter, setShowAgentStarter] = useState(false)
|
||||
const [actionInput, setActionInput] = useState("")
|
||||
const [questionAnswers, setQuestionAnswers] = useState({})
|
||||
|
||||
const {themeMode} = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
const navigate = useNavigate();
|
||||
|
||||
document.title = "Shuffle AI Agents"
|
||||
|
||||
const agentWrapperStyle = {
|
||||
width: 1000,
|
||||
height: 1000,
|
||||
margin: "auto",
|
||||
paddingTop: 100,
|
||||
paddingBottom: 1000,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
}
|
||||
|
||||
if (data.input === undefined || data.input === null) {
|
||||
@@ -75,7 +85,22 @@ const AgentUI = (props) => {
|
||||
}
|
||||
|
||||
if (node_id === undefined || node_id === null || node_id === "") {
|
||||
return
|
||||
// Look for AI agent
|
||||
/*
|
||||
for (var key in execution_data.results) {
|
||||
const item = execution_data.results[key]
|
||||
if (item?.action?.app_name !== "AI Agent") {
|
||||
continue
|
||||
}
|
||||
|
||||
node_id = item?.action?.id
|
||||
break
|
||||
}
|
||||
*/
|
||||
|
||||
if (node_id === undefined || node_id === null || node_id === "") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var found = false
|
||||
@@ -150,15 +175,22 @@ const AgentUI = (props) => {
|
||||
if (responseJson.success !== false) {
|
||||
if (responseJson.status === "EXECUTING") {
|
||||
// Recursively looking for updates until it's not executing anymore
|
||||
setTimeout(() => {
|
||||
GetExecution(execution_id, node_id, authorization)
|
||||
}, 3000)
|
||||
//setTimeout(() => {
|
||||
// GetExecution(execution_id, node_id, authorization)
|
||||
//}, 3000)
|
||||
} else {
|
||||
setDisableButtons(false)
|
||||
setDisableButtons(false)
|
||||
}
|
||||
|
||||
setExecution(responseJson)
|
||||
try {
|
||||
if (JSON.stringify(responseJson) !== JSON.stringify(execution)) {
|
||||
setExecution(responseJson)
|
||||
}
|
||||
} catch(e) {
|
||||
console.log("Error comparing executions: ", e)
|
||||
setExecution(responseJson)
|
||||
}
|
||||
|
||||
findNodeData(responseJson, node_id)
|
||||
} else {
|
||||
setDisableButtons(false)
|
||||
@@ -216,12 +248,53 @@ const AgentUI = (props) => {
|
||||
}
|
||||
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
setTimeout(() => {
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
}, 10000)
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error("Error: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
const getAppAuth = () => {
|
||||
const url = `${globalUrl}/api/v1/apps/authentication`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success !== false) {
|
||||
setAppAuth(responseJson)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error("Error in auth load: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
const getApps = () => {
|
||||
const url = `${globalUrl}/api/v1/apps`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success !== false) {
|
||||
setApps(responseJson)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error("Error in app load: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const executionId = params.get("execution_id")
|
||||
@@ -233,9 +306,15 @@ const AgentUI = (props) => {
|
||||
setShowAgentStarter(true)
|
||||
//toast.warn("No execution ID or node ID provided. Please provide execution_id and node_id in the URL.")
|
||||
}
|
||||
|
||||
getApps()
|
||||
getAppAuth()
|
||||
}, [])
|
||||
|
||||
const maxTimelineWidth = 150
|
||||
const maxTimelineWidth = 300
|
||||
|
||||
var latestEndTime = 0
|
||||
var originalStartTime = 0
|
||||
const TimelineItem = (props) => {
|
||||
const { item, index } = props;
|
||||
const [hovered, setHovered] = useState(false);
|
||||
@@ -258,12 +337,96 @@ const AgentUI = (props) => {
|
||||
</Tooltip>
|
||||
|
||||
const categoryStyle = {
|
||||
width: 20,
|
||||
height: 20,
|
||||
width: 25,
|
||||
height: 25,
|
||||
marginRight: 10,
|
||||
borderRadius: 5,
|
||||
}
|
||||
|
||||
const parsedCategory = item.category === "singul" ?
|
||||
|
||||
const validate = validateJson(item.details)
|
||||
const itemStartTime = item.start_time
|
||||
var itemEndTime = item.end_time
|
||||
if (item.category === "agent" && itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
||||
console.log("Rerender 1: ", itemStartTime, originalStartTime)
|
||||
originalStartTime = itemStartTime
|
||||
}
|
||||
|
||||
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
|
||||
console.log("Rerender 2")
|
||||
latestEndTime = itemEndTime
|
||||
}
|
||||
|
||||
if (itemEndTime === undefined || itemEndTime === null) {
|
||||
// Set it to now
|
||||
itemEndTime = latestEndTime
|
||||
}
|
||||
|
||||
if (item.category == "agent" && itemEndTime === 0) {
|
||||
// Right now -> .toLocaleString() support
|
||||
itemEndTime = Date.now() / 1000
|
||||
|
||||
//<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
|
||||
|
||||
if (itemEndTime > latestEndTime) {
|
||||
latestEndTime = itemEndTime
|
||||
}
|
||||
}
|
||||
|
||||
const totalDuration = latestEndTime - originalStartTime
|
||||
var currentDuration = itemStartTime - itemEndTime
|
||||
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
||||
//var timelineMarginLeft = 0
|
||||
|
||||
// Calculate how long the div should be
|
||||
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
||||
|
||||
//console.log("CURRENT DURATION (1): ", currentDuration, itemStartTime, itemEndTime, originalStartTime, latestEndTime, totalDuration, timelineMarginLeft, timelineWidth)
|
||||
if (totalDuration === currentDuration) {
|
||||
timelineMarginLeft = 0
|
||||
timelineWidth = maxTimelineWidth
|
||||
}
|
||||
|
||||
// Just for simplicity's sake
|
||||
if (currentDuration < -1000000 || currentDuration > 1000000) {
|
||||
currentDuration = 0
|
||||
}
|
||||
|
||||
if (currentDuration < 0) {
|
||||
currentDuration = currentDuration * -1
|
||||
}
|
||||
|
||||
const defaultTopPadding = 10
|
||||
const open = openIndexes.includes(index)
|
||||
|
||||
var questions = []
|
||||
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||
item.type = "finalise"
|
||||
item.category = "finalise"
|
||||
item.label = item?.details?.reason || item.label
|
||||
|
||||
} else if (item?.category === "ask" || item?.details?.action === "ask") {
|
||||
|
||||
item.type = "question"
|
||||
item.category = "ask"
|
||||
item.label = item?.details?.reason || item.label
|
||||
|
||||
for (var fieldKey in item?.details?.fields) {
|
||||
const field = item?.details?.fields[fieldKey]
|
||||
if (field?.key !== "question") {
|
||||
continue
|
||||
}
|
||||
|
||||
questions.push({
|
||||
"question": field?.value,
|
||||
"index": questions.length + 1,
|
||||
})
|
||||
}
|
||||
} else if (item?.details?.action === "api" && item?.details?.tool?.length > 0) {
|
||||
item.label = item?.details?.reason || item.label
|
||||
}
|
||||
|
||||
var parsedCategory = item.category === "singul" ?
|
||||
<Tooltip title="Singul" placement="top">
|
||||
<img src="/images/logos/singul.svg" style={categoryStyle} />
|
||||
</Tooltip>
|
||||
@@ -273,38 +436,167 @@ const AgentUI = (props) => {
|
||||
</Tooltip>
|
||||
:
|
||||
<div style={categoryStyle} />
|
||||
|
||||
const validate = validateJson(item.details)
|
||||
const itemStartTime = item.start_time
|
||||
var itemEndTime = item.end_time
|
||||
if (itemStartTime !== undefined && itemStartTime !== originalStartTime && (itemStartTime < originalStartTime || originalStartTime === 0)) {
|
||||
console.log("Rerender 1")
|
||||
//setOriginalStartTime(itemStartTime)
|
||||
|
||||
var showAuthentication = false
|
||||
var selectedApp = {}
|
||||
if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) {
|
||||
|
||||
// Find the app and inject the image
|
||||
const toolName = item.details.tool.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
||||
for (var appKey in apps) {
|
||||
const app = apps[appKey]
|
||||
|
||||
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
||||
if (appname !== toolName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (app.large_image === undefined || app.large_image === null || app.large_image.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
selectedApp = app
|
||||
|
||||
// Override the category
|
||||
//item.category = app.name
|
||||
//item.label = item?.details?.reason || item.label
|
||||
parsedCategory =
|
||||
<Tooltip title={app.name} placement="top">
|
||||
<img src={app.large_image} style={categoryStyle} />
|
||||
</Tooltip>
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (itemEndTime !== undefined && itemEndTime > latestEndTime) {
|
||||
console.log("Rerender 2")
|
||||
setLatestEndTime(itemEndTime)
|
||||
if (!showAuthentication) {
|
||||
if (item?.details?.run_details?.raw_response !== undefined && item?.details?.run_details?.raw_response !== null && item?.details?.run_details?.raw_response?.includes("app_authentication")) {
|
||||
showAuthentication = true
|
||||
}
|
||||
}
|
||||
|
||||
if (itemEndTime === undefined || itemEndTime === null) {
|
||||
// Set it to now
|
||||
itemEndTime = latestEndTime
|
||||
var questionSubmitDisabled = questions.length === 0 ? true : false
|
||||
for (var qKey in questions) {
|
||||
const q = questions[qKey]
|
||||
if (questionAnswers[q.question] === undefined || questionAnswers[q.question] === null || questionAnswers[q.question] === "") {
|
||||
//console.log("EMPTY QUESTION: ", q)
|
||||
questionSubmitDisabled = true
|
||||
break
|
||||
} else {
|
||||
questionSubmitDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
const totalDuration = latestEndTime - originalStartTime
|
||||
const currentDuration = itemStartTime - itemEndTime
|
||||
var timelineMarginLeft = ((itemStartTime - originalStartTime) / totalDuration) * maxTimelineWidth
|
||||
var timelineWidth = ((itemEndTime - itemStartTime) / totalDuration) * maxTimelineWidth
|
||||
const barColor = item.status === "FINISHED" ? green :
|
||||
item.status === "FAILURE" || item.status == "ABORTED" ? red :
|
||||
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
|
||||
theme.palette.surfaceColor
|
||||
|
||||
if (totalDuration === currentDuration) {
|
||||
timelineMarginLeft = 0
|
||||
timelineWidth = maxTimelineWidth
|
||||
const rerunAgentButton =
|
||||
<Tooltip title="Rerun from the start with the same input" placement="right">
|
||||
<span>
|
||||
<IconButton
|
||||
style={{marginLeft: 20, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
toast.info("Attempting to rerun everything.")
|
||||
setDisableButtons(true)
|
||||
|
||||
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
|
||||
toast.error("No decision details found to rerun. Cannot proceed. Please go back to your workflow or /agents to start over.")
|
||||
} else {
|
||||
//console.log("DETAILS: ", item?.details)
|
||||
for (var messagekey in item?.details?.input?.messages) {
|
||||
const message = item?.details?.input?.messages[messagekey]
|
||||
if (message.role === "user") {
|
||||
setActionInput(message.content)
|
||||
setDisableButtons(true)
|
||||
|
||||
submitInput(message.content)
|
||||
//toast.info("Rerun started. Please wait a few seconds and this page should refresh automatically.")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
const rerunButton =
|
||||
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="right">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item.type !== "decision" || disableButtons}
|
||||
style={{marginLeft: 20, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
//toast.info("Attempting to rerun this decision by itself.")
|
||||
setDisableButtons(true)
|
||||
RerunDecision(item.details)
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
const submitQuestions = (decisionId, questionAnswers) => {
|
||||
console.log("Submitting questions: ", decisionId, questionAnswers)
|
||||
if (decisionId === undefined || decisionId === null || decisionId === "") {
|
||||
toast.error("No decision ID provided. Cannot submit answers.")
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(questionAnswers).length === 0) {
|
||||
toast.error("No answers provided. Cannot submit empty answers.")
|
||||
return
|
||||
}
|
||||
|
||||
// Loop qu
|
||||
var newArgument = {}
|
||||
for (var key in questionAnswers) {
|
||||
const answer = questionAnswers[key]
|
||||
newArgument["question_"+(answer.index)] = answer.value
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const executionId = params.get("execution_id")
|
||||
const nodeId = params.get("node_id")
|
||||
const authorization = params.get("authorization")
|
||||
|
||||
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
|
||||
console.log("PARSED URL: ", url)
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success !== false) {
|
||||
setTimeout(() => {
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
}, 500)
|
||||
|
||||
toast.success("Successfully submitted answers! The agent should continue shortly.")
|
||||
} else {
|
||||
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error("Problem with submitting: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
const defaultTopPadding = 10
|
||||
const open = openIndexes.includes(index)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -315,7 +607,7 @@ const AgentUI = (props) => {
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
if (!hovered) {
|
||||
console.log("HOVER")
|
||||
//console.log("HOVER")
|
||||
setHovered(true)
|
||||
}
|
||||
}}
|
||||
@@ -364,41 +656,50 @@ const AgentUI = (props) => {
|
||||
<div style={{minWidth: 50, maxWidth: 50, paddingTop: defaultTopPadding, }}>
|
||||
{parsedCategory}
|
||||
</div>
|
||||
{/*
|
||||
<div style={{minWidth: 200, maxWidth: 200, paddingTop: defaultTopPadding, }}>
|
||||
{/* To ISO string from unix time */}
|
||||
{new Date(item.start_time * 1000).toLocaleString()}
|
||||
{item?.start_time !== undefined && item?.start_time !== null && item?.start_time !== 0 ?
|
||||
new Date(item.start_time * 1000).toLocaleString()
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
</div>
|
||||
*/}
|
||||
<div style={{minWidth: 100, maxWidth: 100, paddingTop: defaultTopPadding-5, }}>
|
||||
<Chip
|
||||
label={item.type}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
paddingTop: defaultTopPadding,
|
||||
}}>
|
||||
{item.label}
|
||||
</div>
|
||||
|
||||
<Tooltip title={`Time taken: ${currentDuration*-1} seconds. Started: ${new Date(item.start_time * 1000).toLocaleString()}\nFinished: ${new Date(item.end_time * 1000).toLocaleString()}`} placement="right">
|
||||
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right">
|
||||
<div style={{
|
||||
minWidth: maxTimelineWidth,
|
||||
maxWidth: maxTimelineWidth,
|
||||
paddingTop: defaultTopPadding*1.3,
|
||||
paddingTop: defaultTopPadding*1.5,
|
||||
}}>
|
||||
{currentDuration !== 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
|
||||
{currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
|
||||
<div style={{
|
||||
backgroundColor: item.status === "FINISHED" ?
|
||||
green : item.status === "RUNNING" || item.status === "" ?
|
||||
theme.palette.main : theme.palette.surfaceColor,
|
||||
|
||||
backgroundColor: barColor,
|
||||
marginLeft: timelineMarginLeft,
|
||||
minWidth: timelineWidth,
|
||||
maxWidth: timelineWidth,
|
||||
height: 10,
|
||||
}} />
|
||||
: null}
|
||||
minHeight: 10,
|
||||
maxHeight: 10,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}>
|
||||
</div>
|
||||
:
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -407,24 +708,68 @@ const AgentUI = (props) => {
|
||||
maxWidth: 100,
|
||||
display: "flex",
|
||||
}}>
|
||||
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item.type !== "decision" || disableButtons}
|
||||
style={{marginLeft: 20, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
{item.category === "ask" ?
|
||||
<span style={{display: "flex", }}>
|
||||
{rerunButton}
|
||||
{/*
|
||||
<Tooltip title="Approve" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={disableButtons}
|
||||
style={{marginLeft: 20, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
toast.info("Attempting to rerun this decision by itself.")
|
||||
setDisableButtons(true)
|
||||
RerunDecision(item.details)
|
||||
}}
|
||||
>
|
||||
<RestartAltIcon />
|
||||
</IconButton>
|
||||
toast.info("Approving this step.")
|
||||
}}
|
||||
>
|
||||
<CheckIcon style={{color: green, }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Deny" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item.type !== "decision" || disableButtons}
|
||||
style={{marginLeft: 0, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
toast.info("Stopping on this step.")
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{color: red, }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
*/}
|
||||
|
||||
<Tooltip title="See in another window" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
style={{marginLeft: 0, }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
//http://localhost:3002/forms/aadfe022-fe93-431c-8634-de42dd7440ac?authorization=9357f6a6-7d59-44be-ad66-be27657369ac&reference_execution=0726378d-b501-470f-b850-f7fb48cd8ca4&source_node=de446bcf-ad37-4337-9f72-e069c7425fac&backend_url=https://ec4245cd2941.ngrok-free.app
|
||||
const newurl = `/forms/${execution?.workflow?.id}?authorization=${execution.authorization}&reference_execution=${execution.execution_id}&source_node=${agentActionResult?.action?.id}&decision_id=${item.details.run_details.id}&backend_url=${globalUrl}`
|
||||
window.open(newurl, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Tooltip>
|
||||
:
|
||||
item.category === "agent" ?
|
||||
rerunAgentButton
|
||||
:
|
||||
rerunButton
|
||||
}
|
||||
<Tooltip title="Explore results" placement="right">
|
||||
<span>
|
||||
<IconButton
|
||||
@@ -442,6 +787,69 @@ const AgentUI = (props) => {
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showAuthentication && selectedApp.id !== undefined ?
|
||||
<div style={{minWidth: 300, maxWidth: 300, margin: "auto", marginTop: 25, }}>
|
||||
<AuthenticationModal
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
|
||||
setAppAuthentication={setAppAuth}
|
||||
selectedAppData={selectedApp}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{questions?.length > 0 && item?.status === "RUNNING" ?
|
||||
<div>
|
||||
{questions.map((q, questionIndex) => {
|
||||
return (
|
||||
<div style={{marginTop: 25, }}>
|
||||
<Typography variant="body2">
|
||||
{`${q.question}`}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label={`Question ${q.index}`}
|
||||
placeholder="No question found"
|
||||
variant="outlined"
|
||||
style={{width: 800, marginTop: 20, }}
|
||||
multiline
|
||||
minRows={2}
|
||||
defaultValue={questionAnswers[q.question]?.value || ""}
|
||||
onBlur={(e) => {
|
||||
console.log("Change: ", e.target.value)
|
||||
try {
|
||||
questionAnswers[q.question] = {
|
||||
"index": questionIndex,
|
||||
"value": e.target.value,
|
||||
}
|
||||
|
||||
setQuestionAnswers({...questionAnswers, })
|
||||
} catch (e) {
|
||||
toast.warn("Something went wrong. Please contact support@shuffler.io. Details: " + e)
|
||||
}
|
||||
}}
|
||||
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{marginTop: 10, }}
|
||||
disabled={questionSubmitDisabled}
|
||||
onClick={() => {
|
||||
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
||||
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
|
||||
{open ?
|
||||
<div style={{marginTop: 10, marginBottom: 10, }}>
|
||||
@@ -479,7 +887,12 @@ const AgentUI = (props) => {
|
||||
const TimelineRender = (props) => {
|
||||
const { agent_data } = props;
|
||||
|
||||
const actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
||||
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
||||
const validate = validateJson(actionResult?.result)
|
||||
if (validate.valid === true) {
|
||||
actionResult.result = validate.result
|
||||
}
|
||||
|
||||
var timelineItems = [
|
||||
{
|
||||
"label": "AI Agent 2",
|
||||
@@ -493,6 +906,27 @@ const AgentUI = (props) => {
|
||||
},
|
||||
]
|
||||
|
||||
// Setting up the initial item
|
||||
if (agent_data?.started_at === undefined && execution?.started_at !== undefined) {
|
||||
timelineItems[0].start_time = execution?.started_at
|
||||
}
|
||||
|
||||
if (agent_data?.completed_at === undefined && execution?.completed_at !== undefined) {
|
||||
timelineItems[0].end_time = execution?.completed_at
|
||||
}
|
||||
|
||||
// Always prioritise the execution status first
|
||||
// agent (RUNNING) = workflow (EXECUTING)
|
||||
if (execution?.status !== undefined) {
|
||||
timelineItems[0].status = execution?.status
|
||||
}
|
||||
|
||||
if (actionResult?.result?.status !== undefined && actionResult?.result?.status !== null && actionResult?.result?.status?.length > 0) {
|
||||
if (timelineItems[0].status !== "FINISHED" && timelineItems[0].status !== "ABORTED" && timelineItems[0].status !== "FAILURE") {
|
||||
timelineItems[0].status = actionResult?.result?.status
|
||||
}
|
||||
}
|
||||
|
||||
// Autofixer for result lol
|
||||
if ((agent_data?.decisions === undefined || agent_data?.decisions === null)) {
|
||||
const verifiedInput = validateJson(actionResult?.result)
|
||||
@@ -500,6 +934,7 @@ const AgentUI = (props) => {
|
||||
agent_data.decisions = verifiedInput.result?.decisions
|
||||
|
||||
setAgentActionResult(actionResult)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,13 +951,13 @@ const AgentUI = (props) => {
|
||||
}
|
||||
|
||||
var newTimelineItem = {
|
||||
"label": item.action,
|
||||
"label": item?.action,
|
||||
"type": "decision",
|
||||
"category": item.category,
|
||||
"category": item?.category,
|
||||
|
||||
"status": item.run_details.status,
|
||||
"start_time": item.run_details.started_at,
|
||||
"end_time": item.run_details.completed_at,
|
||||
"status": item?.run_details?.status,
|
||||
"start_time": item?.run_details?.started_at,
|
||||
"end_time": item?.run_details?.completed_at,
|
||||
}
|
||||
|
||||
newTimelineItem.details = item
|
||||
@@ -577,6 +1012,14 @@ const AgentUI = (props) => {
|
||||
setAgentRequestLoading(true)
|
||||
//setShowAgentStarter(false);
|
||||
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
|
||||
//
|
||||
setData({})
|
||||
setExecution(null)
|
||||
setAgentRequestLoading(true)
|
||||
setShowAgentStarter(true)
|
||||
setActionInput(inputText)
|
||||
|
||||
setAgentActionResult(null)
|
||||
|
||||
if (inputText === undefined || inputText === null || inputText === "") {
|
||||
toast.error("Please provide a valid input for the AI Agent.")
|
||||
@@ -606,7 +1049,7 @@ const AgentUI = (props) => {
|
||||
},
|
||||
{
|
||||
"name":"action",
|
||||
"value":"list_tickets"
|
||||
"value":"list_tickets,API"
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -637,18 +1080,38 @@ const AgentUI = (props) => {
|
||||
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
|
||||
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
|
||||
if (isCmdEnter || isCtrlEnter) {
|
||||
e.preventDefault()
|
||||
submitInput(actionInput)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={agentWrapperStyle}>
|
||||
<TextField
|
||||
id="copy_element_shuffle"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
{showAgentStarter ?
|
||||
<Box component="form" style={{textAlign: "center", }} onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitInput(actionInput);
|
||||
}}>
|
||||
<Box
|
||||
component="form"
|
||||
style={{textAlign: "center", }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitInput(actionInput);
|
||||
}}
|
||||
>
|
||||
<img src="/images/logos/agent.svg" style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}} />
|
||||
|
||||
<div />
|
||||
|
||||
<Typography variant="h5" style={{marginTop: 30, }}>
|
||||
@@ -661,7 +1124,7 @@ const AgentUI = (props) => {
|
||||
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
||||
multiline
|
||||
minRows={2}
|
||||
defaultValue={execution?.execution_id || ""}
|
||||
defaultValue={actionInput || ""}
|
||||
onChange={(e) => {
|
||||
setActionInput(e.target.value)
|
||||
}}
|
||||
@@ -704,6 +1167,22 @@ const AgentUI = (props) => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Tooltip title="Reload the agent data" placement="top">
|
||||
<span>
|
||||
<Button
|
||||
disabled={execution === null || Object.keys(execution).length === 0}
|
||||
style={{marginLeft: 25, }}
|
||||
variant={"outlined"}
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
|
||||
}}
|
||||
>
|
||||
<RefreshIcon />
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
{buttonState === "timeline" ?
|
||||
<TimelineRender agent_data={data} />
|
||||
:
|
||||
|
||||
@@ -22,6 +22,7 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import useDebouncedCallback from "../utils/useDebouncedCallback.js";
|
||||
import {
|
||||
Zoom,
|
||||
Fade,
|
||||
@@ -275,7 +276,7 @@ export const triggers = [
|
||||
{
|
||||
"name": "alertinfo",
|
||||
"example": "",
|
||||
"value": "Do you want to continue the workflow? Start parameters: $exec",
|
||||
"value": "## Stop or continue?\n\nDetails: $exec",
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
@@ -1259,6 +1260,24 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"multiline": true,
|
||||
}]
|
||||
},
|
||||
/*
|
||||
// An attempt at handling APIs directly. This ~kind of works
|
||||
{
|
||||
"name": "API",
|
||||
"description": "Attempts to take your fields and run an API call with them, whatever they are",
|
||||
"label": "Custom Action",
|
||||
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "fields",
|
||||
"value": "",
|
||||
"description": "A JSON object with the fields to send to the API. Example: {\"url\": \"hello\", \"key2\": \"value2\"}",
|
||||
"required": true,
|
||||
"multiline": true,
|
||||
}
|
||||
]
|
||||
},
|
||||
*/
|
||||
{
|
||||
"name": "Translate standard",
|
||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||
@@ -11589,10 +11608,16 @@ const AngularWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
const handleDragStop = (e, app) => {
|
||||
if (cy === undefined || cy == null) {
|
||||
console.log("Cytoscape not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
var currentnode = cy.getElementById(newNodeId);
|
||||
|
||||
if (currentnode === undefined || currentnode === null || currentnode.length === 0) {
|
||||
return;
|
||||
console.log("No current node found")
|
||||
return
|
||||
}
|
||||
|
||||
if (parsedApp === undefined || parsedApp === null || parsedApp.data === undefined || parsedApp.data === null) {
|
||||
@@ -12411,11 +12436,20 @@ const AngularWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => {
|
||||
const debouncedRefine = useDebouncedCallback(refine, 500)
|
||||
const lastRefinedRef = useRef(currentRefinement)
|
||||
|
||||
const safeRefine = (value) => {
|
||||
if (value === lastRefinedRef.current) return
|
||||
lastRefinedRef.current = value
|
||||
debouncedRefine(value)
|
||||
}
|
||||
|
||||
if (document !== undefined) {
|
||||
const appsearchValue = document.getElementById("appsearch")
|
||||
if (appsearchValue !== undefined && appsearchValue !== null) {
|
||||
if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) {
|
||||
refine(appsearchValue.value)
|
||||
safeRefine(appsearchValue.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12448,8 +12482,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
//if (event.currentTarget.value.length > 0 && !searchOpen) {
|
||||
// setSearchOpen(true)
|
||||
//}
|
||||
|
||||
refine(event.currentTarget.value)
|
||||
safeRefine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
@@ -14725,7 +14758,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
Conditions can't be used for loops [ .# ]{" "}
|
||||
<b>PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "}</b>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
@@ -20236,17 +20269,23 @@ const AngularWorkflow = (defaultprops) => {
|
||||
|
||||
const shownErrors = !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ?
|
||||
<div
|
||||
style={{
|
||||
border: theme.palette.DialogStyle.border,
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
|
||||
style={{
|
||||
border: theme.palette.DialogStyle.border,
|
||||
position: "absolute",
|
||||
bottom: 100,
|
||||
left: leftSideBarOpenByClick ? leftBarSize + 270 : leftBarSize + 115,
|
||||
width: "fit-content",
|
||||
maxWidth: "45vw",
|
||||
minWidth: 300,
|
||||
|
||||
color: theme.palette.DialogStyle.color,
|
||||
padding: 10,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
transition: "left 0.3s ease, top 0.3s ease",
|
||||
}}
|
||||
overflowWrap: "anywhere",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
|
||||
<Tooltip
|
||||
@@ -22726,12 +22765,12 @@ const AngularWorkflow = (defaultprops) => {
|
||||
style={{ float: "right", marginTop: 20, }}
|
||||
|
||||
// Max 5 days in the past
|
||||
disabled={userdata.region_url !== "https://shuffler.io" || executionData.started_at < (Math.floor(Date.now() / 1000) - 432000)}
|
||||
disabled={executionData.started_at < (Math.floor(Date.now() / 1000) - 432000)}
|
||||
onClick={() => {
|
||||
toast("Opening logs in a new tab")
|
||||
|
||||
setTimeout(() => {
|
||||
window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
|
||||
window.open(`${globalUrl}/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
|
||||
}, 250)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1494,13 +1494,13 @@ const ApiExplorerWrapper = (props) => {
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
}}
|
||||
>
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
padding: 15,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
marginBottom: 30,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" style={{marginBottom: 25, }}>
|
||||
There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution!
|
||||
</Typography>
|
||||
|
||||
+103
-107
@@ -8,6 +8,7 @@ import {
|
||||
Typography,
|
||||
FormControlLabel,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
@@ -2680,11 +2681,11 @@ const AppCreator = (defaultprops) => {
|
||||
setErrorCode(responseJson.reason);
|
||||
|
||||
if (responseJson?.details !== undefined && responseJson?.details !== null) {
|
||||
toast.error("Failed to build - contact support@shuffler.io: " + responseJson.details, {
|
||||
toast.error("Failed to build - contact support@shuffler.io:\n\n" + responseJson.details, {
|
||||
autoClose: 60000
|
||||
})
|
||||
} else {
|
||||
toast.error("Failed to build: " + responseJson.reason, {
|
||||
toast.error("Failed to build: \n\n" + responseJson?.reason, {
|
||||
autoClose: 10000
|
||||
})
|
||||
}
|
||||
@@ -2930,7 +2931,7 @@ const AppCreator = (defaultprops) => {
|
||||
Query
|
||||
</MenuItem>
|
||||
</Select>
|
||||
<div style={{ display: "flex", width: 100 }}>
|
||||
<ButtonGroup style={{ display: "flex", width: 100 }}>
|
||||
{index === extraAuth.length - 1 ? (
|
||||
<Button
|
||||
color="primary"
|
||||
@@ -2963,7 +2964,7 @@ const AppCreator = (defaultprops) => {
|
||||
>
|
||||
<RemoveIcon style={{}} />
|
||||
</Button>
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
@@ -3431,13 +3432,13 @@ const AppCreator = (defaultprops) => {
|
||||
const ActionPaper = (props) => {
|
||||
const { data, index } = props
|
||||
|
||||
const [updater, setUpdater] = useState("tmp");
|
||||
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
||||
const [urlPath, setUrlPath] = useState("");
|
||||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
||||
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
||||
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
||||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||||
const [updater, setUpdater] = useState("tmp");
|
||||
const [actionsModalOpen, setActionsModalOpen] = useState(false);
|
||||
const [urlPath, setUrlPath] = useState("");
|
||||
const [fileUploadEnabled, setFileUploadEnabled] = useState(false);
|
||||
const [currentActionMethod, setCurrentActionMethod] = useState(actionNonBodyRequest[0])
|
||||
const [extraBodyFields, setExtraBodyFields] = useState([]);
|
||||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||||
const [currentAction, setCurrentAction] = useState({
|
||||
name: "",
|
||||
file_field: "",
|
||||
@@ -3454,6 +3455,10 @@ const AppCreator = (defaultprops) => {
|
||||
required_bodyfields: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Queries: ", urlPathQueries)
|
||||
}, [urlPathQueries])
|
||||
|
||||
const findBodyParams = (body) => {
|
||||
const regex = /\${(\w+)}/g;
|
||||
const found = body.match(regex);
|
||||
@@ -3462,7 +3467,7 @@ const AppCreator = (defaultprops) => {
|
||||
} else {
|
||||
setExtraBodyFields(found);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const UrlPathParameters = () => {
|
||||
const values = getCurrentPaths(urlPath);
|
||||
@@ -3495,28 +3500,27 @@ const AppCreator = (defaultprops) => {
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
const HandleIndividualChip = (props) => {
|
||||
const { chipData, index } = props;
|
||||
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
|
||||
const { chipData, index } = props;
|
||||
const [chipRequired, setChipRequired] = useState(currentAction.required_bodyfields !== undefined ? currentAction.required_bodyfields.includes(chipData) : false);
|
||||
|
||||
const parsedChip = chipData.startsWith("${") && chipData.endsWith("}") ? chipData.substring(2, chipData.length - 1) : chipData
|
||||
|
||||
return (
|
||||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||||
height: 30,
|
||||
margin: 3,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
cursor: "pointer",
|
||||
borderColor: theme.palette.chipStyle.borderColor,
|
||||
color: theme.palette.chipStyle.color,
|
||||
}}
|
||||
label={parsedChip}
|
||||
onClick={() => {
|
||||
return (
|
||||
<Tooltip title={chipRequired ? "Make not required" : "Make required"}>
|
||||
<Chip
|
||||
style={{
|
||||
backgroundColor: chipRequired ? "#f86a3e" : theme.palette.chipStyle.backgroundColor,
|
||||
height: 30,
|
||||
margin: 3,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
cursor: "pointer",
|
||||
borderColor: theme.palette.chipStyle.borderColor,
|
||||
color: theme.palette.chipStyle.color,
|
||||
}}
|
||||
label={parsedChip}
|
||||
onClick={() => {
|
||||
if (chipRequired) {
|
||||
currentAction["required_bodyfields"].splice(currentAction["required_bodyfields"].indexOf(chipData), 1)
|
||||
} else {
|
||||
@@ -3524,27 +3528,28 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
|
||||
setCurrentAction(currentAction);
|
||||
setChipRequired(!chipRequired);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const setActionField = (field, value) => {
|
||||
currentAction[field] = value
|
||||
setCurrentAction(currentAction)
|
||||
|
||||
//setUrlPathQueries(currentAction.queries)
|
||||
setChipRequired(!chipRequired);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const addPathQuery = () => {
|
||||
const setActionField = (field, value) => {
|
||||
currentAction[field] = value
|
||||
setCurrentAction(currentAction)
|
||||
|
||||
//setUrlPathQueries(currentAction.queries)
|
||||
};
|
||||
|
||||
const addPathQuery = () => {
|
||||
urlPathQueries.push({ name: "", required: true, example: "", });
|
||||
if (updater === "addupdater") {
|
||||
setUpdater("updater");
|
||||
} else {
|
||||
setUpdater("addupdater");
|
||||
}
|
||||
|
||||
setUrlPathQueries(urlPathQueries);
|
||||
};
|
||||
|
||||
@@ -3555,6 +3560,7 @@ const AppCreator = (defaultprops) => {
|
||||
} else {
|
||||
setUpdater("flipupdater");
|
||||
}
|
||||
|
||||
setUrlPathQueries(urlPathQueries);
|
||||
};
|
||||
|
||||
@@ -3573,7 +3579,7 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loopQueries = urlPathQueries.length === 0 ? null : (
|
||||
const loopQueries = urlPathQueries.length === 0 ? null : (
|
||||
<div>
|
||||
<Divider
|
||||
style={{
|
||||
@@ -3591,51 +3597,42 @@ const AppCreator = (defaultprops) => {
|
||||
return (
|
||||
<Paper key={queryIndex} style={actionListStyle}>
|
||||
<div style={{ marginLeft: "5px", width: "100%" }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<TextField
|
||||
required
|
||||
fullWidth={true}
|
||||
defaultValue={query.name}
|
||||
placeholder={"Query name (key)"}
|
||||
label={"Query Key"}
|
||||
helperText={
|
||||
<span style={{ color: theme.palette.text.primary, marginBottom: "2px" }}>
|
||||
Click required to flip
|
||||
</span>
|
||||
}
|
||||
onBlur={(e) => {
|
||||
console.log("IN BLUR: ", e.target.value);
|
||||
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "");
|
||||
setUrlPathQueries(urlPathQueries);
|
||||
}}
|
||||
style={{flex: 3}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
defaultValue={query.example}
|
||||
placeholder={"Default value"}
|
||||
label={"Example"}
|
||||
onBlur={(e) => {
|
||||
urlPathQueries[queryIndex].example = e.target.value.replaceAll(
|
||||
"=",
|
||||
""
|
||||
)
|
||||
|
||||
setUrlPathQueries(urlPathQueries)
|
||||
}}
|
||||
style={{flex: 2}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{display: "flex"}}>
|
||||
<TextField
|
||||
required
|
||||
fullWidth={true}
|
||||
defaultValue={query.name}
|
||||
placeholder={"Query name (key)"}
|
||||
label={"Query Key"}
|
||||
onBlur={(e) => {
|
||||
urlPathQueries[queryIndex].name = e.target.value.replaceAll("=", "")
|
||||
setUrlPathQueries(urlPathQueries)
|
||||
}}
|
||||
style={{flex: 3}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
defaultValue={query.example}
|
||||
placeholder={"Default value"}
|
||||
label={"Example"}
|
||||
onBlur={(e) => {
|
||||
// E.g. for Jira -> JQL -> requires = in param
|
||||
urlPathQueries[queryIndex].example = e.target.value.replaceAll("=","=")
|
||||
setUrlPathQueries(urlPathQueries)
|
||||
}}
|
||||
style={{flex: 2}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => {
|
||||
@@ -3654,7 +3651,7 @@ const AppCreator = (defaultprops) => {
|
||||
deletePathQuery(queryIndex);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
<DeleteIcon />
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
@@ -4106,22 +4103,22 @@ const AppCreator = (defaultprops) => {
|
||||
if (request.header !== undefined && request.header !== null) {
|
||||
var headers = [];
|
||||
for (let [key, value] of Object.entries(request.header)) {
|
||||
if (value === undefined) {
|
||||
if (key.includes(":")) {
|
||||
const keysplit = key.split(":")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
if (value === undefined) {
|
||||
if (key.includes(":")) {
|
||||
const keysplit = key.split(":")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
|
||||
} else if (key.includes("=")) {
|
||||
const keysplit = key.split("=")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
} else if (key.includes("=")) {
|
||||
const keysplit = key.split("=")
|
||||
key = keysplit[0].trim()
|
||||
value = keysplit[1].trim()
|
||||
|
||||
} else {
|
||||
toast("Removed key: ", key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else {
|
||||
toast("Removed key: ", key)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
parameterName !== undefined &&
|
||||
@@ -4392,9 +4389,8 @@ const AppCreator = (defaultprops) => {
|
||||
variant={urlPath.length > 0 ? "contained" : "outlined"}
|
||||
style={{ }}
|
||||
onClick={() => {
|
||||
//console.log(urlPathQueries)
|
||||
//console.log(urlPath)
|
||||
console.log(currentAction);
|
||||
|
||||
const errors = getActionErrors();
|
||||
addActionToView(errors);
|
||||
setActionsModalOpen(false);
|
||||
@@ -4460,7 +4456,7 @@ const AppCreator = (defaultprops) => {
|
||||
|
||||
return (
|
||||
<Paper key={index} style={actionListStyle}>
|
||||
{newActionModal}
|
||||
{newActionModal}
|
||||
|
||||
{error}
|
||||
<Tooltip title="Edit action" placement="bottom">
|
||||
|
||||
@@ -3075,7 +3075,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
|
||||
const appEnding = app?.public === true ? app?.app_version : app?.id
|
||||
|
||||
return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip`
|
||||
return `curl -L \ \\\n "${globalUrl}/api/v1/download_docker_image?image=frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding}" \\\n -H \"Authorization: Bearer APIKEY" \\\n -o image.zip; \\\n docker load -i image.zip${!app?.public ? ` \\\n docker tag frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${appEnding} frikky/shuffle:${app?.name.toLowerCase().replaceAll(' ', '_')}_${app.app_version}` : ``}`
|
||||
}
|
||||
|
||||
const renderedActionOptions = deduplicateByName((
|
||||
@@ -3405,7 +3405,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
<div style={{ textAlign: "center", marginTop: 25 }}>
|
||||
<Link
|
||||
rel="noopener noreferrer"
|
||||
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}`}
|
||||
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}&view=/apps/${params.appid}/integrations/${secondaryApp.name}`}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<Button
|
||||
@@ -4300,8 +4300,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (!isLoggedIn) {
|
||||
//navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
|
||||
toast("You must be logged in to activate apps! Go to /login first.")
|
||||
navigate("/login?message=You must be logged in to activate this app&view=/apps/" + params.appid);
|
||||
// toast("You must be logged in to activate apps! Go to /login first.")
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import { debounce } from "lodash";
|
||||
import AppSelection from "../components/AppSelection.jsx";
|
||||
import AppModal from "../components/AppModal.jsx";
|
||||
import AppCreationModal from "../components/AppCreationModal.jsx";
|
||||
import Dropzone from "../components/Dropzone.jsx";
|
||||
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
@@ -1136,6 +1137,7 @@ const Apps2 = (props) => {
|
||||
const [field2, setField2] = useState("");
|
||||
const [validation, setValidation] = useState(null);
|
||||
const [createAppModalOpen, setCreateAppModalOpen] = useState(false);
|
||||
const [openApiData, setOpenApiData] = useState("");
|
||||
|
||||
const {themeMode, brandColor} = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
@@ -1736,6 +1738,31 @@ const Apps2 = (props) => {
|
||||
// setOpenModal(true);
|
||||
};
|
||||
|
||||
const uploadFile = (e) => {
|
||||
const isFromDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
|
||||
const files = isFromDropzone ? e.dataTransfer.files : e.target.files;
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
try {
|
||||
reader.addEventListener("load", (ev) => {
|
||||
const content = ev.target.result;
|
||||
setOpenApiData(content);
|
||||
setCreateAppModalOpen(true);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log("Error in dropzone: ", err);
|
||||
}
|
||||
|
||||
try {
|
||||
reader.readAsText(files[0]);
|
||||
} catch (error) {
|
||||
toast("Failed to read file");
|
||||
}
|
||||
};
|
||||
|
||||
// Validation and redirect are handled inside AppCreationModal
|
||||
|
||||
useEffect(() => {
|
||||
const apps = currTab === 1 ? userApps : orgApps;
|
||||
const filteredUserAppdata = filterApps(apps, searchQuery, selectedCategory, selectedLabel);
|
||||
@@ -1853,6 +1880,10 @@ const Apps2 = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
style={{ width: "100%", height: "100vh" }}
|
||||
onDrop={uploadFile}
|
||||
>
|
||||
<div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<AppModal
|
||||
@@ -1869,6 +1900,8 @@ const Apps2 = (props) => {
|
||||
theme={theme}
|
||||
globalUrl={globalUrl}
|
||||
isCloud={isCloud}
|
||||
startOpenApi={openApiData?.length > 0}
|
||||
prefillOpenApiData={openApiData}
|
||||
/>
|
||||
{appsModalLoad}
|
||||
<div style={boxStyle}>
|
||||
@@ -2198,6 +2231,24 @@ const Apps2 = (props) => {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
title="Create an app with different options or Just drop a YAML/JSON file here"
|
||||
placement="top"
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: "rgba(33, 33, 33, 1)",
|
||||
color: "rgba(241, 241, 241, 1)",
|
||||
fontSize: 12,
|
||||
width: 240,
|
||||
lineHeight: 1.5,
|
||||
border: "1px solid rgba(73, 73, 73, 1)",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
}
|
||||
},
|
||||
}}
|
||||
arrow
|
||||
>
|
||||
<div style={{
|
||||
width: "25%",
|
||||
minWidth: "25%",
|
||||
@@ -2222,6 +2273,7 @@ const Apps2 = (props) => {
|
||||
Create an App
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -2365,6 +2417,7 @@ const Apps2 = (props) => {
|
||||
<Configure clickAnalytics />
|
||||
</InstantSearch>
|
||||
</div>
|
||||
</Dropzone>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
import Draggable from "react-draggable";
|
||||
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
|
||||
import { LoadStats } from '../components/LineChartWrapper.jsx';
|
||||
|
||||
import {
|
||||
Autocomplete,
|
||||
@@ -828,9 +828,14 @@ const Dashboard = (props) => {
|
||||
}
|
||||
</div>
|
||||
|
||||
<DashboardBarchart
|
||||
timelineData={data}
|
||||
height={50}
|
||||
<LineChartWrapper
|
||||
inputname={"heyo"}
|
||||
keys={data}
|
||||
height={100}
|
||||
width={100}
|
||||
border={false}
|
||||
|
||||
color={"#808080"}
|
||||
/>
|
||||
|
||||
</Paper>
|
||||
|
||||
@@ -400,6 +400,10 @@ const Docs = (defaultprops) => {
|
||||
if (propkey === "app_creation") {
|
||||
navigate('/docs/apps#app-creation-introduction')
|
||||
}
|
||||
|
||||
if (propkey === "api") {
|
||||
navigate('/docs/API')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -690,7 +694,8 @@ const Docs = (defaultprops) => {
|
||||
|
||||
const Heading = (props) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
var id = props.children[0].toLowerCase().toString()
|
||||
|
||||
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase();
|
||||
if (props.level <= 3) {
|
||||
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import React, { useEffect, useState, useContext, useRef, useCallback } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
Box,
|
||||
Stack,
|
||||
Chip,
|
||||
Avatar,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import TrendingUpIcon from '@mui/icons-material/TrendingUp';
|
||||
import TrendingDownIcon from '@mui/icons-material/TrendingDown';
|
||||
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
|
||||
import TaskAltIcon from '@mui/icons-material/TaskAlt';
|
||||
import SuccessFailedRunsWidget from '../components/SuccessFailedRunsWidget.jsx';
|
||||
import RunsOverTimeWidget from '../components/RunsOverTimeWidget.jsx';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DashboardOnboarding from '../components/DashboardOnboarding.jsx';
|
||||
|
||||
const NewDashboard = (props) => {
|
||||
const { globalUrl, userdata } = props;
|
||||
|
||||
// const [workflows, setWorkflows] = useState([]);
|
||||
const { leftSideBarOpenByClick } = useContext(Context);
|
||||
const [sfwControls, setSfwControls] = useState(null);
|
||||
const [loadingSfw, setLoadingSfw] = useState(true);
|
||||
const [loadingRot, setLoadingRot] = useState(true);
|
||||
const [loadingNoti, setLoadingNoti] = useState(true);
|
||||
const [showOverlay, setShowOverlay] = useState(true);
|
||||
const [totals, setTotals] = useState({ days: 30, mode: 'workflows', totalRuns: 0, successRuns: 0, failedRuns: 0, activeDays: 0, timeSavedMinutes: 0, moneySavedDollars: 0 });
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [onboardingOpen, setOnboardingOpen] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem("dashboard_onboarding_complete") === "true" ? false : true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const [overrideDays, setOverrideDays] = useState(undefined);
|
||||
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const handleSfwControls = useCallback((node) => {
|
||||
setSfwControls(node);
|
||||
}, []);
|
||||
|
||||
const formatCurrencyCompact = (value) => {
|
||||
const n = Math.max(0, Number(value) || 0);
|
||||
const abs = Math.abs(n);
|
||||
const fmt = (x, suffix) => `${(Math.round(x * 10) / 10).toString().replace(/\.0$/, '')}${suffix}`;
|
||||
if (abs >= 1e9) return `$${fmt(n / 1e9, 'B')}`;
|
||||
if (abs >= 1e6) return `$${fmt(n / 1e6, 'M')}`;
|
||||
if (abs >= 1e3) return `$${fmt(n / 1e3, 'k')}`;
|
||||
return `$${Math.round(n).toLocaleString()}`;
|
||||
};
|
||||
|
||||
const formatTimeDisplay = (mins) => {
|
||||
const totalMins = Math.max(0, Math.round(mins || 0));
|
||||
if (totalMins < 60) return { display: `${totalMins}m`, title: `${totalMins} minutes` };
|
||||
const totalHours = Math.floor(totalMins / 60);
|
||||
if (totalHours >= 24) {
|
||||
const days = Math.floor(totalHours / 24);
|
||||
return { display: `${days}d`, title: `${totalHours} hours` };
|
||||
}
|
||||
return { display: `${totalHours}h`, title: `${totalHours} hours` };
|
||||
};
|
||||
|
||||
const timeFmt = formatTimeDisplay(totals.timeSavedMinutes);
|
||||
const STATIC_TIME_PERCENT = '62%';
|
||||
const STATIC_MONEY_PERCENT = '46%';
|
||||
|
||||
const unreadCount = notifications.filter(n => n && n.read === false).length;
|
||||
const readCount = notifications.filter(n => n && n.read === true).length;
|
||||
|
||||
// Current values
|
||||
// 1 Workflow run = 15 minutes
|
||||
// 1 Workflow run = $25
|
||||
|
||||
const kpis = [
|
||||
{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879' },
|
||||
{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879' },
|
||||
{ value: String(unreadCount), label: 'Total errors', icon: <ErrorOutlineIcon sx={{ color: '#f87171', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#f87171' },
|
||||
{ value: String(readCount), label: 'Errors resolved', icon: <TaskAltIcon sx={{ color: '#5cc879', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#5cc879' },
|
||||
];
|
||||
|
||||
const getGreeting = () => {
|
||||
try {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 5) return 'Good night';
|
||||
if (hour < 12) return 'Good morning';
|
||||
if (hour < 18) return 'Good afternoon';
|
||||
return 'Good evening';
|
||||
} catch {
|
||||
return 'Hey';
|
||||
}
|
||||
};
|
||||
|
||||
const displayName = userdata !== undefined && userdata?.username !== undefined ? userdata?.username?.split('@')[0]?.charAt(0)?.toUpperCase() + userdata?.username?.split('@')[0]?.slice(1) : 'User';
|
||||
|
||||
useEffect(() => {
|
||||
let t;
|
||||
const anyLoading = loadingSfw || loadingRot || loadingNoti;
|
||||
if (anyLoading) {
|
||||
t = setShowOverlay(true);
|
||||
} else {
|
||||
setShowOverlay(false);
|
||||
}
|
||||
return () => { if (t) clearTimeout(t); };
|
||||
}, [loadingSfw, loadingRot, loadingNoti]);
|
||||
|
||||
// Auto-open onboarding when there aren't enough active days of stats
|
||||
useEffect(() => {
|
||||
try {
|
||||
const alreadyDone = localStorage.getItem("dashboard_onboarding_complete") === "true";
|
||||
if (alreadyDone) {
|
||||
setOnboardingOpen(false);
|
||||
return;
|
||||
}
|
||||
const active = Number(totals?.activeDays || 0);
|
||||
setOnboardingOpen(active < 5);
|
||||
} catch {
|
||||
setOnboardingOpen(true);
|
||||
}
|
||||
}, [totals?.activeDays]);
|
||||
|
||||
// Load notifications
|
||||
useEffect(() => {
|
||||
const loadNotifications = async () => {
|
||||
try {
|
||||
const resp = await fetch(`${globalUrl}/api/v1/notifications`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
if (resp.status !== 200) {
|
||||
setNotifications([]);
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
const list = Array.isArray(data?.notifications) ? data.notifications : (Array.isArray(data) ? data : []);
|
||||
setNotifications(list.filter(Boolean));
|
||||
} catch (e) {
|
||||
setNotifications([]);
|
||||
} finally {
|
||||
setLoadingNoti(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadNotifications();
|
||||
}, [globalUrl]);
|
||||
|
||||
// useEffect(() => {
|
||||
// // Lightweight workflows list for selector in success/failed widget
|
||||
// const loadWorkflows = async () => {
|
||||
// try {
|
||||
// const resp = await fetch(`${globalUrl}/api/v1/workflows`, {
|
||||
// method: 'GET',
|
||||
// credentials: 'include',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// });
|
||||
// if (resp.status !== 200) {
|
||||
// return;
|
||||
// }
|
||||
// const data = await resp.json();
|
||||
// const list = Array.isArray(data?.workflows) ? data.workflows : (Array.isArray(data) ? data : []);
|
||||
// const normalized = list.filter(Boolean).map((w, idx) => ({ id: w?.id || w?.ID || `${idx}`, name: w?.name || w?.Name || `Workflow ${idx+1}` }));
|
||||
// setWorkflows(normalized);
|
||||
// } catch (e) {
|
||||
// // ignore
|
||||
// }
|
||||
// };
|
||||
|
||||
// loadWorkflows();
|
||||
// }, [globalUrl]);
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}>
|
||||
<DashboardOnboarding
|
||||
open={onboardingOpen}
|
||||
globalUrl={globalUrl}
|
||||
onClose={() => setOnboardingOpen(false)}
|
||||
onExplore={() => {
|
||||
// Ensure overrides are set before closing modal
|
||||
setOverrideDays(5);
|
||||
setRotMonthOverride(new Date(new Date().getFullYear(), new Date().getMonth(), 1));
|
||||
|
||||
// Close modal immediately to trigger data fetching
|
||||
setOnboardingOpen(false);
|
||||
}}
|
||||
headerTitle="Unlock your Dashboard"
|
||||
headerSubtitle="Complete these steps to start seeing insights."
|
||||
/>
|
||||
{showOverlay && (
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||
<CircularProgress size={36} thickness={4} />
|
||||
<Typography variant="body2" color="textSecondary">Loading dashboard…</Typography>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Header / Greeting */}
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
|
||||
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
|
||||
<>
|
||||
{sfwControls}
|
||||
</>
|
||||
</Box>
|
||||
|
||||
{/* KPI cards */}
|
||||
<Grid container spacing={2}>
|
||||
{kpis.map((kpi) => (
|
||||
<Grid item xs={12} sm={6} md={3} key={kpi.label}>
|
||||
<Paper style={{ padding: 16, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12
|
||||
,cursor: kpi.label.toLowerCase().includes('total errors') ? 'pointer' : 'default'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (kpi.label.toLowerCase().includes('total errors')) {
|
||||
// navigate to notifications page
|
||||
navigate('/admin?admin_tab=notifications');
|
||||
}
|
||||
}}
|
||||
|
||||
>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Stack sx={{py:2, paddingLeft: 1}}>
|
||||
<Typography variant="h4" title={kpi.title || ''}>{kpi.value}</Typography>
|
||||
<Typography sx={{fontSize: 13}} color="textSecondary">{kpi.label}</Typography>
|
||||
</Stack>
|
||||
<Stack sx={{py: 2, paddingRight: 1, marginTop: kpi.label.toLowerCase().includes('errors') ? -1 : 0}}>
|
||||
{kpi.icon}
|
||||
<Typography variant="body2" color={kpi.color}>{kpi.percentage}</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* Success/Failed widget uses its own internal sub-cards; make wrapper transparent */}
|
||||
<Paper elevation={0} style={{ padding: 0, marginTop: 5, background: 'transparent', boxShadow: 'none', border: 'none' }}>
|
||||
<SuccessFailedRunsWidget
|
||||
globalUrl={globalUrl}
|
||||
overrideDays={overrideDays}
|
||||
dummyMode={onboardingOpen}
|
||||
onControlsChange={handleSfwControls}
|
||||
onLoadingChange={setLoadingSfw}
|
||||
onTotalsChange={setTotals}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{/* Runs over time section */}
|
||||
<Paper style={{ padding: 16, marginTop: 19, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12 }}>
|
||||
<RunsOverTimeWidget globalUrl={globalUrl} onLoadingChange={setLoadingRot} monthOverride={rotMonthOverride} dummyMode={onboardingOpen} />
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewDashboard;
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
const [executionLoading, setExecutionLoading] = useState(false);
|
||||
const [executionData, setExecutionData] = React.useState({});
|
||||
const [executionRunning, setExecutionRunning] = useState(false);
|
||||
const [disableButtons, setDisableButtons] = useState(false);
|
||||
const [workflowQuestion, setWorkflowQuestion] = useState("");
|
||||
const [selectedOrganization, setSelectedOrganization] = React.useState(undefined);
|
||||
const [apps, setApps] = React.useState([]);
|
||||
@@ -84,12 +85,14 @@ const RunWorkflow = (defaultprops) => {
|
||||
const [workflows, setWorkflows] = React.useState([])
|
||||
const [boxWidth, setBoxWidth] = React.useState(500)
|
||||
const [inputQuestions, setInputQuestions] = React.useState([])
|
||||
const [agentic, setAgentic] = React.useState(false)
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const answer = searchParams.get("answer")
|
||||
const execution_id = searchParams.get("reference_execution")
|
||||
const authorization = searchParams.get("authorization")
|
||||
const sourceNode = searchParams.get("source_node")
|
||||
const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
|
||||
const backendUrl = searchParams.get("backend_url") || globalUrl
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,11 +165,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Used to swap from login to register. True = login, false = register
|
||||
|
||||
// Error messages etc
|
||||
const [executionInfo, setExecutionInfo] = useState("");
|
||||
|
||||
const handleValidateForm = (executionArgument) => {
|
||||
// Check if every field exists
|
||||
if (executionArgument === undefined || executionArgument === null) {
|
||||
@@ -184,9 +184,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("EXEC: ", executionArgument)
|
||||
// FIXME: Error with User Input + Required arg (?)
|
||||
// Somehow validation is not happening as it should, and it just checks all
|
||||
// questions if none are selected
|
||||
for (var key in executionArgument) {
|
||||
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
|
||||
console.log("Unanswered, required question: ", key)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -334,17 +337,18 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
const validate = validateJson(executionData.result)
|
||||
|
||||
return (
|
||||
<div style={{marginTop: executionMargin, }}>
|
||||
{workflowQuestion !== "" ? null :
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
<div style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
}
|
||||
|
||||
{workflowQuestion !== "" ? null :
|
||||
validate.valid === false ?
|
||||
<div style={{marginTop: 20, }}>
|
||||
<Divider />
|
||||
{validate?.result !== undefined && validate?.result !== null && validate?.result.length > 0 ?
|
||||
<Divider />
|
||||
: null }
|
||||
<Markdown
|
||||
components={{
|
||||
img: Img,
|
||||
@@ -397,10 +401,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
stop()
|
||||
setMessage("")
|
||||
setExecutionLoading(true)
|
||||
setExecutionData({})
|
||||
setExecutionInfo("")
|
||||
|
||||
setTimeout(() => {
|
||||
setExecutionLoading(true)
|
||||
}, 2500)
|
||||
|
||||
var data = {
|
||||
"execution_argument": executionArgument,
|
||||
"execution_source": "form",
|
||||
@@ -462,6 +469,14 @@ const RunWorkflow = (defaultprops) => {
|
||||
fetchBody.body = JSON.stringify(data)
|
||||
}
|
||||
|
||||
if (agentic === true) {
|
||||
if (url.includes("?")) {
|
||||
url += `&agentic=true&decision_id=${decisionId}`
|
||||
} else {
|
||||
url += `?agentic=true&decision_id=${decisionId}`
|
||||
}
|
||||
}
|
||||
|
||||
// IF there is an execution argument, we should use it
|
||||
fetch(url, fetchBody)
|
||||
.then((response) => {
|
||||
@@ -480,25 +495,30 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization.length === 0) {
|
||||
toast(`This form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form`)
|
||||
}
|
||||
//if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization?.length === 0) {
|
||||
// toast(`This form is not available for you to run. If you this is an error, contact ${supportEmail} with a link to this form (2)`)
|
||||
//}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then(responseJson => {
|
||||
//if (responseJson.success === true) {
|
||||
// setDisableButtons(true)
|
||||
//}
|
||||
|
||||
setExecutionLoading(false)
|
||||
if (responseJson.execution_id !== undefined && responseJson.execution_id !== null && responseJson.execution_id.length > 0) {
|
||||
if (responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id?.length > 0) {
|
||||
navigate(`?execution_id=${responseJson.execution_id}`)
|
||||
}
|
||||
|
||||
if (responseJson.success === false) {
|
||||
|
||||
console.log("Failed sending execution request")
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (responseJson?.reason !== undefined && responseJson?.reason !== null) {
|
||||
if (responseJson?.reason?.toLowerCase().includes("already clicked")) {
|
||||
setMessage("Already answered. You may close this window (2).")
|
||||
setMessage("This form has been answered. You may close this window.")
|
||||
} else {
|
||||
toast.warn(responseJson.reason)
|
||||
toast.warn(responseJson?.reason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,11 +540,17 @@ const RunWorkflow = (defaultprops) => {
|
||||
setExecutionRequest(responseJson)
|
||||
start()
|
||||
}
|
||||
|
||||
// If execution_id or authorization, add them to the URL
|
||||
if (responseJson?.execution_id !== undefined && responseJson?.execution_id !== null && responseJson?.execution_id?.length > 0 && responseJson?.authorization !== undefined && responseJson?.authorization !== null && responseJson?.authorization?.length > 0) {
|
||||
navigate(`?execution_id=${responseJson.execution_id}&authorization=${responseJson.authorization}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
//setExecutionInfo("Error in workflow startup: " + error)
|
||||
toast.warn("Error submitting form. Please try again.")
|
||||
console.log("Error starting workflow: ", error)
|
||||
toast.warn(`Error submitting form. Please try again: ${error}`)
|
||||
|
||||
stop()
|
||||
setMessage("")
|
||||
@@ -597,8 +623,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
||||
const newmarkdown = realtimeMarkdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
} else if (inputWorkflow.form_control.input_markdown !== undefined && inputWorkflow.form_control.input_markdown !== null && inputWorkflow.form_control.input_markdown.length > 0) {
|
||||
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
} else if (inputWorkflow?.form_control?.input_markdown !== undefined && inputWorkflow?.form_control?.input_markdown !== null && inputWorkflow?.form_control?.input_markdown.length > 0) {
|
||||
const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
}
|
||||
}
|
||||
@@ -608,10 +634,10 @@ const RunWorkflow = (defaultprops) => {
|
||||
console.log("Get workflow error: ", error.toString())
|
||||
|
||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
||||
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
} else if (inputWorkflow.form_control.input_markdown !== undefined && inputWorkflow.form_control.input_markdown !== null && inputWorkflow.form_control.input_markdown.length > 0) {
|
||||
const newmarkdown = inputWorkflow.form_control.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
} else if (inputWorkflow?.form_control?.input_markdown !== undefined && inputWorkflow?.form_control?.input_markdown !== null && inputWorkflow?.form_control?.input_markdown.length > 0) {
|
||||
const newmarkdown = inputWorkflow?.form_control?.input_markdown.replace(`{{ ${workflow_id} }}`, "", -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
}
|
||||
})
|
||||
@@ -646,6 +672,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
trig.parameters = []
|
||||
}
|
||||
|
||||
newexec = {}
|
||||
for (var paramkey in trig.parameters) {
|
||||
const param = trig.parameters[paramkey]
|
||||
if (param.name !== "input_questions") {
|
||||
@@ -683,6 +710,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Setting exec arg: ", newexec)
|
||||
setExecutionArgument(newexec)
|
||||
}
|
||||
|
||||
@@ -733,10 +761,10 @@ const RunWorkflow = (defaultprops) => {
|
||||
setInputQuestions(workflow.input_questions)
|
||||
}
|
||||
|
||||
if (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) {
|
||||
if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) {
|
||||
// Look for {{ uuid }} format, and try to run that workflow with their account
|
||||
// This is a hack, but a fun one.
|
||||
var newmarkdown = workflow.form_control.input_markdown.replace("", "")
|
||||
var newmarkdown = workflow?.form_control?.input_markdown.replace("", "")
|
||||
|
||||
const uuidRegex = /{{\s[a-f0-9-]+\s}}/g
|
||||
const found = newmarkdown.match(uuidRegex)
|
||||
@@ -784,8 +812,8 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (workflow.status !== "WAITING") {
|
||||
setMessage("Already answered. You may close this window (3).")
|
||||
if (workflow.status === "EXECUTING" || workflow.status === "SUCCESS" || workflow.status === "ABORTED" || workflow.status === "STOPPED" || workflow.status === "FAILURE" || workflow.status === "FINISHED") {
|
||||
setMessage("Already handled. You may close this window.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,13 +834,17 @@ const RunWorkflow = (defaultprops) => {
|
||||
console.log("Status not 200 for workflows :O!");
|
||||
}
|
||||
|
||||
if ((response.status === 401 || response.status === 403) && authorization === undefined || authorization === null || authorization.length === 0) {
|
||||
toast(`This form is not available to you. If you think this is an error, please contact ${supportEmail} with the URL.`)
|
||||
}
|
||||
//if (response.status >= 400 && authorization === undefined || authorization === null || authorization.length === 0) {
|
||||
// toast.warn(`This form may not be available to you. If you think this is an error, please contact ${supportEmail} with the URL.`)
|
||||
//}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
return
|
||||
}
|
||||
|
||||
// Not sure why this is necessary.
|
||||
if (responseJson.isValid === undefined) {
|
||||
responseJson.isValid = true;
|
||||
@@ -1008,14 +1040,78 @@ const RunWorkflow = (defaultprops) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success == false) {
|
||||
if (responseJson?.success == false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && (workflow.id === undefined || workflow.id === null || workflow.id.length === 0) && responseJson.workflow !== undefined && responseJson.workflow !== null) {
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && disableButtons === false && responseJson?.status !== "" && responseJson?.status !== "WAITING") {
|
||||
setDisableButtons(true)
|
||||
}
|
||||
|
||||
//if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && (workflow.id === undefined || workflow.id === null || workflow.id.length === 0) && responseJson.workflow !== undefined && responseJson.workflow !== null) {
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && responseJson.workflow !== undefined && responseJson.workflow !== null) {
|
||||
setupSourcenode(responseJson.workflow, sourceNode)
|
||||
setWorkflow(responseJson.workflow)
|
||||
|
||||
//const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
|
||||
// Check for decision_id in url
|
||||
if (decisionId?.length > 0 && responseJson?.workflow?.actions?.length > 0 && sourceNode?.length > 0 && responseJson?.results?.length > 0) {
|
||||
console.log("Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
|
||||
|
||||
setAgentic(true)
|
||||
|
||||
for (var resultkey in responseJson.results) {
|
||||
const result = responseJson.results[resultkey]
|
||||
if (result.action.id !== sourceNode) {
|
||||
continue
|
||||
}
|
||||
|
||||
const validated = validateJson(result.result)
|
||||
if (!validated.valid) {
|
||||
console.log("Error parsing result: ", validated.error)
|
||||
continue
|
||||
}
|
||||
|
||||
var parsedresult = validated.result
|
||||
console.log("PARSED RES: ", parsedresult)
|
||||
if (parsedresult?.decisions?.length > 0) {
|
||||
var newexec = executionArgument
|
||||
if (newexec === undefined || newexec === null || Object.keys(newexec).length === 0) {
|
||||
newexec = {}
|
||||
}
|
||||
|
||||
for (var decisionkey in parsedresult?.decisions) {
|
||||
const decision = parsedresult.decisions[decisionkey]
|
||||
if (decision?.run_details?.id !== decisionId) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (var fieldkey in decision?.fields) {
|
||||
const field = decision.fields[fieldkey]
|
||||
if (field.key === "question" && !inputQuestions.find(q => q.name=== field.value)) {
|
||||
console.log("QUESTION: ", field)
|
||||
const newquestion = {
|
||||
"name": field.value,
|
||||
"value": field.key+"_"+fieldkey,
|
||||
}
|
||||
|
||||
inputQuestions.push(newquestion)
|
||||
|
||||
newexec[newquestion.value] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setInputQuestions([...inputQuestions] )
|
||||
console.log("EXEC: ", newexec)
|
||||
setExecutionArgument(newexec)
|
||||
|
||||
responseJson.workflow.input_questions = inputQuestions
|
||||
setWorkflow(responseJson?.workflow)
|
||||
setDisableButtons(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1031,12 +1127,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
localStorage.setItem(storageKey, JSON.stringify(value))
|
||||
}
|
||||
|
||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0) {
|
||||
if (realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown?.length > 0) {
|
||||
const newmarkdown = realtimeMarkdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
|
||||
} else if (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) {
|
||||
const newmarkdown = workflow.form_control.input_markdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
||||
} else if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) {
|
||||
const newmarkdown = workflow?.form_control?.input_markdown.replace(`{{ ${responseJson.workflow.id} }}`, responseJson.result, -1)
|
||||
setRealtimeMarkdown(newmarkdown)
|
||||
}
|
||||
|
||||
@@ -1072,7 +1168,6 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
getWorkflow(props.match.params.key, sourceNode)
|
||||
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null) {
|
||||
console.log("Get execution: ", execution_id)
|
||||
fetchUpdates(execution_id, authorization, true)
|
||||
}
|
||||
|
||||
@@ -1136,13 +1231,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
const buttonStyle = {borderRadius: 25, height: 50, fontSize: 18, backgroundImage: handleValidateForm(executionArgument) || executionLoading ? buttonBackground : "grey", color: "white"}
|
||||
|
||||
// Check if all fields are filled in?
|
||||
var disabledButtons = executionLoading || executionRunning || message.length > 0
|
||||
var disabledButtons = executionLoading || executionRunning || message.length > 0 || disableButtons
|
||||
if (disabledButtons === false && workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0) {
|
||||
// Check field values
|
||||
//disabledButtons = handleValidateForm(executionArgument)
|
||||
}
|
||||
|
||||
const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : "Unknown"
|
||||
const organization = selectedOrganization !== undefined && selectedOrganization !== null ? selectedOrganization.name : ""
|
||||
const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.org !== undefined && selectedOrganization.org !== null? selectedOrganization.org : "support@shuffler.io"
|
||||
//const contact = selectedOrganization !== undefined && selectedOrganization !== null && selectedOrganization.contact !== undefined && selectedOrganization.contact !== null? selectedOrganization.contact : "support@shuffler.io"
|
||||
|
||||
@@ -1321,12 +1416,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
<div style={{paddingTop: 150, marginTop: 150, width: 250, itemAlign: "center", textAlign: "center", margin: "auto", }}>
|
||||
<CircularProgress />
|
||||
<Typography variant="body1" style={{marginTop: 20, }}>
|
||||
Loading Form Details...
|
||||
Loading Details...
|
||||
</Typography>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
{workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ?
|
||||
{workflowQuestion !== "" || (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) ?
|
||||
<div style={{marginBottom: 20, }}>
|
||||
<Markdown
|
||||
components={{
|
||||
@@ -1342,13 +1437,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
}}
|
||||
rehypePlugins={[rehypeRaw]}
|
||||
>
|
||||
{workflowQuestion !== "" ? workflowQuestion : realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow.form_control.input_markdown}
|
||||
{workflowQuestion !== "" ? workflowQuestion : realtimeMarkdown !== undefined && realtimeMarkdown !== null && realtimeMarkdown.length > 0 ? realtimeMarkdown : workflow?.form_control?.input_markdown}
|
||||
</Markdown>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<form onSubmit={(e) => {onSubmit(e)}} style={{margin: "25px 0px 15px 0px",}}>
|
||||
{workflowQuestion !== "" || (workflow.form_control.input_markdown !== undefined && workflow.form_control.input_markdown !== null && workflow.form_control.input_markdown.length > 0) ? null :
|
||||
{workflowQuestion !== "" || (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown.length > 0) ? null :
|
||||
<div>
|
||||
{/*
|
||||
<img
|
||||
@@ -1370,10 +1465,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
<Typography variant="h6" style={{marginBottom: 10, marginTop: 50, textAlign: "center", }}>
|
||||
{organization}
|
||||
</Typography>
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
{organization?.length > 0 &&
|
||||
<Divider style={{marginTop: 20, marginBottom: 20, }}/>
|
||||
}
|
||||
|
||||
{disabledButtons && message.length > 0 ? null :
|
||||
<Typography color="textSecondary" style={{textAlign: "center", }}>
|
||||
<Typography color="textSecondary" style={{textAlign: "center", marginTop: 15, }}>
|
||||
{message}
|
||||
</Typography>
|
||||
}
|
||||
@@ -1412,6 +1509,11 @@ const RunWorkflow = (defaultprops) => {
|
||||
executionArgument[multiChoiceOptions[0]] = multiChoiceOptions[1]
|
||||
}
|
||||
|
||||
const parsedLabel = question?.value?.startsWith("question_") ?
|
||||
""
|
||||
:
|
||||
question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)
|
||||
|
||||
return (
|
||||
<div style={{marginBottom: 10}} key={index}>
|
||||
|
||||
@@ -1457,7 +1559,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
marginTop: 5,
|
||||
}}
|
||||
label={question?.value?.charAt(0)?.toUpperCase() + question?.value?.slice(1)}
|
||||
label={parsedLabel}
|
||||
required
|
||||
|
||||
disabled={disabledButtons}
|
||||
@@ -1542,7 +1644,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
:
|
||||
<Fade in={true} timeout={2500}>
|
||||
<Typography variant="body1" style={{textAlign: "center", marginTop: 30, marginBottom: 20, }}>
|
||||
{disabledButtons ? "Already answered. You may close this window." : ""}
|
||||
{disabledButtons ? "Question answered. You may close this window." : ""}
|
||||
</Typography>
|
||||
</Fade>
|
||||
}
|
||||
@@ -1565,10 +1667,13 @@ const RunWorkflow = (defaultprops) => {
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
setButtonClicked("FINISHED")
|
||||
setExecutionData({
|
||||
status: "FINISHED",
|
||||
})
|
||||
// Timeout 2500 just in case
|
||||
setTimeout(() => {
|
||||
setButtonClicked("FINISHED")
|
||||
setExecutionData({
|
||||
status: "FINISHED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, true)
|
||||
}}>
|
||||
@@ -1586,16 +1691,24 @@ const RunWorkflow = (defaultprops) => {
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
}} onClick={() => {
|
||||
setButtonClicked("ABORTED")
|
||||
setExecutionData({
|
||||
status: "ABORTED",
|
||||
})
|
||||
setTimeout(() => {
|
||||
setButtonClicked("ABORTED")
|
||||
setExecutionData({
|
||||
status: "ABORTED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, false)
|
||||
}}>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{handleValidateForm(executionArgument) === false && disabledButtons === false ?
|
||||
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, underline: "1px solid grey", }}>
|
||||
All required questions have not been answered yet.
|
||||
</Typography>
|
||||
: null}
|
||||
</span>
|
||||
:
|
||||
<div style={{display: "flex", marginTop: "15px"}}>
|
||||
|
||||
@@ -477,7 +477,7 @@ export const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
//var newitem = JSON.parse(base);
|
||||
var newitem = validateJson(base).result
|
||||
|
||||
var to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_");
|
||||
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
|
||||
for (let copykey in copy.namespace) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
@@ -742,7 +742,7 @@ const DropzoneWrapper = memo(({ onDrop, WorkflowView }) => {
|
||||
const Workflows = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata, checkLogin } = props;
|
||||
|
||||
document.title = "Shuffle - Workflows";
|
||||
document.title = "Workflows - Shuffle";
|
||||
let navigate = useNavigate();
|
||||
|
||||
const classes = useStyles(theme)
|
||||
|
||||
+443
-330
@@ -4,16 +4,6 @@ import { useLocation, useNavigate, Link } from "react-router-dom";
|
||||
import ReactDOM from "react-dom"
|
||||
import { getTheme } from "../theme.jsx";
|
||||
|
||||
// Material UI Icons
|
||||
import Add from '@mui/icons-material/Add';
|
||||
import Search from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import QueryStatsIcon from '@mui/icons-material/QueryStats';
|
||||
import GridOnIcon from '@mui/icons-material/GridOn';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import PublishIcon from '@mui/icons-material/Publish';
|
||||
import GetAppIcon from '@mui/icons-material/GetApp';
|
||||
|
||||
// Material UI & Components
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { Navigate } from "react-router-dom";
|
||||
@@ -67,6 +57,7 @@ import {
|
||||
|
||||
// Material UI Icons
|
||||
import {
|
||||
ContentCopy as ContentCopyIcon,
|
||||
Close as CloseIcon,
|
||||
Compare as CompareIcon,
|
||||
Maximize as MaximizeIcon,
|
||||
@@ -105,6 +96,12 @@ import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
BarChart as BarChartIcon,
|
||||
Lock as LockIcon,
|
||||
Clear as ClearIcon,
|
||||
QueryStats as QueryStatsIcon,
|
||||
GridOn as GridOnIcon,
|
||||
List as ListIcon,
|
||||
Publish as PublishIcon,
|
||||
GetApp as GetAppIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
// Additional Components
|
||||
@@ -209,10 +206,10 @@ export const GetIconInfo = (action) => {
|
||||
key: "compare",
|
||||
values: ["compare", "convert", "to", "filter", "translate", "parse"],
|
||||
},
|
||||
{ key: "assets", values: ["cmdb", "assets", "asset", "cmdb", "inventory", "host", "hosts", "device", "devices"] },
|
||||
{ key: "assets", values: ["cmdb", "assets", "asset", "cmdb", "inventory", "host", "hosts", "device", "devices", "app",] },
|
||||
{ key: "close", values: ["close", "stop", "cancel", "block"] },
|
||||
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
|
||||
{ key: "eradication", values: ["eradication", "edr", "xdr"] },
|
||||
{ key: "eradication", values: ["eradication", "edr", "xdr", "sigma", "yara",] },
|
||||
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] },
|
||||
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] },
|
||||
{ key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
|
||||
@@ -235,6 +232,7 @@ export const GetIconInfo = (action) => {
|
||||
values: [
|
||||
"api",
|
||||
"password",
|
||||
"passwd",
|
||||
"protect",
|
||||
],
|
||||
}
|
||||
@@ -835,7 +833,9 @@ const Workflows2 = (props) => {
|
||||
setCurrTab(1);
|
||||
} else if (tabParam === 'all_workflows' && currTab !== 2) {
|
||||
setCurrTab(2);
|
||||
}
|
||||
} else if (tabParam === 'background_processes' && currTab !== 4) {
|
||||
setCurrTab(4);
|
||||
}
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
@@ -853,10 +853,15 @@ const Workflows2 = (props) => {
|
||||
1: 'my_workflows',
|
||||
2: 'all_workflows',
|
||||
3: 'backup_apps',
|
||||
4: 'background_processes',
|
||||
};
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
queryParams.set('tab', tabMapping[newValue]);
|
||||
|
||||
if (newValue === 4) {
|
||||
setShowExecutionStats(true)
|
||||
setView("grid")
|
||||
}
|
||||
|
||||
navigate(`${location.pathname}?${queryParams.toString()}`);
|
||||
};
|
||||
@@ -1553,7 +1558,7 @@ const Workflows2 = (props) => {
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
minWidth: '440px',
|
||||
minWidth: 440,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
@@ -1566,11 +1571,11 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<DialogTitle style={{padding: 50, }}>
|
||||
<div style={{ textAlign: "center", color: theme.palette.DialogStyle?.color }}>
|
||||
Are you sure you want to delete {selectedWorkflowId.length > 0 ? filteredWorkflows.find((w) => w.id === selectedWorkflowId)?.name : `${selectedWorkflowIndexes.length} workflow${selectedWorkflowIndexes.length === 1 ? '' : 's'}`}? <div />
|
||||
|
||||
Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working
|
||||
Other workflows relying on {selectedWorkflowIndexes.length > 0 ? "them" : "it"} one will stop working.
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent
|
||||
@@ -1819,6 +1824,7 @@ const Workflows2 = (props) => {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setIsLoadingWorkflow(false)
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
|
||||
@@ -1956,6 +1962,7 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoadingWorkflow(false)
|
||||
toast(error.toString());
|
||||
});
|
||||
}
|
||||
@@ -2949,6 +2956,8 @@ const Workflows2 = (props) => {
|
||||
triggerfound = true
|
||||
image = wfTriggers[0].large_image
|
||||
|
||||
trigger.status = trigger?.status?.toLowerCase()
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
@@ -2962,6 +2971,8 @@ const Workflows2 = (props) => {
|
||||
triggerfound = true
|
||||
image = wfTriggers[1].large_image
|
||||
|
||||
trigger.status = trigger?.status?.toLowerCase()
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
@@ -3034,10 +3045,11 @@ const Workflows2 = (props) => {
|
||||
|
||||
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
|
||||
return (
|
||||
<div style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily }}>
|
||||
<div
|
||||
id={`workflowbox-${data.id}`}
|
||||
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily }}>
|
||||
|
||||
<Paper square style={paperAppStyle}>
|
||||
|
||||
{selectedCategory !== "" ?
|
||||
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
|
||||
<div
|
||||
@@ -3058,7 +3070,7 @@ const Workflows2 = (props) => {
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
: null}
|
||||
: null}
|
||||
|
||||
<Grid
|
||||
item
|
||||
@@ -3067,7 +3079,6 @@ const Workflows2 = (props) => {
|
||||
<Grid item style={{ display: "flex", maxHeight: 34 }}>
|
||||
{currTab === 2 ? null :
|
||||
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
|
||||
|
||||
<div
|
||||
style={{ cursor: "" }}
|
||||
onClick={() => {
|
||||
@@ -3183,6 +3194,7 @@ const Workflows2 = (props) => {
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item style={workflowActionStyle}>
|
||||
{appGroup.length > 0 ?
|
||||
<div style={{ display: "flex", marginTop: 8, }}>
|
||||
@@ -3437,7 +3449,7 @@ const Workflows2 = (props) => {
|
||||
</Grid>
|
||||
|
||||
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
|
||||
<div style={{ margin: "40px 10px 0px 10px", paddingTop: 0, borderTop: "1px solid rgba(255,255,255,0.3)", }}>
|
||||
<div style={{ margin: "40px 10px 0px 10px", paddingTop: 0, borderTop: `1px solid ${theme.palette.text.secondary}`, zoom: 1.4 }}>
|
||||
<LineChartWrapper
|
||||
inputname={""}
|
||||
keys={foundTimeline?.timeline}
|
||||
@@ -4974,331 +4986,334 @@ const Workflows2 = (props) => {
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, marginBottom: 20, paddingRight: 25, minHeight: 47 }}>
|
||||
{currTab === 4 ? null :
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, marginBottom: 20, paddingRight: 25, minHeight: 47 }}>
|
||||
|
||||
{currTab === 2 ? (
|
||||
<CustomSearchBox
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
) : (
|
||||
<MuiChipsInput
|
||||
style={{
|
||||
width: "25%",
|
||||
maxWidth: "25%",
|
||||
minWidth: "25%",
|
||||
height: 43,
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
zIndex: 1000,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: "fit-content",
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
placeholder: "Filter Workflows",
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <SearchIcon style={{ color: 'white', paddingRight: 5 }} />
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
onKeyDown: (e) => {
|
||||
// Prevent default behavior for Enter and Backspace
|
||||
if (e.key === 'Enter' || e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}
|
||||
},
|
||||
}}
|
||||
clearInputOnBlur={false}
|
||||
sx={{
|
||||
// Container styling
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: "fit-content",
|
||||
borderRadius: '4px',
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
},
|
||||
{currTab === 2 ? (
|
||||
<CustomSearchBox
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
) :
|
||||
(
|
||||
<MuiChipsInput
|
||||
style={{
|
||||
width: "25%",
|
||||
maxWidth: "25%",
|
||||
minWidth: "25%",
|
||||
height: 43,
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
zIndex: 1000,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: "fit-content",
|
||||
maxHeight: "fit-content",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
placeholder: "Filter Workflows",
|
||||
// endAdornment: (
|
||||
// <InputAdornment position="end">
|
||||
// <SearchIcon style={{ color: 'white', paddingRight: 5 }} />
|
||||
// </InputAdornment>
|
||||
// ),
|
||||
onKeyDown: (e) => {
|
||||
// Prevent default behavior for Enter and Backspace
|
||||
if (e.key === 'Enter' || e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.target.focus();
|
||||
}
|
||||
},
|
||||
}}
|
||||
clearInputOnBlur={false}
|
||||
sx={{
|
||||
// Container styling
|
||||
'& .MuiOutlinedInput-root': {
|
||||
height: "fit-content",
|
||||
borderRadius: '4px',
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
'&:hover fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.4)',
|
||||
},
|
||||
},
|
||||
|
||||
// Adjust chip container to center vertically
|
||||
'& .MuiInputBase-root': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
fontSize: 18,
|
||||
padding: '4px 8px',
|
||||
alignItems: 'center',
|
||||
height: "fit-content", // Match height
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
// Adjust chip container to center vertically
|
||||
'& .MuiInputBase-root': {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '4px',
|
||||
fontSize: 18,
|
||||
padding: '4px 8px',
|
||||
alignItems: 'center',
|
||||
height: "fit-content", // Match height
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color
|
||||
},
|
||||
|
||||
// Rest of the styling remains the same...
|
||||
}}
|
||||
value={filters}
|
||||
onChange={(chips) => {
|
||||
setFilters(chips);
|
||||
const remainingCategories = chips.map(chip => {
|
||||
const match = chip.match(/\d+\.\s+(\w+)/i);
|
||||
return match ? match[1] : chip;
|
||||
}).filter(category => {
|
||||
return usecases.some(usecase =>
|
||||
usecase.name.toLowerCase().includes(category.toLowerCase())
|
||||
);
|
||||
});
|
||||
// Rest of the styling remains the same...
|
||||
}}
|
||||
value={filters}
|
||||
onChange={(chips) => {
|
||||
setFilters(chips);
|
||||
const remainingCategories = chips.map(chip => {
|
||||
const match = chip.match(/\d+\.\s+(\w+)/i);
|
||||
return match ? match[1] : chip;
|
||||
}).filter(category => {
|
||||
return usecases.some(usecase =>
|
||||
usecase.name.toLowerCase().includes(category.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
setSelectedCategory(remainingCategories);
|
||||
findWorkflow(chips);
|
||||
setSelectedCategory(remainingCategories);
|
||||
findWorkflow(chips);
|
||||
|
||||
}}
|
||||
//onAdd={(chip) => {
|
||||
// console.log("ADd: ", chip);
|
||||
// addFilter(chip);
|
||||
//}}
|
||||
//onDelete={(_, index) => {
|
||||
// console.log("Remove: ", index);
|
||||
// removeFilter(index);
|
||||
//}}
|
||||
/>
|
||||
)}
|
||||
}}
|
||||
//onAdd={(chip) => {
|
||||
// console.log("ADd: ", chip);
|
||||
// addFilter(chip);
|
||||
//}}
|
||||
//onDelete={(_, index) => {
|
||||
// console.log("Remove: ", index);
|
||||
// removeFilter(index);
|
||||
//}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
currTab !== 2 && (
|
||||
<Select
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
displayEmpty
|
||||
disabled={currTab === 2}
|
||||
multiple
|
||||
style={{
|
||||
width: "25%",
|
||||
minWidth: "25%",
|
||||
maxWidth: "25%",
|
||||
height: 47,
|
||||
borderRadius: 4,
|
||||
fontSize: 18,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(selected) => selected.length ? selected.join(', ') : 'All Categories'}
|
||||
>
|
||||
<MenuItem disabled value="">
|
||||
All Categories
|
||||
</MenuItem>
|
||||
{usecases.map((usecase, index) => {
|
||||
if (usecase?.name === "5. Verify") {
|
||||
return null;
|
||||
}
|
||||
{
|
||||
currTab !== 2 && (
|
||||
<Select
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={selectedCategory}
|
||||
onChange={handleCategoryChange}
|
||||
displayEmpty
|
||||
disabled={currTab === 2}
|
||||
multiple
|
||||
style={{
|
||||
width: "25%",
|
||||
minWidth: "25%",
|
||||
maxWidth: "25%",
|
||||
height: 47,
|
||||
borderRadius: 4,
|
||||
fontSize: 18,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': {
|
||||
borderColor: 'rgba(255, 255, 255, 0.23)',
|
||||
},
|
||||
},
|
||||
}}
|
||||
renderValue={(selected) => selected.length ? selected.join(', ') : 'All Categories'}
|
||||
>
|
||||
<MenuItem disabled value="">
|
||||
All Categories
|
||||
</MenuItem>
|
||||
{usecases.map((usecase, index) => {
|
||||
if (usecase?.name === "5. Verify") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentDone = usecase.matches.length > 0 ? parseInt(usecase.matches.length / usecase.list.length * 100) : 0
|
||||
if (percentDone === 0) {
|
||||
usecase = findMatches(usecase, workflows)
|
||||
}
|
||||
const percentDone = usecase.matches.length > 0 ? parseInt(usecase.matches.length / usecase.list.length * 100) : 0
|
||||
if (percentDone === 0) {
|
||||
usecase = findMatches(usecase, workflows)
|
||||
}
|
||||
|
||||
const category = usecase?.name.split(" ")[1]
|
||||
return (
|
||||
<MenuItem
|
||||
value={category}
|
||||
onClick={() => {
|
||||
if (!filters.includes(usecase?.name.toLowerCase())) {
|
||||
addFilter(usecase.name)
|
||||
} else {
|
||||
removeFilter(filters.indexOf(usecase?.name.toLowerCase()))
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
padding: "12px 16px",
|
||||
borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255,255,255,0.1)"
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: "12px"
|
||||
}}>
|
||||
<Checkbox
|
||||
checked={selectedCategory.includes(category)}
|
||||
style={{
|
||||
padding: 0,
|
||||
marginRight: 8,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
fontWeight: selectedCategory.includes(category) ? 500 : 400
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
padding: "2px 8px",
|
||||
borderRadius: "12px",
|
||||
fontSize: "0.75rem"
|
||||
}}
|
||||
>
|
||||
{usecase?.matches.length}/{usecase?.list.length}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 2 && (
|
||||
<CustomCategoryDropdown attribute="usecase_ids" limit={20} />
|
||||
)
|
||||
}
|
||||
const category = usecase?.name.split(" ")[1]
|
||||
return (
|
||||
<MenuItem
|
||||
value={category}
|
||||
onClick={() => {
|
||||
if (!filters.includes(usecase?.name.toLowerCase())) {
|
||||
addFilter(usecase.name)
|
||||
} else {
|
||||
removeFilter(filters.indexOf(usecase?.name.toLowerCase()))
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
padding: "12px 16px",
|
||||
borderBottom: index === usecases.length - 2 ? "none" : "1px solid rgba(255,255,255,0.05)",
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255,255,255,0.1)"
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: "12px"
|
||||
}}>
|
||||
<Checkbox
|
||||
checked={selectedCategory.includes(category)}
|
||||
style={{
|
||||
padding: 0,
|
||||
marginRight: 8,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%"
|
||||
}}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
fontWeight: selectedCategory.includes(category) ? 500 : 400
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
padding: "2px 8px",
|
||||
borderRadius: "12px",
|
||||
fontSize: "0.75rem"
|
||||
}}
|
||||
>
|
||||
{usecase?.matches.length}/{usecase?.list.length}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
{
|
||||
currTab === 2 && (
|
||||
<CustomCategoryDropdown attribute="usecase_ids" limit={20} />
|
||||
)
|
||||
}
|
||||
|
||||
<div style={{ width: "50%", minWidth: "50%", maxWidth: "50%", height: 47, display: "flex", gap: 5 }}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
justifyContent: "space-around",
|
||||
flex: 0.7,
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
gap: 4
|
||||
}}>
|
||||
<div style={{ width: "50%", minWidth: "50%", maxWidth: "50%", height: 47, display: "flex", gap: 5 }}>
|
||||
<div style={{
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
justifyContent: "space-around",
|
||||
flex: 0.7,
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
gap: 4
|
||||
}}>
|
||||
|
||||
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}}
|
||||
onClick={() => {
|
||||
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showExecutionStats ? "#1a1a1a" : theme.palette.text.primary, background: showExecutionStats ? theme.palette.primary.main : theme.palette.platformColor}}
|
||||
onClick={() => {
|
||||
|
||||
const newView = !showExecutionStats
|
||||
localStorage.setItem("showExecutionStats", newView)
|
||||
setShowExecutionStats(!showExecutionStats)
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<BarChartIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
const newView = !showExecutionStats
|
||||
localStorage.setItem("showExecutionStats", newView)
|
||||
setShowExecutionStats(!showExecutionStats)
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<BarChartIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => navigate("/workflows/debug")}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<QueryStatsIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => navigate("/workflows/debug")}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<QueryStatsIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => {
|
||||
const newView = view === "grid" ? "list" : "grid";
|
||||
localStorage.setItem("workflowView", newView);
|
||||
setView(newView);
|
||||
<Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => {
|
||||
const newView = view === "grid" ? "list" : "grid";
|
||||
localStorage.setItem("workflowView", newView);
|
||||
setView(newView);
|
||||
|
||||
if (view === "grid") {
|
||||
setCurrTab(0)
|
||||
}
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{view === "grid" ?
|
||||
<ListIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} /> :
|
||||
<GridOnIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
if (view === "grid") {
|
||||
setCurrTab(0)
|
||||
}
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{view === "grid" ?
|
||||
<ListIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} /> :
|
||||
<GridOnIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1 }} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Import workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => upload.click()}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{submitLoading ?
|
||||
<CircularProgress color="secondary" /> :
|
||||
<PublishIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Import workflows" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
onClick={() => upload.click()}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
{submitLoading ?
|
||||
<CircularProgress color="secondary" /> :
|
||||
<PublishIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple="multiple"
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={importFiles}
|
||||
/>
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple="multiple"
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={importFiles}
|
||||
/>
|
||||
|
||||
<Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top">
|
||||
<IconButton
|
||||
style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
|
||||
disabled={isCloud || currTab === 2}
|
||||
onClick={() => exportAllWorkflows(workflows)}
|
||||
>
|
||||
<GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateWorkflow}
|
||||
id="create_workflow_button"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
flex: 0.8,
|
||||
textTransform: 'none',
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: 500
|
||||
}}
|
||||
startIcon={<Add/>}
|
||||
>
|
||||
Create Workflow
|
||||
</Button>
|
||||
</div>
|
||||
<Tooltip title={`Download ALL workflows (${workflows.length})`} placement="top">
|
||||
<IconButton
|
||||
style={(isCloud || currTab === 2) ? iconButtonDisabledStyle : { ...iconButtonStyle, cursor: "pointer" }}
|
||||
disabled={isCloud || currTab === 2}
|
||||
onClick={() => exportAllWorkflows(workflows)}
|
||||
>
|
||||
<GetAppIcon style={{ color: theme.palette.text.primary, opacity: currTab === 2 ? 0.5 : 1}} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateWorkflow}
|
||||
id="create_workflow_button"
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
flex: 0.8,
|
||||
textTransform: 'none',
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
fontSize: 16,
|
||||
fontWeight: 500
|
||||
}}
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Create Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
<div style={{
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
@@ -5310,8 +5325,106 @@ const Workflows2 = (props) => {
|
||||
) : (
|
||||
view === "grid" && currTab !== 2 ? (
|
||||
<>
|
||||
<div style={{
|
||||
marginTop: 16,
|
||||
{currTab === 4 && backgroundWorkflows.map((data, index) => {
|
||||
if (data.triggers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
var foundWebhook = ""
|
||||
var foundtrigger = {}
|
||||
for (var triggerKey in data.triggers) {
|
||||
if (data.triggers[triggerKey].trigger_type === "WEBHOOK") {
|
||||
foundWebhook = `${globalUrl}/api/v1/hooks/webhook_${data.triggers[triggerKey].id}`
|
||||
foundtrigger = data.triggers[triggerKey]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundWebhook === "") {
|
||||
return null
|
||||
}
|
||||
|
||||
var webhookName = ``
|
||||
if (data?.name?.toLowerCase().includes("ingest tickets")) {
|
||||
webhookName = "Send your Tickets, Alerts, Cases and Detections here. This will ingest them into Shuffle."
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{padding: 50, }}
|
||||
onMouseEnter={() => {
|
||||
// Find the relevant workflow paper and highlight it
|
||||
const foundElement = document.getElementById(`workflowbox-${data.id}`)
|
||||
if (foundElement) {
|
||||
foundElement.style.border = `3px solid ${theme.palette.primary.main}`
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
const foundElement = document.getElementById(`workflowbox-${data.id}`)
|
||||
if (foundElement) {
|
||||
foundElement.style.border = null
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" style={{ marginBottom: 10, fontFamily: theme.typography?.fontFamily, display: "flex", alignItems: "center", gap: 10 }}>
|
||||
{webhookName}
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
value={foundWebhook}
|
||||
readOnly
|
||||
fullWidth
|
||||
disabled
|
||||
|
||||
// Add start adornment with webhook icon
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<img
|
||||
alt="webhook"
|
||||
src={wfTriggers[0].large_image}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
marginRight: 20,
|
||||
border: foundtrigger.status === "Running" || foundtrigger.status === "running" ? `2px solid ${green}` : `2px solid ${red}`,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tooltip title={"Copy to clipboard"} placement="top">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (navigator.clipboard === undefined) {
|
||||
toast("Your browser doesn't support clipboard copying, please copy manually.", { type: "error" });
|
||||
} else {
|
||||
navigator.clipboard.writeText(foundWebhook);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
marginRight: 10,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
id="copy_webhook_url_button"
|
||||
>
|
||||
<ContentCopyIcon style={{ color: theme.palette.textFieldStyle.color }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</InputAdornment>
|
||||
),
|
||||
style: {
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<div style={{
|
||||
marginTop: 32,
|
||||
width: "100%",
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(365px, 1fr))",
|
||||
|
||||
Reference in New Issue
Block a user