Tons of minor fixes to the based on cloud tests
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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?");
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user