Merging fixes for 2.1.0

This commit is contained in:
Frikky
2025-08-26 16:07:44 +02:00
parent 15bf1011e7
commit 6c60dd3bcc
20 changed files with 2115 additions and 598 deletions
+113 -42
View File
@@ -91,10 +91,13 @@ const Billing = memo((props) => {
const [currentTab, setCurrentTab] = useState(0)
const [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,
@@ -1973,7 +2018,13 @@ const Billing = memo((props) => {
};
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
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);
setCurrentTab(newValue)
}}
style={{ marginTop: 20 }}
TabIndicatorProps={{
@@ -2619,52 +2665,66 @@ const Billing = memo((props) => {
}
}}
>
<Tab
label="Parent Organization"
{isChildOrg ? null :
<Tab
label="All Organization Stats"
style={{ textTransform: 'none',}}
value={0}
/>}
<Tab
label={isChildOrg ? "Organization Stats" : "Parent Organization Stats"}
style={{ textTransform: 'none',}}
value={1}
/>
{isCloud ?
<Tab
label="Cloud-Synced 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,}}>
<BillingStats
isCloud={isCloud}
clickedFromOrgTab={clickedFromOrgTab}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
userdata={userdata}
/>
</div>
{
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}
/>
: 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,7 +2735,18 @@ 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>
</span>
+114 -140
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)
}
@@ -343,7 +377,16 @@ const AppStats = (defaultprops) => {
}
setFilteredStatistics(tmpstats)
handleDataSetting(tmpstats, "day")
handleDataSetting(tmpstats, "day")
// if we have done monthly reset than only show monthly app runs as current month app run
const currentMonth = new Date().getMonth() + 1
if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAppRunsParent(statistics["monthly_app_executions"])
}
if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
}
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
@@ -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,40 +492,45 @@ const AppStats = (defaultprops) => {
}
}
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
// 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))
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
//setApprunCosts(appcostRuns)
}
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
@@ -494,49 +542,8 @@ const AppStats = (defaultprops) => {
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
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 }}>
} */}
<LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
<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={{
+111 -74
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,86 +195,116 @@ 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>
<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>
)
})}
<div style={{display: "flex", width: 400, margin: "auto", }}>
{matchingapps.length > 0 ?
<Autocomplete
style={{flex: 1, }}
multiple
filterSelectedOptions
options={matchingapps}
<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>
value={selectedApps}
onChange={(event, value) => {
setSelectedApps(value)
}}
{showAppsearch ?
<Autocomplete
style={{flex: 1, maxWidth: 200, minWidth: 200, margin: "auto", marginTop: 10, }}
multiple
filterSelectedOptions
options={matchingapps}
getOptionLabel={(option) => {
const parsedname = option.name.replaceAll("_", " ")
value={selectedApps}
onChange={(event, value) => {
setSelectedApps(value)
}}
return (
<div>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
variant="outlined"
label="Select apps"
/>
)
}}
/>
: null}
getOptionLabel={(option) => {
const parsedname = option.name.replaceAll("_", " ")
return (
<div>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
}}
renderInput={(params) => {
return (
<TextField
{...params}
variant="outlined"
label="Select apps"
/>
)
}}
/>
:
<Button
style={{width: 250, margin: 25, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
<Button
style={{flex: 1, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
//if (foundMatchingWorkflow !== null) {
// toast.error("Deletion not implemented for this POC. Please delete the workflow.")
//}
if (newapps.length > 0) {
newapps += ","
}
//else {
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
newapps += app.name
}
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
startIngestion(appname, newapps, appCategory, index)
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
}}
>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
:
"Start Ingestion"
}
</Button>
}
</div>
startIngestion(appname, newapps, appCategory, index)
//}
}}>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
:
"Start Ingestion"
}
</Button>
<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) => {
@@ -94,6 +98,11 @@ const EditWorkflow = (props) => {
const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : [])
const [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();
@@ -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"
+48 -35
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;
}
@@ -121,6 +121,12 @@ const PartnerSettings = (props) => {
toast.error("There should be at least one partner type");
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;
@@ -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,32 +241,41 @@ 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]) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "999px",
py: 1.2,
px: 2.5,
fontSize: "13px",
fontWeight: 500,
fontFamily: theme.typography.fontFamily,
color: "#fff",
backgroundColor: "transparent",
border: `1.5px solid ${
partnerTypeColors[key]
}`,
transition: "all 0.2s ease",
textAlign: "center",
whiteSpace: "nowrap",
color: partnerTypeColors[key],
}}
>
{key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())}
</Box>
))}
{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",
justifyContent: "center",
borderRadius: "999px",
py: 1.2,
px: 2.5,
fontSize: "13px",
fontWeight: 500,
fontFamily: theme.typography.fontFamily,
color: "#fff",
backgroundColor: "transparent",
border: `1.5px solid ${
partnerTypeColors[key]
}`,
transition: "all 0.2s ease",
textAlign: "center",
whiteSpace: "nowrap",
color: partnerTypeColors[key],
}}
>
{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) => {
+200 -30
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);
setlocalcodedata(editorData?.value);
try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent action data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedAction(action);
// Update available variables when action changes
@@ -230,7 +283,13 @@ const CodeEditor = (props) => {
}
const trigger = workflow?.triggers?.find(trigger => trigger.id === triggerId);
setlocalcodedata(editorData?.value);
try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent trigger data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedTrigger(trigger);
// Update available variables when trigger changes
@@ -244,7 +303,13 @@ const CodeEditor = (props) => {
}
const condition = selectedEdge?.conditions?.find(condition => condition.id === conditionId);
setlocalcodedata(editorData?.value);
try {
const indentedContent = autoIndentContent(editorData?.value);
setlocalcodedata(indentedContent || editorData?.value);
} catch (error) {
console.warn('Failed to auto-indent condition data:', error);
setlocalcodedata(editorData?.value);
}
setSelectedCondition(condition);
// 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,39 +1666,66 @@ const CodeEditor = (props) => {
</Tooltip>
: null}
{fullScreenModeEnabled ? null :
<Tooltip
color="primary"
title={`Move window`}
placement="left"
>
<IconButton
id="draggable-dialog-title"
style={{
zIndex: 5000,
position: "absolute",
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 166 : 66,
color: "grey",
cursor: "move",
}}
onClick={() => {
}}
>
<DragIndicatorIcon />
</IconButton>
</Tooltip>
}
<Tooltip
color="primary"
title={`Move window`}
placement="left"
title={fullScreenModeEnabled ? `Exit Fullscreen Mode` : `Enter Fullscreen Mode`}
placement="top"
>
<IconButton
id="draggable-dialog-title"
style={{
zIndex: 5000,
position: "absolute",
top: 6,
right: 56,
top: fullScreenModeEnabled ? 50 : 6,
right: fullScreenModeEnabled ? 136 : 36,
color: "grey",
cursor: "move",
}}
onClick={() => {
setFullScreenModeEnabled(!fullScreenModeEnabled)
localStorage.setItem("codeEditorFullScreen", !fullScreenModeEnabled)
}}
>
<DragIndicatorIcon />
{!fullScreenModeEnabled ?
<FullscreenIcon />
:
<FullscreenExitIcon />
}
</IconButton>
</Tooltip>
<Tooltip
color="primary"
title={`Close window without saving`}
placement="left"
placement="right"
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 6,
right: 6,
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,