Fixed the merge issue

This commit is contained in:
monilprajapati
2024-06-20 21:13:53 +05:30
4 changed files with 1219 additions and 822 deletions
+492 -33
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 = [
@@ -121,7 +177,7 @@ const Billing = (props) => {
maxWidth: 400, maxWidth: 400,
width: "100%", width: "100%",
backgroundColor: theme.palette.platformColor, backgroundColor: theme.palette.platformColor,
borderRadius: theme.palette.borderRadius*2, borderRadius: theme.palette.borderRadius * 2,
border: "1px solid rgba(255,255,255,0.3)", border: "1px solid rgba(255,255,255,0.3)",
marginRight: 10, marginRight: 10,
marginTop: 15, marginTop: 15,
@@ -183,7 +239,7 @@ const Billing = (props) => {
label: "", label: "",
}) })
}) })
.catch(function(error) { .catch(function (error) {
console.error("STRIPE ERROR: ", error) console.error("STRIPE ERROR: ", error)
ReactGA.event({ ReactGA.event({
@@ -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,13 +332,14 @@ 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) {
subscription.name = "Enterprise" subscription.name = "Enterprise"
subscription.currency_text = "$" subscription.currency_text = "$"
subscription.price = subscription.level*180 subscription.price = subscription.level * 180
subscription.limit = subscription.level*100000 subscription.limit = subscription.level * 100000
subscription.interval = subscription.recurrence subscription.interval = subscription.recurrence
subscription.features = [ subscription.features = [
"Includes " + subscription.limit + " app runs/month. ", "Includes " + subscription.limit + " app runs/month. ",
@@ -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}
@@ -388,16 +518,16 @@ const Billing = (props) => {
}} }}
inputProps={{ 'aria-label': 'primary checkbox' }} 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) setTosChecked(!tosChecked)
}}> }}>
Accept Accept
</Typography> </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. 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> </Typography>
<div style={{display: "flex", marginTop: 25, }}> <div style={{ display: "flex", marginTop: 25, }}>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
@@ -415,7 +545,7 @@ const Billing = (props) => {
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<div style={{display: "flex"}}> <div style={{ display: "flex" }}>
{top_text === "Base Cloud Access" && userdata.has_card_available === true ? {top_text === "Base Cloud Access" && userdata.has_card_available === true ?
<Chip <Chip
style={{ style={{
@@ -478,7 +608,7 @@ const Billing = (props) => {
</Typography> </Typography>
{subscription.currency_text !== undefined ? {subscription.currency_text !== undefined ?
<div style={{display: "flex", }}> <div style={{ display: "flex", }}>
<Typography variant="h6" style={{ marginTop: 10, }}> <Typography variant="h6" style={{ marginTop: 10, }}>
{subscription.currency_text}{subscription.price} {subscription.currency_text}{subscription.price}
</Typography> </Typography>
@@ -500,16 +630,16 @@ const Billing = (props) => {
<a <a
href={feature.split("Documentation: ")[1]} href={feature.split("Documentation: ")[1]}
target="_blank" target="_blank"
style={{ textDecoration: "none", color: "#f85a3e",}} style={{ textDecoration: "none", color: "#f85a3e", }}
> >
Documentation to get started Documentation to get started
</a> </a>
} }
if (feature.includes("Worker License: ")) { if (feature.includes("Worker License: ")) {
const fieldId = "webhook_uri_field_"+index const fieldId = "webhook_uri_field_" + index
parsedFeature = parsedFeature =
<span style={{marginTop: 10, }}> <span style={{ marginTop: 10, }}>
<Typography <Typography
variant="body2" variant="body2"
> >
@@ -522,7 +652,7 @@ const Billing = (props) => {
borderRadius: theme.palette.borderRadius, borderRadius: theme.palette.borderRadius,
}} }}
id={fieldId} id={fieldId}
onClick={() => {}} onClick={() => { }}
InputProps={{ InputProps={{
endAdornment: endAdornment:
<InputAdornment position="end"> <InputAdornment position="end">
@@ -565,7 +695,7 @@ const Billing = (props) => {
return ( return (
<li key={index}> <li key={index}>
<Typography variant="body2" color="textPrimary" style={{ }}> <Typography variant="body2" color="textPrimary" style={{}}>
{parsedFeature} {parsedFeature}
</Typography> </Typography>
</li> </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.` `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")
@@ -671,7 +873,7 @@ const Billing = (props) => {
}}> }}>
Get Support Get Support
</Button> </Button>
: null } : null}
</Paper> </Paper>
) )
} }
@@ -967,23 +1169,147 @@ 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 (
<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} {addDealModal}
{clickedFromOrgTab? {clickedFromOrgTab ?
<h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Billing & Licensing</h2>: <h2 style={{ marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Billing & Licensing</h2> :
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}> <Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
Billing & Licensing Billing & Licensing
</Typography>} </Typography>}
{clickedFromOrgTab? {clickedFromOrgTab ?
<span style={{ color: "#9E9E9E" }}>{isCloud ? <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." "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." "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>: }</span> :
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10}}> <Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10 }}>
{isCloud ? {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." "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>} </Typography>}
{userdata.support === true ? {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&nbsp; For sales: Create&nbsp;
<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" }}> <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 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" }}> <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
@@ -1018,12 +1342,12 @@ const Billing = (props) => {
{isChildOrg ? {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. Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this.
</Typography> </Typography>
: null} : 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 : {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null :
<SubscriptionObject <SubscriptionObject
index={0} index={0}
@@ -1037,7 +1361,7 @@ const Billing = (props) => {
highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0} highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0}
/> />
: !isCloud ? : !isCloud ?
<span style={{display: "flex", }}> <span style={{ display: "flex", }}>
<SubscriptionObject <SubscriptionObject
index={0} index={0}
globalUrl={globalUrl} globalUrl={globalUrl}
@@ -1089,7 +1413,7 @@ const Billing = (props) => {
.map((sub, index) => { .map((sub, index) => {
return ( return (
<SubscriptionObject <SubscriptionObject
index={index+1} index={index + 1}
globalUrl={globalUrl} globalUrl={globalUrl}
userdata={userdata} userdata={userdata}
serverside={serverside} serverside={serverside}
@@ -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
+13 -1
View File
@@ -217,6 +217,17 @@ const ParsedAction = (props) => {
selectedAction, selectedApp,setNewSelectedAction, workflow, selectedAction, selectedApp,setNewSelectedAction, workflow,
]) ])
useEffect(() => {
setParamValues(selectedAction.parameters.map((param) => {
return {
name: param.name,
value: param.value,
}
}))
},[
selectedAction, selectedApp,setNewSelectedAction, workflow,
])
useEffect(() => { useEffect(() => {
if (selectedAction.parameters === null || selectedAction.parameters === undefined) { if (selectedAction.parameters === null || selectedAction.parameters === undefined) {
return return
@@ -972,10 +983,12 @@ const ParsedAction = (props) => {
} }
} }
setTimeout(() => {
selectedActionParameters[count].autocompleted = false selectedActionParameters[count].autocompleted = false
selectedAction.parameters[count].autocompleted = false selectedAction.parameters[count].autocompleted = false
selectedActionParameters[count].value = data selectedActionParameters[count].value = data
selectedAction.parameters[count].value = data selectedAction.parameters[count].value = data
}, 100);
setSelectedAction(selectedAction) setSelectedAction(selectedAction)
//setUpdate(Math.random()) //setUpdate(Math.random())
//setUpdate(event.target.value) //setUpdate(event.target.value)
@@ -1143,7 +1156,6 @@ const ParsedAction = (props) => {
// FIXME: Issue #40 - selectedActionParameters not reset // FIXME: Issue #40 - selectedActionParameters not reset
if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) {
var wrapperapp = { var wrapperapp = {
"id": "", "id": "",
"name": "noapp", "name": "noapp",
+16 -90
View File
@@ -3804,15 +3804,9 @@ const releaseToConnectLabel = "Release to Connect"
if (connected.length > 0 && connected !== undefined) { if (connected.length > 0 && connected !== undefined) {
for (let connectkey in connected) { for (let connectkey in connected) {
const edge = connected[connectkey] const edge = connected[connectkey]
if (edge.data.decorator && edge.data.label === releaseToConnectLabel) { //console.log("EDGE:", edge)
// Transform to normal edge
const currentedge = cy.getElementById(edge.data.id) //const edge = edgeBase.json()
if (currentedge !== undefined && currentedge !== null) {
currentedge.data("decorator", false)
currentedge.data("label", "")
}
continue
}
const sourcenode = cy.getElementById(edge.data.source) const sourcenode = cy.getElementById(edge.data.source)
const destinationnode = cy.getElementById(edge.data.target) const destinationnode = cy.getElementById(edge.data.target)
@@ -4027,87 +4021,19 @@ const releaseToConnectLabel = "Release to Connect"
} }
if (nodedata.id === selectedAction.id) { if (nodedata.id === selectedAction.id) {
return return;
} }
if ((nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT" || nodedata.type === "ACTION") && !nodedata.isStartNode) {
// Check if it already has any non-decorator branches attached to it
const branches = cy.elements('edge').jsons()
var branchFound = false
var decoratorIds = []
for (var branchkey in branches) {
if (branches[branchkey].data.source === nodedata.id || branches[branchkey].data.target === nodedata.id) {
if (branches[branchkey].data.decorator === true) { /*
// Tried looking for the closest node by position. aStar path not working entirely.
// Add the source/destination console.log("NODE: ", event.target)
if (branches[branchkey].data.source === nodedata.id) { const closestNode = cy.elements().aStar({
decoratorIds.push(branches[branchkey].data.target) root: nodedata.id,
} else { goal: 'node',
decoratorIds.push(branches[branchkey].data.source) directed: false,
} })
continue
}
branchFound = true
break
}
}
if (!branchFound) {
//console.log("Found action during drag. Checking closest nodes as it doesn't have a valid branch")
var closestNode = null
var minDistance = 300
const draggedNode = event.target
const allnodes = cy.nodes().jsons()
for (var nodekey in allnodes) {
const node = allnodes[nodekey]
if (node.data.id === nodedata.id) {
continue
}
// Decorators
if (node.data.attachedTo !== undefined) {
continue
}
if (node.position === undefined || node.position === null || node.position.x === undefined || node.position.y === undefined) {
continue
}
if (node.data.type !== "ACTION" && node.data.type !== "TRIGGER") {
continue
}
const distance = Math.sqrt(
Math.pow(draggedNode.position('x') - node.position.x, 2) +
Math.pow(draggedNode.position('y') - node.position.y, 2)
)
if (decoratorIds.includes(node.data.id)) {
//console.log("Found existing decorator for: ", node.data.app_name, "Distance: ", distance)
if (distance > 300) {
// Remove the branch
const edgeToRemove = cy.getElementById(branches[branchkey].data.id)
if (edgeToRemove !== null && edgeToRemove !== undefined) {
//console.log("Removing edge: ", edgeToRemove)
edgeToRemove.remove()
//decoratorIds.splice(decoratorIds.indexOf(node.data.id), 1)
break
}
}
}
if (distance < minDistance) {
minDistance = distance
closestNode = node
}
}
if (closestNode !== null && closestNode !== undefined) { if (closestNode !== null && closestNode !== undefined) {
//console.log("Closest node app: ", closestNode.data.app_name, "Distance: ", minDistance) //console.log("Closest node app: ", closestNode.data.app_name, "Distance: ", minDistance)
@@ -4220,11 +4146,11 @@ const releaseToConnectLabel = "Release to Connect"
} }
// Ensure it only happens once // Ensure it only happens once
document.removeEventListener("mousemove", onMouseUpdate, false) document.removeEventListener("mousemove", onMouseUpdate, false);
} };
document.addEventListener("mousemove", onMouseUpdate, false) document.addEventListener("mousemove", onMouseUpdate, false);
} };
useBeforeunload(() => { useBeforeunload(() => {
+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">