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 = [
|
||||
@@ -121,7 +177,7 @@ const Billing = (props) => {
|
||||
maxWidth: 400,
|
||||
width: "100%",
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
borderRadius: theme.palette.borderRadius*2,
|
||||
borderRadius: theme.palette.borderRadius * 2,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
marginRight: 10,
|
||||
marginTop: 15,
|
||||
@@ -183,7 +239,7 @@ const Billing = (props) => {
|
||||
label: "",
|
||||
})
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(function (error) {
|
||||
console.error("STRIPE ERROR: ", error)
|
||||
|
||||
ReactGA.event({
|
||||
@@ -238,6 +294,7 @@ const Billing = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const sendSignatureRequest = (subscription) => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`;
|
||||
|
||||
@@ -275,13 +332,14 @@ 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) {
|
||||
subscription.name = "Enterprise"
|
||||
subscription.currency_text = "$"
|
||||
subscription.price = subscription.level*180
|
||||
subscription.limit = subscription.level*100000
|
||||
subscription.price = subscription.level * 180
|
||||
subscription.limit = subscription.level * 100000
|
||||
subscription.interval = subscription.recurrence
|
||||
subscription.features = [
|
||||
"Includes " + subscription.limit + " app runs/month. ",
|
||||
@@ -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}
|
||||
@@ -388,16 +518,16 @@ const Billing = (props) => {
|
||||
}}
|
||||
inputProps={{ 'aria-label': 'primary checkbox' }}
|
||||
/>
|
||||
<Typography variant="body1" style={{display: "inline-block", marginLeft: 10, marginTop: 25, cursor: "pointer", }} onClick={() => {
|
||||
<Typography variant="body1" style={{ display: "inline-block", marginLeft: 10, marginTop: 25, cursor: "pointer", }} onClick={() => {
|
||||
setTosChecked(!tosChecked)
|
||||
}}>
|
||||
Accept
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{display: "inline-block", marginLeft: 10, }} color="textSecondary">
|
||||
<Typography variant="body2" style={{ display: "inline-block", marginLeft: 10, }} color="textSecondary">
|
||||
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 support@shuffler.io.
|
||||
</Typography>
|
||||
|
||||
<div style={{display: "flex", marginTop: 25, }}>
|
||||
<div style={{ display: "flex", marginTop: 25, }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -415,7 +545,7 @@ const Billing = (props) => {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div style={{display: "flex"}}>
|
||||
<div style={{ display: "flex" }}>
|
||||
{top_text === "Base Cloud Access" && userdata.has_card_available === true ?
|
||||
<Chip
|
||||
style={{
|
||||
@@ -478,7 +608,7 @@ const Billing = (props) => {
|
||||
</Typography>
|
||||
|
||||
{subscription.currency_text !== undefined ?
|
||||
<div style={{display: "flex", }}>
|
||||
<div style={{ display: "flex", }}>
|
||||
<Typography variant="h6" style={{ marginTop: 10, }}>
|
||||
{subscription.currency_text}{subscription.price}
|
||||
</Typography>
|
||||
@@ -500,16 +630,16 @@ const Billing = (props) => {
|
||||
<a
|
||||
href={feature.split("Documentation: ")[1]}
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "#f85a3e",}}
|
||||
style={{ textDecoration: "none", color: "#f85a3e", }}
|
||||
>
|
||||
Documentation to get started
|
||||
</a>
|
||||
}
|
||||
|
||||
if (feature.includes("Worker License: ")) {
|
||||
const fieldId = "webhook_uri_field_"+index
|
||||
const fieldId = "webhook_uri_field_" + index
|
||||
parsedFeature =
|
||||
<span style={{marginTop: 10, }}>
|
||||
<span style={{ marginTop: 10, }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
>
|
||||
@@ -522,7 +652,7 @@ const Billing = (props) => {
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
id={fieldId}
|
||||
onClick={() => {}}
|
||||
onClick={() => { }}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment position="end">
|
||||
@@ -565,7 +695,7 @@ const Billing = (props) => {
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<Typography variant="body2" color="textPrimary" style={{ }}>
|
||||
<Typography variant="body2" color="textPrimary" style={{}}>
|
||||
{parsedFeature}
|
||||
</Typography>
|
||||
</li>
|
||||
@@ -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")
|
||||
@@ -671,7 +873,7 @@ const Billing = (props) => {
|
||||
}}>
|
||||
Get Support
|
||||
</Button>
|
||||
: null }
|
||||
: null}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
@@ -967,23 +1169,147 @@ 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 (
|
||||
<div style={{ width: clickedFromOrgTab? 1030 : "auto", padding: 27, backgroundColor: '#212121', borderRadius: '16px', }}>
|
||||
<div style={{ width: clickedFromOrgTab ? 1030 : "auto", padding: 27, backgroundColor: '#212121', borderRadius: '16px', }}>
|
||||
{addDealModal}
|
||||
{clickedFromOrgTab?
|
||||
<h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Billing & Licensing</h2>:
|
||||
{clickedFromOrgTab ?
|
||||
<h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Billing & Licensing</h2> :
|
||||
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
Billing & Licensing
|
||||
</Typography>}
|
||||
{clickedFromOrgTab?
|
||||
{clickedFromOrgTab ?
|
||||
<span style={{ color: "#9E9E9E" }}>{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."
|
||||
}</span>:
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10}}>
|
||||
}</span> :
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10 }}>
|
||||
{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."
|
||||
:
|
||||
@@ -992,7 +1318,7 @@ const Billing = (props) => {
|
||||
</Typography>}
|
||||
|
||||
{userdata.support === true ?
|
||||
<div style={{marginBottom: 10, marginTop:clickedFromOrgTab?16:null, color:clickedFromOrgTab?"#F1F1F1":null }}>
|
||||
<div style={{ marginBottom: 10, marginTop: clickedFromOrgTab ? 16 : null, color: clickedFromOrgTab ? "#F1F1F1" : null }}>
|
||||
For sales: Create
|
||||
<a href={"https://docs.google.com/document/d/1OeJSi42812EMg7fUAw1HAj1ymOG8rfp8Ma_DGJKvwgI/copy?usp=sharing&organization=" + selectedOrganization.id} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>
|
||||
New Cloud Contract
|
||||
@@ -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
|
||||
@@ -1018,12 +1342,12 @@ const Billing = (props) => {
|
||||
|
||||
|
||||
{isChildOrg ?
|
||||
<Typography variant="h6" style={{marginBottom: 50, }}>
|
||||
<Typography variant="h6" style={{ marginBottom: 50, }}>
|
||||
Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this.
|
||||
</Typography>
|
||||
: null}
|
||||
|
||||
<div style={{display: "flex", maxWidth: 768, minWidth: 768, }}>
|
||||
<div style={{ display: "flex", maxWidth: 768, minWidth: 768, }}>
|
||||
{isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null :
|
||||
<SubscriptionObject
|
||||
index={0}
|
||||
@@ -1037,7 +1361,7 @@ const Billing = (props) => {
|
||||
highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0}
|
||||
/>
|
||||
: !isCloud ?
|
||||
<span style={{display: "flex", }}>
|
||||
<span style={{ display: "flex", }}>
|
||||
<SubscriptionObject
|
||||
index={0}
|
||||
globalUrl={globalUrl}
|
||||
@@ -1089,7 +1413,7 @@ const Billing = (props) => {
|
||||
.map((sub, index) => {
|
||||
return (
|
||||
<SubscriptionObject
|
||||
index={index+1}
|
||||
index={index + 1}
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
serverside={serverside}
|
||||
@@ -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