Tons of minor fixes to the based on cloud tests

This commit is contained in:
Frikky
2025-11-12 11:39:18 +01:00
parent e470dbef7f
commit 822e7c1eb3
36 changed files with 3353 additions and 1882 deletions
+45 -14
View File
@@ -23,7 +23,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import theme, { getTheme } from '../theme.jsx'; import theme, { getTheme } from '../theme.jsx';
import { Button, Skeleton, Tooltip } from '@mui/material'; import { Box, Button, Skeleton, Tooltip, Typography } from '@mui/material';
import { Index } from 'react-instantsearch-dom'; import { Index } from 'react-instantsearch-dom';
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@@ -265,7 +265,7 @@ const AdminNavBar = (props) => {
: selectedOrganization?.image; : selectedOrganization?.image;
return ( return (
!isOrgLoaded && !isUserDataLoaded ? <Loader /> : !isOrgLoaded || !isUserDataLoaded ? <Loader /> :
<Wrapper> <Wrapper>
<div style={{ flexDirection: 'column', width: 220, }}> <div style={{ flexDirection: 'column', width: 220, }}>
<nav style={{ padding: '25px 25px 3px 25px', height: isCloud ? "calc(100% - 60px)" : "calc(100% - 30px)" , fontSize: '16px', borderTopLeftRadius: 8, borderBottomLeftRadius: 8, background: theme.palette.platformColor, color: '#9CA3AF' }}> <nav style={{ padding: '25px 25px 3px 25px', height: isCloud ? "calc(100% - 60px)" : "calc(100% - 30px)" , fontSize: '16px', borderTopLeftRadius: 8, borderBottomLeftRadius: 8, background: theme.palette.platformColor, color: '#9CA3AF' }}>
@@ -358,7 +358,6 @@ const AdminNavBar = (props) => {
export default AdminNavBar; export default AdminNavBar;
const Loader = () => { const Loader = () => {
const dummyItems = Array.from({ length: 6 });
const dummyNavItems = Array.from({ length: 6 }); const dummyNavItems = Array.from({ length: 6 });
const dummyTabItems = ['Org Configuration', 'SSO', 'Notifications', 'Billing & Stats', 'Branding']; const dummyTabItems = ['Org Configuration', 'SSO', 'Notifications', 'Billing & Stats', 'Branding'];
const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context); const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context);
@@ -366,9 +365,7 @@ const Loader = () => {
return ( return (
<div style={{ <div style={{
display: 'flex', display: 'flex',
width: '100%', width: '100%',
height: '100%',
minHeight: '100vh',
maxWidth: '1200px', maxWidth: '1200px',
fontFamily: 'Arial, sans-serif', fontFamily: 'Arial, sans-serif',
paddingLeft: leftSideBarOpenByClick ? windowWidth <= 1300 ? 220 : 200 : 80, paddingLeft: leftSideBarOpenByClick ? windowWidth <= 1300 ? 220 : 200 : 80,
@@ -446,15 +443,49 @@ const Loader = () => {
</div> </div>
<div style={{ flex: 1, padding: '24px' }}> <div style={{ flex: 1, padding: '24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', margin: '0 auto', alignItems: 'flex-start' }}> <div style={{ display: 'flex', flexDirection: 'column', width: '100%', margin: '0 auto', alignItems: 'flex-start' }}>
<Skeleton variant='square' width="200px" height="200px" sx={{ marginBottom: '20px' }} /> <Typography variant="h2">
<Skeleton variant="text" width={289} height={29}/>
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%', marginTop: '50px' }}> </Typography>
{dummyItems.map((_, index) => ( <Typography variant="body2" sx={{mb: 2}}>
<div key={index} style={{ display: 'flex', alignItems: 'center', width: '100%' }}> <Skeleton variant="text" width={550} height={22}/>
<Skeleton width={'400px'} height="36px" /> </Typography>
</div> <div style={{display: 'flex'}}>
))} <Skeleton variant='square' width="145px" height="145px" sx={{ borderRadius: '8px' }} />
<div style={{flexDirection: 'column', marginLeft: '10px', justifyContent: 'center', marginTop: 'auto', marginBottom: 'auto'}}>
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton
key={index}
sx={{ marginBottom: '10px', height: '35px', width: '109px', borderRadius: '4px' }}
/>
))}
</div> </div>
</div>
<Box sx={{ minWidth: 700, marginTop: 5}}>
<Typography variant="h5" sx={{ mb: 2 }}>
<Skeleton variant='text' width="40%" height={30} />
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 2, mb: 3 }}>
<Skeleton variant="rectangular" height={35} style={{borderRadius: '4px'}} />
<Skeleton variant="rectangular" height={35} style={{borderRadius: '4px'}} />
<Skeleton variant="rectangular" height={35} style={{borderRadius: '4px'}}/>
</Box>
<Skeleton variant="rectangular" height={89} sx={{ mb: 4, borderRadius: "4px"}} />
<Skeleton width="30%" height={28} sx={{ mb: 2 }} />
<Skeleton width="90%" height={20} sx={{ mb: 1 }} />
<Skeleton width="70%" height={20} sx={{ mb: 3 }} />
<Skeleton variant="rectangular" height={35} sx={{ mb: 4, borderRadius: '4px' }} />
<Skeleton width="40%" height={35} sx={{ mb: 1, borderRadius: '4px' }} />
<Skeleton width="80%" height={35} sx={{ mb: 1, borderRadius: '4px' }} />
<Skeleton variant="rectangular" height={56} sx={{ mb: 3, borderRadius: '4px' }} />
</Box>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -30,7 +30,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import throttle from "lodash/throttle"; import throttle from "lodash/throttle";
import {getTheme} from "../theme.jsx"; import {getTheme} from "../theme.jsx";
import { validateJson, collapseField, } from "../views/Workflows.jsx"; import { validateJson, collapseField, } from "../views/Workflows2.jsx";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
+1 -1
View File
@@ -33,7 +33,7 @@ import {
ClearRefinements, ClearRefinements,
connectStateResults connectStateResults
} from "react-instantsearch-dom"; } from "react-instantsearch-dom";
import { useDebouncedCallback } from "../utils/useDebouncedCallback"; import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import aa from "search-insights"; import aa from "search-insights";
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
+5 -3
View File
@@ -22,7 +22,7 @@ import {
import aa from 'search-insights' import aa from 'search-insights'
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const Appsearch = props => { const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props
const { themeMode } = useContext(Context) const { themeMode } = useContext(Context)
const theme = getTheme(themeMode) const theme = getTheme(themeMode)
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
@@ -38,6 +38,8 @@ const Appsearch = props => {
const borderRadius = 3 const borderRadius = 3
window.title = "Shuffle | Apps | Find and integration any app" window.title = "Shuffle | Apps | Find and integration any app"
const parsedInputHeight = inputHeight === undefined || inputHeight === null ? 295 : inputHeight
// value={currentRefinement} // value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
@@ -91,7 +93,7 @@ const Appsearch = props => {
var counted = 0 var counted = 0
return ( return (
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}> <Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: parsedInputHeight, minHeight: parsedInputHeight, overflowY: "auto", overflowX: "hidden", }}>
{hits.map((data, index) => { {hits.map((data, index) => {
const paperStyle = { const paperStyle = {
backgroundColor: index === mouseHoverIndex ? theme.palette.hoverColor : theme.palette.textFieldStyle.backgroundColor, backgroundColor: index === mouseHoverIndex ? theme.palette.hoverColor : theme.palette.textFieldStyle.backgroundColor,
@@ -182,7 +184,7 @@ const Appsearch = props => {
const CustomHits = connectHits(InputHits) const CustomHits = connectHits(InputHits)
return ( return (
<div style={{width: isMobile ? null : 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}> <div style={{width: isMobile ? null : 287, minHeight: parsedInputHeight, maxHeight: parsedInputHeight, padding: "16px 16px 64px 16px", alignItems: "center", }}>
<InstantSearch searchClient={searchClient} indexName="appsearch"> <InstantSearch searchClient={searchClient} indexName="appsearch">
<div style={{maxWidth: 450, margin: "auto", }}> <div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox /> <CustomSearchBox />
+1 -1
View File
@@ -3141,7 +3141,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
orgId: subOrg.id, orgId: subOrg.id,
limit: subOrg?.sync_features?.app_executions?.limit || "N/A", limit: subOrg?.sync_features?.app_executions?.limit || "N/A",
usage: stat?.monthly_app_executions || "N/A", usage: stat?.monthly_app_executions || "N/A",
workflows_usage: stat?.total_workflow_executions || "N/A", workflows_usage: stat?.monthly_workflow_executions || "N/A",
workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A", workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A",
app_runs_hard_limit: subOrg?.Billing?.app_runs_hard_limit || 0, app_runs_hard_limit: subOrg?.Billing?.app_runs_hard_limit || 0,
} }
File diff suppressed because it is too large Load Diff
+548 -105
View File
@@ -6,6 +6,7 @@ import theme from '../theme.jsx';
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify';
import AppSearch from "../components/AppSearch1.jsx"; import AppSearch from "../components/AppSearch1.jsx";
@@ -37,14 +38,24 @@ const ChatBot = (props) => {
const [appAuthentication, setAppAuthentication] = React.useState([]); const [appAuthentication, setAppAuthentication] = React.useState([]);
const [inputAuth, setInputAuth] = useState([]) const [inputAuth, setInputAuth] = useState([])
const [forceReauthentication, setForceReauthentication] = useState(false); const [forceReauthentication, setForceReauthentication] = useState(false);
const [selectedType, setSelectedType] = useState("atomic"); const [selectedType, setSelectedType] = useState("support");
const [appname, setAppname] = useState(""); const [appname, setAppname] = useState("");
const [threadId, setThreadId] = useState(""); const [threadId, setThreadId] = useState("");
const [runId, setRunId] = useState(""); const [runId, setRunId] = useState("");
// New state for thread management
const [isLoadingThread, setIsLoadingThread] = useState(false);
const [threadError, setThreadError] = useState("");
const [isActiveOrg, setIsActiveOrg] = useState(true);
const [threadOrgId, setThreadOrgId] = useState("");
const [chatDisabled, setChatDisabled] = useState(false);
const [showAppSearch, setShowAppSearch] = useState(false); const [showAppSearch, setShowAppSearch] = useState(false);
// Get thread ID from URL params
const { threadId: urlThreadId } = useParams();
let navigate = useNavigate();
const waitingMsg = "Processing..." const waitingMsg = "Processing..."
const viewWidth = isMobile ? "92%" : 800 const viewWidth = isMobile ? "92%" : 800
@@ -82,8 +93,159 @@ const ChatBot = (props) => {
} }
}, [appname]) }, [appname])
// Load existing thread if threadId is in URL
useEffect(() => {
if (urlThreadId && urlThreadId !== threadId) {
loadExistingThread(urlThreadId);
}
}, [urlThreadId]);
const loadExistingThread = (threadIdToLoad) => {
setIsLoadingThread(true);
setThreadError("");
fetch(`${globalUrl}/api/v1/conversation/thread`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ thread_id: threadIdToLoad }),
})
.then((response) => {
if (response.status === 403) {
setThreadError("You are not authorized to view this conversation.");
setIsLoadingThread(false);
return null;
}
if (response.status === 404) {
setThreadError("Conversation not found.");
setIsLoadingThread(false);
return null;
}
return response.json();
})
.then((data) => {
if (!data) {
return;
}
if (!data.success) {
setThreadError(data.message || "Failed to load conversation.");
setIsLoadingThread(false);
return;
}
// Set thread data - this is crucial for continuing the conversation
setThreadId(data.thread_id);
console.log("Loaded existing thread:", data.thread_id);
// Transform API message format to UI format
const transformedMessages = (data.messages || []).map((msg, index) => ({
id: `${data.thread_id}_${index}`,
status: msg.role === "user" ? "sent" : "received",
message: msg.content,
timestamp: msg.timestamp
}));
setMessages(transformedMessages);
setThreadOrgId(data.thread_org_id);
setIsActiveOrg(data.is_active_org);
if (!data.is_active_org) {
setChatDisabled(true);
} else {
setChatDisabled(false);
}
setIsLoadingThread(false);
})
.catch((error) => {
console.error("Error loading thread:", error);
setThreadError("Failed to load conversation. Please try again.");
setIsLoadingThread(false);
});
};
const handleClickChangeOrg = (orgId) => {
toast.info("Changing active organization - please wait!");
const data = {
org_id: orgId,
};
// Clear org-specific cached data
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
localStorage.removeItem("workflows");
localStorage.removeItem("apps");
localStorage.removeItem("dashboard_onboarding_complete");
localStorage.removeItem("dashboard_onboarding_completed");
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify(data),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
} else {
// Clear additional cached data
localStorage.removeItem("apps");
localStorage.removeItem("workflows");
localStorage.removeItem("userinfo");
localStorage.removeItem("lastTabOpenByUser");
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success === true) {
if (
responseJson?.region_url !== undefined &&
responseJson?.region_url !== null &&
responseJson?.region_url.length > 0
) {
localStorage.setItem("globalUrl", responseJson.region_url);
}
if (responseJson["reason"] === "SSO_REDIRECT") {
setTimeout(() => {
toast.info("Redirecting to SSO login page as SSO is required for this organization.");
window.location.href = responseJson["url"];
return;
}, 2000);
} else {
setTimeout(() => {
window.location.reload();
}, 2000);
}
toast.success("Successfully changed active organization - refreshing!");
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason);
} else {
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
}
}
})
.catch((error) => {
console.log("error changing: ", error);
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
});
};
window.title = "Shuffle - New Chat" window.title = "Shuffle - New Chat"
let navigate = useNavigate();
// Automatic submit handler based on a lot of stuff :) // Automatic submit handler based on a lot of stuff :)
const handleSubmit = (e, inputmsg) => { const handleSubmit = (e, inputmsg) => {
@@ -102,6 +264,8 @@ const ChatBot = (props) => {
"run_id": runId, "run_id": runId,
} }
console.log("Sending message with thread_id:", threadId);
if (appname !== undefined && appname !== null && appname !== "") { if (appname !== undefined && appname !== null && appname !== "") {
parsedData["app_name"] = appname parsedData["app_name"] = appname
} }
@@ -190,6 +354,12 @@ const ChatBot = (props) => {
if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") { if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") {
setThreadId(data.thread_id) setThreadId(data.thread_id)
// Update URL if this is a new thread (not already in URL)
if (!urlThreadId && data.thread_id !== threadId) {
console.log("Navigating to new thread URL:", data.thread_id);
navigate(`/chat/${data.thread_id}`, { replace: true });
}
} }
if (data.success === undefined) { if (data.success === undefined) {
@@ -423,8 +593,6 @@ const ChatBot = (props) => {
border: `1px solid ${theme.palette.inputColor}`, border: `1px solid ${theme.palette.inputColor}`,
}} }}
color="primary" color="primary"
fullWidth
color="primary"
onClick={(e) => { onClick={(e) => {
console.log("Click? ") console.log("Click? ")
e.preventDefault(); e.preventDefault();
@@ -520,32 +688,38 @@ const ChatBot = (props) => {
} }
function Img(props) { function Img(props) {
return <img style={{ borderRadius: theme.palette?.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />; return <img style={{
borderRadius: 12,
maxWidth: "100%",
height: "auto",
marginTop: 12,
marginBottom: 12,
border: "1px solid rgba(255, 255, 255, 0.1)",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)"
}} alt={props.alt} src={props.src} />;
} }
function CodeHandler(props) { function CodeHandler(props) {
//console.log("Codehandler PROPS: ", props)
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : "" const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
return ( return (
<div <div
style={{ style={{
minWidth: "50%", backgroundColor: props.inline ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.3)",
maxWidth: "100%", borderRadius: props.inline ? "4px" : "8px",
backgroundColor: theme.palette.inputColor, padding: props.inline ? "2px 6px" : "12px 16px",
overflowY: "auto", display: props.inline ? "inline" : "block",
margin: props.inline ? "0 2px" : "8px 0",
// Check if props.inline === true, then do it inline border: "1px solid rgba(255, 255, 255, 0.1)",
padding: props.inline ? 0 : 15, overflowX: "auto"
display: props.inline ? "inline" : "block",
}} }}
> >
<code <code
style={{ style={{
// Wrap if larger than X whiteSpace: props.inline ? "nowrap" : "pre-wrap",
whiteSpace: "pre-wrap", fontSize: props.inline ? "0.9em" : "0.85em",
overflow: "auto", fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
color: "rgba(255, 255, 255, 0.9)"
}} }}
>{propvalue}</code> >{propvalue}</code>
</div> </div>
@@ -654,7 +828,12 @@ const ChatBot = (props) => {
const Paragraph = (props) => { const Paragraph = (props) => {
return ( return (
<p style={{marginTop: 15, marginBottom: 15, }}> <p style={{
marginTop: 8,
marginBottom: 8,
lineHeight: 1.6,
color: "inherit"
}}>
{props.children} {props.children}
</p> </p>
) )
@@ -677,14 +856,183 @@ const ChatBot = (props) => {
const chatWindow = const chatWindow =
<div style={{minWidth: viewWidth, maxWidth: viewWidth, margin: "auto", textAlign: "left", minHeight: 1500, }}> <div style={{minWidth: viewWidth, maxWidth: viewWidth, margin: "auto", textAlign: "left", minHeight: 1500, }}>
{messages.length === 0 ? {/* Loading state for thread */}
<span> {isLoadingThread ? (
<h1>Shuffle AI</h1> <div style={{
textAlign: "center",
marginTop: "30vh",
display: "flex",
flexDirection: "column",
alignItems: "center"
}}>
<CircularProgress
size={48}
style={{
marginBottom: 24,
color: "#ff8544"
}}
/>
<Typography variant="h6" style={{
color: "rgba(255, 255, 255, 0.8)",
fontWeight: 500
}}>
Loading conversation...
</Typography>
<Typography variant="body2" style={{
color: "rgba(255, 255, 255, 0.5)",
marginTop: 8
}}>
Please wait while we fetch your chat history
</Typography>
</div>
) : null}
{/* Error state */}
{threadError ? (
<div style={{
textAlign: "center",
marginTop: "30vh",
padding: "0 20px"
}}>
<div style={{
backgroundColor: theme.palette.surfaceColor,
borderRadius: 16,
padding: 32,
border: "1px solid #ff4444",
maxWidth: 500,
margin: "0 auto",
boxShadow: "0 4px 20px rgba(255, 68, 68, 0.1)"
}}>
<Typography variant="h6" style={{
color: "#ff4444",
marginBottom: 16,
fontWeight: 600,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>
<span style={{marginRight: 8, fontSize: "1.5em"}}>⚠️</span>
Unable to Load Conversation
</Typography>
<Typography variant="body1" style={{
color: "rgba(255, 255, 255, 0.8)",
lineHeight: 1.6
}}>
{threadError}
</Typography>
</div>
</div>
) : null}
{!isActiveOrg && !threadError ? (
<div style={{
backgroundColor: "rgba(255, 133, 68, 0.1)",
padding: "12px 16px",
margin: "16px 0",
borderRadius: 8,
border: "1px solid rgba(255, 133, 68, 0.3)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexWrap: isMobile ? "wrap" : "nowrap",
gap: 12
}}>
<div style={{display: "flex", alignItems: "center", gap: 10, flex: 1}}>
<span style={{fontSize: "1.1em"}}>⚠️</span>
<div>
<Typography variant="body2" style={{color: "#ff8544", fontWeight: 600, marginBottom: 2}}>
Different Organization
</Typography>
<Typography variant="caption" style={{color: "rgba(255, 255, 255, 0.7)", fontSize: "0.8rem"}}>
View only - switch to participate
</Typography>
</div>
</div>
<Button
size="small"
variant="contained"
style={{
backgroundColor: "#ff8544",
color: "white",
borderRadius: 6,
textTransform: "none",
fontWeight: 600,
padding: "6px 16px",
fontSize: "0.85rem",
whiteSpace: "nowrap",
boxShadow: "none"
}}
onClick={() => {
handleClickChangeOrg(threadOrgId);
}}
>
Switch Org
</Button>
</div>
) : null}
{showSamples} {!isLoadingThread && !threadError && messages.length === 0 ?
</span> <div style={{textAlign: "center", marginTop: "25vh"}}>
<div style={{
background: `linear-gradient(135deg, #ff8544 0%, #ff6b35 100%)`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
backgroundClip: "text",
marginBottom: 16
}}>
<Typography variant="h2" style={{
fontWeight: 700,
fontSize: isMobile ? "2rem" : "2.5rem",
letterSpacing: "-0.02em"
}}>
Shuffle Support (beta)
</Typography>
</div>
<Typography variant="h6" style={{
color: "rgba(255, 255, 255, 0.6)",
fontWeight: 400,
marginBottom: 32
}}>
How can we help you today?
</Typography>
{/* Sample prompts */}
<div style={{
display: "flex",
flexDirection: isMobile ? "column" : "row",
gap: 16,
justifyContent: "center",
maxWidth: 800,
margin: "0 auto",
padding: "0 20px"
}}>
{[
"How many incidents did we get last week?",
"Answer the last email from Jim about the new project",
"Is the IP 1.2.3.4 blocked? If not, block it."
].map((sample, index) => (
<Card key={index} style={{
backgroundColor: theme.palette.surfaceColor,
borderRadius: 12,
border: "1px solid rgba(255, 255, 255, 0.1)",
cursor: "pointer",
transition: "all 0.2s ease",
flex: 1,
minWidth: isMobile ? "100%" : "200px"
}} onClick={() => setMessage(sample)}>
<CardContent style={{padding: 20}}>
<Typography variant="body2" style={{
color: "rgba(255, 255, 255, 0.8)",
lineHeight: 1.5,
fontSize: "0.9rem"
}}>
{sample}
</Typography>
</CardContent>
</Card>
))}
</div>
</div>
: null} : null}
<div <div
id="messages-window" id="messages-window"
@@ -698,140 +1046,235 @@ const ChatBot = (props) => {
}} }}
> >
{messages.map((message, index) => { {messages.map((message, index) => {
const float = message.status === "sent" ? "left" : "right"; const isUser = message.status === "sent";
const border = message.status === "error" ? "red" : "rgba(255,255,255,0.3)" const isError = message.status === "error";
const hasAction = message.action !== undefined && message.action !== null && message.action !== "";
const hasAction = message.action !== undefined && message.action !== null && message.action !== ""
return ( return (
// Make a chat bubble component <div key={index} style={{
<div key={index} style={{position: "relative", width: "100%", marginTop: 15, marginLeft: isMobile ? 10 : 0, }}> position: "relative",
<Typography variant="body1" style={{display: "flex", backgroundColor: theme.palette.surfaceColor, color: "white", padding: "0px 10px 0px 10px", borderRadius: theme.palette?.borderRadius, float: float, border: `1px solid ${border}`, "cursor": hasAction ? "pointer" : "default", maxWidth: viewWidth-30, overflowWrap: "break-word", whiteSpace: "pre-line" }} onClick={() => { width: "100%",
if (!hasAction) { marginBottom: 20,
return display: "flex",
} justifyContent: isUser ? "flex-end" : "flex-start",
marginLeft: isMobile ? 10 : 0,
}}>
<div style={{
maxWidth: "75%",
minWidth: "200px",
backgroundColor: isUser ? "#ff8544" : theme.palette.surfaceColor,
color: isUser ? "#000000" : "rgba(255, 255, 255, 0.9)",
padding: "16px 20px",
borderRadius: isUser ? "20px 20px 4px 20px" : "20px 20px 20px 4px",
border: isError ? "1px solid #ff4444" : "1px solid rgba(255,255,255,0.1)",
cursor: hasAction ? "pointer" : "default",
overflowWrap: "break-word",
boxShadow: isUser
? "0 2px 12px rgba(255, 133, 68, 0.3)"
: "0 2px 12px rgba(0, 0, 0, 0.2)",
transition: "all 0.2s ease",
"&:hover": hasAction ? {
transform: "translateY(-1px)",
boxShadow: "0 4px 16px rgba(255, 133, 68, 0.4)"
} : {}
}} onClick={() => {
if (!hasAction) {
return;
}
if (message.action === "login") { if (message.action === "login") {
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT") navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT");
} else if (message.action === "app_authentication") { } else if (message.action === "app_authentication") {
console.log("App auth action!") console.log("App auth action!");
//setAuthenticationModalOpen(true) } else {
} else { console.log("\n\nUnknown click action: ", message.action);
console.log("\n\nUnknown click action: ", message.action) }
}
}}> }}>
{message.message === waitingMsg ? <CircularProgress style={{height: 20, width: 20, marginTop: 20, marginRight: 10, }} /> : null} {message.message === waitingMsg ? (
<span> <div style={{display: "flex", alignItems: "center"}}>
<Markdown <CircularProgress size={20} style={{marginRight: 12, color: "rgba(255, 255, 255, 0.7)"}} />
components={markdownComponents} <span style={{color: "rgba(255, 255, 255, 0.7)"}}>Processing...</span>
id="markdown_wrapper" </div>
style={{ ) : (
minHeight: 20, <div>
marginTop: 0, <Markdown
display: "flex", components={markdownComponents}
flexDirection: "row", style={{
}} color: "inherit",
> lineHeight: 1.6,
{message.message} }}
</Markdown> >
{message.message}
</Markdown>
{message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" ? {message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" && !isUser ? (
<Typography variant="body2" style={{color: "rgba(255,255,255,0.5)", marginTop: 5, }}> <Typography variant="caption" style={{
Thread: {message.thread_id} color: "rgba(255,255,255,0.4)",
</Typography> marginTop: 8,
: null display: "block",
} fontSize: "0.75rem"
</span> }}>
</Typography> Thread: {message.thread_id}
</Typography>
) : null}
</div>
)}
</div>
{message.status === "error" && message.error_message ? {message.status === "error" && message.error_message ? (
<Typography variant="body2" style={{color: "red", }}> <Typography variant="body2" style={{
color: "#ff4444",
marginTop: 8,
fontSize: "0.85rem",
fontStyle: "italic"
}}>
{message.error_message} {message.error_message}
</Typography> </Typography>
: null} ) : null}
{(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ? {(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ? (
<div style={{position: "absolute", right: 0, bottom: -100, }}> <div style={{position: "absolute", right: 0, bottom: -100}}>
<AppSearch <AppSearch
placeholder={"Find your "+message.category+" app"} placeholder={"Find your "+message.category+" app"}
setNewSelectedApp={setAppname} setNewSelectedApp={setAppname}
/> />
</div> </div>
: null} ) : null}
</div> </div>
) );
})} })}
</div> </div>
{showAuthentication} {showAuthentication}
<div style={{position: "fixed", bottom: 0, left: 0, width: "100%", zIndex: 100, backgroundColor: theme.palette.platformColor, }}> <div style={{
<div style={{width: viewWidth, margin: "auto", }}> position: "fixed",
bottom: 0,
left: 0,
width: "100%",
zIndex: 100,
background: `linear-gradient(180deg, transparent 0%, ${theme.palette.platformColor} 20%)`,
backdropFilter: "blur(10px)",
borderTop: "1px solid rgba(255, 255, 255, 0.1)"
}}>
<div style={{width: viewWidth, margin: "auto", padding: "20px 0"}}>
{/* Commented out Query Type section for now
{messages.length === 0 ? {messages.length === 0 ?
<span> <div style={{marginBottom: 20}}>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" style={{
color: "rgba(255, 255, 255, 0.7)",
marginBottom: 12,
fontWeight: 500
}}>
Query Type Query Type
</Typography> </Typography>
<ButtonGroup <ButtonGroup
fullWidth fullWidth
color="secondary" style={{display: "flex", marginTop: 10}}
style={{display: "flex", marginTop: 10, }}
> >
{/*
<Button
fullWidth
disabled
variant={selectedType === "default" ? "contained" : "outlined"}
onClick={() => setSelectedType("default")}
>
Auto
</Button>
*/}
<Button
fullWidth
variant={selectedType === "atomic" ? "contained" : "outlined"}
onClick={() => setSelectedType("atomic")}
>
Auto-run action
</Button>
<Button <Button
fullWidth fullWidth
variant={selectedType === "support" ? "contained" : "outlined"} variant={selectedType === "support" ? "contained" : "outlined"}
onClick={() => setSelectedType("support")} onClick={() => setSelectedType("support")}
style={{
backgroundColor: selectedType === "support" ? "#ff8544" : "transparent",
borderColor: "#ff8544",
color: selectedType === "support" ? "white" : "#ff8544",
textTransform: "none",
fontWeight: 600,
borderRadius: "8px",
padding: "12px 24px"
}}
> >
Support Support
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</span> </div>
: null} : null}
*/}
<form onSubmit={(e) => handleSubmit(e, message)} style={{bottom: 20, marginTop: 10, marginBottom: isMobile ? 0 : 10, maxWidth: viewWidth, minWidth: viewWidth, }}> <form onSubmit={(e) => handleSubmit(e, message)} style={{
marginBottom: isMobile ? 10 : 20,
maxWidth: viewWidth,
minWidth: viewWidth
}}>
<TextField <TextField
id="message" id="message"
fullWidth fullWidth
disabled={loading} disabled={loading || chatDisabled}
label="Send a message" placeholder={chatDisabled ? "Chat disabled - switch organization to participate" : "Type your message..."}
value={message} value={message}
onChange={(e) => setMessage(e.target.value)} onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e, message);
}
}}
variant="outlined" variant="outlined"
autoFocus autoFocus={!chatDisabled}
multiline
maxRows={4}
sx={{
'& .MuiOutlinedInput-root': {
backgroundColor: theme.palette.surfaceColor,
borderRadius: '12px',
border: '1px solid rgba(255, 255, 255, 0.15)',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
'&:hover': {
border: '1px solid rgba(255, 133, 68, 0.4)',
},
'&.Mui-focused': {
border: '1px solid #ff8544',
boxShadow: '0 0 0 2px rgba(255, 133, 68, 0.1)'
}
},
'& .MuiOutlinedInput-input': {
color: 'rgba(255, 255, 255, 0.9)',
padding: '12px 16px',
fontSize: '0.95rem'
},
'& .MuiInputLabel-root': {
color: 'rgba(255, 255, 255, 0.6)',
}
}}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
<IconButton <IconButton
aria-label="send message" aria-label="send message"
disabled={chatDisabled || loading || !message.trim()}
onClick={(e) => handleSubmit(e, message)} onClick={(e) => handleSubmit(e, message)}
style={{
backgroundColor: (!chatDisabled && !loading && message.trim()) ? "#ff8544" : "rgba(255, 255, 255, 0.1)",
color: "white",
margin: "2px",
padding: "8px",
borderRadius: "8px",
transition: "all 0.2s ease"
}}
> >
<SendIcon color="primary" /> <SendIcon />
</IconButton> </IconButton>
) )
}} }}
/> />
</form> </form>
{isMobile ? null :
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 0,}} > {!isMobile ? (
{`The Shuffle AI is a test system for automatic workflow generation and atomic functions for the future of Shuffle. Shuffle AI may use your organization info in the query, and attempts to auto-correct any failed behavior. If you have any questions, please contact us at ${supportEmail}`} <Typography variant="caption" style={{
color: "rgba(255, 255, 255, 0.5)",
textAlign: "center",
display: "block",
fontSize: "0.75rem",
lineHeight: 1.4,
maxWidth: "80%",
margin: "0 auto"
}}>
{messages.length === 0
? `The Shuffle AI is a test system for automatic workflow generation and atomic functions for the future of Shuffle. Shuffle AI may use your organization info in the query, and attempts to auto-correct any failed behavior. If you have any questions, please contact us at ${supportEmail}`
: "Shuffle AI can make mistakes. Always double-check important information."
}
</Typography> </Typography>
} ) : null}
</div> </div>
</div> </div>
</div> </div>
+9 -3
View File
@@ -49,7 +49,7 @@ const CloudSyncTab = (props) => {
const itemColor = "white"; const itemColor = "white";
const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io"; const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io";
const { themeMode, brandColor } = useContext(Context); const { themeMode, brandColor, setUpdateOrg } = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
useEffect(() => { getSettings(); }, []); useEffect(() => { getSettings(); }, []);
@@ -327,7 +327,6 @@ const CloudSyncTab = (props) => {
); );
}; };
const handleGetOrg = (orgId) => { const handleGetOrg = (orgId) => {
if (serverside !== true && window.location.search !== undefined && window.location.search !== null) { if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries()); const params = Object.fromEntries(urlSearchParams.entries());
@@ -337,11 +336,16 @@ const CloudSyncTab = (props) => {
} }
} }
if (orgId === undefined || orgId === null || (orgId.length !== 36 && orgId.length !== 0)) {
return
}
if (orgId.length === 0) { if (orgId.length === 0) {
toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout."); toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.");
return; return;
} }
// Just use this one? // Just use this one?
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
@@ -359,7 +363,7 @@ const CloudSyncTab = (props) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
toast("Failed getting your org. If this persists, please contact support."); toast.warn("Failed getting your org. If this persists, please contact support (cloud sync)")
} else { } else {
if ( if (
responseJson.sync_features === undefined || responseJson.sync_features === undefined ||
@@ -457,6 +461,8 @@ const CloudSyncTab = (props) => {
setLoading(false); setLoading(false);
if (response.status === 200) { if (response.status === 200) {
console.log("Cloud sync success?"); console.log("Cloud sync success?");
handleGetOrg(userdata.active_org.id);
setUpdateOrg(true);
} else { } else {
console.log("Cloud sync fail?"); console.log("Cloud sync fail?");
} }
+1 -1
View File
@@ -36,7 +36,7 @@ import {
Avatar, Avatar,
AvatarGroup, AvatarGroup,
} from "@mui/material" } from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback"; import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CreatorGrid = props => { const CreatorGrid = props => {
+1 -1
View File
@@ -26,7 +26,7 @@ import {
ListItemAvatar, ListItemAvatar,
ListItemText, ListItemText,
} from '@mui/material'; } from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback"; import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
+10 -14
View File
@@ -432,7 +432,7 @@ const EnvironmentTab = memo((props) => {
-e SHUFFLE_SWARM_CONFIG=run \\ -e SHUFFLE_SWARM_CONFIG=run \\
-e BASE_URL="${newUrl}" \\${addProxy ? ` -e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? ` -e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \ \n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
-v /tmp:/tmp \\` : ""} -v /tmp:/tmp \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest ghcr.io/shuffle/shuffle-orborus:latest
`) `)
@@ -467,8 +467,7 @@ const EnvironmentTab = memo((props) => {
-e ENVIRONMENT_NAME="${environment.Name}" \\ -e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${props.userdata.active_org.id}" \\ -e ORG="${props.userdata.active_org.id}" \\
-e BASE_URL="${newUrl}" \\${addProxy ? ` -e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest` ghcr.io/shuffle/shuffle-orborus:latest`
return commandData return commandData
@@ -959,7 +958,7 @@ const EnvironmentTab = memo((props) => {
display: "grid", display: "grid",
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px", gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
width: "100%", width: "100%",
minWidth: 800, minWidth: showLoader ? 800 : 0,
paddingBottom: 0, paddingBottom: 0,
borderBottom: theme.palette.defaultBorder, borderBottom: theme.palette.defaultBorder,
}} }}
@@ -976,7 +975,7 @@ const EnvironmentTab = memo((props) => {
textOverflow: "ellipsis", textOverflow: "ellipsis",
overflow: "hidden", overflow: "hidden",
fontWeight: "bold", fontWeight: "bold",
textAlign: header === "Actions" ? "left" : header === "Distribution" ? "right" : "center", textAlign: "center",
}} }}
/> />
) )
@@ -987,7 +986,6 @@ const EnvironmentTab = memo((props) => {
<ListItem <ListItem
key={rowIndex} key={rowIndex}
style={{ style={{
display: "grid",
backgroundColor: theme.palette.platformColor, backgroundColor: theme.palette.platformColor,
height: 40, height: 40,
width: "100%", width: "100%",
@@ -1674,10 +1672,9 @@ const EnvironmentTab = memo((props) => {
setUpdate(Math.random()) setUpdate(Math.random())
}} }}
/> />
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography> <Typography variant='body2' color="textSecondary">Disable Detection Controller</Typography>
</div> </div>
{/*
<div style={{display: 'flex', alignItems: 'center', }}> <div style={{display: 'flex', alignItems: 'center', }}>
<Checkbox <Checkbox
id="shuffle_skip_pipelines" id="shuffle_skip_pipelines"
@@ -1692,9 +1689,8 @@ const EnvironmentTab = memo((props) => {
setUpdate(Math.random()) setUpdate(Math.random())
}} }}
/> />
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography> <Typography variant='body2' color="textSecondary">Use Remote Pipeline</Typography>
</div> </div>
*/}
</div> </div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}> <Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
@@ -1706,16 +1702,16 @@ const EnvironmentTab = memo((props) => {
</Typography> </Typography>
</div> </div>
</div> </div>
{currentEnvQueue.length === 0 ? null : {currentEnvQueue.length === 0 ? null :
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}> <List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
{currentEnvQueue.map((queueItem, queueIndex) => { {currentEnvQueue.map((queueItem, queueIndex) => {
return ( return (
<ListItem <ListItem
key={queueIndex}
style={{ style={{
backgroundColor: theme.palette.surfaceColor, backgroundColor: theme.palette.platformColor,
border: '1px solid #333333',
borderBottom: theme.palette.defaultBorder, borderBottom: theme.palette.defaultBorder,
maxHeight: 50,
}} }}
> >
<ListItemText style={{minWidth: 50, maxWidth: 50, }}> <ListItemText style={{minWidth: 50, maxWidth: 50, }}>
@@ -1767,7 +1763,7 @@ const EnvironmentTab = memo((props) => {
padding: 15, padding: 15,
textAlign: "center", textAlign: "center",
height: 70, height: 70,
backgroundColor: theme.palette.surfaceColor, backgroundColor: theme.palette.platformColor,
display: "flex", display: "flex",
}} }}
> >
+1 -1
View File
@@ -4,7 +4,7 @@ import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as Pa
import theme from '../theme.jsx'; import theme from '../theme.jsx';
import ReactJson from "react-json-view-ssr"; import ReactJson from "react-json-view-ssr";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { validateJson } from "../views/Workflows.jsx"; import { validateJson } from "../views/Workflows2.jsx";
// import HandleJsonCopy from "./ShuffleCodeEditor1"; // import HandleJsonCopy from "./ShuffleCodeEditor1";
const STATUS_CONFIG = { const STATUS_CONFIG = {
+8 -4
View File
@@ -64,7 +64,7 @@ const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, setSearchBarModalOpen, searchBarModalOpen, logoutUrl, themeMode, handleThemeChange, isDocSearchModalOpen, setIsDocSearchModalOpen, supportEmail} = useContext(Context); const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, updateOrg, setUpdateOrg, setSearchBarModalOpen, searchBarModalOpen, logoutUrl, themeMode, handleThemeChange, isDocSearchModalOpen, setIsDocSearchModalOpen, supportEmail} = useContext(Context);
const [expandLeftNav, setExpandLeftNav] = useState(false); const [expandLeftNav, setExpandLeftNav] = useState(false);
const [activeOrgName, setActiveOrgName] = useState( const [activeOrgName, setActiveOrgName] = useState(
userdata?.active_org?.name || "Select Organziation" userdata?.active_org?.name || "Select Organziation"
@@ -684,7 +684,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}> <Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: <a href="https://github.com/Shuffle/Shuffle/releases" style={{ color: theme.palette.text.primary, textDecoration: "underline" }} target="_blank" rel="noreferrer"> Version: <a href="https://github.com/Shuffle/Shuffle/releases" style={{ color: theme.palette.text.primary, textDecoration: "underline" }} target="_blank" rel="noreferrer">
2.1.1 2.1.2
</a> </a>
</Typography> </Typography>
</Menu> </Menu>
@@ -956,6 +956,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
.then((org) => { .then((org) => {
if (!fetched && org) { if (!fetched && org) {
setActiveOrgData(org); setActiveOrgData(org);
setUpdateOrg(false);
if (!isCloud) { if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) { if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true); setIsProdStatusOn(true);
@@ -972,7 +973,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
return () => { return () => {
fetched = true; fetched = true;
}; };
}, [userdata?.active_org?.id, globalUrl]); }, [userdata?.active_org?.id, globalUrl, updateOrg]);
return ( return (
<div <div
@@ -1226,8 +1227,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}> <Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
<Button <Button
component={Link} component={Link}
to={"/new-dashboard"} to={userdata?.support === true ? "/new-dashboard" : "/workflows"}
onClick={(event) => { onClick={(event) => {
if(!userdata?.support){
return;
}
setOpenautomateTab(true); setOpenautomateTab(true);
setOpenSecurityTab(false); setOpenSecurityTab(false);
setCurrentOpenTab("new-dashboard"); setCurrentOpenTab("new-dashboard");
@@ -35,6 +35,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import InfoIcon from "@mui/icons-material/Info";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
// Helper function to get the correct image path based on app category // Helper function to get the correct image path based on app category
@@ -1034,7 +1035,12 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
{/* Add Usecase Dialog */} {/* Add Usecase Dialog */}
<Dialog <Dialog
open={openDialog} open={openDialog}
onClose={handleCloseDialog} onClose={(event, reason) => {
if (reason !== "backdropClick") {
handleCloseDialog();
}
}}
disableEscapeKeyDown
maxWidth="md" maxWidth="md"
fullWidth fullWidth
PaperProps={{ PaperProps={{
@@ -1642,7 +1648,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
<Box key={contentIndex} sx={{ mb: 1 }}> <Box key={contentIndex} sx={{ mb: 1 }}>
<TextField <TextField
multiline multiline
rows={5} rows={6}
value={content} value={content}
onChange={(e) => { onChange={(e) => {
const newItems = [...formData.navigation.items]; const newItems = [...formData.navigation.items];
@@ -1653,7 +1659,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
navigation: { items: newItems }, navigation: { items: newItems },
}); });
}} }}
placeholder="Content" placeholder="Content (Markdown supported)"
helperText={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
<Typography variant="caption" sx={{ opacity: 0.7 }}>
Markdown syntax is supported
</Typography>
</Box>
}
fullWidth fullWidth
sx={{ sx={{
"& .MuiOutlinedInput-root": { "& .MuiOutlinedInput-root": {
@@ -1661,6 +1675,10 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
color: theme.palette.text.primary, color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder }, "& fieldset": { border: theme.palette.defaultBorder },
}, },
"& .MuiOutlinedInput-root textarea": {
overflow: "auto",
resize: "vertical",
},
}} }}
/> />
{item.content.length > 1 && ( {item.content.length > 1 && (
+20 -13
View File
@@ -104,13 +104,12 @@ const RunDetectionTest = (props) => {
return return
} }
if (haveDetectionPipelines() === false) { if (haveDetectionPipelines().length === 0) {
setDetectionTestRunning(false) setDetectionTestRunning(false)
toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.") toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.")
return return
} }
// 1. Run a new pipeline which exits. // 1. Run a new pipeline which exits.
const detectionTest = `from {message: "<165>1 2025-10-06T12:34:56.789Z myhost.example.com myapp 1234 ID47 [huh eventSource=\\\"App\\\" EventID=\\\"4688\\\" NewProcessName=\\\"notepad.exe\\\" Context=\\\"Testing\\\"] This is a test log message"} | this = message.parse_syslog() | import` const detectionTest = `from {message: "<165>1 2025-10-06T12:34:56.789Z myhost.example.com myapp 1234 ID47 [huh eventSource=\\\"App\\\" EventID=\\\"4688\\\" NewProcessName=\\\"notepad.exe\\\" Context=\\\"Testing\\\"] This is a test log message"} | this = message.parse_syslog() | import`
@@ -124,7 +123,17 @@ const RunDetectionTest = (props) => {
// 1. Submit it to run // 1. Submit it to run
// 2. Check executions if they happened recently~ // 2. Check executions if they happened recently~
if (submitPipelineWrapper !== undefined) { if (submitPipelineWrapper !== undefined) {
submitPipelineWrapper(detectionTest)
//`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
const validEnv = haveDetectionPipelines()
for (var envKey in validEnv) {
const envName = validEnv[envKey]
submitPipelineWrapper(detectionTest, envName)
}
//if (validEnv.length > 0) {
//toast.success(`Submitted detection test pipeline to environment(s): ${validEnv.join(", ")}`)
//}
} }
for (var i = 0; i < 10; i++) { for (var i = 0; i < 10; i++) {
@@ -141,10 +150,10 @@ const RunDetectionTest = (props) => {
const haveDetectionPipelines = () => { const haveDetectionPipelines = () => {
if (pipelines === undefined) { if (pipelines === undefined) {
toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.") toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.")
return false return []
} }
var foundCorrect = 0 var foundCorrect = []
for (var pipelineKey in pipelines) { for (var pipelineKey in pipelines) {
const curPipeline = pipelines[pipelineKey] const curPipeline = pipelines[pipelineKey]
//if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) { //if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) {
@@ -152,27 +161,25 @@ const RunDetectionTest = (props) => {
//} //}
if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) { if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) {
foundCorrect += 1 foundCorrect.push(curPipeline.environment)
} }
} }
if (foundCorrect >= 1) { return foundCorrect
return true
}
return false
} }
return ( return (
<div style={{display: "flex", }}> <div style={{display: "flex", }}>
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}> <ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
<Tooltip title={"Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline"} style={{}} aria-label={"Run detection test"}> <Tooltip title={
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
} style={{}} aria-label={"Run detection test"}>
<div> <div>
<Button <Button
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }} style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
variant="outlined" variant="outlined"
color="secondary" color="secondary"
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning} disabled={haveDetectionPipelines().length === 0 || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
onClick={() => { onClick={() => {
//setPipelineModalOpen(true) //setPipelineModalOpen(true)
runDetectionTest() runDetectionTest()
+280 -25
View File
@@ -13,8 +13,51 @@ function formatCompactNumber(value) {
return `${n}`; return `${n}`;
} }
const RunsOverTimeWidget = (props) => { // Helpers to keep date and "today" logic concise and consistent
const { globalUrl, onLoadingChange, monthOverride, dummyMode } = props; function toYMD(date) {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${dd}`;
}
function entriesContainDateYMD(entries, date) {
const target = toYMD(date);
return Array.isArray(entries) && entries.some((e) => toYMD(e.date) === target);
}
function computeTodayValueForAll(key, data) {
if (key === 'app_executions') {
return Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
}
if (key === 'workflow_executions') {
const parent = Number(data?.daily_workflow_executions ?? 0);
const parentFinished = Number(data?.daily_workflow_executions_finished ?? 0);
const parentFailed = Number(data?.daily_workflow_executions_failed ?? 0);
const childFinished = Number(data?.daily_child_workflow_executions_finished ?? 0);
const childFailed = Number(data?.daily_child_workflow_executions_failed ?? 0);
const parentVal = parent > 0 ? parent : (parentFinished + parentFailed);
return parentVal + childFinished + childFailed;
}
return 0;
}
function computeTodayValueForOrg(key, orgStats) {
if (key === 'workflow_executions') {
const total = orgStats?.daily_workflow_executions;
const finished = Number(orgStats?.daily_workflow_executions_finished ?? 0);
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
return total !== undefined ? Number(total || 0) : (finished + failed);
}
if (key === 'app_executions') {
return Number(orgStats?.daily_app_executions ?? 0);
}
return 0;
}
const RunsOverTimeWidget = (props) => {
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows' const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
const [series, setSeries] = useState([]); const [series, setSeries] = useState([]);
const [days, setDays] = useState(365); // aggregate to last 12 months by default const [days, setDays] = useState(365); // aggregate to last 12 months by default
@@ -24,6 +67,99 @@ const RunsOverTimeWidget = (props) => {
// Helper: fetch time series for a specific statistics key // Helper: fetch time series for a specific statistics key
const fetchSeriesForKey = async (key) => { const fetchSeriesForKey = async (key) => {
try { try {
// If specific org is selected and pre-fetched stats are available, use them directly
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
const dailyStats = orgStats.daily_statistics;
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (!day?.date) continue;
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue;
let value = 0;
if (key === 'workflow_executions') {
const finished = Number(day?.workflow_executions_finished || 0);
const failed = Number(day?.workflow_executions_failed || 0);
value = finished + failed;
} else {
value = Number(day?.[key] || 0);
}
processedEntries.push({ date: day.date, value });
}
// Append today's values if not present
try {
if (!entriesContainDateYMD(processedEntries, new Date())) {
const todayVal = computeTodayValueForOrg(key, orgStats);
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
// If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions
if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) {
// Use the full stats endpoint to get daily_statistics
const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`;
const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
if (r.ok) {
const data = await r.json();
const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
// Process each day's data, combining parent + child org runs
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (day?.date) {
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue; // Filter by days
let value = day?.[key] || 0;
// Add child org executions based on the key
if (key === 'app_executions' && day?.child_app_executions !== undefined) {
value += (day.child_app_executions || 0);
} else if (key === 'workflow_executions') {
// For workflow_executions, sum both child_workflow_executions_finished and child_workflow_executions_failed
const childFinished = day?.child_workflow_executions_finished || 0;
const childFailed = day?.child_workflow_executions_failed || 0;
value += (childFinished + childFailed);
}
processedEntries.push({ date: day.date, value });
}
}
// Append today's datapoint for ALL by combining parent + child daily_* counters
try {
if (!entriesContainDateYMD(processedEntries, new Date())) {
const todayVal = computeTodayValueForAll(key, data);
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
}
// // If a specific org is selected
// if (selectedOrgForStats && selectedOrgForStats !== 'ALL') {
// const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
// const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
// if (r.ok) {
// const j = await r.json();
// return Array.isArray(j?.entries) ? j.entries : [];
// }
// }
// Fallback to global stats
const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`; const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
const doFetch = async (u) => { const doFetch = async (u) => {
const r = await fetch(u, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } }); const r = await fetch(u, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
@@ -34,23 +170,15 @@ const RunsOverTimeWidget = (props) => {
const a = await doFetch(urlA); const a = await doFetch(urlA);
if (a.length > 0) return a; if (a.length > 0) return a;
// Optional org route fallback if present globally // // Final fallback: old aggregate endpoint returning daily_statistics
const orgId = (window && window.selectedOrganization && window.selectedOrganization.id) || null; // const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
if (orgId) { // if (fallback.ok) {
const urlB = `${globalUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`; // const data = await fallback.json();
const b = await doFetch(urlB); // const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
if (b.length > 0) return b; // const valField = key;
} // return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
// }
// Final fallback: old aggregate endpoint returning daily_statistics // return [];
const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
if (fallback.ok) {
const data = await fallback.json();
const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
const valField = key;
return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
}
return [];
} catch (e) { } catch (e) {
return []; return [];
} }
@@ -154,9 +282,13 @@ const RunsOverTimeWidget = (props) => {
} }
}, [monthOverride]); }, [monthOverride]);
useEffect(() => { useEffect(() => {
load(mode); if (loadingSelectedOrgStats === true) {
}, [mode, globalUrl, days, selectedMonth, dummyMode]); setSeries([]);
return;
}
load(mode);
}, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
// Notify parent on loading changes // Notify parent on loading changes
useEffect(() => { useEffect(() => {
@@ -169,7 +301,7 @@ const RunsOverTimeWidget = (props) => {
(Array.isArray(series) ? series : []).map((d) => { (Array.isArray(series) ? series : []).map((d) => {
const dt = new Date(d.key); const dt = new Date(d.key);
const label = selectedMonth instanceof Date const label = selectedMonth instanceof Date
? String(dt.getDate()) // day of month for daily view ? `${dt.toLocaleString('default', { month: 'short' })} ${dt.getDate()}` // e.g., 'Oct 1'
: dt.toLocaleString('default', { month: 'short' }); : dt.toLocaleString('default', { month: 'short' });
return { key: label, data: Number(d?.data || 0) }; return { key: label, data: Number(d?.data || 0) };
}) })
@@ -197,7 +329,7 @@ const RunsOverTimeWidget = (props) => {
backgroundColor: "#1A1A1A", backgroundColor: "#1A1A1A",
border: "1px solid rgba(255,255,255,0.22)", border: "1px solid rgba(255,255,255,0.22)",
color: theme.palette.text.primary, color: theme.palette.text.primary,
padding: 8, padding: 10,
maxWidth: 240, maxWidth: 240,
pointerEvents: 'none', pointerEvents: 'none',
}}> }}>
@@ -208,6 +340,7 @@ const RunsOverTimeWidget = (props) => {
/> />
} }
/>; />;
return ( return (
<div> <div>
@@ -295,7 +428,12 @@ const RunsOverTimeWidget = (props) => {
</div> </div>
<div style={{ marginTop: 24 }}> <div style={{ marginTop: 24 }}>
<div style={{ width: '100%' }}> <div style={{ width: '100%', position: 'relative' }}>
{loadingSelectedOrgStats === true ? (
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="body2" color="textSecondary">Loading</Typography>
</div>
) : (
<BarChart <BarChart
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`} key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
height={300} height={300}
@@ -315,6 +453,123 @@ const RunsOverTimeWidget = (props) => {
} }
animated={false} animated={false}
/> />
)}
{/* Custom reference line overlays for app execution limits (suborg and parent) */}
{mode === 'apps' && barData.length > 0 && (() => {
// Determine limits
const parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null;
const suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null;
const maxValue = Math.max(...barData.map(d => Number(d.data) || 0), 0);
if (maxValue <= 0) return null;
const chartTopPadding = 1;
const chartBottomPadding = 8;
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
// Helpers to compute Y position in percentage
const posFor = (limit) => {
const perc = Math.max(0, Math.min(1, (Number(limit) || 0) / maxValue));
return chartTopPadding + ((1 - perc) * usableHeight);
};
let showParentLimit = false;
let showSuborgLimit = false;
// Suborg/current limit: only when viewing a specific org (not ALL)
if (selectedOrgForStats !== 'ALL' && typeof suborgAppExecutionLimit === 'number' && suborgAppExecutionLimit > 0 && suborgAppExecutionLimit <= maxValue && selectedOrgForStats === orgForLimit?.id) {
showSuborgLimit = true;
}
if (typeof parentAppExecutionLimit === 'number' && parentAppExecutionLimit > 0 && parentAppExecutionLimit <= maxValue) {
if (selectedOrgForStats === 'ALL') {
showParentLimit = true;
} else if (parentAppExecutionLimit !== suborgAppExecutionLimit) {
showParentLimit = true;
}
}
if (!showParentLimit && !showSuborgLimit) return null;
const suborgTop = showSuborgLimit ? posFor(suborgAppExecutionLimit) : null;
const parentTop = showParentLimit ? posFor(parentAppExecutionLimit) : null;
return (
<>
{showSuborgLimit && (
<>
<div
style={{
position: 'absolute',
top: `${suborgTop}%`,
left: '2%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
pointerEvents: 'none',
zIndex: 10,
}}
/>
<div
style={{
position: 'absolute',
top: `${suborgTop}%`,
right: '-5%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 10,
backgroundColor: 'rgba(26, 26, 26, 0.95)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #ff8544',
}}
>
<Typography style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}>
{formatCompactNumber(suborgAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
{showParentLimit && (
<>
<div
style={{
position: 'absolute',
top: `${parentTop}%`,
left: '2%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
pointerEvents: 'none',
zIndex: 9,
opacity: 0.8,
}}
/>
<div
style={{
position: 'absolute',
top: `${parentTop}%`,
right: '-5%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 9,
backgroundColor: 'rgba(26, 26, 26, 0.93)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #FD4C62',
display: 'flex', alignItems: 'center', gap: 6,
}}
>
<Typography style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}>
{formatCompactNumber(parentAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
</>
);
})()}
</div> </div>
</div> </div>
</div> </div>
+3 -2
View File
@@ -768,6 +768,7 @@ const RuntimeDebugger = (props) => {
toast("Error executing workflow: "+responseJson.error) toast("Error executing workflow: "+responseJson.error)
} else { } else {
console.log("Executed workflow: ", responseJson) console.log("Executed workflow: ", responseJson)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
} }
}) })
.catch((error) => { .catch((error) => {
@@ -1236,8 +1237,8 @@ const RuntimeDebugger = (props) => {
setRowsPerPage(newPaginationModel.pageSize) setRowsPerPage(newPaginationModel.pageSize)
// No API call needed - this is just for client-side pagination // No API call needed - this is just for client-side pagination
}} }}
selectionModel={selectedWorkflowExecutions.map((workflow) => workflow.id)}
onRowSelectionModelChange={(newSelection) => { onSelectionModelChange={(newSelection) => {
//console.log("newSelection: ", newSelection) //console.log("newSelection: ", newSelection)
//setSelectedWorkflowExecutionsIndexes(newSelection) //setSelectedWorkflowExecutionsIndexes(newSelection)
var found = [] var found = []
+92 -4
View File
@@ -17,6 +17,8 @@ import {
TextField, TextField,
Chip, Chip,
CircularProgress, CircularProgress,
Select,
MenuItem,
} from '@mui/material'; } from '@mui/material';
import { import {
@@ -39,11 +41,14 @@ const SchedulesTab = memo((props) => {
const [showLoader, setShowLoader] = React.useState(true); const [showLoader, setShowLoader] = React.useState(true);
const [workflows, setWorkflows] = React.useState([]); const [workflows, setWorkflows] = React.useState([]);
const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false); const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
const [newPipelineValue, setNewPipelineValue] = React.useState(`export | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`); const [newPipelineValue, setNewPipelineValue] = React.useState(`export live=true | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
const [ticketWebhook, setTicketWebhook] = React.useState(""); const [ticketWebhook, setTicketWebhook] = React.useState("");
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState(""); const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const [environments, setEnvironments] = React.useState([]);
const [selectedEnvironment, setSelectedEnvironment] = React.useState("");
const { themeMode, brandColor } = useContext(Context); const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
@@ -74,7 +79,7 @@ const SchedulesTab = memo((props) => {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") { if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id) setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`) setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`) setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break; break;
} }
} }
@@ -91,6 +96,7 @@ const SchedulesTab = memo((props) => {
handleGetWorkflows() handleGetWorkflows()
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) { if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
handleGetAllTriggers() handleGetAllTriggers()
handleGetEnvironments()
} }
}, []) }, [])
@@ -168,8 +174,8 @@ const SchedulesTab = memo((props) => {
}) })
} }
const submitPipelineWrapper = (pipelineValue) => { const submitPipelineWrapper = (pipelineValue, environment) => {
submitPipeline(pipelineValue) submitPipeline(pipelineValue, environment)
} }
const NewPipelineView = ( const NewPipelineView = (
@@ -277,9 +283,48 @@ const SchedulesTab = memo((props) => {
setNewPipelineValue(event.target.value) setNewPipelineValue(event.target.value)
} }
/> />
</div> </div>
</DialogContent> </DialogContent>
<DialogActions style={{padding: "0px 50px 50px 50px", }}> <DialogActions style={{padding: "0px 50px 50px 50px", }}>
{environments.length === 0 ? null :
<div>
<Typography variant="body1" color="textSecondary">Runtime Location</Typography>
<Select
label="Runtime Location"
value={selectedEnvironment}
onChange={(event) => {
const selectedEnv = event.target.value
setSelectedEnvironment(selectedEnv)
}}
style={{marginRight: 300, }}
>
{environments.map((environment, index) => {
if (environment.archived) {
return null
}
if (environment.Name === "Cloud") {
return null
}
return (
<MenuItem
key={index}
style={{
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
}}
value={environment}
>
{environment.Name}
</MenuItem>
)
})}
</Select>
</div>
}
<Button <Button
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }} style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
onClick={() => { onClick={() => {
@@ -314,6 +359,10 @@ const SchedulesTab = memo((props) => {
start_node: "", start_node: "",
} }
if (selectedEnvironment !== undefined && selectedEnvironment !== "") {
pipelineConfig.environment = selectedEnvironment.Name
}
if (environment !== undefined && environment !== "") { if (environment !== undefined && environment !== "") {
pipelineConfig.environment = environment pipelineConfig.environment = environment
} }
@@ -441,7 +490,46 @@ const SchedulesTab = memo((props) => {
}); });
}; };
const handleGetEnvironments = () => {
fetch(`${globalUrl}/api/v1/environments`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all triggers");
}
return response.json();
})
.then((responseJson) => {
setEnvironments(responseJson || []);
if (responseJson?.length > 0) {
var selectedEnv = ""
for (var i = 0; i < responseJson?.length; i++) {
const env = responseJson[i]
if (env.archived) {
continue
}
selectedEnv = env.Name
if (env?.data_lake?.enabled === true) {
break
}
}
setSelectedEnvironment(selectedEnv.Name)
}
})
.catch((error) => {
// toast(error.toString());
});
}
const handleGetAllTriggers = () => { const handleGetAllTriggers = () => {
fetch(globalUrl + "/api/v1/triggers", { fetch(globalUrl + "/api/v1/triggers", {
@@ -52,7 +52,7 @@ import {
} from '@mui/icons-material'; } from '@mui/icons-material';
import { validateJson } from "../views/Workflows.jsx"; import { validateJson } from "../views/Workflows2.jsx";
import ReactJson from "react-json-view-ssr"; import ReactJson from "react-json-view-ssr";
import PaperComponent from "../components/PaperComponent.jsx"; import PaperComponent from "../components/PaperComponent.jsx";
@@ -969,6 +969,10 @@ const CodeEditor = (props) => {
// Whelp this is inefficient af. Single loop pls // Whelp this is inefficient af. Single loop pls
// When the found array is empty. // When the found array is empty.
if (found !== null && found !== undefined) { if (found !== null && found !== undefined) {
// Resolve the child paths/variables first,
// So that "$a.b.c" is handled before "$a.b" and then "$a"
// Replace "$parent.child" before "$parent" so "$parent.child" doesn't become "parentValue.child" (which can't be matched later).
found.sort((a, b) => b.length - a.length)
//console.log("FOUND: ", found) //console.log("FOUND: ", found)
try { try {
@@ -24,7 +24,7 @@ import {
LinearYAxisTickSeries, LinearYAxisTickSeries,
LinearYAxisTickLabel, LinearYAxisTickLabel,
} from "reaviz"; } from "reaviz";
import theme from "../theme"; import theme from "../theme.jsx";
// KPI configuration constants // KPI configuration constants
const RUN_MINUTES_SAVED_PER_WORKFLOW = 15; // minutes saved per workflow run const RUN_MINUTES_SAVED_PER_WORKFLOW = 15; // minutes saved per workflow run
@@ -48,7 +48,11 @@ const statusOptions = [
// This is used to format the date in the format of YYYY-MM-DD // This is used to format the date in the format of YYYY-MM-DD
function formatDay(dateInput) { function formatDay(dateInput) {
try { try {
return new Date(dateInput).toISOString().slice(0, 10); const dt = new Date(dateInput);
const y = dt.getFullYear();
const m = String(dt.getMonth() + 1).padStart(2, "0");
const d = String(dt.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`; // local date to avoid UTC shift dropping "today"
} catch { } catch {
return String(dateInput); return String(dateInput);
} }
@@ -221,8 +225,8 @@ function buildGroupedSeries(selectedStatus, okArr, failArr) {
return { grouped, scheme }; return { grouped, scheme };
} }
const SuccessFailedRunsWidget = (props) => { const SuccessFailedRunsWidget = (props) => {
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode } = props; const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode, loadingSelectedOrgStats, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit } = props;
const [mode, setMode] = useState("workflows"); // 'workflows' | 'apps' const [mode, setMode] = useState("workflows"); // 'workflows' | 'apps'
const [days, setDays] = useState(30); const [days, setDays] = useState(30);
@@ -236,6 +240,25 @@ const SuccessFailedRunsWidget = (props) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [wfTotals, setWfTotals] = useState({ ok: 0, fail: 0, activeDays: 0 }); const [wfTotals, setWfTotals] = useState({ ok: 0, fail: 0, activeDays: 0 });
const appExecutionLimit = useMemo(() => {
if (mode !== 'apps') return null;
if(orgForLimit?.id !== selectedOrgForStats && selectedOrgForStats !== "ALL") {
return null;
}
try {
let org = orgForLimit
if(selectedOrgForStats === "ALL") {
org = selectedOrganization;
}
if (!org?.sync_features?.app_executions?.limit) return null;
return Number(org.sync_features.app_executions.limit) || null;
} catch {
return null;
}
}, [mode, selectedOrganization, orgForLimit, selectedOrgForStats]);
useEffect(() => { useEffect(() => {
try { try {
if (typeof onTotalsChange !== "function") return; if (typeof onTotalsChange !== "function") return;
@@ -264,7 +287,132 @@ const SuccessFailedRunsWidget = (props) => {
const fetchSeriesForKey = async (key) => { const fetchSeriesForKey = async (key) => {
try { try {
// If specific org is selected and pre-fetched stats are available, use them directly
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
const dailyStats = orgStats.daily_statistics;
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (!day?.date) continue;
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue;
let value = 0;
if (key === 'workflow_executions') {
const finished = Number(day?.workflow_executions_finished || 0);
const failed = Number(day?.workflow_executions_failed || 0);
value = finished + failed;
} else {
value = Number(day?.[key] || 0);
}
processedEntries.push({ date: day.date, value });
}
// Append today's numbers if missing from daily_statistics, using top-level daily_* fields
try {
const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })();
const hasToday = processedEntries.some(e => {
const dt = new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}` === todayStr;
});
if (!hasToday) {
let todayVal = 0;
if (key === 'workflow_executions') {
const finished = Number(orgStats?.daily_workflow_executions_finished ?? orgStats?.daily_workflow_executions ?? 0);
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
// If daily_workflow_executions exists, prefer that; otherwise sum finished + failed
todayVal = orgStats?.daily_workflow_executions !== undefined ? Number(orgStats.daily_workflow_executions || 0) : (finished + failed);
} else if (key === 'workflow_executions_finished') {
todayVal = Number(orgStats?.daily_workflow_executions_finished ?? 0);
} else if (key === 'workflow_executions_failed') {
todayVal = Number(orgStats?.daily_workflow_executions_failed ?? 0);
} else if (key === 'app_executions') {
todayVal = Number(orgStats?.daily_app_executions ?? 0);
} else if (key === 'app_executions_failed') {
todayVal = Number(orgStats?.daily_app_executions_failed ?? 0);
}
// Only add when we have a numeric value (including 0)
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
// If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions
if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) {
// Use the full stats endpoint to get daily_statistics
const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`;
const r = await fetch(url, { method: "GET", credentials: "include" });
if (r.ok) {
const data = await r.json();
const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
// Process each day's data, combining parent + child org runs
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (day?.date) {
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue; // Filter by days
let value = day?.[key] || 0;
// Add child org executions based on the key
if (key === 'app_executions' && day?.child_app_executions !== undefined) {
value += (day.child_app_executions || 0);
} else if (key === 'app_executions_failed' && day?.child_app_executions_failed !== undefined) {
value += (day.child_app_executions_failed || 0);
} else if (key === 'workflow_executions_finished' && day?.child_workflow_executions_finished !== undefined) {
value += (day.child_workflow_executions_finished || 0);
} else if (key === 'workflow_executions_failed' && day?.child_workflow_executions_failed !== undefined) {
value += (day.child_workflow_executions_failed || 0);
} else if (key === 'workflow_executions') {
// workflow_executions is total (finished + failed), so add both child fields
const childFinished = day?.child_workflow_executions_finished || 0;
const childFailed = day?.child_workflow_executions_failed || 0;
value += (childFinished + childFailed);
}
processedEntries.push({ date: day.date, value });
}
}
// Append today's datapoint for ALL by combining parent + child daily_* counters
try {
const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })();
const hasToday = processedEntries.some(e => { const dt=new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`===todayStr; });
if (!hasToday) {
let todayVal = 0;
if (key === 'app_executions') {
todayVal = Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
} else if (key === 'app_executions_failed') {
todayVal = Number(data?.daily_app_executions_failed ?? 0) + Number(data?.daily_child_app_executions_failed ?? 0);
} else if (key === 'workflow_executions_finished') {
todayVal = Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_child_workflow_executions_finished ?? 0);
} else if (key === 'workflow_executions_failed') {
todayVal = Number(data?.daily_workflow_executions_failed ?? 0) + Number(data?.daily_child_workflow_executions_failed ?? 0);
} else if (key === 'workflow_executions') {
const p = Number(data?.daily_workflow_executions ?? 0);
const cf = Number(data?.daily_child_workflow_executions_finished ?? 0);
const cfa = Number(data?.daily_child_workflow_executions_failed ?? 0);
todayVal = p > 0 ? p : (Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_workflow_executions_failed ?? 0));
todayVal += (cf + cfa);
}
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
}
// Fallback to global stats
const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`; const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
const doFetch = async (u) => { const doFetch = async (u) => {
const r = await fetch(u, { method: "GET", credentials: "include" }); const r = await fetch(u, { method: "GET", credentials: "include" });
@@ -272,25 +420,17 @@ const SuccessFailedRunsWidget = (props) => {
const j = await r.json(); const j = await r.json();
return Array.isArray(j?.entries) ? j.entries : []; return Array.isArray(j?.entries) ? j.entries : [];
}; };
const a = await doFetch(urlA); const a = await doFetch(urlA);
if (a.length > 0) return a; return a;
} catch (e) {
// Optional orgId fallback if exposed globally in app return [];
// const orgId = (window && window.selectedOrganization && window.selectedOrganization.id) || null; }
// if (orgId) { };
// const urlB = `${globalUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
// const b = await doFetch(urlB);
// if (b.length > 0) return b;
// }
// return [];
} catch (e) {
return [];
}
};
const fetchSeries = async () => { const fetchSeries = async () => {
setLoading(true); setLoading(true);
try { try {
// This will only run if dummyMode is true (Onboarding preview)
if (dummyMode) { if (dummyMode) {
// 10-day wave with a couple of bumps for a more dynamic preview // 10-day wave with a couple of bumps for a more dynamic preview
const today = new Date(); const today = new Date();
@@ -324,47 +464,6 @@ const SuccessFailedRunsWidget = (props) => {
let okSeries = normalizeEntries(succ); let okSeries = normalizeEntries(succ);
let failSeries = normalizeEntries(fail); let failSeries = normalizeEntries(fail);
// Fallback: if empty, derive from /api/v1/stats daily_statistics
if (okSeries.length === 0 && failSeries.length === 0) {
const resp = await fetch(`${globalUrl}/api/v1/stats`, {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
});
if (resp.ok) {
const data = await resp.json();
const fieldOk =
mode === "workflows"
? "workflow_executions_finished"
: "app_executions";
const fieldFail =
mode === "workflows"
? "workflow_executions_failed"
: "app_executions_failed";
const list = Array.isArray(data?.daily_statistics)
? data.daily_statistics
: [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
okSeries = list
.filter(Boolean)
.map((d) => ({
key: new Date(d?.date || Date.now()),
id: d?.date || Math.random().toString(36).slice(2),
data: Number(d?.[fieldOk] || 0),
}))
.filter((p) => p.key >= cutoff);
failSeries = list
.filter(Boolean)
.map((d) => ({
key: new Date(d?.date || Date.now()),
id: `f-${d?.date || Math.random().toString(36).slice(2)}`,
data: Number(d?.[fieldFail] || 0),
}))
.filter((p) => p.key >= cutoff);
}
}
setSeriesOk(okSeries); setSeriesOk(okSeries);
setSeriesFail(failSeries); setSeriesFail(failSeries);
} catch (e) { } catch (e) {
@@ -375,16 +474,15 @@ const SuccessFailedRunsWidget = (props) => {
} }
}; };
// Only fetch on first render and when days window or mode changes
const firstLoadRef = React.useRef(true);
useEffect(() => { useEffect(() => {
if (firstLoadRef.current) { if (loadingSelectedOrgStats === true) {
firstLoadRef.current = false; setSeriesOk([]);
setSeriesFail([]);
return;
}
fetchSeries(); fetchSeries();
return; }, [dummyMode, days, globalUrl, mode, selectedOrgForStats, selectedOrganization, loadingSelectedOrgStats]);
}
fetchSeries();
}, [dummyMode, days, globalUrl, mode]);
// Apply external days override (e.g. after onboarding completes) // Apply external days override (e.g. after onboarding completes)
useEffect(() => { useEffect(() => {
@@ -406,7 +504,13 @@ const SuccessFailedRunsWidget = (props) => {
const totalEntries = await fetchSeriesForKey("workflow_executions"); const totalEntries = await fetchSeriesForKey("workflow_executions");
const series = normalizeEntries(totalEntries); const series = normalizeEntries(totalEntries);
const dayKey = (d) => { const dayKey = (d) => {
try { return new Date(d?.date || d?.key).toISOString().slice(0,10); } catch { return null; } try {
const dt = new Date(d?.date || d?.key);
const y = dt.getFullYear();
const m = String(dt.getMonth() + 1).padStart(2, "0");
const day = String(dt.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`; // local date string
} catch { return null; }
}; };
const dayTotals = new Map(); const dayTotals = new Map();
for (const it of series) { for (const it of series) {
@@ -421,14 +525,14 @@ const SuccessFailedRunsWidget = (props) => {
}; };
run(); run();
return () => { aborted = true; }; return () => { aborted = true; };
}, [globalUrl, days, dummyMode, overrideDays]); }, [globalUrl, days, dummyMode, overrideDays, selectedOrgForStats]);
// Notify parent about loading state changes // Notify parent about loading state changes
useEffect(() => { useEffect(() => {
if (typeof onLoadingChange === "function") { if (typeof onLoadingChange === "function") {
onLoadingChange(loading); onLoadingChange(loadingSelectedOrgStats || loading);
} }
}, [loading, onLoadingChange]); }, [loading, loadingSelectedOrgStats, onLoadingChange]);
// Build filters UI once here; optionally render externally via onControlsChange // Build filters UI once here; optionally render externally via onControlsChange
const controlsNode = React.useMemo(() => ( const controlsNode = React.useMemo(() => (
@@ -600,6 +704,9 @@ const SuccessFailedRunsWidget = (props) => {
} }
}, [onControlsChange, controlsNode]); }, [onControlsChange, controlsNode]);
var showParentLimit = false;
var showSuborgLimit = false;
return ( return (
<div style={{ width: "100%", display: "flex", flexDirection: "column" }}> <div style={{ width: "100%", display: "flex", flexDirection: "column" }}>
{/* Top controls row: title left, all filters on the right */} {/* Top controls row: title left, all filters on the right */}
@@ -637,7 +744,11 @@ const SuccessFailedRunsWidget = (props) => {
}} }}
> >
<Typography style={{ fontWeight: 500, marginBottom: 22, fontSize: 18 }}>Successful vs Failed Runs ({mode === "workflows" ? "Workflows" : "Apps"})</Typography> <Typography style={{ fontWeight: 500, marginBottom: 22, fontSize: 18 }}>Successful vs Failed Runs ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
{(() => { {loadingSelectedOrgStats === true ? (
<div style={{ height: 300, display: "flex", alignItems: "center", justifyContent: "center" }}>
<Typography variant="body2" color="textSecondary">Loading</Typography>
</div>
) : (() => {
// Build unified timeline by day or by month // Build unified timeline by day or by month
const useOk = Array.isArray(seriesOk) ? seriesOk : []; const useOk = Array.isArray(seriesOk) ? seriesOk : [];
const useFail = Array.isArray(seriesFail) ? seriesFail : []; const useFail = Array.isArray(seriesFail) ? seriesFail : [];
@@ -690,39 +801,60 @@ const SuccessFailedRunsWidget = (props) => {
); );
const paddedMax = computePaddedMax(okArr, failArr); const paddedMax = computePaddedMax(okArr, failArr);
// Extract parent and suborg app execution limits
let parentAppExecutionLimit = null;
let suborgAppExecutionLimit = null;
if (mode === 'apps') {
parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null;
suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null;
console.log("Mode appExecutionLimit found in sub orgs parentAppExecutionLimit", parentAppExecutionLimit);
console.log("Mode appExecutionLimit found in sub orgs suborgAppExecutionLimit", suborgAppExecutionLimit);
}
if (
typeof appExecutionLimit === 'number' &&
appExecutionLimit > 0 &&
appExecutionLimit <= paddedMax &&
selectedOrgForStats !== "ALL"
) {
showSuborgLimit = true;
}
return ( return (
<AreaChart <div style={{ position: 'relative', width: '100%', height: 300 }}>
height={300} <AreaChart
width={"100%"} height={300}
data={grouped} width={"100%"}
yAxis={ data={grouped}
<LinearYAxis yAxis={
domain={[0, paddedMax]} <LinearYAxis
tickSeries={ domain={[0, paddedMax]}
<LinearYAxisTickSeries tickSeries={
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />} <LinearYAxisTickSeries
/> label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
} />
/> }
} />
xAxis={ }
<LinearXAxis xAxis={
tickSeries={ <LinearXAxis
<LinearXAxisTickSeries tickSeries={
tickSize={0} <LinearXAxisTickSeries
label={ tickSize={0}
<LinearXAxisTickLabel label={
padding={8} <LinearXAxisTickLabel
format={(d) => formatLabel(Number(d))} padding={8}
/> format={(d) => formatLabel(Number(d))}
} />
tickValues={tickValues} }
/> tickValues={tickValues}
} />
/> }
} />
gridlines={<GridlineSeries line={<Gridline direction="y" />} />} }
series={ gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
series={
<AreaSeries <AreaSeries
type="grouped" type="grouped"
interpolation={"smooth"} interpolation={"smooth"}
@@ -822,6 +954,119 @@ const SuccessFailedRunsWidget = (props) => {
/> />
} }
/> />
{/* Custom reference line overlay - only show if limit is within visible range */}
{appExecutionLimit && mode === 'apps' && (() => {
const maxValue = paddedMax || 1;
const limitPercentage = appExecutionLimit / maxValue;
const chartTopPadding = 1;
const chartBottomPadding = 8;
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
const topPercentage = chartTopPadding + ((1 - limitPercentage) * usableHeight);
let parentLimitPos = null;
if (
typeof parentAppExecutionLimit === 'number' &&
parentAppExecutionLimit > 0 &&
parentAppExecutionLimit !== appExecutionLimit &&
parentAppExecutionLimit <= paddedMax
) {
showParentLimit = true;
const parentLimitPerc = parentAppExecutionLimit / paddedMax;
parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight);
}
if (selectedOrgForStats === "ALL") {
showParentLimit = true;
const parentLimitPerc = parentAppExecutionLimit / paddedMax;
parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight);
}
return (
<>
{/* Suborg or Current Limit Reference Line & Label (orange now) */}
{
selectedOrgForStats !== "ALL" && showSuborgLimit && (
<>
<div
style={{
position: 'absolute',
top: `${topPercentage}%`,
left: '3.9%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
pointerEvents: 'none',
zIndex: 10,
}}
/>
<div
style={{
position: 'absolute',
top: `${topPercentage}%`,
right: '-8%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 10,
backgroundColor: 'rgba(26, 26, 26, 0.95)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #ff8544',
}}
>
<Typography
style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}
>
{formatCompactNumber(appExecutionLimit)} limit
</Typography>
</div>
</>
)
}
{/* Parent Org Limit Reference Line & Label (red now) */}
{showParentLimit && (
<>
<div
style={{
position: 'absolute',
top: `${parentLimitPos}%`,
left: '3.9%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
pointerEvents: 'none',
zIndex: 9,
opacity: 0.8,
}}
/>
<div
style={{
position: 'absolute',
top: `${parentLimitPos}%`,
right: '-8%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 9,
backgroundColor: 'rgba(26, 26, 26, 0.93)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #FD4C62',
display: 'flex', alignItems: 'center', gap: 6
}}
>
<Typography
style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}
>
{formatCompactNumber(parentAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
</>
);
})()}
</div>
); );
})()} })()}
<div <div
@@ -844,8 +1089,19 @@ const SuccessFailedRunsWidget = (props) => {
<LegendDot color="#22c55e" /> Successful Runs <LegendDot color="#22c55e" /> Successful Runs
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}> <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<LegendDot color="#ef4444" /> Failed Runs <LegendDot color="#FD4C62" /> Failed Runs
</div> </div>
{/* Chart limit legends */}
{showParentLimit && (
<div style={{ display: "flex", alignItems: "center", gap: 4, marginLeft: 18 }}>
<LegendLineDash color="#FD4C62" height="10" /> <span>Parent Org Limit</span>
</div>
)}
{showSuborgLimit && (
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<LegendLineDash color="#ff8544" height="10" /> <span>Current Org Limit</span>
</div>
)}
</div> </div>
<div <div
style={{ style={{
@@ -999,3 +1255,18 @@ function LegendDot({ color }) {
/> />
); );
} }
function LegendLineDash({ color, height = 10 }) {
// Render a small thinned dashed SVG line
return (
<svg width="24" height={height} style={{ margin: '0 2px' }}>
<line
x1="2" x2="32" y1={height / 2} y2={height / 2}
stroke={color}
strokeWidth="2"
strokeDasharray="8, 5"
strokeLinecap="round"
/>
</svg>
);
}
+1 -1
View File
@@ -20,7 +20,7 @@ import {
Zoom, Zoom,
Chip, Chip,
} from '@mui/material'; } from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback"; import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import WorkflowPaper from "../components/WorkflowPaper.jsx" import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx" import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
@@ -24,7 +24,7 @@ import {
} from "../views/AngularWorkflow.jsx" } from "../views/AngularWorkflow.jsx"
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx" import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
import theme from "../theme.jsx"; import theme from "../theme.jsx";
const itemHeight = 24 const itemHeight = 24
+62 -31
View File
@@ -552,6 +552,37 @@ export default function defaultCytoscapeStyle(theme) {
"font-size": "0px", "font-size": "0px",
}, },
}, },
{
selector: `edge[?correlation]`,
css: {
"curve-style": "straight",
"width": 2,
"line-color": "#ccc",
"target-arrow-color": "#ccc",
"source-arrow-shape": "none",
"target-arrow-shape": "none",
},
},
{
selector: `node[type="CORRELATION"]`,
css: {
label: function (element) {
return element.data("label")
},
shape: "round",
color: "data(color)",
width: "data(width)",
height: "data(height)",
padding: "0px",
margin: "0px",
"background-color": "data(iconBackground)",
"background-fill": "data(fillstyle)",
"background-gradient-direction": "to-right",
"background-gradient-stop-colors": "data(fillGradient)",
"border-width": "0px",
"font-size": "0px",
},
},
{ {
selector: `node[?source_workflow]`, selector: `node[?source_workflow]`,
css: { css: {
@@ -578,41 +609,41 @@ export default function defaultCytoscapeStyle(theme) {
selector: `node[name="switch"]`, selector: `node[name="switch"]`,
css: { css: {
label: function(element) { label: function(element) {
// Load from the actual element // Load from the actual element
var nodeheight = 400 var nodeheight = 400
var conditions = [{ var conditions = [{
"name": "Condition 1", "name": "Condition 1",
"check": "X equals Y", "check": "X equals Y",
}, },
{ {
"name": "Condition 2", "name": "Condition 2",
"check": "X2 equals Y2", "check": "X2 equals Y2",
}, },
{ {
"name": "Condition 3", "name": "Condition 3",
"check": "X3 equals Y3", "check": "X3 equals Y3",
}] }]
conditions.push({ conditions.push({
"name": "Else", "name": "Else",
"check": "If all else fails", "check": "If all else fails",
}) })
const newlines = nodeheight / conditions.length const newlines = nodeheight / conditions.length
console.log("Newlines: ", newlines) console.log("Newlines: ", newlines)
const label = conditions.map((condition) => { const label = conditions.map((condition) => {
return `${condition.name}\n\n\n` return `${condition.name}\n\n\n`
}).join("\n") }).join("\n")
return label return label
}, },
color: theme.palette.text.primary || "white", color: theme.palette.text.primary || "white",
"border-color": "#f85a3e", "border-color": "#f85a3e",
"background-color": "#1f1f1f", "background-color": "#1f1f1f",
"font-size": "19px", "font-size": "19px",
"text-margin-x": "-110px", "text-margin-x": "-110px",
"text-wrap": "wrap", "text-wrap": "wrap",
shape: "roundrectangle", shape: "roundrectangle",
width: "100", width: "100",
height: "300", height: "300",
+11 -1
View File
@@ -1765,7 +1765,17 @@ If you're interested, please let me know a time that works for you, or set up a
setTimeout(() => { setTimeout(() => {
window.location.reload() window.location.reload()
}, 2000); }, 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 { } else {
if (responseJson.reason !== undefined && responseJson.reason !== null) { if (responseJson.reason !== undefined && responseJson.reason !== null) {
if (!responseJson.reason.includes("already")) { if (!responseJson.reason.includes("already")) {
+39 -15
View File
@@ -2,19 +2,23 @@ import React, { useContext, useEffect, useState } from 'react';
import AdminNavBar from '../components/AdminNavBar.jsx'; import AdminNavBar from '../components/AdminNavBar.jsx';
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
import { useNavigate, Link } from "react-router-dom";
const Admin2 = (props) => { const Admin2 = (props) => {
// Destructure props if needed // 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 [selectedTab, setSelectedTab] = useState('editdetails');
const [selectedStatus, setSelectedStatus] = React.useState([]); const [selectedStatus, setSelectedStatus] = React.useState([]);
const [selectedOrganization, setSelectedOrganization] = useState({}); const [selectedOrganization, setSelectedOrganization] = useState({});
const [organizationFeatures, setOrganizationFeatures] = useState({}); const [organizationFeatures, setOrganizationFeatures] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true); const [orgRequest, setOrgRequest] = React.useState(true);
const [isOrgLoaded, setIsOrgLoaded] = React.useState(false); 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"; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
let navigate = useNavigate();
if (document !== undefined) { if (document !== undefined) {
if (selectedOrganization?.name !== undefined) { if (selectedOrganization?.name !== undefined) {
document.title = brandName?.length > 0 ? selectedOrganization?.name + ` - Admin - ${brandName}` : selectedOrganization?.name + ` - Admin - Shuffle`; document.title = brandName?.length > 0 ? selectedOrganization?.name + ` - Admin - ${brandName}` : selectedOrganization?.name + ` - Admin - Shuffle`;
@@ -24,6 +28,9 @@ const Admin2 = (props) => {
} }
const handleGetOrg = (orgId) => { const handleGetOrg = (orgId) => {
if (orgId === undefined || orgId === null || orgId.length !== 36) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET", method: "GET",
@@ -40,13 +47,13 @@ const Admin2 = (props) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
toast( toast.warn("Failed getting your org. If this persists, please contact support. Redirecting to workflows...")
"Failed getting your org. If this persists, please contact support. Redirecting to workflows...",
);
setTimeout(() => { setTimeout(() => {
window.location.href = "/workflows"; window.location.href = "/workflows";
}, 3000); }, 3000);
} else { } else {
setUpdateOrg(false);
if ( if (
responseJson.sync_features === undefined || responseJson.sync_features === undefined ||
responseJson.sync_features === null 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(() => { useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
@@ -216,16 +230,26 @@ const Admin2 = (props) => {
setTimeout(() => { setTimeout(() => {
window.location.reload() window.location.reload()
}, 3000); }, 3000);
toast("Successfully changed active organization - refreshing!");
} else { toast.success("Successfully changed active organization - refreshing!");
if (responseJson.reason !== undefined && responseJson.reason !== null) { if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
if (!responseJson.reason.includes("already")) { navigate(`/admin?org_id=${responseJson.org_id}`)
toast("Failed changing org: " + responseJson.reason); } else {
} if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
} else { navigate(`/admin`)
toast("Failed changing org") } 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) => { .catch((error) => {
console.log("error changing: ", error); console.log("error changing: ", error);
+345 -67
View File
@@ -6,7 +6,11 @@ import { getTheme } from "../theme.jsx";
import { toast } from "react-toastify" import { toast } from "react-toastify"
import ReactJson from "react-json-view-ssr"; import ReactJson from "react-json-view-ssr";
import { v4 as uuidv4} from "uuid"; 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 { import {
Box, Box,
@@ -18,6 +22,8 @@ import {
Tooltip, Tooltip,
IconButton, IconButton,
TextField, TextField,
Popover,
Divider,
} from '@mui/material' } from '@mui/material'
import { import {
@@ -32,10 +38,13 @@ import {
Close as CloseIcon, Close as CloseIcon,
OpenInNew as OpenInNewIcon, OpenInNew as OpenInNewIcon,
Refresh as RefreshIcon, Refresh as RefreshIcon,
Add as AddIcon,
Warning as WarningIcon,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
green, green,
yellow,
red, red,
} from '../views/AngularWorkflow.jsx' } from '../views/AngularWorkflow.jsx'
@@ -55,11 +64,35 @@ const AgentUI = (props) => {
const [actionInput, setActionInput] = useState("") const [actionInput, setActionInput] = useState("")
const [questionAnswers, setQuestionAnswers] = 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 {themeMode} = useContext(Context)
const theme = getTheme(themeMode) const theme = getTheme(themeMode)
const navigate = useNavigate(); const navigate = useNavigate();
document.title = "Shuffle AI Agents" if (document !== undefined && document !== null && !document?.title?.includes("Agent")) {
document.title = "Shuffle AI Agents"
}
const agentWrapperStyle = { const agentWrapperStyle = {
width: 1000, 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) => { const findNodeData = (execution_data, node_id) => {
if (execution_data === undefined || execution_data === null) { if (execution_data === undefined || execution_data === null) {
return return
@@ -311,7 +382,73 @@ const AgentUI = (props) => {
getAppAuth() 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&note=${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 latestEndTime = 0
var originalStartTime = 0 var originalStartTime = 0
@@ -332,6 +469,11 @@ const AgentUI = (props) => {
<ErrorIcon style={{color: red, marginRight: 10, }} /> <ErrorIcon style={{color: red, marginRight: 10, }} />
</Tooltip> </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"> <Tooltip title={`Not started yet: ${item.status}`} placement="top">
<HourglassDisabledIcon style={{marginRight: 10, }} /> <HourglassDisabledIcon style={{marginRight: 10, }} />
</Tooltip> </Tooltip>
@@ -399,6 +541,10 @@ const AgentUI = (props) => {
const defaultTopPadding = 10 const defaultTopPadding = 10
const open = openIndexes.includes(index) 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 = [] var questions = []
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") { if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
item.type = "finalise" item.type = "finalise"
@@ -434,6 +580,10 @@ const AgentUI = (props) => {
<Tooltip title="Ask" placement="top"> <Tooltip title="Ask" placement="top">
<img src="/images/workflows/UserInput2.svg" style={categoryStyle} /> <img src="/images/workflows/UserInput2.svg" style={categoryStyle} />
</Tooltip> </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} /> <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 === "FAILURE" || item.status == "ABORTED" ? red :
item.status === "RUNNING" || item.status === "" ? theme.palette.main : item.status === "RUNNING" || item.status === "" ? theme.palette.main :
theme.palette.surfaceColor red
const rerunAgentButton = const rerunAgentButton =
<Tooltip title="Rerun from the start with the same input" placement="right"> <Tooltip title="Rerun from the start with the same input" placement="right">
@@ -501,7 +651,7 @@ const AgentUI = (props) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
toast.info("Attempting to rerun everything.") toast.info("Rerunning agent with the same input.")
setDisableButtons(true) setDisableButtons(true)
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) { if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
@@ -529,7 +679,7 @@ const AgentUI = (props) => {
const rerunButton = 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> <span>
<IconButton <IconButton
disabled={item.type !== "decision" || disableButtons} disabled={item.type !== "decision" || disableButtons}
@@ -546,56 +696,7 @@ const AgentUI = (props) => {
<RestartAltIcon /> <RestartAltIcon />
</IconButton> </IconButton>
</span> </span>
</Tooltip> </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&note=${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)
})
}
return ( return (
<div <div
@@ -675,6 +776,7 @@ const AgentUI = (props) => {
minWidth: 300, minWidth: 300,
maxWidth: 300, maxWidth: 300,
paddingTop: defaultTopPadding, paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}> }}>
{item.label} {item.label}
</div> </div>
@@ -746,10 +848,11 @@ const AgentUI = (props) => {
</Tooltip> </Tooltip>
*/} */}
<Tooltip title="See in another window" placement="left"> <Tooltip title="Answer in the Form UI" placement="left">
<span> <span>
<IconButton <IconButton
style={{marginLeft: 0, }} style={{marginLeft: 0, }}
disabled={item?.details?.run_details?.status === "FINISHED"}
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
@@ -767,6 +870,28 @@ const AgentUI = (props) => {
: :
item.category === "agent" ? item.category === "agent" ?
rerunAgentButton 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 rerunButton
} }
@@ -805,13 +930,25 @@ const AgentUI = (props) => {
{questions.map((q, questionIndex) => { {questions.map((q, questionIndex) => {
return ( return (
<div style={{marginTop: 25, }}> <div style={{marginTop: 25, }}>
<Typography variant="body2"> <div id="markdown_wrapper_outer" style={{cursor: "default", }}>
{`${q.question}`} <Markdown
</Typography> components={markdownComponents}
id="markdown_wrapper"
className={"style.reactMarkdown"}
escapeHtml={false}
skipHtml={false}
remarkPlugins={[remarkGfm]}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{q.question}
</Markdown>
</div>
<TextField <TextField
label={`Question ${q.index}`} label={`Question ${q.index}`}
placeholder="No question found" placeholder="Your answer here"
variant="outlined" variant="outlined"
style={{width: 800, marginTop: 20, }} style={{width: 800, marginTop: 20, }}
multiline multiline
@@ -838,7 +975,7 @@ const AgentUI = (props) => {
<Button <Button
variant="contained" variant="contained"
style={{marginTop: 10, }} style={{marginTop: 16, }}
disabled={questionSubmitDisabled} disabled={questionSubmitDisabled}
onClick={() => { onClick={() => {
submitQuestions(item?.details?.run_details?.id, questionAnswers) submitQuestions(item?.details?.run_details?.id, questionAnswers)
@@ -887,6 +1024,8 @@ const AgentUI = (props) => {
const TimelineRender = (props) => { const TimelineRender = (props) => {
const { agent_data } = props; const { agent_data } = props;
const [continuationText, setContinuationText] = useState("")
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
const validate = validateJson(actionResult?.result) const validate = validateJson(actionResult?.result)
if (validate.valid === true) { if (validate.valid === true) {
@@ -938,6 +1077,7 @@ const AgentUI = (props) => {
} }
} }
var finishDecisionId = ""
var sortedTimelineItems = [] var sortedTimelineItems = []
for (var key in agent_data?.decisions) { for (var key in agent_data?.decisions) {
const item = agent_data.decisions[key] const item = agent_data.decisions[key]
@@ -962,6 +1102,11 @@ const AgentUI = (props) => {
newTimelineItem.details = item newTimelineItem.details = item
timelineItems.push(newTimelineItem) 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) => { 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> </div>
) )
} }
@@ -1021,14 +1232,28 @@ const AgentUI = (props) => {
setAgentActionResult(null) setAgentActionResult(null)
document.title = `Agent: ${inputText.substring(0, 30)}...`
if (inputText === undefined || inputText === null || inputText === "") { if (inputText === undefined || inputText === null || inputText === "") {
toast.error("Please provide a valid input for the AI Agent.") toast.error("Please provide a valid input for the AI Agent.")
setAgentRequestLoading(false)
return return
} }
// 1. Run the execution. Can this be a single-action run? // 1. Run the execution. Can this be a single-action run?
// 2. Get the execution ID and node ID from the response. // 2. Get the execution ID and node ID from the response.
const uuid = uuidv4() 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 = { const data = {
"id": uuid, "id": uuid,
"name":"agent", "name":"agent",
@@ -1049,7 +1274,7 @@ const AgentUI = (props) => {
}, },
{ {
"name":"action", "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 isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
if (isCmdEnter || isCtrlEnter) { if (isCmdEnter || isCtrlEnter) {
@@ -1089,6 +1314,11 @@ const AgentUI = (props) => {
} }
} }
const chipStyle = {
margin: 4,
cursor: "pointer",
}
return ( return (
<div style={agentWrapperStyle}> <div style={agentWrapperStyle}>
<TextField <TextField
@@ -1100,7 +1330,7 @@ const AgentUI = (props) => {
<Box <Box
component="form" component="form"
style={{textAlign: "center", }} style={{textAlign: "center", }}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDownRoot}
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
submitInput(actionInput); submitInput(actionInput);
@@ -1123,7 +1353,7 @@ const AgentUI = (props) => {
disabled={agentRequestLoading} disabled={agentRequestLoading}
style={{width: 450, marginRight: 20, marginTop: 30, }} style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline multiline
minRows={2} minRows={1}
defaultValue={actionInput || ""} defaultValue={actionInput || ""}
onChange={(e) => { onChange={(e) => {
setActionInput(e.target.value) 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> </Box>
: :
<div> <div>
+196 -175
View File
@@ -135,12 +135,10 @@ import {
OpenInFull as OpenInFullIcon, OpenInFull as OpenInFullIcon,
Difference as DifferenceIcon, Difference as DifferenceIcon,
DataObject as DataObjectIcon, DataObject as DataObjectIcon,
SwapHoriz as SwapHorizIcon
} from "@mui/icons-material"; } from "@mui/icons-material";
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
//import * as cytoscape from "cytoscape";
import cytoscape from "cytoscape"; import cytoscape from "cytoscape";
import edgehandles from "cytoscape-edgehandles"; import edgehandles from "cytoscape-edgehandles";
import CytoscapeComponent from "react-cytoscapejs"; import CytoscapeComponent from "react-cytoscapejs";
@@ -152,7 +150,7 @@ import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
import LineChartWrapper from "../components/LineChartWrapper.jsx"; import LineChartWrapper from "../components/LineChartWrapper.jsx";
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.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 { GetParsedPaths, internalIds, } from "../views/Apps.jsx";
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
@@ -1065,7 +1063,29 @@ const AngularWorkflow = (defaultprops) => {
"type": "", "type": "",
}, },
"description": "Build & integrate tools easily with standard input and standard output. Built by Shuffle. https://singul.io", "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", "name": "Cases",
"description": "Available actions for case management", "description": "Available actions for case management",
"label": "Cases", "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) => { 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 url = `${globalUrl}/api/v2/workflows/${id}/executions`
var method = "GET" var method = "GET"
if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") { 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 ShowCopyingTooltip = () => {
const [showCopying, setShowCopying] = React.useState(true) const [showCopying, setShowCopying] = React.useState(true)
@@ -22161,8 +22311,8 @@ const AngularWorkflow = (defaultprops) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
: null} : 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={{ 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", }}> <div style={{ display: "flex", }}>
<Breadcrumbs <Breadcrumbs
@@ -23073,7 +23223,14 @@ const AngularWorkflow = (defaultprops) => {
height: imgsize, height: imgsize,
border: `2px solid ${statusColor}`, border: `2px solid ${statusColor}`,
borderRadius: executionData.start === data.action.id ? 25 : 5, 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 = ( actionimg = (
<img <img
alt={"User Input Trigger"} 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 ( return (
<div <div
key={index} key={index}
@@ -23669,150 +23833,7 @@ const AngularWorkflow = (defaultprops) => {
) )
} }
var draggingDisabled = false; 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 ""
}
const currentSuggestion = getErrorSuggestion(validate.result) const currentSuggestion = getErrorSuggestion(validate.result)
const codePopoutModal = !codeModalOpen ? null : ( const codePopoutModal = !codeModalOpen ? null : (
+3 -3
View File
@@ -25,7 +25,7 @@ import {
Divider, Divider,
} from "@mui/material"; } from "@mui/material";
import { validateJson, } from "../views/Workflows.jsx"; import { validateJson, } from "../views/Workflows2.jsx";
import { isMobile } from "react-device-detect" import { isMobile } from "react-device-detect"
import theme from "../theme.jsx"; import theme from "../theme.jsx";
import PaperComponent from "../components/PaperComponent.jsx"; import PaperComponent from "../components/PaperComponent.jsx";
@@ -277,7 +277,8 @@ const ApiExplorerWrapper = (props) => {
parsedapp.name !== undefined && parsedapp.name !== undefined &&
parsedapp.name !== null && parsedapp.name !== null &&
parsedapp.name.length !== 0; parsedapp.name.length !== 0;
if(parsedapp?.id.length > 0){
if (parsedapp?.id.length > 0) {
setSelectedAppData(parsedapp) setSelectedAppData(parsedapp)
handleAppAuthenticationType(parsedapp) handleAppAuthenticationType(parsedapp)
const apptype = selectedAppData?.generated === false ? "python" : "openapi" const apptype = selectedAppData?.generated === false ? "python" : "openapi"
@@ -441,7 +442,6 @@ const ApiExplorerWrapper = (props) => {
selectedAppData.name = appname selectedAppData.name = appname
} }
console.log("APPNAME: ", appname, openapi.id)
if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") { if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") {
setAppAuthentication(data) setAppAuthentication(data)
setSelectedAuthentication({}) setSelectedAuthentication({})
+3 -3
View File
@@ -3455,9 +3455,9 @@ const AppCreator = (defaultprops) => {
required_bodyfields: [], required_bodyfields: [],
}); });
useEffect(() => { //useEffect(() => {
console.log("Queries: ", urlPathQueries) // console.log("Queries: ", urlPathQueries)
}, [urlPathQueries]) //}, [urlPathQueries])
const findBodyParams = (body) => { const findBodyParams = (body) => {
const regex = /\${(\w+)}/g; const regex = /\${(\w+)}/g;
+2 -2
View File
@@ -74,7 +74,7 @@ import {
} from "react-instantsearch-dom"; } from "react-instantsearch-dom";
import AppStats from "../components/AppStats.jsx"; import AppStats from "../components/AppStats.jsx";
import ParsedAction from "../components/ParsedAction.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 { base64_decode, appCategories } from "../views/AppCreator.jsx";
import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx"; import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
@@ -4454,7 +4454,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
): null} ): null}
{userdata && (userdata?.active_org?.creator_org?.length > 0 || userdata?.active_org?.child_orgs?.length === 0) ? 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> </div>
) : ( ) : (
+137 -35
View File
@@ -6,7 +6,7 @@ import ReactJson from "react-json-view-ssr";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { BrowserView, MobileView } from "react-device-detect"; import { BrowserView, MobileView } from "react-device-detect";
import { useParams, useNavigate, Link, useLocation } from "react-router-dom"; 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 remarkGfm from 'remark-gfm'
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
import { import {
@@ -87,6 +87,34 @@ const innerHrefStyle = {
textDecoration: "none", 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) => { export const CopyToClipboard = (props) => {
const { text, style, onCopy } = 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) => { export const Img = (props) => {
@@ -250,9 +358,23 @@ export const CodeHandler = (props) => {
// Need to check if it's singletick or multi // Need to check if it's singletick or multi
if (props.inline === true) { if (props.inline === true) {
// Show it inline // Enhanced inline code styling for readability and long strings
return ( 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} {newprop}
</span> </span>
) )
@@ -318,6 +440,8 @@ const Docs = (defaultprops) => {
props.match = {} props.match = {}
props.match.params = params props.match.params = params
window.title = "Shuffle - Documentation"
//console.log("PARAMS: ", params) //console.log("PARAMS: ", params)
const { themeMode } = useContext(Context) const { themeMode } = useContext(Context)
const theme = getTheme(themeMode) const theme = getTheme(themeMode)
@@ -668,36 +792,13 @@ const Docs = (defaultprops) => {
minHeight: "80vh", 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 Heading = (props) => {
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase(); var id = (props?.children?.[0] ?? props.children ?? '').toString().toLowerCase();
if (props.level <= 3) { if (props?.level <= 3) {
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-"); id = props?.children?.[0]?.toLowerCase().toString().replaceAll(" ", "-") ?? '';
} }
const element = React.createElement( const element = React.createElement(
@@ -1098,11 +1199,6 @@ const Docs = (defaultprops) => {
fontSize: isMobile ? "1.3rem" : "1.1rem", fontSize: isMobile ? "1.3rem" : "1.1rem",
}; };
const alertNote = {
padding: "10px",
borderLeft: "5px solid #f86a3e",
backgroundColor: "rgb(26,26,26)",
};
const CustomButton = (props) => { const CustomButton = (props) => {
const { title, icon, link } = props const { title, icon, link } = props
@@ -1192,7 +1288,7 @@ const Docs = (defaultprops) => {
</Typography> </Typography>
{showPartnerLogo === true ? null : {showPartnerLogo === true ? null :
<div style={{ display: "flex", marginTop: 25, }}> <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" /> <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> </div>
} }
@@ -1230,6 +1326,8 @@ const Docs = (defaultprops) => {
</div> </div>
</div> </div>
const markdownComponents = { const markdownComponents = {
img: Img, img: Img,
code: CodeHandler, code: CodeHandler,
@@ -1242,6 +1340,10 @@ const Docs = (defaultprops) => {
a: OuterLink, a: OuterLink,
p: Paragraph, p: Paragraph,
blockquote: Blockquote, blockquote: Blockquote,
table: TableRenderer,
tr: TableRowRenderer,
th: TableHeaderCellRenderer,
td: TableCellRenderer,
} }
+1 -1
View File
@@ -329,7 +329,7 @@ const LoginPage = props => {
const tmpMessage = new URLSearchParams(window.location.search).get("message") const tmpMessage = new URLSearchParams(window.location.search).get("message")
if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) { if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) {
setMessage(tmpMessage) setMessage(tmpMessage)
toast(tmpMessage) toast.warn(tmpMessage)
} }
} }
}, []) }, [])
+200 -13
View File
@@ -11,7 +11,9 @@ import {
Select, Select,
MenuItem, MenuItem,
Tooltip, Tooltip,
CircularProgress, CircularProgress,
FormControl,
InputLabel,
} from '@mui/material'; } from '@mui/material';
import { toast } from "react-toastify"; import { toast } from "react-toastify";
@@ -54,6 +56,15 @@ const NewDashboard = (props) => {
const [overrideDays, setOverrideDays] = useState(undefined); const [overrideDays, setOverrideDays] = useState(undefined);
const [rotMonthOverride, setRotMonthOverride] = useState(undefined); const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
const [isProdStatusOn, setIsProdStatusOn] = useState(false) 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 = const isCloud =
serverside === true || typeof window === "undefined" 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'; const displayName = userdata !== undefined && userdata?.username !== undefined ? userdata?.username?.split('@')[0]?.charAt(0)?.toUpperCase() + userdata?.username?.split('@')[0]?.slice(1) : 'User';
useEffect(() => { useEffect(() => {
let t; const anyLoading =
const anyLoading = loadingSfw || loadingRot || loadingNoti; loadingSfw ||
if (anyLoading) { loadingRot ||
t = setShowOverlay(true); loadingNoti ||
} else { loadingSelectedOrgStats ||
setShowOverlay(false); !selectedOrganization ||
} !selectedOrgForStats;
return () => { if (t) clearTimeout(t); }; setShowOverlay(anyLoading);
}, [loadingSfw, loadingRot, loadingNoti]); }, [
loadingSfw,
loadingRot,
loadingNoti,
loadingSelectedOrgStats,
selectedOrganization,
selectedOrgForStats,
]);
// Auto-open onboarding when there aren't enough active days of stats // Auto-open onboarding when there aren't enough active days of stats
useEffect(() => { useEffect(() => {
@@ -211,6 +229,7 @@ const NewDashboard = (props) => {
.then((response) => (response.ok ? response.json() : null)) .then((response) => (response.ok ? response.json() : null))
.then((org) => { .then((org) => {
if (!fetched && org) { if (!fetched && org) {
setSelectedOrganization(org);
if (!isCloud) { if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) { if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true); setIsProdStatusOn(true);
@@ -229,8 +248,136 @@ const NewDashboard = (props) => {
}; };
}, [userdata?.active_org?.id, globalUrl]); }, [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 ( 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' }}> <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 <DashboardOnboarding
open={onboardingOpen} open={onboardingOpen}
globalUrl={globalUrl} globalUrl={globalUrl}
@@ -251,6 +398,7 @@ const NewDashboard = (props) => {
headerTitle="Unlock your Dashboard" headerTitle="Unlock your Dashboard"
headerSubtitle="Complete these steps to start seeing insights." headerSubtitle="Complete these steps to start seeing insights."
/> />
)}
{showOverlay && ( {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={{ 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 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
@@ -262,9 +410,33 @@ const NewDashboard = (props) => {
{/* Header / Greeting */} {/* Header / Greeting */}
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}> <Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography> <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} {sfwControls}
</> </Box>
</Box> </Box>
{/* KPI cards */} {/* KPI cards */}
@@ -309,12 +481,27 @@ const NewDashboard = (props) => {
onControlsChange={handleSfwControls} onControlsChange={handleSfwControls}
onLoadingChange={setLoadingSfw} onLoadingChange={setLoadingSfw}
onTotalsChange={setTotals} onTotalsChange={setTotals}
loadingSelectedOrgStats={loadingSelectedOrgStats}
selectedOrganization={selectedOrganization}
selectedOrgForStats={selectedOrgForStats}
orgStats={selectedOrgStats}
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
/> />
</Paper> </Paper>
{/* Runs over time section */} {/* 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 }}> <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> </Paper>
</div> </div>
); );
+110 -63
View File
@@ -3,10 +3,10 @@ import React, {useState, useEffect, useContext} from 'react';
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import ReactJson from "react-json-view-ssr"; 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 { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
import { useNavigate, Link, useParams } from "react-router-dom"; 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 EditWorkflow from "../components/EditWorkflow.jsx"
import { toast } from "react-toastify" import { toast } from "react-toastify"
import { makeStyles } from '@mui/material/styles'; import { makeStyles } from '@mui/material/styles';
@@ -47,6 +47,10 @@ import {
OpenInNew as OpenInNewIcon, OpenInNew as OpenInNewIcon,
Edit as EditIcon, Edit as EditIcon,
Polyline as PolylineIcon, Polyline as PolylineIcon,
CheckCircle as CheckCircleIcon,
DirectionsRun as DirectionsRunIcon,
Error as ErrorIcon,
Pause as PauseIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
@@ -145,6 +149,7 @@ const RunWorkflow = (defaultprops) => {
padding: "25px 50px 50px 50px", padding: "25px 50px 50px 50px",
borderRadius: 25, borderRadius: 25,
minHeight: 500, minHeight: 500,
position: "relative",
} }
const params = useParams(); const params = useParams();
@@ -189,7 +194,7 @@ const RunWorkflow = (defaultprops) => {
// questions if none are selected // questions if none are selected
for (var key in executionArgument) { for (var key in executionArgument) {
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") { if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
console.log("Unanswered, required question: ", key) //console.log("Unanswered, required question: ", key)
return false return false
} }
} }
@@ -406,7 +411,7 @@ const RunWorkflow = (defaultprops) => {
setTimeout(() => { setTimeout(() => {
setExecutionLoading(true) setExecutionLoading(true)
}, 2500) }, 250)
var data = { var data = {
"execution_argument": executionArgument, "execution_argument": executionArgument,
@@ -842,6 +847,7 @@ const RunWorkflow = (defaultprops) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success === false) { if (responseJson.success === false) {
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
return return
} }
@@ -915,20 +921,37 @@ const RunWorkflow = (defaultprops) => {
//console.log("Updating data!") //console.log("Updating data!")
setExecutionData(responseJson) setExecutionData(responseJson)
for (var key in responseJson.results) { for (var key in responseJson.results) {
if (responseJson.results[key].status === "WAITING") { if (responseJson.results[key].status !== "WAITING") {
const validate = validateJson(responseJson.results[key].result) continue
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
} }
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 { } else {
console.log("NOT updating executiondata state."); console.log("NOT updating executiondata state.");
@@ -1034,7 +1057,7 @@ const RunWorkflow = (defaultprops) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log("Status not 200 for stream results :O!"); 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(); 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") { 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) 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) { 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) setupSourcenode(responseJson.workflow, sourceNode)
setWorkflow(responseJson.workflow) setWorkflow(responseJson.workflow)
//const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows //const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
// Check for decision_id in url // Check for decision_id in url
if (decisionId?.length > 0 && responseJson?.workflow?.actions?.length > 0 && sourceNode?.length > 0 && responseJson?.results?.length > 0) { 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) setAgentic(true)
@@ -1409,6 +1433,27 @@ const RunWorkflow = (defaultprops) => {
const basedata = const basedata =
<div style={bodyDivStyle}> <div style={bodyDivStyle}>
<Paper style={boxStyle}> <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 === true ?
<ExplorerUi /> <ExplorerUi />
: :
@@ -1562,7 +1607,6 @@ const RunWorkflow = (defaultprops) => {
label={parsedLabel} label={parsedLabel}
required required
disabled={disabledButtons}
fullWidth={true} fullWidth={true}
placeholder="" placeholder=""
id="emailfield" id="emailfield"
@@ -1582,43 +1626,46 @@ const RunWorkflow = (defaultprops) => {
: :
(answer !== undefined && answer !== null) || message !== "" ? null : (answer !== undefined && answer !== null) || message !== "" ? null :
<span> executionRunning ? null :
{foundSourcenode !== undefined && foundSourcenode !== null ? <span>
"Add Note" {foundSourcenode !== undefined && foundSourcenode !== null ?
: "Add Note"
"Runtime Argument" :
} "Runtime Argument"
}
<div style={{marginBottom: 5}}> <div style={{marginBottom: 5}}>
<TextField <TextField
color="primary" disabled={executionRunning}
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }} color="primary"
multiLine style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
maxRows={2} multiLine
type="text" maxRows={2}
autoComplete="off" type="text"
InputProps={{ autoComplete="off"
autocomplete: "off", InputProps={{
form: {
autocomplete: "off", autocomplete: "off",
}, form: {
style:{ autocomplete: "off",
height: "50px", },
color: "white", style:{
fontSize: "1em", height: "50px",
}, color: "white",
}} fontSize: "1em",
fullWidth={true} },
placeholder="" }}
id="emailfield" fullWidth={true}
margin="normal" placeholder=""
variant="outlined" id="emailfield"
onChange={(e) => { margin="normal"
setExecutionArgument(e.target.value) variant="outlined"
}} onChange={(e) => {
/> setExecutionArgument(e.target.value)
</div> }}
</span> />
</div>
</span>
} }
@@ -1724,12 +1771,12 @@ const RunWorkflow = (defaultprops) => {
> >
{executionLoading ? {executionLoading ?
<CircularProgress color="secondary" style={{color: "white",}} /> <CircularProgress color="secondary" style={{color: "white",}} />
: : executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ?
executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ? "Run Again" "Run Again"
: : executionData?.status === "WAITING" ?
"Submit" "Submit answers"
} : "Submit"}
</Button> </Button>
</div> </div>
} }
@@ -1780,7 +1827,7 @@ const RunWorkflow = (defaultprops) => {
} }
return ( 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}: {foundresult?.action?.label?.replaceAll("_", " ")} - {foundresult.status}:
<br /> <br />
@@ -1807,7 +1854,7 @@ const RunWorkflow = (defaultprops) => {
{workflowQuestion !== "" ? null : {workflowQuestion !== "" ? null :
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 10, }} > <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> </Typography>
} }
</div> </div>
-576
View File
@@ -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 = { const chipStyle = {
backgroundColor: "#3d3f43", backgroundColor: "#3d3f43",
marginRight: 5, marginRight: 5,
@@ -427,292 +137,6 @@ const chipStyle = {
color: "white", 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 //Custom hook for handling styling of the dropzone
+190 -60
View File
@@ -102,6 +102,7 @@ import {
List as ListIcon, List as ListIcon,
Publish as PublishIcon, Publish as PublishIcon,
GetApp as GetAppIcon, GetApp as GetAppIcon,
Image as ImageIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
// Additional Components // Additional Components
@@ -139,8 +140,115 @@ const getCookie = (name) => {
return ""; 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) => { export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs. // Finds the icon based on the action. Should be verbs.
const iconList = [ const iconList = [
{ key: "cases", values: ["cases", "ticket", "alert"] }, { key: "cases", values: ["cases", "ticket", "tickets", "alert"] },
{ key: "cache_add", values: ["set_cache"] }, { key: "cache_add", values: ["set_cache"] },
{ key: "cache_get", values: ["get_cache"] }, { key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter"] }, { key: "filter", values: ["filter"] },
@@ -181,7 +289,7 @@ export const GetIconInfo = (action) => {
key: "repeat", key: "repeat",
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",], 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: "extract", values: ["extract", "unpack", "decompress", "open"] },
{ key: "inflate", values: ["inflate", "pack", "compress"] }, { key: "inflate", values: ["inflate", "pack", "compress"] },
{ {
@@ -211,7 +319,6 @@ export const GetIconInfo = (action) => {
{ key: "communication", values: ["communication", "comms", "email", "mail",] }, { key: "communication", values: ["communication", "comms", "email", "mail",] },
{ key: "eradication", values: ["eradication", "edr", "xdr", "sigma", "yara",] }, { key: "eradication", values: ["eradication", "edr", "xdr", "sigma", "yara",] },
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] }, { 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: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
{ {
key: "send", key: "send",
@@ -235,7 +342,8 @@ export const GetIconInfo = (action) => {
"passwd", "passwd",
"protect", "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 = "" var selectedKey = ""
@@ -482,29 +590,35 @@ export const GetIconInfo = (action) => {
return selectedItem; 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.name === "headers" || field.name === "cookies") {
if (field === undefined || field === null) { return true
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 false
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
} }
export const validateJson = (showResult) => { export const validateJson = (showResult) => {
@@ -700,7 +814,8 @@ const Workflows2 = (props) => {
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid"); const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; 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; const imgSize = 60;
@@ -2158,7 +2273,7 @@ const Workflows2 = (props) => {
position: "relative", position: "relative",
borderRadius: "8px", borderRadius: "8px",
// backgroundColor: "#212121", // backgroundColor: "#212121",
padding: "15px 15px 0px 20px", padding: "15px 15px 0px 30px",
}; };
const gridContainer = { const gridContainer = {
@@ -2177,7 +2292,6 @@ const Workflows2 = (props) => {
justifyContent: "space-between", justifyContent: "space-between",
fontFamily: theme.typography?.fontFamily, fontFamily: theme.typography?.fontFamily,
textAlign: "center", textAlign: "center",
margin: "auto",
}; };
const exportAllWorkflows = (allWorkflows) => { const exportAllWorkflows = (allWorkflows) => {
@@ -2959,7 +3073,7 @@ const Workflows2 = (props) => {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) { if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5, //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 = image =
@@ -2997,10 +3111,10 @@ const Workflows2 = (props) => {
relevantTrigger = trigger relevantTrigger = trigger
if (trigger?.status === "running") { if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}` imageStyle.border = `2px solid ${green}`
break break
} else { } else {
imageStyle.border = `3px solid ${red}` imageStyle.border = `2px solid ${red}`
} }
@@ -3012,10 +3126,10 @@ const Workflows2 = (props) => {
relevantTrigger = trigger relevantTrigger = trigger
if (trigger?.status === "running") { if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}` imageStyle.border = `2px solid ${green}`
break break
} else { } 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 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) const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
return ( return (
<div <div
id={`workflowbox-${data.id}`} 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 ? {foundImage?.length > 0 ?
@@ -3140,26 +3253,7 @@ const Workflows2 = (props) => {
style={{ display: "flex", flexDirection: "column", width: "100%", fontFamily: theme.typography?.fontFamily }} style={{ display: "flex", flexDirection: "column", width: "100%", fontFamily: theme.typography?.fontFamily }}
> >
<Grid item style={{ display: "flex", maxHeight: 34 }}> <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 <Tooltip arrow
onMouseEnter={() => { onMouseEnter={() => {
@@ -3228,7 +3322,7 @@ const Workflows2 = (props) => {
<Typography <Typography
style={{ style={{
textAlign: "center", textAlign: "left",
marginBottom: 0, marginBottom: 0,
paddingBottom: 0, paddingBottom: 0,
fontSize: 22, fontSize: 22,
@@ -3239,8 +3333,9 @@ const Workflows2 = (props) => {
fontWeight: 500, fontWeight: 500,
minWidth: 375, minWidth: 375,
maxWidth: 375, maxWidth: 375,
margin: "auto",
overflow: "hidden", overflow: "hidden",
color: "rgba(255,255,255,0.85)",
}} }}
> >
<Link <Link
@@ -3431,14 +3526,13 @@ const Workflows2 = (props) => {
style={{ style={{
justifyContent: "left", justifyContent: "left",
overflow: "hidden", overflow: "hidden",
marginTop: 13, marginTop: 8,
maxHeight: 35, maxHeight: 35,
fontFamily: theme.typography?.fontFamily, fontFamily: theme.typography?.fontFamily,
textAlign: "center", textAlign: "left",
minWidth: 200, minWidth: 200,
maxWidth: 200, maxWidth: 200,
margin: "auto",
}} }}
> >
{data.tags !== undefined && data.tags !== null {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" ? {(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"> <Tooltip title="Edit Form" placement="top">
<div style={{ position: "absolute", top: 80, right: 8, }}> <div style={{ position: "absolute", top: 100, right: 8, }}>
<IconButton <IconButton
aria-label="more" aria-label="more"
aria-controls="long-menu" aria-controls="long-menu"
@@ -3521,6 +3615,27 @@ const Workflows2 = (props) => {
</Tooltip> </Tooltip>
: null} : 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> </Grid>
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 && {showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
@@ -5305,6 +5420,21 @@ const Workflows2 = (props) => {
</IconButton> </IconButton>
</Tooltip> </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"> <Tooltip title="Explore Workflow Runs (debugger)" placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle} style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}