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
+99 -28
View File
@@ -91,10 +91,13 @@ const Billing = memo((props) => {
const [currentTab, setCurrentTab] = useState(0)
const [allChildOrgs, setAllChildOrgs] = useState([])
const [allChildOrgsStats, setAllChildOrgsStats] = useState([])
const [statistics, setStatistics] = useState([])
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
useEffect(() => {
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) {
const percentage = ((userdata.app_execution_usage + userdata.app_executions_suborgs) / userdata.app_execution_limit) * 100;
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
setCurrentAppRunsInPercentage(Math.round(percentage));
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){
setIsLoggedIn(true)
}
}, [userdata]);
}, [monthlyAppRunsParent, monthlyAllSuborgExecutions, userdata]);
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 = {
padding: 20,
// maxWidth: 400,
@@ -1974,6 +2019,12 @@ const Billing = memo((props) => {
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 (
<Wrapper clickedFromOrgTab={clickedFromOrgTab}>
<div style={{ height: "100%", width: "100%"}}>
@@ -2373,8 +2424,8 @@ const Billing = memo((props) => {
<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}.
</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, }}>
{billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
<div style={{ display: 'flex', width: '50%', flexDirection: 'row', marginTop: 5, }}>
{/* {billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
isChildOrg ? null : (
<ConsultationManagement
globalUrl={globalUrl}
@@ -2382,7 +2433,7 @@ const Billing = memo((props) => {
selectedOrganization={selectedOrganization}
/>
)
) : null}
) : null} */}
<TrainingService />
</div>
</div>
@@ -2410,17 +2461,17 @@ const Billing = memo((props) => {
}}
/>
<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>
{userdata?.active_org?.creator_org?.length > 0 ? null :
(
<>
<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 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>
</>
)}
@@ -2602,12 +2653,7 @@ const Billing = memo((props) => {
<Tabs
value={currentTab}
onChange={(event, newValue) => {
setCurrentTab(-1)
// Force re-render
setTimeout(() => {
setCurrentTab(newValue)
}, 100);
}}
style={{ marginTop: 20 }}
TabIndicatorProps={{
@@ -2619,52 +2665,66 @@ const Billing = memo((props) => {
}
}}
>
{isChildOrg ? null :
<Tab
label="Parent Organization"
label="All Organization Stats"
style={{ textTransform: 'none',}}
value={0}
/>
{isCloud ?
/>}
<Tab
label="Cloud-Synced Stats"
label={isChildOrg ? "Organization Stats" : "Parent Organization Stats"}
style={{ textTransform: 'none',}}
value={1}
/>
: null}
{isChildOrg ? null :
<Tab
label="Child Organization Stats"
disabled={isChildOrg}
style={{ textTransform: 'none', }}
value={2}
/>}
{isCloud ?
<Tab
label="Cloud-Synced Stats"
style={{ textTransform: 'none', }}
value={3}
/>
: null}
</Tabs>
<div style={{paddingBottom: 200, minHeight: 750, }}>
{currentTab === 0 ?
<div style={{ marginTop: 30,}}>
{
currentTab === 0 ?
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
statistics={statistics}
monthlyAppRunsParent={monthlyAppRunsParent}
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
setMonthlyAllSuborgExecutions={setMonthlyAllSuborgExecutions}
setMonthlyAppRunsParent={setMonthlyAppRunsParent}
currentTab={currentTab}
/>
</div>
: currentTab === 1 ?
<div style={{ marginTop: 30,}}>
<div>
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
syncStats={true}
statistics={statistics}
monthlyAppRunsParent={monthlyAppRunsParent}
monthlyAllSuborgExecutions={monthlyAllSuborgExecutions}
setMonthlyAllSuborgExecutions={setMonthlyAllSuborgExecutions}
setMonthlyAppRunsParent={setMonthlyAppRunsParent}
currentTab={currentTab}
/>
</div>
:
: currentTab === 2 ?
<BillingStatsChildOrg
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
@@ -2675,6 +2735,17 @@ const Billing = memo((props) => {
setAllChildOrgs={setAllChildOrgs}
allChildOrgsStats={allChildOrgsStats}
setAllChildOrgsStats={setAllChildOrgsStats}
currentTab={currentTab}
/>
:
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
currentTab={currentTab}
syncStats={true}
/>
}
</div>
+85 -111
View File
@@ -45,6 +45,12 @@ const AppStats = (defaultprops) => {
inputWorkflows,
clickedFromOrgTab,
syncStats,
statistics,
monthlyAppRunsParent,
setMonthlyAppRunsParent,
monthlyAllSuborgExecutions,
setMonthlyAllSuborgExecutions,
currentTab
} = defaultprops;
const [keys, setKeys] = useState([])
@@ -57,7 +63,6 @@ const AppStats = (defaultprops) => {
const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("")
const [statistics, setStatistics] = useState(undefined);
const [filteredStatistics, setFilteredStatistics] = useState(undefined);
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) => {
@@ -219,6 +229,7 @@ const AppStats = (defaultprops) => {
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
if (statistics[statKey] === undefined || statistics[statKey] === null) {
setFilteredStatistics(statistics)
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
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())
foundstarttime.setDate(1)
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())
foundendtime.setDate(foundendtime.getDate() + 1)
// Check if endtime is after the daily statistics["date"] string
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
@@ -290,8 +308,20 @@ const AppStats = (defaultprops) => {
}
const date = new Date(item["date"])
if (date >= foundstarttime) {
if (date <= foundendtime) {
// Normalize the date to start of day for comparison
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)
}
}
@@ -326,6 +356,10 @@ const AppStats = (defaultprops) => {
workflowexecutions += item["workflow_executions"]
appexecutions += item["app_executions"]
if (currentTab === 0) {
appexecutions += (item["child_app_executions"] ?? 0)
}
estimatedcost += (item["app_executions"] * invocationCost)
}
@@ -344,6 +378,15 @@ const AppStats = (defaultprops) => {
setFilteredStatistics(tmpstats)
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) {
@@ -429,7 +472,7 @@ const AppStats = (defaultprops) => {
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(item["date"]),
data: inputdata["child_app_executions"]
data: item["child_app_executions"]
})
}
@@ -449,6 +492,12 @@ const AppStats = (defaultprops) => {
}
}
// Only add today's data if endTime is not set or if today falls within the selected date range
const today = new Date()
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
(new Date(endTime) >= today.setHours(0, 0, 0, 0))
if (shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
@@ -467,8 +516,6 @@ const AppStats = (defaultprops) => {
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
//setApprunCosts(appcostRuns)
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
@@ -484,6 +531,7 @@ const AppStats = (defaultprops) => {
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
if (childorgappRuns["data"].length > 0) {
@@ -496,47 +544,6 @@ const AppStats = (defaultprops) => {
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 = {
textAlign: "center",
padding: "40px",
@@ -656,15 +663,19 @@ const AppStats = (defaultprops) => {
]
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">
All shown statistics are gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/stats`}
target="_blank"
style={{ textDecoration: "none", color: theme.palette.linkColor,}}
>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={{}}/>
{syncStats !== true ? null :
"PS: You are currently looking at data from your onprem synced org"}
@@ -675,7 +686,7 @@ const AppStats = (defaultprops) => {
{filteredStatistics !== undefined ?
<div style={{flex: 1, display: "flex", textAlign: "center",}}>
{syncStats == true ? null :
{/* {syncStats == true ? null :
<Tooltip title={
<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}.
@@ -695,7 +706,7 @@ const AppStats = (defaultprops) => {
</Typography>
</Box>
</Tooltip>
}
} */}
{syncStats === true ? null :
<Tooltip title={
@@ -714,7 +725,7 @@ const AppStats = (defaultprops) => {
</Tooltip>
}
{syncStats === true ? null :
{syncStats === true || currentTab === 0 ? null :
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
Workflow runs in the selected period
@@ -731,7 +742,7 @@ const AppStats = (defaultprops) => {
</Tooltip>
}
{syncStats === true ? null :
{/* {syncStats === true ? null :
<Tooltip title={
<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}.
@@ -746,15 +757,9 @@ const AppStats = (defaultprops) => {
</Typography>
</Box>
</Tooltip>
}
</div>
: null}
</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 style={{ display: "flex", flexDirection: "column", gap: "10px", justifyContent: 'center', alignItems: 'flex-start', paddingTop: 10 }}>
<div
style={{
display: "flex",
@@ -878,64 +883,33 @@ const AppStats = (defaultprops) => {
</div>
</div>
</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>
</LocalizationProvider>
)}
: null}
</div>
</div>
{appRuns === undefined ?
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
:
<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
:
<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
:
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/>
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"} border={false} />
}
{/*appRunCosts === undefined ?
@@ -944,7 +918,7 @@ const AppStats = (defaultprops) => {
<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", }}>
{resultLoading ?
<div style={{margin: "auto", alignItems: "center", width: 350, height: "100%", }}>
@@ -1000,7 +974,7 @@ const AppStats = (defaultprops) => {
)
const dataWrapper = (
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
<div style={{ maxWidth: 1366, margin: "auto", }}>{data}</div>
);
return dataWrapper;
+102 -12
View File
@@ -35,6 +35,7 @@ import {
InputLabel,
Pagination,
PaginationItem,
Avatar,
} from "@mui/material";
import {
@@ -134,7 +135,7 @@ const CacheView = memo((props) => {
// Direct category migration from ../components/Files.jsx
const [selectAllChecked, setSelectAllChecked] = 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 [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
@@ -322,7 +323,6 @@ const CacheView = memo((props) => {
}
var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache`
if (category !== undefined && category !== null && category !== "default" && category !== "") {
url += "?category=" + category.replaceAll(" ", "_")
} 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"]
for (var key in responseJson.keys) {
var foundcategory = responseJson.keys[key].category
@@ -585,7 +585,7 @@ const CacheView = memo((props) => {
})
.then((responseJson) => {
setAddCache(responseJson);
toast("New key added Successfully!");
toast.success("New key added!");
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
setModalOpen(false);
})
@@ -699,6 +699,7 @@ const CacheView = memo((props) => {
</IconButton>
</Tooltip>
</div>
<TextField
color="primary"
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: 0, }}
@@ -1461,12 +1462,25 @@ const CacheView = memo((props) => {
sortable: true,
},
{
width: 600,
width: 540,
field: 'value',
filterable: true,
headerName: 'Value',
renderCell: (props) => {
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)
return (
@@ -1485,8 +1499,8 @@ const CacheView = memo((props) => {
backgroundColor: theme.palette.platformColor,
border: theme.palette.defaultBorder,
padding: 5,
minWidth: 600,
maxHeight: 600,
minWidth: 500,
maxHeight: 500,
overflowY: "auto",
}}
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',
headerName: 'Actions',
@@ -1520,7 +1602,7 @@ const CacheView = memo((props) => {
return (
<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
disabled={data.workflow_id?.length === 0}
style={{}}
@@ -1528,7 +1610,7 @@ const CacheView = memo((props) => {
<OpenInNewIcon
style={{
color:
data.workflow_id?.length !== 0
data.workflow_id?.length === 36
? "#FF8444"
: "grey",
}}
@@ -1539,6 +1621,7 @@ const CacheView = memo((props) => {
title={"Go to workflow"}
style={{}}
aria-label={"Download"}
placement="left"
>
<span>
<a
@@ -1552,13 +1635,13 @@ const CacheView = memo((props) => {
>
<IconButton
disabled={data.workflow_id?.length ===0}
style={{marginLeft: 10}}
style={{marginLeft: 0}}
>
<OpenInNewIcon
style={{
width: 24, height: 24,
color:
data.workflow_id?.length !== 0
data.workflow_id?.length === 36
? "#FF8444"
: "grey",
}}
@@ -1825,6 +1908,13 @@ const CacheView = memo((props) => {
>
Learn more
</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>
</div>
@@ -2244,7 +2334,7 @@ const CacheView = memo((props) => {
setSelectedRows(newSelection);
}}
keepNonExistentRowsSelected={false}
getRowId={(row) => row.key}
getRowId={(row) => `${row?.key}_${row?.category}`}
autoHeight={true}
sx={{
+65 -28
View File
@@ -23,8 +23,12 @@ import {
import {
Rocket as RocketIcon,
FilterAlt as FilterAltIcon,
Add as AddIcon,
} from '@mui/icons-material';
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CollectIngestModal = (props) => {
const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props;
@@ -86,11 +90,13 @@ const CollectIngestModal = (props) => {
}
const IngestItem = (props) => {
const { type, appCategory, index } = props
const { type, appCategory, index, webhook } = props
const [hovering, setHovering] = useState(false);
const [selectedApps, setSelectedApps] = useState([]);
//const [isFinished, setIsFinished] = useState(false);
const [showAppsearch, setShowAppsearch] = useState(false);
const [algoliaOptions, setAlgoliaOptions] = useState([]);
const appname = type
const ingestedAmount = 20
@@ -167,7 +173,7 @@ const CollectIngestModal = (props) => {
//<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12}
style={{
minHeight: hovering ? 250 : 140,
minHeight: hovering ? 200 : 200,
maxHeight: hovering ? "auto" : 140,
cursor: "pointer",
position: "relative",
@@ -176,7 +182,7 @@ const CollectIngestModal = (props) => {
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}`,
textAlign: "center",
marginBottom: 5,
marginBottom: 10,
overflow: "hidden",
}}
@@ -189,21 +195,42 @@ const CollectIngestModal = (props) => {
}}
onMouseLeave={() => setHovering(false)}
>
<div style={{marginTop: 35, marginBottom: 35, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
<div style={{display: "flex", }}>
<Typography variant="h4" style={{marginTop: 10, }}>
<div style={{flex: 1, margin: "auto", marginTop: 50, }}>
{appname}
</Typography>
<div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
{selectedApps.map((app, index) => {
// 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>
)
})}
<Tooltip title="Select Apps" placement="top">
<IconButton
style={{marginLeft: 10, marginRight: 50, }}
variant="outlined"
color="secondary"
onClick={() => {
setShowAppsearch(!showAppsearch)
}}
>
<AddIcon style={{color: theme.palette.primary.main, }} />
</IconButton>
</Tooltip>
</div>
<div style={{display: "flex", width: 400, margin: "auto", }}>
{matchingapps.length > 0 ?
{showAppsearch ?
<Autocomplete
style={{flex: 1, }}
style={{flex: 1, maxWidth: 200, minWidth: 200, margin: "auto", marginTop: 10, }}
multiple
filterSelectedOptions
options={matchingapps}
@@ -235,19 +262,12 @@ const CollectIngestModal = (props) => {
)
}}
/>
: null}
:
<Button
style={{flex: 1, }}
style={{width: 250, margin: 25, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
//if (foundMatchingWorkflow !== null) {
// toast.error("Deletion not implemented for this POC. Please delete the workflow.")
//}
//else {
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
@@ -261,14 +281,30 @@ const CollectIngestModal = (props) => {
}
startIngestion(appname, newapps, appCategory, index)
//}
}}>
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
}}
>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
:
"Start Ingestion"
}
</Button>
}
</div>
<div style={{flex: 1, marginTop: 50, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
<Typography variant="h4" style={{marginTop: 10, }}>
{appname}
</Typography>
</div>
</div>
{foundMatchingWorkflow !== null ?
@@ -319,7 +355,7 @@ const CollectIngestModal = (props) => {
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: 500,
minWidth: 850,
minHeight: 700,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
@@ -350,13 +386,14 @@ const CollectIngestModal = (props) => {
</Typography>
<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="Ingest Assets" appCategory={"assets"} index={2} />
<IngestItem type="Ingest Users " appCategory={"users"} index={2} />
<IngestItem type="Enable Search" index={2} />
<IngestItem type="Enable Mitre Att&ck techniques" index={2} />
<IngestItem type="Enable Detection Rules" index={2} />
<IngestItem type="Ingest Logs" index={2} />
<IngestItem type="Track Assets" appCategory={"assets"} index={2} />
</Grid>
</DialogContent>
</Dialog>
+229 -29
View File
@@ -3,6 +3,7 @@ import { getTheme } from '../theme.jsx';
import { isMobile } from "react-device-detect"
import { MuiChipsInput } from "mui-chips-input";
import { toast } from "react-toastify"
import ReactGA from 'react-ga4';
import UsecaseSearch from "../components/UsecaseSearch.jsx"
import WorkflowGrid from "../components/WorkflowGrid.jsx"
import dayjs from 'dayjs';
@@ -63,7 +64,10 @@ import {
Add as AddIcon,
Remove as RemoveIcon,
EditNote as EditNoteIcon,
AutoAwesome as AutoAwesomeIcon
AutoAwesome as AutoAwesomeIcon,
CloudUpload as CloudUploadIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon
} from "@mui/icons-material";
const EditWorkflow = (props) => {
@@ -95,6 +99,11 @@ const EditWorkflow = (props) => {
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();
useEffect(() => {
@@ -103,6 +112,59 @@ const EditWorkflow = (props) => {
}
}, [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) {
setTimeout(() => {
const foundScroll = document.getElementById(scrollTo)
@@ -308,7 +370,7 @@ const EditWorkflow = (props) => {
variant="contained"
style={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true}
disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true || uploadedImage !== null}
onClick={() => {
setSubmitLoading(true)
@@ -395,24 +457,46 @@ const EditWorkflow = (props) => {
<Tooltip placement="top" arrow
title={
<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>
}
>
<span>
<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"
onClick={async () => {
// Check if description is provided in the visible field for new workflows
if (!innerWorkflow.default_return_value || innerWorkflow.default_return_value.trim().length === 0) {
toast.error("You need to describe what you want to generate so the AI can auto generate the entire workflow");
// Track AI Generate button click
if (isCloud) {
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;
}
setAiGenerateLoading(true);
toast.info("Creating workflow...");
let workflowId = null;
try {
// Step 1: Create basic workflow WITHOUT using setNewWorkflow (to avoid modal closing)
const workflowData = {
@@ -455,15 +539,20 @@ const EditWorkflow = (props) => {
return;
}
const workflowId = workflowJson.id;
console.log("Created workflow with ID:", workflowId);
workflowId = workflowJson.id;
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 = {
query: innerWorkflow.default_return_value,
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", {
method: "POST",
headers: {
@@ -477,36 +566,55 @@ const EditWorkflow = (props) => {
const json = await aiResponse.json();
// Handle AI response and provide feedback
if (aiResponse.status !== 200) {
console.log("AI generation failed, but workflow created:", json.message || "Unexpected response");
toast.warning("Workflow created successfully, but AI generation failed. Opening workflow editor...");
} else if (json.success === true && typeof json.message === "string") {
// AI "rejection" message
console.log("AI rejected request:", json.message);
toast.warning(`AI: ${json.message}. Opening empty workflow editor...`);
} else if (json.success === false) {
console.log("AI generation failed:", json.message || "Operation failed");
toast.warning("AI generation failed. Opening empty workflow editor...");
} else if (!json || Object.keys(json).length === 0) {
console.log("Empty AI response");
toast.warning("AI returned empty response. Opening workflow editor...");
if (aiResponse.status === 422) {
// AI rejection with reason
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "ai_rejected",
label: workflowId,
});
}
toast.warning(`AI: ${json.reason || "Request rejected"}. Opening workflow editor...`);
} else if (aiResponse.status !== 200) {
// Other HTTP errors
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "generation_failed",
label: workflowId,
});
}
toast.warning("Workflow created, but AI generation failed. Opening workflow editor...");
} else {
// Successful generation
if (isCloud) {
ReactGA.event({
category: "AIGeneratedNewWorkflow",
action: "generation_success",
label: workflowId,
});
}
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(() => {
setModalOpen(false);
setAiGenerateLoading(false);
window.location.href = `/workflows/${workflowId}`;
}, 1500); // Give user time to read the final message
}, 1500);
} 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);
setAiGenerateLoading(false);
// Don't close modal on error so user can try again
}
}}
>
@@ -753,6 +861,98 @@ const EditWorkflow = (props) => {
/>
</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 ?
<div style={{ marginTop: 50, }}>
<TextField
@@ -1104,7 +1304,7 @@ const EditWorkflow = (props) => {
</Typography>
<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>
<FormControl style={{ marginTop: 15, }}>
+5 -1
View File
@@ -418,6 +418,10 @@ const LicencePopup = (props) => {
showSupport = true
}
if (userdata?.app_execution_limit >= 300000) {
top_text = "Enterprise Plan"
}
if (subscription.name.includes("Open Source")) {
top_text = "Open Source"
showSupport = true
@@ -761,7 +765,7 @@ const LicencePopup = (props) => {
{
isCloud ?
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.`
:
+8 -8
View File
@@ -87,15 +87,15 @@ const menuData = {
],
Services: [
{
title: "Professional Services",
title: "Proof of Concept",
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",
path: "/professional-services",
path: "/poc",
gaData: {
category: "navbar",
action: "services_click",
label: "professional_services_click"
label: "proof_of_concept_click"
}
},
{
@@ -103,7 +103,7 @@ const menuData = {
description:
"Support to help you build automations with confidence.",
icon: "/images/Support.svg",
path: "/contact?category=support",
path: "/support",
gaData: {
category: "navbar",
action: "services_click",
@@ -1486,7 +1486,7 @@ const Navbar = (props) => {
}
}}
>
Become a partner
Become a Partner
</Button>
<Button
fullWidth
@@ -1516,7 +1516,7 @@ const Navbar = (props) => {
}
}}
>
Discover partners
Discover Partners
</Button>
</Box>
</Box>
@@ -1661,7 +1661,7 @@ const Navbar = (props) => {
const topbarHeight = showTopbar ? 40 : 0
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}}>
{/* uncommit this to show topbar for release */}
{/* <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={{ display: "flex" }}>
<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
required
style={{
@@ -499,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
</div>
{userdata?.support ? (
<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 }}>
<Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
@@ -525,13 +525,13 @@ const OrgHeaderexpandedNew = (props) => {
{isCloud ? (
<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} />
</div>
) : null}
</div>
<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" }}>
<TextField
required
+46 -26
View File
@@ -150,6 +150,7 @@ const ParsedAction = (props) => {
globalUrl,
setSelectedActionEnvironment,
requiresAuthentication,
setRequiresAuthentication,
hideExtraTypes,
scrollConfig,
setScrollConfig,
@@ -1076,8 +1077,8 @@ const ParsedAction = (props) => {
var toReplace = event.target.value
if (!toReplace.startsWith("{") && !toReplace.startsWith("[")) {
toReplace = toReplace.replaceAll('\\"', '"').replaceAll('"', '\\"')
if (!toReplace?.startsWith("{") && !toReplace?.startsWith("[")) {
toReplace = toReplace?.replaceAll('\\"', '"').replaceAll('"', '\\"')
}
if (
@@ -1234,7 +1235,7 @@ const ParsedAction = (props) => {
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)
const parsedvalue = event.target.value
if (parsedvalue.includes(".#")) {
@@ -1250,7 +1251,7 @@ const ParsedAction = (props) => {
selectedAction.parameters[1].value = splitparsed[1]
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,
})
} else if (selectedAction.parameters[1].value.includes(".#")) {
@@ -1293,7 +1294,7 @@ const ParsedAction = (props) => {
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
if (paramcheck !== undefined) {
// 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)
if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) {
paramcheck["value_replace"] = [{
@@ -2056,14 +2057,14 @@ const ParsedAction = (props) => {
value={appActionName}
onChange={(event) => {
let newValue = event.target.value
newValue = newValue.replaceAll(" ", "_")
newValue = newValue?.replaceAll(" ", "_")
setAppActionName(newValue)
}}
onBlur={(e) => {
// Copy the name value
const name = e.target.value
const parsedBaseLabel = "$" + prevActionName.toLowerCase().replaceAll(" ", "_")
const newname = "$" + name.toLowerCase().replaceAll(" ", "_")
const parsedBaseLabel = "$" + prevActionName?.toLowerCase()?.replaceAll(" ", "_")
const newname = "$" + name?.toLowerCase()?.replaceAll(" ", "_")
// Check if it's the same as the current name in use
//if (name === selectedAction.label) {
@@ -2288,9 +2289,9 @@ const ParsedAction = (props) => {
)}
{selectedApp.name !== undefined &&
selectedAction.authentication !== null &&
((selectedAction.authentication !== null &&
selectedAction.authentication !== undefined &&
selectedAction.authentication.length === 0 &&
selectedAction.authentication.length === 0) || isAgent || isIntegration) &&
requiresAuthentication ? (
<div style={{ marginTop: 15 }}>
<Tooltip
@@ -2303,6 +2304,7 @@ const ParsedAction = (props) => {
color="primary"
style={{
textTransform: "none",
fontWeight: "bold",
}}
fullWidth
variant="contained"
@@ -2315,7 +2317,7 @@ const ParsedAction = (props) => {
}}
>
<AddIcon style={{ marginRight: 10 }} /> Authenticate{" "}
{selectedApp.name.replaceAll("_", " ")}
{isAgent || isIntegration ? "API" : selectedApp.name?.replaceAll("_", " ")}
</Button>
</span>
</Tooltip>
@@ -2813,7 +2815,7 @@ const ParsedAction = (props) => {
}}
filterOptions={(options, { inputValue }) => {
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
}}
@@ -2823,8 +2825,8 @@ const ParsedAction = (props) => {
}
const newname = (
option.name.charAt(0).toUpperCase() + option.name.substring(1)
).replaceAll("_", " ");
option.name?.charAt(0).toUpperCase() + option.name?.substring(1)
)?.replaceAll("_", " ");
return newname;
}}
@@ -2876,7 +2878,7 @@ const ParsedAction = (props) => {
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 extraDescription = ""
@@ -3151,8 +3153,20 @@ const ParsedAction = (props) => {
setSelectedAction(selectedAction)
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
src={app.large_image}
style={{
@@ -3227,8 +3241,8 @@ const ParsedAction = (props) => {
}
const newname = (
option.app_name.charAt(0).toUpperCase() + option.app_name.substring(1)
).replaceAll("_", " ");
option.app_name?.charAt(0).toUpperCase() + option.app_name?.substring(1)
)?.replaceAll("_", " ");
return newname;
}}
options={selectedAction.matching_actions}
@@ -3262,8 +3276,8 @@ const ParsedAction = (props) => {
newActionname = (
newActionname.charAt(0).toUpperCase() +
newActionname.substring(1)
).replaceAll("_", " ");
newActionname?.substring(1)
)?.replaceAll("_", " ");
return (
<div style={{ display: "flex" }}>
@@ -3505,6 +3519,8 @@ const ParsedAction = (props) => {
if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") {
// Show a key popout button
showCacheConfig = true
} else if (data.name === "category" && selectedAction.app_name === "Shuffle Tools") {
showCacheConfig = true
}
var disabled = false;
@@ -3732,6 +3748,7 @@ const ParsedAction = (props) => {
}
const clickedFieldId = "rightside_field_" + count;
const parameterFieldId = "param_" + data.name.replace(/[^a-zA-Z0-9_-]/g, '_');
var baseHelperText = ""
if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) {
@@ -3748,8 +3765,8 @@ const ParsedAction = (props) => {
}
tmpitem = (
tmpitem.charAt(0).toUpperCase() + tmpitem.substring(1)
).replaceAll("_", " ");
tmpitem?.charAt(0).toUpperCase() + tmpitem?.substring(1)
)?.replaceAll("_", " ");
if (tmpitem === "Username basic") {
tmpitem = "Username"
@@ -3873,6 +3890,9 @@ const ParsedAction = (props) => {
autofill="off"
autoComplete="off"
id={clickedFieldId}
data-parameter={data.name}
data-param-id={parameterFieldId}
name={data.name}
disabled={disabled}
style={{
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
@@ -4315,7 +4335,7 @@ const ParsedAction = (props) => {
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
var selected = false
@@ -4378,9 +4398,9 @@ const ParsedAction = (props) => {
? values[0].autocomplete
: "$" + values[0].autocomplete;
toComplete = toComplete.toLowerCase().replaceAll(" ", "_");
toComplete = toComplete?.toLowerCase()?.replaceAll(" ", "_");
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;
}
@@ -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
if (data.variant === undefined || data.variant === null) {
data.variant = "STATIC_VALUE"
+17 -16
View File
@@ -222,7 +222,7 @@ const PartnerDetails = (props) => {
<div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}>
<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
</Typography>
<Skeleton
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
/>
</div> */}
<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
</div>
<Skeleton
@@ -282,7 +282,7 @@ const PartnerDetails = (props) => {
/>
</div>
<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
</Typography>
<Skeleton
@@ -297,7 +297,7 @@ const PartnerDetails = (props) => {
/>
</div>
<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
</div>
<Skeleton
@@ -313,7 +313,7 @@ const PartnerDetails = (props) => {
</div>
</div>
<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
</Typography>
<Skeleton
@@ -329,7 +329,7 @@ const PartnerDetails = (props) => {
</div>
<div>
<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
</Typography>
<Skeleton
@@ -344,7 +344,7 @@ const PartnerDetails = (props) => {
/>
</div>
<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
</Typography>
<Skeleton
@@ -359,7 +359,7 @@ const PartnerDetails = (props) => {
/>
</div>
<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
</Typography>
<Skeleton
@@ -397,7 +397,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Name
</Typography>
@@ -544,6 +544,7 @@ const PartnerDetails = (props) => {
style={{
marginRight: "12px",
color: theme.palette.text.primary,
fontFamily: theme?.typography?.fontFamily
}}
>
Solutions
@@ -586,7 +587,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Region
</Typography>
@@ -602,7 +603,7 @@ const PartnerDetails = (props) => {
<div style={{ alignItems: "flex-start", marginLeft: 13, display: "flex", flexDirection: "column" }}>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Country
</Typography>
@@ -686,7 +687,7 @@ const PartnerDetails = (props) => {
<div style={{ marginTop: "10px", }} />
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Description
</Typography>
@@ -746,7 +747,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Website URL
</Typography>
@@ -809,7 +810,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Article URL
</Typography>
@@ -872,7 +873,7 @@ const PartnerDetails = (props) => {
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Contact Email
</Typography>
@@ -894,7 +895,7 @@ const PartnerDetails = (props) => {
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="https://www.example.com"
placeholder="support@shuffler.io"
type="name"
id="standard-required"
margin="normal"
+25 -12
View File
@@ -42,7 +42,7 @@ const PartnerSettings = (props) => {
// Partner Types handling : Getting from org status
useEffect(() => {
const partnerTypes = {};
userdata?.org_status.forEach(status => {
userdata?.org_status?.forEach(status => {
if (status.includes("_partner")) {
partnerTypes[status] = true;
}
@@ -122,6 +122,12 @@ const PartnerSettings = (props) => {
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);
const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id;
const data = {
@@ -153,17 +159,15 @@ const PartnerSettings = (props) => {
})
.then((response) => {
setIsPublishing(false);
if (response.status !== 200) {
if (response.status === 200) {
toast.success("Partner details successfully updated");
} else {
toast.error("Failed to publish partner");
}
return response.json();
})
.then((responseJson) => {
toast.success("Partner details successfully updated");
})
.catch((error) => {
setIsPublishing(false);
toast.error("Failed to update partner details: " + error?.message);
toast.error("Failed to update partner details: " + error?.reason);
})
}
@@ -237,8 +241,16 @@ const PartnerSettings = (props) => {
sx={{display:"flex", alignItems:"flex-start", gap:2, justifyContent:"flex-start"
}}>
<Typography variant='h3' sx={{ marginBottom: "15px", marginTop: 0, }}>Configuration</Typography>
{Object?.entries(partnerTypes)?.map(([key, value]) => (
{Object?.entries(partnerTypes)
?.filter(([key, value]) => key !== "distribution_partner")
?.map(([key, value]) => {
let displayText = key?.replace("_", " ")?.replace(/\b\w/g, char => char.toUpperCase());
if (displayText === "Tech Partner") {
displayText = "Technology Partner";
}
return (
<Box
key={key}
sx={{
display: "flex",
alignItems: "center",
@@ -260,9 +272,10 @@ const PartnerSettings = (props) => {
color: partnerTypeColors[key],
}}
>
{key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())}
{displayText}
</Box>
))}
);
})}
</Box>
<Box
sx={{
@@ -318,9 +331,9 @@ const PartnerSettings = (props) => {
boxShadow: "none",
marginRight: 4,
px: 3,
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50",
backgroundColor: partnerData?.public ? "#FD4C62" : "#2BC07E",
"&:hover": {
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50"
backgroundColor: partnerData?.public ? "#FD4C62" : "#2BC07E"
}
}}
variant="contained"
+2 -3
View File
@@ -66,18 +66,17 @@ const PartnerTab = (props) => {
})
.then((response) => {
if (response.status !== 200) {
toast("Failed to get partner data")
toast.info("No partner details found");
}
return response.json();
})
.then((responseJson) => {
if(responseJson.success) {
setPartnerData(responseJson?.partner);
console.log("responseJson", responseJson)
setLoadingPartnerData(false);
}else{
setLoadingPartnerData(false);
toast(responseJson?.reason)
console.error(responseJson?.reason)
}
})
.catch((error) => {
+192 -22
View File
@@ -47,6 +47,8 @@ import {
RestartAlt as RestartAltIcon,
ArrowForward as ArrowForwardIcon,
KeyboardReturn as KeyboardReturnIcon,
FormatIndentIncrease as FormatIndentIncreaseIcon,
Fullscreen as FullscreenIcon,
} from '@mui/icons-material';
@@ -143,6 +145,34 @@ const CodeEditor = (props) => {
handleConditionFieldChange,
} = 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 {codelang, setcodelang} = props
@@ -184,6 +214,7 @@ const CodeEditor = (props) => {
"result": baseResult,
})
const [executing, setExecuting] = useState(false)
const [fullScreenModeEnabled, setFullScreenModeEnabled] = useState(fullScreenMode === true || fullScreenMode === "true" || localStorage.getItem("codeEditorFullScreen") === "true")
const liquidOpen = Boolean(anchorEl);
const mathOpen = Boolean(anchorEl2);
@@ -200,6 +231,22 @@ const CodeEditor = (props) => {
expectedOutput(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();
const [searchParams] = useSearchParams();
const actionId = searchParams.get('action_id');
@@ -217,7 +264,13 @@ const CodeEditor = (props) => {
}
const action = workflow?.actions?.find(action => action.id === actionId);
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);
// Update available variables when action changes
@@ -230,7 +283,13 @@ const CodeEditor = (props) => {
}
const trigger = workflow?.triggers?.find(trigger => trigger.id === triggerId);
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);
// Update available variables when trigger changes
@@ -244,7 +303,13 @@ const CodeEditor = (props) => {
}
const condition = selectedEdge?.conditions?.find(condition => condition.id === conditionId);
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);
// Update available variables when condition changes
updateAvailableVariables(actionlist);
@@ -963,7 +1028,6 @@ const CodeEditor = (props) => {
// vs
// $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.
console.log("FOUNDLOC: ", fixedVariable, foundlocation)
for (var j = 0; j < actionlist.length; j++) {
if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) {
continue
@@ -990,7 +1054,6 @@ const CodeEditor = (props) => {
}
try {
console.log("REPLACE: ", foundlocation, fixedVariable, newvalue)
if (newvalue !== "") {
if (foundlocation === -1) {
input = input.replace(fixedVariable, newvalue, 1)
@@ -1303,7 +1366,7 @@ const CodeEditor = (props) => {
editor.completers = [customCompleter]
}
if (fullScreenMode) {
if (fullScreenMode && fullScreenModeEnabled === true) {
return (
<AceEditor
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 { innerdata, parsedPaths, defaultExpanded } = option
@@ -1526,15 +1643,15 @@ const CodeEditor = (props) => {
// zIndex: 12501,
pointerEvents: "auto",
color: theme.palette.DialogStyle.color,
minWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile || isWorkflowEditor ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor ? "100%" : "700px",
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px",
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,
opacity: isWorkflowEditor ? 0.93 : 1,
opacity: isWorkflowEditor || fullScreenModeEnabled ? 0.93 : 1,
},
}}
>
@@ -1549,6 +1666,7 @@ const CodeEditor = (props) => {
</Tooltip>
: null}
{fullScreenModeEnabled ? null :
<Tooltip
color="primary"
title={`Move window`}
@@ -1559,8 +1677,8 @@ const CodeEditor = (props) => {
style={{
zIndex: 5000,
position: "absolute",
top: 6,
right: 56,
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 166 : 66,
color: "grey",
cursor: "move",
@@ -1571,17 +1689,43 @@ const CodeEditor = (props) => {
<DragIndicatorIcon />
</IconButton>
</Tooltip>
}
<Tooltip
color="primary"
title={`Close window without saving`}
placement="left"
title={fullScreenModeEnabled ? `Exit Fullscreen Mode` : `Enter Fullscreen Mode`}
placement="top"
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 6,
right: 6,
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 136 : 36,
color: "grey",
}}
onClick={() => {
setFullScreenModeEnabled(!fullScreenModeEnabled)
localStorage.setItem("codeEditorFullScreen", !fullScreenModeEnabled)
}}
>
{!fullScreenModeEnabled ?
<FullscreenIcon />
:
<FullscreenExitIcon />
}
</IconButton>
</Tooltip>
<Tooltip
color="primary"
title={`Close window without saving`}
placement="right"
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 106 : 6,
color: "grey",
}}
onClick={() => {
@@ -2127,6 +2271,30 @@ const CodeEditor = (props) => {
marginLeft: 100,
}}
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={() => {
if (fixExample !== undefined) {
const newExample = fixExample(editorData.example)
@@ -2196,14 +2364,15 @@ const CodeEditor = (props) => {
value={localcodedata}
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
theme="gruvbox"
height={isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : isWorkflowEditor ? "90vw" : "100%"}
height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"}
markers={markers}
highlightActiveLine={false}
enableBasicAutocompletion={true}
completers={[customCompleter]}
showPrintMargin={false}
style={{
wordBreak: "break-word",
@@ -2351,8 +2520,9 @@ const CodeEditor = (props) => {
style={{
border: `1px solid rgba(255, 255, 255, 0.15)`,
position: "absolute",
top: 20,
right: 100,
top: fullScreenModeEnabled ? 50 : 20,
right: fullScreenModeEnabled ? 240 : 120,
maxHeight: 35,
minWidth: 70,
zIndex: 1200,
@@ -2466,8 +2636,8 @@ const CodeEditor = (props) => {
borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`,
padding: 10,
maxHeight: 190,
minheight: 190,
maxHeight: fullScreenModeEnabled ? 300 : 190,
minheight: fullScreenModeEnabled ? 300 : 190,
overflow: "auto",
}}
collapsed={false}
@@ -2528,7 +2698,7 @@ const CodeEditor = (props) => {
</div>
<div style={{ display: 'flex', width: isWorkflowEditor ? "90%" : "100%", }}>
<div style={{ display: 'flex', width: fullScreenModeEnabled ? "92%" : isWorkflowEditor ? "90%" : "100%", }}>
<Button
style={{
height: 35,
+14 -4
View File
@@ -39,6 +39,7 @@ const AgentUI = (props) => {
const [buttonState, setButtonState] = useState("timeline")
const [execution, setExecution] = useState(null)
const [agentActionResult, setAgentActionResult] = useState(null)
const [agentRequestLoading, setAgentRequestLoading] = useState(false)
const [data, setData] = useState({})
const [openIndexes, setOpenIndexes] = useState([])
const [disableButtons, setDisableButtons] = useState(false)
@@ -571,8 +572,9 @@ const AgentUI = (props) => {
}
const submitInput = (inputText) => {
toast.info("Submitting AI Agent input: " + inputText);
//toast.info("Submitting AI Agent input: " + inputText);
setAgentRequestLoading(true)
//setShowAgentStarter(false);
//GetExecution(execution?.execution_id, execution?.node_id, execution?.authorization);
@@ -615,10 +617,11 @@ const AgentUI = (props) => {
credentials: "include",
})
.then((response) => {
setAgentRequestLoading(false)
return response.json()
})
.then((responseJson) => {
toast.success("Got response!")
//toast.success("Got response!")
console.log("Agent run response: ", responseJson)
if (responseJson.success === true && responseJson.authorization !== undefined && responseJson.execution_id !== undefined) {
@@ -628,6 +631,7 @@ const AgentUI = (props) => {
}
})
.catch((error) => {
setAgentRequestLoading(false)
toast.error("Error: " + error)
})
@@ -651,15 +655,21 @@ const AgentUI = (props) => {
Shuffle AI Agents
</Typography>
<TextField
label="Agent Input"
label="What do you want to do?"
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 || ""}
onChange={(e) => {
setActionInput(e.target.value)
}}
InputProps={{
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit">
<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) => {
if (response.status !== 200) {
console.log("Failed to activate");
console.log("Failed to activate: " + response.statusText);
}
return response.json();
@@ -776,27 +776,27 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (responseJson.success === false) {
if (action === undefined || action === null) {
if (responseJson.reason !== undefined) {
toast("Failed to activate the app: "+responseJson.reason);
toast.warn("Failed to activate the app: "+responseJson.reason);
} else {
toast("Failed to activate the app");
toast.warn("Failed to activate the app");
}
} else {
if (responseJson.reason !== undefined) {
toast("Failed to perform action: "+responseJson.reason);
toast.warn("Failed to perform action: "+responseJson.reason);
} 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 {
if ((checkLogin !== undefined && checkLogin !== null) && !multiple_request) {
if (!showDistributionPopup && (checkLogin !== undefined && checkLogin !== null) && !multiple_request) {
checkLogin()
}
if (action === undefined || action === null) {
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 {
toast("App activated for your organization!")
toast.success("App activated for your organization!")
}
} else {
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) => {
toast(error.toString());
toast.error(error.toString());
});
};
@@ -1450,7 +1450,12 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
const index = searchClient.initIndex("appsearch");
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
.search(appname)
@@ -3921,19 +3926,22 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (action === "activate_all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
);
)
// run app activation request for each org
const orgIds = childOrgs.map((data) => data.id);
// run app activation requset for each org
orgIds.forEach((orgId) => {
activateApp("activate", orgId, true)
});
})
setTimeout(() => {
toast.success("App activated for all sub-orgs");
}, 5000);
}, 5000)
} else if (action === "deactivate_all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
);
)
const orgIds = childOrgs.map((data) => data.id);
// run app deactivation request for each org
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.");
return;
}
activateApp("activate", id);
} else if (action === "deactivate_single") {
if (id === null) {
toast.error("Please select a sub-org to deactivate the app for.");
return;
}
activateApp("deactivate", id);
}
};
@@ -3973,8 +3983,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "600px",
minHeight: "320px",
minWidth: 600,
minHeight: 320,
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
@@ -3997,9 +4007,12 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
pr: 1,
pb: 1,
}}
style={{
padding: "50px 50px 50px 50px",
}}
>
<Typography variant="h6" fontWeight={600} color="text.primary">
Select sub-org to distribute App
Suborg App Distribution
</Typography>
<IconButton
onClick={() => setShowDistributionPopup(false)}
+16 -12
View File
@@ -949,29 +949,33 @@ const CustomLabelDropdown = connectRefinementList(LabelDropdown);
const filterApps = (apps, searchQuery, selectedCategory, selectedLabel) => {
if (!Array.isArray(apps)) return [];
const normalizedSearchQuery = (searchQuery || "").toLowerCase();
return apps.filter((app) => {
if (!app) return false;
const matchesSearchQuery = (
searchQuery === "" || // If searchQuery is empty, match all apps
app.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(app.tags && app.tags.some(tag =>
tag.toLowerCase().includes(searchQuery.toLowerCase())
normalizedSearchQuery === "" || // If searchQuery is empty, match all apps
(app.name && app.name.toLowerCase().includes(normalizedSearchQuery)) ||
(app.tags && Array.isArray(app.tags) && app.tags.some(tag =>
tag && typeof tag === 'string' && tag.toLowerCase().includes(normalizedSearchQuery)
)) ||
(app.categories && app.categories.some((category) =>
category.toLowerCase().includes(searchQuery.toLowerCase())
(app.categories && Array.isArray(app.categories) && app.categories.some((category) =>
category && typeof category === 'string' && category.toLowerCase().includes(normalizedSearchQuery)
))
);
const matchesSelectedCategories = (
selectedCategory.length === 0 || // If no category is selected, match all apps
(app.categories && app.categories.some(category =>
selectedCategory.includes(category)
!Array.isArray(selectedCategory) || selectedCategory.length === 0 || // If no category is selected, match all apps
(app.categories && Array.isArray(app.categories) && app.categories.some(category =>
category && selectedCategory.includes(category)
))
);
const matchesSelectedTags = (
selectedLabel.length === 0 || // If no label is selected, match all apps
(app.tags && app.tags.some(tag =>
selectedLabel.includes(tag)
!Array.isArray(selectedLabel) || selectedLabel.length === 0 || // If no label is selected, match all apps
(app.tags && Array.isArray(app.tags) && app.tags.some(tag =>
tag && selectedLabel.includes(tag)
))
);
+17 -6
View File
@@ -113,19 +113,29 @@ export const CopyToClipboard = (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(
`p`,
{},
props.children,
cleanedChildren,
)
if (props.children[0] != undefined) {
if(typeof props.children[0] === "string") {
if (props.children[0].includes('.mp4')) {
if (cleanedChildren[0] !== undefined) {
if(typeof cleanedChildren[0] === "string") {
if (cleanedChildren[0].includes('.mp4')) {
return (
<div>
<video width="640" height="480" controls>
<source src={`${props.children[0]}`} type="video/mp4" />
<source src={`${cleanedChildren[0]}`} type="video/mp4" />
</video>
</div>
)
@@ -169,6 +179,7 @@ export const Img = (props) => {
// Find parent container and check width
const isArticlePage = window.location.pathname.includes("/articles/")
const isFormPage = window.location.pathname.includes("/forms/")
const isWorkflowPage = window.location.pathname.includes("/workflows/")
var height = "auto"
var width = isArticlePage ? 1000 : isFormPage ? 400: 750
@@ -176,7 +187,7 @@ export const Img = (props) => {
const theme = getTheme(themeMode)
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,
width: width,
maxWidth: width,
+2 -2
View File
@@ -453,7 +453,7 @@ const LoginPage = props => {
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) {
@@ -932,7 +932,7 @@ const LoginPage = props => {
helperText={
handleValidateForm(username, password)
? ""
: "Password must be at least 9 characters long"
: "Password must be at least 10 characters long"
}
/>
</div>
+359 -34
View File
@@ -17,11 +17,13 @@ import GetAppIcon from '@mui/icons-material/GetApp';
// Material UI & Components
import { makeStyles } from "@mui/styles";
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 EditWorkflow from "../components/EditWorkflow.jsx"
import Priority from "../components/Priority.jsx";
import { Context } from "../context/ContextApi.jsx";
import { isMobile } from "react-device-detect"
// Material UI Components
import {
@@ -99,6 +101,9 @@ import {
Psychology as PsychologyIcon,
Wifi as WifiIcon,
Devices as DevicesIcon,
AutoAwesome as AutoAwesomeIcon,
BarChart as BarChartIcon,
Lock as LockIcon,
} from "@mui/icons-material";
// Additional Components
@@ -115,13 +120,13 @@ import { InstantSearch, Configure, connectHits, connectSearchBox, connectRefinem
import { debounce } from "lodash";
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 svgSize = 24;
const imagesize = 22;
const imagesize = 23;
@@ -212,8 +217,14 @@ export const GetIconInfo = (action) => {
"release",
],
},
{
key: "secret",
values: [
"api",
"password",
"protect",
],
}
];
var selectedKey = ""
@@ -402,6 +413,12 @@ export const GetIconInfo = (action) => {
iconBackgroundColor: "green",
originalIcon: <DevicesIcon />,
},
secret: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <LockIcon />,
}
}
/*
@@ -670,6 +687,8 @@ const Workflows2 = (props) => {
const [isLoadingWorkflow, setIsLoadingWorkflow] = useState(false);
const [isLoadingPublicWorkflow, setIsLoadingPublicWorkflow] = useState(false);
const [view, setView] = useState(localStorage?.getItem("workflowView") || "grid");
const [showExecutionStats, setShowExecutionStats] = React.useState(localStorage?.getItem("showExecutionStats") === "true" || false);
const imgSize = 60;
const { themeMode, brandColor, brandName } = useContext(Context);
@@ -728,6 +747,8 @@ const Workflows2 = (props) => {
var upload = "";
const [workflows, setWorkflows] = React.useState([]);
const [workflowTimelines, setWorkflowTimelines] = React.useState([]);
const [backgroundWorkflows, setBackgroundWorkflows] = React.useState([]);
const [backupWorkflows, setBackupWorkflows] = React.useState([]);
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
const [selectedUsecases, setSelectedUsecases] = React.useState([]);
@@ -767,6 +788,7 @@ const Workflows2 = (props) => {
const [actionImageList, setActionImageList] = React.useState([{ "large_image": "" }])
const [firstLoad, setFirstLoad] = React.useState(true);
const [aiAnnouncementModalOpen, setAiAnnouncementModalOpen] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
const [usecases, setUsecases] = 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 =
// window.location.host === "localhost:3002" ||
// window.location.host === "shuffler.io";
@@ -1154,6 +1223,74 @@ const Workflows2 = (props) => {
</Dialog>
) : 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 ? (
<Dialog
open={deleteModalOpen}
@@ -1352,6 +1489,53 @@ const Workflows2 = (props) => {
}
}, []);
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) => {
var storageWorkflows = []
setIsLoadingWorkflow(true)
@@ -1407,15 +1591,26 @@ const Workflows2 = (props) => {
toast.info("No workflows found in this org. Feel free to look into our public workflows!" , {
timeout: 7500,
})
setCurrTab(2)
}
localStorage.setItem("workflows", "[]")
setWorkflows([])
setFilteredWorkflows([])
}
var newarray = []
var backupWf = []
var backgroundWf = []
for (var wfkey in responseJson) {
const wf = responseJson[wfkey]
if (wf.public === true || wf.hidden === true) {
if (wf?.public === true || wf?.hidden === true) {
continue
}
if (wf?.background_processing === true) {
backgroundWf.push(wf)
continue
}
@@ -1427,6 +1622,10 @@ const Workflows2 = (props) => {
newarray.push(wf)
}
if (backgroundWf.length > 0) {
setBackgroundWorkflows(backgroundWf)
}
if (backupWf.length > 0) {
setBackupWorkflows(backupWf)
}
@@ -1659,11 +1858,9 @@ const Workflows2 = (props) => {
const paperAppStyle = {
minHeight: 146,
maxHeight: 146,
overflow: "hidden",
width: "100%",
color: "white",
display: "flex",
fontFamily: theme.typography?.fontFamily,
boxSizing: "border-box",
position: "relative",
@@ -2449,11 +2646,8 @@ const Workflows2 = (props) => {
var orgName = "";
var orgId = "";
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
var imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
@@ -2462,13 +2656,15 @@ const Workflows2 = (props) => {
? 20
: 0,
borderRadius: 10,
border:
foundOrg.id === userdata.active_org.id
? `3px solid ${boxColor}`
: null,
cursor: "pointer",
marginRight: 10,
};
}
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
imageStyle.border = foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null
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 = ""
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()
@@ -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} />
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) {
data.objectID = data.id
@@ -2546,10 +2781,12 @@ const Workflows2 = (props) => {
}
}
const foundTimeline = workflowTimelines.find((timeline) => timeline.id === data.id)
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 }}>
<Paper square style={paperAppStyle}>
{selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
<div
@@ -2558,11 +2795,12 @@ const Workflows2 = (props) => {
position: "absolute",
top: 0,
left: 0,
height: paperAppStyle.minHeight,
width: 3,
backgroundColor: boxColor,
borderRadius: "0 100px 0 0",
fontFamily: theme.typography?.fontFamily,
height: "100%",
}}
onClick={() => {
addFilter(selectedCategory)
@@ -2577,17 +2815,27 @@ const Workflows2 = (props) => {
>
<Grid item style={{ display: "flex", maxHeight: 34 }}>
{currTab === 2 ? null :
<Tooltip title={`Org "${orgName}". Click to edit image.`} placement="bottom">
<Tooltip title={`${relevantTrigger?.name}: ${relevantTrigger?.status}`} placement="bottom">
<div
styl={{ cursor: "pointer" }}
style={{ cursor: "" }}
onClick={() => {
navigate("/admin")
//navigate("/admin")
}}
>
{image}
{image?.includes("data:image") ?
<img
alt={orgName}
src={image}
style={imageStyle}
/>
:
image
}
</div>
</Tooltip>
}
<Tooltip arrow
onMouseEnter={() => {
/*
@@ -2874,7 +3122,8 @@ const Workflows2 = (props) => {
})
: null}
</Grid>
{data.actions !== undefined && data.actions !== null && type !== "public" ? (
{type !== "public" ? (
<div style={{ position: "absolute", top: 10, right: 10, }}>
<IconButton
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" ?
<Tooltip title="Edit Form" placement="top">
<div style={{ position: "absolute", top: 50, right: 8, }}>
<div style={{ position: "absolute", top: 80, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -2908,8 +3157,8 @@ const Workflows2 = (props) => {
: null}
{(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">
<div style={{ position: "absolute", top: 85, right: 8, }}>
<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: 40, right: 8, }}>
<IconButton
aria-label="more"
aria-controls="long-menu"
@@ -2919,19 +3168,38 @@ const Workflows2 = (props) => {
}}
style={{
padding: "0px",
color: "#979797",
transparency: 0.5,
}}
color="primary"
>
<ErrorOutlineIcon style={{
<ErrorOutlineIcon
style={{
color: "#f86a3e",
marginRight: 2,
}} />
}}
/>
</IconButton>
</div>
</Tooltip>
: null}
</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>
</div>
)
}
@@ -4414,16 +4682,33 @@ const Workflows2 = (props) => {
{backupWorkflows.length > 0 &&
<Tab
label={`Onprem Backup (${backupWorkflows.length})`}
value={3}
style={{
...tabStyle,
borderLeft: "1px solid rgba(255,255,255,0.3)",
marginLeft: 25,
...(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
label="Org Forms"
value={5}
onClick={() => {
navigate("/forms")
}}
@@ -4431,7 +4716,7 @@ const Workflows2 = (props) => {
...tabStyle,
marginRight: 0,
marginLeft: 25,
...(currTab === 4 ? tabActive : {})
...(currTab === 5 ? tabActive : {})
}}
/>
</Tabs>
@@ -4664,7 +4949,22 @@ const Workflows2 = (props) => {
paddingRight: 1,
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
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
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 :
myWorkflows.length === 0 ?
@@ -5216,6 +5538,7 @@ const Workflows2 = (props) => {
{deleteModal}
{exportVerifyModal}
{publishModal}
{aiAnnouncementModal}
{workflowDownloadModalOpen}
{/*!drawerOpen ?
@@ -5279,8 +5602,10 @@ const Workflows2 = (props) => {
width: '100%',
height: '100%',
}
// 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>;
};