Fixed a lot of billing-ui problems, and added rate limiting alerts
This commit is contained in:
@@ -24,9 +24,12 @@ import {
|
||||
Chip,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
LinearProgress
|
||||
} from "@mui/material";
|
||||
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useNavigate, Link, json } from "react-router-dom";
|
||||
import { Autocomplete } from "@mui/material";
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
@@ -35,12 +38,15 @@ import {
|
||||
ContentCopy as ContentCopyIcon,
|
||||
Draw as DrawIcon,
|
||||
Close as CloseIcon,
|
||||
Delete,
|
||||
RestaurantRounded,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
//import { useAlert
|
||||
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
|
||||
import BillingStats from "../components/BillingStats.jsx";
|
||||
import { handlePayasyougo } from "../views/HandlePaymentNew.jsx"
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
const Billing = (props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props;
|
||||
@@ -58,6 +64,56 @@ const Billing = (props) => {
|
||||
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);
|
||||
|
||||
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 products = [
|
||||
@@ -238,6 +294,7 @@ const Billing = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const sendSignatureRequest = (subscription) => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`;
|
||||
|
||||
@@ -275,6 +332,7 @@ const Billing = (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('');
|
||||
|
||||
var top_text = "Base Cloud Access"
|
||||
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
|
||||
}
|
||||
|
||||
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 (
|
||||
<Paper
|
||||
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.`
|
||||
}
|
||||
</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 ?
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -617,7 +819,7 @@ const Billing = (props) => {
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
handlePayasyougo(userdata)
|
||||
handlePayasyougo(userdata, selectedOrganization, BillingEmail)
|
||||
//navigate("/pricing?tab=cloud&highlight=true")
|
||||
} else {
|
||||
//window.open("https://shuffler.io/pricing?tab=onprem&highlight=true", "_blank")
|
||||
@@ -967,6 +1169,130 @@ const Billing = (props) => {
|
||||
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
|
||||
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" }}>
|
||||
Sales Process
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
:
|
||||
null
|
||||
@@ -1333,9 +1657,144 @@ const Billing = (props) => {
|
||||
|
||||
</div>
|
||||
) : null*/}
|
||||
<div style={{ marginTop: 100, }}>
|
||||
<div style={{ marginTop: 50, marginLeft: 10 }}>
|
||||
<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"
|
||||
>
|
||||
Utilization & Stats
|
||||
|
||||
@@ -2153,7 +2153,7 @@ const Workflows = (props) => {
|
||||
}
|
||||
|
||||
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}>
|
||||
{selectedCategory !== "" ?
|
||||
<Tooltip title={`Usecase Category: ${selectedCategory}`} placement="bottom">
|
||||
|
||||
Reference in New Issue
Block a user