diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index e52bdce7..e567a5c7 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -7,7 +7,7 @@ import countries from "../components/Countries.jsx"; import { Box, Paper, - Typography, + Typography, Divider, Button, Grid, @@ -19,94 +19,150 @@ import { DialogTitle, DialogContent, TextField, - InputAdornment, + InputAdornment, IconButton, - Chip, + Chip, Checkbox, - Tooltip, + 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" +import { toast } from "react-toastify" import { - Cached as CachedIcon, - ContentCopy as ContentCopyIcon, - Draw as DrawIcon, - Close as CloseIcon, + Cached as CachedIcon, + 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; - //const alert = useAlert(); - let navigate = useNavigate(); + const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props; + //const alert = useAlert(); + let navigate = useNavigate(); - const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); - const [dealList, setDealList] = React.useState([]); - const [dealName, setDealName] = React.useState(""); - const [dealAddress, setDealAddress] = React.useState(""); - const [dealType, setDealType] = React.useState("MSSP"); - const [dealCountry, setDealCountry] = React.useState("United States"); - const [dealCurrency, setDealCurrency] = React.useState("USD"); - const [dealStatus, setDealStatus] = React.useState("initiated"); - const [dealValue, setDealValue] = React.useState(""); - const [dealDiscount, setDealDiscount] = React.useState(""); - const [dealerror, setDealerror] = React.useState(""); + const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); + const [dealList, setDealList] = React.useState([]); + const [dealName, setDealName] = React.useState(""); + const [dealAddress, setDealAddress] = React.useState(""); + const [dealType, setDealType] = React.useState("MSSP"); + const [dealCountry, setDealCountry] = React.useState("United States"); + const [dealCurrency, setDealCurrency] = React.useState("USD"); + const [dealStatus, setDealStatus] = React.useState("initiated"); + const [dealValue, setDealValue] = React.useState(""); + const [dealDiscount, setDealDiscount] = React.useState(""); + const [dealerror, setDealerror] = React.useState(""); + const [openChangeEmailBox, setOpenChangeEmailBox] = useState(false); + const [isMouseOverOnChangeEmail, setIsMouseOverOnChangeEmail] = useState(false); + const [currentAppRunsInPercentage, setCurrentAppRunsInPercentage] = useState(0); + const [currentAppRunsInNumber, setCurrentAppRunsInNumber] = useState(0); + const [alertThresholds, setAlertThresholds] = useState(selectedOrganization.Billing !== undefined && selectedOrganization.Billing.AlertThreshold !== undefined && selectedOrganization.Billing.AlertThreshold !== null ? selectedOrganization.Billing.AlertThreshold : [{ percentage: '', count: '', Email_send: false }]); + const [currentIndex, setCurrentIndex] = useState(0); + + 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 = [ - { code: "", label: "MSSP", phone: "" }, - { code: "", label: "Enterprise", phone: "" }, - { code: "", label: "Consultancy", phone: "" }, - { code: "", label: "Support", phone: "" }, - ]; + { code: "", label: "MSSP", phone: "" }, + { code: "", label: "Enterprise", phone: "" }, + { code: "", label: "Consultancy", phone: "" }, + { code: "", label: "Support", phone: "" }, + ]; const handleGetDeals = (orgId) => { - console.log("Get deals!"); + console.log("Get deals!"); - if (orgId.length === 0) { - toast( - "Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout." - ); - return; - } + if (orgId.length === 0) { + toast( + "Organization ID not defined (get deals). Please contact us on https://shuffler.io if this persists logout." + ); + return; + } - const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; - fetch(url, { - method: "GET", - credentials: "include", - headers: { - "Content-Type": "application/json", - }, - }) - .then((response) => { - if (response.status !== 200) { - console.log("Bad status code in get deals: ", response.status); - } + const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; + fetch(url, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Bad status code in get deals: ", response.status); + } - return response.json(); - }) - .then((responseJson) => { - console.log("Got deals: ", responseJson); - if (responseJson.success === false) { - toast("Failed loading deals. Contact support if this persists"); - } else { - setDealList(responseJson); - } - }) - .catch((error) => { - console.log("Error getting org deals: ", error); - toast( - "Failed getting deals for your org. Contact support if this persists." - ); - }); - }; + return response.json(); + }) + .then((responseJson) => { + console.log("Got deals: ", responseJson); + if (responseJson.success === false) { + toast("Failed loading deals. Contact support if this persists"); + } else { + setDealList(responseJson); + } + }) + .catch((error) => { + console.log("Error getting org deals: ", error); + toast( + "Failed getting deals for your org. Contact support if this persists." + ); + }); + }; useEffect(() => { if (isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true) { @@ -121,15 +177,15 @@ 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, + marginRight: 10, marginTop: 15, } - const isCloud = - window.location.host === "localhost:3002" || - window.location.host === "shuffler.io"; + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; billingInfo.subscription = { "active": true, @@ -161,82 +217,83 @@ const Billing = (props) => { var checkoutObject = { lineItems: [ { - price: priceItem, + price: priceItem, quantity: 1 }, ], mode: "subscription", billingAddressCollection: "auto", - successUrl: successUrl, - cancelUrl: failUrl, + successUrl: successUrl, + cancelUrl: failUrl, clientReferenceId: props.userdata.active_org.id, } //submitType: "donate", stripe.redirectToCheckout(checkoutObject) - .then(function (result) { - console.log("SUCCESS STRIPE?: ", result) + .then(function (result) { + console.log("SUCCESS STRIPE?: ", result) - ReactGA.event({ - category: "pricing", - action: "add_card_success", - label: "", + ReactGA.event({ + category: "pricing", + action: "add_card_success", + label: "", + }) }) - }) - .catch(function(error) { - console.error("STRIPE ERROR: ", error) + .catch(function (error) { + console.error("STRIPE ERROR: ", error) - ReactGA.event({ - category: "pricing", - action: "add_card_error", - label: "", - }) - }); + ReactGA.event({ + category: "pricing", + action: "add_card_error", + label: "", + }) + }); } const cancelSubscriptions = (subscription_id) => { - const orgId = selectedOrganization.id; - const data = { - subscription_id: subscription_id, - action: "cancel", - org_id: selectedOrganization.id, - }; + const orgId = selectedOrganization.id; + const data = { + subscription_id: subscription_id, + action: "cancel", + org_id: selectedOrganization.id, + }; - const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } - - if (handleGetOrg != undefined) { - handleGetOrg(selectedOrganization.id); + const url = globalUrl + `/api/v1/orgs/${orgId}/cancel`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); } - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success !== undefined && responseJson.success) { - toast("Successfully stopped subscription!"); - } else { - toast("Failed stopping subscription. Please contact us."); - } - }) - .catch(function (error) { - console.log("Error: ", error); - toast("Failed stopping subscription. Please contact us."); - }); - }; + if (handleGetOrg != undefined) { + handleGetOrg(selectedOrganization.id); + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success !== undefined && responseJson.success) { + toast("Successfully stopped subscription!"); + } else { + toast("Failed stopping subscription. Please contact us."); + } + }) + .catch(function (error) { + console.log("Error: ", error); + toast("Failed stopping subscription. Please contact us."); + }); + }; + const sendSignatureRequest = (subscription) => { const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; @@ -246,46 +303,47 @@ const Billing = (props) => { org_id: selectedOrganization.id, subscription: subscription, }), - mode: "cors", - method: "POST", - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => { - if (response.status !== 200) { - console.log("Error in response"); - } - return response.json(); - }) - .then((responseJson) => { - console.log("Response from signature request: ", responseJson); - }) - .catch((error) => { - console.log("Error: ", error); + mode: "cors", + method: "POST", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, }) + .then((response) => { + if (response.status !== 200) { + console.log("Error in response"); + } + return response.json(); + }) + .then((responseJson) => { + console.log("Response from signature request: ", responseJson); + }) + .catch((error) => { + console.log("Error: ", error); + }) } const SubscriptionObject = (props) => { - const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; + const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; - const [signatureOpen, setSignatureOpen] = React.useState(false); - const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) + const [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. ", - "Multi-Tenancy and Region-Selection", + "Multi-Tenancy and Region-Selection", "And all other features from /pricing", ] } @@ -301,17 +359,17 @@ const Billing = (props) => { if (subscription.name.includes("default")) { top_text = "Custom Contract" newPaperstyle.border = "1px solid #f85a3e" - showSupport = true + showSupport = true } if (subscription.name.includes("App Run Units")) { top_text = "Cloud Access" - showSupport = true + showSupport = true } if (subscription.name.includes("Open Source")) { top_text = "Open Source" - showSupport = true + showSupport = true } if (subscription.name.includes("Scale")) { @@ -327,8 +385,80 @@ 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 ( - setHovered(true)} onMouseLeave={() => setHovered(false)} @@ -336,34 +466,34 @@ const Billing = (props) => { - { - e.preventDefault(); - setSignatureOpen(false); - setTosChecked(false) - }} - > - - + { + e.preventDefault(); + setSignatureOpen(false); + setTosChecked(false) + }} + > + + Read and Accept the EULA @@ -371,13 +501,13 @@ const Billing = (props) => { rows={17} multiline fullWidth - InputProps={{ - readOnly: true, - style: { - fontSize: 14, - color: "rgba(255, 255, 255, 0.6)", - } - }} + InputProps={{ + readOnly: true, + style: { + fontSize: 14, + color: "rgba(255, 255, 255, 0.6)", + } + }} value={subscription.eula} /> { }} inputProps={{ 'aria-label': 'primary checkbox' }} /> - { + { setTosChecked(!tosChecked) }}> Accept - + By clicking the “accept” button, you are signing the document, electronically agreeing that it has the same legal validity and effects as a handwritten signature, and that you have the competent authority to represent and sign on behalf an entity. Need support or have questions? Contact us at support@shuffler.io. -
+
-
+
{top_text === "Base Cloud Access" && userdata.has_card_available === true ? { @@ -436,80 +566,80 @@ const Billing = (props) => { }} variant="outlined" color="primary" - /> + /> : null} {top_text} {top_text === "Base Cloud Access" && userdata.has_card_available === false ? - - : null} + : null} {isCloud && highlight === true && top_text !== "Base Cloud Access" ? - { setSignatureOpen(true) }} > - + - : null} + : null}
- -
- - {subscription.name} - + +
+ + {subscription.name} + - {subscription.currency_text !== undefined ? -
- - {subscription.currency_text}{subscription.price} - - - / {subscription.interval} - -
+ {subscription.currency_text !== undefined ? +
+ + {subscription.currency_text}{subscription.price} + + + / {subscription.interval} + +
: null} - - Features - -
    + + Features + +
      {subscription.features !== undefined && subscription.features !== null ? subscription.features.map((feature, index) => { var parsedFeature = feature if (feature.includes("Documentation: ")) { - parsedFeature = - Documentation to get started } if (feature.includes("Worker License: ")) { - const fieldId = "webhook_uri_field_"+index + const fieldId = "webhook_uri_field_" + index parsedFeature = - + @@ -517,47 +647,47 @@ const Billing = (props) => { {}} - InputProps={{ - endAdornment: - - { - var copyText = document.getElementById(fieldId); - if (copyText !== undefined && copyText !== null) { - console.log("NAVIGATOR: ", navigator); - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } + style={{ + backgroundColor: theme.palette.inputColor, + borderRadius: theme.palette.borderRadius, + }} + id={fieldId} + onClick={() => { }} + InputProps={{ + endAdornment: + + { + var copyText = document.getElementById(fieldId); + if (copyText !== undefined && copyText !== null) { + console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } - navigator.clipboard.writeText(copyText.value); - copyText.select(); - copyText.setSelectionRange( - 0, - 99999 - ); /* For mobile devices */ + navigator.clipboard.writeText(copyText.value); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ - /* Copy the text inside the text field */ - document.execCommand("copy"); - toast("Copied Webhook URL"); - } else { - console.log("Couldn't find webhook URI field: ", copyText); - } - }} - edge="end" - > - - - - }} + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied Webhook URL"); + } else { + console.log("Couldn't find webhook URI field: ", copyText); + } + }} + edge="end" + > + + + + }} fullWidth /> @@ -565,30 +695,102 @@ const Billing = (props) => { return (
    • - + {parsedFeature}
    • ) }) : null} -
    -
- {isCloud && (highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ? - - - {subscription.name.includes("Scale") ? - "" - : + +
+ {isCloud && (highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ? + + + {subscription.name.includes("Scale") ? + "" + : - userdata.has_card_available === true ? - "While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month." - : - `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.` - } + userdata.has_card_available === true ? + "While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month." + : + `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.` + } + +
+ + Billing email: {BillingEmail} - Billing email: {selectedOrganization.org} - {/*isCloud ? + {userdata.has_card_available === true && ( + + )} + + Change Billing Email + + + Enter the new billing email address. + + { if (event.key === 'Enter') HandleChangeBillingEmail(selectedOrganization.id) }} + onChange={(e) => setNewBillingEmail(e.target.value)} + /> + + + + + + +
+ {/*isCloud ? : null*/} - + {userdata.has_card_available === true ? + - {userdata.has_card_available === true ? - : null} - -
+ + : null} - {showSupport ? + {showSupport ? - : null } - + : null} + ) } const addDealModal = ( - { - setSelectedDealModalOpen(false); - }} - PaperProps={{ - style: { - backgroundColor: theme.palette.surfaceColor, - color: "white", - minWidth: "800px", - minHeight: "320px", - }, - }} - > - - Register new deal - - -
- { - setDealName(e.target.value); - }} - /> - { - setDealAddress(e.target.value); - }} - /> -
-
- { - setDealValue(e.target.value); - }} - /> - option.label} - onChange={(event, newValue) => { - setDealCountry(newValue.label); - }} - renderOption={(props, option) => ( - img": { mr: 2, flexShrink: 0 } }} - {...props} - > - - {option.label} ({option.code}) +{option.phone} - - )} - renderInput={(params) => ( - - )} - /> - { - setDealType(newValue); - }} - getOptionLabel={(option) => option.label} - renderOption={(props, option) => ( - img": { mr: 2, flexShrink: 0 } }} - {...props} - > - {option.label} - - )} - renderInput={(params) => ( - - )} - /> -
- {dealerror.length > 0 ? ( - - error registering: {dealerror} - - ) : null} -
- - -
-
-
- ); + //setDealName("") + //setDealAddress("") + //setDealCountry("") + //setDealValue("") + }} + > + Cancel + + +
+ + + ); - const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => { - if (dealerror.length > 0) { - setDealerror(""); - } + const submitDeal = (dealName, dealAddress, dealCountry, dealValue) => { + if (dealerror.length > 0) { + setDealerror(""); + } - const orgId = selectedOrganization.id; - const data = { - reseller_org: orgId, - name: dealName, - address: dealAddress, - country: dealCountry, - value: dealValue, - }; + const orgId = selectedOrganization.id; + const data = { + reseller_org: orgId, + name: dealName, + address: dealAddress, + country: dealCountry, + value: dealValue, + }; - const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then(function (response) { - if (response.status !== 200) { - console.log("Error in response"); - } + const url = `${globalUrl}/api/v1/orgs/${orgId}/deals`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } - return response.json(); - }) - .then(function (responseJson) { - if (responseJson.success === true) { - setSelectedDealModalOpen(false); - toast( - "Added new deal! We will be in touch shortly with an update." - ); + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + setSelectedDealModalOpen(false); + toast( + "Added new deal! We will be in touch shortly with an update." + ); + + setDealName(""); + setDealAddress(""); + setDealValue(""); + setDealCountry("United States"); + setDealType("MSSP"); + } else { + setDealerror(responseJson.reason); + } + }) + .catch(function (error) { + //console.log("Error: ", error); + setDealerror(error.toString()); + toast("Failed adding deal reg: ", error); + }); + }; + const addAlertThreshold = () => { + setAlertThresholds([...alertThresholds, { percentage: '', count: '', Email_send: false }]); + }; + + const updateAlertThreshold = (index, field, value) => { + + if (field === 'percentage') { + if (value > 100 || value < 0) { + value = 0 + toast("The percentage value should be between 0 and 100") + } + } else if (field === 'count') { + if (value < 0 || value >= userdata.app_execution_limit) { + value = 0 + toast("The count value should be greater than 0 and less than the total app execution limit") + } + } + + + const totalValue = userdata.app_execution_limit; + const newAlertThresholds = alertThresholds.map((threshold, i) => { + if (i === index) { + const newValue = parseFloat(value); + if (field === 'percentage') { + const newCount = (newValue / 100) * totalValue; + return { + ...threshold, + percentage: isNaN(newValue) ? '' : Math.round(newValue), + count: isNaN(newCount) ? '' : Math.round(newCount), + Email_send: false + }; + } else if (field === 'count') { + const newPercentage = (newValue / totalValue) * 100; + return { + ...threshold, + count: newValue, + percentage: isNaN(newPercentage) ? '' : Math.round(newPercentage), + Email_send: false + }; + } + } + return threshold; + }); + setAlertThresholds(newAlertThresholds); + }; + + + const handleDeleteAlertThreshold = (index) => { + const newAlertThresholds = alertThresholds.filter((_, i) => i !== index); + setAlertThresholds(newAlertThresholds); + + // Update currentIndex based on remaining elements + const findCurrentIndex = newAlertThresholds.some(threshold => threshold.Email_send === false); + setCurrentIndex(findCurrentIndex ? newAlertThresholds.findIndex(threshold => threshold.Email_send === false) : - 1); + }; + + 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; + } + }; - setDealName(""); - setDealAddress(""); - setDealValue(""); - setDealCountry("United States"); - setDealType("MSSP"); - } else { - setDealerror(responseJson.reason); - } - }) - .catch(function (error) { - //console.log("Error: ", error); - setDealerror(error.toString()); - toast("Failed adding deal reg: ", error); - }); - }; const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null return ( -
- {addDealModal} - {clickedFromOrgTab? -

Billing & Licensing

: - - Billing & Licensing - } - {clickedFromOrgTab? - {isCloud ? - "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." - : - "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." - }: - - {isCloud ? +
+ {addDealModal} + {clickedFromOrgTab ? +

Billing & Licensing

: + + Billing & Licensing + } + {clickedFromOrgTab ? + {isCloud ? "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." : "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." - } - } + } : + + {isCloud ? + "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." + : + "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." + } + } - {userdata.support === true ? -
+ {userdata.support === true ? +
For sales: Create  New Cloud Contract @@ -1001,7 +1327,7 @@ const Billing = (props) => { New Onprem Contract -   -   +   -   Google Drive Link @@ -1009,22 +1335,20 @@ const Billing = (props) => { Sales Process - -
: null } - {isChildOrg ? - - Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this. - + {isChildOrg ? + + Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this. + : null} -
- {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : +
+ {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : { subscription={billingInfo.subscription} highlight={selectedOrganization.subscriptions === undefined || selectedOrganization.subscriptions === null || selectedOrganization.subscriptions.length === 0} /> - : !isCloud ? - - - - - : null} + : !isCloud ? + + + + + : null} {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 && - !isChildOrg ? - selectedOrganization.subscriptions - .reverse() - .map((sub, index) => { - return ( - - ) - }) - : null} - {/* + !isChildOrg ? + selectedOrganization.subscriptions + .reverse() + .map((sub, index) => { + return ( + + ) + }) + : null} + {/* { */} -
+
- {/*isCloud && + {/*isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true ? (
@@ -1333,21 +1657,156 @@ const Billing = (props) => {
) : null*/} -
+
- Utilization & Stats + Manage Billing -
- + Manage your billing and licensing information below. When you reach the certain thresholds of your subscription limit, you will be notified by email. + + Current Usage: + + + You have used {currentAppRunsInPercentage}% of total app execution limit or {userdata.app_execution_usage} app runs out of {userdata.app_execution_limit} app runs. + + +
+ + Set email alert thresholds for app runs + + + You will be notified by email when you reach the + {currentIndex !== -1 + ? " " + getSafeValue(alertThresholds[currentIndex].percentage) + '%' + " " + : " " + '0%' + " " + } + of your total app execution limit or + {currentIndex !== -1 + ? " " + getSafeValue(alertThresholds[currentIndex].count) + " " + : " " + 0 + " "} + app runs. + + +
+ {alertThresholds.map((threshold, index) => ( +
+ updateAlertThreshold(index, 'percentage', e.target.value)} + margin="normal" + variant="outlined" + inputProps={{ + max: 100, + }} + /> + updateAlertThreshold(index, 'count', e.target.value)} + margin="normal" + variant="outlined" + /> + { + alertThresholds.length > 1 && + ( + + ) + } +
+ ))} +
+
+ + + +
+
+ + Utilization & Stats + +
+ + />
) } diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index c0d18d6b..dae46392 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -217,6 +217,17 @@ const ParsedAction = (props) => { selectedAction, selectedApp,setNewSelectedAction, workflow, ]) + useEffect(() => { + setParamValues(selectedAction.parameters.map((param) => { + return { + name: param.name, + value: param.value, + } + })) + },[ + selectedAction, selectedApp,setNewSelectedAction, workflow, + ]) + useEffect(() => { if (selectedAction.parameters === null || selectedAction.parameters === undefined) { return @@ -972,10 +983,12 @@ const ParsedAction = (props) => { } } + setTimeout(() => { selectedActionParameters[count].autocompleted = false selectedAction.parameters[count].autocompleted = false selectedActionParameters[count].value = data selectedAction.parameters[count].value = data + }, 100); setSelectedAction(selectedAction) //setUpdate(Math.random()) //setUpdate(event.target.value) @@ -1143,7 +1156,6 @@ const ParsedAction = (props) => { // FIXME: Issue #40 - selectedActionParameters not reset if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { - var wrapperapp = { "id": "", "name": "noapp", diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 807cf2f9..da1d020d 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -3804,15 +3804,9 @@ const releaseToConnectLabel = "Release to Connect" if (connected.length > 0 && connected !== undefined) { for (let connectkey in connected) { const edge = connected[connectkey] - if (edge.data.decorator && edge.data.label === releaseToConnectLabel) { - // Transform to normal edge - const currentedge = cy.getElementById(edge.data.id) - if (currentedge !== undefined && currentedge !== null) { - currentedge.data("decorator", false) - currentedge.data("label", "") - } - continue - } + //console.log("EDGE:", edge) + + //const edge = edgeBase.json() const sourcenode = cy.getElementById(edge.data.source) const destinationnode = cy.getElementById(edge.data.target) @@ -4027,87 +4021,19 @@ const releaseToConnectLabel = "Release to Connect" } 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) { - - // Add the source/destination - if (branches[branchkey].data.source === nodedata.id) { - decoratorIds.push(branches[branchkey].data.target) - } else { - decoratorIds.push(branches[branchkey].data.source) - } - - 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 - } - } + /* + // Tried looking for the closest node by position. aStar path not working entirely. + console.log("NODE: ", event.target) + const closestNode = cy.elements().aStar({ + root: nodedata.id, + goal: 'node', + directed: false, + }) if (closestNode !== null && closestNode !== undefined) { //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 - document.removeEventListener("mousemove", onMouseUpdate, false) - } + document.removeEventListener("mousemove", onMouseUpdate, false); + }; - document.addEventListener("mousemove", onMouseUpdate, false) - } + document.addEventListener("mousemove", onMouseUpdate, false); + }; useBeforeunload(() => { diff --git a/frontend/src/views/Workflows.jsx b/frontend/src/views/Workflows.jsx index 299e0c2d..ffdf0ec4 100755 --- a/frontend/src/views/Workflows.jsx +++ b/frontend/src/views/Workflows.jsx @@ -2153,7 +2153,7 @@ const Workflows = (props) => { } return ( -
+
{selectedCategory !== "" ?