Merging fixes for 2.1.0

This commit is contained in:
Frikky
2025-08-26 16:07:44 +02:00
parent 15bf1011e7
commit 6c60dd3bcc
20 changed files with 2115 additions and 598 deletions
+113 -42
View File
@@ -91,10 +91,13 @@ const Billing = memo((props) => {
const [currentTab, setCurrentTab] = useState(0) const [currentTab, setCurrentTab] = useState(0)
const [allChildOrgs, setAllChildOrgs] = useState([]) const [allChildOrgs, setAllChildOrgs] = useState([])
const [allChildOrgsStats, setAllChildOrgsStats] = useState([]) const [allChildOrgsStats, setAllChildOrgsStats] = useState([])
const [statistics, setStatistics] = useState([])
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
useEffect(() => { useEffect(() => {
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) { if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
const percentage = ((userdata.app_execution_usage + userdata.app_executions_suborgs) / userdata.app_execution_limit) * 100; const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
setCurrentAppRunsInPercentage(Math.round(percentage)); setCurrentAppRunsInPercentage(Math.round(percentage));
setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage - userdata.app_executions_suborgs); setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage - userdata.app_executions_suborgs);
} }
@@ -102,7 +105,7 @@ const Billing = memo((props) => {
if (userdata?.id?.length > 0 && isLoggedIn === false){ if (userdata?.id?.length > 0 && isLoggedIn === false){
setIsLoggedIn(true) setIsLoggedIn(true)
} }
}, [userdata]); }, [monthlyAppRunsParent, monthlyAllSuborgExecutions, userdata]);
const [BillingEmail, setBillingEmail] = useState(selectedOrganization?.Billing?.Email); const [BillingEmail, setBillingEmail] = useState(selectedOrganization?.Billing?.Email);
@@ -199,6 +202,48 @@ const Billing = memo((props) => {
} }
}, []) }, [])
const getStats = (orgid) => {
if (orgid === undefined || orgid === null) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgid}/stats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
setStatistics(responseJson);
})
.catch((error) => {
console.log("error: ", error)
});
}
useEffect(() => {
if (selectedOrganization && selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id);
}
}, [selectedOrganization]);
const paperStyle = { const paperStyle = {
padding: 20, padding: 20,
// maxWidth: 400, // maxWidth: 400,
@@ -1973,7 +2018,13 @@ const Billing = memo((props) => {
}; };
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
useEffect(() => {
if (isChildOrg && currentTab === 0) {
setCurrentTab(1);
}
}, [isChildOrg, currentTab]);
return ( return (
<Wrapper clickedFromOrgTab={clickedFromOrgTab}> <Wrapper clickedFromOrgTab={clickedFromOrgTab}>
<div style={{ height: "100%", width: "100%"}}> <div style={{ height: "100%", width: "100%"}}>
@@ -2373,8 +2424,8 @@ const Billing = memo((props) => {
<Typography variant="body2" color="textSecondary" style={{fontSize: 16,}}> <Typography variant="body2" color="textSecondary" style={{fontSize: 16,}}>
We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at {supportEmail}. We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at {supportEmail}.
</Typography>We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at </Typography>We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 5, }}> <div style={{ display: 'flex', width: '50%', flexDirection: 'row', marginTop: 5, }}>
{billingInfo.subscription !== undefined && billingInfo.subscription !== null ? ( {/* {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
isChildOrg ? null : ( isChildOrg ? null : (
<ConsultationManagement <ConsultationManagement
globalUrl={globalUrl} globalUrl={globalUrl}
@@ -2382,7 +2433,7 @@ const Billing = memo((props) => {
selectedOrganization={selectedOrganization} selectedOrganization={selectedOrganization}
/> />
) )
) : null} ) : null} */}
<TrainingService /> <TrainingService />
</div> </div>
</div> </div>
@@ -2410,17 +2461,17 @@ const Billing = memo((props) => {
}} }}
/> />
<Typography style={{marginTop: 10, fontSize: 16,}} color="textSecondary"> <Typography style={{marginTop: 10, fontSize: 16,}} color="textSecondary">
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{userdata.app_execution_usage + userdata.app_executions_suborgs}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs. You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs this month.
</Typography> </Typography>
{userdata?.active_org?.creator_org?.length > 0 ? null : {userdata?.active_org?.creator_org?.length > 0 ? null :
( (
<> <>
<Typography color="textSecondary" style={{ marginTop: 20, fontSize: 16 }}> <Typography color="textSecondary" style={{ marginTop: 20, fontSize: 16 }}>
Parent Organization App Executions: <strong>{userdata.app_execution_usage}</strong> Parent Organization App Executions: <strong>{monthlyAppRunsParent}</strong>
</Typography> </Typography>
<Typography color="textSecondary" style={{ fontSize: 16 }}> <Typography color="textSecondary" style={{ fontSize: 16 }}>
Sub-Organization App Executions: <strong>{userdata.app_executions_suborgs || "N/A"}</strong> Sub-Organization App Executions: <strong>{monthlyAllSuborgExecutions || "N/A"}</strong>
</Typography> </Typography>
</> </>
)} )}
@@ -2602,12 +2653,7 @@ const Billing = memo((props) => {
<Tabs <Tabs
value={currentTab} value={currentTab}
onChange={(event, newValue) => { onChange={(event, newValue) => {
setCurrentTab(-1) setCurrentTab(newValue)
// Force re-render
setTimeout(() => {
setCurrentTab(newValue)
}, 100);
}} }}
style={{ marginTop: 20 }} style={{ marginTop: 20 }}
TabIndicatorProps={{ TabIndicatorProps={{
@@ -2619,52 +2665,66 @@ const Billing = memo((props) => {
} }
}} }}
> >
<Tab {isChildOrg ? null :
label="Parent Organization" <Tab
label="All Organization Stats"
style={{ textTransform: 'none',}} style={{ textTransform: 'none',}}
value={0} value={0}
/>}
<Tab
label={isChildOrg ? "Organization Stats" : "Parent Organization Stats"}
style={{ textTransform: 'none',}}
value={1}
/> />
{isChildOrg ? null :
{isCloud ?
<Tab
label="Cloud-Synced Stats"
style={{ textTransform: 'none', }}
value={1}
/>
: null}
<Tab <Tab
label="Child Organization Stats" label="Child Organization Stats"
disabled={isChildOrg} disabled={isChildOrg}
style={{ textTransform: 'none', }} style={{ textTransform: 'none', }}
value={2} value={2}
/> />}
{isCloud ?
<Tab
label="Cloud-Synced Stats"
style={{ textTransform: 'none', }}
value={3}
/>
: null}
</Tabs> </Tabs>
<div style={{paddingBottom: 200, minHeight: 750, }}> <div style={{paddingBottom: 200, minHeight: 750, }}>
{currentTab === 0 ? {
<div style={{ marginTop: 30,}}> currentTab === 0 ?
<BillingStats <BillingStats
isCloud={isCloud} isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab} clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl} globalUrl={globalUrl}
selectedOrganization={selectedOrganization} selectedOrganization={selectedOrganization}
userdata={userdata} userdata={userdata}
/> statistics={statistics}
</div> monthlyAppRunsParent={monthlyAppRunsParent}
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
setMonthlyAllSuborgExecutions={setMonthlyAllSuborgExecutions}
setMonthlyAppRunsParent={setMonthlyAppRunsParent}
currentTab={currentTab}
/>
: currentTab === 1 ? : currentTab === 1 ?
<div style={{ marginTop: 30,}}> <div>
<BillingStats <BillingStats
isCloud={isCloud} isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab} clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl} globalUrl={globalUrl}
selectedOrganization={selectedOrganization} selectedOrganization={selectedOrganization}
userdata={userdata} userdata={userdata}
statistics={statistics}
syncStats={true} monthlyAppRunsParent={monthlyAppRunsParent}
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
setMonthlyAllSuborgExecutions={setMonthlyAllSuborgExecutions}
setMonthlyAppRunsParent={setMonthlyAppRunsParent}
currentTab={currentTab}
/> />
</div> </div>
: : currentTab === 2 ?
<BillingStatsChildOrg <BillingStatsChildOrg
isCloud={isCloud} isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab} clickedFromOrgTab={clickedFromOrgTab}
@@ -2675,7 +2735,18 @@ const Billing = memo((props) => {
setAllChildOrgs={setAllChildOrgs} setAllChildOrgs={setAllChildOrgs}
allChildOrgsStats={allChildOrgsStats} allChildOrgsStats={allChildOrgsStats}
setAllChildOrgsStats={setAllChildOrgsStats} setAllChildOrgsStats={setAllChildOrgsStats}
currentTab={currentTab}
/> />
:
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
currentTab={currentTab}
syncStats={true}
/>
} }
</div> </div>
</span> </span>
+114 -140
View File
@@ -45,6 +45,12 @@ const AppStats = (defaultprops) => {
inputWorkflows, inputWorkflows,
clickedFromOrgTab, clickedFromOrgTab,
syncStats, syncStats,
statistics,
monthlyAppRunsParent,
setMonthlyAppRunsParent,
monthlyAllSuborgExecutions,
setMonthlyAllSuborgExecutions,
currentTab
} = defaultprops; } = defaultprops;
const [keys, setKeys] = useState([]) const [keys, setKeys] = useState([])
@@ -57,7 +63,6 @@ const AppStats = (defaultprops) => {
const [endTime, setEndTime] = useState("") const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("") const [startTime, setStartTime] = useState("")
const [statistics, setStatistics] = useState(undefined);
const [filteredStatistics, setFilteredStatistics] = useState(undefined); const [filteredStatistics, setFilteredStatistics] = useState(undefined);
const [apprunCost, setApprunCost] = useState(0) const [apprunCost, setApprunCost] = useState(0)
@@ -78,6 +83,11 @@ const AppStats = (defaultprops) => {
} }
}, []) }, [])
useEffect(() => {
if (statistics && statistics?.org_id?.length > 0) {
handleDataSetting(statistics, "day")
}
}, [statistics])
const getWorkflowStats = async (workflow, startTime, endTime) => { const getWorkflowStats = async (workflow, startTime, endTime) => {
@@ -219,6 +229,7 @@ const AppStats = (defaultprops) => {
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
if (statistics[statKey] === undefined || statistics[statKey] === null) { if (statistics[statKey] === undefined || statistics[statKey] === null) {
setFilteredStatistics(statistics) setFilteredStatistics(statistics)
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
return return
} }
@@ -265,20 +276,27 @@ const AppStats = (defaultprops) => {
} }
} }
// Make a date at the 1st of the current month // Make a date at the 1st of the current month - only when no start time is selected
var foundstarttime = (new Date()) var foundstarttime = (new Date())
foundstarttime.setDate(1)
if (startTime !== "" && startTime !== undefined && startTime !== null) { if (startTime !== "" && startTime !== undefined && startTime !== null) {
foundstarttime = startTime foundstarttime = new Date(startTime)
// Set to start of day to include the entire start date
foundstarttime.setHours(0, 0, 0, 0)
} else {
// Default to 1st of current month when no start time is selected
foundstarttime.setDate(1)
foundstarttime.setHours(0, 0, 0, 0)
} }
// Set to tomorrow by default // Set end time properly
var foundendtime = (new Date()) var foundendtime = (new Date())
foundendtime.setDate(foundendtime.getDate() + 1)
// Check if endtime is after the daily statistics["date"] string
if (endTime !== "" && endTime !== undefined && endTime !== null) { if (endTime !== "" && endTime !== undefined && endTime !== null) {
foundendtime = endTime foundendtime = new Date(endTime)
// Set to end of day to include the entire end date
foundendtime.setHours(23, 59, 59, 999)
} else {
// Default to current date when no end time is selected
foundendtime.setHours(23, 59, 59, 999)
} }
// Check if start time is before the daily statistics["date"] string // Check if start time is before the daily statistics["date"] string
@@ -290,8 +308,20 @@ const AppStats = (defaultprops) => {
} }
const date = new Date(item["date"]) const date = new Date(item["date"])
if (date >= foundstarttime) { // Normalize the date to start of day for comparison
if (date <= foundendtime) { const normalizedDate = new Date(date)
normalizedDate.setHours(0, 0, 0, 0)
// Normalize foundstarttime for comparison
const normalizedStartTime = new Date(foundstarttime)
normalizedStartTime.setHours(0, 0, 0, 0)
// Normalize foundendtime for comparison
const normalizedEndTime = new Date(foundendtime)
normalizedEndTime.setHours(0, 0, 0, 0)
if (normalizedDate >= normalizedStartTime) {
if (normalizedDate <= normalizedEndTime) {
newlist.push(item) newlist.push(item)
} }
} }
@@ -326,6 +356,10 @@ const AppStats = (defaultprops) => {
workflowexecutions += item["workflow_executions"] workflowexecutions += item["workflow_executions"]
appexecutions += item["app_executions"] appexecutions += item["app_executions"]
if (currentTab === 0) {
appexecutions += (item["child_app_executions"] ?? 0)
}
estimatedcost += (item["app_executions"] * invocationCost) estimatedcost += (item["app_executions"] * invocationCost)
} }
@@ -343,7 +377,16 @@ const AppStats = (defaultprops) => {
} }
setFilteredStatistics(tmpstats) setFilteredStatistics(tmpstats)
handleDataSetting(tmpstats, "day") handleDataSetting(tmpstats, "day")
// if we have done monthly reset than only show monthly app runs as current month app run
const currentMonth = new Date().getMonth() + 1
if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAppRunsParent(statistics["monthly_app_executions"])
}
if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
}
if (workflows !== undefined && workflows !== null && workflows.length > 0) { if (workflows !== undefined && workflows !== null && workflows.length > 0) {
@@ -429,7 +472,7 @@ const AppStats = (defaultprops) => {
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) { if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({ childorgappRuns["data"].push({
key: new Date(item["date"]), key: new Date(item["date"]),
data: inputdata["child_app_executions"] data: item["child_app_executions"]
}) })
} }
@@ -449,40 +492,45 @@ const AppStats = (defaultprops) => {
} }
} }
// Adds data for today // Only add today's data if endTime is not set or if today falls within the selected date range
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { const today = new Date()
appRuns["data"].push({ const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
key: new Date(), (new Date(endTime) >= today.setHours(0, 0, 0, 0))
data: inputdata["daily_app_executions"]
})
appcostRuns["data"].push({ if (shouldAddTodayData) {
key: new Date(), // Adds data for today
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2) if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
}) appRuns["data"].push({
} key: new Date(),
data: inputdata["daily_app_executions"]
})
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) { appcostRuns["data"].push({
childorgappRuns["data"].push({ key: new Date(),
key: new Date(), data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
data: inputdata["daily_child_app_executions"] })
}) }
//setApprunCosts(appcostRuns) if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
} childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) { if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({ workflowRuns["data"].push({
key: new Date(), key: new Date(),
data: inputdata["daily_workflow_executions"] data: inputdata["daily_workflow_executions"]
}) })
} }
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) { if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({ subflowRuns["data"].push({
key: new Date(), key: new Date(),
data: inputdata["daily_subflow_executions"] data: inputdata["daily_subflow_executions"]
}) })
}
} }
// Only for parent orgs // Only for parent orgs
@@ -494,49 +542,8 @@ const AppStats = (defaultprops) => {
setWorkflowRuns(workflowRuns) setWorkflowRuns(workflowRuns)
setAppruns(appRuns) setAppruns(appRuns)
setApprunCosts(appcostRuns) setApprunCosts(appcostRuns)
}
const getStats = (orgid) => {
if (orgid === undefined || orgid === null) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgid}/stats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson["success"] === false) {
return
}
setStatistics(responseJson)
handleDataSetting(responseJson, "day")
})
.catch((error) => {
console.log("error: ", error)
});
} }
useEffect(() => {
if(selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id)
}
}, [selectedOrganization])
const paperStyle = { const paperStyle = {
textAlign: "center", textAlign: "center",
padding: "40px", padding: "40px",
@@ -656,15 +663,19 @@ const AppStats = (defaultprops) => {
] ]
const data = ( const data = (
<div className="content" style={{width: "100%", margin: "auto", }}> <div className="content" style={{width: "100%", margin: "auto", marginTop: 20}}>
<Typography style={{margin: "auto", marginLeft: 10, marginBottom: 20, fontSize: 16}} color="textSecondary"> <Typography style={{margin: "auto", marginLeft: 10, marginBottom: 20, fontSize: 16}} color="textSecondary">
All shown statistics are gathered from <a All shown statistics are gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/stats`} href={`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/stats`}
target="_blank" target="_blank"
style={{ textDecoration: "none", color: theme.palette.linkColor,}} style={{ textDecoration: "none", color: theme.palette.linkColor,}}
>Your Organisation Statistics. </a> >Your Organisation Statistics. </a>
It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. <b>The billing tracker is in Beta, and is always calculated manually before being invoiced.</b>
{currentTab === 0 ?
<span>
All Organization app runs are calculated base on addition of parent org app runs + all child org app runs.
</span>: <span>It exists to give you more insight into your workflows, and to
understand your utilization of the Shuffle platform.{" "}</span>}
<br style={{}}/> <br style={{}}/>
{syncStats !== true ? null : {syncStats !== true ? null :
"PS: You are currently looking at data from your onprem synced org"} "PS: You are currently looking at data from your onprem synced org"}
@@ -675,7 +686,7 @@ const AppStats = (defaultprops) => {
{filteredStatistics !== undefined ? {filteredStatistics !== undefined ?
<div style={{flex: 1, display: "flex", textAlign: "center",}}> <div style={{flex: 1, display: "flex", textAlign: "center",}}>
{syncStats == true ? null : {/* {syncStats == true ? null :
<Tooltip title={ <Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}. The cost of app runs in the selected period based on {filteredStatistics.monthly_app_executions} App Runs. These numbers do not exclude your included 10.000/month or {includedExecutions} App Runs per month. App Run cost: ${invocationCost}.
@@ -695,7 +706,7 @@ const AppStats = (defaultprops) => {
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
} } */}
{syncStats === true ? null : {syncStats === true ? null :
<Tooltip title={ <Tooltip title={
@@ -714,7 +725,7 @@ const AppStats = (defaultprops) => {
</Tooltip> </Tooltip>
} }
{syncStats === true ? null : {syncStats === true || currentTab === 0 ? null :
<Tooltip title={ <Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
Workflow runs in the selected period Workflow runs in the selected period
@@ -731,7 +742,7 @@ const AppStats = (defaultprops) => {
</Tooltip> </Tooltip>
} }
{syncStats === true ? null : {/* {syncStats === true ? null :
<Tooltip title={ <Tooltip title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}. Estimated cost to be billed at the end of the current month. Subtracted contractually included app runs. Actual cost month to date: ${monthToDateCost}. App Run cost: ${invocationCost}.
@@ -746,15 +757,9 @@ const AppStats = (defaultprops) => {
</Typography> </Typography>
</Box> </Box>
</Tooltip> </Tooltip>
} } */}
</div> <LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
: null} <div style={{ display: "flex", flexDirection: "column", gap: "10px", justifyContent: 'center', alignItems: 'flex-start', paddingTop: 10 }}>
</div>
{clickedFromOrgTab? (
<LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
<div style={{ display: "flex", flexDirection: "row", width: "100%", gap: "10px", justifyContent: 'center', alignItems: 'center', paddingTop: 10 }}>
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -878,64 +883,33 @@ const AppStats = (defaultprops) => {
</div> </div>
</div> </div>
</LocalizationProvider> </LocalizationProvider>
):(
<LocalizationProvider dateAdapter={AdapterDayjs} style={{flex: 1, }}>
<div style={{display: "flex", flexDirection: "column", }}>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search from"
format="YYYY-MM-DD HH:mm:ss"
value={startTime}
onChange={handleStartTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search until"
format="YYYY-MM-DD HH:mm:ss"
value={endTime}
onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</div> </div>
</LocalizationProvider> : null}
)} </div>
</div> </div>
{appRuns === undefined ? {appRuns === undefined ?
null null
: :
<LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"App Runs - Current Org"}/> <LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"App Runs - Current Org"} border={false}/>
} }
{childOrgsAppRuns === undefined ? {childOrgsAppRuns === undefined || currentTab === 1 ?
null null
: :
<LineChartWrapper keys={childOrgsAppRuns} height={300} width={"100%"} inputname={"Child Org App Runs"}/> <LineChartWrapper keys={childOrgsAppRuns} height={300} width={"100%"} inputname={"Child Org App Runs"} border={false} />
} }
{workflowRuns === undefined ? {workflowRuns === undefined || currentTab === 0?
null null
: :
<LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"}/> <LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"} border={false} />
} }
{subflowRuns === undefined ? {subflowRuns === undefined || currentTab === 0 ?
null null
: :
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/> <LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"} border={false} />
} }
{/*appRunCosts === undefined ? {/*appRunCosts === undefined ?
@@ -944,7 +918,7 @@ const AppStats = (defaultprops) => {
<LineChartWrapper keys={appRunCosts} height={300} width={"100%"} inputname={"Apprun cost - Cost per day"}/> <LineChartWrapper keys={appRunCosts} height={300} width={"100%"} inputname={"Apprun cost - Cost per day"}/>
*/} */}
{syncStats === true ? null : {syncStats === true || currentTab === 0 ? null :
<div style={{height: 150+resultRows.length * 25, padding: "10px 0px 10px 0px", }}> <div style={{height: 150+resultRows.length * 25, padding: "10px 0px 10px 0px", }}>
{resultLoading ? {resultLoading ?
<div style={{margin: "auto", alignItems: "center", width: 350, height: "100%", }}> <div style={{margin: "auto", alignItems: "center", width: 350, height: "100%", }}>
@@ -1000,7 +974,7 @@ const AppStats = (defaultprops) => {
) )
const dataWrapper = ( const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div> <div style={{ maxWidth: 1366, margin: "auto", }}>{data}</div>
); );
return dataWrapper; return dataWrapper;
+102 -12
View File
@@ -35,6 +35,7 @@ import {
InputLabel, InputLabel,
Pagination, Pagination,
PaginationItem, PaginationItem,
Avatar,
} from "@mui/material"; } from "@mui/material";
import { import {
@@ -134,7 +135,7 @@ const CacheView = memo((props) => {
// Direct category migration from ../components/Files.jsx // Direct category migration from ../components/Files.jsx
const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [selectAllChecked, setSelectAllChecked] = React.useState(false)
const [renderTextBox, setRenderTextBox] = React.useState(false); const [renderTextBox, setRenderTextBox] = React.useState(false);
const [datastoreCategories, setDatastoreCategories] = React.useState(["default"]); const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
const [selectedCategory, setSelectedCategory] = React.useState("default"); const [selectedCategory, setSelectedCategory] = React.useState("default");
const [selectedFileId, setSelectedFileId] = React.useState(""); const [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("") const [updateToThisCategory, setUpdateToThisCategory] = useState("")
@@ -322,7 +323,6 @@ const CacheView = memo((props) => {
} }
var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache` var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache`
if (category !== undefined && category !== null && category !== "default" && category !== "") { if (category !== undefined && category !== null && category !== "default" && category !== "") {
url += "?category=" + category.replaceAll(" ", "_") url += "?category=" + category.replaceAll(" ", "_")
} else { } else {
@@ -398,7 +398,7 @@ const CacheView = memo((props) => {
} }
} }
if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 1 && datastoreCategories[0] === "default") { if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 2 && datastoreCategories[0] === "default") {
var newcategories = ["default"] var newcategories = ["default"]
for (var key in responseJson.keys) { for (var key in responseJson.keys) {
var foundcategory = responseJson.keys[key].category var foundcategory = responseJson.keys[key].category
@@ -585,7 +585,7 @@ const CacheView = memo((props) => {
}) })
.then((responseJson) => { .then((responseJson) => {
setAddCache(responseJson); setAddCache(responseJson);
toast("New key added Successfully!"); toast.success("New key added!");
listOrgCache(orgId, selectedCategory, 0, pageSize, page); listOrgCache(orgId, selectedCategory, 0, pageSize, page);
setModalOpen(false); setModalOpen(false);
}) })
@@ -699,6 +699,7 @@ const CacheView = memo((props) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
<TextField <TextField
color="primary" color="primary"
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: 0, }} style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: 0, }}
@@ -1461,12 +1462,25 @@ const CacheView = memo((props) => {
sortable: true, sortable: true,
}, },
{ {
width: 600, width: 540,
field: 'value', field: 'value',
filterable: true, filterable: true,
headerName: 'Value', headerName: 'Value',
renderCell: (props) => { renderCell: (props) => {
const data = props.row const data = props.row
if (data?.category?.toLowerCase() === "protected") {
return (
<Typography
variant="body2"
type="password"
style={{maxHeight: 200, overflow: "hidden", }}
>
***************
</Typography>
)
}
const validate = validateJson(data.value) const validate = validateJson(data.value)
return ( return (
@@ -1485,8 +1499,8 @@ const CacheView = memo((props) => {
backgroundColor: theme.palette.platformColor, backgroundColor: theme.palette.platformColor,
border: theme.palette.defaultBorder, border: theme.palette.defaultBorder,
padding: 5, padding: 5,
minWidth: 600, minWidth: 500,
maxHeight: 600, maxHeight: 500,
overflowY: "auto", overflowY: "auto",
}} }}
collapsed={true} collapsed={true}
@@ -1507,6 +1521,74 @@ const CacheView = memo((props) => {
) )
} }
}, },
{
field: 'category',
headerName: 'Category',
description: 'Category for this key.',
width: 75,
filterable: false,
sortable: true,
renderCell: (props) => {
// Return avatar with hover for the category
const data = props.row
const clickCategory = (e) => {
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(data.category)
return
}
setSelectedCategory(data.category)
if (data.category === "all" || data.category === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, data.category, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${data.category}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data.category}`)
}
}
const iconDetails = GetIconInfo({
"app_name": data.category,
"name": data.category,
})
const avatarLetter = (data.category === "" || data.category === "default" ? " " : data.category.charAt(0).toUpperCase())[0]
return (
<Tooltip title={data.category === "" || data.category === "default" ? "No category" : `Category name: ${data.category}`} placement="left">
<Avatar
onClick={(e) => {
clickCategory(e)
}}
style={{
color: "white",
backgroundColor: iconDetails?.iconBackgroundColor || theme.palette.primary.secondary,
marginLeft: 15,
height: 30,
width: 30,
cursor: data.category !== "" && data.category !== "default" ? "pointer" : "default",
}}
variant="rounded"
>
{iconDetails?.originalIcon ?
iconDetails?.originalIcon
:
avatarLetter
}
</Avatar>
</Tooltip>
)
}
},
{ {
field: 'actions', field: 'actions',
headerName: 'Actions', headerName: 'Actions',
@@ -1520,7 +1602,7 @@ const CacheView = memo((props) => {
return ( return (
<span style={{ display: "flex" }}> <span style={{ display: "flex" }}>
{data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined ? {data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined || data?.workflow_id?.length !== 36 ?
<IconButton <IconButton
disabled={data.workflow_id?.length === 0} disabled={data.workflow_id?.length === 0}
style={{}} style={{}}
@@ -1528,7 +1610,7 @@ const CacheView = memo((props) => {
<OpenInNewIcon <OpenInNewIcon
style={{ style={{
color: color:
data.workflow_id?.length !== 0 data.workflow_id?.length === 36
? "#FF8444" ? "#FF8444"
: "grey", : "grey",
}} }}
@@ -1539,6 +1621,7 @@ const CacheView = memo((props) => {
title={"Go to workflow"} title={"Go to workflow"}
style={{}} style={{}}
aria-label={"Download"} aria-label={"Download"}
placement="left"
> >
<span> <span>
<a <a
@@ -1552,13 +1635,13 @@ const CacheView = memo((props) => {
> >
<IconButton <IconButton
disabled={data.workflow_id?.length ===0} disabled={data.workflow_id?.length ===0}
style={{marginLeft: 10}} style={{marginLeft: 0}}
> >
<OpenInNewIcon <OpenInNewIcon
style={{ style={{
width: 24, height: 24, width: 24, height: 24,
color: color:
data.workflow_id?.length !== 0 data.workflow_id?.length === 36
? "#FF8444" ? "#FF8444"
: "grey", : "grey",
}} }}
@@ -1825,6 +1908,13 @@ const CacheView = memo((props) => {
> >
Learn more Learn more
</a> </a>
{selectedCategory === "protected" ?
<div style={{ color: red, }}>
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. This is a basic protection, and is NOT bulletproof.
</div>
: null}
</Typography> </Typography>
</div> </div>
@@ -2244,7 +2334,7 @@ const CacheView = memo((props) => {
setSelectedRows(newSelection); setSelectedRows(newSelection);
}} }}
keepNonExistentRowsSelected={false} keepNonExistentRowsSelected={false}
getRowId={(row) => row.key} getRowId={(row) => `${row?.key}_${row?.category}`}
autoHeight={true} autoHeight={true}
sx={{ sx={{
+111 -74
View File
@@ -23,8 +23,12 @@ import {
import { import {
Rocket as RocketIcon, Rocket as RocketIcon,
FilterAlt as FilterAltIcon, FilterAlt as FilterAltIcon,
Add as AddIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CollectIngestModal = (props) => { const CollectIngestModal = (props) => {
const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props; const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props;
@@ -86,11 +90,13 @@ const CollectIngestModal = (props) => {
} }
const IngestItem = (props) => { const IngestItem = (props) => {
const { type, appCategory, index } = props const { type, appCategory, index, webhook } = props
const [hovering, setHovering] = useState(false); const [hovering, setHovering] = useState(false);
const [selectedApps, setSelectedApps] = useState([]); const [selectedApps, setSelectedApps] = useState([]);
//const [isFinished, setIsFinished] = useState(false);
const [showAppsearch, setShowAppsearch] = useState(false);
const [algoliaOptions, setAlgoliaOptions] = useState([]);
const appname = type const appname = type
const ingestedAmount = 20 const ingestedAmount = 20
@@ -167,7 +173,7 @@ const CollectIngestModal = (props) => {
//<Grid item xs={hovering ? 12 : 5.9} //<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12} <Grid item xs={12}
style={{ style={{
minHeight: hovering ? 250 : 140, minHeight: hovering ? 200 : 200,
maxHeight: hovering ? "auto" : 140, maxHeight: hovering ? "auto" : 140,
cursor: "pointer", cursor: "pointer",
position: "relative", position: "relative",
@@ -176,7 +182,7 @@ const CollectIngestModal = (props) => {
borderRadius: theme.palette.borderRadius, borderRadius: theme.palette.borderRadius,
border: hovering ? `2px solid ${theme.palette.primary.main}` : foundMatchingWorkflow !== null ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`, border: hovering ? `2px solid ${theme.palette.primary.main}` : foundMatchingWorkflow !== null ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`,
textAlign: "center", textAlign: "center",
marginBottom: 5, marginBottom: 10,
overflow: "hidden", overflow: "hidden",
}} }}
@@ -189,86 +195,116 @@ const CollectIngestModal = (props) => {
}} }}
onMouseLeave={() => setHovering(false)} onMouseLeave={() => setHovering(false)}
> >
<div style={{marginTop: 35, marginBottom: 35, }}> <div style={{display: "flex", }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
<Typography variant="h4" style={{marginTop: 10, }}> <div style={{flex: 1, margin: "auto", marginTop: 50, }}>
{appname} <div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
</Typography> {selectedApps.map((app, index) => {
</div> // Show image of each one
return (
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
<Tooltip title={app.name} placement="top">
<img
style={{height: 40, width: 40, borderRadius: 50}}
src={app?.large_image || app?.icon || app?.image || "/static/images/default_app_icon.png"}
/>
</Tooltip>
</div>
)
})}
<div style={{display: "flex", width: 400, margin: "auto", }}> <Tooltip title="Select Apps" placement="top">
{matchingapps.length > 0 ? <IconButton
<Autocomplete style={{marginLeft: 10, marginRight: 50, }}
style={{flex: 1, }} variant="outlined"
multiple color="secondary"
filterSelectedOptions onClick={() => {
options={matchingapps} setShowAppsearch(!showAppsearch)
}}
>
<AddIcon style={{color: theme.palette.primary.main, }} />
</IconButton>
</Tooltip>
</div>
value={selectedApps} {showAppsearch ?
onChange={(event, value) => { <Autocomplete
setSelectedApps(value) style={{flex: 1, maxWidth: 200, minWidth: 200, margin: "auto", marginTop: 10, }}
}} multiple
filterSelectedOptions
options={matchingapps}
getOptionLabel={(option) => { value={selectedApps}
const parsedname = option.name.replaceAll("_", " ") onChange={(event, value) => {
setSelectedApps(value)
}}
return ( getOptionLabel={(option) => {
<div> const parsedname = option.name.replaceAll("_", " ")
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
variant="outlined"
label="Select apps"
/>
)
}}
/>
: null}
return (
<div>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
variant="outlined"
label="Select apps"
/>
)
}}
/>
:
<Button
style={{width: 250, margin: 25, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
<Button toast.info("Starting ingest for relevant apps")
style={{flex: 1, }} var newapps = ""
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"} for (var key in selectedApps) {
onClick={() => { const app = selectedApps[key]
//if (foundMatchingWorkflow !== null) { if (newapps.length > 0) {
// toast.error("Deletion not implemented for this POC. Please delete the workflow.") newapps += ","
//} }
//else { newapps += app.name
toast.info("Starting ingest for relevant apps") }
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) { startIngestion(appname, newapps, appCategory, index)
newapps += "," if (webhook === true) {
} startIngestion(appname+"_webhook", newapps, appCategory, index)
}
newapps += app.name }}
>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
:
"Start Ingestion"
}
</Button>
} }
</div>
startIngestion(appname, newapps, appCategory, index) <div style={{flex: 1, marginTop: 50, }}>
//} {iconDetails?.originalIcon && (
}}> iconDetails?.originalIcon
{foundMatchingWorkflow !== null ? )}
"Re-Create Ingestion"
: <Typography variant="h4" style={{marginTop: 10, }}>
"Start Ingestion"
} {appname}
</Button> </Typography>
</div>
</div> </div>
{foundMatchingWorkflow !== null ? {foundMatchingWorkflow !== null ?
@@ -319,7 +355,7 @@ const CollectIngestModal = (props) => {
sx: { sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius, borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border, border: theme?.palette?.DialogStyle?.border,
minWidth: 500, minWidth: 850,
minHeight: 700, minHeight: 700,
fontFamily: theme?.typography?.fontFamily, fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
@@ -350,13 +386,14 @@ const CollectIngestModal = (props) => {
</Typography> </Typography>
<Grid container> <Grid container>
<IngestItem type="Ingest Tickets" appCategory={"cases"} index={1} /> <IngestItem type="Ingest Tickets" appCategory={"cases"} webhook={true} index={1} />
<IngestItem type="Enable Threat feeds" index={2} /> <IngestItem type="Enable Threat feeds" index={2} />
<IngestItem type="Ingest Assets" appCategory={"assets"} index={2} />
<IngestItem type="Ingest Users " appCategory={"users"} index={2} />
<IngestItem type="Enable Search" index={2} /> <IngestItem type="Enable Search" index={2} />
<IngestItem type="Enable Mitre Att&ck techniques" index={2} /> <IngestItem type="Enable Mitre Att&ck techniques" index={2} />
<IngestItem type="Enable Detection Rules" index={2} /> <IngestItem type="Enable Detection Rules" index={2} />
<IngestItem type="Ingest Logs" index={2} /> <IngestItem type="Ingest Logs" index={2} />
<IngestItem type="Track Assets" appCategory={"assets"} index={2} />
</Grid> </Grid>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+229 -29
View File
@@ -3,6 +3,7 @@ import { getTheme } from '../theme.jsx';
import { isMobile } from "react-device-detect" import { isMobile } from "react-device-detect"
import { MuiChipsInput } from "mui-chips-input"; import { MuiChipsInput } from "mui-chips-input";
import { toast } from "react-toastify" import { toast } from "react-toastify"
import ReactGA from 'react-ga4';
import UsecaseSearch from "../components/UsecaseSearch.jsx" import UsecaseSearch from "../components/UsecaseSearch.jsx"
import WorkflowGrid from "../components/WorkflowGrid.jsx" import WorkflowGrid from "../components/WorkflowGrid.jsx"
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -63,7 +64,10 @@ import {
Add as AddIcon, Add as AddIcon,
Remove as RemoveIcon, Remove as RemoveIcon,
EditNote as EditNoteIcon, EditNote as EditNoteIcon,
AutoAwesome as AutoAwesomeIcon AutoAwesome as AutoAwesomeIcon,
CloudUpload as CloudUploadIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon
} from "@mui/icons-material"; } from "@mui/icons-material";
const EditWorkflow = (props) => { const EditWorkflow = (props) => {
@@ -94,6 +98,11 @@ const EditWorkflow = (props) => {
const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : []) const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : [])
const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth) const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth)
// Flowchart upload states
const [uploadedImage, setUploadedImage] = React.useState(null)
const [imageBase64, setImageBase64] = React.useState("")
const [imageUploading, setImageUploading] = React.useState(false)
const classes = useStyles(); const classes = useStyles();
@@ -103,6 +112,59 @@ const EditWorkflow = (props) => {
} }
}, [formWidth]) }, [formWidth])
// Handle file upload and base64 conversion
const handleImageUpload = (file) => {
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg']
if (!allowedTypes.includes(file.type)) {
toast.error("Please upload a PNG, JPG, or JPEG image")
return
}
const maxSize = 5 * 1024 * 1024 // 5MB in bytes
if (file.size > maxSize) {
toast.error("Image must be less than 5MB")
return
}
setImageUploading(true)
const reader = new FileReader()
reader.onload = (e) => {
const base64 = e.target.result
setImageBase64(base64)
setUploadedImage({
name: file.name,
size: file.size,
type: file.type
})
setImageUploading(false)
// Disable "Create from scratch" when image is uploaded
if (newWorkflow) {
setWorkflowAsCode(false)
}
}
reader.onerror = () => {
toast.error("Failed to read image file")
setImageUploading(false)
}
reader.readAsDataURL(file)
}
const removeUploadedImage = () => {
setUploadedImage(null)
setImageBase64("")
setImageUploading(false)
}
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) { if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) {
setTimeout(() => { setTimeout(() => {
const foundScroll = document.getElementById(scrollTo) const foundScroll = document.getElementById(scrollTo)
@@ -308,7 +370,7 @@ const EditWorkflow = (props) => {
variant="contained" variant="contained"
style={{}} style={{}}
id="save_workflow_button" id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true} disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true || uploadedImage !== null}
onClick={() => { onClick={() => {
setSubmitLoading(true) setSubmitLoading(true)
@@ -395,24 +457,46 @@ const EditWorkflow = (props) => {
<Tooltip placement="top" arrow <Tooltip placement="top" arrow
title={ title={
<Typography variant="body1" style={{padding: 10, }}> <Typography variant="body1" style={{padding: 10, }}>
Generate using the name, description, usecases and tags provided. Required: Name + Description Generate using the name, description, usecases and tags provided. Required: Name + (Description OR Flowchart Image)
</Typography> </Typography>
} }
> >
<span> <span>
<Button <Button
disabled={name.length === 0 || innerWorkflow?.default_return_value?.length === 0 || aiGenerateLoading === true || submitLoading === true} disabled={
name.length === 0 ||
aiGenerateLoading === true ||
submitLoading === true ||
(uploadedImage === null && innerWorkflow?.default_return_value?.trim()?.length === 0)
}
variant="aiButton" variant="aiButton"
onClick={async () => { onClick={async () => {
// Check if description is provided in the visible field for new workflows // Track AI Generate button click
if (!innerWorkflow.default_return_value || innerWorkflow.default_return_value.trim().length === 0) { if (isCloud) {
toast.error("You need to describe what you want to generate so the AI can auto generate the entire workflow"); ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "button_click",
label: userdata?.active_org?.id || "",
});
}
// check AI enabled for local installations (not cloud)
if (!isCloud && (!userdata?.ai_enabled || userdata?.ai_enabled === false)) {
toast.warn("Local AI is not enabled. Setup required: https://shuffler.io/docs/AI-Features");
return;
}
// Check if description is provided OR image is uploaded for new workflows
if (uploadedImage === null && (!innerWorkflow?.default_return_value || innerWorkflow?.default_return_value?.trim()?.length === 0)) {
toast.error("You need to either upload a flowchart image OR describe what you want to generate so the AI can auto generate the entire workflow");
return; return;
} }
setAiGenerateLoading(true); setAiGenerateLoading(true);
toast.info("Creating workflow..."); toast.info("Creating workflow...");
let workflowId = null;
try { try {
// Step 1: Create basic workflow WITHOUT using setNewWorkflow (to avoid modal closing) // Step 1: Create basic workflow WITHOUT using setNewWorkflow (to avoid modal closing)
const workflowData = { const workflowData = {
@@ -455,15 +539,20 @@ const EditWorkflow = (props) => {
return; return;
} }
const workflowId = workflowJson.id; workflowId = workflowJson.id;
console.log("Created workflow with ID:", workflowId); toast.success("Workflow created! AI is now generating your workflow - please wait a few minutes...");
// Step 2: Generate AI content for the workflow // Step 3: Generate AI content for the workflow
const data = { const data = {
query: innerWorkflow.default_return_value, query: innerWorkflow.default_return_value,
workflow_id: workflowId, workflow_id: workflowId,
}; };
// Only include image_url if an image was uploaded
if (imageBase64 && imageBase64.length > 0) {
data.image_url = imageBase64;
}
const aiResponse = await fetch(globalUrl + "/api/v2/workflows/generate/llm", { const aiResponse = await fetch(globalUrl + "/api/v2/workflows/generate/llm", {
method: "POST", method: "POST",
headers: { headers: {
@@ -477,36 +566,55 @@ const EditWorkflow = (props) => {
const json = await aiResponse.json(); const json = await aiResponse.json();
// Handle AI response and provide feedback // Handle AI response and provide feedback
if (aiResponse.status !== 200) { if (aiResponse.status === 422) {
console.log("AI generation failed, but workflow created:", json.message || "Unexpected response"); // AI rejection with reason
toast.warning("Workflow created successfully, but AI generation failed. Opening workflow editor..."); if (isCloud) {
} else if (json.success === true && typeof json.message === "string") { ReactGA.event({
// AI "rejection" message category: "AIGeneratedNewWorkflow",
console.log("AI rejected request:", json.message); action: "ai_rejected",
toast.warning(`AI: ${json.message}. Opening empty workflow editor...`); label: workflowId,
} else if (json.success === false) { });
console.log("AI generation failed:", json.message || "Operation failed"); }
toast.warning("AI generation failed. Opening empty workflow editor..."); toast.warning(`AI: ${json.reason || "Request rejected"}. Opening workflow editor...`);
} else if (!json || Object.keys(json).length === 0) { } else if (aiResponse.status !== 200) {
console.log("Empty AI response"); // Other HTTP errors
toast.warning("AI returned empty response. Opening workflow editor..."); if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "generation_failed",
label: workflowId,
});
}
toast.warning("Workflow created, but AI generation failed. Opening workflow editor...");
} else { } else {
// Successful generation
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "generation_success",
label: workflowId,
});
}
toast.success("Workflow generated successfully! Opening editor..."); toast.success("Workflow generated successfully! Opening editor...");
console.log("AI generation successful");
} }
// Step 3: Now close modal and redirect (only after everything is complete) // Step 4: Close modal and redirect (after everything is complete)
setTimeout(() => { setTimeout(() => {
setModalOpen(false); setModalOpen(false);
setAiGenerateLoading(false); setAiGenerateLoading(false);
window.location.href = `/workflows/${workflowId}`; window.location.href = `/workflows/${workflowId}`;
}, 1500); // Give user time to read the final message }, 1500);
} catch (error) { } catch (error) {
console.error("Error in AI workflow generation:", error); if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "error",
label: workflowId || "no_workflow",
});
}
toast.error("Failed to generate. Please try again later: " + error.message); toast.error("Failed to generate. Please try again later: " + error.message);
setAiGenerateLoading(false); setAiGenerateLoading(false);
// Don't close modal on error so user can try again
} }
}} }}
> >
@@ -753,6 +861,98 @@ const EditWorkflow = (props) => {
/> />
</div> </div>
{/* Flowchart Upload Section - Only for new workflows */}
{newWorkflow === true ? (
<div style={{ marginTop: 100, }}>
{!uploadedImage ? (
<div
style={{
border: `2px solid rgba(255,255,255,0.3)`,
borderRadius: 8,
padding: 20,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.2s ease',
backgroundColor: theme.palette.surfaceColor,
'&:hover': {
backgroundColor: theme.palette.primary.main + '10'
}
}}
onClick={() => {
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/png,image/jpeg,image/jpg'
input.onchange = (e) => {
if (e.target.files.length > 0) {
handleImageUpload(e.target.files[0])
}
}
input.click()
}}
>
{imageUploading ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' }}>
<CircularProgress size={24} style={{ marginBottom: 10 }} />
<Typography variant="body2" color="textSecondary">
Processing image...
</Typography>
</div>
) : (
<div>
<CloudUploadIcon
style={{
fontSize: 48,
color: theme.palette.textSecondary || '#666',
marginBottom: 10
}}
/>
<Typography variant="h6" style={{ marginBottom: 5 }}>
Generate Workflow from Flowchart
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}>
Click to upload your flowchart - AI will convert it to a workflow
</Typography>
<Typography variant="caption" color="textSecondary">
PNG, JPG, JPEG Max 5MB
</Typography>
</div>
)}
</div>
) : (
<div
style={{
border: `1px solid ${theme.palette.primary.main}`,
borderRadius: 8,
padding: 15,
backgroundColor: theme.palette.primary.main + '10',
display: 'flex',
alignItems: 'center',
gap: 15
}}
>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 10 }}>
<CheckCircleIcon style={{ color: theme.palette.success.main }} />
<div>
<Typography variant="body2" style={{ fontWeight: 'bold' }}>
{uploadedImage.name}
</Typography>
<Typography variant="caption" color="textSecondary">
{formatFileSize(uploadedImage.size)}
</Typography>
</div>
</div>
<IconButton
onClick={removeUploadedImage}
style={{ color: theme.palette.text.secondary }}
size="small"
>
<CloseIcon />
</IconButton>
</div>
)}
</div>
) : null}
{showMoreClicked === true ? {showMoreClicked === true ?
<div style={{ marginTop: 50, }}> <div style={{ marginTop: 50, }}>
<TextField <TextField
@@ -1104,7 +1304,7 @@ const EditWorkflow = (props) => {
</Typography> </Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}> <Typography variant="body2" color="textSecondary" style={{ marginBottom: 20, }}>
<b>Beta Feature</b>: When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up. When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up.
</Typography> </Typography>
<FormControl style={{ marginTop: 15, }}> <FormControl style={{ marginTop: 15, }}>
+5 -1
View File
@@ -418,6 +418,10 @@ const LicencePopup = (props) => {
showSupport = true showSupport = true
} }
if (userdata?.app_execution_limit >= 300000) {
top_text = "Enterprise Plan"
}
if (subscription.name.includes("Open Source")) { if (subscription.name.includes("Open Source")) {
top_text = "Open Source" top_text = "Open Source"
showSupport = true showSupport = true
@@ -761,7 +765,7 @@ const LicencePopup = (props) => {
{ {
isCloud ? isCloud ?
userdata?.app_execution_limit && userdata?.app_execution_limit !== 10000 ? userdata?.app_execution_limit && userdata?.app_execution_limit !== 10000 ?
"You have already subscribed to the Scale plan, which includes " + (userdata?.app_execution_limit/1000) + "K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information." : `You have already subscribed to the ${top_text}, which includes ${userdata?.app_execution_limit/1000}K app runs/month. You can increase the limit by upgrading current plan. Contact support@shuffler.io for more information.` :
`You are using free Starter plan with max ${userdata?.app_execution_limit === 10000 ? "10,000" : "2,000"} runs per month. Upgrade to increase this limit.` `You are using free Starter plan with max ${userdata?.app_execution_limit === 10000 ? "10,000" : "2,000"} runs per month. Upgrade to increase this limit.`
: :
+8 -8
View File
@@ -87,15 +87,15 @@ const menuData = {
], ],
Services: [ Services: [
{ {
title: "Professional Services", title: "Proof of Concept",
description: description:
"Professional Services help you solve problems at your convenience.", "Register for POC to test the best of Shuffle for free for 30 days.",
icon: "/images/ProfessionalServices.svg", icon: "/images/ProfessionalServices.svg",
path: "/professional-services", path: "/poc",
gaData: { gaData: {
category: "navbar", category: "navbar",
action: "services_click", action: "services_click",
label: "professional_services_click" label: "proof_of_concept_click"
} }
}, },
{ {
@@ -103,7 +103,7 @@ const menuData = {
description: description:
"Support to help you build automations with confidence.", "Support to help you build automations with confidence.",
icon: "/images/Support.svg", icon: "/images/Support.svg",
path: "/contact?category=support", path: "/support",
gaData: { gaData: {
category: "navbar", category: "navbar",
action: "services_click", action: "services_click",
@@ -1486,7 +1486,7 @@ const Navbar = (props) => {
} }
}} }}
> >
Become a partner Become a Partner
</Button> </Button>
<Button <Button
fullWidth fullWidth
@@ -1516,7 +1516,7 @@ const Navbar = (props) => {
} }
}} }}
> >
Discover partners Discover Partners
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -1661,7 +1661,7 @@ const Navbar = (props) => {
const topbarHeight = showTopbar ? 40 : 0 const topbarHeight = showTopbar ? 40 : 0
const topbar = !isCloud || !showTopbar ? null : const topbar = !isCloud || !showTopbar ? null :
curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/professional-services" ? curpath === "/" || curpath.includes("/docs") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" || curpath === "/usecases" || curpath === "/training" || curpath === "/poc" || curpath === "/professional-services" ?
<span style={{ zIndex: 50001, marginTop: -4}}> <span style={{ zIndex: 50001, marginTop: -4}}>
{/* uncommit this to show topbar for release */} {/* uncommit this to show topbar for release */}
{/* <div style={{ position: "relative", height: topbarHeight, backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", overflow: "hidden", }}> {/* <div style={{ position: "relative", height: topbarHeight, backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)", overflow: "hidden", }}>
@@ -419,7 +419,7 @@ const OrgHeaderexpandedNew = (props) => {
<div style={{ marginTop: 8, display: "flex" }} /> <div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}> <div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{color: theme.palette.text.primary}}>Name</Typography> <Typography variant="text" style={{color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily}}>Name</Typography>
<TextField <TextField
required required
style={{ style={{
@@ -499,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
</div> </div>
{userdata?.support ? ( {userdata?.support ? (
<div style={{ alignItems: 'center' }}> <div style={{ alignItems: 'center' }}>
<div style={{ marginRight: '12px', color: theme.palette.text.primary }}>Status</div> <div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}>Status</div>
<FormControl style={{ width: 220, height: 35 }}> <FormControl style={{ width: 220, height: 35 }}>
<Select <Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}} style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
@@ -525,13 +525,13 @@ const OrgHeaderexpandedNew = (props) => {
{isCloud ? ( {isCloud ? (
<div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }} > <div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }} >
<Typography variant="text" style={{color: theme.palette.text.primary}}>Change Region</Typography> <Typography variant="text" style={{color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily}}>Change Region</Typography>
<RegionChangeModal selectedOrganization={selectedOrganization} setSelectedRegion={setSelectedRegion} userdata={userdata} handleSendChangeRegionMail={handleSendChangeRegionMail} /> <RegionChangeModal selectedOrganization={selectedOrganization} setSelectedRegion={setSelectedRegion} userdata={userdata} handleSendChangeRegionMail={handleSendChangeRegionMail} />
</div> </div>
) : null} ) : null}
</div> </div>
<div style={{ marginTop: "10px" }} /> <div style={{ marginTop: "10px" }} />
<Typography variant="text" style={{color: theme.palette.text.primary}}>Description</Typography> <Typography variant="text" style={{color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily}}>Description</Typography>
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<TextField <TextField
required required
+46 -26
View File
@@ -150,6 +150,7 @@ const ParsedAction = (props) => {
globalUrl, globalUrl,
setSelectedActionEnvironment, setSelectedActionEnvironment,
requiresAuthentication, requiresAuthentication,
setRequiresAuthentication,
hideExtraTypes, hideExtraTypes,
scrollConfig, scrollConfig,
setScrollConfig, setScrollConfig,
@@ -1076,8 +1077,8 @@ const ParsedAction = (props) => {
var toReplace = event.target.value var toReplace = event.target.value
if (!toReplace.startsWith("{") && !toReplace.startsWith("[")) { if (!toReplace?.startsWith("{") && !toReplace?.startsWith("[")) {
toReplace = toReplace.replaceAll('\\"', '"').replaceAll('"', '\\"') toReplace = toReplace?.replaceAll('\\"', '"').replaceAll('"', '\\"')
} }
if ( if (
@@ -1234,7 +1235,7 @@ const ParsedAction = (props) => {
console.log("APIKEY - this shouldn't show up!") console.log("APIKEY - this shouldn't show up!")
} }
if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && data.name === "input_list") { if (selectedAction.app_name === "Shuffle Tools" && (selectedAction.name === "filter_list" || selectedAction.name === "is_in_datastore") && data.name === "input_list") {
//console.log("FILTER LIST!: ", event, count, data) //console.log("FILTER LIST!: ", event, count, data)
const parsedvalue = event.target.value const parsedvalue = event.target.value
if (parsedvalue.includes(".#")) { if (parsedvalue.includes(".#")) {
@@ -1250,7 +1251,7 @@ const ParsedAction = (props) => {
selectedAction.parameters[1].value = splitparsed[1] selectedAction.parameters[1].value = splitparsed[1]
if (splitparsed.length >= 2) { if (splitparsed.length >= 2) {
toast.warn("Filter list only supports filtering on the first list. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.", { toast.warn("Datastore checker/Filter list only supports filtering on the first list. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.", {
autoClose: 10000, autoClose: 10000,
}) })
} else if (selectedAction.parameters[1].value.includes(".#")) { } else if (selectedAction.parameters[1].value.includes(".#")) {
@@ -1293,7 +1294,7 @@ const ParsedAction = (props) => {
const paramcheck = selectedAction.parameters.find(param => param.name === "body") const paramcheck = selectedAction.parameters.find(param => param.name === "body")
if (paramcheck !== undefined) { if (paramcheck !== undefined) {
// Escapes all double quotes // Escapes all double quotes
const toReplace = event.target.value.trim().replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); const toReplace = event.target.value?.trim()?.replaceAll("\\\"", "\"")?.replaceAll("\"", "\\\"");
console.log("REPLACE WITH: ", toReplace) console.log("REPLACE WITH: ", toReplace)
if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) {
paramcheck["value_replace"] = [{ paramcheck["value_replace"] = [{
@@ -2056,14 +2057,14 @@ const ParsedAction = (props) => {
value={appActionName} value={appActionName}
onChange={(event) => { onChange={(event) => {
let newValue = event.target.value let newValue = event.target.value
newValue = newValue.replaceAll(" ", "_") newValue = newValue?.replaceAll(" ", "_")
setAppActionName(newValue) setAppActionName(newValue)
}} }}
onBlur={(e) => { onBlur={(e) => {
// Copy the name value // Copy the name value
const name = e.target.value const name = e.target.value
const parsedBaseLabel = "$" + prevActionName.toLowerCase().replaceAll(" ", "_") const parsedBaseLabel = "$" + prevActionName?.toLowerCase()?.replaceAll(" ", "_")
const newname = "$" + name.toLowerCase().replaceAll(" ", "_") const newname = "$" + name?.toLowerCase()?.replaceAll(" ", "_")
// Check if it's the same as the current name in use // Check if it's the same as the current name in use
//if (name === selectedAction.label) { //if (name === selectedAction.label) {
@@ -2288,9 +2289,9 @@ const ParsedAction = (props) => {
)} )}
{selectedApp.name !== undefined && {selectedApp.name !== undefined &&
selectedAction.authentication !== null && ((selectedAction.authentication !== null &&
selectedAction.authentication !== undefined && selectedAction.authentication !== undefined &&
selectedAction.authentication.length === 0 && selectedAction.authentication.length === 0) || isAgent || isIntegration) &&
requiresAuthentication ? ( requiresAuthentication ? (
<div style={{ marginTop: 15 }}> <div style={{ marginTop: 15 }}>
<Tooltip <Tooltip
@@ -2303,6 +2304,7 @@ const ParsedAction = (props) => {
color="primary" color="primary"
style={{ style={{
textTransform: "none", textTransform: "none",
fontWeight: "bold",
}} }}
fullWidth fullWidth
variant="contained" variant="contained"
@@ -2315,7 +2317,7 @@ const ParsedAction = (props) => {
}} }}
> >
<AddIcon style={{ marginRight: 10 }} /> Authenticate{" "} <AddIcon style={{ marginRight: 10 }} /> Authenticate{" "}
{selectedApp.name.replaceAll("_", " ")} {isAgent || isIntegration ? "API" : selectedApp.name?.replaceAll("_", " ")}
</Button> </Button>
</span> </span>
</Tooltip> </Tooltip>
@@ -2813,7 +2815,7 @@ const ParsedAction = (props) => {
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
const lowercaseValue = inputValue.toLowerCase() const lowercaseValue = inputValue.toLowerCase()
options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) options = options.filter(x => x.name?.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description?.toLowerCase().includes(lowercaseValue))
return options return options
}} }}
@@ -2823,8 +2825,8 @@ const ParsedAction = (props) => {
} }
const newname = ( const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1) option.name?.charAt(0).toUpperCase() + option.name?.substring(1)
).replaceAll("_", " "); )?.replaceAll("_", " ");
return newname; return newname;
}} }}
@@ -2876,7 +2878,7 @@ const ParsedAction = (props) => {
option.label = "No name" option.label = "No name"
} }
newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1)).replaceAll("_", " "); newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1))?.replaceAll("_", " ");
var method = "" var method = ""
var extraDescription = "" var extraDescription = ""
@@ -3151,8 +3153,20 @@ const ParsedAction = (props) => {
setSelectedAction(selectedAction) setSelectedAction(selectedAction)
setUpdate(Math.random()) setUpdate(Math.random())
var requiresAuth = app?.authentication?.required
if (requiresAuth && appAuthentication?.length > 0) {
for (var key in appAuthentication) {
if (appAuthentication[key]?.app?.name === app?.name) {
requiresAuth = false
break
}
}
}
setRequiresAuthentication(requiresAuth);
}}> }}>
<Tooltip title={`Select ${app.name.replaceAll("_", " ")}`} placement="top"> <Tooltip title={`Select ${app.name?.replaceAll("_", " ")}`} placement="top">
<img <img
src={app.large_image} src={app.large_image}
style={{ style={{
@@ -3227,8 +3241,8 @@ const ParsedAction = (props) => {
} }
const newname = ( const newname = (
option.app_name.charAt(0).toUpperCase() + option.app_name.substring(1) option.app_name?.charAt(0).toUpperCase() + option.app_name?.substring(1)
).replaceAll("_", " "); )?.replaceAll("_", " ");
return newname; return newname;
}} }}
options={selectedAction.matching_actions} options={selectedAction.matching_actions}
@@ -3262,8 +3276,8 @@ const ParsedAction = (props) => {
newActionname = ( newActionname = (
newActionname.charAt(0).toUpperCase() + newActionname.charAt(0).toUpperCase() +
newActionname.substring(1) newActionname?.substring(1)
).replaceAll("_", " "); )?.replaceAll("_", " ");
return ( return (
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
@@ -3505,6 +3519,8 @@ const ParsedAction = (props) => {
if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") { if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") {
// Show a key popout button // Show a key popout button
showCacheConfig = true showCacheConfig = true
} else if (data.name === "category" && selectedAction.app_name === "Shuffle Tools") {
showCacheConfig = true
} }
var disabled = false; var disabled = false;
@@ -3732,6 +3748,7 @@ const ParsedAction = (props) => {
} }
const clickedFieldId = "rightside_field_" + count; const clickedFieldId = "rightside_field_" + count;
const parameterFieldId = "param_" + data.name.replace(/[^a-zA-Z0-9_-]/g, '_');
var baseHelperText = "" var baseHelperText = ""
if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) { if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) {
@@ -3748,8 +3765,8 @@ const ParsedAction = (props) => {
} }
tmpitem = ( tmpitem = (
tmpitem.charAt(0).toUpperCase() + tmpitem.substring(1) tmpitem?.charAt(0).toUpperCase() + tmpitem?.substring(1)
).replaceAll("_", " "); )?.replaceAll("_", " ");
if (tmpitem === "Username basic") { if (tmpitem === "Username basic") {
tmpitem = "Username" tmpitem = "Username"
@@ -3873,6 +3890,9 @@ const ParsedAction = (props) => {
autofill="off" autofill="off"
autoComplete="off" autoComplete="off"
id={clickedFieldId} id={clickedFieldId}
data-parameter={data.name}
data-param-id={parameterFieldId}
name={data.name}
disabled={disabled} disabled={disabled}
style={{ style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor, backgroundColor: theme.palette.textFieldStyle.backgroundColor,
@@ -4315,7 +4335,7 @@ const ParsedAction = (props) => {
viewed_data = split_data[0] viewed_data = split_data[0]
} }
viewed_data = (viewed_data.charAt(0).toUpperCase() + viewed_data.slice(1)).replaceAll("_", " ") viewed_data = (viewed_data?.charAt(0).toUpperCase() + viewed_data?.slice(1))?.replaceAll("_", " ")
// Check if it's selected or not and highlight // Check if it's selected or not and highlight
var selected = false var selected = false
@@ -4378,9 +4398,9 @@ const ParsedAction = (props) => {
? values[0].autocomplete ? values[0].autocomplete
: "$" + values[0].autocomplete; : "$" + values[0].autocomplete;
toComplete = toComplete.toLowerCase().replaceAll(" ", "_"); toComplete = toComplete?.toLowerCase()?.replaceAll(" ", "_");
for (let [key, keyval] in Object.entries(values)) { for (let [key, keyval] in Object.entries(values)) {
if (key == 0 || values[key].autocomplete.length === 0) { if (key == 0 || values[key]?.autocomplete?.length === 0) {
continue; continue;
} }
@@ -4730,7 +4750,7 @@ const ParsedAction = (props) => {
); );
} }
const buttonTitle = `Authenticate API ${selectedApp.name.replaceAll("_", " ")}` const buttonTitle = `Authenticate the ${selectedApp?.name?.replaceAll("_", " ")} API`
const hasAutocomplete = data?.autocompleted === true const hasAutocomplete = data?.autocompleted === true
if (data.variant === undefined || data.variant === null) { if (data.variant === undefined || data.variant === null) {
data.variant = "STATIC_VALUE" data.variant = "STATIC_VALUE"
+17 -16
View File
@@ -222,7 +222,7 @@ const PartnerDetails = (props) => {
<div style={{ marginTop: 8, display: "flex" }} /> <div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}> <div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Name Name
</Typography> </Typography>
<Skeleton <Skeleton
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
/> />
</div> */} </div> */}
<div style={{ alignItems: "center" }}> <div style={{ alignItems: "center" }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}> <div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Solutions Solutions
</div> </div>
<Skeleton <Skeleton
@@ -282,7 +282,7 @@ const PartnerDetails = (props) => {
/> />
</div> </div>
<div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }}> <div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Region Region
</Typography> </Typography>
<Skeleton <Skeleton
@@ -297,7 +297,7 @@ const PartnerDetails = (props) => {
/> />
</div> </div>
<div style={{ alignItems: "center", marginLeft: 12 }}> <div style={{ alignItems: "center", marginLeft: 12 }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}> <div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Country Country
</div> </div>
<Skeleton <Skeleton
@@ -313,7 +313,7 @@ const PartnerDetails = (props) => {
</div> </div>
</div> </div>
<div style={{ marginTop: "10px" }}> <div style={{ marginTop: "10px" }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Description Description
</Typography> </Typography>
<Skeleton <Skeleton
@@ -329,7 +329,7 @@ const PartnerDetails = (props) => {
</div> </div>
<div> <div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}> <div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Website URL Website URL
</Typography> </Typography>
<Skeleton <Skeleton
@@ -344,7 +344,7 @@ const PartnerDetails = (props) => {
/> />
</div> </div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}> <div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Article URL Article URL
</Typography> </Typography>
<Skeleton <Skeleton
@@ -359,7 +359,7 @@ const PartnerDetails = (props) => {
/> />
</div> </div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}> <div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}> <Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Contact Email Contact Email
</Typography> </Typography>
<Skeleton <Skeleton
@@ -397,7 +397,7 @@ const PartnerDetails = (props) => {
> >
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Name Name
</Typography> </Typography>
@@ -544,6 +544,7 @@ const PartnerDetails = (props) => {
style={{ style={{
marginRight: "12px", marginRight: "12px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontFamily: theme?.typography?.fontFamily
}} }}
> >
Solutions Solutions
@@ -586,7 +587,7 @@ const PartnerDetails = (props) => {
> >
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Region Region
</Typography> </Typography>
@@ -602,7 +603,7 @@ const PartnerDetails = (props) => {
<div style={{ alignItems: "flex-start", marginLeft: 13, display: "flex", flexDirection: "column" }}> <div style={{ alignItems: "flex-start", marginLeft: 13, display: "flex", flexDirection: "column" }}>
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Country Country
</Typography> </Typography>
@@ -686,7 +687,7 @@ const PartnerDetails = (props) => {
<div style={{ marginTop: "10px", }} /> <div style={{ marginTop: "10px", }} />
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Description Description
</Typography> </Typography>
@@ -746,7 +747,7 @@ const PartnerDetails = (props) => {
> >
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Website URL Website URL
</Typography> </Typography>
@@ -809,7 +810,7 @@ const PartnerDetails = (props) => {
> >
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Article URL Article URL
</Typography> </Typography>
@@ -872,7 +873,7 @@ const PartnerDetails = (props) => {
> >
<Typography <Typography
variant="text" variant="text"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
> >
Contact Email Contact Email
</Typography> </Typography>
@@ -894,7 +895,7 @@ const PartnerDetails = (props) => {
cursor: isDisabled ? "not-allowed" : "pointer", cursor: isDisabled ? "not-allowed" : "pointer",
}} }}
fullWidth={true} fullWidth={true}
placeholder="https://www.example.com" placeholder="support@shuffler.io"
type="name" type="name"
id="standard-required" id="standard-required"
margin="normal" margin="normal"
+48 -35
View File
@@ -42,7 +42,7 @@ const PartnerSettings = (props) => {
// Partner Types handling : Getting from org status // Partner Types handling : Getting from org status
useEffect(() => { useEffect(() => {
const partnerTypes = {}; const partnerTypes = {};
userdata?.org_status.forEach(status => { userdata?.org_status?.forEach(status => {
if (status.includes("_partner")) { if (status.includes("_partner")) {
partnerTypes[status] = true; partnerTypes[status] = true;
} }
@@ -121,6 +121,12 @@ const PartnerSettings = (props) => {
toast.error("There should be at least one partner type"); toast.error("There should be at least one partner type");
return; return;
} }
// Validate description length
if (partnerData?.description && partnerData.description.length > 1000) {
toast.error("Description should be less than or equal to 1000 characters");
return;
}
setIsPublishing(true); setIsPublishing(true);
const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id; const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id;
@@ -153,17 +159,15 @@ const PartnerSettings = (props) => {
}) })
.then((response) => { .then((response) => {
setIsPublishing(false); setIsPublishing(false);
if (response.status !== 200) { if (response.status === 200) {
toast.success("Partner details successfully updated");
} else {
toast.error("Failed to publish partner"); toast.error("Failed to publish partner");
} }
return response.json();
})
.then((responseJson) => {
toast.success("Partner details successfully updated");
}) })
.catch((error) => { .catch((error) => {
setIsPublishing(false); setIsPublishing(false);
toast.error("Failed to update partner details: " + error?.message); toast.error("Failed to update partner details: " + error?.reason);
}) })
} }
@@ -237,32 +241,41 @@ const PartnerSettings = (props) => {
sx={{display:"flex", alignItems:"flex-start", gap:2, justifyContent:"flex-start" sx={{display:"flex", alignItems:"flex-start", gap:2, justifyContent:"flex-start"
}}> }}>
<Typography variant='h3' sx={{ marginBottom: "15px", marginTop: 0, }}>Configuration</Typography> <Typography variant='h3' sx={{ marginBottom: "15px", marginTop: 0, }}>Configuration</Typography>
{Object?.entries(partnerTypes)?.map(([key, value]) => ( {Object?.entries(partnerTypes)
<Box ?.filter(([key, value]) => key !== "distribution_partner")
sx={{ ?.map(([key, value]) => {
display: "flex", let displayText = key?.replace("_", " ")?.replace(/\b\w/g, char => char.toUpperCase());
alignItems: "center", if (displayText === "Tech Partner") {
justifyContent: "center", displayText = "Technology Partner";
borderRadius: "999px", }
py: 1.2, return (
px: 2.5, <Box
fontSize: "13px", key={key}
fontWeight: 500, sx={{
fontFamily: theme.typography.fontFamily, display: "flex",
color: "#fff", alignItems: "center",
backgroundColor: "transparent", justifyContent: "center",
border: `1.5px solid ${ borderRadius: "999px",
partnerTypeColors[key] py: 1.2,
}`, px: 2.5,
transition: "all 0.2s ease", fontSize: "13px",
textAlign: "center", fontWeight: 500,
whiteSpace: "nowrap", fontFamily: theme.typography.fontFamily,
color: partnerTypeColors[key], color: "#fff",
}} backgroundColor: "transparent",
> border: `1.5px solid ${
{key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())} partnerTypeColors[key]
</Box> }`,
))} transition: "all 0.2s ease",
textAlign: "center",
whiteSpace: "nowrap",
color: partnerTypeColors[key],
}}
>
{displayText}
</Box>
);
})}
</Box> </Box>
<Box <Box
sx={{ sx={{
@@ -318,9 +331,9 @@ const PartnerSettings = (props) => {
boxShadow: "none", boxShadow: "none",
marginRight: 4, marginRight: 4,
px: 3, px: 3,
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50", backgroundColor: partnerData?.public ? "#FD4C62" : "#2BC07E",
"&:hover": { "&:hover": {
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50" backgroundColor: partnerData?.public ? "#FD4C62" : "#2BC07E"
} }
}} }}
variant="contained" variant="contained"
+2 -3
View File
@@ -66,18 +66,17 @@ const PartnerTab = (props) => {
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
toast("Failed to get partner data") toast.info("No partner details found");
} }
return response.json(); return response.json();
}) })
.then((responseJson) => { .then((responseJson) => {
if(responseJson.success) { if(responseJson.success) {
setPartnerData(responseJson?.partner); setPartnerData(responseJson?.partner);
console.log("responseJson", responseJson)
setLoadingPartnerData(false); setLoadingPartnerData(false);
}else{ }else{
setLoadingPartnerData(false); setLoadingPartnerData(false);
toast(responseJson?.reason) console.error(responseJson?.reason)
} }
}) })
.catch((error) => { .catch((error) => {
+200 -30
View File
@@ -47,6 +47,8 @@ import {
RestartAlt as RestartAltIcon, RestartAlt as RestartAltIcon,
ArrowForward as ArrowForwardIcon, ArrowForward as ArrowForwardIcon,
KeyboardReturn as KeyboardReturnIcon, KeyboardReturn as KeyboardReturnIcon,
FormatIndentIncrease as FormatIndentIncreaseIcon,
Fullscreen as FullscreenIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
@@ -143,6 +145,34 @@ const CodeEditor = (props) => {
handleConditionFieldChange, handleConditionFieldChange,
} = props } = props
// Auto-indent JSON-like content (with safety hehe)
const autoIndentContent = React.useCallback((content) => {
// Safety checks :)
if (!content || typeof content !== 'string' || content.trim().length === 0) {
return content;
}
try {
// Check if content looks like JSON (starts with { or [)
const trimmedContent = content.trim();
if (trimmedContent.startsWith('{') || trimmedContent.startsWith('[')) {
try {
const parsed = JSON.parse(trimmedContent);
return IndentJsonLikeString(JSON.stringify(parsed), 2);
} catch (parseError) {
return IndentJsonLikeString(content, 2);
}
}
// Return original content if it doesn't look like JSON
return content;
} catch (error) {
// If anything goes wrong, return original content
console.warn('Auto-indent failed, using original content:', error);
return content;
}
}, []);
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata); const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
// const {codelang, setcodelang} = props // const {codelang, setcodelang} = props
@@ -184,6 +214,7 @@ const CodeEditor = (props) => {
"result": baseResult, "result": baseResult,
}) })
const [executing, setExecuting] = useState(false) const [executing, setExecuting] = useState(false)
const [fullScreenModeEnabled, setFullScreenModeEnabled] = useState(fullScreenMode === true || fullScreenMode === "true" || localStorage.getItem("codeEditorFullScreen") === "true")
const liquidOpen = Boolean(anchorEl); const liquidOpen = Boolean(anchorEl);
const mathOpen = Boolean(anchorEl2); const mathOpen = Boolean(anchorEl2);
@@ -200,6 +231,22 @@ const CodeEditor = (props) => {
expectedOutput(localcodedata) expectedOutput(localcodedata)
}, [localcodedata]) }, [localcodedata])
// Auto-indent when codedata prop changes
useEffect(() => {
if (codedata && codedata !== localcodedata && typeof codedata === 'string') {
try {
const indentedContent = autoIndentContent(codedata);
if (indentedContent !== undefined && indentedContent !== null) {
setlocalcodedata(indentedContent);
}
} catch (error) {
console.warn('Failed to auto-indent codedata:', error);
// Fallback to original codedata
setlocalcodedata(codedata);
}
}
}, [codedata, autoIndentContent])
let navigate = useNavigate(); let navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const actionId = searchParams.get('action_id'); const actionId = searchParams.get('action_id');
@@ -217,7 +264,13 @@ const CodeEditor = (props) => {
} }
const action = workflow?.actions?.find(action => action.id === actionId); const action = workflow?.actions?.find(action => action.id === actionId);
setlocalcodedata(editorData?.value); try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent action data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedAction(action); setSelectedAction(action);
// Update available variables when action changes // Update available variables when action changes
@@ -230,7 +283,13 @@ const CodeEditor = (props) => {
} }
const trigger = workflow?.triggers?.find(trigger => trigger.id === triggerId); const trigger = workflow?.triggers?.find(trigger => trigger.id === triggerId);
setlocalcodedata(editorData?.value); try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent trigger data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedTrigger(trigger); setSelectedTrigger(trigger);
// Update available variables when trigger changes // Update available variables when trigger changes
@@ -244,7 +303,13 @@ const CodeEditor = (props) => {
} }
const condition = selectedEdge?.conditions?.find(condition => condition.id === conditionId); const condition = selectedEdge?.conditions?.find(condition => condition.id === conditionId);
setlocalcodedata(editorData?.value); try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent condition data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedCondition(condition); setSelectedCondition(condition);
// Update available variables when condition changes // Update available variables when condition changes
updateAvailableVariables(actionlist); updateAvailableVariables(actionlist);
@@ -963,7 +1028,6 @@ const CodeEditor = (props) => {
// vs // vs
// $variable.#.subvalue // $variable.#.subvalue
// if you put both of those lines in the same editor, then it will replace both (somehow). Make sure $variable.#.subvalue exists while testing. // if you put both of those lines in the same editor, then it will replace both (somehow). Make sure $variable.#.subvalue exists while testing.
console.log("FOUNDLOC: ", fixedVariable, foundlocation)
for (var j = 0; j < actionlist.length; j++) { for (var j = 0; j < actionlist.length; j++) {
if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) { if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) {
continue continue
@@ -990,7 +1054,6 @@ const CodeEditor = (props) => {
} }
try { try {
console.log("REPLACE: ", foundlocation, fixedVariable, newvalue)
if (newvalue !== "") { if (newvalue !== "") {
if (foundlocation === -1) { if (foundlocation === -1) {
input = input.replace(fixedVariable, newvalue, 1) input = input.replace(fixedVariable, newvalue, 1)
@@ -1303,7 +1366,7 @@ const CodeEditor = (props) => {
editor.completers = [customCompleter] editor.completers = [customCompleter]
} }
if (fullScreenMode) { if (fullScreenMode && fullScreenModeEnabled === true) {
return ( return (
<AceEditor <AceEditor
mode="python" mode="python"
@@ -1454,6 +1517,60 @@ const CodeEditor = (props) => {
) )
} }
const IndentJsonLikeString = (input, indentSize = 2) => {
const indent = ' '.repeat(indentSize);
let level = 0;
let inString = false;
let escapeNext = false;
let result = '';
for (let i = 0; i < input.length; i++) {
let char = input[i];
if (escapeNext) {
result += char;
escapeNext = false;
continue;
}
if (char === '\\') {
escapeNext = true;
result += char;
continue;
}
if (char === '"') {
inString = !inString;
result += char;
continue;
}
if (!inString) {
if (char === '{' || char === '[') {
result += char + '\n' + indent.repeat(++level);
continue;
} else if (char === '}' || char === ']') {
result += '\n' + indent.repeat(--level) + char;
continue;
} else if (char === ',') {
result += char + '\n' + indent.repeat(level);
continue;
} else if (char === ':') {
result += ': ';
continue;
} else if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
// Skip whitespace characters when not in string
continue;
}
}
result += char;
}
return result;
}
const SourceDataOption = (option) => { const SourceDataOption = (option) => {
const { innerdata, parsedPaths, defaultExpanded } = option const { innerdata, parsedPaths, defaultExpanded } = option
@@ -1526,15 +1643,15 @@ const CodeEditor = (props) => {
// zIndex: 12501, // zIndex: 12501,
pointerEvents: "auto", pointerEvents: "auto",
color: theme.palette.DialogStyle.color, color: theme.palette.DialogStyle.color,
minWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "80%", minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "1100px", maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile || isWorkflowEditor ? "100%" : "auto", minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor ? "100%" : "700px", maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px",
border: "3px solid rgba(255,255,255,0.3)", border: "3px solid rgba(255,255,255,0.3)",
padding: isMobile ? "25px 10px 25px 10px" : isWorkflowEditor ? "25px 10px 25px 200px" : "25px", padding: fullScreenModeEnabled ? "50px 0px 20px 200px" : isMobile ? "25px 10px 25px 10px" : isWorkflowEditor ? "25px 10px 25px 200px" : "25px",
backgroundColor: themeMode === "dark" ? "black" : theme.palette.DialogStyle.backgroundColor, backgroundColor: themeMode === "dark" ? "black" : theme.palette.DialogStyle.backgroundColor,
opacity: isWorkflowEditor ? 0.93 : 1, opacity: isWorkflowEditor || fullScreenModeEnabled ? 0.93 : 1,
}, },
}} }}
> >
@@ -1549,39 +1666,66 @@ const CodeEditor = (props) => {
</Tooltip> </Tooltip>
: null} : null}
{fullScreenModeEnabled ? null :
<Tooltip
color="primary"
title={`Move window`}
placement="left"
>
<IconButton
id="draggable-dialog-title"
style={{
zIndex: 5000,
position: "absolute",
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 166 : 66,
color: "grey",
cursor: "move",
}}
onClick={() => {
}}
>
<DragIndicatorIcon />
</IconButton>
</Tooltip>
}
<Tooltip <Tooltip
color="primary" color="primary"
title={`Move window`} title={fullScreenModeEnabled ? `Exit Fullscreen Mode` : `Enter Fullscreen Mode`}
placement="left" placement="top"
> >
<IconButton <IconButton
id="draggable-dialog-title"
style={{ style={{
zIndex: 5000, zIndex: 5000,
position: "absolute", position: "absolute",
top: 6, top: fullScreenModeEnabled ? 50 : 6,
right: 56, right: fullScreenModeEnabled ? 136 : 36,
color: "grey", color: "grey",
cursor: "move",
}} }}
onClick={() => { onClick={() => {
setFullScreenModeEnabled(!fullScreenModeEnabled)
localStorage.setItem("codeEditorFullScreen", !fullScreenModeEnabled)
}} }}
> >
<DragIndicatorIcon /> {!fullScreenModeEnabled ?
<FullscreenIcon />
:
<FullscreenExitIcon />
}
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip <Tooltip
color="primary" color="primary"
title={`Close window without saving`} title={`Close window without saving`}
placement="left" placement="right"
> >
<IconButton <IconButton
style={{ style={{
zIndex: 5000, zIndex: 5000,
position: "absolute", position: "absolute",
top: 6, top: fullScreenModeEnabled ? 50 : 6,
right: 6, right: fullScreenModeEnabled ? 106 : 6,
color: "grey", color: "grey",
}} }}
onClick={() => { onClick={() => {
@@ -2127,6 +2271,30 @@ const CodeEditor = (props) => {
marginLeft: 100, marginLeft: 100,
}} }}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0} disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
onClick={() => {
const indentedText = IndentJsonLikeString(localcodedata, 2)
if (indentedText !== undefined && indentedText !== null) {
setlocalcodedata(indentedText)
} else {
toast.warn("Could not indent the text. Please check the input format.", { autoClose: 5000 })
}
}}
color="secondary"
>
<Tooltip
title={"Indent Text"}
placement="top"
>
<FormatIndentIncreaseIcon />
</Tooltip>
</IconButton>
<IconButton
style={{
height: 50,
width: 50,
}}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
onClick={() => { onClick={() => {
if (fixExample !== undefined) { if (fixExample !== undefined) {
const newExample = fixExample(editorData.example) const newExample = fixExample(editorData.example)
@@ -2196,14 +2364,15 @@ const CodeEditor = (props) => {
value={localcodedata} value={localcodedata}
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"} mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
theme="gruvbox" theme="gruvbox"
height={isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550} height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : isWorkflowEditor ? "90vw" : "100%"} width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"}
markers={markers} markers={markers}
highlightActiveLine={false} highlightActiveLine={false}
enableBasicAutocompletion={true} enableBasicAutocompletion={true}
completers={[customCompleter]} completers={[customCompleter]}
showPrintMargin={false}
style={{ style={{
wordBreak: "break-word", wordBreak: "break-word",
@@ -2351,8 +2520,9 @@ const CodeEditor = (props) => {
style={{ style={{
border: `1px solid rgba(255, 255, 255, 0.15)`, border: `1px solid rgba(255, 255, 255, 0.15)`,
position: "absolute", position: "absolute",
top: 20, top: fullScreenModeEnabled ? 50 : 20,
right: 100, right: fullScreenModeEnabled ? 240 : 120,
maxHeight: 35, maxHeight: 35,
minWidth: 70, minWidth: 70,
zIndex: 1200, zIndex: 1200,
@@ -2466,8 +2636,8 @@ const CodeEditor = (props) => {
borderRadius: 5, borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`, border: `2px solid ${theme.palette.inputColor}`,
padding: 10, padding: 10,
maxHeight: 190, maxHeight: fullScreenModeEnabled ? 300 : 190,
minheight: 190, minheight: fullScreenModeEnabled ? 300 : 190,
overflow: "auto", overflow: "auto",
}} }}
collapsed={false} collapsed={false}
@@ -2528,7 +2698,7 @@ const CodeEditor = (props) => {
</div> </div>
<div style={{ display: 'flex', width: isWorkflowEditor ? "90%" : "100%", }}> <div style={{ display: 'flex', width: fullScreenModeEnabled ? "92%" : isWorkflowEditor ? "90%" : "100%", }}>
<Button <Button
style={{ style={{
height: 35, height: 35,
+14 -4
View File
@@ -39,6 +39,7 @@ const AgentUI = (props) => {
const [buttonState, setButtonState] = useState("timeline") const [buttonState, setButtonState] = useState("timeline")
const [execution, setExecution] = useState(null) const [execution, setExecution] = useState(null)
const [agentActionResult, setAgentActionResult] = useState(null) const [agentActionResult, setAgentActionResult] = useState(null)
const [agentRequestLoading, setAgentRequestLoading] = useState(false)
const [data, setData] = useState({}) const [data, setData] = useState({})
const [openIndexes, setOpenIndexes] = useState([]) const [openIndexes, setOpenIndexes] = useState([])
const [disableButtons, setDisableButtons] = useState(false) const [disableButtons, setDisableButtons] = useState(false)
@@ -571,8 +572,9 @@ const AgentUI = (props) => {
} }
const submitInput = (inputText) => { const submitInput = (inputText) => {
toast.info("Submitting AI Agent input: " + inputText); //toast.info("Submitting AI Agent input: " + inputText);
setAgentRequestLoading(true)
//setShowAgentStarter(false); //setShowAgentStarter(false);
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization); //GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
@@ -615,10 +617,11 @@ const AgentUI = (props) => {
credentials: "include", credentials: "include",
}) })
.then((response) => { .then((response) => {
setAgentRequestLoading(false)
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
toast.success("Got response!") //toast.success("Got response!")
console.log("Agent run response: ", responseJson) console.log("Agent run response: ", responseJson)
if (responseJson.success === true && responseJson.authorization !== undefined && responseJson.execution_id !== undefined) { if (responseJson.success === true && responseJson.authorization !== undefined && responseJson.execution_id !== undefined) {
@@ -628,6 +631,7 @@ const AgentUI = (props) => {
} }
}) })
.catch((error) => { .catch((error) => {
setAgentRequestLoading(false)
toast.error("Error: " + error) toast.error("Error: " + error)
}) })
@@ -651,15 +655,21 @@ const AgentUI = (props) => {
Shuffle AI Agents Shuffle AI Agents
</Typography> </Typography>
<TextField <TextField
label="Agent Input" label="What do you want to do?"
variant="outlined" variant="outlined"
style={{width: 300, marginRight: 20, marginTop: 30, }} disabled={agentRequestLoading}
style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline
minRows={2}
defaultValue={execution?.execution_id || ""} defaultValue={execution?.execution_id || ""}
onChange={(e) => { onChange={(e) => {
setActionInput(e.target.value) setActionInput(e.target.value)
}} }}
InputProps={{ InputProps={{
endAdornment: ( endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON."> <Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit"> <IconButton type="submit">
<SendIcon <SendIcon
File diff suppressed because it is too large Load Diff
+31 -18
View File
@@ -767,7 +767,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log("Failed to activate"); console.log("Failed to activate: " + response.statusText);
} }
return response.json(); return response.json();
@@ -776,27 +776,27 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (responseJson.success === false) { if (responseJson.success === false) {
if (action === undefined || action === null) { if (action === undefined || action === null) {
if (responseJson.reason !== undefined) { if (responseJson.reason !== undefined) {
toast("Failed to activate the app: "+responseJson.reason); toast.warn("Failed to activate the app: "+responseJson.reason);
} else { } else {
toast("Failed to activate the app"); toast.warn("Failed to activate the app");
} }
} else { } else {
if (responseJson.reason !== undefined) { if (responseJson.reason !== undefined) {
toast("Failed to perform action: "+responseJson.reason); toast.warn("Failed to perform action: "+responseJson.reason);
} else { } else {
toast(`Failed to perform action. Please try again or contact ${supportEmail}`); toast.warn(`Failed to perform action. Please try again or contact ${supportEmail}`);
} }
} }
} else { } else {
if ((checkLogin !== undefined && checkLogin !== null) && !multiple_request) { if (!showDistributionPopup && (checkLogin !== undefined && checkLogin !== null) && !multiple_request) {
checkLogin() checkLogin()
} }
if (action === undefined || action === null) { if (action === undefined || action === null) {
if (appExists) { if (appExists) {
toast("App deactivated for your organization! Existing workflows with the app will continue to work.") toast.success("App deactivated for your organization! Existing workflows with the app will continue to work.")
} else { } else {
toast("App activated for your organization!") toast.success("App activated for your organization!")
} }
} else { } else {
if (responseJson.success && !multiple_request &&(responseJson.reason !== undefined || responseJson.reason !== null)) { if (responseJson.success && !multiple_request &&(responseJson.reason !== undefined || responseJson.reason !== null)) {
@@ -806,7 +806,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
} }
}) })
.catch((error) => { .catch((error) => {
toast(error.toString()); toast.error(error.toString());
}); });
}; };
@@ -1450,7 +1450,12 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const index = searchClient.initIndex("appsearch"); const index = searchClient.initIndex("appsearch");
console.log("Running appsearch for: ", appname); console.log("Running appsearch for: ", appname);
if (appname === "integration") {
// Redirect to https://singul.io
window.location.href = "https://singul.io"
} else if (appname === "shuffle_agent") {
navigate("/agents")
}
index index
.search(appname) .search(appname)
@@ -3921,19 +3926,22 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (action === "activate_all") { if (action === "activate_all") {
const childOrgs = userdata.orgs.filter( const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id (data) => data.creator_org === userdata.active_org.id
); )
// run app activation request for each org
const orgIds = childOrgs.map((data) => data.id); const orgIds = childOrgs.map((data) => data.id);
// run app activation requset for each org
orgIds.forEach((orgId) => { orgIds.forEach((orgId) => {
activateApp("activate", orgId, true) activateApp("activate", orgId, true)
}); })
setTimeout(() => { setTimeout(() => {
toast.success("App activated for all sub-orgs"); toast.success("App activated for all sub-orgs");
}, 5000); }, 5000)
} else if (action === "deactivate_all") { } else if (action === "deactivate_all") {
const childOrgs = userdata.orgs.filter( const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id (data) => data.creator_org === userdata.active_org.id
); )
const orgIds = childOrgs.map((data) => data.id); const orgIds = childOrgs.map((data) => data.id);
// run app deactivation request for each org // run app deactivation request for each org
orgIds.forEach((orgId) => { orgIds.forEach((orgId) => {
@@ -3949,12 +3957,14 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
toast.error("Please select a sub-org to activate the app for."); toast.error("Please select a sub-org to activate the app for.");
return; return;
} }
activateApp("activate", id); activateApp("activate", id);
} else if (action === "deactivate_single") { } else if (action === "deactivate_single") {
if (id === null) { if (id === null) {
toast.error("Please select a sub-org to deactivate the app for."); toast.error("Please select a sub-org to deactivate the app for.");
return; return;
} }
activateApp("deactivate", id); activateApp("deactivate", id);
} }
}; };
@@ -3973,8 +3983,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
fontFamily: theme?.typography?.fontFamily, fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000, zIndex: 1000,
minWidth: "600px", minWidth: 600,
minHeight: "320px", minHeight: 320,
overflow: "auto", overflow: "auto",
'& .MuiDialogContent-root': { '& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
@@ -3997,9 +4007,12 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
pr: 1, pr: 1,
pb: 1, pb: 1,
}} }}
style={{
padding: "50px 50px 50px 50px",
}}
> >
<Typography variant="h6" fontWeight={600} color="text.primary"> <Typography variant="h6" fontWeight={600} color="text.primary">
Select sub-org to distribute App Suborg App Distribution
</Typography> </Typography>
<IconButton <IconButton
onClick={() => setShowDistributionPopup(false)} onClick={() => setShowDistributionPopup(false)}
+16 -12
View File
@@ -949,29 +949,33 @@ const CustomLabelDropdown = connectRefinementList(LabelDropdown);
const filterApps = (apps, searchQuery, selectedCategory, selectedLabel) => { const filterApps = (apps, searchQuery, selectedCategory, selectedLabel) => {
if (!Array.isArray(apps)) return []; if (!Array.isArray(apps)) return [];
const normalizedSearchQuery = (searchQuery || "").toLowerCase();
return apps.filter((app) => { return apps.filter((app) => {
if (!app) return false;
const matchesSearchQuery = ( const matchesSearchQuery = (
searchQuery === "" || // If searchQuery is empty, match all apps normalizedSearchQuery === "" || // If searchQuery is empty, match all apps
app.name.toLowerCase().includes(searchQuery.toLowerCase()) || (app.name && app.name.toLowerCase().includes(normalizedSearchQuery)) ||
(app.tags && app.tags.some(tag => (app.tags && Array.isArray(app.tags) && app.tags.some(tag =>
tag.toLowerCase().includes(searchQuery.toLowerCase()) tag && typeof tag === 'string' && tag.toLowerCase().includes(normalizedSearchQuery)
)) || )) ||
(app.categories && app.categories.some((category) => (app.categories && Array.isArray(app.categories) && app.categories.some((category) =>
category.toLowerCase().includes(searchQuery.toLowerCase()) category && typeof category === 'string' && category.toLowerCase().includes(normalizedSearchQuery)
)) ))
); );
const matchesSelectedCategories = ( const matchesSelectedCategories = (
selectedCategory.length === 0 || // If no category is selected, match all apps !Array.isArray(selectedCategory) || selectedCategory.length === 0 || // If no category is selected, match all apps
(app.categories && app.categories.some(category => (app.categories && Array.isArray(app.categories) && app.categories.some(category =>
selectedCategory.includes(category) category && selectedCategory.includes(category)
)) ))
); );
const matchesSelectedTags = ( const matchesSelectedTags = (
selectedLabel.length === 0 || // If no label is selected, match all apps !Array.isArray(selectedLabel) || selectedLabel.length === 0 || // If no label is selected, match all apps
(app.tags && app.tags.some(tag => (app.tags && Array.isArray(app.tags) && app.tags.some(tag =>
selectedLabel.includes(tag) tag && selectedLabel.includes(tag)
)) ))
); );
+17 -6
View File
@@ -113,19 +113,29 @@ export const CopyToClipboard = (props) => {
} }
export const Paragraph = (props) => { export const Paragraph = (props) => {
// Filter out stray HTML artifacts like '>' or '/>' caused by HTML parsing edge-cases
const cleanedChildren = React.Children.toArray(props.children).filter((child) => {
if (typeof child === 'string') {
const trimmed = child.trim();
// Remove single '>' or '/>' leftovers
if (trimmed === '>' || trimmed === '/>') return false;
}
return true;
});
const element = React.createElement( const element = React.createElement(
`p`, `p`,
{}, {},
props.children, cleanedChildren,
) )
if (props.children[0] != undefined) { if (cleanedChildren[0] !== undefined) {
if(typeof props.children[0] === "string") { if(typeof cleanedChildren[0] === "string") {
if (props.children[0].includes('.mp4')) { if (cleanedChildren[0].includes('.mp4')) {
return ( return (
<div> <div>
<video width="640" height="480" controls> <video width="640" height="480" controls>
<source src={`${props.children[0]}`} type="video/mp4" /> <source src={`${cleanedChildren[0]}`} type="video/mp4" />
</video> </video>
</div> </div>
) )
@@ -169,6 +179,7 @@ export const Img = (props) => {
// Find parent container and check width // Find parent container and check width
const isArticlePage = window.location.pathname.includes("/articles/") const isArticlePage = window.location.pathname.includes("/articles/")
const isFormPage = window.location.pathname.includes("/forms/") const isFormPage = window.location.pathname.includes("/forms/")
const isWorkflowPage = window.location.pathname.includes("/workflows/")
var height = "auto" var height = "auto"
var width = isArticlePage ? 1000 : isFormPage ? 400: 750 var width = isArticlePage ? 1000 : isFormPage ? 400: 750
@@ -176,7 +187,7 @@ export const Img = (props) => {
const theme = getTheme(themeMode) const theme = getTheme(themeMode)
const docsImageStyle = { const docsImageStyle = {
border: isFormPage ? null : "1px solid rgba(255,255,255,0.3)", border: isFormPage || isWorkflowPage ? null : "1px solid rgba(255,255,255,0.3)",
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
width: width, width: width,
maxWidth: width, maxWidth: width,
+2 -2
View File
@@ -453,7 +453,7 @@ const LoginPage = props => {
return (username.length > 0 && password.length > 0); return (username.length > 0 && password.length > 0);
} }
return (username.length > 1 && password.length > 8); return (username.length > 1 && password.length > 9);
} }
if (isLoggedIn === true && serverside !== true) { if (isLoggedIn === true && serverside !== true) {
@@ -932,7 +932,7 @@ const LoginPage = props => {
helperText={ helperText={
handleValidateForm(username, password) handleValidateForm(username, password)
? "" ? ""
: "Password must be at least 9 characters long" : "Password must be at least 10 characters long"
} }
/> />
</div> </div>
+369 -44
View File
@@ -17,11 +17,13 @@ import GetAppIcon from '@mui/icons-material/GetApp';
// Material UI & Components // Material UI & Components
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { isMobile } from "react-device-detect"
import LineChartWrapper from "../components/LineChartWrapper.jsx";
import SecurityFramework from '../components/SecurityFramework.jsx'; import SecurityFramework from '../components/SecurityFramework.jsx';
import EditWorkflow from "../components/EditWorkflow.jsx" import EditWorkflow from "../components/EditWorkflow.jsx"
import Priority from "../components/Priority.jsx"; import Priority from "../components/Priority.jsx";
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
import { isMobile } from "react-device-detect"
// Material UI Components // Material UI Components
import { import {
@@ -99,6 +101,9 @@ import {
Psychology as PsychologyIcon, Psychology as PsychologyIcon,
Wifi as WifiIcon, Wifi as WifiIcon,
Devices as DevicesIcon, Devices as DevicesIcon,
AutoAwesome as AutoAwesomeIcon,
BarChart as BarChartIcon,
Lock as LockIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
// Additional Components // Additional Components
@@ -115,13 +120,13 @@ import { InstantSearch, Configure, connectHits, connectSearchBox, connectRefinem
import { debounce } from "lodash"; import { debounce } from "lodash";
import { removeQuery } from "../components/ScrollToTop.jsx"; import { removeQuery } from "../components/ScrollToTop.jsx";
import {green, yellow, red, grey } from "../views/AngularWorkflow.jsx" import {green, yellow, red, grey, triggers as wfTriggers, } from "../views/AngularWorkflow.jsx"
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e"); const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e");
const svgSize = 24; const svgSize = 24;
const imagesize = 22; const imagesize = 23;
@@ -212,8 +217,14 @@ export const GetIconInfo = (action) => {
"release", "release",
], ],
}, },
{
key: "secret",
values: [
"api",
"password",
"protect",
],
}
]; ];
var selectedKey = "" var selectedKey = ""
@@ -402,6 +413,12 @@ export const GetIconInfo = (action) => {
iconBackgroundColor: "green", iconBackgroundColor: "green",
originalIcon: <DevicesIcon />, originalIcon: <DevicesIcon />,
}, },
secret: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <LockIcon />,
}
} }
/* /*
@@ -670,6 +687,8 @@ const Workflows2 = (props) => {
const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false); const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false);
const [isLoadingPublicWorkflow, setIsLoadingPublicWorkflow] = useState(false); const [isLoadingPublicWorkflow, setIsLoadingPublicWorkflow] = useState(false);
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid"); const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true" || false);
const imgSize = 60; const imgSize = 60;
const { themeMode, brandColor, brandName } = useContext(Context); const { themeMode, brandColor, brandName } = useContext(Context);
@@ -728,6 +747,8 @@ const Workflows2 = (props) => {
var upload = ""; var upload = "";
const [workflows, setWorkflows] = React.useState([]); const [workflows, setWorkflows] = React.useState([]);
const [workflowTimelines, setWorkflowTimelines] = React.useState([]);
const [backgroundWorkflows, setBackgroundWorkflows] = React.useState([]);
const [backupWorkflows, setBackupWorkflows] = React.useState([]); const [backupWorkflows, setBackupWorkflows] = React.useState([]);
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [selectedUsecases, setSelectedUsecases] = React.useState([]); const [selectedUsecases, setSelectedUsecases] = React.useState([]);
@@ -767,6 +788,7 @@ const Workflows2 = (props) => {
const [actionImageList, setActionImageList] = React.useState([{ "large_image": "" }]) const [actionImageList, setActionImageList] = React.useState([{ "large_image": "" }])
const [firstLoad, setFirstLoad] = React.useState(true); const [firstLoad, setFirstLoad] = React.useState(true);
const [aiAnnouncementModalOpen, setAiAnnouncementModalOpen] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(false); const [showMoreClicked, setShowMoreClicked] = React.useState(false);
const [usecases, setUsecases] = React.useState([]); const [usecases, setUsecases] = React.useState([]);
const [allUsecases, setAllUsecases] = React.useState({ const [allUsecases, setAllUsecases] = React.useState({
@@ -875,6 +897,53 @@ const Workflows2 = (props) => {
} }
React.useEffect(() => {
const bannerID = "banner_ai_announcement";
if (isLoggedIn === true && userdata && userdata.tutorials !== undefined && userdata.tutorials !== null) {
// Check if user has already dismissed this banner - tutorials is array of objects with 'name' field
const alreadyDismissed = userdata.tutorials.some(tutorial => tutorial.name === bannerID);
if (!alreadyDismissed && !aiAnnouncementModalOpen) {
// Show the banner as the user hasn't seen it yet
setAiAnnouncementModalOpen(true);
}
}
}, [isLoggedIn, userdata]);
const dismissAiAnnouncement = () => {
const bannerID = "banner_ai_announcement";
setAiAnnouncementModalOpen(false);
fetch(globalUrl + '/api/v1/users/updateuser', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
tutorial: bannerID,
user_id: userdata.id
}),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Failed to dismiss AI announcement banner");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success) {
console.log("AI announcement banner dismissed successfully");
}
})
.catch((error) => {
console.log("Error dismissing AI announcement:", error);
});
};
//const isCloud = //const isCloud =
// window.location.host === "localhost:3002" || // window.location.host === "localhost:3002" ||
// window.location.host === "shuffler.io"; // window.location.host === "shuffler.io";
@@ -1154,6 +1223,74 @@ const Workflows2 = (props) => {
</Dialog> </Dialog>
) : null; ) : null;
const aiAnnouncementModal = aiAnnouncementModalOpen ? (
<Dialog
open={aiAnnouncementModalOpen}
onClose={() => setAiAnnouncementModalOpen(false)}
PaperProps={{
style: {
backgroundColor: "#1a1a1a",
color: "white",
minWidth: 500,
maxWidth: 550,
padding: 30,
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: "1px solid #333",
},
}}
>
<DialogTitle style={{ marginBottom: 0, textAlign: "center", position: "relative", paddingBottom: 10 }}>
<IconButton
style={{
position: "absolute",
top: -10,
right: -15,
color: "rgba(255,255,255,0.7)",
}}
onClick={() => setAiAnnouncementModalOpen(false)}
>
<CloseIcon />
</IconButton>
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", marginBottom: 15 }}>
<AutoAwesomeIcon style={{ marginRight: 12, color: "#f85a3e", fontSize: 32 }} />
<Typography variant="h5" style={{ color: "rgba(255,255,255,0.9)" }}>
🎉 Introducing AI Workflow Generation!
</Typography>
</div>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.8)", textAlign: "left", paddingTop: 0 }}>
<Typography variant="body1" style={{ marginBottom: 18, fontSize: "15px", lineHeight: 1.5 }}>
🤖 Simply describe what you want your workflow to do, and our AI will automatically generate the workflow for you!
</Typography>
<Typography variant="body1" style={{ marginBottom: 18, fontSize: "15px", lineHeight: 1.5 }}>
<strong>Quick start:</strong> Click "Create Workflow" Describe your workflow "AI Generate" Done!
</Typography>
<Typography variant="body1" style={{ marginBottom: 25, fontSize: "14px", lineHeight: 1.4, color: "rgba(255,255,255,0.6)" }}>
For self-hosted setups: <a href="https://shuffler.io/docs/AI-Features" target="_blank" rel="noopener noreferrer" style={{ color: "#f85a3e", textDecoration: "none" }}>setup docs</a>
</Typography>
<div style={{ textAlign: "center", marginTop: 20 }}>
<Button
variant="contained"
onClick={dismissAiAnnouncement}
style={{
backgroundColor: "#f85a3e",
color: "white",
padding: "10px 25px",
fontSize: "15px",
textTransform: "none",
borderRadius: "6px"
}}
>
Got it, let's try it! 🚀
</Button>
</div>
</DialogContent>
</Dialog>
) : null;
const deleteModal = deleteModalOpen ? ( const deleteModal = deleteModalOpen ? (
<Dialog <Dialog
open={deleteModalOpen} open={deleteModalOpen}
@@ -1170,13 +1307,13 @@ const Workflows2 = (props) => {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000, zIndex: 1000,
'& .MuiDialogContent-root': { '& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
}, },
'& .MuiDialogTitle-root': { '& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
}, },
} }
}} }}
> >
<DialogTitle> <DialogTitle>
<div style={{ textAlign: "center", color: theme.palette.DialogStyle?.color }}> <div style={{ textAlign: "center", color: theme.palette.DialogStyle?.color }}>
@@ -1351,6 +1488,53 @@ const Workflows2 = (props) => {
window.location.reload(); window.location.reload();
} }
}, []); }, []);
useEffect(() => {
if (workflows?.length === 0) {
return
}
if (workflowTimelines?.length > 0) {
return
}
//if (isLoggedIn !== true) {
// return
//}
const results = Promise.all(
workflows.slice(0,16).map((workflow, index) => {
return fetch(`${globalUrl}/api/v2/workflows/${workflow.id}/executions`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => response.json())
})
)
results.then((res) => {
var newarray = []
for (var resKey in res) {
const result = res[resKey]
if (result?.timeline === undefined || result?.timeline === null) {
continue
}
newarray.push({
"id": result.id,
"timeline": result.timeline,
})
}
setWorkflowTimelines(newarray)
})
}, [workflows])
const getAvailableWorkflows = (amount) => { const getAvailableWorkflows = (amount) => {
var storageWorkflows = [] var storageWorkflows = []
@@ -1407,18 +1591,29 @@ const Workflows2 = (props) => {
toast.info("No workflows found in this org. Feel free to look into our public workflows!" , { toast.info("No workflows found in this org. Feel free to look into our public workflows!" , {
timeout: 7500, timeout: 7500,
}) })
setCurrTab(2) setCurrTab(2)
} }
localStorage.setItem("workflows", "[]")
setWorkflows([])
setFilteredWorkflows([])
} }
var newarray = [] var newarray = []
var backupWf = [] var backupWf = []
var backgroundWf = []
for (var wfkey in responseJson) { for (var wfkey in responseJson) {
const wf = responseJson[wfkey] const wf = responseJson[wfkey]
if (wf.public === true || wf.hidden === true) { if (wf?.public === true || wf?.hidden === true) {
continue continue
} }
if (wf?.background_processing === true) {
backgroundWf.push(wf)
continue
}
if (wf?.backup_config?.onprem_backup === true) { if (wf?.backup_config?.onprem_backup === true) {
backupWf.push(wf) backupWf.push(wf)
continue continue
@@ -1427,6 +1622,10 @@ const Workflows2 = (props) => {
newarray.push(wf) newarray.push(wf)
} }
if (backgroundWf.length > 0) {
setBackgroundWorkflows(backgroundWf)
}
if (backupWf.length > 0) { if (backupWf.length > 0) {
setBackupWorkflows(backupWf) setBackupWorkflows(backupWf)
} }
@@ -1659,11 +1858,9 @@ const Workflows2 = (props) => {
const paperAppStyle = { const paperAppStyle = {
minHeight: 146, minHeight: 146,
maxHeight: 146,
overflow: "hidden", overflow: "hidden",
width: "100%", width: "100%",
color: "white", color: "white",
display: "flex",
fontFamily: theme.typography?.fontFamily, fontFamily: theme.typography?.fontFamily,
boxSizing: "border-box", boxSizing: "border-box",
position: "relative", position: "relative",
@@ -2449,26 +2646,25 @@ const Workflows2 = (props) => {
var orgName = ""; var orgName = "";
var orgId = ""; var orgId = "";
var imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
cursor: "pointer",
marginRight: 10,
}
if (userdata.orgs !== undefined) { if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) { if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5, //position: "absolute", bottom: 5, right: -5,
const imageStyle = { imageStyle.border = foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
border:
foundOrg.id === userdata.active_org.id
? `3px solid ${boxColor}`
: null,
cursor: "pointer",
marginRight: 10,
};
image = image =
@@ -2492,6 +2688,44 @@ const Workflows2 = (props) => {
} }
} }
var triggerfound = false
var triggerstarted = false
var relevantTrigger = {}
for (var triggerkey in data?.triggers) {
const trigger = data?.triggers[triggerkey]
if (trigger?.trigger_type === "WEBHOOK") {
triggerfound = true
image = wfTriggers[0].large_image
relevantTrigger = trigger
if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}`
break
} else {
imageStyle.border = `3px solid ${red}`
}
} else if (trigger?.trigger_type === "SCHEDULE") {
triggerfound = true
image = wfTriggers[1].large_image
relevantTrigger = trigger
if (trigger?.status === "running") {
imageStyle.border = `3px solid ${green}`
break
} else {
imageStyle.border = `3px solid ${red}`
}
}
}
if (!triggerfound) {
image = ""
}
var selectedCategory = "" var selectedCategory = ""
if (data.usecase_ids !== undefined && data.usecase_ids !== null && data.usecase_ids.length > 0 && usecases !== null && usecases !== undefined && usecases.length > 0) { if (data.usecase_ids !== undefined && data.usecase_ids !== null && data.usecase_ids.length > 0 && usecases !== null && usecases !== undefined && usecases.length > 0) {
const oldcolor = boxColor.valueOf() const oldcolor = boxColor.valueOf()
@@ -2534,6 +2768,7 @@ const Workflows2 = (props) => {
} }
image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle} /> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle} /> image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle} /> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle} />
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : "Shuffle" const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : "Shuffle"
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) { if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
data.objectID = data.id data.objectID = data.id
@@ -2546,10 +2781,12 @@ const Workflows2 = (props) => {
} }
} }
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
return ( return (
<div 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 }}> <div 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 }}>
<Paper square style={paperAppStyle}> <Paper square style={paperAppStyle}>
{selectedCategory !== "" ? {selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom"> <Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
<div <div
@@ -2558,11 +2795,12 @@ const Workflows2 = (props) => {
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
height: paperAppStyle.minHeight,
width: 3, width: 3,
backgroundColor: boxColor, backgroundColor: boxColor,
borderRadius: "0 100px 0 0", borderRadius: "0 100px 0 0",
fontFamily: theme.typography?.fontFamily, fontFamily: theme.typography?.fontFamily,
height: "100%",
}} }}
onClick={() => { onClick={() => {
addFilter(selectedCategory) addFilter(selectedCategory)
@@ -2577,17 +2815,27 @@ const Workflows2 = (props) => {
> >
<Grid item style={{ display: "flex", maxHeight: 34 }}> <Grid item style={{ display: "flex", maxHeight: 34 }}>
{currTab === 2 ? null : {currTab === 2 ? null :
<Tooltip title={`Org "${orgName}". Click to edit image.`} placement="bottom"> <Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
<div <div
styl={{ cursor: "pointer" }} style={{ cursor: "" }}
onClick={() => { onClick={() => {
navigate("/admin") //navigate("/admin")
}} }}
> >
{image} {image?.includes("data:image") ?
<img
alt={orgName}
src={image}
style={imageStyle}
/>
:
image
}
</div> </div>
</Tooltip> </Tooltip>
} }
<Tooltip arrow <Tooltip arrow
onMouseEnter={() => { onMouseEnter={() => {
/* /*
@@ -2874,7 +3122,8 @@ const Workflows2 = (props) => {
}) })
: null} : null}
</Grid> </Grid>
{data.actions !== undefined && data.actions !== null && type !== "public" ? (
{type !== "public" ? (
<div style={{ position: "absolute", top: 10, right: 10, }}> <div style={{ position: "absolute", top: 10, right: 10, }}>
<IconButton <IconButton
aria-label="more" aria-label="more"
@@ -2891,7 +3140,7 @@ const Workflows2 = (props) => {
{(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ? {(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ?
<Tooltip title="Edit Form" placement="top"> <Tooltip title="Edit Form" placement="top">
<div style={{ position: "absolute", top: 50, right: 8, }}> <div style={{ position: "absolute", top: 80, right: 8, }}>
<IconButton <IconButton
aria-label="more" aria-label="more"
aria-controls="long-menu" aria-controls="long-menu"
@@ -2908,8 +3157,8 @@ const Workflows2 = (props) => {
: null} : null}
{(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ? {(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ?
<Tooltip title={`Explore more than ${data?.validation?.errors?.length} notifications. When the last execution finishes without errors AND notifications stop occuring, this icon disappears.`} placement="top"> <Tooltip title={`Explore more than ${data?.validation?.errors?.length} notifications for this workflow. When the last execution finishes without errors AND notifications stop occuring, this icon disappears.`} placement="top">
<div style={{ position: "absolute", top: 85, right: 8, }}> <div style={{ position: "absolute", top: 40, right: 8, }}>
<IconButton <IconButton
aria-label="more" aria-label="more"
aria-controls="long-menu" aria-controls="long-menu"
@@ -2919,19 +3168,38 @@ const Workflows2 = (props) => {
}} }}
style={{ style={{
padding: "0px", padding: "0px",
color: "#979797", transparency: 0.5,
}} }}
color="primary"
> >
<ErrorOutlineIcon style={{ <ErrorOutlineIcon
marginRight: 2, style={{
}} /> color: "#f86a3e",
marginRight: 2,
}}
/>
</IconButton> </IconButton>
</div> </div>
</Tooltip> </Tooltip>
: null} : null}
</Grid> </Grid>
{showExecutionStats === true && foundTimeline !== undefined && foundTimeline?.timeline?.length > 0 &&
<div style={{ margin: "40px 15px 0px 15px", paddingTop: 0, borderTop: "1px solid rgba(255,255,255,0.3)", }}>
<LineChartWrapper
inputname={""}
keys={foundTimeline?.timeline}
height={100}
width={100}
border={false}
color={"#808080"}
/>
</div>
}
</Paper> </Paper>
</div> </div>
) )
} }
@@ -4414,16 +4682,33 @@ const Workflows2 = (props) => {
{backupWorkflows.length > 0 && {backupWorkflows.length > 0 &&
<Tab <Tab
label={`Onprem Backup (${backupWorkflows.length})`} label={`Onprem Backup (${backupWorkflows.length})`}
value={3}
style={{ style={{
...tabStyle, ...tabStyle,
borderLeft: "1px solid rgba(255,255,255,0.3)",
marginLeft: 25, marginLeft: 25,
...(currTab === 3 ? tabActive : {}) ...(currTab === 3 ? tabActive : {})
}} }}
/> />
} }
{backgroundWorkflows.length > 0 &&
<Tab
label={`Background Processes`}
value={4}
style={{
...tabStyle,
borderLeft: "1px solid rgba(255,255,255,0.3)",
borderRight: "1px solid rgba(255,255,255,0.3)",
marginLeft: 25,
...(currTab === 4 ? tabActive : {})
}}
/>
}
<Tab <Tab
label="Org Forms" label="Org Forms"
value={5}
onClick={() => { onClick={() => {
navigate("/forms") navigate("/forms")
}} }}
@@ -4431,7 +4716,7 @@ const Workflows2 = (props) => {
...tabStyle, ...tabStyle,
marginRight: 0, marginRight: 0,
marginLeft: 25, marginLeft: 25,
...(currTab === 4 ? tabActive : {}) ...(currTab === 5 ? tabActive : {})
}} }}
/> />
</Tabs> </Tabs>
@@ -4664,7 +4949,22 @@ const Workflows2 = (props) => {
paddingRight: 1, paddingRight: 1,
gap: 4 gap: 4
}}> }}>
<Tooltip title="Explore Workflow Runs" placement="top">
<Tooltip title="Show/Hide Workflow Runs for top workflows" placement="top">
<IconButton
onClick={() => {
const newView = !showExecutionStats
localStorage.setItem("showExecutionStats", newView)
setShowExecutionStats(!showExecutionStats)
}}
color={showExecutionStats ? "primary" : "default"}
>
<BarChartIcon />
</IconButton>
</Tooltip>
<Tooltip title="Explore Workflow Runs (debugger)" placement="top">
<IconButton <IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle} style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => navigate("/workflows/debug")} onClick={() => navigate("/workflows/debug")}
@@ -4817,6 +5117,28 @@ const Workflows2 = (props) => {
) )
})} })}
{currTab === 4 && backgroundWorkflows.map((data, index) => {
// Shouldn't be a part of this list
if (data.public === true) {
return null
}
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
})}
{ {
currTab !== 1 ? null : currTab !== 1 ? null :
myWorkflows.length === 0 ? myWorkflows.length === 0 ?
@@ -5216,6 +5538,7 @@ const Workflows2 = (props) => {
{deleteModal} {deleteModal}
{exportVerifyModal} {exportVerifyModal}
{publishModal} {publishModal}
{aiAnnouncementModal}
{workflowDownloadModalOpen} {workflowDownloadModalOpen}
{/*!drawerOpen ? {/*!drawerOpen ?
@@ -5279,8 +5602,10 @@ const Workflows2 = (props) => {
width: '100%', width: '100%',
height: '100%', height: '100%',
} }
// Maybe use gridview or something, idk // Maybe use gridview or something, idk
return <div style={isSafari ? safariStyle : {zoom: 0.7, minHeight: "80vh",}}>{loadedCheck}</div>; //return <div style={isSafari ? safariStyle : {zoom: 0.7, minHeight: "80vh",}}>{loadedCheck}</div>;
return <div style={isSafari ? safariStyle : {minHeight: "80vh",}}>{loadedCheck}</div>;
}; };