Tons of minor fixes to the based on cloud tests
This commit is contained in:
@@ -1765,7 +1765,17 @@ If you're interested, please let me know a time that works for you, or set up a
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 2000);
|
||||
toast("Successfully changed active organization - refreshing!");
|
||||
|
||||
toast.success("Successfully changed active organization - refreshing!");
|
||||
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
|
||||
navigate(`/admin?org_id=${responseJson.org_id}`)
|
||||
} else {
|
||||
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
|
||||
navigate(`/admin`)
|
||||
} else {
|
||||
toast("No pivot?")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
|
||||
@@ -2,19 +2,23 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import AdminNavBar from '../components/AdminNavBar.jsx';
|
||||
import { toast } from "react-toastify";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
|
||||
|
||||
const Admin2 = (props) => {
|
||||
// Destructure props if needed
|
||||
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, isLoggedIn} = props;
|
||||
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, } = props;
|
||||
const [selectedTab, setSelectedTab] = useState('editdetails');
|
||||
const [selectedStatus, setSelectedStatus] = React.useState([]);
|
||||
const [selectedOrganization, setSelectedOrganization] = useState({});
|
||||
const [organizationFeatures, setOrganizationFeatures] = useState({});
|
||||
const [orgRequest, setOrgRequest] = React.useState(true);
|
||||
const [isOrgLoaded, setIsOrgLoaded] = React.useState(false);
|
||||
const {brandName} = useContext(Context)
|
||||
const {brandName, updateOrg, setUpdateOrg} = useContext(Context)
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
if (document !== undefined) {
|
||||
if (selectedOrganization?.name !== undefined) {
|
||||
document.title = brandName?.length > 0 ? selectedOrganization?.name + ` - Admin - ${brandName}` : selectedOrganization?.name + ` - Admin - Shuffle`;
|
||||
@@ -24,6 +28,9 @@ const Admin2 = (props) => {
|
||||
}
|
||||
|
||||
const handleGetOrg = (orgId) => {
|
||||
if (orgId === undefined || orgId === null || orgId.length !== 36) {
|
||||
return
|
||||
}
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
|
||||
method: "GET",
|
||||
@@ -40,13 +47,13 @@ const Admin2 = (props) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast(
|
||||
"Failed getting your org. If this persists, please contact support. Redirecting to workflows...",
|
||||
);
|
||||
toast.warn("Failed getting your org. If this persists, please contact support. Redirecting to workflows...")
|
||||
setTimeout(() => {
|
||||
window.location.href = "/workflows";
|
||||
}, 3000);
|
||||
} else {
|
||||
|
||||
setUpdateOrg(false);
|
||||
if (
|
||||
responseJson.sync_features === undefined ||
|
||||
responseJson.sync_features === null
|
||||
@@ -163,6 +170,13 @@ const Admin2 = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (updateOrg && userdata?.active_org?.id !== undefined && userdata?.active_org?.id !== null && userdata?.active_org?.id.length > 0) {
|
||||
handleGetOrg(userdata.active_org.id);
|
||||
setUpdateOrg(false);
|
||||
}
|
||||
|
||||
}, [updateOrg]);
|
||||
|
||||
useEffect(() => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
@@ -216,16 +230,26 @@ const Admin2 = (props) => {
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 3000);
|
||||
toast("Successfully changed active organization - refreshing!");
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
toast("Failed changing org: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
toast("Failed changing org")
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Successfully changed active organization - refreshing!");
|
||||
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
|
||||
navigate(`/admin?org_id=${responseJson.org_id}`)
|
||||
} else {
|
||||
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
|
||||
navigate(`/admin`)
|
||||
} else {
|
||||
toast("No pivot?")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
if (!responseJson.reason.includes("already")) {
|
||||
toast("Failed changing org: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
toast("Failed changing org")
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error changing: ", error);
|
||||
|
||||
+345
-67
@@ -6,7 +6,11 @@ import { getTheme } from "../theme.jsx";
|
||||
import { toast } from "react-toastify"
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { v4 as uuidv4} from "uuid";
|
||||
import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows.jsx";
|
||||
import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows2.jsx";
|
||||
import AppSearch from "../components/Appsearch.jsx";
|
||||
import Markdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Paragraph, Blockquote, CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
|
||||
import {
|
||||
Box,
|
||||
@@ -18,6 +22,8 @@ import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
TextField,
|
||||
Popover,
|
||||
Divider,
|
||||
} from '@mui/material'
|
||||
|
||||
import {
|
||||
@@ -32,10 +38,13 @@ import {
|
||||
Close as CloseIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Refresh as RefreshIcon,
|
||||
Add as AddIcon,
|
||||
Warning as WarningIcon,
|
||||
} from '@mui/icons-material'
|
||||
|
||||
import {
|
||||
green,
|
||||
yellow,
|
||||
red,
|
||||
} from '../views/AngularWorkflow.jsx'
|
||||
|
||||
@@ -55,11 +64,35 @@ const AgentUI = (props) => {
|
||||
const [actionInput, setActionInput] = useState("")
|
||||
const [questionAnswers, setQuestionAnswers] = useState({})
|
||||
|
||||
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
||||
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
|
||||
const [chosenApps, setChosenApps] = useState([])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
|
||||
return
|
||||
}
|
||||
|
||||
setNewSelectedApp({})
|
||||
setAppPickerAnchor(null)
|
||||
if (chosenApps.findIndex((app) => app.id === newSelectedApp.objectID) !== -1) {
|
||||
} else {
|
||||
setChosenApps(chosenApps.concat([{
|
||||
name: newSelectedApp.name,
|
||||
id: newSelectedApp.objectID,
|
||||
image: newSelectedApp.image_url,
|
||||
}]))
|
||||
}
|
||||
}, [newSelectedApp])
|
||||
|
||||
const {themeMode} = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
const navigate = useNavigate();
|
||||
|
||||
document.title = "Shuffle AI Agents"
|
||||
if (document !== undefined && document !== null && !document?.title?.includes("Agent")) {
|
||||
document.title = "Shuffle AI Agents"
|
||||
}
|
||||
|
||||
const agentWrapperStyle = {
|
||||
width: 1000,
|
||||
@@ -79,6 +112,44 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const Heading = (props) => {
|
||||
const element = React.createElement(
|
||||
`h${props.level}`,
|
||||
{ style: { marginTop: 40 } },
|
||||
props.children
|
||||
);
|
||||
|
||||
return (
|
||||
<Typography>
|
||||
{props.level !== 1 ? (
|
||||
<Divider
|
||||
style={{
|
||||
width: "90%",
|
||||
marginTop: 40,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{element}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const markdownComponents = {
|
||||
img: Img,
|
||||
code: CodeHandler,
|
||||
h1: Heading,
|
||||
h2: Heading,
|
||||
h3: Heading,
|
||||
h4: Heading,
|
||||
h5: Heading,
|
||||
h6: Heading,
|
||||
a: OuterLink,
|
||||
p: Paragraph,
|
||||
blockquote: Blockquote,
|
||||
}
|
||||
|
||||
const findNodeData = (execution_data, node_id) => {
|
||||
if (execution_data === undefined || execution_data === null) {
|
||||
return
|
||||
@@ -311,7 +382,73 @@ const AgentUI = (props) => {
|
||||
getAppAuth()
|
||||
}, [])
|
||||
|
||||
const maxTimelineWidth = 300
|
||||
const maxTimelineWidth = 380
|
||||
|
||||
const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
|
||||
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
|
||||
}
|
||||
|
||||
var newArgument = {}
|
||||
if (isContinuation === true) {
|
||||
// Just a single answer
|
||||
for (var key in questionAnswers) {
|
||||
const answer = questionAnswers[key]
|
||||
newArgument[key] = answer
|
||||
}
|
||||
|
||||
if (Object.keys(newArgument).length === 0) {
|
||||
toast.error("No answers details. Cannot submit the answer.")
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
for (var key in questionAnswers) {
|
||||
const answer = questionAnswers[key]
|
||||
if (isContinuation === true) {
|
||||
newArgument["question_"+(answer.index)] = answer.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setAgentRequestLoading(true)
|
||||
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}`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setAgentRequestLoading(false)
|
||||
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) => {
|
||||
setAgentRequestLoading(false)
|
||||
toast.error("Problem with submitting: " + error)
|
||||
})
|
||||
}
|
||||
|
||||
var latestEndTime = 0
|
||||
var originalStartTime = 0
|
||||
@@ -332,6 +469,11 @@ const AgentUI = (props) => {
|
||||
<ErrorIcon style={{color: red, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
:
|
||||
item.status === "IGNORED" || item.status === "IGNORE" ?
|
||||
<Tooltip title={`${item.status}: Previous FAILURE before the agent was reran.`} placement="top">
|
||||
<WarningIcon style={{color: yellow, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={`Not started yet: ${item.status}`} placement="top">
|
||||
<HourglassDisabledIcon style={{marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
@@ -399,6 +541,10 @@ const AgentUI = (props) => {
|
||||
const defaultTopPadding = 10
|
||||
const open = openIndexes.includes(index)
|
||||
|
||||
if (item?.type === "agent" && item?.details?.original_input !== undefined) {
|
||||
document.title = "Agent: " + item?.details?.original_input?.substring(0, 50)
|
||||
}
|
||||
|
||||
var questions = []
|
||||
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||
item.type = "finalise"
|
||||
@@ -434,6 +580,10 @@ const AgentUI = (props) => {
|
||||
<Tooltip title="Ask" placement="top">
|
||||
<img src="/images/workflows/UserInput2.svg" style={categoryStyle} />
|
||||
</Tooltip>
|
||||
: item.category === "finalise" || item.category === "finish" || item.action === "finish" ?
|
||||
<Tooltip title="The action finished successfully" placement="top">
|
||||
<CheckIcon style={{color: green, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
:
|
||||
<div style={categoryStyle} />
|
||||
|
||||
@@ -487,10 +637,10 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const barColor = item.status === "FINISHED" ? green :
|
||||
const barColor = item.status === "IGNORED" ? yellow : item.status === "FINISHED" ? green :
|
||||
item.status === "FAILURE" || item.status == "ABORTED" ? red :
|
||||
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
|
||||
theme.palette.surfaceColor
|
||||
red
|
||||
|
||||
const rerunAgentButton =
|
||||
<Tooltip title="Rerun from the start with the same input" placement="right">
|
||||
@@ -501,7 +651,7 @@ const AgentUI = (props) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
toast.info("Attempting to rerun everything.")
|
||||
toast.info("Rerunning agent with the same input.")
|
||||
setDisableButtons(true)
|
||||
|
||||
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
|
||||
@@ -529,7 +679,7 @@ const AgentUI = (props) => {
|
||||
|
||||
|
||||
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">
|
||||
<Tooltip title="Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." placement="right">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item.type !== "decision" || disableButtons}
|
||||
@@ -546,56 +696,7 @@ const AgentUI = (props) => {
|
||||
<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)
|
||||
})
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -675,6 +776,7 @@ const AgentUI = (props) => {
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
paddingTop: defaultTopPadding,
|
||||
paddingBottom: defaultTopPadding,
|
||||
}}>
|
||||
{item.label}
|
||||
</div>
|
||||
@@ -746,10 +848,11 @@ const AgentUI = (props) => {
|
||||
</Tooltip>
|
||||
*/}
|
||||
|
||||
<Tooltip title="See in another window" placement="left">
|
||||
<Tooltip title="Answer in the Form UI" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
style={{marginLeft: 0, }}
|
||||
disabled={item?.details?.run_details?.status === "FINISHED"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@@ -767,6 +870,28 @@ const AgentUI = (props) => {
|
||||
:
|
||||
item.category === "agent" ?
|
||||
rerunAgentButton
|
||||
:
|
||||
item?.type === "decision" ?
|
||||
<div style={{display: "flex", }}>
|
||||
{rerunButton}
|
||||
<Tooltip title="Explore/debug execution" placement="left">
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
|
||||
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
|
||||
window.open(item?.details?.run_details?.debug_url, '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon color={barColor === red ? "primary" : "secondary"} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
:
|
||||
rerunButton
|
||||
}
|
||||
@@ -805,13 +930,25 @@ const AgentUI = (props) => {
|
||||
{questions.map((q, questionIndex) => {
|
||||
return (
|
||||
<div style={{marginTop: 25, }}>
|
||||
<Typography variant="body2">
|
||||
{`${q.question}`}
|
||||
</Typography>
|
||||
<div id="markdown_wrapper_outer" style={{cursor: "default", }}>
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
id="markdown_wrapper"
|
||||
className={"style.reactMarkdown"}
|
||||
escapeHtml={false}
|
||||
skipHtml={false}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
style={{
|
||||
maxWidth: "100%", minWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{q.question}
|
||||
</Markdown>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
label={`Question ${q.index}`}
|
||||
placeholder="No question found"
|
||||
placeholder="Your answer here"
|
||||
variant="outlined"
|
||||
style={{width: 800, marginTop: 20, }}
|
||||
multiline
|
||||
@@ -838,7 +975,7 @@ const AgentUI = (props) => {
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{marginTop: 10, }}
|
||||
style={{marginTop: 16, }}
|
||||
disabled={questionSubmitDisabled}
|
||||
onClick={() => {
|
||||
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
||||
@@ -887,6 +1024,8 @@ const AgentUI = (props) => {
|
||||
const TimelineRender = (props) => {
|
||||
const { agent_data } = props;
|
||||
|
||||
const [continuationText, setContinuationText] = useState("")
|
||||
|
||||
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
||||
const validate = validateJson(actionResult?.result)
|
||||
if (validate.valid === true) {
|
||||
@@ -938,6 +1077,7 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
var finishDecisionId = ""
|
||||
var sortedTimelineItems = []
|
||||
for (var key in agent_data?.decisions) {
|
||||
const item = agent_data.decisions[key]
|
||||
@@ -962,6 +1102,11 @@ const AgentUI = (props) => {
|
||||
|
||||
newTimelineItem.details = item
|
||||
timelineItems.push(newTimelineItem)
|
||||
|
||||
if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||
finishDecisionId = item?.run_details?.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
timelineItems.sort((a, b) => {
|
||||
@@ -1002,6 +1147,72 @@ const AgentUI = (props) => {
|
||||
)
|
||||
})}
|
||||
|
||||
{finishDecisionId !== "" ?
|
||||
<Box
|
||||
component="form"
|
||||
style={{width: "100%", textAlign: "center",}}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Uses the submitQuestion and adds more details to
|
||||
//setAgentRequestLoading ?
|
||||
submitQuestions(finishDecisionId, {
|
||||
"continue": continuationText,
|
||||
}, true)
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", maxWidth: 550, minWidth: 550, margin: "auto", marginTop: 50, }}>
|
||||
<div>
|
||||
<TextField
|
||||
label="Add more details to the current task"
|
||||
variant="outlined"
|
||||
disabled={agentRequestLoading}
|
||||
style={{width: 400, margin: "auto", }}
|
||||
multiline
|
||||
minRows={1}
|
||||
onChange={(e) => {
|
||||
console.log("Value: ", e.target.value)
|
||||
//setActionInput(e.target.value)
|
||||
//
|
||||
setContinuationText(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
agentRequestLoading ?
|
||||
<CircularProgress size={24} style={{marginRight: 10, }} />
|
||||
:
|
||||
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
|
||||
<IconButton
|
||||
type="submit"
|
||||
disabled={continuationText === ""}
|
||||
>
|
||||
<SendIcon
|
||||
color={continuationText === "" ? "disabled" : "primary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
|
||||
Any failed tasks will be set to ignored (TBD). <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
|
||||
OR
|
||||
</Typography>
|
||||
<Button
|
||||
variant={"contained"}
|
||||
color="primary"
|
||||
disabled={true}
|
||||
style={{marginTop: 10, marginLeft: 20, minWidth: 150, maxWidth: 150, height: 56, }}
|
||||
>
|
||||
Create as Workflow
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
: null}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1021,14 +1232,28 @@ const AgentUI = (props) => {
|
||||
|
||||
setAgentActionResult(null)
|
||||
|
||||
document.title = `Agent: ${inputText.substring(0, 30)}...`
|
||||
if (inputText === undefined || inputText === null || inputText === "") {
|
||||
toast.error("Please provide a valid input for the AI Agent.")
|
||||
setAgentRequestLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Run the execution. Can this be a single-action run?
|
||||
// 2. Get the execution ID and node ID from the response.
|
||||
const uuid = uuidv4()
|
||||
var parsedAction = "list_tickets,API" // Default action for now
|
||||
if (chosenApps.length > 0) {
|
||||
parsedAction = ""
|
||||
for (var appKey in chosenApps) {
|
||||
const app = chosenApps[appKey]
|
||||
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
|
||||
parsedAction += `app:${app.id}:${appname.replaceAll(",", "").replaceAll(":", "")},`
|
||||
}
|
||||
|
||||
parsedAction = parsedAction.slice(0, -1) // Remove last comma
|
||||
}
|
||||
|
||||
const data = {
|
||||
"id": uuid,
|
||||
"name":"agent",
|
||||
@@ -1049,7 +1274,7 @@ const AgentUI = (props) => {
|
||||
},
|
||||
{
|
||||
"name":"action",
|
||||
"value":"list_tickets,API"
|
||||
"value": parsedAction,
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -1080,7 +1305,7 @@ const AgentUI = (props) => {
|
||||
|
||||
}
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
const handleKeyDownRoot = (e) => {
|
||||
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
|
||||
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
|
||||
if (isCmdEnter || isCtrlEnter) {
|
||||
@@ -1089,6 +1314,11 @@ const AgentUI = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const chipStyle = {
|
||||
margin: 4,
|
||||
cursor: "pointer",
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={agentWrapperStyle}>
|
||||
<TextField
|
||||
@@ -1100,7 +1330,7 @@ const AgentUI = (props) => {
|
||||
<Box
|
||||
component="form"
|
||||
style={{textAlign: "center", }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyDown={handleKeyDownRoot}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitInput(actionInput);
|
||||
@@ -1123,7 +1353,7 @@ const AgentUI = (props) => {
|
||||
disabled={agentRequestLoading}
|
||||
style={{width: 450, marginRight: 20, marginTop: 30, }}
|
||||
multiline
|
||||
minRows={2}
|
||||
minRows={1}
|
||||
defaultValue={actionInput || ""}
|
||||
onChange={(e) => {
|
||||
setActionInput(e.target.value)
|
||||
@@ -1143,6 +1373,54 @@ const AgentUI = (props) => {
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{display: "flex", margin: "auto", paddingTop: 10, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
|
||||
<div>
|
||||
<Chip
|
||||
id="add_app_chip"
|
||||
icon={<AddIcon />} label="Select Apps"
|
||||
style={chipStyle}
|
||||
onClick={() => {
|
||||
setAppPickerAnchor(document.getElementById("add_app_chip"))
|
||||
}}
|
||||
/>
|
||||
<Popover
|
||||
open={appPickerAnchor !== null}
|
||||
anchorEl={appPickerAnchor}
|
||||
onClose={() => {
|
||||
setAppPickerAnchor(null)
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<AppSearch
|
||||
userdata={userdata}
|
||||
defaultSearch={""}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
inputHeight={200}
|
||||
/>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
{chosenApps.map((app, index) => {
|
||||
const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
|
||||
const chosenImagePath = app?.image
|
||||
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
|
||||
|
||||
return (
|
||||
<Chip icon={chosenImage} label={chosenName} variant="outlined"
|
||||
style={chipStyle}
|
||||
onDelete={() => {
|
||||
const newChosenApps = chosenApps.filter((a, i) => i !== index)
|
||||
setChosenApps(newChosenApps)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Box>
|
||||
:
|
||||
<div>
|
||||
|
||||
@@ -135,12 +135,10 @@ import {
|
||||
OpenInFull as OpenInFullIcon,
|
||||
Difference as DifferenceIcon,
|
||||
DataObject as DataObjectIcon,
|
||||
SwapHoriz as SwapHorizIcon
|
||||
} from "@mui/icons-material";
|
||||
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
|
||||
//import * as cytoscape from "cytoscape";
|
||||
|
||||
import cytoscape from "cytoscape";
|
||||
|
||||
|
||||
import edgehandles from "cytoscape-edgehandles";
|
||||
|
||||
import CytoscapeComponent from "react-cytoscapejs";
|
||||
@@ -152,7 +150,7 @@ import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
import LineChartWrapper from "../components/LineChartWrapper.jsx";
|
||||
|
||||
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
|
||||
import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows.jsx";
|
||||
import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows2.jsx";
|
||||
import { GetParsedPaths, internalIds, } from "../views/Apps.jsx";
|
||||
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
@@ -1065,7 +1063,29 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"type": "",
|
||||
},
|
||||
"description": "Build & integrate tools easily with standard input and standard output. Built by Shuffle. https://singul.io",
|
||||
"actions": [{
|
||||
"actions": [
|
||||
{
|
||||
"name": "Translate standard",
|
||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||
"label": "Translate standard",
|
||||
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
|
||||
"parameters": [{
|
||||
"name": "source_data",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"multiline": true,
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"value": "OCSF",
|
||||
"description": "The standard to use from https://github.com/Shuffle/standards/tree/main",
|
||||
"options": [
|
||||
"OCSF"
|
||||
],
|
||||
"required": true,
|
||||
"multiline": false,
|
||||
}]
|
||||
}, {
|
||||
"name": "Cases",
|
||||
"description": "Available actions for case management",
|
||||
"label": "Cases",
|
||||
@@ -1277,29 +1297,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
]
|
||||
},
|
||||
*/
|
||||
{
|
||||
"name": "Translate standard",
|
||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||
"label": "Translate standard",
|
||||
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
|
||||
"parameters": [{
|
||||
"name": "source_data",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"multiline": true,
|
||||
},
|
||||
{
|
||||
"name": "standard",
|
||||
"value": "OCSF",
|
||||
"description": "The standard to use from https://github.com/Shuffle/standards/tree/main",
|
||||
"options": [
|
||||
"OCSF"
|
||||
],
|
||||
"required": true,
|
||||
"multiline": false,
|
||||
}]
|
||||
},
|
||||
*/
|
||||
]}]
|
||||
|
||||
|
||||
@@ -2244,6 +2242,11 @@ const AngularWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
const getWorkflowExecution = (id, execution_id, filter, orgId) => {
|
||||
if (id === undefined) {
|
||||
console.log("No workflow ID defined for getting executions")
|
||||
return
|
||||
}
|
||||
|
||||
var url = `${globalUrl}/api/v2/workflows/${id}/executions`
|
||||
var method = "GET"
|
||||
if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") {
|
||||
@@ -22082,6 +22085,153 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Should probably put this on the backend instead when notifications are made :))
|
||||
const getErrorSuggestion = (result) => {
|
||||
if (result === undefined || result === null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if array with json inside to handle one item at a time~
|
||||
if (typeof result === "object" && result.length !== undefined) {
|
||||
if (result.length > 0) {
|
||||
// Check type inside
|
||||
if (typeof result[0] === "object") {
|
||||
result = result[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success === true && result.status === 200) {
|
||||
if (result.body !== undefined && result.body !== null) {
|
||||
const stringbody = result.body.toString()
|
||||
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (stringbody.length > 1000) {
|
||||
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.status === 429) {
|
||||
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
|
||||
}
|
||||
|
||||
if (result.status === 405) {
|
||||
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
|
||||
}
|
||||
|
||||
if (result.status === 415) {
|
||||
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
|
||||
}
|
||||
|
||||
if (result.status === 401) {
|
||||
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 403) {
|
||||
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 404) {
|
||||
return "The URL, or content of the URL is incorrect. Check it and try again."
|
||||
}
|
||||
|
||||
if (result.status === 400) {
|
||||
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 200 || result.status === 201 || result.status === 204) {
|
||||
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
|
||||
}
|
||||
|
||||
|
||||
// Validate and check for newlines
|
||||
if (result.success !== false) {
|
||||
|
||||
var stringjson = result
|
||||
const valid = validateJson(stringjson, true)
|
||||
if (valid.valid === false) {
|
||||
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
|
||||
// Look for newline
|
||||
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
|
||||
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
|
||||
} else {
|
||||
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return ""
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
stringjson = JSON.stringify(result)
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
stringjson = stringjson.toLowerCase()
|
||||
if (stringjson.includes("localhost")) {
|
||||
return "You can't use localhost in apps. Use the external ip or url of the server instead"
|
||||
}
|
||||
|
||||
if (stringjson.includes("manifest unknown")) {
|
||||
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
|
||||
}
|
||||
|
||||
if (stringjson.toLowerCase().includes("too many values to unpack")) {
|
||||
return "This is a known error with old apps. Please rebuild the app. Contact support@shuffler.io if it persists after rebuild."
|
||||
}
|
||||
|
||||
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
|
||||
return "Consider whether your Orborus environment can connect to a local IP or not."
|
||||
}
|
||||
|
||||
if (stringjson.includes("kms/")) {
|
||||
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
if (stringjson.includes("string indices must be integers")) {
|
||||
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
|
||||
}
|
||||
|
||||
if (stringjson.includes("invalidurl")) {
|
||||
// IF count of "http" is more than one, 1, it's prolly invalid
|
||||
var additionalinfo = ""
|
||||
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
|
||||
additionalinfo = "You may be using multiple 'http' in the URL. "
|
||||
}
|
||||
|
||||
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
|
||||
}
|
||||
|
||||
if (stringjson.includes("result too large to handle")) {
|
||||
return "Execution loading failed. Reload the execution by closing it and clicking it again"
|
||||
}
|
||||
|
||||
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
|
||||
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
|
||||
}
|
||||
|
||||
if (stringjson.toLowerCase().includes("invalid header")) {
|
||||
return "A header or authentication token in the app is invalid. Check the app's configuration"
|
||||
}
|
||||
|
||||
|
||||
if (stringjson.includes("connectionerror")) {
|
||||
if (stringjson.includes("kms")) {
|
||||
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
const ShowCopyingTooltip = () => {
|
||||
const [showCopying, setShowCopying] = React.useState(true)
|
||||
|
||||
@@ -22161,8 +22311,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
{executionModalView === 0 ? (
|
||||
|
||||
{executionModalView === 0 ? (
|
||||
<div style={{ position: "relative", padding: isMobile ? "0px 0px 0px 10px" : "25px 25px 25px 25px", zIndex: 12502, backgroundColor: theme.palette.drawer.backgroundColor, height: "100%", }}>
|
||||
<div style={{ display: "flex", }}>
|
||||
<Breadcrumbs
|
||||
@@ -23073,7 +23223,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
height: imgsize,
|
||||
border: `2px solid ${statusColor}`,
|
||||
borderRadius: executionData.start === data.action.id ? 25 : 5,
|
||||
|
||||
cursor: isCloud ? "pointer" : "default",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
window.open(`/apps/${data?.action?.app_name}`, "_blank")
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23095,7 +23252,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
|
||||
if (data?.action?.name === "User Input" || data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
|
||||
actionimg = (
|
||||
<img
|
||||
alt={"User Input Trigger"}
|
||||
@@ -23222,6 +23379,13 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (relevant_errors.length === 0) {
|
||||
const foundError = getErrorSuggestion(validate.result)
|
||||
if (foundError !== undefined && foundError !== null && foundError !== "") {
|
||||
relevant_errors = [foundError]
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
@@ -23669,150 +23833,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
)
|
||||
}
|
||||
|
||||
var draggingDisabled = false;
|
||||
|
||||
// Should probably put this on the backend instead when notifications are made :))
|
||||
const getErrorSuggestion = (result) => {
|
||||
if (result === undefined || result === null) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if array with json inside to handle one item at a time~
|
||||
if (typeof result === "object" && result.length !== undefined) {
|
||||
if (result.length > 0) {
|
||||
// Check type inside
|
||||
if (typeof result[0] === "object") {
|
||||
result = result[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.success === true && result.status === 200) {
|
||||
if (result.body !== undefined && result.body !== null) {
|
||||
const stringbody = result.body.toString()
|
||||
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (stringbody.length > 1000) {
|
||||
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.status === 429) {
|
||||
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
|
||||
}
|
||||
|
||||
if (result.status === 405) {
|
||||
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
|
||||
}
|
||||
|
||||
if (result.status === 415) {
|
||||
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
|
||||
}
|
||||
|
||||
if (result.status === 401) {
|
||||
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 403) {
|
||||
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 404) {
|
||||
return "The URL, or content of the URL is incorrect. Check it and try again."
|
||||
}
|
||||
|
||||
if (result.status === 400) {
|
||||
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
|
||||
}
|
||||
|
||||
if (result.status === 200 || result.status === 201 || result.status === 204) {
|
||||
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
|
||||
}
|
||||
|
||||
|
||||
// Validate and check for newlines
|
||||
if (result.success !== false) {
|
||||
|
||||
var stringjson = result
|
||||
const valid = validateJson(stringjson, true)
|
||||
if (valid.valid === false) {
|
||||
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
|
||||
// Look for newline
|
||||
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
|
||||
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
|
||||
} else {
|
||||
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//return ""
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
stringjson = JSON.stringify(result)
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
stringjson = stringjson.toLowerCase()
|
||||
if (stringjson.includes("localhost")) {
|
||||
return "You can't use localhost in apps. Use the external ip or url of the server instead"
|
||||
}
|
||||
|
||||
if (stringjson.includes("manifest unknown")) {
|
||||
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
|
||||
}
|
||||
|
||||
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
|
||||
return "Consider whether your Orborus environment can connect to a local IP or not."
|
||||
}
|
||||
|
||||
if (stringjson.includes("kms/")) {
|
||||
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
if (stringjson.includes("string indices must be integers")) {
|
||||
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
|
||||
}
|
||||
|
||||
if (stringjson.includes("invalidurl")) {
|
||||
// IF count of "http" is more than one, 1, it's prolly invalid
|
||||
var additionalinfo = ""
|
||||
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
|
||||
additionalinfo = "You may be using multiple 'http' in the URL. "
|
||||
}
|
||||
|
||||
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
|
||||
}
|
||||
|
||||
if (stringjson.includes("result too large to handle")) {
|
||||
return "Execution loading failed. Reload the execution by closing it and clicking it again"
|
||||
}
|
||||
|
||||
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
|
||||
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
|
||||
}
|
||||
|
||||
if (stringjson.toLowerCase().includes("invalid header")) {
|
||||
return "A header or authentication token in the app is invalid. Check the app's configuration"
|
||||
}
|
||||
|
||||
|
||||
if (stringjson.includes("connectionerror")) {
|
||||
if (stringjson.includes("kms")) {
|
||||
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
|
||||
}
|
||||
|
||||
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
var draggingDisabled = false;
|
||||
|
||||
const currentSuggestion = getErrorSuggestion(validate.result)
|
||||
const codePopoutModal = !codeModalOpen ? null : (
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
|
||||
import { validateJson, } from "../views/Workflows.jsx";
|
||||
import { validateJson, } from "../views/Workflows2.jsx";
|
||||
import { isMobile } from "react-device-detect"
|
||||
import theme from "../theme.jsx";
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
@@ -277,7 +277,8 @@ const ApiExplorerWrapper = (props) => {
|
||||
parsedapp.name !== undefined &&
|
||||
parsedapp.name !== null &&
|
||||
parsedapp.name.length !== 0;
|
||||
if(parsedapp?.id.length > 0){
|
||||
|
||||
if (parsedapp?.id.length > 0) {
|
||||
setSelectedAppData(parsedapp)
|
||||
handleAppAuthenticationType(parsedapp)
|
||||
const apptype = selectedAppData?.generated === false ? "python" : "openapi"
|
||||
@@ -441,7 +442,6 @@ const ApiExplorerWrapper = (props) => {
|
||||
selectedAppData.name = appname
|
||||
}
|
||||
|
||||
console.log("APPNAME: ", appname, openapi.id)
|
||||
if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") {
|
||||
setAppAuthentication(data)
|
||||
setSelectedAuthentication({})
|
||||
|
||||
@@ -3455,9 +3455,9 @@ const AppCreator = (defaultprops) => {
|
||||
required_bodyfields: [],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Queries: ", urlPathQueries)
|
||||
}, [urlPathQueries])
|
||||
//useEffect(() => {
|
||||
// console.log("Queries: ", urlPathQueries)
|
||||
//}, [urlPathQueries])
|
||||
|
||||
const findBodyParams = (body) => {
|
||||
const regex = /\${(\w+)}/g;
|
||||
|
||||
@@ -74,7 +74,7 @@ import {
|
||||
} from "react-instantsearch-dom";
|
||||
import AppStats from "../components/AppStats.jsx";
|
||||
import ParsedAction from "../components/ParsedAction.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import { base64_decode, appCategories } from "../views/AppCreator.jsx";
|
||||
import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
@@ -4454,7 +4454,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
): null}
|
||||
|
||||
{userdata && (userdata?.active_org?.creator_org?.length > 0 || userdata?.active_org?.child_orgs?.length === 0) ? null : (
|
||||
<Button variant="outlined" color="secondary" onClick={()=> {setShowDistributionPopup(true)}} >Distribute App</Button>
|
||||
<Button variant="outlined" color="secondary" onClick={()=> {setShowDistributionPopup(true)}}>Distribute</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+137
-35
@@ -6,7 +6,7 @@ import ReactJson from "react-json-view-ssr";
|
||||
import { isMobile } from "react-device-detect";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
import { useParams, useNavigate, Link, useLocation } from "react-router-dom";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import {
|
||||
@@ -87,6 +87,34 @@ const innerHrefStyle = {
|
||||
textDecoration: "none",
|
||||
};
|
||||
|
||||
const noteLabelStyle = {
|
||||
fontWeight: "bold",
|
||||
color: "#f86a3e",
|
||||
display: "block",
|
||||
marginBottom: "5px",
|
||||
};
|
||||
|
||||
const alertNote = {
|
||||
padding: "10px",
|
||||
borderLeft: "5px solid #f86a3e",
|
||||
backgroundColor: "rgb(26,26,26)",
|
||||
};
|
||||
|
||||
export const Blockquote = ({ children }) => {
|
||||
|
||||
const textContent = children.map(child =>
|
||||
child.props && child.props.children ? child.props.children.join('') : child
|
||||
).join('').trim();
|
||||
|
||||
// Maybe some more contents....
|
||||
const isNote = textContent.startsWith("[!TIP]");
|
||||
return (
|
||||
<blockquote style={isNote ? alertNote : {}}>
|
||||
{isNote && <span style={noteLabelStyle}>Tips:</span>}
|
||||
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
export const CopyToClipboard = (props) => {
|
||||
const { text, style, onCopy } = props;
|
||||
@@ -173,6 +201,86 @@ export const OuterLink = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Markdown table renderers for improved styling and readability
|
||||
export const TableRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
overflowX: "auto",
|
||||
marginTop: 16,
|
||||
marginBottom: 24,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
}}
|
||||
>
|
||||
<table
|
||||
style={{
|
||||
width: "100%",
|
||||
borderCollapse: "separate",
|
||||
borderSpacing: 0,
|
||||
minWidth: 600,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableRowRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<tr
|
||||
style={{
|
||||
borderBottom: "1px solid rgba(255,255,255,0.12)",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableHeaderCellRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<th
|
||||
style={{
|
||||
textAlign: "left",
|
||||
padding: "12px 14px",
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
color: theme.palette.textColor,
|
||||
fontWeight: 600,
|
||||
position: "sticky",
|
||||
top: 0,
|
||||
borderBottom: "1px solid rgba(255,255,255,0.2)",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
export const TableCellRenderer = (props) => {
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
padding: "10px 14px",
|
||||
verticalAlign: "top",
|
||||
color: theme.palette.textColor,
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Img = (props) => {
|
||||
@@ -250,9 +358,23 @@ export const CodeHandler = (props) => {
|
||||
|
||||
// Need to check if it's singletick or multi
|
||||
if (props.inline === true) {
|
||||
// Show it inline
|
||||
// Enhanced inline code styling for readability and long strings
|
||||
return (
|
||||
<span style={{ backgroundColor: theme.palette.inputColor, display: "inline", whiteSpace: "pre-wrap", padding: "6px 3px 6px 3px", }}>
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
border: "1px solid rgba(255,255,255,0.15)",
|
||||
borderRadius: "6px",
|
||||
padding: "1px 6px",
|
||||
margin: "0 2px",
|
||||
display: "inline-block",
|
||||
lineHeight: 1.6,
|
||||
fontSize: "0.95em",
|
||||
fontFamily:
|
||||
'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{newprop}
|
||||
</span>
|
||||
)
|
||||
@@ -318,6 +440,8 @@ const Docs = (defaultprops) => {
|
||||
props.match = {}
|
||||
props.match.params = params
|
||||
|
||||
window.title = "Shuffle - Documentation"
|
||||
|
||||
//console.log("PARAMS: ", params)
|
||||
const { themeMode } = useContext(Context)
|
||||
const theme = getTheme(themeMode)
|
||||
@@ -668,36 +792,13 @@ const Docs = (defaultprops) => {
|
||||
minHeight: "80vh",
|
||||
};
|
||||
|
||||
const noteLabelStyle = {
|
||||
fontWeight: "bold",
|
||||
color: "#f86a3e",
|
||||
display: "block",
|
||||
marginBottom: "5px",
|
||||
};
|
||||
|
||||
const Blockquote = ({ children }) => {
|
||||
|
||||
const textContent = children.map(child =>
|
||||
child.props && child.props.children ? child.props.children.join('') : child
|
||||
).join('').trim();
|
||||
|
||||
// Maybe some more contents....
|
||||
const isNote = textContent.startsWith("[!TIP]");
|
||||
return (
|
||||
<blockquote style={isNote ? alertNote : {}}>
|
||||
{isNote && <span style={noteLabelStyle}>Tips:</span>}
|
||||
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
|
||||
</blockquote>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const Heading = (props) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase();
|
||||
if (props.level <= 3) {
|
||||
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
|
||||
var id = (props?.children?.[0] ?? props.children ?? '').toString().toLowerCase();
|
||||
if (props?.level <= 3) {
|
||||
id = props?.children?.[0]?.toLowerCase().toString().replaceAll(" ", "-") ?? '';
|
||||
}
|
||||
|
||||
const element = React.createElement(
|
||||
@@ -1098,11 +1199,6 @@ const Docs = (defaultprops) => {
|
||||
fontSize: isMobile ? "1.3rem" : "1.1rem",
|
||||
};
|
||||
|
||||
const alertNote = {
|
||||
padding: "10px",
|
||||
borderLeft: "5px solid #f86a3e",
|
||||
backgroundColor: "rgb(26,26,26)",
|
||||
};
|
||||
|
||||
const CustomButton = (props) => {
|
||||
const { title, icon, link } = props
|
||||
@@ -1192,7 +1288,7 @@ const Docs = (defaultprops) => {
|
||||
</Typography>
|
||||
{showPartnerLogo === true ? null :
|
||||
<div style={{ display: "flex", marginTop: 25, }}>
|
||||
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> />
|
||||
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://shuffler.io/contact?category=support" />
|
||||
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
|
||||
</div>
|
||||
}
|
||||
@@ -1230,6 +1326,8 @@ const Docs = (defaultprops) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
const markdownComponents = {
|
||||
img: Img,
|
||||
code: CodeHandler,
|
||||
@@ -1242,6 +1340,10 @@ const Docs = (defaultprops) => {
|
||||
a: OuterLink,
|
||||
p: Paragraph,
|
||||
blockquote: Blockquote,
|
||||
table: TableRenderer,
|
||||
tr: TableRowRenderer,
|
||||
th: TableHeaderCellRenderer,
|
||||
td: TableCellRenderer,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ const LoginPage = props => {
|
||||
const tmpMessage = new URLSearchParams(window.location.search).get("message")
|
||||
if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) {
|
||||
setMessage(tmpMessage)
|
||||
toast(tmpMessage)
|
||||
toast.warn(tmpMessage)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
Select,
|
||||
MenuItem,
|
||||
Tooltip,
|
||||
CircularProgress,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from '@mui/material';
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
@@ -54,6 +56,15 @@ const NewDashboard = (props) => {
|
||||
const [overrideDays, setOverrideDays] = useState(undefined);
|
||||
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
|
||||
const [isProdStatusOn, setIsProdStatusOn] = useState(false)
|
||||
const [selectedOrganization, setSelectedOrganization] = useState(null)
|
||||
const [selectedOrgForStats, setSelectedOrgForStats] = useState(userdata?.active_org?.id || null)
|
||||
const [availableOrgs, setAvailableOrgs] = useState([])
|
||||
const [selectedOrgStats, setSelectedOrgStats] = useState(null)
|
||||
const [loadingSelectedOrgStats, setLoadingSelectedOrgStats] = useState(false)
|
||||
const [selectedOrgDetailsForStats, setSelectedOrgDetailsForStats] = useState(null)
|
||||
const [fallbackToParentStats, setFallbackToParentStats] = useState(false);
|
||||
|
||||
document.title = "Shuffle - Dashboard";
|
||||
|
||||
const isCloud =
|
||||
serverside === true || typeof window === "undefined"
|
||||
@@ -121,15 +132,22 @@ const NewDashboard = (props) => {
|
||||
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]);
|
||||
const anyLoading =
|
||||
loadingSfw ||
|
||||
loadingRot ||
|
||||
loadingNoti ||
|
||||
loadingSelectedOrgStats ||
|
||||
!selectedOrganization ||
|
||||
!selectedOrgForStats;
|
||||
setShowOverlay(anyLoading);
|
||||
}, [
|
||||
loadingSfw,
|
||||
loadingRot,
|
||||
loadingNoti,
|
||||
loadingSelectedOrgStats,
|
||||
selectedOrganization,
|
||||
selectedOrgForStats,
|
||||
]);
|
||||
|
||||
// Auto-open onboarding when there aren't enough active days of stats
|
||||
useEffect(() => {
|
||||
@@ -211,6 +229,7 @@ const NewDashboard = (props) => {
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((org) => {
|
||||
if (!fetched && org) {
|
||||
setSelectedOrganization(org);
|
||||
if (!isCloud) {
|
||||
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
|
||||
setIsProdStatusOn(true);
|
||||
@@ -229,8 +248,136 @@ const NewDashboard = (props) => {
|
||||
};
|
||||
}, [userdata?.active_org?.id, globalUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedOrgForStats) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fetched = false;
|
||||
fetch(`${globalUrl}/api/v1/orgs/${selectedOrgForStats}`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((org) => {
|
||||
if (!fetched && org) {
|
||||
setSelectedOrgDetailsForStats(org);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return () => {
|
||||
fetched = true;
|
||||
};
|
||||
}, [selectedOrgForStats, globalUrl]);
|
||||
|
||||
|
||||
// Build list of available orgs for stats (parent + child orgs)
|
||||
useEffect(() => {
|
||||
if (!selectedOrganization) return;
|
||||
|
||||
const orgs = [{ id: 'ALL', name: 'All Organizations', isAll: true }];
|
||||
|
||||
// Add parent org
|
||||
if (selectedOrganization?.id) {
|
||||
orgs.push({
|
||||
id: selectedOrganization.id,
|
||||
name: selectedOrganization.name || 'Parent Organization',
|
||||
isAll: false
|
||||
});
|
||||
}
|
||||
|
||||
// Add child orgs if they exist
|
||||
if (selectedOrganization?.child_orgs && Array.isArray(selectedOrganization.child_orgs)) {
|
||||
selectedOrganization.child_orgs.forEach(child => {
|
||||
if (child?.id && child?.name) {
|
||||
orgs.push({
|
||||
id: child.id,
|
||||
name: child.name,
|
||||
isAll: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setAvailableOrgs(orgs);
|
||||
setSelectedOrgForStats(selectedOrganization?.id);
|
||||
}, [selectedOrganization]);
|
||||
|
||||
// Fetch statistics for specifically selected org (not for ALL)
|
||||
useEffect(() => {
|
||||
|
||||
let aborted = false;
|
||||
const load = async () => {
|
||||
try {
|
||||
if (!selectedOrgForStats || selectedOrgForStats === 'ALL') {
|
||||
setSelectedOrgStats(null);
|
||||
setFallbackToParentStats(false);
|
||||
return;
|
||||
}
|
||||
setLoadingSelectedOrgStats(true);
|
||||
const resp = await fetch(`${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
let fallback = false;
|
||||
let data = null;
|
||||
if (resp.ok) {
|
||||
data = await resp.json();
|
||||
}
|
||||
const isParentContext = selectedOrganization && userdata?.active_org?.id === selectedOrganization.id;
|
||||
if (!data || !Array.isArray(data.daily_statistics) || data.daily_statistics.length === 0) {
|
||||
fallback = true;
|
||||
setSelectedOrgStats(null);
|
||||
// Only fallback to parent if on parent org dashboard
|
||||
if (isParentContext && selectedOrganization && selectedOrgForStats !== selectedOrganization.id) {
|
||||
toast.info('No stats available for selected org. Showing parent org stats.');
|
||||
setSelectedOrgForStats(selectedOrganization.id);
|
||||
}
|
||||
} else {
|
||||
// If data exists but has near-zero activity over the last 30 days, fallback to parent
|
||||
try {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - 30);
|
||||
const recent = data.daily_statistics.filter((d) => {
|
||||
if (!d?.date) return false;
|
||||
const dt = new Date(d.date);
|
||||
return dt >= cutoff;
|
||||
});
|
||||
const sumWork = recent.reduce((s, d) => s + Number(d?.workflow_executions_finished || 0) + Number(d?.workflow_executions_failed || 0), 0);
|
||||
const sumApp = recent.reduce((s, d) => s + Number(d?.app_executions || 0), 0);
|
||||
const nearZero = (Number(sumWork) + Number(sumApp)) <= 0;
|
||||
if (nearZero && isParentContext && selectedOrganization && selectedOrgForStats !== selectedOrganization.id) {
|
||||
fallback = true;
|
||||
setSelectedOrgStats(null);
|
||||
toast.info('Selected org has no activity in the last 30 days. Showing parent org stats.');
|
||||
setSelectedOrgForStats(selectedOrganization.id);
|
||||
} else {
|
||||
setSelectedOrgStats(data || null);
|
||||
}
|
||||
} catch {
|
||||
setSelectedOrgStats(data || null);
|
||||
}
|
||||
}
|
||||
setFallbackToParentStats(fallback && isParentContext);
|
||||
} catch {
|
||||
if (!aborted) setSelectedOrgStats(null);
|
||||
setFallbackToParentStats(false);
|
||||
} finally {
|
||||
if (!aborted) setLoadingSelectedOrgStats(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Calling the function.
|
||||
load();
|
||||
return () => { aborted = true; };
|
||||
}, [selectedOrgForStats, selectedOrganization, globalUrl, userdata?.active_org?.id]);
|
||||
|
||||
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' }}>
|
||||
{(onboardingOpen && !loadingSelectedOrgStats) && (
|
||||
<DashboardOnboarding
|
||||
open={onboardingOpen}
|
||||
globalUrl={globalUrl}
|
||||
@@ -251,6 +398,7 @@ const NewDashboard = (props) => {
|
||||
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 }}>
|
||||
@@ -262,9 +410,33 @@ const NewDashboard = (props) => {
|
||||
{/* Header / Greeting */}
|
||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
|
||||
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
|
||||
<>
|
||||
<Box style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
{availableOrgs.length > 2 && (
|
||||
<FormControl size="small" variant="outlined" style={{ minWidth: 200, marginTop: -8 }} sx={{
|
||||
'& .MuiInputBase-root': {
|
||||
height: 40,
|
||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||
borderRadius: '20px',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
|
||||
}}>
|
||||
<InputLabel id="stats-org-select-label">View Stats For</InputLabel>
|
||||
<Select
|
||||
labelId="stats-org-select-label"
|
||||
label="View Stats"
|
||||
value={selectedOrgForStats || ''}
|
||||
onChange={(e) => setSelectedOrgForStats(e.target.value)}
|
||||
>
|
||||
{availableOrgs.map((org) => (
|
||||
<MenuItem key={org.id} value={org.id}>
|
||||
{org.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
{sfwControls}
|
||||
</>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* KPI cards */}
|
||||
@@ -309,12 +481,27 @@ const NewDashboard = (props) => {
|
||||
onControlsChange={handleSfwControls}
|
||||
onLoadingChange={setLoadingSfw}
|
||||
onTotalsChange={setTotals}
|
||||
loadingSelectedOrgStats={loadingSelectedOrgStats}
|
||||
selectedOrganization={selectedOrganization}
|
||||
selectedOrgForStats={selectedOrgForStats}
|
||||
orgStats={selectedOrgStats}
|
||||
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
|
||||
/>
|
||||
</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} />
|
||||
<RunsOverTimeWidget
|
||||
globalUrl={globalUrl}
|
||||
onLoadingChange={setLoadingRot}
|
||||
loadingSelectedOrgStats={loadingSelectedOrgStats}
|
||||
monthOverride={rotMonthOverride}
|
||||
dummyMode={onboardingOpen}
|
||||
selectedOrganization={selectedOrganization}
|
||||
selectedOrgForStats={selectedOrgForStats}
|
||||
orgStats={selectedOrgStats}
|
||||
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
|
||||
/>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,10 +3,10 @@ import React, {useState, useEffect, useContext} from 'react';
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
import { green, yellow, red, grey} from "./AngularWorkflow.jsx";
|
||||
import { green, yellow, red, grey} from "../views/AngularWorkflow.jsx";
|
||||
import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
import { validateJson, collapseField, GetIconInfo } from "./Workflows.jsx";
|
||||
import { validateJson, collapseField, GetIconInfo } from "../views/Workflows2.jsx";
|
||||
import EditWorkflow from "../components/EditWorkflow.jsx"
|
||||
import { toast } from "react-toastify"
|
||||
import { makeStyles } from '@mui/material/styles';
|
||||
@@ -47,6 +47,10 @@ import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Edit as EditIcon,
|
||||
Polyline as PolylineIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
DirectionsRun as DirectionsRunIcon,
|
||||
Error as ErrorIcon,
|
||||
Pause as PauseIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
@@ -145,6 +149,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
padding: "25px 50px 50px 50px",
|
||||
borderRadius: 25,
|
||||
minHeight: 500,
|
||||
position: "relative",
|
||||
}
|
||||
|
||||
const params = useParams();
|
||||
@@ -189,7 +194,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
// 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)
|
||||
//console.log("Unanswered, required question: ", key)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -406,7 +411,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
setTimeout(() => {
|
||||
setExecutionLoading(true)
|
||||
}, 2500)
|
||||
}, 250)
|
||||
|
||||
var data = {
|
||||
"execution_argument": executionArgument,
|
||||
@@ -842,6 +847,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -915,20 +921,37 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
//console.log("Updating data!")
|
||||
setExecutionData(responseJson)
|
||||
|
||||
for (var key in responseJson.results) {
|
||||
if (responseJson.results[key].status === "WAITING") {
|
||||
const validate = validateJson(responseJson.results[key].result)
|
||||
if (validate.valid && typeof validate.result === "string") {
|
||||
validate.result = JSON.parse(validate.result)
|
||||
}
|
||||
|
||||
if (validate.result["information"] !== undefined && validate.result["information"] !== null) {
|
||||
setWorkflowQuestion(validate.result["information"])
|
||||
}
|
||||
|
||||
break
|
||||
if (responseJson.results[key].status !== "WAITING") {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
const validate = validateJson(responseJson.results[key].result)
|
||||
if (validate.valid && typeof validate.result === "string") {
|
||||
validate.result = JSON.parse(validate.result)
|
||||
}
|
||||
|
||||
console.log("Found waiting!: ", validate.result)
|
||||
|
||||
if (validate?.result?.information !== undefined && validate?.result?.information !== null) {
|
||||
console.log("Success! Not checking again.")
|
||||
setWorkflowQuestion(validate?.result?.information)
|
||||
} else {
|
||||
console.log("No information found for questions?: ", validate.result)
|
||||
|
||||
// Specific case for "too early" config of waiting
|
||||
if (typeof validate.result === "string" || Object.keys(validate.result).length === 2) {
|
||||
setTimeout(() => {
|
||||
fetchUpdates(responseJson.execution_id, responseJson.authorization)
|
||||
}, 2000)
|
||||
} else {
|
||||
console.log("NOT re-fetching")
|
||||
}
|
||||
//setWorkflowQuestions{
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
} else {
|
||||
console.log("NOT updating executiondata state.");
|
||||
@@ -1034,7 +1057,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for stream results :O!");
|
||||
|
||||
toast.warn("Error getting results.. Please try again or contact support@shuffler.io if this persists.")
|
||||
//toast.warn("Error getting results.. Please try again or contact support@shuffler.io if this persists.")
|
||||
}
|
||||
|
||||
return response.json();
|
||||
@@ -1045,18 +1068,19 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
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") {
|
||||
console.log("IN here 1")
|
||||
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) {
|
||||
console.log("IN here 2")
|
||||
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)
|
||||
console.log("AGENTIC: Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
|
||||
|
||||
setAgentic(true)
|
||||
|
||||
@@ -1409,6 +1433,27 @@ const RunWorkflow = (defaultprops) => {
|
||||
const basedata =
|
||||
<div style={bodyDivStyle}>
|
||||
<Paper style={boxStyle}>
|
||||
<div style={{position: "absolute", top: 20, right: 10,}}>
|
||||
{executionData?.status === "" ? null :
|
||||
executionData?.status === "FINISHED" ?
|
||||
<Tooltip title="The previous Workflow Finished" placement="top">
|
||||
<CheckCircleIcon style={{color: green, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: executionData?.status === "EXECUTING" ?
|
||||
<Tooltip title="The Workflow is current running" placement="top">
|
||||
<DirectionsRunIcon style={{color: theme.palette.secondary, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: executionData?.status === "ABORTED" || executionData?.status === "FAILURE" ?
|
||||
<Tooltip title={`The workflow run failed with status ${executionData?.status}`} placement="top">
|
||||
<ErrorIcon style={{color: red, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: executionData?.status === "WAITING" ?
|
||||
<Tooltip title={`The workflow is waiting for user input`} placement="top">
|
||||
<PauseIcon style={{color: yellow, marginRight: 10, }} />
|
||||
</Tooltip>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{explorerUi === true ?
|
||||
<ExplorerUi />
|
||||
:
|
||||
@@ -1562,7 +1607,6 @@ const RunWorkflow = (defaultprops) => {
|
||||
label={parsedLabel}
|
||||
required
|
||||
|
||||
disabled={disabledButtons}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
@@ -1582,43 +1626,46 @@ const RunWorkflow = (defaultprops) => {
|
||||
:
|
||||
(answer !== undefined && answer !== null) || message !== "" ? null :
|
||||
|
||||
<span>
|
||||
{foundSourcenode !== undefined && foundSourcenode !== null ?
|
||||
"Add Note"
|
||||
:
|
||||
"Runtime Argument"
|
||||
}
|
||||
executionRunning ? null :
|
||||
<span>
|
||||
{foundSourcenode !== undefined && foundSourcenode !== null ?
|
||||
"Add Note"
|
||||
:
|
||||
"Runtime Argument"
|
||||
}
|
||||
|
||||
<div style={{marginBottom: 5}}>
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
|
||||
multiLine
|
||||
maxRows={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
InputProps={{
|
||||
autocomplete: "off",
|
||||
form: {
|
||||
<div style={{marginBottom: 5}}>
|
||||
<TextField
|
||||
disabled={executionRunning}
|
||||
color="primary"
|
||||
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
|
||||
multiLine
|
||||
maxRows={2}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
InputProps={{
|
||||
autocomplete: "off",
|
||||
},
|
||||
style:{
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(e) => {
|
||||
setExecutionArgument(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
form: {
|
||||
autocomplete: "off",
|
||||
},
|
||||
style:{
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder=""
|
||||
id="emailfield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(e) => {
|
||||
setExecutionArgument(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1724,12 +1771,12 @@ const RunWorkflow = (defaultprops) => {
|
||||
>
|
||||
{executionLoading ?
|
||||
<CircularProgress color="secondary" style={{color: "white",}} />
|
||||
:
|
||||
executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ? "Run Again"
|
||||
:
|
||||
"Submit"
|
||||
}
|
||||
</Button>
|
||||
: executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ?
|
||||
"Run Again"
|
||||
: executionData?.status === "WAITING" ?
|
||||
"Submit answers"
|
||||
: "Submit"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1780,7 +1827,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginBottom: 10, }}>
|
||||
<div style={{marginBottom: 10, borderBottom: "1px solid rgba(255,255,255,0.3)", paddingBottom: 5, }} key={index}>
|
||||
{foundresult?.action?.label?.replaceAll("_", " ")} - {foundresult.status}:
|
||||
<br />
|
||||
|
||||
@@ -1807,7 +1854,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
{workflowQuestion !== "" ? null :
|
||||
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 10, }} >
|
||||
Form submission data includes your Organization's unique ID while logged in, or a unique identifier for your browser otherwise. Your input will be automatically sanitized.
|
||||
Form submission data includes your Organization's unique ID, or a unique identifier for your browser. Your input will be automatically sanitized.
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -126,296 +126,6 @@ const useStyles = makeStyles(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Takes an action in Shuffle and
|
||||
// Returns information about the icon, the color etc to be used
|
||||
// This can be used for actions of all types
|
||||
export const GetIconInfo = (action) => {
|
||||
// Finds the icon based on the action. Should be verbs.
|
||||
const iconList = [
|
||||
{ key: "cases", values: ["cases"] },
|
||||
{ key: "cache_add", values: ["set_cache"] },
|
||||
{ key: "cache_get", values: ["get_cache"] },
|
||||
{ key: "filter", values: ["filter"] },
|
||||
{ key: "merge", values: ["join", "merge", "route", "router", "routing"] },
|
||||
{
|
||||
key: "search",
|
||||
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
|
||||
},
|
||||
{ key: "list", values: ["list", "head", "options"] },
|
||||
{
|
||||
key: "download",
|
||||
values: [
|
||||
"capture",
|
||||
"get",
|
||||
"download",
|
||||
"return",
|
||||
"hello_world",
|
||||
"curl",
|
||||
"request",
|
||||
"export",
|
||||
"preview",
|
||||
],
|
||||
},
|
||||
{ key: "add", values: ["add", "accept",] },
|
||||
{ key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] },
|
||||
{
|
||||
key: "send",
|
||||
values: [
|
||||
"send",
|
||||
"dispatch",
|
||||
"mail",
|
||||
"forward",
|
||||
"post",
|
||||
"submit",
|
||||
"mark",
|
||||
"set",
|
||||
"release",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "repeat",
|
||||
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
|
||||
},
|
||||
{ key: "code", values: ["code", "bash", "python", "react", "go"] },
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise"] },
|
||||
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
|
||||
{ key: "inflate", values: ["inflate", "pack", "compress"] },
|
||||
{
|
||||
key: "edit",
|
||||
values: [
|
||||
"modify",
|
||||
"update",
|
||||
"create",
|
||||
"edit",
|
||||
"put",
|
||||
"patch",
|
||||
"change",
|
||||
"replace",
|
||||
"conver",
|
||||
"map",
|
||||
"format",
|
||||
"escape",
|
||||
"describe",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "compare",
|
||||
values: ["compare", "convert", "to", "filter", "translate", "parse", "generate", ],
|
||||
},
|
||||
{ key: "close", values: ["close", "stop", "cancel", "block"] },
|
||||
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
|
||||
];
|
||||
|
||||
var selectedKey = ""
|
||||
if (action.app_name == "Integration Framework") {
|
||||
selectedKey = "magic"
|
||||
} else if (action.name === undefined || action.name === null) {
|
||||
} else {
|
||||
const actionname = action.name.toLowerCase()
|
||||
for (var key in iconList) {
|
||||
//console.log(iconList[key], actionname)
|
||||
const found = iconList[key].values.find((value) =>
|
||||
actionname.includes(value)
|
||||
)
|
||||
if (found !== null && found !== undefined) {
|
||||
selectedKey = iconList[key].key
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some of these are manually parsed or created instead of material ui
|
||||
//M8 0C3.58 0 0 1.79 0 4C0 6.21 3.58 8 8 8C12.42 8 16 6.21 16 4C16 1.79 12.42 0 8 0ZM0 6V9C0 11.21 3.58 13 8 13C12.42 13 16 11.21 16 9V6C16 8.21 12.42 10 8 10C3.58 10 0 8.21 0 6ZM0 11V14C0 16.21 3.58 18 8 18C9.41 18 10.79 17.81 12 17.46V14.46C10.79 14.81 9.41 15 8 15C3.58 15 0 13.21 0 11ZM17 11V14H14V16H17V19H19V16H22V14H19V11
|
||||
//https://www.figma.com/file/uCfnMs5w6wnLx6ehPHEV74/Figma-Material-Design-System-v3_0?node-id=834%3A21
|
||||
//COLORS: https://www.pinterest.co.uk/pin/326299935499972946/
|
||||
const defaultColor = "#f76b1c";
|
||||
const defaultGradient = ["#fad961", "#f76b1c"];
|
||||
const parsedIcons = {
|
||||
magic: {
|
||||
icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "red",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#8A2BE2"],
|
||||
},
|
||||
communication: {
|
||||
icon: "M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cases: {
|
||||
icon: "M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cache_add: {
|
||||
icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cache_get: {
|
||||
icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
repeat: {
|
||||
icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CachedIcon />,
|
||||
},
|
||||
add: {
|
||||
icon: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <AddIcon />,
|
||||
},
|
||||
edit: {
|
||||
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <EditIcon />,
|
||||
},
|
||||
filter: {
|
||||
icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#f5515f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#f5515f", "#a1051d"],
|
||||
},
|
||||
merge: {
|
||||
icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#f5515f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#f5515f", "#a1051d"],
|
||||
},
|
||||
compare: {
|
||||
icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CompareIcon />,
|
||||
},
|
||||
extract: {
|
||||
icon: "M3 3h18v2H3z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <MaximizeIcon />,
|
||||
},
|
||||
inflate: {
|
||||
icon: "M6 19h12v2H6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <MinimizeIcon />,
|
||||
},
|
||||
list: {
|
||||
icon: "M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <TocIcon />,
|
||||
},
|
||||
code: {
|
||||
icon: "M9.4 16.6 4.8 12l4.6-4.6L8 6l-6 6 6 6zm5.2 0 4.6-4.6-4.6-4.6L16 6l6 6-6 6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CodeIcon />,
|
||||
fillGradient: ["#836ef5", "#3335eb"],
|
||||
},
|
||||
execute: {
|
||||
icon: "M8 5v14l11-7z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <PlayArrowIcon />,
|
||||
},
|
||||
delete: {
|
||||
icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#03030e",
|
||||
originalIcon: <DeleteIcon />,
|
||||
fillGradient: ["#03030e", "#205d66"],
|
||||
},
|
||||
close: {
|
||||
icon: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#03030e",
|
||||
originalIcon: <CloseIcon />,
|
||||
fillGradient: ["#03030e", "#205d66"],
|
||||
},
|
||||
send: {
|
||||
icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#0373da",
|
||||
originalIcon: <SendIcon />,
|
||||
fillGradient: ["#0bc8bf", "#0373da"],
|
||||
},
|
||||
download: {
|
||||
icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#0373da",
|
||||
originalIcon: <GetAppIcon />,
|
||||
fillGradient: ["#0bc8bf", "#0373da"],
|
||||
},
|
||||
search: {
|
||||
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "green",
|
||||
originalIcon: <SearchIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
var selectedItem = parsedIcons[selectedKey];
|
||||
if (selectedItem === undefined || selectedItem === null) {
|
||||
return {
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
iconBackground: "black",
|
||||
originalIcon: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedItem.fillGradient === undefined) {
|
||||
selectedItem.fillGradient = defaultGradient;
|
||||
selectedItem.iconBackgroundColor = defaultColor;
|
||||
}
|
||||
|
||||
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
|
||||
console.log(
|
||||
`MISSING PATH FOR ${selectedKey} (find in scope): `,
|
||||
selectedItem.originalIcon.type.type
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(selectedItem.originalIcon === undefined ||
|
||||
selectedItem.originalIcon === "") &&
|
||||
selectedItem.icon !== "" &&
|
||||
selectedItem.icon !== undefined
|
||||
) {
|
||||
const svg_pin = (
|
||||
<svg
|
||||
width={svgSize}
|
||||
height={svgSize}
|
||||
viewBox={`0 0 ${svgSize} ${svgSize}`}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={selectedItem.icon} fill={selectedItem.iconColor}></path>
|
||||
</svg>
|
||||
);
|
||||
selectedItem.originalIcon = svg_pin;
|
||||
}
|
||||
|
||||
return selectedItem;
|
||||
};
|
||||
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43",
|
||||
marginRight: 5,
|
||||
@@ -427,292 +137,6 @@ const chipStyle = {
|
||||
color: "white",
|
||||
};
|
||||
|
||||
export const collapseField = (field, inputdata) => {
|
||||
if (field === undefined || field === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.name === "headers" || field.name === "cookies") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.name === "result") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.type === "array") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If more than 10 keys in object, collapse
|
||||
if (field.type === "object") {
|
||||
if (Object.keys(field.src).length > 7) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
if (typeof copy.name === "string") {
|
||||
copy.name = copy.name.replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
//lol
|
||||
if (typeof base === 'object' || typeof base === 'dict') {
|
||||
base = JSON.stringify(base)
|
||||
}
|
||||
|
||||
if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") {
|
||||
base_node_name = "exec"
|
||||
}
|
||||
|
||||
//console.log("COPY: ", base_node_name, copy);
|
||||
|
||||
//var newitem = JSON.parse(base);
|
||||
var newitem = validateJson(base).result
|
||||
|
||||
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
|
||||
for (let copykey in copy.namespace) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.namespace[copykey]];
|
||||
if (!isNaN(copy.namespace[copykey])) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.namespace[copykey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.name];
|
||||
if (!isNaN(copy.name)) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.name;
|
||||
}
|
||||
}
|
||||
|
||||
to_be_copied = to_be_copied.replaceAll(" ", "_");
|
||||
console.log("COPY: ", to_be_copied);
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
//console.log("NAVIGATOR: ", navigator);
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(to_be_copied);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
//console.log("COPYING!");
|
||||
toast("Copied JSON path to clipboard.")
|
||||
} else {
|
||||
console.log("Couldn't find element ", elementName);
|
||||
}
|
||||
}
|
||||
|
||||
export const handleReactJsonClipboard = (copy) => {
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
if (
|
||||
copy.namespace !== undefined &&
|
||||
copy.name !== undefined &&
|
||||
copy.src !== undefined
|
||||
) {
|
||||
copy = copy.src;
|
||||
}
|
||||
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
var stringified = JSON.stringify(copy);
|
||||
if (stringified.startsWith('"') && stringified.endsWith('"')) {
|
||||
stringified = stringified.substring(1, stringified.length - 1);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(stringified);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); /* For mobile devices */
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
|
||||
console.log("COPYING!");
|
||||
toast.success("Copied Value, NOT json path.")
|
||||
} else {
|
||||
console.log("Failed to copy from " + elementName + ": ", copyText);
|
||||
}
|
||||
}
|
||||
|
||||
export const validateJson = (showResult) => {
|
||||
if (showResult === undefined || showResult === null) {
|
||||
return {
|
||||
valid: false,
|
||||
result: "",
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof showResult === 'string') {
|
||||
showResult = showResult.split(" False").join(" false")
|
||||
showResult = showResult.split(" True").join(" true")
|
||||
|
||||
showResult.replaceAll("False,", "false,")
|
||||
showResult.replaceAll("True,", "true,")
|
||||
}
|
||||
|
||||
if (typeof showResult === "object" || typeof showResult === "array") {
|
||||
return {
|
||||
valid: true,
|
||||
result: showResult,
|
||||
}
|
||||
}
|
||||
|
||||
if (showResult[0] === "\"") {
|
||||
return {
|
||||
valid: false,
|
||||
result: showResult,
|
||||
}
|
||||
}
|
||||
|
||||
var jsonvalid = true
|
||||
try {
|
||||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||||
jsonvalid = false
|
||||
|
||||
return {
|
||||
valid: jsonvalid,
|
||||
result: showResult,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
try {
|
||||
showResult = showResult.split("'").join('"');
|
||||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
jsonvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
var result = showResult;
|
||||
try {
|
||||
result = jsonvalid ? JSON.parse(showResult, {"storeAsString": true}) : showResult;
|
||||
} catch (e) {
|
||||
////console.log("Failed parsing JSON even though its valid: ", e)
|
||||
jsonvalid = false;
|
||||
}
|
||||
|
||||
if (jsonvalid === false) {
|
||||
|
||||
if (typeof showResult === 'string') {
|
||||
showResult = showResult.trim()
|
||||
}
|
||||
|
||||
try {
|
||||
var newstr = showResult.replaceAll("'", '"')
|
||||
|
||||
// Basic workarounds for issues with Python Dicts -> JSON
|
||||
if (newstr.includes(": None")) {
|
||||
newstr = newstr.replaceAll(": None", ': null')
|
||||
}
|
||||
|
||||
if (newstr.includes("[\"{") && newstr.includes("}\"]")) {
|
||||
newstr = newstr.replaceAll("[\"{", '[{')
|
||||
newstr = newstr.replaceAll("}\"]", '}]')
|
||||
}
|
||||
|
||||
if (newstr.includes("{\"[") && newstr.includes("]\"}")) {
|
||||
newstr = newstr.replaceAll("{\"[", '[{')
|
||||
newstr = newstr.replaceAll("]\"}", '}]')
|
||||
}
|
||||
|
||||
result = JSON.parse(newstr)
|
||||
jsonvalid = true
|
||||
} catch (e) {
|
||||
|
||||
//console.log("Failed parsing JSON even though its valid (2): ", e)
|
||||
jsonvalid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonvalid && typeof result === "number") {
|
||||
jsonvalid = false
|
||||
}
|
||||
|
||||
// This is where we start recursing
|
||||
if (jsonvalid) {
|
||||
// Check fields if they can be parsed too
|
||||
try {
|
||||
for (const [key, value] of Object.entries(result)) {
|
||||
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
|
||||
//console.log("CHECKING STRING: ", value)
|
||||
|
||||
const inside_result = validateJson(value)
|
||||
if (inside_result.valid) {
|
||||
//console.log("INSIDE RESULT: ", inside_result.result)
|
||||
|
||||
if (typeof inside_result.result === "string") {
|
||||
const newres = JSON.parse(inside_result.result)
|
||||
|
||||
result[key] = newres
|
||||
} else {
|
||||
result[key] = inside_result.result
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Usually only reaches here if raw array > dict > value
|
||||
if (typeof showResult !== "array") {
|
||||
for (const [subkey, subvalue] of Object.entries(value)) {
|
||||
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
|
||||
const inside_result = validateJson(subvalue)
|
||||
if (inside_result.valid) {
|
||||
if (typeof inside_result.result === "string") {
|
||||
const newres = JSON.parse(inside_result.result)
|
||||
result[key][subkey] = newres
|
||||
} else {
|
||||
result[key][subkey] = inside_result.result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//console.log("Failed parsing inside json subvalues: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valid: jsonvalid,
|
||||
result: result,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
//Custom hook for handling styling of the dropzone
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
List as ListIcon,
|
||||
Publish as PublishIcon,
|
||||
GetApp as GetAppIcon,
|
||||
Image as ImageIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
// Additional Components
|
||||
@@ -139,8 +140,115 @@ const getCookie = (name) => {
|
||||
return "";
|
||||
};
|
||||
|
||||
export const HandleJsonCopy = (base, copy, base_node_name) => {
|
||||
if (typeof copy.name === "string") {
|
||||
copy.name = copy.name.replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
//lol
|
||||
if (typeof base === 'object' || typeof base === 'dict') {
|
||||
base = JSON.stringify(base)
|
||||
}
|
||||
|
||||
if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") {
|
||||
base_node_name = "exec"
|
||||
}
|
||||
|
||||
//console.log("COPY: ", base_node_name, copy);
|
||||
|
||||
//var newitem = JSON.parse(base);
|
||||
var newitem = validateJson(base).result
|
||||
|
||||
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
|
||||
for (let copykey in copy.namespace) {
|
||||
if (copy.namespace[copykey].includes("Results for")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.namespace[copykey]];
|
||||
if (!isNaN(copy.namespace[copykey])) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.namespace[copykey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newitem !== undefined && newitem !== null) {
|
||||
newitem = newitem[copy.name];
|
||||
if (!isNaN(copy.name)) {
|
||||
to_be_copied += ".#";
|
||||
} else {
|
||||
to_be_copied += "." + copy.name;
|
||||
}
|
||||
}
|
||||
|
||||
to_be_copied = to_be_copied.replaceAll(" ", "_");
|
||||
console.log("COPY: ", to_be_copied);
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
//console.log("NAVIGATOR: ", navigator);
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(to_be_copied).catch(() => {
|
||||
const temp = document.createElement("textarea");
|
||||
temp.value = to_be_copied;
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(temp);
|
||||
});
|
||||
//console.log("COPYING!");
|
||||
toast("Copied JSON path to clipboard.")
|
||||
} else {
|
||||
console.log("Couldn't find element ", elementName);
|
||||
}
|
||||
}
|
||||
|
||||
export const handleReactJsonClipboard = (copy) => {
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText = document.getElementById(elementName);
|
||||
if (copyText !== null && copyText !== undefined) {
|
||||
if (
|
||||
copy.namespace !== undefined &&
|
||||
copy.name !== undefined &&
|
||||
copy.src !== undefined
|
||||
) {
|
||||
copy = copy.src;
|
||||
}
|
||||
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
var stringified = JSON.stringify(copy);
|
||||
if (stringified.startsWith('"') && stringified.endsWith('"')) {
|
||||
stringified = stringified.substring(1, stringified.length - 1);
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(stringified).catch(() => {
|
||||
const temp = document.createElement("textarea");
|
||||
temp.value = stringified;
|
||||
document.body.appendChild(temp);
|
||||
temp.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(temp);
|
||||
});
|
||||
|
||||
console.log("COPYING!");
|
||||
toast.success("Copied Value, NOT json path.")
|
||||
} else {
|
||||
console.log("Failed to copy from " + elementName + ": ", copyText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -150,7 +258,7 @@ const getCookie = (name) => {
|
||||
export const GetIconInfo = (action) => {
|
||||
// Finds the icon based on the action. Should be verbs.
|
||||
const iconList = [
|
||||
{ key: "cases", values: ["cases", "ticket", "alert"] },
|
||||
{ key: "cases", values: ["cases", "ticket", "tickets", "alert"] },
|
||||
{ key: "cache_add", values: ["set_cache"] },
|
||||
{ key: "cache_get", values: ["get_cache"] },
|
||||
{ key: "filter", values: ["filter"] },
|
||||
@@ -181,7 +289,7 @@ export const GetIconInfo = (action) => {
|
||||
key: "repeat",
|
||||
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
|
||||
},
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise"] },
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise", "control", "ctrl"] },
|
||||
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
|
||||
{ key: "inflate", values: ["inflate", "pack", "compress"] },
|
||||
{
|
||||
@@ -211,7 +319,6 @@ export const GetIconInfo = (action) => {
|
||||
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
|
||||
{ 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"] },
|
||||
{
|
||||
key: "send",
|
||||
@@ -235,7 +342,8 @@ export const GetIconInfo = (action) => {
|
||||
"passwd",
|
||||
"protect",
|
||||
],
|
||||
}
|
||||
},
|
||||
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", ] },
|
||||
];
|
||||
|
||||
var selectedKey = ""
|
||||
@@ -482,29 +590,35 @@ export const GetIconInfo = (action) => {
|
||||
return selectedItem;
|
||||
};
|
||||
|
||||
export const collapseField = (field, inputdata) => {
|
||||
if (field === undefined || field === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
export const collapseField = (field) => {
|
||||
if (field === undefined || field === null) {
|
||||
return true
|
||||
if (field.name === "headers" || field.name === "cookies") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.name === "result") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (field.type === "array") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If more than 10 keys in object, collapse
|
||||
if (field.type === "object") {
|
||||
if (Object.keys(field.src).length > 7) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (field.name === "headers" || field.name === "cookies") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (field.type === "array") {
|
||||
return true
|
||||
}
|
||||
|
||||
// If more than 10 keys in object, collapse
|
||||
if (field.type === "object") {
|
||||
if (Object.keys(field.src).length > 7) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return false
|
||||
}
|
||||
|
||||
export const validateJson = (showResult) => {
|
||||
@@ -700,7 +814,8 @@ const Workflows2 = (props) => {
|
||||
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true" || isCloud)
|
||||
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true")
|
||||
const [showWorkflowImages, setShowWorkflowImages] = React.useState(localStorage?.getItem("showWorkflowImages") === "true")
|
||||
|
||||
const imgSize = 60;
|
||||
|
||||
@@ -2158,7 +2273,7 @@ const Workflows2 = (props) => {
|
||||
position: "relative",
|
||||
borderRadius: "8px",
|
||||
// backgroundColor: "#212121",
|
||||
padding: "15px 15px 0px 20px",
|
||||
padding: "15px 15px 0px 30px",
|
||||
};
|
||||
|
||||
const gridContainer = {
|
||||
@@ -2177,7 +2292,6 @@ const Workflows2 = (props) => {
|
||||
justifyContent: "space-between",
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
textAlign: "center",
|
||||
margin: "auto",
|
||||
};
|
||||
|
||||
const exportAllWorkflows = (allWorkflows) => {
|
||||
@@ -2959,7 +3073,7 @@ const Workflows2 = (props) => {
|
||||
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
|
||||
if (foundOrg !== undefined && foundOrg !== null) {
|
||||
//position: "absolute", bottom: 5, right: -5,
|
||||
imageStyle.border = foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null
|
||||
imageStyle.border = foundOrg.id === userdata.active_org.id ? `2px solid ${boxColor}` : null
|
||||
|
||||
|
||||
image =
|
||||
@@ -2997,10 +3111,10 @@ const Workflows2 = (props) => {
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
imageStyle.border = `2px solid ${green}`
|
||||
break
|
||||
} else {
|
||||
imageStyle.border = `3px solid ${red}`
|
||||
imageStyle.border = `2px solid ${red}`
|
||||
}
|
||||
|
||||
|
||||
@@ -3012,10 +3126,10 @@ const Workflows2 = (props) => {
|
||||
|
||||
relevantTrigger = trigger
|
||||
if (trigger?.status === "running") {
|
||||
imageStyle.border = `3px solid ${green}`
|
||||
imageStyle.border = `2px solid ${green}`
|
||||
break
|
||||
} else {
|
||||
imageStyle.border = `3px solid ${red}`
|
||||
imageStyle.border = `2px solid ${red}`
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3081,13 +3195,12 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
|
||||
//const isPublicWorkflow = data?.objectID === undefined || data?.objectID === null
|
||||
const foundImage = data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
|
||||
|
||||
const foundImage = currTab !== 2 && showWorkflowImages === false ? "" : data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
|
||||
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
|
||||
return (
|
||||
<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, overflow: "hidden", }}
|
||||
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : "inherit", borderBottom: isDistributed || hasSuborgs ? `1px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily, overflow: "hidden", }}
|
||||
>
|
||||
|
||||
{foundImage?.length > 0 ?
|
||||
@@ -3140,26 +3253,7 @@ const Workflows2 = (props) => {
|
||||
style={{ display: "flex", flexDirection: "column", width: "100%", fontFamily: theme.typography?.fontFamily }}
|
||||
>
|
||||
<Grid item style={{ display: "flex", maxHeight: 34 }}>
|
||||
{currTab === 2 ? null :
|
||||
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
|
||||
<div
|
||||
style={{ cursor: "" }}
|
||||
onClick={() => {
|
||||
//navigate("/admin")
|
||||
}}
|
||||
>
|
||||
{image?.includes("data:image") ?
|
||||
<img
|
||||
alt={orgName}
|
||||
src={image}
|
||||
style={imageStyle}
|
||||
/>
|
||||
:
|
||||
image
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
|
||||
<Tooltip arrow
|
||||
onMouseEnter={() => {
|
||||
@@ -3228,7 +3322,7 @@ const Workflows2 = (props) => {
|
||||
|
||||
<Typography
|
||||
style={{
|
||||
textAlign: "center",
|
||||
textAlign: "left",
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0,
|
||||
fontSize: 22,
|
||||
@@ -3239,8 +3333,9 @@ const Workflows2 = (props) => {
|
||||
fontWeight: 500,
|
||||
minWidth: 375,
|
||||
maxWidth: 375,
|
||||
margin: "auto",
|
||||
overflow: "hidden",
|
||||
|
||||
color: "rgba(255,255,255,0.85)",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
@@ -3431,14 +3526,13 @@ const Workflows2 = (props) => {
|
||||
style={{
|
||||
justifyContent: "left",
|
||||
overflow: "hidden",
|
||||
marginTop: 13,
|
||||
marginTop: 8,
|
||||
maxHeight: 35,
|
||||
fontFamily: theme.typography?.fontFamily,
|
||||
|
||||
textAlign: "center",
|
||||
textAlign: "left",
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
margin: "auto",
|
||||
}}
|
||||
>
|
||||
{data.tags !== undefined && data.tags !== null
|
||||
@@ -3478,7 +3572,7 @@ const Workflows2 = (props) => {
|
||||
|
||||
{(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ?
|
||||
<Tooltip title="Edit Form" placement="top">
|
||||
<div style={{ position: "absolute", top: 80, right: 8, }}>
|
||||
<div style={{ position: "absolute", top: 100, right: 8, }}>
|
||||
<IconButton
|
||||
aria-label="more"
|
||||
aria-controls="long-menu"
|
||||
@@ -3521,6 +3615,27 @@ const Workflows2 = (props) => {
|
||||
</Tooltip>
|
||||
: null}
|
||||
|
||||
{currTab === 2 ? null :
|
||||
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
|
||||
<div
|
||||
style={{ position: "absolute", top: 70, right: -2, cursor: "" }}
|
||||
onClick={() => {
|
||||
//navigate("/admin")
|
||||
}}
|
||||
>
|
||||
{image?.includes("data:image") ?
|
||||
<img
|
||||
alt={orgName}
|
||||
src={image}
|
||||
style={imageStyle}
|
||||
/>
|
||||
:
|
||||
image
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
</Grid>
|
||||
|
||||
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
|
||||
@@ -5305,6 +5420,21 @@ const Workflows2 = (props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Show/Hide Workflow image" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showWorkflowImages ? "#1a1a1a" : theme.palette.text.primary, background: showWorkflowImages ? theme.palette.primary.main : theme.palette.platformColor}}
|
||||
onClick={() => {
|
||||
|
||||
const newView = !showWorkflowImages
|
||||
localStorage.setItem("showWorkflowImages", newView)
|
||||
setShowWorkflowImages(!showWorkflowImages)
|
||||
}}
|
||||
disabled={currTab === 2}
|
||||
>
|
||||
<ImageIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
|
||||
<IconButton
|
||||
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
|
||||
|
||||
Reference in New Issue
Block a user