Merging fixes for 2.1.0
This commit is contained in:
@@ -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"
|
||||
style={{ textTransform: 'none', }}
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, }}>
|
||||
|
||||
@@ -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.`
|
||||
|
||||
:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)}
|
||||
|
||||
@@ -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)
|
||||
))
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user