Tons of minor fixes to the based on cloud tests

This commit is contained in:
Frikky
2025-11-12 11:39:18 +01:00
parent e470dbef7f
commit 822e7c1eb3
36 changed files with 3353 additions and 1882 deletions
+43 -12
View File
@@ -23,7 +23,7 @@ import {
} from '@mui/icons-material';
import theme, { getTheme } from '../theme.jsx';
import { Button, Skeleton, Tooltip } from '@mui/material';
import { Box, Button, Skeleton, Tooltip, Typography } from '@mui/material';
import { Index } from 'react-instantsearch-dom';
import { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify';
@@ -265,7 +265,7 @@ const AdminNavBar = (props) => {
: selectedOrganization?.image;
return (
!isOrgLoaded && !isUserDataLoaded ? <Loader /> :
!isOrgLoaded || !isUserDataLoaded ? <Loader /> :
<Wrapper>
<div style={{ flexDirection: 'column', width: 220, }}>
<nav style={{ padding: '25px 25px 3px 25px', height: isCloud ? "calc(100% - 60px)" : "calc(100% - 30px)" , fontSize: '16px', borderTopLeftRadius: 8, borderBottomLeftRadius: 8, background: theme.palette.platformColor, color: '#9CA3AF' }}>
@@ -358,7 +358,6 @@ const AdminNavBar = (props) => {
export default AdminNavBar;
const Loader = () => {
const dummyItems = Array.from({ length: 6 });
const dummyNavItems = Array.from({ length: 6 });
const dummyTabItems = ['Org Configuration', 'SSO', 'Notifications', 'Billing & Stats', 'Branding'];
const { leftSideBarOpenByClick, windowWidth, themeMode } = useContext(Context);
@@ -367,8 +366,6 @@ const Loader = () => {
<div style={{
display: 'flex',
width: '100%',
height: '100%',
minHeight: '100vh',
maxWidth: '1200px',
fontFamily: 'Arial, sans-serif',
paddingLeft: leftSideBarOpenByClick ? windowWidth <= 1300 ? 220 : 200 : 80,
@@ -446,16 +443,50 @@ 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>
</div>
+1 -1
View File
@@ -30,7 +30,7 @@ import {
} from "@mui/material";
import throttle from "lodash/throttle";
import {getTheme} from "../theme.jsx";
import { validateJson, collapseField, } from "../views/Workflows.jsx";
import { validateJson, collapseField, } from "../views/Workflows2.jsx";
import DeleteIcon from "@mui/icons-material/Delete";
import { Context } from "../context/ContextApi.jsx";
+1 -1
View File
@@ -33,7 +33,7 @@ import {
ClearRefinements,
connectStateResults
} from "react-instantsearch-dom";
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import aa from "search-insights";
import { useLocation } from 'react-router-dom';
+5 -3
View File
@@ -22,7 +22,7 @@ import {
import aa from 'search-insights'
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
@@ -38,6 +38,8 @@ const Appsearch = props => {
const borderRadius = 3
window.title = "Shuffle | Apps | Find and integration any app"
const parsedInputHeight = inputHeight === undefined || inputHeight === null ? 295 : inputHeight
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
@@ -91,7 +93,7 @@ const Appsearch = props => {
var counted = 0
return (
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: parsedInputHeight, minHeight: parsedInputHeight, overflowY: "auto", overflowX: "hidden", }}>
{hits.map((data, index) => {
const paperStyle = {
backgroundColor: index === mouseHoverIndex ? theme.palette.hoverColor : theme.palette.textFieldStyle.backgroundColor,
@@ -182,7 +184,7 @@ const Appsearch = props => {
const CustomHits = connectHits(InputHits)
return (
<div style={{width: isMobile ? null : 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}>
<div style={{width: isMobile ? null : 287, minHeight: parsedInputHeight, maxHeight: parsedInputHeight, padding: "16px 16px 64px 16px", alignItems: "center", }}>
<InstantSearch searchClient={searchClient} indexName="appsearch">
<div style={{maxWidth: 450, margin: "auto", }}>
<CustomSearchBox />
+1 -1
View File
@@ -3141,7 +3141,7 @@ const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization,
orgId: subOrg.id,
limit: subOrg?.sync_features?.app_executions?.limit || "N/A",
usage: stat?.monthly_app_executions || "N/A",
workflows_usage: stat?.total_workflow_executions || "N/A",
workflows_usage: stat?.monthly_workflow_executions || "N/A",
workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A",
app_runs_hard_limit: subOrg?.Billing?.app_runs_hard_limit || 0,
}
+123 -32
View File
@@ -4,10 +4,11 @@ import { getTheme } from "../theme.jsx";
import { toast } from 'react-toastify';
import ReactJson from "react-json-view-ssr";
import { GetIconInfo, } from "../views/Workflows2.jsx";
import { validateJson, handleReactJsonClipboard, } from "../views/Workflows.jsx";
import { validateJson, handleReactJsonClipboard, GetIconInfo, } from "../views/Workflows2.jsx";
import { red } from "../views/AngularWorkflow.jsx";
import CollectIngestModal from "../components/CollectIngestModal.jsx";
import CorrelationGraph from "../components/CorrelationGraph.jsx";
import { useNavigate, Link, useParams } from "react-router-dom";
import {
Typography,
Tooltip,
@@ -79,6 +80,7 @@ import {
Settings as SettingsIcon,
FilterAlt as FilterAltIcon,
CompareArrows as CompareArrowsIcon,
Hub as HubIcon,
} from "@mui/icons-material";
import { Context } from "../context/ContextApi.jsx";
@@ -146,6 +148,9 @@ const CacheView = memo((props) => {
const [showAutomationMenu, setShowAutomationMenu] = useState(false);
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
const [showCorrelations, setShowCorrelations] = useState(undefined);
let navigate = useNavigate();
useEffect(() => {
if (selectedCategory === "" || selectedCategory === null || selectedCategory === undefined || selectedCategory === "default") {
@@ -165,7 +170,7 @@ const CacheView = memo((props) => {
const defaultAutomation = [
{
"name": "Run workflow",
"description": "Runs a workflow with the updated value.",
"description": "Runs one or more workflows with the updated value as runtime argument",
"options": [{
"key": "workflow_id",
"value": "",
@@ -173,6 +178,7 @@ const CacheView = memo((props) => {
"icon": <AirIcon />,
"enabled": false,
},
/*
{
"name": "Correlate Categories",
"description": "",
@@ -185,20 +191,10 @@ const CacheView = memo((props) => {
"enabled": false,
"disabled": false,
},
{
"name": "Run AI Agent",
"description": "",
"options": [{
"key": "",
"value": "",
}],
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
},
*/
{
"name": "Send webhook",
"description": "Sends the updated value to a specified webhook URL.",
"description": "Sends the updated value to a specified webhook URL as a POST request",
"options": [{
"key": "webhook_url",
"value": "",
@@ -207,7 +203,29 @@ const CacheView = memo((props) => {
"enabled": false,
},
{
"name": "Run AI Agent",
"description": "Runs an AI Agent to process the updated value. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
"options": [{
"key": "",
"value": "",
}],
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
},
{
"name": "Enrich",
"description": "Enriches the data. Only runs on valid JSON data AND if the 'enrichment' field does not exist.",
"type": "singul",
"options": [{
"key": "",
"value": "",
}],
"icon": "/images/logos/singul.svg",
"disabled": false,
},
{
"name": "Send message",
@@ -221,18 +239,6 @@ const CacheView = memo((props) => {
"disabled": true,
"enabled": false,
},
{
"name": "Enrich",
"description": "",
"type": "singul",
"options": [{
"key": "",
"value": "",
}],
"icon": "/images/logos/singul.svg",
"enabled": false,
"disabled": true,
},
]
const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
@@ -322,7 +328,10 @@ const CacheView = memo((props) => {
setSelectedCategory(categoryParam)
}
listOrgCache(orgId, "", 0, pageSize, page)
setTimeout(() => {
listOrgCache(orgId, chosenCategory, 0, pageSize, page)
}, 100)
}, [])
@@ -495,9 +504,6 @@ const CacheView = memo((props) => {
const deleteEntry = (orgId, key, itemCategory, refreshList) => {
console.log(`Deleting entry for orgId: ${orgId}, key: ${key}, category: ${itemCategory}`);
const method = "POST"
const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache`
var parsed = {
@@ -1064,7 +1070,16 @@ const CacheView = memo((props) => {
saveAutomation(newAutomations)
}
console.log("AUTO: ", automation)
return (
<Tooltip title={
<Typography style={{margin: 10, }}>
{automation?.description}
</Typography>
}
placement="right"
style={theme.palette.tooltip}
>
<div key={index} style={{
marginBottom: 10,
backgroundColor: hovered ? theme.palette.hoverColor : "transparent",
@@ -1079,7 +1094,7 @@ const CacheView = memo((props) => {
style={{ marginRight: 10, marginTop: -10, }}
checked={automation.enabled}
// Check if automation options have a value
disabled={automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === "")}
disabled={automation.name !== "Enrich" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
onChange={(e) => {
e.stopPropagation()
e.preventDefault()
@@ -1104,6 +1119,12 @@ const CacheView = memo((props) => {
return
}
if (automation?.name === "Enrich") {
automation.enabled = !automation.enabled
runSave()
return
}
setShowOptions(!showOptions)
// Auto saves when the options are closed/saved
@@ -1465,6 +1486,7 @@ const CacheView = memo((props) => {
})
)}
</div>
</Tooltip>
)
}
@@ -1740,6 +1762,37 @@ const CacheView = memo((props) => {
</IconButton>
</span>
</Tooltip>
<Tooltip
title={"Explore correlations: Requires a category."}
style={{}}
aria-label={"Edit"}
>
<span>
<IconButton
style={{ padding: "6px" }}
disabled={userdata?.support !== true || data?.category === "" || data?.category === null || data?.category === undefined}
onClick={(e) => {
if (userdata?.support !== true) {
toast.warn("Correlation graphs are a beta feature.")
}
toast.info("Opening correlations window")
// add the key we are correlating
const currentParams = new URLSearchParams(window.location.search);
currentParams.set("category", selectedCategory);
currentParams.set("correlations", "true");
currentParams.set("key", data.key);
navigate(`${window.location.pathname}?${currentParams.toString()}`);
setShowCorrelations(data)
}}
>
<HubIcon />
</IconButton>
</span>
</Tooltip>
<Tooltip
title={data?.org_id !== selectedOrganization.id ? "You can not access public URL for this key as it is controlled by parent organization." : "Public URL (types: text, raw, json)" }
style={{ marginLeft: 0, }}
@@ -1886,6 +1939,44 @@ const CacheView = memo((props) => {
}}>
{modalView}
{showCorrelations !== undefined ?
<Dialog
open={showCorrelations !== undefined}
onClose={() => setShowCorrelations(undefined)}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: 1000,
minHeight: 768,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
Correlations
</DialogTitle>
<DialogContent>
<CorrelationGraph
globalUrl={globalUrl}
datastoreData={showCorrelations}
userdata={userdata}
/>
</DialogContent>
</Dialog>
: null}
<CollectIngestModal
globalUrl={globalUrl}
open={showCollectIngestMenu}
@@ -2377,7 +2468,7 @@ const CacheView = memo((props) => {
setSelectedRows(newSelection);
}}
keepNonExistentRowsSelected={false}
getRowId={(row) => row?.category ? `${row.key}_${row.category}` : row.key}
getRowId={(row) => `${row?.key}_${row?.category}`}
autoHeight={true}
sx={{
+532 -89
View File
@@ -6,6 +6,7 @@ import theme from '../theme.jsx';
import Markdown from 'react-markdown'
import { isMobile } from "react-device-detect";
import { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify';
import AppSearch from "../components/AppSearch1.jsx";
@@ -37,14 +38,24 @@ const ChatBot = (props) => {
const [appAuthentication, setAppAuthentication] = React.useState([]);
const [inputAuth, setInputAuth] = useState([])
const [forceReauthentication, setForceReauthentication] = useState(false);
const [selectedType, setSelectedType] = useState("atomic");
const [selectedType, setSelectedType] = useState("support");
const [appname, setAppname] = useState("");
const [threadId, setThreadId] = useState("");
const [runId, setRunId] = useState("");
// New state for thread management
const [isLoadingThread, setIsLoadingThread] = useState(false);
const [threadError, setThreadError] = useState("");
const [isActiveOrg, setIsActiveOrg] = useState(true);
const [threadOrgId, setThreadOrgId] = useState("");
const [chatDisabled, setChatDisabled] = useState(false);
const [showAppSearch, setShowAppSearch] = useState(false);
// Get thread ID from URL params
const { threadId: urlThreadId } = useParams();
let navigate = useNavigate();
const waitingMsg = "Processing..."
const viewWidth = isMobile ? "92%" : 800
@@ -82,8 +93,159 @@ const ChatBot = (props) => {
}
}, [appname])
// Load existing thread if threadId is in URL
useEffect(() => {
if (urlThreadId && urlThreadId !== threadId) {
loadExistingThread(urlThreadId);
}
}, [urlThreadId]);
const loadExistingThread = (threadIdToLoad) => {
setIsLoadingThread(true);
setThreadError("");
fetch(`${globalUrl}/api/v1/conversation/thread`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({ thread_id: threadIdToLoad }),
})
.then((response) => {
if (response.status === 403) {
setThreadError("You are not authorized to view this conversation.");
setIsLoadingThread(false);
return null;
}
if (response.status === 404) {
setThreadError("Conversation not found.");
setIsLoadingThread(false);
return null;
}
return response.json();
})
.then((data) => {
if (!data) {
return;
}
if (!data.success) {
setThreadError(data.message || "Failed to load conversation.");
setIsLoadingThread(false);
return;
}
// Set thread data - this is crucial for continuing the conversation
setThreadId(data.thread_id);
console.log("Loaded existing thread:", data.thread_id);
// Transform API message format to UI format
const transformedMessages = (data.messages || []).map((msg, index) => ({
id: `${data.thread_id}_${index}`,
status: msg.role === "user" ? "sent" : "received",
message: msg.content,
timestamp: msg.timestamp
}));
setMessages(transformedMessages);
setThreadOrgId(data.thread_org_id);
setIsActiveOrg(data.is_active_org);
if (!data.is_active_org) {
setChatDisabled(true);
} else {
setChatDisabled(false);
}
setIsLoadingThread(false);
})
.catch((error) => {
console.error("Error loading thread:", error);
setThreadError("Failed to load conversation. Please try again.");
setIsLoadingThread(false);
});
};
const handleClickChangeOrg = (orgId) => {
toast.info("Changing active organization - please wait!");
const data = {
org_id: orgId,
};
// Clear org-specific cached data
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
localStorage.removeItem("workflows");
localStorage.removeItem("apps");
localStorage.removeItem("dashboard_onboarding_complete");
localStorage.removeItem("dashboard_onboarding_completed");
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify(data),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
} else {
// Clear additional cached data
localStorage.removeItem("apps");
localStorage.removeItem("workflows");
localStorage.removeItem("userinfo");
localStorage.removeItem("lastTabOpenByUser");
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success === true) {
if (
responseJson?.region_url !== undefined &&
responseJson?.region_url !== null &&
responseJson?.region_url.length > 0
) {
localStorage.setItem("globalUrl", responseJson.region_url);
}
if (responseJson["reason"] === "SSO_REDIRECT") {
setTimeout(() => {
toast.info("Redirecting to SSO login page as SSO is required for this organization.");
window.location.href = responseJson["url"];
return;
}, 2000);
} else {
setTimeout(() => {
window.location.reload();
}, 2000);
}
toast.success("Successfully changed active organization - refreshing!");
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason);
} else {
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
}
}
})
.catch((error) => {
console.log("error changing: ", error);
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
});
};
window.title = "Shuffle - New Chat"
let navigate = useNavigate();
// Automatic submit handler based on a lot of stuff :)
const handleSubmit = (e, inputmsg) => {
@@ -102,6 +264,8 @@ const ChatBot = (props) => {
"run_id": runId,
}
console.log("Sending message with thread_id:", threadId);
if (appname !== undefined && appname !== null && appname !== "") {
parsedData["app_name"] = appname
}
@@ -190,6 +354,12 @@ const ChatBot = (props) => {
if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") {
setThreadId(data.thread_id)
// Update URL if this is a new thread (not already in URL)
if (!urlThreadId && data.thread_id !== threadId) {
console.log("Navigating to new thread URL:", data.thread_id);
navigate(`/chat/${data.thread_id}`, { replace: true });
}
}
if (data.success === undefined) {
@@ -423,8 +593,6 @@ const ChatBot = (props) => {
border: `1px solid ${theme.palette.inputColor}`,
}}
color="primary"
fullWidth
color="primary"
onClick={(e) => {
console.log("Click? ")
e.preventDefault();
@@ -520,32 +688,38 @@ const ChatBot = (props) => {
}
function Img(props) {
return <img style={{ borderRadius: theme.palette?.borderRadius, width: 750, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
return <img style={{
borderRadius: 12,
maxWidth: "100%",
height: "auto",
marginTop: 12,
marginBottom: 12,
border: "1px solid rgba(255, 255, 255, 0.1)",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.2)"
}} alt={props.alt} src={props.src} />;
}
function CodeHandler(props) {
//console.log("Codehandler PROPS: ", props)
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
return (
<div
style={{
minWidth: "50%",
maxWidth: "100%",
backgroundColor: theme.palette.inputColor,
overflowY: "auto",
// Check if props.inline === true, then do it inline
padding: props.inline ? 0 : 15,
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={() => {
<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
return;
}
if (message.action === "login") {
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT")
navigate("/login?view=/conversation&message=You must log in to use ShuffleGPT");
} else if (message.action === "app_authentication") {
console.log("App auth action!")
//setAuthenticationModalOpen(true)
console.log("App auth action!");
} else {
console.log("\n\nUnknown click action: ", message.action)
console.log("\n\nUnknown click action: ", message.action);
}
}}>
{message.message === waitingMsg ? <CircularProgress style={{height: 20, width: 20, marginTop: 20, marginRight: 10, }} /> : null}
<span>
{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}
id="markdown_wrapper"
style={{
minHeight: 20,
marginTop: 0,
display: "flex",
flexDirection: "row",
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, }}>
{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
}
</span>
</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}`}
</Typography>
{!isMobile ? (
<Typography variant="caption" style={{
color: "rgba(255, 255, 255, 0.5)",
textAlign: "center",
display: "block",
fontSize: "0.75rem",
lineHeight: 1.4,
maxWidth: "80%",
margin: "0 auto"
}}>
{messages.length === 0
? `The Shuffle AI is a test system for automatic workflow generation and atomic functions for the future of Shuffle. Shuffle AI may use your organization info in the query, and attempts to auto-correct any failed behavior. If you have any questions, please contact us at ${supportEmail}`
: "Shuffle AI can make mistakes. Always double-check important information."
}
</Typography>
) : null}
</div>
</div>
</div>
+9 -3
View File
@@ -49,7 +49,7 @@ const CloudSyncTab = (props) => {
const itemColor = "white";
const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io";
const { themeMode, brandColor } = useContext(Context);
const { themeMode, brandColor, setUpdateOrg } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
useEffect(() => { getSettings(); }, []);
@@ -327,7 +327,6 @@ const CloudSyncTab = (props) => {
);
};
const handleGetOrg = (orgId) => {
if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
@@ -337,11 +336,16 @@ const CloudSyncTab = (props) => {
}
}
if (orgId === undefined || orgId === null || (orgId.length !== 36 && orgId.length !== 0)) {
return
}
if (orgId.length === 0) {
toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.");
return;
}
// Just use this one?
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
@@ -359,7 +363,7 @@ const CloudSyncTab = (props) => {
})
.then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed getting your org. If this persists, please contact support.");
toast.warn("Failed getting your org. If this persists, please contact support (cloud sync)")
} else {
if (
responseJson.sync_features === undefined ||
@@ -457,6 +461,8 @@ const CloudSyncTab = (props) => {
setLoading(false);
if (response.status === 200) {
console.log("Cloud sync success?");
handleGetOrg(userdata.active_org.id);
setUpdateOrg(true);
} else {
console.log("Cloud sync fail?");
}
+1 -1
View File
@@ -36,7 +36,7 @@ import {
Avatar,
AvatarGroup,
} from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CreatorGrid = props => {
+1 -1
View File
@@ -26,7 +26,7 @@ import {
ListItemAvatar,
ListItemText,
} from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
+10 -14
View File
@@ -432,7 +432,7 @@ const EnvironmentTab = memo((props) => {
-e SHUFFLE_SWARM_CONFIG=run \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? `
-e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \ \n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? `
-v /tmp:/tmp \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest
`)
@@ -467,8 +467,7 @@ const EnvironmentTab = memo((props) => {
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${props.userdata.active_org.id}" \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
-e HTTPS_PROXY=IP:PORT \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest`
return commandData
@@ -959,7 +958,7 @@ const EnvironmentTab = memo((props) => {
display: "grid",
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
width: "100%",
minWidth: 800,
minWidth: showLoader ? 800 : 0,
paddingBottom: 0,
borderBottom: theme.palette.defaultBorder,
}}
@@ -976,7 +975,7 @@ const EnvironmentTab = memo((props) => {
textOverflow: "ellipsis",
overflow: "hidden",
fontWeight: "bold",
textAlign: header === "Actions" ? "left" : header === "Distribution" ? "right" : "center",
textAlign: "center",
}}
/>
)
@@ -987,7 +986,6 @@ const EnvironmentTab = memo((props) => {
<ListItem
key={rowIndex}
style={{
display: "grid",
backgroundColor: theme.palette.platformColor,
height: 40,
width: "100%",
@@ -1674,10 +1672,9 @@ const EnvironmentTab = memo((props) => {
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
<Typography variant='body2' color="textSecondary">Disable Detection Controller</Typography>
</div>
{/*
<div style={{display: 'flex', alignItems: 'center', }}>
<Checkbox
id="shuffle_skip_pipelines"
@@ -1692,9 +1689,8 @@ const EnvironmentTab = memo((props) => {
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography>
<Typography variant='body2' color="textSecondary">Use Remote Pipeline</Typography>
</div>
*/}
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
@@ -1706,16 +1702,16 @@ const EnvironmentTab = memo((props) => {
</Typography>
</div>
</div>
{currentEnvQueue.length === 0 ? null :
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
{currentEnvQueue.map((queueItem, queueIndex) => {
return (
<ListItem
key={queueIndex}
style={{
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.platformColor,
border: '1px solid #333333',
borderBottom: theme.palette.defaultBorder,
maxHeight: 50,
}}
>
<ListItemText style={{minWidth: 50, maxWidth: 50, }}>
@@ -1767,7 +1763,7 @@ const EnvironmentTab = memo((props) => {
padding: 15,
textAlign: "center",
height: 70,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: theme.palette.platformColor,
display: "flex",
}}
>
+1 -1
View File
@@ -4,7 +4,7 @@ import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as Pa
import theme from '../theme.jsx';
import ReactJson from "react-json-view-ssr";
import { toast } from 'react-toastify';
import { validateJson } from "../views/Workflows.jsx";
import { validateJson } from "../views/Workflows2.jsx";
// import HandleJsonCopy from "./ShuffleCodeEditor1";
const STATUS_CONFIG = {
+8 -4
View File
@@ -64,7 +64,7 @@ const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
const navigate = useNavigate();
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, setSearchBarModalOpen, searchBarModalOpen, logoutUrl, themeMode, handleThemeChange, isDocSearchModalOpen, setIsDocSearchModalOpen, supportEmail} = useContext(Context);
const {setLeftSideBarOpenByClick, leftSideBarOpenByClick, updateOrg, setUpdateOrg, setSearchBarModalOpen, searchBarModalOpen, logoutUrl, themeMode, handleThemeChange, isDocSearchModalOpen, setIsDocSearchModalOpen, supportEmail} = useContext(Context);
const [expandLeftNav, setExpandLeftNav] = useState(false);
const [activeOrgName, setActiveOrgName] = useState(
userdata?.active_org?.name || "Select Organziation"
@@ -684,7 +684,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: <a href="https://github.com/Shuffle/Shuffle/releases" style={{ color: theme.palette.text.primary, textDecoration: "underline" }} target="_blank" rel="noreferrer">
2.1.1
2.1.2
</a>
</Typography>
</Menu>
@@ -956,6 +956,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
.then((org) => {
if (!fetched && org) {
setActiveOrgData(org);
setUpdateOrg(false);
if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
@@ -972,7 +973,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
return () => {
fetched = true;
};
}, [userdata?.active_org?.id, globalUrl]);
}, [userdata?.active_org?.id, globalUrl, updateOrg]);
return (
<div
@@ -1226,8 +1227,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
<Button
component={Link}
to={"/new-dashboard"}
to={userdata?.support === true ? "/new-dashboard" : "/workflows"}
onClick={(event) => {
if(!userdata?.support){
return;
}
setOpenautomateTab(true);
setOpenSecurityTab(false);
setCurrentOpenTab("new-dashboard");
@@ -35,6 +35,7 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import InfoIcon from "@mui/icons-material/Info";
import { Link, useNavigate } from "react-router-dom";
// Helper function to get the correct image path based on app category
@@ -1034,7 +1035,12 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
{/* Add Usecase Dialog */}
<Dialog
open={openDialog}
onClose={handleCloseDialog}
onClose={(event, reason) => {
if (reason !== "backdropClick") {
handleCloseDialog();
}
}}
disableEscapeKeyDown
maxWidth="md"
fullWidth
PaperProps={{
@@ -1642,7 +1648,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
<Box key={contentIndex} sx={{ mb: 1 }}>
<TextField
multiline
rows={5}
rows={6}
value={content}
onChange={(e) => {
const newItems = [...formData.navigation.items];
@@ -1653,7 +1659,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
navigation: { items: newItems },
});
}}
placeholder="Content"
placeholder="Content (Markdown supported)"
helperText={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
<Typography variant="caption" sx={{ opacity: 0.7 }}>
Markdown syntax is supported
</Typography>
</Box>
}
fullWidth
sx={{
"& .MuiOutlinedInput-root": {
@@ -1661,6 +1675,10 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
color: theme.palette.text.primary,
"& fieldset": { border: theme.palette.defaultBorder },
},
"& .MuiOutlinedInput-root textarea": {
overflow: "auto",
resize: "vertical",
},
}}
/>
{item.content.length > 1 && (
+20 -13
View File
@@ -104,13 +104,12 @@ const RunDetectionTest = (props) => {
return
}
if (haveDetectionPipelines() === false) {
if (haveDetectionPipelines().length === 0) {
setDetectionTestRunning(false)
toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.")
return
}
// 1. Run a new pipeline which exits.
const detectionTest = `from {message: "<165>1 2025-10-06T12:34:56.789Z myhost.example.com myapp 1234 ID47 [huh eventSource=\\\"App\\\" EventID=\\\"4688\\\" NewProcessName=\\\"notepad.exe\\\" Context=\\\"Testing\\\"] This is a test log message"} | this = message.parse_syslog() | import`
@@ -124,7 +123,17 @@ const RunDetectionTest = (props) => {
// 1. Submit it to run
// 2. Check executions if they happened recently~
if (submitPipelineWrapper !== undefined) {
submitPipelineWrapper(detectionTest)
//`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
const validEnv = haveDetectionPipelines()
for (var envKey in validEnv) {
const envName = validEnv[envKey]
submitPipelineWrapper(detectionTest, envName)
}
//if (validEnv.length > 0) {
//toast.success(`Submitted detection test pipeline to environment(s): ${validEnv.join(", ")}`)
//}
}
for (var i = 0; i < 10; i++) {
@@ -141,10 +150,10 @@ const RunDetectionTest = (props) => {
const haveDetectionPipelines = () => {
if (pipelines === undefined) {
toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.")
return false
return []
}
var foundCorrect = 0
var foundCorrect = []
for (var pipelineKey in pipelines) {
const curPipeline = pipelines[pipelineKey]
//if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) {
@@ -152,27 +161,25 @@ const RunDetectionTest = (props) => {
//}
if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) {
foundCorrect += 1
foundCorrect.push(curPipeline.environment)
}
}
if (foundCorrect >= 1) {
return true
}
return false
return foundCorrect
}
return (
<div style={{display: "flex", }}>
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
<Tooltip title={"Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline"} style={{}} aria-label={"Run detection test"}>
<Tooltip title={
`Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline\n\nEnvironments: ${haveDetectionPipelines().join(", ")}`
} style={{}} aria-label={"Run detection test"}>
<div>
<Button
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
variant="outlined"
color="secondary"
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
disabled={haveDetectionPipelines().length === 0 || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
onClick={() => {
//setPipelineModalOpen(true)
runDetectionTest()
+278 -23
View File
@@ -13,8 +13,51 @@ function formatCompactNumber(value) {
return `${n}`;
}
const RunsOverTimeWidget = (props) => {
const { globalUrl, onLoadingChange, monthOverride, dummyMode } = props;
// Helpers to keep date and "today" logic concise and consistent
function toYMD(date) {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${dd}`;
}
function entriesContainDateYMD(entries, date) {
const target = toYMD(date);
return Array.isArray(entries) && entries.some((e) => toYMD(e.date) === target);
}
function computeTodayValueForAll(key, data) {
if (key === 'app_executions') {
return Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
}
if (key === 'workflow_executions') {
const parent = Number(data?.daily_workflow_executions ?? 0);
const parentFinished = Number(data?.daily_workflow_executions_finished ?? 0);
const parentFailed = Number(data?.daily_workflow_executions_failed ?? 0);
const childFinished = Number(data?.daily_child_workflow_executions_finished ?? 0);
const childFailed = Number(data?.daily_child_workflow_executions_failed ?? 0);
const parentVal = parent > 0 ? parent : (parentFinished + parentFailed);
return parentVal + childFinished + childFailed;
}
return 0;
}
function computeTodayValueForOrg(key, orgStats) {
if (key === 'workflow_executions') {
const total = orgStats?.daily_workflow_executions;
const finished = Number(orgStats?.daily_workflow_executions_finished ?? 0);
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
return total !== undefined ? Number(total || 0) : (finished + failed);
}
if (key === 'app_executions') {
return Number(orgStats?.daily_app_executions ?? 0);
}
return 0;
}
const RunsOverTimeWidget = (props) => {
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
const [series, setSeries] = useState([]);
const [days, setDays] = useState(365); // aggregate to last 12 months by default
@@ -24,6 +67,99 @@ const RunsOverTimeWidget = (props) => {
// Helper: fetch time series for a specific statistics key
const fetchSeriesForKey = async (key) => {
try {
// If specific org is selected and pre-fetched stats are available, use them directly
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
const dailyStats = orgStats.daily_statistics;
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (!day?.date) continue;
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue;
let value = 0;
if (key === 'workflow_executions') {
const finished = Number(day?.workflow_executions_finished || 0);
const failed = Number(day?.workflow_executions_failed || 0);
value = finished + failed;
} else {
value = Number(day?.[key] || 0);
}
processedEntries.push({ date: day.date, value });
}
// Append today's values if not present
try {
if (!entriesContainDateYMD(processedEntries, new Date())) {
const todayVal = computeTodayValueForOrg(key, orgStats);
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
// If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions
if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) {
// Use the full stats endpoint to get daily_statistics
const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`;
const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
if (r.ok) {
const data = await r.json();
const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
// Process each day's data, combining parent + child org runs
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (day?.date) {
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue; // Filter by days
let value = day?.[key] || 0;
// Add child org executions based on the key
if (key === 'app_executions' && day?.child_app_executions !== undefined) {
value += (day.child_app_executions || 0);
} else if (key === 'workflow_executions') {
// For workflow_executions, sum both child_workflow_executions_finished and child_workflow_executions_failed
const childFinished = day?.child_workflow_executions_finished || 0;
const childFailed = day?.child_workflow_executions_failed || 0;
value += (childFinished + childFailed);
}
processedEntries.push({ date: day.date, value });
}
}
// Append today's datapoint for ALL by combining parent + child daily_* counters
try {
if (!entriesContainDateYMD(processedEntries, new Date())) {
const todayVal = computeTodayValueForAll(key, data);
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
}
// // If a specific org is selected
// if (selectedOrgForStats && selectedOrgForStats !== 'ALL') {
// const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
// const r = await fetch(url, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
// if (r.ok) {
// const j = await r.json();
// return Array.isArray(j?.entries) ? j.entries : [];
// }
// }
// Fallback to global stats
const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
const doFetch = async (u) => {
const r = await fetch(u, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
@@ -34,23 +170,15 @@ const RunsOverTimeWidget = (props) => {
const a = await doFetch(urlA);
if (a.length > 0) return a;
// Optional org route fallback if present globally
const orgId = (window && window.selectedOrganization && window.selectedOrganization.id) || null;
if (orgId) {
const urlB = `${globalUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
const b = await doFetch(urlB);
if (b.length > 0) return b;
}
// Final fallback: old aggregate endpoint returning daily_statistics
const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
if (fallback.ok) {
const data = await fallback.json();
const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
const valField = key;
return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
}
return [];
// // Final fallback: old aggregate endpoint returning daily_statistics
// const fallback = await fetch(`${globalUrl}/api/v1/stats`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json' } });
// if (fallback.ok) {
// const data = await fallback.json();
// const daily = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
// const valField = key;
// return daily.map((d) => ({ date: d?.date, value: Number(d?.[valField] || 0) }));
// }
// return [];
} catch (e) {
return [];
}
@@ -155,8 +283,12 @@ const RunsOverTimeWidget = (props) => {
}, [monthOverride]);
useEffect(() => {
if (loadingSelectedOrgStats === true) {
setSeries([]);
return;
}
load(mode);
}, [mode, globalUrl, days, selectedMonth, dummyMode]);
}, [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',
}}>
@@ -209,6 +341,7 @@ const RunsOverTimeWidget = (props) => {
}
/>;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
@@ -295,7 +428,12 @@ const RunsOverTimeWidget = (props) => {
</div>
<div style={{ marginTop: 24 }}>
<div style={{ width: '100%' }}>
<div style={{ width: '100%', position: 'relative' }}>
{loadingSelectedOrgStats === true ? (
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Typography variant="body2" color="textSecondary">Loading</Typography>
</div>
) : (
<BarChart
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
height={300}
@@ -315,6 +453,123 @@ const RunsOverTimeWidget = (props) => {
}
animated={false}
/>
)}
{/* Custom reference line overlays for app execution limits (suborg and parent) */}
{mode === 'apps' && barData.length > 0 && (() => {
// Determine limits
const parentAppExecutionLimit = selectedOrganization?.sync_features?.app_executions?.limit ? Number(selectedOrganization.sync_features.app_executions.limit) : null;
const suborgAppExecutionLimit = orgForLimit?.sync_features?.app_executions?.limit ? Number(orgForLimit.sync_features.app_executions.limit) : null;
const maxValue = Math.max(...barData.map(d => Number(d.data) || 0), 0);
if (maxValue <= 0) return null;
const chartTopPadding = 1;
const chartBottomPadding = 8;
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
// Helpers to compute Y position in percentage
const posFor = (limit) => {
const perc = Math.max(0, Math.min(1, (Number(limit) || 0) / maxValue));
return chartTopPadding + ((1 - perc) * usableHeight);
};
let showParentLimit = false;
let showSuborgLimit = false;
// Suborg/current limit: only when viewing a specific org (not ALL)
if (selectedOrgForStats !== 'ALL' && typeof suborgAppExecutionLimit === 'number' && suborgAppExecutionLimit > 0 && suborgAppExecutionLimit <= maxValue && selectedOrgForStats === orgForLimit?.id) {
showSuborgLimit = true;
}
if (typeof parentAppExecutionLimit === 'number' && parentAppExecutionLimit > 0 && parentAppExecutionLimit <= maxValue) {
if (selectedOrgForStats === 'ALL') {
showParentLimit = true;
} else if (parentAppExecutionLimit !== suborgAppExecutionLimit) {
showParentLimit = true;
}
}
if (!showParentLimit && !showSuborgLimit) return null;
const suborgTop = showSuborgLimit ? posFor(suborgAppExecutionLimit) : null;
const parentTop = showParentLimit ? posFor(parentAppExecutionLimit) : null;
return (
<>
{showSuborgLimit && (
<>
<div
style={{
position: 'absolute',
top: `${suborgTop}%`,
left: '2%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
pointerEvents: 'none',
zIndex: 10,
}}
/>
<div
style={{
position: 'absolute',
top: `${suborgTop}%`,
right: '-5%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 10,
backgroundColor: 'rgba(26, 26, 26, 0.95)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #ff8544',
}}
>
<Typography style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}>
{formatCompactNumber(suborgAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
{showParentLimit && (
<>
<div
style={{
position: 'absolute',
top: `${parentTop}%`,
left: '2%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
pointerEvents: 'none',
zIndex: 9,
opacity: 0.8,
}}
/>
<div
style={{
position: 'absolute',
top: `${parentTop}%`,
right: '-5%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 9,
backgroundColor: 'rgba(26, 26, 26, 0.93)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #FD4C62',
display: 'flex', alignItems: 'center', gap: 6,
}}
>
<Typography style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}>
{formatCompactNumber(parentAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
</>
);
})()}
</div>
</div>
</div>
+3 -2
View File
@@ -768,6 +768,7 @@ const RuntimeDebugger = (props) => {
toast("Error executing workflow: "+responseJson.error)
} else {
console.log("Executed workflow: ", responseJson)
submitSearch(workflowId, status, startTime, endTime, rowCursor, maxExecutionCount, suborgWorkflowRuns)
}
})
.catch((error) => {
@@ -1236,8 +1237,8 @@ const RuntimeDebugger = (props) => {
setRowsPerPage(newPaginationModel.pageSize)
// No API call needed - this is just for client-side pagination
}}
onRowSelectionModelChange={(newSelection) => {
selectionModel={selectedWorkflowExecutions.map((workflow) => workflow.id)}
onSelectionModelChange={(newSelection) => {
//console.log("newSelection: ", newSelection)
//setSelectedWorkflowExecutionsIndexes(newSelection)
var found = []
+92 -4
View File
@@ -17,6 +17,8 @@ import {
TextField,
Chip,
CircularProgress,
Select,
MenuItem,
} from '@mui/material';
import {
@@ -39,11 +41,14 @@ const SchedulesTab = memo((props) => {
const [showLoader, setShowLoader] = React.useState(true);
const [workflows, setWorkflows] = React.useState([]);
const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
const [newPipelineValue, setNewPipelineValue] = React.useState(`export | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
const [newPipelineValue, setNewPipelineValue] = React.useState(`export live=true | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
const [ticketWebhook, setTicketWebhook] = React.useState("");
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const [environments, setEnvironments] = React.useState([]);
const [selectedEnvironment, setSelectedEnvironment] = React.useState("");
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
@@ -74,7 +79,7 @@ const SchedulesTab = memo((props) => {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export live=true | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
}
}
@@ -91,6 +96,7 @@ const SchedulesTab = memo((props) => {
handleGetWorkflows()
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
handleGetAllTriggers()
handleGetEnvironments()
}
}, [])
@@ -168,8 +174,8 @@ const SchedulesTab = memo((props) => {
})
}
const submitPipelineWrapper = (pipelineValue) => {
submitPipeline(pipelineValue)
const submitPipelineWrapper = (pipelineValue, environment) => {
submitPipeline(pipelineValue, environment)
}
const NewPipelineView = (
@@ -277,9 +283,48 @@ const SchedulesTab = memo((props) => {
setNewPipelineValue(event.target.value)
}
/>
</div>
</DialogContent>
<DialogActions style={{padding: "0px 50px 50px 50px", }}>
{environments.length === 0 ? null :
<div>
<Typography variant="body1" color="textSecondary">Runtime Location</Typography>
<Select
label="Runtime Location"
value={selectedEnvironment}
onChange={(event) => {
const selectedEnv = event.target.value
setSelectedEnvironment(selectedEnv)
}}
style={{marginRight: 300, }}
>
{environments.map((environment, index) => {
if (environment.archived) {
return null
}
if (environment.Name === "Cloud") {
return null
}
return (
<MenuItem
key={index}
style={{
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
}}
value={environment}
>
{environment.Name}
</MenuItem>
)
})}
</Select>
</div>
}
<Button
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
onClick={() => {
@@ -314,6 +359,10 @@ const SchedulesTab = memo((props) => {
start_node: "",
}
if (selectedEnvironment !== undefined && selectedEnvironment !== "") {
pipelineConfig.environment = selectedEnvironment.Name
}
if (environment !== undefined && environment !== "") {
pipelineConfig.environment = environment
}
@@ -441,7 +490,46 @@ const SchedulesTab = memo((props) => {
});
};
const handleGetEnvironments = () => {
fetch(`${globalUrl}/api/v1/environments`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all triggers");
}
return response.json();
})
.then((responseJson) => {
setEnvironments(responseJson || []);
if (responseJson?.length > 0) {
var selectedEnv = ""
for (var i = 0; i < responseJson?.length; i++) {
const env = responseJson[i]
if (env.archived) {
continue
}
selectedEnv = env.Name
if (env?.data_lake?.enabled === true) {
break
}
}
setSelectedEnvironment(selectedEnv.Name)
}
})
.catch((error) => {
// toast(error.toString());
});
}
const handleGetAllTriggers = () => {
fetch(globalUrl + "/api/v1/triggers", {
@@ -52,7 +52,7 @@ import {
} from '@mui/icons-material';
import { validateJson } from "../views/Workflows.jsx";
import { validateJson } from "../views/Workflows2.jsx";
import ReactJson from "react-json-view-ssr";
import PaperComponent from "../components/PaperComponent.jsx";
@@ -969,6 +969,10 @@ const CodeEditor = (props) => {
// Whelp this is inefficient af. Single loop pls
// When the found array is empty.
if (found !== null && found !== undefined) {
// Resolve the child paths/variables first,
// So that "$a.b.c" is handled before "$a.b" and then "$a"
// Replace "$parent.child" before "$parent" so "$parent.child" doesn't become "parentValue.child" (which can't be matched later).
found.sort((a, b) => b.length - a.length)
//console.log("FOUND: ", found)
try {
@@ -24,7 +24,7 @@ import {
LinearYAxisTickSeries,
LinearYAxisTickLabel,
} from "reaviz";
import theme from "../theme";
import theme from "../theme.jsx";
// KPI configuration constants
const RUN_MINUTES_SAVED_PER_WORKFLOW = 15; // minutes saved per workflow run
@@ -48,7 +48,11 @@ const statusOptions = [
// This is used to format the date in the format of YYYY-MM-DD
function formatDay(dateInput) {
try {
return new Date(dateInput).toISOString().slice(0, 10);
const dt = new Date(dateInput);
const y = dt.getFullYear();
const m = String(dt.getMonth() + 1).padStart(2, "0");
const d = String(dt.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`; // local date to avoid UTC shift dropping "today"
} catch {
return String(dateInput);
}
@@ -221,8 +225,8 @@ function buildGroupedSeries(selectedStatus, okArr, failArr) {
return { grouped, scheme };
}
const SuccessFailedRunsWidget = (props) => {
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode } = props;
const SuccessFailedRunsWidget = (props) => {
const { globalUrl, workflows, onControlsChange, onLoadingChange, onTotalsChange, overrideDays, dummyMode, loadingSelectedOrgStats, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit } = props;
const [mode, setMode] = useState("workflows"); // 'workflows' | 'apps'
const [days, setDays] = useState(30);
@@ -236,6 +240,25 @@ const SuccessFailedRunsWidget = (props) => {
const [loading, setLoading] = useState(false);
const [wfTotals, setWfTotals] = useState({ ok: 0, fail: 0, activeDays: 0 });
const appExecutionLimit = useMemo(() => {
if (mode !== 'apps') return null;
if(orgForLimit?.id !== selectedOrgForStats && selectedOrgForStats !== "ALL") {
return null;
}
try {
let org = orgForLimit
if(selectedOrgForStats === "ALL") {
org = selectedOrganization;
}
if (!org?.sync_features?.app_executions?.limit) return null;
return Number(org.sync_features.app_executions.limit) || null;
} catch {
return null;
}
}, [mode, selectedOrganization, orgForLimit, selectedOrgForStats]);
useEffect(() => {
try {
if (typeof onTotalsChange !== "function") return;
@@ -264,7 +287,132 @@ const SuccessFailedRunsWidget = (props) => {
const fetchSeriesForKey = async (key) => {
try {
// If specific org is selected and pre-fetched stats are available, use them directly
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
const dailyStats = orgStats.daily_statistics;
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (!day?.date) continue;
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue;
let value = 0;
if (key === 'workflow_executions') {
const finished = Number(day?.workflow_executions_finished || 0);
const failed = Number(day?.workflow_executions_failed || 0);
value = finished + failed;
} else {
value = Number(day?.[key] || 0);
}
processedEntries.push({ date: day.date, value });
}
// Append today's numbers if missing from daily_statistics, using top-level daily_* fields
try {
const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })();
const hasToday = processedEntries.some(e => {
const dt = new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}` === todayStr;
});
if (!hasToday) {
let todayVal = 0;
if (key === 'workflow_executions') {
const finished = Number(orgStats?.daily_workflow_executions_finished ?? orgStats?.daily_workflow_executions ?? 0);
const failed = Number(orgStats?.daily_workflow_executions_failed ?? 0);
// If daily_workflow_executions exists, prefer that; otherwise sum finished + failed
todayVal = orgStats?.daily_workflow_executions !== undefined ? Number(orgStats.daily_workflow_executions || 0) : (finished + failed);
} else if (key === 'workflow_executions_finished') {
todayVal = Number(orgStats?.daily_workflow_executions_finished ?? 0);
} else if (key === 'workflow_executions_failed') {
todayVal = Number(orgStats?.daily_workflow_executions_failed ?? 0);
} else if (key === 'app_executions') {
todayVal = Number(orgStats?.daily_app_executions ?? 0);
} else if (key === 'app_executions_failed') {
todayVal = Number(orgStats?.daily_app_executions_failed ?? 0);
}
// Only add when we have a numeric value (including 0)
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
// If selectedOrgForStats is 'ALL', use parent org's full stats endpoint and combine app_executions + child_app_executions
if (selectedOrgForStats === 'ALL' && selectedOrganization?.id) {
// Use the full stats endpoint to get daily_statistics
const url = `${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrganization.id)}/stats`;
const r = await fetch(url, { method: "GET", credentials: "include" });
if (r.ok) {
const data = await r.json();
const dailyStats = Array.isArray(data?.daily_statistics) ? data.daily_statistics : [];
// Process each day's data, combining parent + child org runs
const processedEntries = [];
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - days);
for (const day of dailyStats) {
if (day?.date) {
const dayDate = new Date(day.date);
if (dayDate < cutoff) continue; // Filter by days
let value = day?.[key] || 0;
// Add child org executions based on the key
if (key === 'app_executions' && day?.child_app_executions !== undefined) {
value += (day.child_app_executions || 0);
} else if (key === 'app_executions_failed' && day?.child_app_executions_failed !== undefined) {
value += (day.child_app_executions_failed || 0);
} else if (key === 'workflow_executions_finished' && day?.child_workflow_executions_finished !== undefined) {
value += (day.child_workflow_executions_finished || 0);
} else if (key === 'workflow_executions_failed' && day?.child_workflow_executions_failed !== undefined) {
value += (day.child_workflow_executions_failed || 0);
} else if (key === 'workflow_executions') {
// workflow_executions is total (finished + failed), so add both child fields
const childFinished = day?.child_workflow_executions_finished || 0;
const childFailed = day?.child_workflow_executions_failed || 0;
value += (childFinished + childFailed);
}
processedEntries.push({ date: day.date, value });
}
}
// Append today's datapoint for ALL by combining parent + child daily_* counters
try {
const todayStr = (() => { const d = new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; })();
const hasToday = processedEntries.some(e => { const dt=new Date(e.date); const y=dt.getFullYear(); const m=String(dt.getMonth()+1).padStart(2,'0'); const dd=String(dt.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`===todayStr; });
if (!hasToday) {
let todayVal = 0;
if (key === 'app_executions') {
todayVal = Number(data?.daily_app_executions ?? 0) + Number(data?.daily_child_app_executions ?? 0);
} else if (key === 'app_executions_failed') {
todayVal = Number(data?.daily_app_executions_failed ?? 0) + Number(data?.daily_child_app_executions_failed ?? 0);
} else if (key === 'workflow_executions_finished') {
todayVal = Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_child_workflow_executions_finished ?? 0);
} else if (key === 'workflow_executions_failed') {
todayVal = Number(data?.daily_workflow_executions_failed ?? 0) + Number(data?.daily_child_workflow_executions_failed ?? 0);
} else if (key === 'workflow_executions') {
const p = Number(data?.daily_workflow_executions ?? 0);
const cf = Number(data?.daily_child_workflow_executions_finished ?? 0);
const cfa = Number(data?.daily_child_workflow_executions_failed ?? 0);
todayVal = p > 0 ? p : (Number(data?.daily_workflow_executions_finished ?? 0) + Number(data?.daily_workflow_executions_failed ?? 0));
todayVal += (cf + cfa);
}
if (!Number.isNaN(todayVal)) {
processedEntries.push({ date: new Date().toISOString(), value: todayVal });
}
}
} catch {}
return processedEntries;
}
}
// Fallback to global stats
const urlA = `${globalUrl}/api/v1/stats/${encodeURIComponent(key)}?days=${encodeURIComponent(days)}`;
const doFetch = async (u) => {
const r = await fetch(u, { method: "GET", credentials: "include" });
@@ -273,16 +421,7 @@ const SuccessFailedRunsWidget = (props) => {
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 [];
return a;
} catch (e) {
return [];
}
@@ -291,6 +430,7 @@ const SuccessFailedRunsWidget = (props) => {
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;
fetchSeries();
if (loadingSelectedOrgStats === true) {
setSeriesOk([]);
setSeriesFail([]);
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,7 +801,28 @@ 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 (
<div style={{ position: 'relative', width: '100%', height: 300 }}>
<AreaChart
height={300}
width={"100%"}
@@ -822,6 +954,119 @@ const SuccessFailedRunsWidget = (props) => {
/>
}
/>
{/* Custom reference line overlay - only show if limit is within visible range */}
{appExecutionLimit && mode === 'apps' && (() => {
const maxValue = paddedMax || 1;
const limitPercentage = appExecutionLimit / maxValue;
const chartTopPadding = 1;
const chartBottomPadding = 8;
const usableHeight = 100 - chartTopPadding - chartBottomPadding;
const topPercentage = chartTopPadding + ((1 - limitPercentage) * usableHeight);
let parentLimitPos = null;
if (
typeof parentAppExecutionLimit === 'number' &&
parentAppExecutionLimit > 0 &&
parentAppExecutionLimit !== appExecutionLimit &&
parentAppExecutionLimit <= paddedMax
) {
showParentLimit = true;
const parentLimitPerc = parentAppExecutionLimit / paddedMax;
parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight);
}
if (selectedOrgForStats === "ALL") {
showParentLimit = true;
const parentLimitPerc = parentAppExecutionLimit / paddedMax;
parentLimitPos = chartTopPadding + ((1 - parentLimitPerc) * usableHeight);
}
return (
<>
{/* Suborg or Current Limit Reference Line & Label (orange now) */}
{
selectedOrgForStats !== "ALL" && showSuborgLimit && (
<>
<div
style={{
position: 'absolute',
top: `${topPercentage}%`,
left: '3.9%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #ff8544 0px, #ff8544 8px, transparent 8px, transparent 16px)',
pointerEvents: 'none',
zIndex: 10,
}}
/>
<div
style={{
position: 'absolute',
top: `${topPercentage}%`,
right: '-8%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 10,
backgroundColor: 'rgba(26, 26, 26, 0.95)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #ff8544',
}}
>
<Typography
style={{ fontSize: '12px', fontWeight: 600, color: '#ff8544' }}
>
{formatCompactNumber(appExecutionLimit)} limit
</Typography>
</div>
</>
)
}
{/* Parent Org Limit Reference Line & Label (red now) */}
{showParentLimit && (
<>
<div
style={{
position: 'absolute',
top: `${parentLimitPos}%`,
left: '3.9%',
right: '0%',
height: '2px',
backgroundImage: 'repeating-linear-gradient(to right, #FD4C62 0px, #FD4C62 12px, transparent 12px, transparent 22px)',
pointerEvents: 'none',
zIndex: 9,
opacity: 0.8,
}}
/>
<div
style={{
position: 'absolute',
top: `${parentLimitPos}%`,
right: '-8%',
transform: 'translate(0, -50%)',
pointerEvents: 'none',
zIndex: 9,
backgroundColor: 'rgba(26, 26, 26, 0.93)',
padding: '2px 8px',
borderRadius: '4px',
border: '1px solid #FD4C62',
display: 'flex', alignItems: 'center', gap: 6
}}
>
<Typography
style={{ fontSize: '12px', fontWeight: 600, color: '#FD4C62' }}
>
{formatCompactNumber(parentAppExecutionLimit)} limit
</Typography>
</div>
</>
)}
</>
);
})()}
</div>
);
})()}
<div
@@ -844,8 +1089,19 @@ const SuccessFailedRunsWidget = (props) => {
<LegendDot color="#22c55e" /> Successful Runs
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<LegendDot color="#ef4444" /> Failed Runs
<LegendDot color="#FD4C62" /> Failed Runs
</div>
{/* Chart limit legends */}
{showParentLimit && (
<div style={{ display: "flex", alignItems: "center", gap: 4, marginLeft: 18 }}>
<LegendLineDash color="#FD4C62" height="10" /> <span>Parent Org Limit</span>
</div>
)}
{showSuborgLimit && (
<div style={{ display: "flex", alignItems: "center", gap: 4 }}>
<LegendLineDash color="#ff8544" height="10" /> <span>Current Org Limit</span>
</div>
)}
</div>
<div
style={{
@@ -999,3 +1255,18 @@ function LegendDot({ color }) {
/>
);
}
function LegendLineDash({ color, height = 10 }) {
// Render a small thinned dashed SVG line
return (
<svg width="24" height={height} style={{ margin: '0 2px' }}>
<line
x1="2" x2="32" y1={height / 2} y2={height / 2}
stroke={color}
strokeWidth="2"
strokeDasharray="8, 5"
strokeLinecap="round"
/>
</svg>
);
}
+1 -1
View File
@@ -20,7 +20,7 @@ import {
Zoom,
Chip,
} from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
@@ -24,7 +24,7 @@ import {
} from "../views/AngularWorkflow.jsx"
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
import theme from "../theme.jsx";
const itemHeight = 24
+31
View File
@@ -552,6 +552,37 @@ export default function defaultCytoscapeStyle(theme) {
"font-size": "0px",
},
},
{
selector: `edge[?correlation]`,
css: {
"curve-style": "straight",
"width": 2,
"line-color": "#ccc",
"target-arrow-color": "#ccc",
"source-arrow-shape": "none",
"target-arrow-shape": "none",
},
},
{
selector: `node[type="CORRELATION"]`,
css: {
label: function (element) {
return element.data("label")
},
shape: "round",
color: "data(color)",
width: "data(width)",
height: "data(height)",
padding: "0px",
margin: "0px",
"background-color": "data(iconBackground)",
"background-fill": "data(fillstyle)",
"background-gradient-direction": "to-right",
"background-gradient-stop-colors": "data(fillGradient)",
"border-width": "0px",
"font-size": "0px",
},
},
{
selector: `node[?source_workflow]`,
css: {
+11 -1
View File
@@ -1765,7 +1765,17 @@ If you're interested, please let me know a time that works for you, or set up a
setTimeout(() => {
window.location.reload()
}, 2000);
toast("Successfully changed active organization - refreshing!");
toast.success("Successfully changed active organization - refreshing!");
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
navigate(`/admin?org_id=${responseJson.org_id}`)
} else {
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
navigate(`/admin`)
} else {
toast("No pivot?")
}
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
if (!responseJson.reason.includes("already")) {
+30 -6
View File
@@ -2,19 +2,23 @@ import React, { useContext, useEffect, useState } from 'react';
import AdminNavBar from '../components/AdminNavBar.jsx';
import { toast } from "react-toastify";
import { Context } from '../context/ContextApi.jsx';
import { useNavigate, Link } from "react-router-dom";
const Admin2 = (props) => {
// Destructure props if needed
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, isLoggedIn} = props;
const { userdata, globalUrl, serverside, checkLogin, notifications, setNotifications, stripeKey, isLoaded, } = props;
const [selectedTab, setSelectedTab] = useState('editdetails');
const [selectedStatus, setSelectedStatus] = React.useState([]);
const [selectedOrganization, setSelectedOrganization] = useState({});
const [organizationFeatures, setOrganizationFeatures] = useState({});
const [orgRequest, setOrgRequest] = React.useState(true);
const [isOrgLoaded, setIsOrgLoaded] = React.useState(false);
const {brandName} = useContext(Context)
const {brandName, updateOrg, setUpdateOrg} = useContext(Context)
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
let navigate = useNavigate();
if (document !== undefined) {
if (selectedOrganization?.name !== undefined) {
document.title = brandName?.length > 0 ? selectedOrganization?.name + ` - Admin - ${brandName}` : selectedOrganization?.name + ` - Admin - Shuffle`;
@@ -24,6 +28,9 @@ const Admin2 = (props) => {
}
const handleGetOrg = (orgId) => {
if (orgId === undefined || orgId === null || orgId.length !== 36) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgId}`, {
method: "GET",
@@ -40,13 +47,13 @@ const Admin2 = (props) => {
})
.then((responseJson) => {
if (responseJson["success"] === false) {
toast(
"Failed getting your org. If this persists, please contact support. Redirecting to workflows...",
);
toast.warn("Failed getting your org. If this persists, please contact support. Redirecting to workflows...")
setTimeout(() => {
window.location.href = "/workflows";
}, 3000);
} else {
setUpdateOrg(false);
if (
responseJson.sync_features === undefined ||
responseJson.sync_features === null
@@ -163,6 +170,13 @@ const Admin2 = (props) => {
});
};
useEffect(() => {
if (updateOrg && userdata?.active_org?.id !== undefined && userdata?.active_org?.id !== null && userdata?.active_org?.id.length > 0) {
handleGetOrg(userdata.active_org.id);
setUpdateOrg(false);
}
}, [updateOrg]);
useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search);
@@ -216,7 +230,17 @@ const Admin2 = (props) => {
setTimeout(() => {
window.location.reload()
}, 3000);
toast("Successfully changed active organization - refreshing!");
toast.success("Successfully changed active organization - refreshing!");
if (responseJson.org_id !== undefined && responseJson.org_id !== null && responseJson.org_id.length === 36) {
navigate(`/admin?org_id=${responseJson.org_id}`)
} else {
if (orgId !== undefined && orgId !== null && orgId?.includes("@")) {
navigate(`/admin`)
} else {
toast("No pivot?")
}
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
if (!responseJson.reason.includes("already")) {
+343 -65
View File
@@ -6,7 +6,11 @@ import { getTheme } from "../theme.jsx";
import { toast } from "react-toastify"
import ReactJson from "react-json-view-ssr";
import { v4 as uuidv4} from "uuid";
import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows.jsx";
import { validateJson, collapseField, handleReactJsonClipboard, HandleJsonCopy } from "../views/Workflows2.jsx";
import AppSearch from "../components/Appsearch.jsx";
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Paragraph, Blockquote, CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
import {
Box,
@@ -18,6 +22,8 @@ import {
Tooltip,
IconButton,
TextField,
Popover,
Divider,
} from '@mui/material'
import {
@@ -32,10 +38,13 @@ import {
Close as CloseIcon,
OpenInNew as OpenInNewIcon,
Refresh as RefreshIcon,
Add as AddIcon,
Warning as WarningIcon,
} from '@mui/icons-material'
import {
green,
yellow,
red,
} from '../views/AngularWorkflow.jsx'
@@ -55,11 +64,35 @@ const AgentUI = (props) => {
const [actionInput, setActionInput] = useState("")
const [questionAnswers, setQuestionAnswers] = useState({})
const [newSelectedApp, setNewSelectedApp] = React.useState({})
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
const [chosenApps, setChosenApps] = useState([])
useEffect(() => {
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
return
}
setNewSelectedApp({})
setAppPickerAnchor(null)
if (chosenApps.findIndex((app) => app.id === newSelectedApp.objectID) !== -1) {
} else {
setChosenApps(chosenApps.concat([{
name: newSelectedApp.name,
id: newSelectedApp.objectID,
image: newSelectedApp.image_url,
}]))
}
}, [newSelectedApp])
const {themeMode} = useContext(Context)
const theme = getTheme(themeMode)
const navigate = useNavigate();
if (document !== undefined && document !== null && !document?.title?.includes("Agent")) {
document.title = "Shuffle AI Agents"
}
const agentWrapperStyle = {
width: 1000,
@@ -79,6 +112,44 @@ const AgentUI = (props) => {
}
}
const Heading = (props) => {
const element = React.createElement(
`h${props.level}`,
{ style: { marginTop: 40 } },
props.children
);
return (
<Typography>
{props.level !== 1 ? (
<Divider
style={{
width: "90%",
marginTop: 40,
backgroundColor: theme.palette.inputColor,
}}
/>
) : null}
{element}
</Typography>
);
}
const markdownComponents = {
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
p: Paragraph,
blockquote: Blockquote,
}
const findNodeData = (execution_data, node_id) => {
if (execution_data === undefined || execution_data === null) {
return
@@ -311,7 +382,73 @@ const AgentUI = (props) => {
getAppAuth()
}, [])
const maxTimelineWidth = 300
const maxTimelineWidth = 380
const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
console.log("Submitting questions: ", decisionId, questionAnswers)
if (decisionId === undefined || decisionId === null || decisionId === "") {
toast.error("No decision ID provided. Cannot submit answers.")
return
}
if (Object.keys(questionAnswers).length === 0) {
toast.error("No answers provided. Cannot submit empty answers.")
return
}
var newArgument = {}
if (isContinuation === true) {
// Just a single answer
for (var key in questionAnswers) {
const answer = questionAnswers[key]
newArgument[key] = answer
}
if (Object.keys(newArgument).length === 0) {
toast.error("No answers details. Cannot submit the answer.")
return
}
} else {
for (var key in questionAnswers) {
const answer = questionAnswers[key]
if (isContinuation === true) {
newArgument["question_"+(answer.index)] = answer.value
}
}
}
setAgentRequestLoading(true)
const params = new URLSearchParams(window.location.search)
const executionId = params.get("execution_id")
const nodeId = params.get("node_id")
const authorization = params.get("authorization")
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
fetch(url, {
method: "GET",
credentials: "include",
})
.then((response) => {
setAgentRequestLoading(false)
return response.json()
})
.then((responseJson) => {
if (responseJson.success !== false) {
setTimeout(() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}, 500)
toast.success("Successfully submitted answers! The agent should continue shortly.")
} else {
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
}
})
.catch((error) => {
setAgentRequestLoading(false)
toast.error("Problem with submitting: " + error)
})
}
var latestEndTime = 0
var originalStartTime = 0
@@ -332,6 +469,11 @@ const AgentUI = (props) => {
<ErrorIcon style={{color: red, marginRight: 10, }} />
</Tooltip>
:
item.status === "IGNORED" || item.status === "IGNORE" ?
<Tooltip title={`${item.status}: Previous FAILURE before the agent was reran.`} placement="top">
<WarningIcon style={{color: yellow, marginRight: 10, }} />
</Tooltip>
:
<Tooltip title={`Not started yet: ${item.status}`} placement="top">
<HourglassDisabledIcon style={{marginRight: 10, }} />
</Tooltip>
@@ -399,6 +541,10 @@ const AgentUI = (props) => {
const defaultTopPadding = 10
const open = openIndexes.includes(index)
if (item?.type === "agent" && item?.details?.original_input !== undefined) {
document.title = "Agent: " + item?.details?.original_input?.substring(0, 50)
}
var questions = []
if (item?.details?.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
item.type = "finalise"
@@ -434,6 +580,10 @@ const AgentUI = (props) => {
<Tooltip title="Ask" placement="top">
<img src="/images/workflows/UserInput2.svg" style={categoryStyle} />
</Tooltip>
: item.category === "finalise" || item.category === "finish" || item.action === "finish" ?
<Tooltip title="The action finished successfully" placement="top">
<CheckIcon style={{color: green, marginRight: 10, }} />
</Tooltip>
:
<div style={categoryStyle} />
@@ -487,10 +637,10 @@ const AgentUI = (props) => {
}
}
const barColor = item.status === "FINISHED" ? green :
const barColor = item.status === "IGNORED" ? yellow : item.status === "FINISHED" ? green :
item.status === "FAILURE" || item.status == "ABORTED" ? red :
item.status === "RUNNING" || item.status === "" ? theme.palette.main :
theme.palette.surfaceColor
red
const rerunAgentButton =
<Tooltip title="Rerun from the start with the same input" placement="right">
@@ -501,7 +651,7 @@ const AgentUI = (props) => {
e.preventDefault()
e.stopPropagation()
toast.info("Attempting to rerun everything.")
toast.info("Rerunning agent with the same input.")
setDisableButtons(true)
if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) {
@@ -529,7 +679,7 @@ const AgentUI = (props) => {
const rerunButton =
<Tooltip title="Rerun JUST this decision. This can be used if an agent decision action somehow stopped and didn't get a result." placement="right">
<Tooltip title="Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." placement="right">
<span>
<IconButton
disabled={item.type !== "decision" || disableButtons}
@@ -548,55 +698,6 @@ const AgentUI = (props) => {
</span>
</Tooltip>
const submitQuestions = (decisionId, questionAnswers) => {
console.log("Submitting questions: ", decisionId, questionAnswers)
if (decisionId === undefined || decisionId === null || decisionId === "") {
toast.error("No decision ID provided. Cannot submit answers.")
return
}
if (Object.keys(questionAnswers).length === 0) {
toast.error("No answers provided. Cannot submit empty answers.")
return
}
// Loop qu
var newArgument = {}
for (var key in questionAnswers) {
const answer = questionAnswers[key]
newArgument["question_"+(answer.index)] = answer.value
}
const params = new URLSearchParams(window.location.search)
const executionId = params.get("execution_id")
const nodeId = params.get("node_id")
const authorization = params.get("authorization")
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
console.log("PARSED URL: ", url)
fetch(url, {
method: "GET",
credentials: "include",
})
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson.success !== false) {
setTimeout(() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}, 500)
toast.success("Successfully submitted answers! The agent should continue shortly.")
} else {
toast.warn("Failed to submit answers. Please try again or contact support@shuffler.io if this persists..")
}
})
.catch((error) => {
toast.error("Problem with submitting: " + error)
})
}
return (
<div
style={{
@@ -675,6 +776,7 @@ const AgentUI = (props) => {
minWidth: 300,
maxWidth: 300,
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}>
{item.label}
</div>
@@ -746,10 +848,11 @@ const AgentUI = (props) => {
</Tooltip>
*/}
<Tooltip title="See in another window" placement="left">
<Tooltip title="Answer in the Form UI" placement="left">
<span>
<IconButton
style={{marginLeft: 0, }}
disabled={item?.details?.run_details?.status === "FINISHED"}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
@@ -767,6 +870,28 @@ const AgentUI = (props) => {
:
item.category === "agent" ?
rerunAgentButton
:
item?.type === "decision" ?
<div style={{display: "flex", }}>
{rerunButton}
<Tooltip title="Explore/debug execution" placement="left">
<span>
<IconButton
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
style={{marginLeft: 0, }}
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
//http://localhost:3002/forms/aadfe022-fe93-431c-8634-de42dd7440ac?authorization=9357f6a6-7d59-44be-ad66-be27657369ac&reference_execution=0726378d-b501-470f-b850-f7fb48cd8ca4&source_node=de446bcf-ad37-4337-9f72-e069c7425fac&backend_url=https://ec4245cd2941.ngrok-free.app
window.open(item?.details?.run_details?.debug_url, '_blank', 'noopener,noreferrer');
}}
>
<OpenInNewIcon color={barColor === red ? "primary" : "secondary"} />
</IconButton>
</span>
</Tooltip>
</div>
:
rerunButton
}
@@ -805,13 +930,25 @@ const AgentUI = (props) => {
{questions.map((q, questionIndex) => {
return (
<div style={{marginTop: 25, }}>
<Typography variant="body2">
{`${q.question}`}
</Typography>
<div id="markdown_wrapper_outer" style={{cursor: "default", }}>
<Markdown
components={markdownComponents}
id="markdown_wrapper"
className={"style.reactMarkdown"}
escapeHtml={false}
skipHtml={false}
remarkPlugins={[remarkGfm]}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{q.question}
</Markdown>
</div>
<TextField
label={`Question ${q.index}`}
placeholder="No question found"
placeholder="Your answer here"
variant="outlined"
style={{width: 800, marginTop: 20, }}
multiline
@@ -838,7 +975,7 @@ const AgentUI = (props) => {
<Button
variant="contained"
style={{marginTop: 10, }}
style={{marginTop: 16, }}
disabled={questionSubmitDisabled}
onClick={() => {
submitQuestions(item?.details?.run_details?.id, questionAnswers)
@@ -887,6 +1024,8 @@ const AgentUI = (props) => {
const TimelineRender = (props) => {
const { agent_data } = props;
const [continuationText, setContinuationText] = useState("")
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
const validate = validateJson(actionResult?.result)
if (validate.valid === true) {
@@ -938,6 +1077,7 @@ const AgentUI = (props) => {
}
}
var finishDecisionId = ""
var sortedTimelineItems = []
for (var key in agent_data?.decisions) {
const item = agent_data.decisions[key]
@@ -962,6 +1102,11 @@ const AgentUI = (props) => {
newTimelineItem.details = item
timelineItems.push(newTimelineItem)
if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
finishDecisionId = item?.run_details?.id
}
}
timelineItems.sort((a, b) => {
@@ -1002,6 +1147,72 @@ const AgentUI = (props) => {
)
})}
{finishDecisionId !== "" ?
<Box
component="form"
style={{width: "100%", textAlign: "center",}}
onSubmit={(e) => {
e.preventDefault();
// Uses the submitQuestion and adds more details to
//setAgentRequestLoading ?
submitQuestions(finishDecisionId, {
"continue": continuationText,
}, true)
}}
>
<div style={{display: "flex", maxWidth: 550, minWidth: 550, margin: "auto", marginTop: 50, }}>
<div>
<TextField
label="Add more details to the current task"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 400, margin: "auto", }}
multiline
minRows={1}
onChange={(e) => {
console.log("Value: ", e.target.value)
//setActionInput(e.target.value)
//
setContinuationText(e.target.value)
}}
InputProps={{
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton
type="submit"
disabled={continuationText === ""}
>
<SendIcon
color={continuationText === "" ? "disabled" : "primary"}
/>
</IconButton>
</Tooltip>
),
}}
/>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
Any failed tasks will be set to ignored (TBD). <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
</Typography>
</div>
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
OR
</Typography>
<Button
variant={"contained"}
color="primary"
disabled={true}
style={{marginTop: 10, marginLeft: 20, minWidth: 150, maxWidth: 150, height: 56, }}
>
Create as Workflow
</Button>
</div>
</Box>
: null}
</div>
)
}
@@ -1021,14 +1232,28 @@ const AgentUI = (props) => {
setAgentActionResult(null)
document.title = `Agent: ${inputText.substring(0, 30)}...`
if (inputText === undefined || inputText === null || inputText === "") {
toast.error("Please provide a valid input for the AI Agent.")
setAgentRequestLoading(false)
return
}
// 1. Run the execution. Can this be a single-action run?
// 2. Get the execution ID and node ID from the response.
const uuid = uuidv4()
var parsedAction = "list_tickets,API" // Default action for now
if (chosenApps.length > 0) {
parsedAction = ""
for (var appKey in chosenApps) {
const app = chosenApps[appKey]
const appname = app.name.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_")
parsedAction += `app:${app.id}:${appname.replaceAll(",", "").replaceAll(":", "")},`
}
parsedAction = parsedAction.slice(0, -1) // Remove last comma
}
const data = {
"id": uuid,
"name":"agent",
@@ -1049,7 +1274,7 @@ const AgentUI = (props) => {
},
{
"name":"action",
"value":"list_tickets,API"
"value": parsedAction,
}
]}
@@ -1080,7 +1305,7 @@ const AgentUI = (props) => {
}
const handleKeyDown = (e) => {
const handleKeyDownRoot = (e) => {
const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS
const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux
if (isCmdEnter || isCtrlEnter) {
@@ -1089,6 +1314,11 @@ const AgentUI = (props) => {
}
}
const chipStyle = {
margin: 4,
cursor: "pointer",
}
return (
<div style={agentWrapperStyle}>
<TextField
@@ -1100,7 +1330,7 @@ const AgentUI = (props) => {
<Box
component="form"
style={{textAlign: "center", }}
onKeyDown={handleKeyDown}
onKeyDown={handleKeyDownRoot}
onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
@@ -1123,7 +1353,7 @@ const AgentUI = (props) => {
disabled={agentRequestLoading}
style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline
minRows={2}
minRows={1}
defaultValue={actionInput || ""}
onChange={(e) => {
setActionInput(e.target.value)
@@ -1143,6 +1373,54 @@ const AgentUI = (props) => {
),
}}
/>
<div style={{display: "flex", margin: "auto", paddingTop: 10, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
<div>
<Chip
id="add_app_chip"
icon={<AddIcon />} label="Select Apps"
style={chipStyle}
onClick={() => {
setAppPickerAnchor(document.getElementById("add_app_chip"))
}}
/>
<Popover
open={appPickerAnchor !== null}
anchorEl={appPickerAnchor}
onClose={() => {
setAppPickerAnchor(null)
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<AppSearch
userdata={userdata}
defaultSearch={""}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
inputHeight={200}
/>
</Popover>
</div>
{chosenApps.map((app, index) => {
const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
const chosenImagePath = app?.image
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
return (
<Chip icon={chosenImage} label={chosenName} variant="outlined"
style={chipStyle}
onDelete={() => {
const newChosenApps = chosenApps.filter((a, i) => i !== index)
setChosenApps(newChosenApps)
}}
/>
)
})}
</div>
</Box>
:
<div>
+194 -173
View File
@@ -135,12 +135,10 @@ import {
OpenInFull as OpenInFullIcon,
Difference as DifferenceIcon,
DataObject as DataObjectIcon,
SwapHoriz as SwapHorizIcon
} from "@mui/icons-material";
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
//import * as cytoscape from "cytoscape";
import cytoscape from "cytoscape";
import edgehandles from "cytoscape-edgehandles";
import CytoscapeComponent from "react-cytoscapejs";
@@ -152,7 +150,7 @@ import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
import LineChartWrapper from "../components/LineChartWrapper.jsx";
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows.jsx";
import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows2.jsx";
import { GetParsedPaths, internalIds, } from "../views/Apps.jsx";
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
@@ -1065,7 +1063,29 @@ const AngularWorkflow = (defaultprops) => {
"type": "",
},
"description": "Build & integrate tools easily with standard input and standard output. Built by Shuffle. https://singul.io",
"actions": [{
"actions": [
{
"name": "Translate standard",
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
"label": "Translate standard",
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
"parameters": [{
"name": "source_data",
"value": "",
"required": true,
"multiline": true,
},
{
"name": "standard",
"value": "OCSF",
"description": "The standard to use from https://github.com/Shuffle/standards/tree/main",
"options": [
"OCSF"
],
"required": true,
"multiline": false,
}]
}, {
"name": "Cases",
"description": "Available actions for case management",
"label": "Cases",
@@ -1278,28 +1298,6 @@ const AngularWorkflow = (defaultprops) => {
]
},
*/
{
"name": "Translate standard",
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
"label": "Translate standard",
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
"parameters": [{
"name": "source_data",
"value": "",
"required": true,
"multiline": true,
},
{
"name": "standard",
"value": "OCSF",
"description": "The standard to use from https://github.com/Shuffle/standards/tree/main",
"options": [
"OCSF"
],
"required": true,
"multiline": false,
}]
},
]}]
@@ -2244,6 +2242,11 @@ const AngularWorkflow = (defaultprops) => {
};
const getWorkflowExecution = (id, execution_id, filter, orgId) => {
if (id === undefined) {
console.log("No workflow ID defined for getting executions")
return
}
var url = `${globalUrl}/api/v2/workflows/${id}/executions`
var method = "GET"
if (filter === undefined || filter === null || filter.toUpperCase() === "ALL") {
@@ -22082,6 +22085,153 @@ const AngularWorkflow = (defaultprops) => {
}
}
// Should probably put this on the backend instead when notifications are made :))
const getErrorSuggestion = (result) => {
if (result === undefined || result === null) {
return ""
}
// Check if array with json inside to handle one item at a time~
if (typeof result === "object" && result.length !== undefined) {
if (result.length > 0) {
// Check type inside
if (typeof result[0] === "object") {
result = result[0]
}
}
}
if (result.success === true && result.status === 200) {
if (result.body !== undefined && result.body !== null) {
const stringbody = result.body.toString()
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
return ""
}
if (stringbody.length > 1000) {
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
}
}
}
if (result.status === 429) {
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
}
if (result.status === 405) {
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
}
if (result.status === 415) {
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
}
if (result.status === 401) {
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
}
if (result.status === 403) {
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
}
if (result.status === 404) {
return "The URL, or content of the URL is incorrect. Check it and try again."
}
if (result.status === 400) {
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
}
if (result.status === 200 || result.status === 201 || result.status === 204) {
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
}
// Validate and check for newlines
if (result.success !== false) {
var stringjson = result
const valid = validateJson(stringjson, true)
if (valid.valid === false) {
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
// Look for newline
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
} else {
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
}
}
}
//return ""
}
try {
stringjson = JSON.stringify(result)
} catch (e) {
}
stringjson = stringjson.toLowerCase()
if (stringjson.includes("localhost")) {
return "You can't use localhost in apps. Use the external ip or url of the server instead"
}
if (stringjson.includes("manifest unknown")) {
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
}
if (stringjson.toLowerCase().includes("too many values to unpack")) {
return "This is a known error with old apps. Please rebuild the app. Contact support@shuffler.io if it persists after rebuild."
}
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
return "Consider whether your Orborus environment can connect to a local IP or not."
}
if (stringjson.includes("kms/")) {
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
}
if (stringjson.includes("string indices must be integers")) {
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
}
if (stringjson.includes("invalidurl")) {
// IF count of "http" is more than one, 1, it's prolly invalid
var additionalinfo = ""
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
additionalinfo = "You may be using multiple 'http' in the URL. "
}
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
}
if (stringjson.includes("result too large to handle")) {
return "Execution loading failed. Reload the execution by closing it and clicking it again"
}
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
}
if (stringjson.toLowerCase().includes("invalid header")) {
return "A header or authentication token in the app is invalid. Check the app's configuration"
}
if (stringjson.includes("connectionerror")) {
if (stringjson.includes("kms")) {
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
}
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
}
return ""
}
const ShowCopyingTooltip = () => {
const [showCopying, setShowCopying] = React.useState(true)
@@ -22161,8 +22311,8 @@ const AngularWorkflow = (defaultprops) => {
</IconButton>
</Tooltip>
: null}
{executionModalView === 0 ? (
{executionModalView === 0 ? (
<div style={{ position: "relative", padding: isMobile ? "0px 0px 0px 10px" : "25px 25px 25px 25px", zIndex: 12502, backgroundColor: theme.palette.drawer.backgroundColor, height: "100%", }}>
<div style={{ display: "flex", }}>
<Breadcrumbs
@@ -23073,6 +23223,13 @@ const AngularWorkflow = (defaultprops) => {
height: imgsize,
border: `2px solid ${statusColor}`,
borderRadius: executionData.start === data.action.id ? 25 : 5,
cursor: isCloud ? "pointer" : "default",
}}
onClick={() => {
if (isCloud) {
window.open(`/apps/${data?.action?.app_name}`, "_blank")
}
}}
/>
);
@@ -23095,7 +23252,7 @@ const AngularWorkflow = (defaultprops) => {
);
}
if (data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
if (data?.action?.name === "User Input" || data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
actionimg = (
<img
alt={"User Input Trigger"}
@@ -23222,6 +23379,13 @@ const AngularWorkflow = (defaultprops) => {
}
}
if (relevant_errors.length === 0) {
const foundError = getErrorSuggestion(validate.result)
if (foundError !== undefined && foundError !== null && foundError !== "") {
relevant_errors = [foundError]
}
}
return (
<div
key={index}
@@ -23671,149 +23835,6 @@ const AngularWorkflow = (defaultprops) => {
var draggingDisabled = false;
// Should probably put this on the backend instead when notifications are made :))
const getErrorSuggestion = (result) => {
if (result === undefined || result === null) {
return ""
}
// Check if array with json inside to handle one item at a time~
if (typeof result === "object" && result.length !== undefined) {
if (result.length > 0) {
// Check type inside
if (typeof result[0] === "object") {
result = result[0]
}
}
}
if (result.success === true && result.status === 200) {
if (result.body !== undefined && result.body !== null) {
const stringbody = result.body.toString()
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
return ""
}
if (stringbody.length > 1000) {
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
}
}
}
if (result.status === 429) {
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
}
if (result.status === 405) {
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
}
if (result.status === 415) {
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
}
if (result.status === 401) {
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
}
if (result.status === 403) {
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
}
if (result.status === 404) {
return "The URL, or content of the URL is incorrect. Check it and try again."
}
if (result.status === 400) {
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
}
if (result.status === 200 || result.status === 201 || result.status === 204) {
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
}
// Validate and check for newlines
if (result.success !== false) {
var stringjson = result
const valid = validateJson(stringjson, true)
if (valid.valid === false) {
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
// Look for newline
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
} else {
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
}
}
}
//return ""
}
try {
stringjson = JSON.stringify(result)
} catch (e) {
}
stringjson = stringjson.toLowerCase()
if (stringjson.includes("localhost")) {
return "You can't use localhost in apps. Use the external ip or url of the server instead"
}
if (stringjson.includes("manifest unknown")) {
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
}
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
return "Consider whether your Orborus environment can connect to a local IP or not."
}
if (stringjson.includes("kms/")) {
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
}
if (stringjson.includes("string indices must be integers")) {
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
}
if (stringjson.includes("invalidurl")) {
// IF count of "http" is more than one, 1, it's prolly invalid
var additionalinfo = ""
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
additionalinfo = "You may be using multiple 'http' in the URL. "
}
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
}
if (stringjson.includes("result too large to handle")) {
return "Execution loading failed. Reload the execution by closing it and clicking it again"
}
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
}
if (stringjson.toLowerCase().includes("invalid header")) {
return "A header or authentication token in the app is invalid. Check the app's configuration"
}
if (stringjson.includes("connectionerror")) {
if (stringjson.includes("kms")) {
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
}
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
}
return ""
}
const currentSuggestion = getErrorSuggestion(validate.result)
const codePopoutModal = !codeModalOpen ? null : (
<Dialog
+3 -3
View File
@@ -25,7 +25,7 @@ import {
Divider,
} from "@mui/material";
import { validateJson, } from "../views/Workflows.jsx";
import { validateJson, } from "../views/Workflows2.jsx";
import { isMobile } from "react-device-detect"
import theme from "../theme.jsx";
import PaperComponent from "../components/PaperComponent.jsx";
@@ -277,7 +277,8 @@ const ApiExplorerWrapper = (props) => {
parsedapp.name !== undefined &&
parsedapp.name !== null &&
parsedapp.name.length !== 0;
if(parsedapp?.id.length > 0){
if (parsedapp?.id.length > 0) {
setSelectedAppData(parsedapp)
handleAppAuthenticationType(parsedapp)
const apptype = selectedAppData?.generated === false ? "python" : "openapi"
@@ -441,7 +442,6 @@ const ApiExplorerWrapper = (props) => {
selectedAppData.name = appname
}
console.log("APPNAME: ", appname, openapi.id)
if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") {
setAppAuthentication(data)
setSelectedAuthentication({})
+3 -3
View File
@@ -3455,9 +3455,9 @@ const AppCreator = (defaultprops) => {
required_bodyfields: [],
});
useEffect(() => {
console.log("Queries: ", urlPathQueries)
}, [urlPathQueries])
//useEffect(() => {
// console.log("Queries: ", urlPathQueries)
//}, [urlPathQueries])
const findBodyParams = (body) => {
const regex = /\${(\w+)}/g;
+2 -2
View File
@@ -74,7 +74,7 @@ import {
} from "react-instantsearch-dom";
import AppStats from "../components/AppStats.jsx";
import ParsedAction from "../components/ParsedAction.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
import { base64_decode, appCategories } from "../views/AppCreator.jsx";
import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
@@ -4454,7 +4454,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
): null}
{userdata && (userdata?.active_org?.creator_org?.length > 0 || userdata?.active_org?.child_orgs?.length === 0) ? null : (
<Button variant="outlined" color="secondary" onClick={()=> {setShowDistributionPopup(true)}} >Distribute App</Button>
<Button variant="outlined" color="secondary" onClick={()=> {setShowDistributionPopup(true)}}>Distribute</Button>
)}
</div>
) : (
+137 -35
View File
@@ -6,7 +6,7 @@ import ReactJson from "react-json-view-ssr";
import { isMobile } from "react-device-detect";
import { BrowserView, MobileView } from "react-device-detect";
import { useParams, useNavigate, Link, useLocation } from "react-router-dom";
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
import remarkGfm from 'remark-gfm'
import { Context } from "../context/ContextApi.jsx";
import {
@@ -87,6 +87,34 @@ const innerHrefStyle = {
textDecoration: "none",
};
const noteLabelStyle = {
fontWeight: "bold",
color: "#f86a3e",
display: "block",
marginBottom: "5px",
};
const alertNote = {
padding: "10px",
borderLeft: "5px solid #f86a3e",
backgroundColor: "rgb(26,26,26)",
};
export const Blockquote = ({ children }) => {
const textContent = children.map(child =>
child.props && child.props.children ? child.props.children.join('') : child
).join('').trim();
// Maybe some more contents....
const isNote = textContent.startsWith("[!TIP]");
return (
<blockquote style={isNote ? alertNote : {}}>
{isNote && <span style={noteLabelStyle}>Tips:</span>}
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
</blockquote>
);
};
export const CopyToClipboard = (props) => {
const { text, style, onCopy } = props;
@@ -173,6 +201,86 @@ export const OuterLink = (props) => {
);
}
// Markdown table renderers for improved styling and readability
export const TableRenderer = (props) => {
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
return (
<div
style={{
overflowX: "auto",
marginTop: 16,
marginBottom: 24,
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: theme.palette?.borderRadius,
}}
>
<table
style={{
width: "100%",
borderCollapse: "separate",
borderSpacing: 0,
minWidth: 600,
}}
>
{props.children}
</table>
</div>
);
}
export const TableRowRenderer = (props) => {
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
return (
<tr
style={{
borderBottom: "1px solid rgba(255,255,255,0.12)",
}}
>
{props.children}
</tr>
);
}
export const TableHeaderCellRenderer = (props) => {
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
return (
<th
style={{
textAlign: "left",
padding: "12px 14px",
backgroundColor: theme.palette.platformColor,
color: theme.palette.textColor,
fontWeight: 600,
position: "sticky",
top: 0,
borderBottom: "1px solid rgba(255,255,255,0.2)",
}}
>
{props.children}
</th>
);
}
export const TableCellRenderer = (props) => {
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
return (
<td
style={{
padding: "10px 14px",
verticalAlign: "top",
color: theme.palette.textColor,
backgroundColor: "transparent",
}}
>
{props.children}
</td>
);
}
export const Img = (props) => {
@@ -250,9 +358,23 @@ export const CodeHandler = (props) => {
// Need to check if it's singletick or multi
if (props.inline === true) {
// Show it inline
// Enhanced inline code styling for readability and long strings
return (
<span style={{ backgroundColor: theme.palette.inputColor, display: "inline", whiteSpace: "pre-wrap", padding: "6px 3px 6px 3px", }}>
<span
style={{
backgroundColor: theme.palette.inputColor,
border: "1px solid rgba(255,255,255,0.15)",
borderRadius: "6px",
padding: "1px 6px",
margin: "0 2px",
display: "inline-block",
lineHeight: 1.6,
fontSize: "0.95em",
fontFamily:
'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
whiteSpace: "pre-wrap",
}}
>
{newprop}
</span>
)
@@ -318,6 +440,8 @@ const Docs = (defaultprops) => {
props.match = {}
props.match.params = params
window.title = "Shuffle - Documentation"
//console.log("PARAMS: ", params)
const { themeMode } = useContext(Context)
const theme = getTheme(themeMode)
@@ -668,36 +792,13 @@ const Docs = (defaultprops) => {
minHeight: "80vh",
};
const noteLabelStyle = {
fontWeight: "bold",
color: "#f86a3e",
display: "block",
marginBottom: "5px",
};
const Blockquote = ({ children }) => {
const textContent = children.map(child =>
child.props && child.props.children ? child.props.children.join('') : child
).join('').trim();
// Maybe some more contents....
const isNote = textContent.startsWith("[!TIP]");
return (
<blockquote style={isNote ? alertNote : {}}>
{isNote && <span style={noteLabelStyle}>Tips:</span>}
{isNote ? textContent.replace("[!TIP]", "").trim() : children}
</blockquote>
);
};
const Heading = (props) => {
const [hover, setHover] = useState(false);
var id = (props.children?.[0] ?? props.children ?? '').toString().toLowerCase();
if (props.level <= 3) {
id = props.children[0].toLowerCase().toString().replaceAll(" ", "-");
var id = (props?.children?.[0] ?? props.children ?? '').toString().toLowerCase();
if (props?.level <= 3) {
id = props?.children?.[0]?.toLowerCase().toString().replaceAll(" ", "-") ?? '';
}
const element = React.createElement(
@@ -1098,11 +1199,6 @@ const Docs = (defaultprops) => {
fontSize: isMobile ? "1.3rem" : "1.1rem",
};
const alertNote = {
padding: "10px",
borderLeft: "5px solid #f86a3e",
backgroundColor: "rgb(26,26,26)",
};
const CustomButton = (props) => {
const { title, icon, link } = props
@@ -1192,7 +1288,7 @@ const Docs = (defaultprops) => {
</Typography>
{showPartnerLogo === true ? null :
<div style={{ display: "flex", marginTop: 25, }}>
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> />
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://shuffler.io/contact?category=support" />
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
</div>
}
@@ -1230,6 +1326,8 @@ const Docs = (defaultprops) => {
</div>
</div>
const markdownComponents = {
img: Img,
code: CodeHandler,
@@ -1242,6 +1340,10 @@ const Docs = (defaultprops) => {
a: OuterLink,
p: Paragraph,
blockquote: Blockquote,
table: TableRenderer,
tr: TableRowRenderer,
th: TableHeaderCellRenderer,
td: TableCellRenderer,
}
+1 -1
View File
@@ -329,7 +329,7 @@ const LoginPage = props => {
const tmpMessage = new URLSearchParams(window.location.search).get("message")
if (tmpMessage !== undefined && tmpMessage !== null && message !== tmpMessage) {
setMessage(tmpMessage)
toast(tmpMessage)
toast.warn(tmpMessage)
}
}
}, [])
+199 -12
View File
@@ -12,6 +12,8 @@ import {
MenuItem,
Tooltip,
CircularProgress,
FormControl,
InputLabel,
} from '@mui/material';
import { toast } from "react-toastify";
@@ -54,6 +56,15 @@ const NewDashboard = (props) => {
const [overrideDays, setOverrideDays] = useState(undefined);
const [rotMonthOverride, setRotMonthOverride] = useState(undefined);
const [isProdStatusOn, setIsProdStatusOn] = useState(false)
const [selectedOrganization, setSelectedOrganization] = useState(null)
const [selectedOrgForStats, setSelectedOrgForStats] = useState(userdata?.active_org?.id || null)
const [availableOrgs, setAvailableOrgs] = useState([])
const [selectedOrgStats, setSelectedOrgStats] = useState(null)
const [loadingSelectedOrgStats, setLoadingSelectedOrgStats] = useState(false)
const [selectedOrgDetailsForStats, setSelectedOrgDetailsForStats] = useState(null)
const [fallbackToParentStats, setFallbackToParentStats] = useState(false);
document.title = "Shuffle - Dashboard";
const isCloud =
serverside === true || typeof window === "undefined"
@@ -121,15 +132,22 @@ const NewDashboard = (props) => {
const displayName = userdata !== undefined && userdata?.username !== undefined ? userdata?.username?.split('@')[0]?.charAt(0)?.toUpperCase() + userdata?.username?.split('@')[0]?.slice(1) : 'User';
useEffect(() => {
let t;
const anyLoading = loadingSfw || loadingRot || loadingNoti;
if (anyLoading) {
t = setShowOverlay(true);
} else {
setShowOverlay(false);
}
return () => { if (t) clearTimeout(t); };
}, [loadingSfw, loadingRot, loadingNoti]);
const anyLoading =
loadingSfw ||
loadingRot ||
loadingNoti ||
loadingSelectedOrgStats ||
!selectedOrganization ||
!selectedOrgForStats;
setShowOverlay(anyLoading);
}, [
loadingSfw,
loadingRot,
loadingNoti,
loadingSelectedOrgStats,
selectedOrganization,
selectedOrgForStats,
]);
// Auto-open onboarding when there aren't enough active days of stats
useEffect(() => {
@@ -211,6 +229,7 @@ const NewDashboard = (props) => {
.then((response) => (response.ok ? response.json() : null))
.then((org) => {
if (!fetched && org) {
setSelectedOrganization(org);
if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
@@ -229,8 +248,136 @@ const NewDashboard = (props) => {
};
}, [userdata?.active_org?.id, globalUrl]);
useEffect(() => {
if (!selectedOrgForStats) {
return;
}
let fetched = false;
fetch(`${globalUrl}/api/v1/orgs/${selectedOrgForStats}`, {
method: "GET",
credentials: "include",
headers: { "Content-Type": "application/json" },
})
.then((response) => (response.ok ? response.json() : null))
.then((org) => {
if (!fetched && org) {
setSelectedOrgDetailsForStats(org);
}
})
.catch(() => {});
return () => {
fetched = true;
};
}, [selectedOrgForStats, globalUrl]);
// Build list of available orgs for stats (parent + child orgs)
useEffect(() => {
if (!selectedOrganization) return;
const orgs = [{ id: 'ALL', name: 'All Organizations', isAll: true }];
// Add parent org
if (selectedOrganization?.id) {
orgs.push({
id: selectedOrganization.id,
name: selectedOrganization.name || 'Parent Organization',
isAll: false
});
}
// Add child orgs if they exist
if (selectedOrganization?.child_orgs && Array.isArray(selectedOrganization.child_orgs)) {
selectedOrganization.child_orgs.forEach(child => {
if (child?.id && child?.name) {
orgs.push({
id: child.id,
name: child.name,
isAll: false
});
}
});
}
setAvailableOrgs(orgs);
setSelectedOrgForStats(selectedOrganization?.id);
}, [selectedOrganization]);
// Fetch statistics for specifically selected org (not for ALL)
useEffect(() => {
let aborted = false;
const load = async () => {
try {
if (!selectedOrgForStats || selectedOrgForStats === 'ALL') {
setSelectedOrgStats(null);
setFallbackToParentStats(false);
return;
}
setLoadingSelectedOrgStats(true);
const resp = await fetch(`${globalUrl}/api/v1/orgs/${encodeURIComponent(selectedOrgForStats)}/stats`, {
method: 'GET',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
});
let fallback = false;
let data = null;
if (resp.ok) {
data = await resp.json();
}
const isParentContext = selectedOrganization && userdata?.active_org?.id === selectedOrganization.id;
if (!data || !Array.isArray(data.daily_statistics) || data.daily_statistics.length === 0) {
fallback = true;
setSelectedOrgStats(null);
// Only fallback to parent if on parent org dashboard
if (isParentContext && selectedOrganization && selectedOrgForStats !== selectedOrganization.id) {
toast.info('No stats available for selected org. Showing parent org stats.');
setSelectedOrgForStats(selectedOrganization.id);
}
} else {
// If data exists but has near-zero activity over the last 30 days, fallback to parent
try {
const cutoff = new Date();
cutoff.setDate(cutoff.getDate() - 30);
const recent = data.daily_statistics.filter((d) => {
if (!d?.date) return false;
const dt = new Date(d.date);
return dt >= cutoff;
});
const sumWork = recent.reduce((s, d) => s + Number(d?.workflow_executions_finished || 0) + Number(d?.workflow_executions_failed || 0), 0);
const sumApp = recent.reduce((s, d) => s + Number(d?.app_executions || 0), 0);
const nearZero = (Number(sumWork) + Number(sumApp)) <= 0;
if (nearZero && isParentContext && selectedOrganization && selectedOrgForStats !== selectedOrganization.id) {
fallback = true;
setSelectedOrgStats(null);
toast.info('Selected org has no activity in the last 30 days. Showing parent org stats.');
setSelectedOrgForStats(selectedOrganization.id);
} else {
setSelectedOrgStats(data || null);
}
} catch {
setSelectedOrgStats(data || null);
}
}
setFallbackToParentStats(fallback && isParentContext);
} catch {
if (!aborted) setSelectedOrgStats(null);
setFallbackToParentStats(false);
} finally {
if (!aborted) setLoadingSelectedOrgStats(false);
}
};
// Calling the function.
load();
return () => { aborted = true; };
}, [selectedOrgForStats, selectedOrganization, globalUrl, userdata?.active_org?.id]);
return (
<div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}>
{(onboardingOpen && !loadingSelectedOrgStats) && (
<DashboardOnboarding
open={onboardingOpen}
globalUrl={globalUrl}
@@ -251,6 +398,7 @@ const NewDashboard = (props) => {
headerTitle="Unlock your Dashboard"
headerSubtitle="Complete these steps to start seeing insights."
/>
)}
{showOverlay && (
<div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
@@ -262,9 +410,33 @@ const NewDashboard = (props) => {
{/* Header / Greeting */}
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
<>
<Box style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
{availableOrgs.length > 2 && (
<FormControl size="small" variant="outlined" style={{ minWidth: 200, marginTop: -8 }} sx={{
'& .MuiInputBase-root': {
height: 40,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: '20px',
},
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
}}>
<InputLabel id="stats-org-select-label">View Stats For</InputLabel>
<Select
labelId="stats-org-select-label"
label="View Stats"
value={selectedOrgForStats || ''}
onChange={(e) => setSelectedOrgForStats(e.target.value)}
>
{availableOrgs.map((org) => (
<MenuItem key={org.id} value={org.id}>
{org.name}
</MenuItem>
))}
</Select>
</FormControl>
)}
{sfwControls}
</>
</Box>
</Box>
{/* KPI cards */}
@@ -309,12 +481,27 @@ const NewDashboard = (props) => {
onControlsChange={handleSfwControls}
onLoadingChange={setLoadingSfw}
onTotalsChange={setTotals}
loadingSelectedOrgStats={loadingSelectedOrgStats}
selectedOrganization={selectedOrganization}
selectedOrgForStats={selectedOrgForStats}
orgStats={selectedOrgStats}
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
/>
</Paper>
{/* Runs over time section */}
<Paper style={{ padding: 16, marginTop: 19, background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: 12 }}>
<RunsOverTimeWidget globalUrl={globalUrl} onLoadingChange={setLoadingRot} monthOverride={rotMonthOverride} dummyMode={onboardingOpen} />
<RunsOverTimeWidget
globalUrl={globalUrl}
onLoadingChange={setLoadingRot}
loadingSelectedOrgStats={loadingSelectedOrgStats}
monthOverride={rotMonthOverride}
dummyMode={onboardingOpen}
selectedOrganization={selectedOrganization}
selectedOrgForStats={selectedOrgForStats}
orgStats={selectedOrgStats}
orgForLimit={selectedOrgDetailsForStats || selectedOrganization}
/>
</Paper>
</div>
);
+67 -20
View File
@@ -3,10 +3,10 @@ import React, {useState, useEffect, useContext} from 'react';
import ReactDOM from "react-dom"
import ReactJson from "react-json-view-ssr";
import { green, yellow, red, grey} from "./AngularWorkflow.jsx";
import { green, yellow, red, grey} from "../views/AngularWorkflow.jsx";
import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx";
import { useNavigate, Link, useParams } from "react-router-dom";
import { validateJson, collapseField, GetIconInfo } from "./Workflows.jsx";
import { validateJson, collapseField, GetIconInfo } from "../views/Workflows2.jsx";
import EditWorkflow from "../components/EditWorkflow.jsx"
import { toast } from "react-toastify"
import { makeStyles } from '@mui/material/styles';
@@ -47,6 +47,10 @@ import {
OpenInNew as OpenInNewIcon,
Edit as EditIcon,
Polyline as PolylineIcon,
CheckCircle as CheckCircleIcon,
DirectionsRun as DirectionsRunIcon,
Error as ErrorIcon,
Pause as PauseIcon,
} from '@mui/icons-material';
import { Context } from '../context/ContextApi.jsx';
@@ -145,6 +149,7 @@ const RunWorkflow = (defaultprops) => {
padding: "25px 50px 50px 50px",
borderRadius: 25,
minHeight: 500,
position: "relative",
}
const params = useParams();
@@ -189,7 +194,7 @@ const RunWorkflow = (defaultprops) => {
// questions if none are selected
for (var key in executionArgument) {
if (executionArgument[key] === undefined || executionArgument[key] === null || executionArgument[key] === "") {
console.log("Unanswered, required question: ", key)
//console.log("Unanswered, required question: ", key)
return false
}
}
@@ -406,7 +411,7 @@ const RunWorkflow = (defaultprops) => {
setTimeout(() => {
setExecutionLoading(true)
}, 2500)
}, 250)
var data = {
"execution_argument": executionArgument,
@@ -842,6 +847,7 @@ const RunWorkflow = (defaultprops) => {
})
.then((responseJson) => {
if (responseJson.success === false) {
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
return
}
@@ -915,21 +921,38 @@ const RunWorkflow = (defaultprops) => {
//console.log("Updating data!")
setExecutionData(responseJson)
for (var key in responseJson.results) {
if (responseJson.results[key].status === "WAITING") {
if (responseJson.results[key].status !== "WAITING") {
continue
}
const validate = validateJson(responseJson.results[key].result)
if (validate.valid && typeof validate.result === "string") {
validate.result = JSON.parse(validate.result)
}
if (validate.result["information"] !== undefined && validate.result["information"] !== null) {
setWorkflowQuestion(validate.result["information"])
console.log("Found waiting!: ", validate.result)
if (validate?.result?.information !== undefined && validate?.result?.information !== null) {
console.log("Success! Not checking again.")
setWorkflowQuestion(validate?.result?.information)
} else {
console.log("No information found for questions?: ", validate.result)
// Specific case for "too early" config of waiting
if (typeof validate.result === "string" || Object.keys(validate.result).length === 2) {
setTimeout(() => {
fetchUpdates(responseJson.execution_id, responseJson.authorization)
}, 2000)
} else {
console.log("NOT re-fetching")
}
//setWorkflowQuestions{
}
break
}
}
} else {
console.log("NOT updating executiondata state.");
}
@@ -1034,7 +1057,7 @@ const RunWorkflow = (defaultprops) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
toast.warn("Error getting results.. Please try again or contact support@shuffler.io if this persists.")
//toast.warn("Error getting results.. Please try again or contact support@shuffler.io if this persists.")
}
return response.json();
@@ -1045,18 +1068,19 @@ const RunWorkflow = (defaultprops) => {
}
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && disableButtons === false && responseJson?.status !== "" && responseJson?.status !== "WAITING") {
console.log("IN here 1")
setDisableButtons(true)
}
//if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && (workflow.id === undefined || workflow.id === null || workflow.id.length === 0) && responseJson.workflow !== undefined && responseJson.workflow !== null) {
if (execution_id !== undefined && execution_id !== null && authorization !== undefined && authorization !== null && execution_id.length > 0 && authorization.length > 0 && responseJson.workflow !== undefined && responseJson.workflow !== null) {
console.log("IN here 2")
setupSourcenode(responseJson.workflow, sourceNode)
setWorkflow(responseJson.workflow)
//const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
// Check for decision_id in url
if (decisionId?.length > 0 && responseJson?.workflow?.actions?.length > 0 && sourceNode?.length > 0 && responseJson?.results?.length > 0) {
console.log("Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
console.log("AGENTIC: Setting workflow: ", responseJson.workflow, ", EXEC RESULTS: ", responseJson.results)
setAgentic(true)
@@ -1409,6 +1433,27 @@ const RunWorkflow = (defaultprops) => {
const basedata =
<div style={bodyDivStyle}>
<Paper style={boxStyle}>
<div style={{position: "absolute", top: 20, right: 10,}}>
{executionData?.status === "" ? null :
executionData?.status === "FINISHED" ?
<Tooltip title="The previous Workflow Finished" placement="top">
<CheckCircleIcon style={{color: green, marginRight: 10, }} />
</Tooltip>
: executionData?.status === "EXECUTING" ?
<Tooltip title="The Workflow is current running" placement="top">
<DirectionsRunIcon style={{color: theme.palette.secondary, marginRight: 10, }} />
</Tooltip>
: executionData?.status === "ABORTED" || executionData?.status === "FAILURE" ?
<Tooltip title={`The workflow run failed with status ${executionData?.status}`} placement="top">
<ErrorIcon style={{color: red, marginRight: 10, }} />
</Tooltip>
: executionData?.status === "WAITING" ?
<Tooltip title={`The workflow is waiting for user input`} placement="top">
<PauseIcon style={{color: yellow, marginRight: 10, }} />
</Tooltip>
: null}
</div>
{explorerUi === true ?
<ExplorerUi />
:
@@ -1562,7 +1607,6 @@ const RunWorkflow = (defaultprops) => {
label={parsedLabel}
required
disabled={disabledButtons}
fullWidth={true}
placeholder=""
id="emailfield"
@@ -1582,6 +1626,7 @@ const RunWorkflow = (defaultprops) => {
:
(answer !== undefined && answer !== null) || message !== "" ? null :
executionRunning ? null :
<span>
{foundSourcenode !== undefined && foundSourcenode !== null ?
"Add Note"
@@ -1591,6 +1636,7 @@ const RunWorkflow = (defaultprops) => {
<div style={{marginBottom: 5}}>
<TextField
disabled={executionRunning}
color="primary"
style={{backgroundColor: theme.palette.inputColor, marginTop: 5, }}
multiLine
@@ -1619,6 +1665,7 @@ const RunWorkflow = (defaultprops) => {
/>
</div>
</span>
}
@@ -1724,11 +1771,11 @@ const RunWorkflow = (defaultprops) => {
>
{executionLoading ?
<CircularProgress color="secondary" style={{color: "white",}} />
:
executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ? "Run Again"
:
"Submit"
}
: executionData.result !== undefined && executionData.result !== null && executionData.result.length > 0 ?
"Run Again"
: executionData?.status === "WAITING" ?
"Submit answers"
: "Submit"}
</Button>
</div>
}
@@ -1780,7 +1827,7 @@ const RunWorkflow = (defaultprops) => {
}
return (
<div style={{marginBottom: 10, }}>
<div style={{marginBottom: 10, borderBottom: "1px solid rgba(255,255,255,0.3)", paddingBottom: 5, }} key={index}>
{foundresult?.action?.label?.replaceAll("_", " ")} - {foundresult.status}:
<br />
@@ -1807,7 +1854,7 @@ const RunWorkflow = (defaultprops) => {
{workflowQuestion !== "" ? null :
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 10, }} >
Form submission data includes your Organization's unique ID while logged in, or a unique identifier for your browser otherwise. Your input will be automatically sanitized.
Form submission data includes your Organization's unique ID, or a unique identifier for your browser. Your input will be automatically sanitized.
</Typography>
}
</div>
-576
View File
@@ -126,296 +126,6 @@ const useStyles = makeStyles(() => {
}
})
// Takes an action in Shuffle and
// Returns information about the icon, the color etc to be used
// This can be used for actions of all types
export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs.
const iconList = [
{ key: "cases", values: ["cases"] },
{ key: "cache_add", values: ["set_cache"] },
{ key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter"] },
{ key: "merge", values: ["join", "merge", "route", "router", "routing"] },
{
key: "search",
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
},
{ key: "list", values: ["list", "head", "options"] },
{
key: "download",
values: [
"capture",
"get",
"download",
"return",
"hello_world",
"curl",
"request",
"export",
"preview",
],
},
{ key: "add", values: ["add", "accept",] },
{ key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] },
{
key: "send",
values: [
"send",
"dispatch",
"mail",
"forward",
"post",
"submit",
"mark",
"set",
"release",
],
},
{
key: "repeat",
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
},
{ key: "code", values: ["code", "bash", "python", "react", "go"] },
{ key: "execute", values: ["execute", "run", "play", "raise"] },
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
{ key: "inflate", values: ["inflate", "pack", "compress"] },
{
key: "edit",
values: [
"modify",
"update",
"create",
"edit",
"put",
"patch",
"change",
"replace",
"conver",
"map",
"format",
"escape",
"describe",
],
},
{
key: "compare",
values: ["compare", "convert", "to", "filter", "translate", "parse", "generate", ],
},
{ key: "close", values: ["close", "stop", "cancel", "block"] },
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
];
var selectedKey = ""
if (action.app_name == "Integration Framework") {
selectedKey = "magic"
} else if (action.name === undefined || action.name === null) {
} else {
const actionname = action.name.toLowerCase()
for (var key in iconList) {
//console.log(iconList[key], actionname)
const found = iconList[key].values.find((value) =>
actionname.includes(value)
)
if (found !== null && found !== undefined) {
selectedKey = iconList[key].key
break
}
}
}
// Some of these are manually parsed or created instead of material ui
//M8 0C3.58 0 0 1.79 0 4C0 6.21 3.58 8 8 8C12.42 8 16 6.21 16 4C16 1.79 12.42 0 8 0ZM0 6V9C0 11.21 3.58 13 8 13C12.42 13 16 11.21 16 9V6C16 8.21 12.42 10 8 10C3.58 10 0 8.21 0 6ZM0 11V14C0 16.21 3.58 18 8 18C9.41 18 10.79 17.81 12 17.46V14.46C10.79 14.81 9.41 15 8 15C3.58 15 0 13.21 0 11ZM17 11V14H14V16H17V19H19V16H22V14H19V11
//https://www.figma.com/file/uCfnMs5w6wnLx6ehPHEV74/Figma-Material-Design-System-v3_0?node-id=834%3A21
//COLORS: https://www.pinterest.co.uk/pin/326299935499972946/
const defaultColor = "#f76b1c";
const defaultGradient = ["#fad961", "#f76b1c"];
const parsedIcons = {
magic: {
icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12z",
iconColor: "white",
iconBackgroundColor: "red",
originalIcon: "",
fillGradient: ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#8A2BE2"],
},
communication: {
icon: "M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
cases: {
icon: "M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
cache_add: {
icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
cache_get: {
icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
iconColor: "white",
iconBackgroundColor: "#8acc3f",
originalIcon: "",
fillGradient: ["#8acc3f", "#459622"],
},
repeat: {
icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <CachedIcon />,
},
add: {
icon: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <AddIcon />,
},
edit: {
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <EditIcon />,
},
filter: {
icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
iconColor: "white",
iconBackgroundColor: "#f5515f",
originalIcon: "",
fillGradient: ["#f5515f", "#a1051d"],
},
merge: {
icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
iconColor: "white",
iconBackgroundColor: "#f5515f",
originalIcon: "",
fillGradient: ["#f5515f", "#a1051d"],
},
compare: {
icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <CompareIcon />,
},
extract: {
icon: "M3 3h18v2H3z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <MaximizeIcon />,
},
inflate: {
icon: "M6 19h12v2H6z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <MinimizeIcon />,
},
list: {
icon: "M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <TocIcon />,
},
code: {
icon: "M9.4 16.6 4.8 12l4.6-4.6L8 6l-6 6 6 6zm5.2 0 4.6-4.6-4.6-4.6L16 6l6 6-6 6z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <CodeIcon />,
fillGradient: ["#836ef5", "#3335eb"],
},
execute: {
icon: "M8 5v14l11-7z",
iconColor: "white",
iconBackgroundColor: defaultColor,
originalIcon: <PlayArrowIcon />,
},
delete: {
icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
iconColor: "white",
iconBackgroundColor: "#03030e",
originalIcon: <DeleteIcon />,
fillGradient: ["#03030e", "#205d66"],
},
close: {
icon: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
iconColor: "white",
iconBackgroundColor: "#03030e",
originalIcon: <CloseIcon />,
fillGradient: ["#03030e", "#205d66"],
},
send: {
icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
iconColor: "white",
iconBackgroundColor: "#0373da",
originalIcon: <SendIcon />,
fillGradient: ["#0bc8bf", "#0373da"],
},
download: {
icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
iconColor: "white",
iconBackgroundColor: "#0373da",
originalIcon: <GetAppIcon />,
fillGradient: ["#0bc8bf", "#0373da"],
},
search: {
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <SearchIcon />,
},
};
var selectedItem = parsedIcons[selectedKey];
if (selectedItem === undefined || selectedItem === null) {
return {
icon: "",
iconColor: "",
iconBackground: "black",
originalIcon: "",
};
}
if (selectedItem.fillGradient === undefined) {
selectedItem.fillGradient = defaultGradient;
selectedItem.iconBackgroundColor = defaultColor;
}
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
console.log(
`MISSING PATH FOR ${selectedKey} (find in scope): `,
selectedItem.originalIcon.type.type
);
}
if (
(selectedItem.originalIcon === undefined ||
selectedItem.originalIcon === "") &&
selectedItem.icon !== "" &&
selectedItem.icon !== undefined
) {
const svg_pin = (
<svg
width={svgSize}
height={svgSize}
viewBox={`0 0 ${svgSize} ${svgSize}`}
version="1.1"
xmlns="http://www.w3.org/2000/svg"
>
<path d={selectedItem.icon} fill={selectedItem.iconColor}></path>
</svg>
);
selectedItem.originalIcon = svg_pin;
}
return selectedItem;
};
const chipStyle = {
backgroundColor: "#3d3f43",
marginRight: 5,
@@ -427,292 +137,6 @@ const chipStyle = {
color: "white",
};
export const collapseField = (field, inputdata) => {
if (field === undefined || field === null) {
return true
}
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
return false
}
if (field.name === "headers" || field.name === "cookies") {
return true
}
if (field.name === "result") {
return false
}
if (field.type === "array") {
return true
}
// If more than 10 keys in object, collapse
if (field.type === "object") {
if (Object.keys(field.src).length > 7) {
return true
}
}
return false
}
export const HandleJsonCopy = (base, copy, base_node_name) => {
if (typeof copy.name === "string") {
copy.name = copy.name.replaceAll(" ", "_");
}
//lol
if (typeof base === 'object' || typeof base === 'dict') {
base = JSON.stringify(base)
}
if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") {
base_node_name = "exec"
}
//console.log("COPY: ", base_node_name, copy);
//var newitem = JSON.parse(base);
var newitem = validateJson(base).result
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
for (let copykey in copy.namespace) {
if (copy.namespace[copykey].includes("Results for")) {
continue;
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.namespace[copykey]];
if (!isNaN(copy.namespace[copykey])) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.namespace[copykey];
}
}
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.name];
if (!isNaN(copy.name)) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.name;
}
}
to_be_copied = to_be_copied.replaceAll(" ", "_");
console.log("COPY: ", to_be_copied);
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
//console.log("NAVIGATOR: ", navigator);
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(to_be_copied);
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
//console.log("COPYING!");
toast("Copied JSON path to clipboard.")
} else {
console.log("Couldn't find element ", elementName);
}
}
export const handleReactJsonClipboard = (copy) => {
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
if (
copy.namespace !== undefined &&
copy.name !== undefined &&
copy.src !== undefined
) {
copy = copy.src;
}
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
var stringified = JSON.stringify(copy);
if (stringified.startsWith('"') && stringified.endsWith('"')) {
stringified = stringified.substring(1, stringified.length - 1);
}
navigator.clipboard.writeText(stringified);
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
console.log("COPYING!");
toast.success("Copied Value, NOT json path.")
} else {
console.log("Failed to copy from " + elementName + ": ", copyText);
}
}
export const validateJson = (showResult) => {
if (showResult === undefined || showResult === null) {
return {
valid: false,
result: "",
}
}
if (typeof showResult === 'string') {
showResult = showResult.split(" False").join(" false")
showResult = showResult.split(" True").join(" true")
showResult.replaceAll("False,", "false,")
showResult.replaceAll("True,", "true,")
}
if (typeof showResult === "object" || typeof showResult === "array") {
return {
valid: true,
result: showResult,
}
}
if (showResult[0] === "\"") {
return {
valid: false,
result: showResult,
}
}
var jsonvalid = true
try {
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false
return {
valid: jsonvalid,
result: showResult,
};
}
} catch (e) {
try {
showResult = showResult.split("'").join('"');
if (!showResult.includes("{") && !showResult.includes("[")) {
jsonvalid = false;
}
} catch (e) {
jsonvalid = false;
}
}
var result = showResult;
try {
result = jsonvalid ? JSON.parse(showResult, {"storeAsString": true}) : showResult;
} catch (e) {
////console.log("Failed parsing JSON even though its valid: ", e)
jsonvalid = false;
}
if (jsonvalid === false) {
if (typeof showResult === 'string') {
showResult = showResult.trim()
}
try {
var newstr = showResult.replaceAll("'", '"')
// Basic workarounds for issues with Python Dicts -> JSON
if (newstr.includes(": None")) {
newstr = newstr.replaceAll(": None", ': null')
}
if (newstr.includes("[\"{") && newstr.includes("}\"]")) {
newstr = newstr.replaceAll("[\"{", '[{')
newstr = newstr.replaceAll("}\"]", '}]')
}
if (newstr.includes("{\"[") && newstr.includes("]\"}")) {
newstr = newstr.replaceAll("{\"[", '[{')
newstr = newstr.replaceAll("]\"}", '}]')
}
result = JSON.parse(newstr)
jsonvalid = true
} catch (e) {
//console.log("Failed parsing JSON even though its valid (2): ", e)
jsonvalid = false
}
}
if (jsonvalid && typeof result === "number") {
jsonvalid = false
}
// This is where we start recursing
if (jsonvalid) {
// Check fields if they can be parsed too
try {
for (const [key, value] of Object.entries(result)) {
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) {
//console.log("CHECKING STRING: ", value)
const inside_result = validateJson(value)
if (inside_result.valid) {
//console.log("INSIDE RESULT: ", inside_result.result)
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key] = newres
} else {
result[key] = inside_result.result
}
}
} else {
// Usually only reaches here if raw array > dict > value
if (typeof showResult !== "array") {
for (const [subkey, subvalue] of Object.entries(value)) {
if (typeof subvalue === "string" && (subvalue.startsWith("{") || subvalue.startsWith("["))) {
const inside_result = validateJson(subvalue)
if (inside_result.valid) {
if (typeof inside_result.result === "string") {
const newres = JSON.parse(inside_result.result)
result[key][subkey] = newres
} else {
result[key][subkey] = inside_result.result
}
}
}
}
}
}
}
} catch (e) {
//console.log("Failed parsing inside json subvalues: ", e)
}
}
return {
valid: jsonvalid,
result: result,
};
};
//Custom hook for handling styling of the dropzone
+174 -44
View File
@@ -102,6 +102,7 @@ import {
List as ListIcon,
Publish as PublishIcon,
GetApp as GetAppIcon,
Image as ImageIcon,
} from "@mui/icons-material";
// Additional Components
@@ -139,8 +140,115 @@ const getCookie = (name) => {
return "";
};
export const HandleJsonCopy = (base, copy, base_node_name) => {
if (typeof copy.name === "string") {
copy.name = copy.name.replaceAll(" ", "_");
}
//lol
if (typeof base === 'object' || typeof base === 'dict') {
base = JSON.stringify(base)
}
if (base_node_name === "execution_argument" || base_node_name === "Runtime Argument") {
base_node_name = "exec"
}
//console.log("COPY: ", base_node_name, copy);
//var newitem = JSON.parse(base);
var newitem = validateJson(base).result
var to_be_copied = "$" + base_node_name?.toLowerCase()?.replaceAll(" ", "_");
for (let copykey in copy.namespace) {
if (copy.namespace[copykey].includes("Results for")) {
continue;
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.namespace[copykey]];
if (!isNaN(copy.namespace[copykey])) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.namespace[copykey];
}
}
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.name];
if (!isNaN(copy.name)) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.name;
}
}
to_be_copied = to_be_copied.replaceAll(" ", "_");
console.log("COPY: ", to_be_copied);
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
//console.log("NAVIGATOR: ", navigator);
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(to_be_copied).catch(() => {
const temp = document.createElement("textarea");
temp.value = to_be_copied;
document.body.appendChild(temp);
temp.select();
document.execCommand("copy");
document.body.removeChild(temp);
});
//console.log("COPYING!");
toast("Copied JSON path to clipboard.")
} else {
console.log("Couldn't find element ", elementName);
}
}
export const handleReactJsonClipboard = (copy) => {
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
if (
copy.namespace !== undefined &&
copy.name !== undefined &&
copy.src !== undefined
) {
copy = copy.src;
}
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
var stringified = JSON.stringify(copy);
if (stringified.startsWith('"') && stringified.endsWith('"')) {
stringified = stringified.substring(1, stringified.length - 1);
}
navigator.clipboard.writeText(stringified).catch(() => {
const temp = document.createElement("textarea");
temp.value = stringified;
document.body.appendChild(temp);
temp.select();
document.execCommand("copy");
document.body.removeChild(temp);
});
console.log("COPYING!");
toast.success("Copied Value, NOT json path.")
} else {
console.log("Failed to copy from " + elementName + ": ", copyText);
}
}
@@ -150,7 +258,7 @@ const getCookie = (name) => {
export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs.
const iconList = [
{ key: "cases", values: ["cases", "ticket", "alert"] },
{ key: "cases", values: ["cases", "ticket", "tickets", "alert"] },
{ key: "cache_add", values: ["set_cache"] },
{ key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter"] },
@@ -181,7 +289,7 @@ export const GetIconInfo = (action) => {
key: "repeat",
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
},
{ key: "execute", values: ["execute", "run", "play", "raise"] },
{ key: "execute", values: ["execute", "run", "play", "raise", "control", "ctrl"] },
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
{ key: "inflate", values: ["inflate", "pack", "compress"] },
{
@@ -211,7 +319,6 @@ export const GetIconInfo = (action) => {
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
{ key: "eradication", values: ["eradication", "edr", "xdr", "sigma", "yara",] },
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] },
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] },
{ key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
{
key: "send",
@@ -235,7 +342,8 @@ export const GetIconInfo = (action) => {
"passwd",
"protect",
],
}
},
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", ] },
];
var selectedKey = ""
@@ -482,17 +590,23 @@ export const GetIconInfo = (action) => {
return selectedItem;
};
export const collapseField = (field) => {
export const collapseField = (field, inputdata) => {
if (field === undefined || field === null) {
return true
}
if (field.namespace !== undefined && field.namespace !== null && field.namespace.length === 1) {
return false
}
if (field.name === "headers" || field.name === "cookies") {
return true
}
if (field.name === "result") {
return false
}
if (field.type === "array") {
return true
}
@@ -700,7 +814,8 @@ const Workflows2 = (props) => {
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true" || isCloud)
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true")
const [showWorkflowImages, setShowWorkflowImages] = React.useState(localStorage?.getItem("showWorkflowImages") === "true")
const imgSize = 60;
@@ -2158,7 +2273,7 @@ const Workflows2 = (props) => {
position: "relative",
borderRadius: "8px",
// backgroundColor: "#212121",
padding: "15px 15px 0px 20px",
padding: "15px 15px 0px 30px",
};
const gridContainer = {
@@ -2177,7 +2292,6 @@ const Workflows2 = (props) => {
justifyContent: "space-between",
fontFamily: theme.typography?.fontFamily,
textAlign: "center",
margin: "auto",
};
const exportAllWorkflows = (allWorkflows) => {
@@ -2959,7 +3073,7 @@ const Workflows2 = (props) => {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
imageStyle.border = foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null
imageStyle.border = foundOrg.id === userdata.active_org.id ? `2px solid ${boxColor}` : null
image =
@@ -2997,10 +3111,10 @@ const Workflows2 = (props) => {
relevantTrigger = trigger
if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}`
imageStyle.border = `2px solid ${green}`
break
} else {
imageStyle.border = `3px solid ${red}`
imageStyle.border = `2px solid ${red}`
}
@@ -3012,10 +3126,10 @@ const Workflows2 = (props) => {
relevantTrigger = trigger
if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}`
imageStyle.border = `2px solid ${green}`
break
} else {
imageStyle.border = `3px solid ${red}`
imageStyle.border = `2px solid ${red}`
}
}
@@ -3081,13 +3195,12 @@ const Workflows2 = (props) => {
}
//const isPublicWorkflow = data?.objectID === undefined || data?.objectID === null
const foundImage = data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
const foundImage = currTab !== 2 && showWorkflowImages === false ? "" : data?.image_url === undefined || data?.image_url === null || data?.image_url === "" ? data?.image : data?.image_url
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
return (
<div
id={`workflowbox-${data.id}`}
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? `2px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily, overflow: "hidden", }}
style={{ width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : "inherit", borderBottom: isDistributed || hasSuborgs ? `1px solid ${theme.palette.distributionColor}` : "inherit", borderRadius: theme.palette?.borderRadius, backgroundColor: "#212121", fontFamily: theme.typography?.fontFamily, overflow: "hidden", }}
>
{foundImage?.length > 0 ?
@@ -3140,26 +3253,7 @@ const Workflows2 = (props) => {
style={{ display: "flex", flexDirection: "column", width: "100%", fontFamily: theme.typography?.fontFamily }}
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
{currTab === 2 ? null :
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
<div
style={{ cursor: "" }}
onClick={() => {
//navigate("/admin")
}}
>
{image?.includes("data:image") ?
<img
alt={orgName}
src={image}
style={imageStyle}
/>
:
image
}
</div>
</Tooltip>
}
<Tooltip arrow
onMouseEnter={() => {
@@ -3228,7 +3322,7 @@ const Workflows2 = (props) => {
<Typography
style={{
textAlign: "center",
textAlign: "left",
marginBottom: 0,
paddingBottom: 0,
fontSize: 22,
@@ -3239,8 +3333,9 @@ const Workflows2 = (props) => {
fontWeight: 500,
minWidth: 375,
maxWidth: 375,
margin: "auto",
overflow: "hidden",
color: "rgba(255,255,255,0.85)",
}}
>
<Link
@@ -3431,14 +3526,13 @@ const Workflows2 = (props) => {
style={{
justifyContent: "left",
overflow: "hidden",
marginTop: 13,
marginTop: 8,
maxHeight: 35,
fontFamily: theme.typography?.fontFamily,
textAlign: "center",
textAlign: "left",
minWidth: 200,
maxWidth: 200,
margin: "auto",
}}
>
{data.tags !== undefined && data.tags !== null
@@ -3478,7 +3572,7 @@ const Workflows2 = (props) => {
{(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ?
<Tooltip title="Edit Form" placement="top">
<div style={{ position: "absolute", top: 80, right: 8, }}>
<div style={{ position: "absolute", top: 100, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -3521,6 +3615,27 @@ const Workflows2 = (props) => {
</Tooltip>
: null}
{currTab === 2 ? null :
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
<div
style={{ position: "absolute", top: 70, right: -2, cursor: "" }}
onClick={() => {
//navigate("/admin")
}}
>
{image?.includes("data:image") ?
<img
alt={orgName}
src={image}
style={imageStyle}
/>
:
image
}
</div>
</Tooltip>
}
</Grid>
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
@@ -5305,6 +5420,21 @@ const Workflows2 = (props) => {
</IconButton>
</Tooltip>
<Tooltip title="Show/Hide Workflow image" placement="top">
<IconButton
style={currTab === 2 ? iconButtonDisabledStyle : {...iconButtonStyle, color: showWorkflowImages ? "#1a1a1a" : theme.palette.text.primary, background: showWorkflowImages ? theme.palette.primary.main : theme.palette.platformColor}}
onClick={() => {
const newView = !showWorkflowImages
localStorage.setItem("showWorkflowImages", newView)
setShowWorkflowImages(!showWorkflowImages)
}}
disabled={currTab === 2}
>
<ImageIcon />
</IconButton>
</Tooltip>
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
<IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}