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';
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 { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify';
@@ -265,7 +265,7 @@ const AdminNavBar = (props) => {
: selectedOrganization?.image;
return (
!isOrgLoaded && !isUserDataLoaded ? <Loader /> :
!isOrgLoaded || !isUserDataLoaded ? <Loader /> :
<Wrapper>
<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' }}>
@@ -358,7 +358,6 @@ const AdminNavBar = (props) => {
export default AdminNavBar;
const Loader = () => {
const dummyItems = Array.from({ length: 6 });
const dummyNavItems = Array.from({ length: 6 });
const dummyTabItems = ['Org Configuration', 'SSO', 'Notifications', 'Billing & Stats', 'Branding'];
const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context);
@@ -366,9 +365,7 @@ const Loader = () => {
return (
<div style={{
display: 'flex',
width: '100%',
height: '100%',
minHeight: '100vh',
width: '100%',
maxWidth: '1200px',
fontFamily: 'Arial, sans-serif',
paddingLeft: leftSideBarOpenByClick ? windowWidth <= 1300 ? 220 : 200 : 80,
@@ -446,15 +443,49 @@ const Loader = () => {
</div>
<div style={{ flex: 1, padding: '24px' }}>
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', margin: '0 auto', alignItems: 'flex-start' }}>
<Skeleton variant='square' width="200px" height="200px" sx={{ marginBottom: '20px' }} />
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', width: '100%', marginTop: '50px' }}>
{dummyItems.map((_, index) => (
<div key={index} style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Skeleton width={'400px'} height="36px" />
</div>
))}
<Typography variant="h2">
<Skeleton variant="text" width={289} height={29}/>
</Typography>
<Typography variant="body2" sx={{mb: 2}}>
<Skeleton variant="text" width={550} height={22}/>
</Typography>
<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>
<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>
+1 -1
View File
@@ -30,7 +30,7 @@ import {
} from "@mui/material";
import throttle from "lodash/throttle";
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 { Context } from "../context/ContextApi.jsx";
+1 -1
View File
@@ -33,7 +33,7 @@ import {
ClearRefinements,
connectStateResults
} from "react-instantsearch-dom";
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import aa from "search-insights";
import { useLocation } from 'react-router-dom';
+5 -3
View File
@@ -22,7 +22,7 @@ import {
import aa from 'search-insights'
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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 theme = getTheme(themeMode)
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
window.title = "Shuffle | Apps | Find and integration any app"
const parsedInputHeight = inputHeight === undefined || inputHeight === null ? 295 : inputHeight
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
@@ -91,7 +93,7 @@ const Appsearch = props => {
var counted = 0
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) => {
const paperStyle = {
backgroundColor: index === mouseHoverIndex ? theme.palette.hoverColor : theme.palette.textFieldStyle.backgroundColor,
@@ -182,7 +184,7 @@ const Appsearch = props => {
const CustomHits = connectHits(InputHits)
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">
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
+1 -1
View File
@@ -3141,7 +3141,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
orgId: subOrg.id,
limit: subOrg?.sync_features?.app_executions?.limit || "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",
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 { isMobile } from "react-device-detect";
import { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify';
import AppSearch from "../components/AppSearch1.jsx";
@@ -37,14 +38,24 @@ const ChatBot = (props) => {
const [appAuthentication, setAppAuthentication] = React.useState([]);
const [inputAuth, setInputAuth] = useState([])
const [forceReauthentication, setForceReauthentication] = useState(false);
const [selectedType, setSelectedType] = useState("atomic");
const [selectedType, setSelectedType] = useState("support");
const [appname, setAppname] = useState("");
const [threadId, setThreadId] = 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);
// Get thread ID from URL params
const { threadId: urlThreadId } = useParams();
let navigate = useNavigate();
const waitingMsg = "Processing..."
const viewWidth = isMobile ? "92%" : 800
@@ -82,8 +93,159 @@ const ChatBot = (props) => {
}
}, [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"
let navigate = useNavigate();
// Automatic submit handler based on a lot of stuff :)
const handleSubmit = (e, inputmsg) => {
@@ -102,6 +264,8 @@ const ChatBot = (props) => {
"run_id": runId,
}
console.log("Sending message with thread_id:", threadId);
if (appname !== undefined && appname !== null && appname !== "") {
parsedData["app_name"] = appname
}
@@ -190,6 +354,12 @@ const ChatBot = (props) => {
if (data.thread_id !== undefined && data.thread_id !== null && 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) {
@@ -423,8 +593,6 @@ const ChatBot = (props) => {
border: `1px solid ${theme.palette.inputColor}`,
}}
color="primary"
fullWidth
color="primary"
onClick={(e) => {
console.log("Click? ")
e.preventDefault();
@@ -520,32 +688,38 @@ const ChatBot = (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) {
//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] : ""
return (
<div
style={{
minWidth: "50%",
maxWidth: "100%",
backgroundColor: theme.palette.inputColor,
overflowY: "auto",
// Check if props.inline === true, then do it inline
padding: props.inline ? 0 : 15,
display: props.inline ? "inline" : "block",
backgroundColor: props.inline ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.3)",
borderRadius: props.inline ? "4px" : "8px",
padding: props.inline ? "2px 6px" : "12px 16px",
display: props.inline ? "inline" : "block",
margin: props.inline ? "0 2px" : "8px 0",
border: "1px solid rgba(255, 255, 255, 0.1)",
overflowX: "auto"
}}
>
<code
style={{
// Wrap if larger than X
whiteSpace: "pre-wrap",
overflow: "auto",
whiteSpace: props.inline ? "nowrap" : "pre-wrap",
fontSize: props.inline ? "0.9em" : "0.85em",
fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', monospace",
color: "rgba(255, 255, 255, 0.9)"
}}
>{propvalue}</code>
</div>
@@ -654,7 +828,12 @@ const ChatBot = (props) => {
const Paragraph = (props) => {
return (
<p style={{marginTop: 15, marginBottom: 15, }}>
<p style={{
marginTop: 8,
marginBottom: 8,
lineHeight: 1.6,
color: "inherit"
}}>
{props.children}
</p>
)
@@ -677,14 +856,183 @@ const ChatBot = (props) => {
const chatWindow =
<div style={{minWidth: viewWidth, maxWidth: viewWidth, margin: "auto", textAlign: "left", minHeight: 1500, }}>
{messages.length === 0 ?
<span>
<h1>Shuffle AI</h1>
{/* Loading state for thread */}
{isLoadingThread ? (
<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}
</span>
{!isLoadingThread && !threadError && messages.length === 0 ?
<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}
<div
id="messages-window"
@@ -698,140 +1046,235 @@ const ChatBot = (props) => {
}}
>
{messages.map((message, index) => {
const float = message.status === "sent" ? "left" : "right";
const border = message.status === "error" ? "red" : "rgba(255,255,255,0.3)"
const hasAction = message.action !== undefined && message.action !== null && message.action !== ""
const isUser = message.status === "sent";
const isError = message.status === "error";
const hasAction = message.action !== undefined && message.action !== null && message.action !== "";
return (
// Make a chat bubble component
<div key={index} style={{position: "relative", width: "100%", marginTop: 15, marginLeft: isMobile ? 10 : 0, }}>
<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={() => {
if (!hasAction) {
return
}
<div key={index} style={{
position: "relative",
width: "100%",
marginBottom: 20,
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") {
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT")
} else if (message.action === "app_authentication") {
console.log("App auth action!")
//setAuthenticationModalOpen(true)
} else {
console.log("\n\nUnknown click action: ", message.action)
}
if (message.action === "login") {
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT");
} else if (message.action === "app_authentication") {
console.log("App auth action!");
} else {
console.log("\n\nUnknown click action: ", message.action);
}
}}>
{message.message === waitingMsg ? <CircularProgress style={{height: 20, width: 20, marginTop: 20, marginRight: 10, }} /> : null}
<span>
<Markdown
components={markdownComponents}
id="markdown_wrapper"
style={{
minHeight: 20,
marginTop: 0,
display: "flex",
flexDirection: "row",
}}
>
{message.message}
</Markdown>
{message.message === waitingMsg ? (
<div style={{display: "flex", alignItems: "center"}}>
<CircularProgress size={20} style={{marginRight: 12, color: "rgba(255, 255, 255, 0.7)"}} />
<span style={{color: "rgba(255, 255, 255, 0.7)"}}>Processing...</span>
</div>
) : (
<div>
<Markdown
components={markdownComponents}
style={{
color: "inherit",
lineHeight: 1.6,
}}
>
{message.message}
</Markdown>
{message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" ?
<Typography variant="body2" style={{color: "rgba(255,255,255,0.5)", marginTop: 5, }}>
Thread: {message.thread_id}
</Typography>
: null
}
</span>
</Typography>
{message.thread_id !== undefined && message.thread_id !== null && message.thread_id !== "" && !isUser ? (
<Typography variant="caption" style={{
color: "rgba(255,255,255,0.4)",
marginTop: 8,
display: "block",
fontSize: "0.75rem"
}}>
Thread: {message.thread_id}
</Typography>
) : null}
</div>
)}
</div>
{message.status === "error" && message.error_message ?
<Typography variant="body2" style={{color: "red", }}>
{message.status === "error" && message.error_message ? (
<Typography variant="body2" style={{
color: "#ff4444",
marginTop: 8,
fontSize: "0.85rem",
fontStyle: "italic"
}}>
{message.error_message}
</Typography>
: null}
) : null}
{(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ?
<div style={{position: "absolute", right: 0, bottom: -100, }}>
{(message.action === "select_category" || message.action === "select_app") && showAppSearch && index === messages.length-1 ? (
<div style={{position: "absolute", right: 0, bottom: -100}}>
<AppSearch
placeholder={"Find your "+message.category+" app"}
setNewSelectedApp={setAppname}
/>
</div>
: null}
) : null}
</div>
)
);
})}
</div>
{showAuthentication}
<div style={{position: "fixed", bottom: 0, left: 0, width: "100%", zIndex: 100, backgroundColor: theme.palette.platformColor, }}>
<div style={{width: viewWidth, margin: "auto", }}>
<div style={{
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 ?
<span>
<Typography variant="body2" color="textSecondary">
<div style={{marginBottom: 20}}>
<Typography variant="body2" style={{
color: "rgba(255, 255, 255, 0.7)",
marginBottom: 12,
fontWeight: 500
}}>
Query Type
</Typography>
<ButtonGroup
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
fullWidth
variant={selectedType === "support" ? "contained" : "outlined"}
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
</Button>
</ButtonGroup>
</span>
</div>
: 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
id="message"
fullWidth
disabled={loading}
label="Send a message"
disabled={loading || chatDisabled}
placeholder={chatDisabled ? "Chat disabled - switch organization to participate" : "Type your message..."}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e, message);
}
}}
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={{
endAdornment: (
<IconButton
aria-label="send message"
disabled={chatDisabled || loading || !message.trim()}
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>
)
}}
/>
</form>
{isMobile ? null :
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 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}`}
{!isMobile ? (
<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>
}
) : null}
</div>
</div>
</div>
+9 -3
View File
@@ -49,7 +49,7 @@ const CloudSyncTab = (props) => {
const itemColor = "white";
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);
useEffect(() => { getSettings(); }, []);
@@ -327,7 +327,6 @@ const CloudSyncTab = (props) => {
);
};
const handleGetOrg = (orgId) => {
if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search);
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) {
toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.");
return;
}
// Just use this one?
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
@@ -359,7 +363,7 @@ const CloudSyncTab = (props) => {
})
.then((responseJson) => {
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 {
if (
responseJson.sync_features === undefined ||
@@ -457,6 +461,8 @@ const CloudSyncTab = (props) => {
setLoading(false);
if (response.status === 200) {
console.log("Cloud sync success?");
handleGetOrg(userdata.active_org.id);
setUpdateOrg(true);
} else {
console.log("Cloud sync fail?");
}
+1 -1
View File
@@ -36,7 +36,7 @@ import {
Avatar,
AvatarGroup,
} from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CreatorGrid = props => {
+1 -1
View File
@@ -26,7 +26,7 @@ import {
ListItemAvatar,
ListItemText,
} 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 BASE_URL="${newUrl}" \\${addProxy ? `
-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 \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest
`)
@@ -467,8 +467,7 @@ const EnvironmentTab = memo((props) => {
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${props.userdata.active_org.id}" \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
-e HTTPS_PROXY=IP:PORT \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest`
return commandData
@@ -959,7 +958,7 @@ const EnvironmentTab = memo((props) => {
display: "grid",
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
width: "100%",
minWidth: 800,
minWidth: showLoader ? 800 : 0,
paddingBottom: 0,
borderBottom: theme.palette.defaultBorder,
}}
@@ -976,7 +975,7 @@ const EnvironmentTab = memo((props) => {
textOverflow: "ellipsis",
overflow: "hidden",
fontWeight: "bold",
textAlign: header === "Actions" ? "left" : header === "Distribution" ? "right" : "center",
textAlign: "center",
}}
/>
)
@@ -987,7 +986,6 @@ const EnvironmentTab = memo((props) => {
<ListItem
key={rowIndex}
style={{
display: "grid",
backgroundColor: theme.palette.platformColor,
height: 40,
width: "100%",
@@ -1674,10 +1672,9 @@ const EnvironmentTab = memo((props) => {
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
<Typography variant='body2' color="textSecondary">Disable Detection Controller</Typography>
</div>
{/*
<div style={{display: 'flex', alignItems: 'center', }}>
<Checkbox
id="shuffle_skip_pipelines"
@@ -1692,9 +1689,8 @@ const EnvironmentTab = memo((props) => {
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography>
<Typography variant='body2' color="textSecondary">Use Remote Pipeline</Typography>
</div>
*/}
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
@@ -1706,16 +1702,16 @@ const EnvironmentTab = memo((props) => {
</Typography>
</div>
</div>
{currentEnvQueue.length === 0 ? null :
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
{currentEnvQueue.map((queueItem, queueIndex) => {
return (
<ListItem
key={queueIndex}
style={{
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.platformColor,
border: '1px solid #333333',
borderBottom: theme.palette.defaultBorder,
maxHeight: 50,
}}
>
<ListItemText style={{minWidth: 50, maxWidth: 50, }}>
@@ -1767,7 +1763,7 @@ const EnvironmentTab = memo((props) => {
padding: 15,
textAlign: "center",
height: 70,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.platformColor,
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 ReactJson from "react-json-view-ssr";
import { toast } from 'react-toastify';
import { validateJson } from "../views/Workflows.jsx";
import { validateJson } from "../views/Workflows2.jsx";
// import HandleJsonCopy from "./ShuffleCodeEditor1";
const STATUS_CONFIG = {
+8 -4
View File
@@ -64,7 +64,7 @@ const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
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 [activeOrgName, setActiveOrgName] = useState(
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 }}>
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>
</Typography>
</Menu>
@@ -956,6 +956,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
.then((org) => {
if (!fetched && org) {
setActiveOrgData(org);
setUpdateOrg(false);
if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
@@ -972,7 +973,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
return () => {
fetched = true;
};
}, [userdata?.active_org?.id, globalUrl]);
}, [userdata?.active_org?.id, globalUrl, updateOrg]);
return (
<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", }}>
<Button
component={Link}
to={"/new-dashboard"}
to={userdata?.support === true ? "/new-dashboard" : "/workflows"}
onClick={(event) => {
if(!userdata?.support){
return;
}
setOpenautomateTab(true);
setOpenSecurityTab(false);
setCurrentOpenTab("new-dashboard");
@@ -35,6 +35,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import InfoIcon from "@mui/icons-material/Info";
import { Link, useNavigate } from "react-router-dom";
// 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 */}
<Dialog
open={openDialog}
onClose={handleCloseDialog}
onClose={(event, reason) => {
if (reason !== "backdropClick") {
handleCloseDialog();
}
}}
disableEscapeKeyDown
maxWidth="md"
fullWidth
PaperProps={{
@@ -1642,7 +1648,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
<Box key={contentIndex} sx={{ mb: 1 }}>
<TextField
multiline
rows={5}
rows={6}
value={content}
onChange={(e) => {
const newItems = [...formData.navigation.items];
@@ -1653,7 +1659,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
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
sx={{
"& .MuiOutlinedInput-root": {
@@ -1661,6 +1675,10 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder },
},
"& .MuiOutlinedInput-root textarea": {
overflow: "auto",
resize: "vertical",
},
}}
/>
{item.content.length > 1 && (
+20 -13
View File
@@ -104,13 +104,12 @@ const RunDetectionTest = (props) => {
return
}
if (haveDetectionPipelines() === false) {
if (haveDetectionPipelines().length === 0) {
setDetectionTestRunning(false)
toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.")
return
}
// 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`
@@ -124,7 +123,17 @@ const RunDetectionTest = (props) => {
// 1. Submit it to run
// 2. Check executions if they happened recently~
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++) {
@@ -141,10 +150,10 @@ const RunDetectionTest = (props) => {
const haveDetectionPipelines = () => {
if (pipelines === undefined) {
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) {
const curPipeline = pipelines[pipelineKey]
//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")) {
foundCorrect += 1
foundCorrect.push(curPipeline.environment)
}
}
if (foundCorrect >= 1) {
return true
}
return false
return foundCorrect
}
return (
<div style={{display: "flex", }}>
<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>
<Button
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
variant="outlined"
color="secondary"
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
disabled={haveDetectionPipelines().length === 0 || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
onClick={() => {
//setPipelineModalOpen(true)
runDetectionTest()
+280 -25
View File
@@ -13,8 +13,51 @@ function formatCompactNumber(value) {
return `${n}`;
}
const RunsOverTimeWidget = (props) => {
const { globalUrl, onLoadingChange, monthOverride, dummyMode } = props;
// Helpers to keep date and "today" logic concise and consistent
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 [series, setSeries] = useState([]);
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
const fetchSeriesForKey = async (key) => {
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 doFetch = async (u) => {
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);
if (a.length > 0) return a;
// Optional org route fallback if present globally
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;
}
// Final fallback: old aggregate endpoint returning daily_statistics
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 [];
// // Final fallback: old aggregate endpoint returning daily_statistics
// 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) {
return [];
}
@@ -154,9 +282,13 @@ const RunsOverTimeWidget = (props) => {
}
}, [monthOverride]);
useEffect(() => {
load(mode);
}, [mode, globalUrl, days, selectedMonth, dummyMode]);
useEffect(() => {
if (loadingSelectedOrgStats === true) {
setSeries([]);
return;
}
load(mode);
}, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
// Notify parent on loading changes
useEffect(() => {
@@ -169,7 +301,7 @@ const RunsOverTimeWidget = (props) => {
(Array.isArray(series) ? series : []).map((d) => {
const dt = new Date(d.key);
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' });
return { key: label, data: Number(d?.data || 0) };
})
@@ -197,7 +329,7 @@ const RunsOverTimeWidget = (props) => {
backgroundColor: "#1A1A1A",
border: "1px solid rgba(255,255,255,0.22)",
color: theme.palette.text.primary,
padding: 8,
padding: 10,
maxWidth: 240,
pointerEvents: 'none',
}}>
@@ -208,6 +340,7 @@ const RunsOverTimeWidget = (props) => {
/>
}
/>;
return (
<div>
@@ -295,7 +428,12 @@ const RunsOverTimeWidget = (props) => {
</div>
<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
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
height={300}
@@ -315,6 +453,123 @@ const RunsOverTimeWidget = (props) => {
}
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>
+3 -2
View File
@@ -768,6 +768,7 @@ const RuntimeDebugger = (props) => {
toast("Error executing workflow: "+responseJson.error)
} else {
console.log("Executed workflow: ", responseJson)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}
})
.catch((error) => {
@@ -1236,8 +1237,8 @@ const RuntimeDebugger = (props) => {
setRowsPerPage(newPaginationModel.pageSize)
// No API call needed - this is just for client-side pagination
}}
onRowSelectionModelChange={(newSelection) => {
selectionModel={selectedWorkflowExecutions.map((workflow) => workflow.id)}
onSelectionModelChange={(newSelection) => {
//console.log("newSelection: ", newSelection)
//setSelectedWorkflowExecutionsIndexes(newSelection)
var found = []
+92 -4
View File
@@ -17,6 +17,8 @@ import {
TextField,
Chip,
CircularProgress,
Select,
MenuItem,
} from '@mui/material';
import {
@@ -39,11 +41,14 @@ const SchedulesTab = memo((props) => {
const [showLoader, setShowLoader] = React.useState(true);
const [workflows, setWorkflows] = React.useState([]);
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 [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const [environments, setEnvironments] = React.useState([]);
const [selectedEnvironment, setSelectedEnvironment] = React.useState("");
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
@@ -74,7 +79,7 @@ const SchedulesTab = memo((props) => {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].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;
}
}
@@ -91,6 +96,7 @@ const SchedulesTab = memo((props) => {
handleGetWorkflows()
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
handleGetAllTriggers()
handleGetEnvironments()
}
}, [])
@@ -168,8 +174,8 @@ const SchedulesTab = memo((props) => {
})
}
const submitPipelineWrapper = (pipelineValue) => {
submitPipeline(pipelineValue)
const submitPipelineWrapper = (pipelineValue, environment) => {
submitPipeline(pipelineValue, environment)
}
const NewPipelineView = (
@@ -277,9 +283,48 @@ const SchedulesTab = memo((props) => {
setNewPipelineValue(event.target.value)
}
/>
</div>
</DialogContent>
<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
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
onClick={() => {
@@ -314,6 +359,10 @@ const SchedulesTab = memo((props) => {
start_node: "",
}
if (selectedEnvironment !== undefined && selectedEnvironment !== "") {
pipelineConfig.environment = selectedEnvironment.Name
}
if (environment !== undefined && 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 = () => {
fetch(globalUrl + "/api/v1/triggers", {
@@ -52,7 +52,7 @@ import {
} from '@mui/icons-material';
import { validateJson } from "../views/Workflows.jsx";
import { validateJson } from "../views/Workflows2.jsx";
import ReactJson from "react-json-view-ssr";
import PaperComponent from "../components/PaperComponent.jsx";
@@ -969,6 +969,10 @@ const CodeEditor = (props) => {
// Whelp this is inefficient af. Single loop pls
// When the found array is empty.
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)
try {
@@ -24,7 +24,7 @@ import {
LinearYAxisTickSeries,
LinearYAxisTickLabel,
} from "reaviz";
import theme from "../theme";
import theme from "../theme.jsx";
// KPI configuration constants
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
function formatDay(dateInput) {
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 {
return String(dateInput);
}
@@ -221,8 +225,8 @@ function buildGroupedSeries(selectedStatus, okArr, failArr) {
return { grouped, scheme };
}
const SuccessFailedRunsWidget = (props) => {
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode } = props;
const SuccessFailedRunsWidget = (props) => {
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode, loadingSelectedOrgStats, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit } = props;
const [mode, setMode] = useState("workflows"); // 'workflows' | 'apps'
const [days, setDays] = useState(30);
@@ -236,6 +240,25 @@ const SuccessFailedRunsWidget = (props) => {
const [loading, setLoading] = useState(false);
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(() => {
try {
if (typeof onTotalsChange !== "function") return;
@@ -264,7 +287,132 @@ const SuccessFailedRunsWidget = (props) => {
const fetchSeriesForKey = async (key) => {
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 doFetch = async (u) => {
const r = await fetch(u, { method: "GET", credentials: "include" });
@@ -272,25 +420,17 @@ const SuccessFailedRunsWidget = (props) => {
const j = await r.json();
return Array.isArray(j?.entries) ? j.entries : [];
};
const a = await doFetch(urlA);
if (a.length > 0) return a;
// Optional orgId fallback if exposed globally in app
// 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 a = await doFetch(urlA);
return a;
} catch (e) {
return [];
}
};
const fetchSeries = async () => {
setLoading(true);
try {
// This will only run if dummyMode is true (Onboarding preview)
if (dummyMode) {
// 10-day wave with a couple of bumps for a more dynamic preview
const today = new Date();
@@ -324,47 +464,6 @@ const SuccessFailedRunsWidget = (props) => {
let okSeries = normalizeEntries(succ);
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);
setSeriesFail(failSeries);
} 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(() => {
if (firstLoadRef.current) {
firstLoadRef.current = false;
if (loadingSelectedOrgStats === true) {
setSeriesOk([]);
setSeriesFail([]);
return;
}
fetchSeries();
return;
}
fetchSeries();
}, [dummyMode, days, globalUrl, mode]);
}, [dummyMode, days, globalUrl, mode, selectedOrgForStats, selectedOrganization, loadingSelectedOrgStats]);
// Apply external days override (e.g. after onboarding completes)
useEffect(() => {
@@ -406,7 +504,13 @@ const SuccessFailedRunsWidget = (props) => {
const totalEntries = await fetchSeriesForKey("workflow_executions");
const series = normalizeEntries(totalEntries);
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();
for (const it of series) {
@@ -421,14 +525,14 @@ const SuccessFailedRunsWidget = (props) => {
};
run();
return () => { aborted = true; };
}, [globalUrl, days, dummyMode, overrideDays]);
}, [globalUrl, days, dummyMode, overrideDays, selectedOrgForStats]);
// Notify parent about loading state changes
useEffect(() => {
if (typeof onLoadingChange === "function") {
onLoadingChange(loading);
onLoadingChange(loadingSelectedOrgStats || loading);
}
}, [loading, onLoadingChange]);
}, [loading, loadingSelectedOrgStats, onLoadingChange]);
// Build filters UI once here; optionally render externally via onControlsChange
const controlsNode = React.useMemo(() => (
@@ -600,6 +704,9 @@ const SuccessFailedRunsWidget = (props) => {
}
}, [onControlsChange, controlsNode]);
var showParentLimit = false;
var showSuborgLimit = false;
return (
<div style={{ width: "100%", display: "flex", flexDirection: "column" }}>
{/* 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>
{(() => {
{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
const useOk = Array.isArray(seriesOk) ? seriesOk : [];
const useFail = Array.isArray(seriesFail) ? seriesFail : [];
@@ -690,39 +801,60 @@ const SuccessFailedRunsWidget = (props) => {
);
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 (
<AreaChart
height={300}
width={"100%"}
data={grouped}
yAxis={
<LinearYAxis
domain={[0, paddedMax]}
tickSeries={
<LinearYAxisTickSeries
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
/>
}
/>
}
xAxis={
<LinearXAxis
tickSeries={
<LinearXAxisTickSeries
tickSize={0}
label={
<LinearXAxisTickLabel
padding={8}
format={(d) => formatLabel(Number(d))}
/>
}
tickValues={tickValues}
/>
}
/>
}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
series={
<div style={{ position: 'relative', width: '100%', height: 300 }}>
<AreaChart
height={300}
width={"100%"}
data={grouped}
yAxis={
<LinearYAxis
domain={[0, paddedMax]}
tickSeries={
<LinearYAxisTickSeries
label={<LinearYAxisTickLabel format={(d) => formatCompactNumber(d)} />}
/>
}
/>
}
xAxis={
<LinearXAxis
tickSeries={
<LinearXAxisTickSeries
tickSize={0}
label={
<LinearXAxisTickLabel
padding={8}
format={(d) => formatLabel(Number(d))}
/>
}
tickValues={tickValues}
/>
}
/>
}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
series={
<AreaSeries
type="grouped"
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
@@ -844,8 +1089,19 @@ const SuccessFailedRunsWidget = (props) => {
<LegendDot color="#22c55e" /> Successful Runs
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<LegendDot color="#ef4444" /> Failed Runs
<LegendDot color="#FD4C62" /> Failed Runs
</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
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,
Chip,
} from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
@@ -24,7 +24,7 @@ import {
} from "../views/AngularWorkflow.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";
const itemHeight = 24