[feature] - add queue management changes

This commit is contained in:
Lalit Deore
2026-05-07 18:35:07 +05:30
parent 8258baa92e
commit c9cfa6caa2
2 changed files with 389 additions and 72 deletions
+389 -71
View File
@@ -52,6 +52,7 @@ import {
Cancel as CancelIcon, Cancel as CancelIcon,
Shield as ShieldIcon, Shield as ShieldIcon,
Cancel as XCircleIcon, Cancel as XCircleIcon,
LockOutlined as LockIcon,
FlashOn as ZapIcon, FlashOn as ZapIcon,
People as UsersIcon, People as UsersIcon,
FmdGoodOutlined as FmdGoodOutlinedIcon, FmdGoodOutlined as FmdGoodOutlinedIcon,
@@ -203,7 +204,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
> >
{isProdStatusOn {isProdStatusOn
? 'Your organization has full access to all enterprise features and capabilities.' ? '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> </Typography>
{/* Features Grid */} {/* Features Grid */}
@@ -223,8 +224,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
}) })
.map((feature, index) => { .map((feature, index) => {
const Icon = feature.icon; const Icon = feature.icon;
const isAvailable = isProdStatusOn && feature.isActive; const isAvailable = isProdStatusOn;
const statusColor = isAvailable ? colors.success : colors.disabled; const statusColor = isAvailable ? colors.success : colors.warning;
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg; const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
return ( return (
@@ -235,19 +236,34 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
alignItems: 'center', alignItems: 'center',
gap: 14, gap: 14,
padding: '14px 16px', padding: '14px 16px',
paddingLeft: !isAvailable ? 20 : 16,
borderRadius: 10, borderRadius: 10,
background: bgColor, background: bgColor,
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`, border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
transition: 'all 0.2s', 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 */} {/* Icon */}
<div <div
style={{ style={{
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 8, borderRadius: 8,
background: isAvailable ? colors.success + '20' : colors.disabledBg, background: isAvailable ? colors.success + '20' : `${colors.warning}20`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -264,7 +280,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
fontSize: 15, fontSize: 15,
fontWeight: 600, fontWeight: 600,
color: colors.textPrimary, color: colors.textPrimary,
marginBottom: 4, marginBottom: 3,
}} }}
> >
{feature.label} {feature.label}
@@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
<div <div
style={{ style={{
fontSize: 13, fontSize: 13,
color: colors.textSecondary, color: !isAvailable ? colors.warning : colors.textSecondary,
lineHeight: 1.4, lineHeight: 1.4,
}} }}
> >
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }} style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
/> />
) : ( ) : (
<XCircleIcon <LockIcon
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }} style={{ color: colors.warning, fontSize: 20, flexShrink: 0 }}
/> />
)} )}
</div> </div>
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
/> />
{!isProdStatusOn && ( {!isProdStatusOn && (
<div <div style={{
style={{
padding: 20,
borderRadius: 12, borderRadius: 12,
background: themeMode == "dark" ? "#212121" : "#ffffff", overflow: 'hidden',
border: `1px solid ${colors.border}`, border: `1px solid ${colors.accent}40`,
marginBottom: 20, marginBottom: 24,
}} }}>
> {/* Header */}
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}> <div style={{
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} /> background: `linear-gradient(135deg, ${colors.accent}14 0%, ${colors.accent}06 100%)`,
<div> padding: '16px 20px',
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}> borderBottom: `1px solid ${colors.accent}20`,
About Shuffle Enterprise display: 'flex',
</Typography> alignItems: 'center',
<Typography gap: 12,
variant="body2" }}>
color="textSecondary" <div style={{
style={{ width: 36,
margin: 0, height: 36,
lineHeight: 1.6, borderRadius: 8,
}} background: `${colors.accent}20`,
> display: 'flex',
Shuffle Enterprise is designed for organizations that require scalability, high alignItems: 'center',
availability, dedicated support, and robust infrastructure to run mission-critical justifyContent: 'center',
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a> flexShrink: 0,
</Typography> }}>
<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, fontSize: 12, lineHeight: 1.4 }}>
Scale your security operations without limits
</Typography>
</div>
</div> </div>
</div>
</div>)}
{/* CTA Section */} {/* Body */}
{!isProdStatusOn && ( <div style={{
<div padding: '16px 20px 20px',
style={{ background: themeMode === "dark" ? "#212121" : "#ffffff",
padding: 20, }}>
borderRadius: 12, <div style={{
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`, display: 'grid',
border: `1px solid #FF854440`, gridTemplateColumns: '1fr 1fr',
marginBottom: 24, gap: 8,
}} marginBottom: 16,
> }}>
<div style={{ marginBottom: 16 }}> {[
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}> { icon: ZapIcon, text: 'Higher App Run Limits' },
Ready to upgrade? { 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> </Typography>
<Typography
variant="body2" <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
color="textSecondary" <Button
style={{ href="https://shuffler.io/pricing?env=Self-Hosted"
margin: 0, target="_blank"
lineHeight: 1.6, rel="noreferrer"
}} variant="contained"
color="primary"
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
>
Upgrade Now
</Button>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
target="_blank"
rel="noreferrer"
variant="outlined"
startIcon={<MailIcon sx={{ fontSize: 16 }} />}
> >
Contact our team to learn more about enterprise features and pricing. Talk to Sales
</Typography> </Button>
</div>
</div> </div>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
startIcon={<MailIcon sx={{ fontSize: 16}} />}
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
>
Contact 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 Billing = memo((props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props; const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
//const alert = useAlert(); //const alert = useAlert();
@@ -413,7 +672,7 @@ const Billing = memo((props) => {
const [statistics, setStatistics] = useState([]) const [statistics, setStatistics] = useState([])
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0) const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0) const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
const [billingEnvironments, setBillingEnvironments] = useState([])
useEffect(() => { useEffect(() => {
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) { if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100; 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) => { const getStats = (orgid) => {
if (orgid === undefined || orgid === null) { if (orgid === undefined || orgid === null) {
@@ -563,6 +845,9 @@ const Billing = memo((props) => {
useEffect(() => { useEffect(() => {
if (selectedOrganization && selectedOrganization?.id?.length > 0) { if (selectedOrganization && selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id); getStats(selectedOrganization.id);
if (!isCloud) {
getBillingEnvironments();
}
} }
}, [selectedOrganization]); }, [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 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(() => { useEffect(() => {
if (isChildOrg && currentTab === 0) { if (isChildOrg && currentTab === 0) {
setCurrentTab(1); setCurrentTab(1);
@@ -2800,6 +3096,28 @@ const Billing = memo((props) => {
</div> </div>
) : null*/} ) : 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 && ( {!isChildOrg && isCloud && (
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services"> <div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services">
<Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}> <Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}>
-1
View File
@@ -65,7 +65,6 @@ import { Context } from "../context/ContextApi.jsx";
import { import {
SearchBox, SearchBox,
StaticRefinementList,
RefinementList, RefinementList,
InstantSearch, InstantSearch,
connectSearchBox, connectSearchBox,