From c9cfa6caa2c5baa65c156d6ac0911e52258daa85 Mon Sep 17 00:00:00 2001 From: Lalit Deore Date: Thu, 7 May 2026 18:35:07 +0530 Subject: [PATCH] [feature] - add queue management changes --- frontend/src/components/Billing.jsx | 460 +++++++++++++++++++++++----- frontend/src/views/AppExplorer.jsx | 1 - 2 files changed, 389 insertions(+), 72 deletions(-) diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 0a4a03e4..959ce875 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -52,6 +52,7 @@ import { Cancel as CancelIcon, Shield as ShieldIcon, Cancel as XCircleIcon, + LockOutlined as LockIcon, FlashOn as ZapIcon, People as UsersIcon, FmdGoodOutlined as FmdGoodOutlinedIcon, @@ -203,7 +204,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => > {isProdStatusOn ? 'Your organization has full access to all enterprise features and capabilities.' - : 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'} + : 'Your organization is running on the open-source plan. Upgrade to Enterprise to remove limits and unlock advanced capabilities.'} {/* Features Grid */} @@ -223,8 +224,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => }) .map((feature, index) => { const Icon = feature.icon; - const isAvailable = isProdStatusOn && feature.isActive; - const statusColor = isAvailable ? colors.success : colors.disabled; + const isAvailable = isProdStatusOn; + const statusColor = isAvailable ? colors.success : colors.warning; const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg; return ( @@ -235,19 +236,34 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => alignItems: 'center', gap: 14, padding: '14px 16px', + paddingLeft: !isAvailable ? 20 : 16, borderRadius: 10, background: bgColor, border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`, transition: 'all 0.2s', + position: 'relative', + overflow: 'hidden', }} > + {!isAvailable && ( +
+ )} + {/* Icon */}
fontSize: 15, fontWeight: 600, color: colors.textPrimary, - marginBottom: 4, + marginBottom: 3, }} > {feature.label} @@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => style={{ color: colors.success, fontSize: 20, flexShrink: 0 }} /> ) : ( - )}
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) => /> {!isProdStatusOn && ( -
-
- -
- - About Shuffle Enterprise - - - Shuffle Enterprise is designed for organizations that require scalability, high - availability, dedicated support, and robust infrastructure to run mission-critical - workflows in production environments. learn more - + overflow: 'hidden', + border: `1px solid ${colors.accent}40`, + marginBottom: 24, + }}> + {/* Header */} +
+
+ +
+
+ + Unlock Shuffle Enterprise + + + Scale your security operations without limits + +
-
-
)} - {/* CTA Section */} - {!isProdStatusOn && ( -
-
- - Ready to upgrade? + {/* Body */} +
+
+ {[ + { icon: ZapIcon, text: 'Higher App Run Limits' }, + { icon: UsersIcon, text: 'Multi-Tenant Support' }, + { icon: ShieldIcon, text: 'Enterprise SLA' }, + { icon: FmdGoodOutlinedIcon, text: 'Multi-Location Deploy' }, + ].map((item, i) => { + const ItemIcon = item.icon; + return ( +
+ + + {item.text} + +
+ ); + })} +
+ + + Purpose-built for security teams that need scalability, high availability, and dedicated + expert support to run mission-critical workflows in production.{' '} + + Learn more + - + + +
-
)}
); }; +const AppRunsQueueCard = memo(({ environment, isAirGapped, isCloudSynching, totalRuns, limit, theme, navigate }) => { + + const usagePct = limit > 0 ? (totalRuns / limit) * 100 : 0; + const hardPauseLimit = limit * 2; + const hardPausePct = hardPauseLimit > 0 ? Math.min((totalRuns / hardPauseLimit) * 100, 100) : 0; + const mainBarPct = Math.min(usagePct, 100); + + const queueSize = environment?.queue !== undefined && environment?.queue !== null + ? Math.max(0, environment.queue) + : 0; + + const isThrottled = isCloudSynching ? false : (isAirGapped ? hardPausePct >= 100 : usagePct >= 100); + + let status, statusColor, statusBg; + if (isThrottled) { + status = 'Throttled'; + statusColor = '#ef4444'; + statusBg = 'rgba(239, 68, 68, 0.12)'; + } else if (!isCloudSynching && usagePct >= 80) { + status = 'Warning'; + statusColor = '#f59e0b'; + statusBg = 'rgba(245, 158, 11, 0.12)'; + } else { + status = 'Healthy'; + statusColor = theme.palette.green; + statusBg = `${theme.palette.green}1f`; + } + + const throttleRate = isThrottled ? '1/min' : '\u2014'; + const estClearTime = isThrottled && queueSize > 0 ? `${queueSize} min` : '\u2014'; + const mainBarColor = isThrottled ? '#ef4444' : usagePct >= 80 && !isCloudSynching ? '#f59e0b' : theme.palette.green; + + const envTypeLabel = environment?.run_type === 'cloud' ? 'Cloud' : 'On-prem'; + const envName = environment?.Name || environment?.name || 'Default'; + + const borderColor = theme.palette.slateGrayColor; + const trackBg = theme.palette.slateGrayColor; + const mutedText = theme.palette.text.secondary; + + return ( +
+ {/* Title row */} +
+ + {envTypeLabel} - {envName} · App runs / month + +
+ + {status} +
+
+ + {/* Main number */} +
+ + {totalRuns.toLocaleString()} + + + / {limit.toLocaleString()} + +
+ + {isAirGapped && !isCloudSynching && ( + + No throttle until {hardPauseLimit.toLocaleString()} runs · 2× your plan limit + + )} + + {/* Main usage bar */} +
+
+ {/* 80% threshold marker */} +
+
+
+ 0 + 80% threshold + {limit.toLocaleString()} +
+ + {/* Throttle limit row */} + {isAirGapped && ( + <> +
+ + Burst throttle threshold (2× limit) · workflows throttle to 1/min above this + + + {totalRuns.toLocaleString()} / {hardPauseLimit.toLocaleString()} + +
+ +
+
+
+ + )} + + {/* Alert box for Warning / Throttled */} + {status !== 'Healthy' && ( +
+ + {isThrottled ? 'Running slow \u2014 workflows are still running' : 'Approaching your monthly limit'} + + + {isThrottled + ? isAirGapped + ? `You've exceeded the burst threshold of ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). Your workflows are still running \u2014 there is no hard stop. Executions slow to 1 per minute until next month.` + : `You've exceeded your ${limit.toLocaleString()} monthly limit. Your instance keeps running \u2014 executions slow to 1 per minute until next month. Nothing is lost. You can view or clear the queue from the Locations tab.` + : isAirGapped + ? `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs. Workflows run normally \u2014 slowdown only begins at ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). No action needed.` + : `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs (${Math.max(0, limit - totalRuns).toLocaleString()} remaining). If you reach 100%, executions continue at a reduced rate of 1 per minute, nothing stops or is lost.` + } + + +
+ )} + + {/* Stats row */} +
+ {[ + { label: 'Queued jobs', value: queueSize }, + { label: 'Throttle rate', value: throttleRate }, + { label: 'Est. clear time', value: estClearTime }, + ].map((stat, i) => ( +
+ + {stat.label} + + + {stat.value} + +
+ ))} +
+
+ ); +}); + const Billing = memo((props) => { const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props; //const alert = useAlert(); @@ -413,7 +672,7 @@ const Billing = memo((props) => { const [statistics, setStatistics] = useState([]) const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0) const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0) - + const [billingEnvironments, setBillingEnvironments] = useState([]) useEffect(() => { if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) { const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100; @@ -525,6 +784,29 @@ const Billing = memo((props) => { }, []) + const getBillingEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) return; + return response.json(); + }) + .then((responseJson) => { + if (responseJson && Array.isArray(responseJson)) { + setBillingEnvironments(responseJson); + } + }) + .catch((error) => { + console.log("Error fetching environments for billing:", error); + }); + }; + const getStats = (orgid) => { if (orgid === undefined || orgid === null) { @@ -563,6 +845,9 @@ const Billing = memo((props) => { useEffect(() => { if (selectedOrganization && selectedOrganization?.id?.length > 0) { getStats(selectedOrganization.id); + if (!isCloud) { + getBillingEnvironments(); + } } }, [selectedOrganization]); @@ -2387,6 +2672,17 @@ const Billing = memo((props) => { const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null + const activeQueueEnvs = Array.isArray(billingEnvironments) ? billingEnvironments.filter(env => env != null && !env.archived && env.Type !== 'cloud') : []; + const totalQueueSize = activeQueueEnvs.reduce((sum, env) => sum + Math.max(0, env?.queue || 0), 0); + const aggregatedQueueEnv = { + Name: `${activeQueueEnvs.length} Runtime Location${activeQueueEnvs.length !== 1 ? 's' : ''}`, + run_type: 'on-prem', + queue: totalQueueSize, + }; + const appExecLimit = selectedOrganization?.sync_features?.app_executions?.limit ?? 0; + const isAirGapped = selectedOrganization != null && (selectedOrganization.cloud_sync_active === true || selectedOrganization.cloud_sync === true) ? false : appExecLimit < 300000 ? false : true; + const isCloudSynching = selectedOrganization != null && selectedOrganization.cloud_sync === true && appExecLimit >= 300000; + useEffect(() => { if (isChildOrg && currentTab === 0) { setCurrentTab(1); @@ -2800,6 +3096,28 @@ const Billing = memo((props) => {
) : null*/} + + {/* Queue Management */} + {!isCloud && activeQueueEnvs.length > 0 && !isChildOrg && ( +
+ + Queue Management + + + Real-time status of your app run usage and workflow queue across all runtime locations. + + +
+ )} + {!isChildOrg && isCloud && (
diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index 1a169020..38297924 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -65,7 +65,6 @@ import { Context } from "../context/ContextApi.jsx"; import { SearchBox, - StaticRefinementList, RefinementList, InstantSearch, connectSearchBox,