Merge pull request #1976 from LalitDeore/queu-management-fixes

[feature] - add queue management changes for onprem
This commit is contained in:
Lalit Deore
2026-05-07 18:39:09 +05:30
committed by GitHub
2 changed files with 389 additions and 72 deletions
+382 -64
View File
@@ -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.'}
</Typography>
{/* 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 && (
<div style={{
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 3,
background: colors.warning,
borderRadius: '10px 0 0 10px',
}} />
)}
{/* Icon */}
<div
style={{
width: 40,
height: 40,
borderRadius: 8,
background: isAvailable ? colors.success + '20' : colors.disabledBg,
background: isAvailable ? colors.success + '20' : `${colors.warning}20`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -264,7 +280,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
fontSize: 15,
fontWeight: 600,
color: colors.textPrimary,
marginBottom: 4,
marginBottom: 3,
}}
>
{feature.label}
@@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
<div
style={{
fontSize: 13,
color: colors.textSecondary,
color: !isAvailable ? colors.warning : colors.textSecondary,
lineHeight: 1.4,
}}
>
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
/>
) : (
<XCircleIcon
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }}
<LockIcon
style={{ color: colors.warning, fontSize: 20, flexShrink: 0 }}
/>
)}
</div>
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
/>
{!isProdStatusOn && (
<div
style={{
padding: 20,
<div style={{
borderRadius: 12,
background: themeMode == "dark" ? "#212121" : "#ffffff",
border: `1px solid ${colors.border}`,
marginBottom: 20,
}}
>
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} />
<div>
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
About Shuffle Enterprise
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{
margin: 0,
lineHeight: 1.6,
}}
>
Shuffle Enterprise is designed for organizations that require scalability, high
availability, dedicated support, and robust infrastructure to run mission-critical
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a>
</Typography>
</div>
</div>
</div>)}
{/* CTA Section */}
{!isProdStatusOn && (
<div
style={{
padding: 20,
borderRadius: 12,
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`,
border: `1px solid #FF854440`,
overflow: 'hidden',
border: `1px solid ${colors.accent}40`,
marginBottom: 24,
}}
>
<div style={{ marginBottom: 16 }}>
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
Ready to upgrade?
}}>
{/* Header */}
<div style={{
background: `linear-gradient(135deg, ${colors.accent}14 0%, ${colors.accent}06 100%)`,
padding: '16px 20px',
borderBottom: `1px solid ${colors.accent}20`,
display: 'flex',
alignItems: 'center',
gap: 12,
}}>
<div style={{
width: 36,
height: 36,
borderRadius: 8,
background: `${colors.accent}20`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}>
<ShieldIcon style={{ color: colors.accent, fontSize: 20 }} />
</div>
<div>
<Typography variant="h6" style={{ fontWeight: 700, margin: 0, fontSize: 16 }}>
Unlock Shuffle Enterprise
</Typography>
<Typography
variant="body2"
color="textSecondary"
style={{
margin: 0,
lineHeight: 1.6,
}}
>
Contact our team to learn more about enterprise features and pricing.
<Typography variant="body2" color="textSecondary" style={{ margin: 0, fontSize: 12, lineHeight: 1.4 }}>
Scale your security operations without limits
</Typography>
</div>
</div>
{/* Body */}
<div style={{
padding: '16px 20px 20px',
background: themeMode === "dark" ? "#212121" : "#ffffff",
}}>
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 8,
marginBottom: 16,
}}>
{[
{ 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 (
<div key={i} style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 12px',
borderRadius: 8,
background: themeMode === "dark" ? `${colors.accent}08` : `${colors.accent}06`,
}}>
<ItemIcon style={{ color: colors.accent, fontSize: 16 }} />
<span style={{ fontSize: 13, color: colors.textPrimary, fontWeight: 500 }}>
{item.text}
</span>
</div>
);
})}
</div>
<Typography variant="body2" color="textSecondary" style={{
marginBottom: 16,
lineHeight: 1.6,
fontSize: 13,
}}>
Purpose-built for security teams that need scalability, high availability, and dedicated
expert support to run mission-critical workflows in production.{' '}
<a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{ color: colors.accent }}>
Learn more
</a>
</Typography>
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
href="https://shuffler.io/pricing?env=Self-Hosted"
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
startIcon={<MailIcon sx={{ fontSize: 16}} />}
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
>
Contact Sales
Upgrade Now
</Button>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
target="_blank"
rel="noreferrer"
variant="outlined"
startIcon={<MailIcon sx={{ fontSize: 16 }} />}
>
Talk to Sales
</Button>
</div>
</div>
</div>
)}
</div>
);
};
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 (
<div style={{
border: `1px solid ${borderColor}`,
borderRadius: 12,
padding: '20px 24px',
backgroundColor: theme.palette.platformColor,
marginBottom: 16,
maxWidth: 800,
}}>
{/* Title row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<Typography variant="body2" style={{ color: mutedText, fontSize: 13, fontWeight: 500 }}>
{envTypeLabel} - {envName} · App runs / month
</Typography>
<div style={{
padding: '4px 14px',
borderRadius: 20,
backgroundColor: statusBg,
border: `1px solid ${statusColor}60`,
display: 'inline-flex',
alignItems: 'center',
gap: 6,
}}>
<span style={{ width: 7, height: 7, borderRadius: '50%', backgroundColor: statusColor, display: 'inline-block', boxShadow: `0 0 6px ${statusColor}` }} />
<span style={{ color: statusColor, fontWeight: 700, fontSize: 13 }}>{status}</span>
</div>
</div>
{/* Main number */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 2 }}>
<span style={{ fontSize: 28, fontWeight: 700, color: theme.palette.text.primary }}>
{totalRuns.toLocaleString()}
</span>
<span style={{ fontSize: 18, color: mutedText }}>
/ {limit.toLocaleString()}
</span>
</div>
{isAirGapped && !isCloudSynching && (
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 14 }}>
No throttle until {hardPauseLimit.toLocaleString()} runs · 2× your plan limit
</Typography>
)}
{/* Main usage bar */}
<div style={{ position: 'relative', height: 8, borderRadius: 4, backgroundColor: trackBg, marginBottom: 4 }}>
<div style={{
position: 'absolute',
left: 0, top: 0, bottom: 0,
width: `${mainBarPct}%`,
borderRadius: 4,
backgroundColor: mainBarColor,
transition: 'width 0.4s ease',
}} />
{/* 80% threshold marker */}
<div style={{
position: 'absolute',
left: '80%',
top: -3,
bottom: -3,
width: 2,
backgroundColor: borderColor,
borderRadius: 1,
}} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 14 }}>
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>0</Typography>
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>80% threshold</Typography>
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>{limit.toLocaleString()}</Typography>
</div>
{/* Throttle limit row */}
{isAirGapped && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
Burst throttle threshold (2× limit) · workflows throttle to 1/min above this
</Typography>
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
{totalRuns.toLocaleString()} / {hardPauseLimit.toLocaleString()}
</Typography>
</div>
<div style={{ position: 'relative', height: 6, borderRadius: 3, backgroundColor: trackBg, marginBottom: 16 }}>
<div style={{
position: 'absolute',
left: 0, top: 0, bottom: 0,
width: `${hardPausePct}%`,
borderRadius: 3,
backgroundColor: '#ef4444',
transition: 'width 0.4s ease',
}} />
</div>
</>
)}
{/* Alert box for Warning / Throttled */}
{status !== 'Healthy' && (
<div style={{
borderRadius: 8,
padding: '14px 16px',
backgroundColor: 'rgba(245, 158, 11, 0.08)',
border: '1px solid rgba(245, 158, 11, 0.3)',
marginBottom: 16,
}}>
<Typography variant="body2" style={{ color: '#f59e0b', fontWeight: 700, marginBottom: 6, fontSize: 14 }}>
{isThrottled ? 'Running slow \u2014 workflows are still running' : 'Approaching your monthly limit'}
</Typography>
<Typography variant="body2" style={{ color: theme.palette.text.primary, fontSize: 13, lineHeight: 1.6, marginBottom: 12 }}>
{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.`
}
</Typography>
<Button
variant="outlined"
size="small"
sx={{
borderColor: '#f59e0b',
color: '#f59e0b',
textTransform: 'none',
fontSize: 13,
borderRadius: '6px',
padding: '5px 14px',
}}
onClick={() => {
if (isThrottled) navigate('/admin?tab=locations');
else window.open('https://shuffler.io/contact', '_blank', 'noopener,noreferrer');
}}
>
{isThrottled ? 'Manage queue' : 'Contact Us'}
</Button>
</div>
)}
{/* Stats row */}
<div style={{ display: 'flex', borderTop: `1px solid ${borderColor}`, paddingTop: 16 }}>
{[
{ label: 'Queued jobs', value: queueSize },
{ label: 'Throttle rate', value: throttleRate },
{ label: 'Est. clear time', value: estClearTime },
].map((stat, i) => (
<div key={i} style={{
flex: 1,
paddingRight: i < 2 ? 16 : 0,
borderRight: i < 2 ? `1px solid ${borderColor}` : 'none',
marginRight: i < 2 ? 16 : 0,
}}>
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 4 }}>
{stat.label}
</Typography>
<Typography style={{ fontWeight: 700, fontSize: 18, color: theme.palette.text.primary }}>
{stat.value}
</Typography>
</div>
))}
</div>
</div>
);
});
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) => {
</div>
) : null*/}
{/* Queue Management */}
{!isCloud && activeQueueEnvs.length > 0 && !isChildOrg && (
<div style={{ maxWidth: 800, marginTop: 32, marginBottom: 8 }}>
<Typography variant="h6" style={{ marginBottom: 6, fontSize: 20, fontWeight: 600 }}>
Queue Management
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 16, fontSize: 14 }}>
Real-time status of your app run usage and workflow queue across all runtime locations.
</Typography>
<AppRunsQueueCard
environment={aggregatedQueueEnv}
totalRuns={Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}
limit={selectedOrganization?.sync_features?.app_executions?.limit || 25000}
theme={theme}
navigate={navigate}
isAirGapped={isAirGapped}
isCloudSynching={isCloudSynching}
/>
</div>
)}
{!isChildOrg && isCloud && (
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services">
<Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}>
-1
View File
@@ -65,7 +65,6 @@ import { Context } from "../context/ContextApi.jsx";
import {
SearchBox,
StaticRefinementList,
RefinementList,
InstantSearch,
connectSearchBox,