import React, { useState, useEffect, memo, useMemo, useContext } from "react";
import ReactGA from 'react-ga4';
import { getTheme } from "../theme.jsx";
import countries from "../components/Countries.jsx";
import {
Box,
Paper,
Typography,
Divider,
Button,
Grid,
Card,
List,
ListItemText,
ListItem,
Dialog,
DialogTitle,
DialogContent,
TextField,
InputAdornment,
IconButton,
Chip,
Checkbox,
Tooltip,
DialogContentText,
DialogActions,
LinearProgress,
Slider,
Tabs,
Tab,
CircularProgress,
} from "@mui/material";
import { useNavigate, Link, json } from "react-router-dom";
import { Autocomplete } from "@mui/material";
import { toast } from "react-toastify"
import {
Cached as CachedIcon,
ContentCopy as ContentCopyIcon,
Draw as DrawIcon,
Close as CloseIcon,
Delete,
RestaurantRounded,
Cloud,
CheckCircle,
Padding,
Edit,
Search as SearchIcon
} from "@mui/icons-material";
//import { useAlert
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
import BillingStats from "./BillingStats.jsx";
import { handlePayasyougo } from "../views/HandlePaymentNew.jsx"
import DeleteIcon from '@mui/icons-material/Delete';
import { Context } from "../context/ContextApi.jsx";
import LicencePopup from "./LicencePopup.jsx";
import { DataGrid } from "@mui/x-data-grid";
const Billing = memo((props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
//const alert = useAlert();
let navigate = useNavigate();
const { themeMode, brandColor,supportEmail } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
const [isLoggedIn, setIsLoggedIn] = useState(false)
const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false);
const [dealList, setDealList] = React.useState([]);
const [dealName, setDealName] = React.useState("");
const [dealAddress, setDealAddress] = React.useState("");
const [dealType, setDealType] = React.useState("MSSP");
const [dealCountry, setDealCountry] = React.useState("United States");
const [dealCurrency, setDealCurrency] = React.useState("USD");
const [dealStatus, setDealStatus] = React.useState("initiated");
const [dealValue, setDealValue] = React.useState("");
const [dealDiscount, setDealDiscount] = React.useState("");
const [dealerror, setDealerror] = React.useState("");
const [openChangeEmailBox, setOpenChangeEmailBox] = useState(false);
const [isMouseOverOnChangeEmail, setIsMouseOverOnChangeEmail] = useState(false);
const [currentAppRunsInPercentage, setCurrentAppRunsInPercentage] = useState(0);
const [currentAppRunsInNumber, setCurrentAppRunsInNumber] = useState(0);
const [alertThresholds, setAlertThresholds] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.AlertThreshold !== undefined && selectedOrganization.Billing.AlertThreshold !== null ? selectedOrganization.Billing.AlertThreshold : [{ percentage: '', count: '', Email_send: false }]);
const [currentIndex, setCurrentIndex] = useState(0);
const [deleteAlertIndex, setDeleteAlertIndex] = useState(-1);
const [deleteAlertVerification, setDeleteAlertVerification] = useState(false);
const [isScale, setIsScale] = useState(false);
const [currentTab, setCurrentTab] = useState(0)
const [allChildOrgs, setAllChildOrgs] = useState([])
const [allChildOrgsStats, setAllChildOrgsStats] = useState([])
useEffect(() => {
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) {
const percentage = (userdata.app_execution_usage / userdata.app_execution_limit) * 100;
setCurrentAppRunsInPercentage(Math.round(percentage));
setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage);
}
if (userdata?.id?.length > 0 && isLoggedIn === false){
setIsLoggedIn(true)
}
}, [userdata]);
const [BillingEmail, setBillingEmail] = useState(selectedOrganization?.Billing?.Email);
useEffect(() => {
const urlIncludesProfessionalServices = window.location.href.includes("professional-services");
if (props?.isCloud && urlIncludesProfessionalServices) {
const professionalServicesSection =
document.getElementById("professional-services");
if (professionalServicesSection) {
professionalServicesSection.scrollIntoView({ behavior: "smooth" });
}
}
}, []);
useEffect(() => {
if (BillingEmail !== selectedOrganization?.Billing?.Email) {
setBillingEmail(selectedOrganization?.Billing?.Email);
}
// Set and sort the alert thresholds
const alertThresholds = selectedOrganization.Billing !== undefined &&
selectedOrganization.Billing.AlertThreshold !== undefined &&
selectedOrganization.Billing.AlertThreshold !== null
? selectedOrganization.Billing.AlertThreshold
: [{ percentage: '', count: '', Email_send: false }];
const sortedAlertThresholds = alertThresholds.sort((a, b) => {
const countA = parseFloat(a.count);
const countB = parseFloat(b.count);
if (isNaN(countA)) return 1;
if (isNaN(countB)) return -1;
return countA - countB;
});
setAlertThresholds(sortedAlertThresholds);
const findCurrentIndex = sortedAlertThresholds.some(threshold => threshold.Email_send === false);
setCurrentIndex(findCurrentIndex ? sortedAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
}, [selectedOrganization]);
const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : ""
const products = [
{ code: "", label: "MSSP", phone: "" },
{ code: "", label: "Enterprise", phone: "" },
{ code: "", label: "Consultancy", phone: "" },
{ code: "", label: "Support", phone: "" },
];
const handleGetDeals = (orgId) => {
if (orgId.length === 0) {
toast(
"Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout."
);
return;
}
const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Bad status code in get deals: ", response.status);
}
return response.json();
})
.then((responseJson) => {
console.log("Got deals: ", responseJson);
if (responseJson.success === false) {
toast("Failed loading deals. Contact support if this persists");
} else {
setDealList(responseJson);
}
})
.catch((error) => {
console.log("Error getting org deals: ", error);
toast(
"Failed getting deals for your org. Contact support if this persists."
);
});
};
useEffect(() => {
if (isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true) {
handleGetDeals(selectedOrganization.id);
}
}, [])
const paperStyle = {
padding: 20,
// maxWidth: 400,
width: 340,
height: 'auto',
// width: "100%",
backgroundColor: "#1e1e1e",
borderRadius: 10,
marginRight: 10,
marginTop: 15,
}
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io" ||
window.location.host === "sandbox.shuffler.io";
billingInfo.subscription = {
"active": true,
"name": "Pay as you go",
"price": typecost_single,
"currency": "USD",
"currency_text": "$",
"interval": "app run / month",
"description": "Pay as you go",
"features": [
"Includes 10.000 app run/month for free. ",
"Pay for what you use with no minimum commitment and cancel anytime.",
],
"limit": 10000,
}
const handleStripeRedirect = () => {
//var priceItem = "price_1MRNF1DzMUgUjxHSfFTUb2Xh"
if (stripe == "") {
console.log("Stripe not loaded")
return
}
var priceItem = "price_1MROFrDzMUgUjxHShcSxgHO1"
const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success`
const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure`
var checkoutObject = {
lineItems: [
{
price: priceItem,
quantity: 1
},
],
mode: "subscription",
billingAddressCollection: "auto",
successUrl: successUrl,
cancelUrl: failUrl,
clientReferenceId: props.userdata.active_org.id,
}
//submitType: "donate",
stripe.redirectToCheckout(checkoutObject)
.then(function (result) {
console.log("SUCCESS STRIPE?: ", result)
ReactGA.event({
category: "pricing",
action: "add_card_success",
label: "",
})
})
.catch(function (error) {
console.error("STRIPE ERROR: ", error)
ReactGA.event({
category: "pricing",
action: "add_card_error",
label: "",
})
});
}
const cancelSubscriptions = (subscription_id) => {
const orgId = selectedOrganization.id;
const data = {
subscription_id: subscription_id,
action: "cancel",
org_id: selectedOrganization.id,
};
const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`;
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
}
if (handleGetOrg != undefined) {
handleGetOrg(selectedOrganization.id);
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success !== undefined && responseJson.success) {
toast("Successfully stopped subscription!");
} else {
toast("Failed stopping subscription. Please contact us.");
}
})
.catch(function (error) {
console.log("Error: ", error);
toast("Failed stopping subscription. Please contact us.");
});
};
const sendSignatureRequest = (subscription) => {
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`;
fetch(url, {
body: JSON.stringify({
org_id: selectedOrganization.id,
subscription: subscription,
}),
mode: "cors",
method: "POST",
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then((responseJson) => {
console.log("Response from signature request: ", responseJson);
})
.catch((error) => {
console.log("Error: ", error);
})
}
const SubscriptionObject = (props) => {
const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props;
const [signatureOpen, setSignatureOpen] = React.useState(false);
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
const [hovered, setHovered] = React.useState(false)
const [newBillingEmail, setNewBillingEmail] = useState('');
const {supportEmail} = useContext(Context);
var top_text = "Base Cloud Access"
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
subscription.name = "Enterprise"
subscription.currency_text = "$"
subscription.price = subscription.level * 180
subscription.limit = subscription.level * 100000
subscription.interval = subscription.recurrence
subscription.features = [
"Includes " + subscription.limit + " app runs/month. ",
"Multi-Tenancy and Region-Selection",
"And all other features from /pricing",
]
}
var newPaperstyle = JSON.parse(JSON.stringify(paperStyle))
if (subscription.name === "Enterprise" && subscription.active === true) {
top_text = "Current Plan"
// newPaperstyle.border = "1px solid #f85a3e"
}
var showSupport = false
if (subscription.name.includes("default")) {
top_text = "Custom Contract"
// newPaperstyle.border = "1px solid rgba(255,255,255,0.3)"
showSupport = true
}
if (subscription.name.includes("App Run Units")) {
top_text = "Cloud Access"
showSupport = true
}
if (subscription.name.includes("Open Source")) {
top_text = "Open Source"
showSupport = true
}
if (subscription.name.includes("Scale")) {
top_text = "Scale access"
setIsScale(true)
}
if (highlight === true) {
// Add an "Upgrade now" button
// newPaperstyle.border = "1px solid rgba(255,255,255,0.3)"
}
// if (hovered) {
// newPaperstyle.backgroundColor = "#2b2b2b"
// }
const handleClickOpen = () => {
setOpenChangeEmailBox(true);
};
const handleCloseChangeEmailBox = () => {
setOpenChangeEmailBox(false);
};
const getCircularReplacer = () => {
const seen = new WeakSet();
return (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};
const HandleChangeBillingEmail = (orgId) => {
const email = newBillingEmail;
const emailPattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
if (!emailPattern.test(email)) {
toast("Please enter a valid email address");
return;
} else {
setNewBillingEmail(email);
}
toast("Updating billing email. Please Wait")
const data = {
org_id: orgId,
email: newBillingEmail,
name: selectedOrganization.name,
description: selectedOrganization.description,
image: selectedOrganization.image,
defaults: selectedOrganization.defaults,
sso_config: selectedOrganization.sso_config,
mfa_required: selectedOrganization.mfa_required,
billing: {
email: newBillingEmail,
AlertThreshold: selectedOrganization?.Billing?.AlertThreshold,
Consultation: selectedOrganization?.Billing?.Consultation,
},
};
const url = `${globalUrl}/api/v1/orgs/${orgId}/billing`;
fetch(url, {
method: "POST",
body: JSON.stringify(data),
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Bad status code in get org:", response.status);
}
return response.json();
}).then((responseJson) => {
if (responseJson.success === true) {
toast.success("Successfully updated billing email");
setBillingEmail(newBillingEmail);
setOpenChangeEmailBox(false);
} else {
toast.error("Failed to update billing email. Please try again.");
}
})
.catch((error) => {
console.log("Error getting org:", error);
});
}
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{
e.preventDefault();
setSignatureOpen(false);
setTosChecked(false)
}}
>
Read and Accept the EULA
{
setTosChecked(e.target.checked)
}}
inputProps={{ 'aria-label': 'primary checkbox' }}
/>
{
setTosChecked(!tosChecked)
}}>
Accept
By clicking the “accept” button, you are signing the document, electronically agreeing that it has the same legal validity and effects as a handwritten signature, and that you have the competent authority to represent and sign on behalf an entity. Need support or have questions? Contact us at {supportEmail}
{
setSignatureOpen(false)
subscription.eula_signed = true
sendSignatureRequest(subscription)
}}
>
Submit
Current Plan
{top_text === "Base Cloud Access" && userdata.has_card_available === true ?
{
console.log("Clicked chip")
}}
variant="outlined"
color="primary"
/>
: null}
{top_text}
{top_text === "Base Cloud Access" && userdata.has_card_available === false ?
: null}
{isCloud && highlight === true && top_text !== "Base Cloud Access" ?
{
setSignatureOpen(true)
}}
>
: null}
{subscription.name}
{subscription.currency_text !== undefined ?
{subscription.currency_text}{subscription.price}
/ {subscription.interval}
: null}
Features
{subscription.features !== undefined && subscription.features !== null ?
subscription.features.map((feature, index) => {
var parsedFeature = feature
if (feature.includes("Documentation: ")) {
parsedFeature =
Documentation to get started
}
if (feature.includes("Worker License: ")) {
const fieldId = "webhook_uri_field_" + index
parsedFeature =
Use the {feature.split("Worker License: ")[0]} Worker
{ }}
InputProps={{
endAdornment:
{
var copyText = document.getElementById(fieldId);
if (copyText !== undefined && copyText !== null) {
console.log("NAVIGATOR: ", navigator);
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(copyText.value);
copyText.select();
copyText.setSelectionRange(
0,
99999
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Copied Webhook URL");
} else {
console.log("Couldn't find webhook URI field: ", copyText);
}
}}
edge="end"
>
}}
fullWidth
/>
}
return (
{parsedFeature}
)
})
: null}
{isCloud && (highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ?
{subscription.name.includes("Scale") ?
""
:
userdata.has_card_available === true ?
"While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month."
:
isCloud ?
`You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.`
:
`You are not subscribed to any plan and are using the free, open source plan. This plan has no enforced limits, but scale issues may occur due to CPU congestion.`
}
{BillingEmail?.length > 0 ? `Billing email: ${BillingEmail}` : null}
{userdata.has_card_available === true && (
setIsMouseOverOnChangeEmail(true)}
onMouseOut={() => setIsMouseOverOnChangeEmail(false)}
style={{
width: 'auto',
height: 20,
textTransform: 'none',
marginLeft: 'auto',
backgroundColor: 'transparent',
color: '#f86743',
boxShadow: 'none',
border: 'none',
cursor: 'pointer',
transition: 'background-color 0.3s, color 0.3s',
}}
>
Change
)}
Change Billing Email
Enter the new billing email address.
{ if (event.key === 'Enter') HandleChangeBillingEmail(selectedOrganization.id) }}
onChange={(e) => setNewBillingEmail(e.target.value)}
/>
Cancel
HandleChangeBillingEmail(selectedOrganization.id)}
>
Save
{/*isCloud ?
{
window.open("https://checkout.stripe.com/c/pay/ppage_1O8UttDzMUgUjxHSBh3krC6Y#fidkdWxOYHwnPyd1blpxYHZxWkBhfWJOY3RoVEJdXDBPSW9hR3RxcG1GcjU1R01nbE5PQUcnKSdobGF2Jz9%2BJ2JwbGEnPydjY2M3MjA0NyhnYzcyKDFkMTQoPWFmPSgwYDI9MTA0NGAyZDNhPWZhNmcnKSdocGxhJz8nNWYwMjNnZGMoZGdgPCgxPDxhKD1kNjUoMjZmPTcxYzw9NjBjYzZmMGRnJykndmxhJz8nZGYxYWZhY2MoZ2FjNygxPDA2KDxhZ2MoZjY1PWM8NGcxYzUxMjJkYTRmJ3gpJ2dgcWR2Jz9eWCknaWR8anBxUXx1YCc%2FJ3Zsa2JpYFpscWBoJyknd2BjYHd3YHdKd2xibGsnPydtcXF1PyoqaWpmZGltanZxPzY1NTcnKSdpamZkaWAnP2twaWl4JSUl")
}}
>
Add Payment Method
: null*/}
{
if (isCloud) {
handlePayasyougo(userdata, selectedOrganization, BillingEmail)
//navigate("/pricing?tab=cloud&highlight=true")
} else {
//window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
handlePayasyougo()
}
}}
>
{userdata.has_card_available === true ?
"Manage Card Details"
:
"Add Card Details"
}
{userdata.has_card_available === true ?
{
if (isCloud) {
navigate("/pricing?tab=cloud&highlight=true")
} else {
window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
}
}}
>
Upgrade plan
: null}
: null}
{/* {showSupport ?
{
if (window.drift !== undefined) {
//window.drift.api.startInteraction({ interactionId: 340045 })
window.drift.api.startInteraction({ interactionId: 340043 })
} else {
navigate("/contact")
}
}}>
Get Support
: null} */}
)
}
const ConsultationManagement = (props) => {
const { globalUrl, userdata, selectedOrganization, } = props;
const [inputHour, setInputHour] = React.useState(
selectedOrganization.Billing &&
selectedOrganization.Billing.Consultation &&
selectedOrganization.Billing.Consultation.hours !== undefined &&
selectedOrganization.Billing.Consultation.hours !== ""
? selectedOrganization.Billing.Consultation.hours
: 0
);
const [inputMinutes, setInputMinutes] = React.useState(
selectedOrganization.Billing &&
selectedOrganization.Billing.Consultation &&
selectedOrganization.Billing.Consultation.minutes !== undefined &&
selectedOrganization.Billing.Consultation.minutes !== ""
? selectedOrganization.Billing.Consultation.minutes
: 0
);
const [editConsultation, setEditConsultation] = React.useState(false);
const [openUpgradePlan, setOpenUpgradePlan] = React.useState(false);
const [consultationHours, setConsultationHours] = React.useState(1);
const [message, setMessage] = React.useState("");
const [hovered, setHovered] = React.useState(false)
const [getProfessionalServices, setGetProfessionalServices] = React.useState(false)
const [clickOnBuy, setClickOnBuy] = React.useState(false)
const formatedHours = String(inputHour).padStart(2, "0")
const formatedMinutes = String(inputMinutes).padStart(2, "0")
const handleHourChange = (event) => {
setInputHour(parseInt(event.target.value, 10));
};
const handleMinuteChange = (event) => {
setInputMinutes(parseInt(event.target.value, 10));
};
const toggleEditMode = () => {
setEditConsultation(!editConsultation);
};
const handleCancel = () => {
setEditConsultation(false);
};
const handleSave = () => {
toast("Saving consultation hours. Please wait.")
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`;
const data = {
org_id: selectedOrganization.id,
name: selectedOrganization.name,
description: selectedOrganization.description,
image: selectedOrganization.image,
defaults: selectedOrganization.defaults,
sso_config: selectedOrganization.sso_config,
mfa_required: selectedOrganization.mfa_required,
Billing: {
Consultation: {
hours: String(inputHour),
minutes: String(inputMinutes),
},
AlertThreshold: selectedOrganization.Billing.AlertThreshold,
email: selectedOrganization.Billing.email,
}
};
fetch(url, {
body: JSON.stringify(data),
mode: "cors",
method: "POST",
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
toast.success("Consultation hours saved successfully");
setEditConsultation(false);
} else {
toast.error("Failed saving consultation hours.");
}
if (inputHour > 0 || inputMinutes > 0) {
setGetProfessionalServices(true)
} else {
setGetProfessionalServices(false)
}
})
.catch((error) => {
console.log("Error: ", error);
});
}
const handleUpgradeConsultation = () => {
toast("Sending request for consultation hours. Please wait.")
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/consultation`;
const data = {
org_id: selectedOrganization.id,
consultationHours: String(consultationHours),
message: message,
};
fetch(url, {
body: JSON.stringify(data),
mode: "cors",
method: "POST",
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
toast.success("Thank you for your request. We will get back to you soon.");
setOpenUpgradePlan(false);
setEditConsultation(false);
} else {
toast.error("Failed sending consultation hours request. Please try again later.");
}
})
.catch((error) => {
console.log("Error: ", error);
});
}
useEffect(() => {
if (inputHour !== undefined && inputMinutes !== undefined && inputHour > 0 || inputMinutes > 0) {
setGetProfessionalServices(true)
} else {
setGetProfessionalServices(false)
}
})
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}>
Professional Services
Consultation & Management
You currently have a total of {inputHour} hours and {inputMinutes} minutes of professional services available by our experts.
{editConsultation ?
<>
:
>
:
{`${formatedHours}h:${formatedMinutes}m`}
}
{userdata.support === true ?
{editConsultation ? (
Cancel
) : (
Edit
)}
{editConsultation && Save }
: null}
Features
Build custom apps, integrations, and worklows for your specific use cases or applications
Help solve / debug / update / add features and capabilities of the platform
{
if (Cloud) {
ReactGA.event({
category: "Billing",
action: "Buy_consultation_hours",
});
}
setClickOnBuy(true)
}}
>
Buy
{ setClickOnBuy(false) }} PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: '440px',
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}>
You will be taken to Stripe to book professional service hours. You can adjust the number of hours on the left side of the Stripe page.
{
if (Cloud) {
ReactGA.event({
category: "Billing",
action: "Buy_consultation_hours",
})
}
const url = userdata.licensed === true
? "https://buy.stripe.com/aEU3fk17o0L092g5kt"
: "https://buy.stripe.com/aEU3fk17o0L092g5kt"
window.open(url, "_blank")
}}
>
Book Professional Service Hours
{
if (getProfessionalServices) {
setOpenUpgradePlan(true);
}
}}
>
Use Professional Service Hours
setOpenUpgradePlan(false)}
fullWidth
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: '440px',
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}
>
Upgrade Consultation Plan
Enter the total hours of consultation you want.
setConsultationHours(val)}
aria-labelledby="continuous-slider"
step={1}
min={1}
max={(inputHour === "0" && inputMinutes > 0) ? 1 : inputHour}
style={{ width: '80%', color: theme.palette.primary.main }}
marks
valueLabelDisplay="auto"
/>
If you have any additional requirements or questions, please leave a message below.
setMessage(e.target.value)}
/>
Submit Request for {consultationHours} hours
)
}
const TrainingService = () => {
const [hovered, setHovered] = React.useState(false)
const [openPrivateTraining, setOpenPrivateTraining] = React.useState(false)
const [PrivateTrainingMember, setPrivateTrainingMember] = React.useState(5)
const [message, setMessage] = React.useState("");
const handlePrivateTraining = () => {
toast("Submitting your request for private training. Please wait...")
const data = {
org_id: selectedOrganization.id,
trainingMembers: String(PrivateTrainingMember),
message: message
}
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/privateTraining`
fetch(url, {
body: JSON.stringify(data),
mode: "cors",
method: "POST",
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
}).then((response) => {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
}).then((responseJson) => {
if (responseJson.success === true) {
toast.success("Your request for private training has been submitted successfully. We will get back to you soon.")
setOpenPrivateTraining(false)
} else {
toast.error(`Failed sending request for private training. Please try again later or contact support@shuffler.io for help.`)
}
})
}
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
Training
Become a Shuffle Expert
Public Training
Public course on Automation for Security Professionals
Covers Shuffle Platform, Apps, Workflows, Usecases, JSON, Liquid Formatting, and more.
Private Training
Everything from Public Training
Customized for your team’s usecases, date and time, location, and more.
{
if (Cloud) {
ReactGA.event({
category: "Billing",
action: "click_public_training_button",
label: "Public Training",
userId: userdata?.id
});
}
navigate("/training")
}}
>
Public Training
{
if (Cloud) {
ReactGA.event({
category: "Billing",
action: "click_private_training_button",
label: "Private Training",
userId: userdata?.id,
});
}
setOpenPrivateTraining(true);
}}
>
Private Training
setOpenPrivateTraining(false)}
fullWidth
style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: '440px',
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
}
}}
>
Private Training
Enter the total members for private training. Minimum 5 members required.
setPrivateTrainingMember(val)}
aria-labelledby="continuous-slider"
step={1}
min={5}
max={50}
style={{ width: '80%', color: theme.palette.primary.main }}
marks
valueLabelDisplay="auto"
/>
If you have any additional requirements or questions, please leave a message below.
setMessage(e.target.value)}
/>
Submit Request
)
}
const addDealModal = (
{
setSelectedDealModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
minWidth: "800px",
minHeight: "320px",
},
}}
>
Register new deal
{
setDealName(e.target.value);
}}
/>
{
setDealAddress(e.target.value);
}}
/>
{
setDealValue(e.target.value);
}}
/>
option.label}
onChange={(event, newValue) => {
setDealCountry(newValue.label);
}}
renderOption={(props, option) => (
img": { mr: 2, flexShrink: 0 } }}
{...props}
>
{option.label} ({option.code}) +{option.phone}
)}
renderInput={(params) => (
)}
/>
{
setDealType(newValue);
}}
getOptionLabel={(option) => option.label}
renderOption={(props, option) => (
img": { mr: 2, flexShrink: 0 } }}
{...props}
>
{option.label}
)}
renderInput={(params) => (
)}
/>
{dealerror.length > 0 ? (
error registering: {dealerror}
) : null}
{
setSelectedDealModalOpen(false);
//setDealName("")
//setDealAddress("")
//setDealCountry("")
//setDealValue("")
}}
>
Cancel
{
submitDeal(dealName, dealAddress, dealCountry, dealValue);
}}
>
Submit
);
const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => {
if (dealerror.length > 0) {
setDealerror("");
}
const orgId = selectedOrganization.id;
const data = {
reseller_org: orgId,
name: dealName,
address: dealAddress,
country: dealCountry,
value: dealValue,
};
const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`;
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success === true) {
setSelectedDealModalOpen(false);
toast(
"Added new deal! We will be in touch shortly with an update."
);
setDealName("");
setDealAddress("");
setDealValue("");
setDealCountry("United States");
setDealType("MSSP");
} else {
setDealerror(responseJson.reason);
}
})
.catch(function (error) {
//console.log("Error: ", error);
setDealerror(error.toString());
toast("Failed adding deal reg: ", error);
});
};
const addAlertThreshold = () => {
setAlertThresholds([...alertThresholds, { percentage: '', count: '', Email_send: false }]);
};
const updateAlertThreshold = (index, field, value) => {
if (field === 'percentage') {
if (value > 100 || value < 0) {
value = 0
toast("The percentage value should be between 0 and 100")
}
} else if (field === 'count') {
if (value < 0 || value >= userdata.app_execution_limit) {
value = 0
toast("The count value should be greater than 0 and less than the total app execution limit")
}
}
const totalValue = userdata.app_execution_limit;
const newAlertThresholds = alertThresholds.map((threshold, i) => {
if (i === index) {
const newValue = parseFloat(value);
if (field === 'percentage') {
const newCount = (newValue / 100) * totalValue;
return {
...threshold,
percentage: isNaN(newValue) ? '' : Math.round(newValue),
count: isNaN(newCount) ? '' : Math.round(newCount),
Email_send: false
};
} else if (field === 'count') {
const newPercentage = (newValue / totalValue) * 100;
return {
...threshold,
count: newValue,
percentage: isNaN(newPercentage) ? '' : Math.round(newPercentage),
Email_send: false
};
}
}
return threshold;
});
setAlertThresholds(newAlertThresholds);
};
const handleDeleteAlertThreshold = (index) => {
const newAlertThresholds = alertThresholds.filter((_, i) => i !== index);
setAlertThresholds(newAlertThresholds);
// Update currentIndex based on remaining elements
const findCurrentIndex = newAlertThresholds.some(threshold => threshold.Email_send === false);
setCurrentIndex(findCurrentIndex ? newAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
toast.info("Alert Threshold deleted successfully. Don't forget to save your changes.");
};
const HandleEditOrgForAlertThreshold = (orgId) => {
// Use the `some` method to check for invalid counts
const invalidCount = alertThresholds.some((threshold) => {
if (threshold.count === '') {
toast("Please enter a valid Count or Percentage value");
return true; // Stop checking further and return true if invalid
}
return false;
});
// If any invalid count is found, return early
if (invalidCount) {
return;
}
toast("Updating Email Alert Threshold. Please wait...");
const data = {
org_id: orgId,
name: selectedOrganization?.name,
description: selectedOrganization?.description,
image: selectedOrganization?.image,
defaults: selectedOrganization?.defaults,
sso_config: selectedOrganization?.sso_config,
mfa_required: selectedOrganization?.mfa_required,
billing: {
email: BillingEmail,
AlertThreshold: alertThresholds.map(threshold => ({
...threshold,
percentage: parseInt(threshold.percentage, 10),
count: parseInt(threshold.count, 10),
})),
Consultation: selectedOrganization?.billing?.Consultation,
},
};
const url = `${globalUrl}/api/v1/orgs/${orgId}`;
fetch(url, {
method: "POST",
body: JSON.stringify(data),
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Bad status code in get org:", response.status);
}
return response.json();
}).then((responseJson) => {
console.log("Got org:", responseJson);
if (responseJson.success === true) {
toast.success("Successfully updated Email Alert Thresholds");
const findCurrentIndex = alertThresholds.some(threshold => threshold.Email_send === false);
setCurrentIndex(findCurrentIndex ? alertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1);
} else {
toast.error("Failed to update Email Alert Thresholds. Please try again.");
}
})
.catch((error) => {
console.log("Error getting org:", error);
});
};
const getSafeValue = (value) => {
if (value === undefined || value === null || isNaN(value)) {
return 0;
} else {
return value;
}
};
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
return (
{addDealModal}
{clickedFromOrgTab ?
Billing & Licensing :
Billing & Licensing
}
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
<>
{clickedFromOrgTab ?
{isCloud ?
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
:
"Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update."
} :
{isCloud ?
"Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below."
:
"Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update."
}
}
> }
{userdata.support === true ?
For sales: Create
EU contract
or
NOT EU contract
-
Google Drive Link
-
Sales Process (old)
:
null
}
{isChildOrg ?
Licensing is handled by your parent organisation. Reach out to {supportEmail} if you have questions about this.
: null}
{/* {isCloud &&
selectedOrganization.subscriptions !== undefined &&
selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 &&
!isChildOrg ?
selectedOrganization.subscriptions
.reverse()
.map((sub, index) => {
return (
)
})
: null} */}
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null :
: !isCloud ?
{/* */}
{/* */}
: null}
{/*
Quantity : {sub.level}
Recurrence : {sub.recurrence}
{sub.active ? (
Started :{" "}
{new Date(sub.startdate * 1000).toISOString()}
{
cancelSubscriptions(sub.reference);
}}
>
Cancel subscription
) : (
Cancelled :{" "}
{new Date(
sub.cancellationdate * 1000
).toISOString()}
Status : Deactivated
)}
*/}
{/*isCloud &&
selectedOrganization.partner_info !== undefined &&
selectedOrganization.partner_info.reseller === true ? (
Reseller dashboard
{
setSelectedDealModalOpen(true);
}}
>
Add deal
{
handleGetDeals(userdata.active_org.id);
}}
>
{dealList.length === 0 ? (
No deals registered yet. Click "Add deal" to register one
) : (
dealList.map((deal, index) => {
var bgColor = "#27292d";
if (index % 2 === 0) {
bgColor = "#1f2023";
}
return (
);
})
)}
) : null*/}
{!isChildOrg && isCloud && (
Professional Services
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}.
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
{billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
isChildOrg ? null : (
)
) : null}
)}
{isCloud ? (
Manage Billing
Manage your billing and licensing information below. When you reach the certain thresholds of your subscription limit, you will be notified by email.
Current Usage:
You have used {currentAppRunsInPercentage}% of total app execution limit or {userdata.app_execution_usage} app runs out of {userdata.app_execution_limit} app runs.
Set email alert thresholds for app runs
You will be notified by email when you reach the
{currentIndex !== -1
? " " + getSafeValue(alertThresholds[currentIndex].percentage) + '%' + " "
: " " + '0%' + " "
}
of your total app execution limit or
{currentIndex !== -1
? " " + getSafeValue(alertThresholds[currentIndex].count) + " "
: " " + 0 + " "}
app runs.
Please note : Once your app runs reach the set alert threshold, all admins in the organization will receive an email notification.
{alertThresholds.map((threshold, index) => (
updateAlertThreshold(index, 'percentage', e.target.value)}
margin="normal"
variant="outlined"
inputProps={{
max: 100,
}}
/>
updateAlertThreshold(index, 'count', e.target.value)}
margin="normal"
variant="outlined"
/>
{alertThresholds[index].Email_send === true && (
)}
{alertThresholds.length > 1 && (
{
setDeleteAlertVerification(true);
setDeleteAlertIndex(index);
}}
>
)}
setDeleteAlertVerification(false)}
sx={{
'& .MuiBackdrop-root': { backgroundColor: 'rgba(0, 0, 0, 0.3)' },
}}
>
Are you sure you want to delete this threshold?
setDeleteAlertVerification(false)}
>
Cancel
{
handleDeleteAlertThreshold(deleteAlertIndex);
setDeleteAlertVerification(false);
}}
>
Delete
))}
Add Threshold
{ HandleEditOrgForAlertThreshold(selectedOrganization.id) }}
>
Save
): null}
Utilization & Stats
{isChildOrg ? (
): (
<>
setCurrentTab(newValue)}
style={{ marginTop: 20 }}
TabIndicatorProps={{
style: {
height: 3,
backgroundColor: theme.palette.primary.main,
marginLeft: 12,
marginRight: 12,
}
}}
>
{currentTab === 0 ? (
): (
)}
>
)}
)
})
export default memo(Billing);
const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, allChildOrgs, setAllChildOrgs, allChildOrgsStats, setAllChildOrgsStats }) => {
const [subOrgStats, setSubOrgStats] = useState([]);
const [subOrgs, setSubOrgs] = useState([]);
const [subOrgStatsRows, setSubOrgStatsRows] = useState([]);
const [subOrgStatsColumns, setSubOrgStatsColumns] = useState([]);
const [allOrgLoaded, setAllOrgLoaded] = useState(false);
const [allOrgStatsLoaded, setAllOrgStatsLoaded] = useState(false);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState("")
const [editingOrgId, setEditingOrgId] = useState("")
const [limit, setLimit] = useState("")
const [tableCreated, setTableCreated] = useState(false)
const [searchQuery, setSearchQuery] = useState("");
const [filteredRows, setFilteredRows] = useState([]);
const { themeMode, brandColor, supportEmail } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
// Handle page change
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};
const HanldeLoadStats = async () => {
const childOrgs = selectedOrganization.child_orgs;
if (allChildOrgsStats.length > 0){
setSubOrgStats(allChildOrgsStats)
setAllOrgStatsLoaded(true)
if (allChildOrgsStats.length > 0 && allChildOrgs.length > 0 && subOrgStatsRows.length === 0 && subOrgStatsColumns.length === 0) {
HandleCreateTable(allChildOrgsStats, allChildOrgs)
}
return
}
const promises = childOrgs.map((org) => {
// get org stats base on region url
const baseUrl = org?.region_url?.length > 0 && !window?.location?.origin?.includes("localhost") ? org?.region_url : globalUrl;
const url = `${baseUrl}/api/v1/orgs/${org.id}/stats`;
return fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
});
try {
const responses = await Promise.all(promises);
setSubOrgStats(responses);
setAllChildOrgsStats(responses);
setAllOrgStatsLoaded(true);
} catch (error) {
console.error("Error loading stats:", error);
}
};
const HandleGetSuborg = async () => {
const childOrgs = selectedOrganization.child_orgs
if (allChildOrgs.length > 0){
setSubOrgs(allChildOrgs)
setAllOrgLoaded(true)
if (allChildOrgsStats.length > 0 && allChildOrgs.length > 0 && subOrgStatsRows.length === 0 && subOrgStatsColumns.length === 0) {
HandleCreateTable(allChildOrgsStats, allChildOrgs)
}
return
}
const promises = childOrgs.map((org) => {
const baseUrl = org?.region_url?.length > 0 && !window?.location?.origin?.includes("localhost") ? org?.region_url : globalUrl;
const url = `${baseUrl}/api/v1/orgs/${org.id}`;
return fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
});
try {
const responses = await Promise.all(promises);
setSubOrgs(responses);
setAllChildOrgs(responses);
setAllOrgLoaded(true);
} catch (error) {
console.error("Error loading suborgs:", error);
}
};
useEffect(() => {
if (subOrgStats.length === 0 && selectedOrganization && selectedOrganization?.child_orgs?.length > 0) {
HanldeLoadStats()
}
if (subOrgs.length === 0 && selectedOrganization && selectedOrganization?.child_orgs?.length > 0) {
HandleGetSuborg()
}
}, [selectedOrganization?.child_orgs]);
useEffect(() => {
if (allOrgLoaded && allOrgStatsLoaded && !tableCreated) {
HandleCreateTable(subOrgStats, subOrgs)
}
}
, [allOrgLoaded, allOrgStatsLoaded, tableCreated])
const HandleCreateTable = (subOrgStats, subOrgs) => {
if (subOrgStats.length === 0 || subOrgs.length === 0) return;
// check whether all of the suborg.success is false
const allSubOrgStatsSuccess = subOrgStats.every((stat) => stat.success === false);
const allSubOrgsSuccess = subOrgs.every((org) => org.success === false);
if (allSubOrgStatsSuccess || allSubOrgsSuccess) {
setSubOrgStats([])
setSubOrgs([])
setTableCreated(true)
return
}
const rows = subOrgStats.map((stat, index) => {
const subOrg = subOrgs[index]
if (!subOrg) return null;
return {
id: index,
name: subOrg.name,
orgId: subOrg.id,
limit: subOrg?.sync_features?.app_executions?.limit || "N/A",
usage: stat?.monthly_app_executions || "N/A",
workflows_usage: stat?.total_workflow_executions || "N/A",
workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A",
}
})
setSubOrgStatsRows(rows)
const columns = [
{ field: "id", headerName: "ID", width: 100 },
{ field: "name", headerName: "Name", width: 200 },
{ field: "usage", headerName: "App Execution Usage", width: 200 },
{
field: "limit", headerName: "App Execution Limit", width: 200, renderCell: (params) => {
return (
<>
{params.value}
{
setOpen(true)
setEditingOrgId(params.row.orgId)
setEditing("app_executions")
if (params.value === "N/A") {
setLimit("")
} else {
setLimit(params.value)
}
}}
>
>
)
}
},
{ field: "workflows_usage", headerName: "Workflow Execution Usage", width: 200 },
{ field: "workflow_usage_limit", headerName: "Workflow Execution Limit", width: 200, renderCell: (params) => {
return (
<>
{params.value}
{
setOpen(true)
setEditingOrgId(params.row.orgId)
setEditing("workflow_executions")
if (params.value === "N/A") {
setLimit("")
} else {
setLimit(params.value)
}
}}
>
>
)}
},
]
setSubOrgStatsColumns(columns)
if (allOrgLoaded && allOrgStatsLoaded && !tableCreated) {
setTableCreated(true)
}
}
const HandleEditLimit = (orgId, editing, limit) => {
// change limit as number if string
if (typeof limit === "string") {
limit = parseInt(limit, 10)
}
if (isNaN(limit)) {
toast.error("Please enter a valid number")
return
}
if (selectedOrganization.sync_features.app_executions.limit <= 10000) {
toast.error("Insufficient app execution limit to increase child org limit")
return
}
// check whether limit is greater than than parent org limit
if (editing === "app_executions" && limit > selectedOrganization.sync_features.app_executions.limit && !userdata.support) {
toast.error("App execution limit cannot be greater than parent org limit")
return
}
if (editing === "workflow_executions" && limit > selectedOrganization.sync_features.workflow_executions.limit && !userdata.support) {
toast.error("Workflow execution limit cannot be greater than parent org limit")
return
}
// find the org in the subOrgs array
const orgIndex = subOrgs.findIndex((org) => org.id === orgId)
if (orgIndex === -1) {
toast.error("Organization not found")
return
}
const org = subOrgs[orgIndex]
org.sync_features[editing].limit = limit
org.sync_features.editing = true
const sync_features = org.sync_features
const data = {
org_id: orgId,
sync_features: sync_features,
}
const url = `${globalUrl}/api/v1/orgs/${orgId}`;
fetch(url, {
method: "POST",
credentials: "include",
crossDomain: true,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed updating org: ", responseJson.reason);
} else {
toast("Successfully change suborg limit!");
if (editing === "app_executions") {
setSubOrgStatsRows((prevRows) => {
const newRows = [...prevRows];
newRows[orgIndex].limit = limit;
return newRows;
});
}else if (editing === "workflow_executions") {
setSubOrgStatsRows((prevRows) => {
const newRows = [...prevRows];
newRows[orgIndex].workflow_usage_limit = limit;
return newRows;
});
}
}
})
)
.catch((error) => {
toast("Err: " + error.toString());
});
}
const HandleClosePopUP = () => {
setOpen(false)
setEditing("")
setEditingOrgId("")
setLimit("")
}
return (
{open && (
)}
Child Organizations
View and configure execution limits for child organizations. Click the edit icon to modify app and workflow execution limits.
{tableCreated ? (
subOrgStatsRows.length > 0 && subOrgStatsColumns.length > 0 ? (
<>
),
}}
onChange={(e) => {
setSearchQuery(e.target.value.toLowerCase());
const filtered = subOrgStatsRows.filter((row) =>
row.name.toLowerCase().includes(e.target.value.toLowerCase().trim()) ||
row.orgId.toLowerCase().includes(e.target.value.toLowerCase().trim())
);
setFilteredRows(filtered);
}}
/>
>
) : (
{selectedOrganization.child_orgs.length === 0 ? "No child organizations exist." : "Unable to load child organization stats. Statistics may not be initialized yet." }
)
) : (
)}
);
});
const IncreaseLimitPopUp = memo(({ open, onClose, limit, setLimit, HandleEditLimit, editingOrgId, editing}) => {
const [currentLimit, setCurrentLimit] = useState(limit)
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
return(
Increase {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit
setCurrentLimit(e.target.value)}
label={`${editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit`}
type="string"
variant="outlined"
fullWidth
InputProps={{
style: {
color: theme.palette.text.primary,
},
}}
InputLabelProps={{
style: {
color: theme.palette.text.primary,
},
}}
margin="normal"
onKeyUp={(e) => {
if (e.key === "Enter") {
HandleEditLimit(editingOrgId, editing, currentLimit)
setLimit(currentLimit)
onClose()
}}
}
>
Cancel
{
HandleEditLimit(editingOrgId, editing, currentLimit)
setLimit(currentLimit)
onClose()
}} style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginRight: 10 }}>
Save
)
})
const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => {
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
const wrapperStyle = useMemo(() => ({
width: clickedFromOrgTab
? "100%"
: "auto",
padding: "27px 10px 19px 27px",
backgroundColor: theme.palette.platformColor,
height: '100%',
boxSizing: 'border-box',
overflow: 'hidden',
maxHeight: "1700px",
overflowY: "auto",
scrollbarColor: theme.palette.scrollbarColorTransparent,
scrollbarWidth: 'thin'
}), [clickedFromOrgTab, theme]);
return (
{children}
);
});
const Wrapper = memo(({ children, clickedFromOrgTab }) => {
return (
{children}
);
});