Fixed a lot of billing-ui problems, and added rate limiting alerts

This commit is contained in:
Frikky
2024-06-20 14:57:40 +02:00
parent 298f7c402e
commit ed6ecc0bf4
2 changed files with 1190 additions and 731 deletions
+466 -7
View File
@@ -24,9 +24,12 @@ import {
Chip, Chip,
Checkbox, Checkbox,
Tooltip, Tooltip,
DialogContentText,
DialogActions,
LinearProgress
} from "@mui/material"; } from "@mui/material";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link, json } from "react-router-dom";
import { Autocomplete } from "@mui/material"; import { Autocomplete } from "@mui/material";
import { toast } from "react-toastify" import { toast } from "react-toastify"
@@ -35,12 +38,15 @@ import {
ContentCopy as ContentCopyIcon, ContentCopy as ContentCopyIcon,
Draw as DrawIcon, Draw as DrawIcon,
Close as CloseIcon, Close as CloseIcon,
Delete,
RestaurantRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
//import { useAlert //import { useAlert
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
import BillingStats from "../components/BillingStats.jsx"; import BillingStats from "../components/BillingStats.jsx";
import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" import { handlePayasyougo } from "../views/HandlePaymentNew.jsx"
import DeleteIcon from '@mui/icons-material/Delete';
const Billing = (props) => { const Billing = (props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props; const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props;
@@ -58,6 +64,56 @@ const Billing = (props) => {
const [dealValue, setDealValue] = React.useState(""); const [dealValue, setDealValue] = React.useState("");
const [dealDiscount, setDealDiscount] = React.useState(""); const [dealDiscount, setDealDiscount] = React.useState("");
const [dealerror, setDealerror] = 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);
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);
}
}, [userdata]);
const [BillingEmail, setBillingEmail] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.Email !== undefined && selectedOrganization.Billing.Email != null && selectedOrganization.Billing.Email.length > 0 ? selectedOrganization.Billing.Email : selectedOrganization.org);
useState(() => {
// Set the billing email
setBillingEmail(
selectedOrganization.Billing !== undefined &&
selectedOrganization.Billing.Email !== undefined &&
selectedOrganization.Billing.Email.length > 0
? selectedOrganization.Billing.Email
: selectedOrganization.org
);
// 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 stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : ""
const products = [ const products = [
@@ -238,6 +294,7 @@ const Billing = (props) => {
}); });
}; };
const sendSignatureRequest = (subscription) => { const sendSignatureRequest = (subscription) => {
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`;
@@ -275,6 +332,7 @@ const Billing = (props) => {
const [signatureOpen, setSignatureOpen] = React.useState(false); const [signatureOpen, setSignatureOpen] = React.useState(false);
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
const [hovered, setHovered] = React.useState(false) const [hovered, setHovered] = React.useState(false)
const [newBillingEmail, setNewBillingEmail] = useState('');
var top_text = "Base Cloud Access" var top_text = "Base Cloud Access"
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) { if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
@@ -327,6 +385,78 @@ const Billing = (props) => {
newPaperstyle.backgroundColor = theme.palette.surfaceColor newPaperstyle.backgroundColor = theme.palette.surfaceColor
} }
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}$/;
console.log("Pattern matches: ", emailPattern.test(email));
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,
billing: {
email: newBillingEmail,
},
};
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) => {
console.log("Got org:", 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 ( return (
<Paper <Paper
style={newPaperstyle} style={newPaperstyle}
@@ -587,7 +717,79 @@ const Billing = (props) => {
`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 plan with max 10,000 app runs per month. Upgrade to deactivate this limit.`
} }
</Typography> </Typography>
Billing email: {selectedOrganization.org} <div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography variant="body2" style={{}}>
Billing email: {BillingEmail}
</Typography>
{userdata.has_card_available === true && (
<Button
variant="contained"
onClick={handleClickOpen}
onMouseOver={() => 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
</Button>
)}
<Dialog
open={openChangeEmailBox}
onClose={handleCloseChangeEmailBox}
PaperProps={{
style: {
padding: '20px',
borderRadius: '8px',
maxWidth: '500px',
width: '100%'
}
}}
>
<DialogTitle style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>Change Billing Email</DialogTitle>
<DialogContent>
<DialogContentText style={{ marginBottom: '20px' }}>
Enter the new billing email address.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="email"
label="New Billing Email"
type="email"
fullWidth
value={newBillingEmail}
onKeyPress={(event) => { if (event.key === 'Enter') HandleChangeBillingEmail(selectedOrganization.id) }}
onChange={(e) => setNewBillingEmail(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button
onClick={handleCloseChangeEmailBox}
color="primary"
style={{ textTransform: 'none', marginRight: '10px' }}
>
Cancel
</Button>
<Button
color="primary"
style={{ textTransform: 'none' }}
onClick={(event) => HandleChangeBillingEmail(selectedOrganization.id)}
>
Save
</Button>
</DialogActions>
</Dialog>
</div>
{/*isCloud ? {/*isCloud ?
<Button <Button
variant="contained" variant="contained"
@@ -617,7 +819,7 @@ const Billing = (props) => {
}} }}
onClick={() => { onClick={() => {
if (isCloud) { if (isCloud) {
handlePayasyougo(userdata) handlePayasyougo(userdata, selectedOrganization, BillingEmail)
//navigate("/pricing?tab=cloud&highlight=true") //navigate("/pricing?tab=cloud&highlight=true")
} else { } else {
//window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank") //window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
@@ -967,6 +1169,130 @@ const Billing = (props) => {
toast("Failed adding deal reg: ", error); 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);
};
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,
billing: {
email: BillingEmail,
AlertThreshold: alertThresholds.map(threshold => ({
...threshold,
percentage: parseInt(threshold.percentage, 10),
count: parseInt(threshold.count, 10),
})),
},
};
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 const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null
return ( return (
@@ -1009,8 +1335,6 @@ const Billing = (props) => {
<a href={"https://github.com/Shuffle/Shuffle-docs/tree/master/handbook/Sales"} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}> <a href={"https://github.com/Shuffle/Shuffle-docs/tree/master/handbook/Sales"} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>
Sales Process Sales Process
</a> </a>
</div> </div>
: :
null null
@@ -1333,9 +1657,144 @@ const Billing = (props) => {
</div> </div>
) : null*/} ) : null*/}
<div style={{ marginTop: 100, }}> <div style={{ marginTop: 50, marginLeft: 10 }}>
<Typography <Typography
style={{ marginTop: 40, marginLeft: 10, marginBottom: 5 }} style={{ marginTop: 40, marginBottom: 5 }}
variant="h4"
>
Manage Billing
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10 }}>
Manage your billing and licensing information below. When you reach the certain thresholds of your subscription limit, you will be notified by email.
</Typography>
<Typography variant="body1">Current Usage:</Typography>
<LinearProgress
variant="determinate"
value={currentAppRunsInPercentage}
style={{
width: "50%",
height: 15,
borderRadius: 10,
margin: '10px 0',
marginBottom: 10,
}}
/>
<Typography variant="body1" color="textSecondary">
You have used <strong>{currentAppRunsInPercentage}%</strong> of total app execution limit or <strong>{userdata.app_execution_usage}</strong> app runs out of <strong>{userdata.app_execution_limit}</strong> app runs.
</Typography>
<div>
<Typography variant="body1" style={{ marginTop: 20 }}>
Set email alert thresholds for app runs
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 10 }}>
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.
</Typography>
<div style={{ marginTop: 15 }}>
{alertThresholds.map((threshold, index) => (
<div key={index} style={{ display: 'flex', alignItems: 'center' }}>
<TextField
style={{
marginTop: 10,
backgroundColor: theme.palette.inputColor,
width: 250,
}}
InputProps={{
style: {
height: 50,
color: 'white',
},
endAdornment: '%',
}}
color="primary"
fullWidth
label="Alert threshold (%)"
type="number"
value={threshold.percentage}
onChange={(e) => updateAlertThreshold(index, 'percentage', e.target.value)}
margin="normal"
variant="outlined"
inputProps={{
max: 100,
}}
/>
<TextField
style={{
marginTop: 10,
backgroundColor: theme.palette.inputColor,
width: 250,
marginLeft: 15,
}}
InputProps={{
style: {
height: 50,
color: 'white',
},
}}
color="primary"
fullWidth
label="Alert threshold (count)"
type="number"
value={threshold.count}
onChange={(e) => updateAlertThreshold(index, 'count', e.target.value)}
margin="normal"
variant="outlined"
/>
{
alertThresholds.length > 1 &&
(
<Button
disableRipple
disableElevation
sx={{
padding: 0,
color: 'red',
'&:hover': {
backgroundColor: 'transparent',
},
}}
onClick={() => { handleDeleteAlertThreshold(index) }}
>
<DeleteIcon />
</Button>
)
}
</div>
))}
</div>
</div>
<Button
variant="contained"
color="primary"
style={{ marginTop: 15, textTransform: 'none' }}
onClick={addAlertThreshold}
>
Add Threshold
</Button>
<Button
variant="contained"
color="primary"
style={{ marginTop: 15, textTransform: 'none', marginLeft: 20 }}
onClick={() => { HandleEditOrgForAlertThreshold(selectedOrganization.id) }}
>
Save
</Button>
</div>
<div style={{ marginTop: 40, display: 'flex', flexDirection: 'column' }}>
<Typography
style={{ marginTop: 10, marginLeft: 10, marginBottom: 5 }}
variant="h4" variant="h4"
> >
Utilization & Stats Utilization & Stats
+1 -1
View File
@@ -2153,7 +2153,7 @@ const Workflows = (props) => {
} }
return ( return (
<div style={{width: "100%", minWidth: 321, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "1px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}> <div style={{width: "100%", minWidth: 320, position: "relative", border: highlightIds.includes(data.id) ? "2px solid #f85a3e" : isDistributed || hasSuborgs ? "1px solid #40E0D0" : "inherit", borderRadius: theme.palette.borderRadius, }}>
<Paper square style={paperAppStyle}> <Paper square style={paperAppStyle}>
{selectedCategory !== "" ? {selectedCategory !== "" ?
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom"> <Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">