Lightmode/Darkmode merge including many fixes since 2.0.2
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, useContext, memo, useMemo } from "react";
|
||||
import React, { useState, useEffect, memo, useMemo, useContext } from "react";
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
import theme from "../theme.jsx";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import countries from "../components/Countries.jsx";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
@@ -26,7 +26,10 @@ import {
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
LinearProgress,
|
||||
Slider
|
||||
Slider,
|
||||
Tabs,
|
||||
Tab,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useNavigate, Link, json } from "react-router-dom";
|
||||
@@ -43,6 +46,8 @@ import {
|
||||
Cloud,
|
||||
CheckCircle,
|
||||
Padding,
|
||||
Edit,
|
||||
Search as SearchIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
//import { useAlert
|
||||
@@ -52,11 +57,14 @@ import { handlePayasyougo } from "../views/HandlePaymentNew.jsx"
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import LicencePopup from "./LicencePopup.jsx";
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
|
||||
const Billing = memo((props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
||||
//const alert = useAlert();
|
||||
let navigate = useNavigate();
|
||||
const { themeMode, brandColor,supportEmail } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false);
|
||||
const [dealList, setDealList] = React.useState([]);
|
||||
@@ -78,6 +86,9 @@ const Billing = memo((props) => {
|
||||
const [deleteAlertIndex, setDeleteAlertIndex] = useState(-1);
|
||||
const [deleteAlertVerification, setDeleteAlertVerification] = useState(false);
|
||||
const [isScale, setIsScale] = useState(false);
|
||||
const [currentTab, setCurrentTab] = useState(0)
|
||||
const [allChildOrgs, setAllChildOrgs] = useState([])
|
||||
const [allChildOrgsStats, setAllChildOrgsStats] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (userdata.app_execution_limit !== undefined && userdata.app_execution_usage !== undefined) {
|
||||
@@ -85,6 +96,7 @@ const Billing = memo((props) => {
|
||||
setCurrentAppRunsInPercentage(Math.round(percentage));
|
||||
setCurrentAppRunsInNumber(userdata.app_execution_limit - userdata.app_execution_usage);
|
||||
}
|
||||
|
||||
if (userdata?.id?.length > 0 && isLoggedIn === false){
|
||||
setIsLoggedIn(true)
|
||||
}
|
||||
@@ -92,6 +104,17 @@ const Billing = memo((props) => {
|
||||
|
||||
const [BillingEmail, setBillingEmail] = useState(selectedOrganization?.Billing?.Email);
|
||||
|
||||
useEffect(() => {
|
||||
const urlIncludesProfessionalServices = window.location.href.includes("professional-services");
|
||||
if (props?.isCloud && urlIncludesProfessionalServices) {
|
||||
const professionalServicesSection =
|
||||
document.getElementById("professional-services");
|
||||
if (professionalServicesSection) {
|
||||
professionalServicesSection.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (BillingEmail !== selectedOrganization?.Billing?.Email) {
|
||||
setBillingEmail(selectedOrganization?.Billing?.Email);
|
||||
@@ -337,6 +360,7 @@ const Billing = memo((props) => {
|
||||
const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed)
|
||||
const [hovered, setHovered] = React.useState(false)
|
||||
const [newBillingEmail, setNewBillingEmail] = useState('');
|
||||
const {supportEmail} = useContext(Context);
|
||||
|
||||
var top_text = "Base Cloud Access"
|
||||
if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) {
|
||||
@@ -537,7 +561,7 @@ const Billing = memo((props) => {
|
||||
Accept
|
||||
</Typography>
|
||||
<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 {supportEmail}
|
||||
</Typography>
|
||||
|
||||
<div style={{ display: "flex", marginTop: 25, }}>
|
||||
@@ -1066,12 +1090,12 @@ const Billing = memo((props) => {
|
||||
padding: 20,
|
||||
// maxWidth: 400,
|
||||
width: "100%",
|
||||
height: 480,
|
||||
backgroundColor: hovered ? "#2b2b2b" : "#1e1e1e",
|
||||
height: 500,
|
||||
backgroundColor: hovered ? theme.palette.cardHoverColor : theme.palette.cardBackgroundColor,
|
||||
borderRadius: theme.palette?.borderRadius * 2,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
marginRight: 10,
|
||||
marginTop: 15,
|
||||
marginTop: 15,
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}>
|
||||
@@ -1138,12 +1162,12 @@ const Billing = memo((props) => {
|
||||
Features
|
||||
</Typography>
|
||||
<ul>
|
||||
<li>
|
||||
<li style={{color : theme.palette.text.primary}}>
|
||||
<Typography variant="body2" color="textPrimary" style={{}}>
|
||||
Build custom apps, integrations, and worklows for your specific use cases or applications
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<li style={{color : theme.palette.text.primary}}>
|
||||
<Typography variant="body2" color="textPrimary" style={{}}>
|
||||
Help solve / debug / update / add features and capabilities of the platform
|
||||
</Typography>
|
||||
@@ -1154,15 +1178,13 @@ const Billing = memo((props) => {
|
||||
<Button
|
||||
fullWidth
|
||||
disabled={false}
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{
|
||||
marginTop: userdata.support ? 0 : 10,
|
||||
marginTop: userdata.support ? 3 : 15,
|
||||
borderRadius: 4,
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
color: "#1A1A1A",
|
||||
backgroundColor: "#FF8544",
|
||||
textTransform: 'none',
|
||||
}}
|
||||
onClick={() => {
|
||||
@@ -1228,7 +1250,7 @@ const Billing = memo((props) => {
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{
|
||||
marginTop: userdata.support ? 5 : 10,
|
||||
marginTop: userdata.support ? 10 : 15,
|
||||
borderRadius: 4,
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
@@ -1354,7 +1376,7 @@ const Billing = memo((props) => {
|
||||
toast.success("Your request for private training has been submitted successfully. We will get back to you soon.")
|
||||
setOpenPrivateTraining(false)
|
||||
} else {
|
||||
toast.error("Failed sending request for private training. Please try again later or contact support@shuffler.io for help.")
|
||||
toast.error(`Failed sending request for private training. Please try again later or contact support@shuffler.io for help.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1363,10 +1385,10 @@ const Billing = memo((props) => {
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
height: 480,
|
||||
height: 500,
|
||||
// maxWidth: 400,
|
||||
width: "100%",
|
||||
backgroundColor: hovered ? "#2b2b2b" : "#1e1e1e",
|
||||
backgroundColor: hovered ? theme.palette.cardHoverColor : theme.palette.cardBackgroundColor,
|
||||
borderRadius: theme.palette?.borderRadius * 2,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
marginRight: 10,
|
||||
@@ -1387,12 +1409,12 @@ const Billing = memo((props) => {
|
||||
Public Training
|
||||
</Typography>
|
||||
<ul>
|
||||
<li>
|
||||
<li style={{color : theme.palette.text.primary}}>
|
||||
<Typography variant="body2" color="textPrimary">
|
||||
Public course on Automation for Security Professionals
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<li style={{color : theme.palette.text.primary}}>
|
||||
<Typography variant="body2" color="textPrimary">
|
||||
Covers Shuffle Platform, Apps, Workflows, Usecases, JSON, Liquid Formatting, and more.
|
||||
</Typography>
|
||||
@@ -1402,12 +1424,12 @@ const Billing = memo((props) => {
|
||||
Private Training
|
||||
</Typography>
|
||||
<ul>
|
||||
<li>
|
||||
<li style={{color : theme.palette.text.primary}}>
|
||||
<Typography variant="body2" color="textPrimary">
|
||||
Everything from Public Training
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<li style={{color : theme.palette.text.primary}}>
|
||||
<Typography variant="body2" color="textPrimary" >
|
||||
Customized for your team’s usecases, date and time, location, and more.
|
||||
</Typography>
|
||||
@@ -1425,9 +1447,7 @@ const Billing = memo((props) => {
|
||||
borderRadius: 4,
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
color: "#1A1A1A",
|
||||
textTransform: 'none',
|
||||
backgroundColor: "#FF8544",
|
||||
width: "100%",
|
||||
}}
|
||||
onClick={() => {
|
||||
@@ -1971,16 +1991,18 @@ const Billing = memo((props) => {
|
||||
<div style={{ width: "100%",}}>
|
||||
{addDealModal}
|
||||
{clickedFromOrgTab ?
|
||||
<Typography style={{fontSize: 24, fontWeight: "bold", marginBottom: 8, marginTop: 0, color: "#ffffff" }}>Billing & Licensing</Typography> :
|
||||
<Typography variant="h5" style={{fontSize: 24, fontWeight: 500, marginBottom: 8, marginTop: 0, }}>Billing & Licensing</Typography> :
|
||||
<Typography variant="h4" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
Billing & Licensing
|
||||
</Typography>}
|
||||
{clickedFromOrgTab ?
|
||||
<span style={{ color: "#9E9E9E", fontSize: 16 }}>{isCloud ?
|
||||
{userdata?.org_status?.includes("integration_partner") && userdata?.org_status?.includes("sub_org") ? null :
|
||||
<>
|
||||
{clickedFromOrgTab ?
|
||||
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16 }}>{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> :
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 0, marginBottom: 10, fontSize: 16 }}>
|
||||
{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."
|
||||
@@ -1988,9 +2010,10 @@ const Billing = memo((props) => {
|
||||
"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."
|
||||
}
|
||||
</Typography>}
|
||||
</> }
|
||||
|
||||
{userdata.support === true ?
|
||||
<div style={{ marginBottom: 10, marginTop: clickedFromOrgTab ? 16 : null, color: clickedFromOrgTab ? "#F1F1F1" : null }}>
|
||||
<Typography style={{ marginBottom: 10, marginTop: clickedFromOrgTab ? 16 : null, color: clickedFromOrgTab ? theme.palette.text.primary : null }}>
|
||||
For sales: Create
|
||||
<a href={"https://docs.google.com/document/d/1N-ZJNn8lWaqiXITrqYcnTt53oXGLNYFEzc5PU-tdAps/copy"} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#FF8444" }}>
|
||||
EU contract
|
||||
@@ -2007,7 +2030,7 @@ const Billing = memo((props) => {
|
||||
<a href={"https://github.com/Shuffle/Shuffle-docs/tree/master/handbook/Sales"} target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#FF8444" }}>
|
||||
Sales Process (old)
|
||||
</a>
|
||||
</div>
|
||||
</Typography>
|
||||
:
|
||||
null
|
||||
}
|
||||
@@ -2015,11 +2038,11 @@ const Billing = memo((props) => {
|
||||
|
||||
{isChildOrg ?
|
||||
<Typography variant="h6" style={{ marginBottom: 50, }}>
|
||||
Licensing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this.
|
||||
Licensing is handled by your parent organisation. Reach out to {supportEmail} if you have questions about this.
|
||||
</Typography>
|
||||
: null}
|
||||
|
||||
<div style={{ display: "flex", width: clickedFromOrgTab ? "100%" : "auto", overflowX: 'auto', overflowY: 'hidden', scrollbarWidth: 'thin', scrollbarColor: '#494949 #2f2f2f', height: isChildOrg ? 0 : "100%", marginTop: 20}} >
|
||||
<div style={{ display: "flex", width: clickedFromOrgTab ? "100%" : "auto", overflowX: 'auto', overflowY: 'hidden', scrollbarWidth: 'thin', scrollbarColor: theme.palette.scrollbarColor, height: isChildOrg ? 0 : "100%", marginTop: 20}} >
|
||||
<div style={{ display: "flex", flexDirection: "column", width: "100%", }}>
|
||||
{/* {isCloud &&
|
||||
selectedOrganization.subscriptions !== undefined &&
|
||||
@@ -2354,13 +2377,13 @@ const Billing = memo((props) => {
|
||||
</div>
|
||||
) : null*/}
|
||||
{!isChildOrg && isCloud && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }}>
|
||||
<Typography style={{ marginBottom: 5, fontSize: 24, fontWeight: "bold" }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services">
|
||||
<Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}>
|
||||
Professional Services
|
||||
</Typography>
|
||||
<Typography color="textSecondary" style={{fontSize: 16,}}>
|
||||
We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at support@shuffler.io.
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{fontSize: 16,}}>
|
||||
We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at {supportEmail}.
|
||||
</Typography>We offer priority support through consultations and training to help you make the most of our product. If you have any questions, please reach out to us at
|
||||
<div style={{ display: 'flex', flexDirection: 'row', marginTop: 5, }}>
|
||||
{billingInfo.subscription !== undefined && billingInfo.subscription !== null ? (
|
||||
isChildOrg ? null : (
|
||||
@@ -2426,14 +2449,16 @@ const Billing = memo((props) => {
|
||||
<TextField
|
||||
style={{
|
||||
marginTop: 10,
|
||||
backgroundColor: "#212121",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
borderRadius: theme.palette.textFieldStyle.borderRadius,
|
||||
width: 250,
|
||||
height: 50,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: 50,
|
||||
color: 'white',
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
},
|
||||
endAdornment: '%',
|
||||
}}
|
||||
@@ -2456,14 +2481,16 @@ const Billing = memo((props) => {
|
||||
style={{
|
||||
marginTop: 10,
|
||||
height: 50,
|
||||
backgroundColor: "#212121",
|
||||
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
width: 250,
|
||||
marginLeft: 15,
|
||||
marginLeft: 15,
|
||||
borderRadius: theme.palette.textFieldStyle.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: 50,
|
||||
color: 'white',
|
||||
color: theme.palette.textFieldStyle.color,
|
||||
},
|
||||
}}
|
||||
InputLabelProps={{
|
||||
@@ -2505,7 +2532,7 @@ const Billing = memo((props) => {
|
||||
setDeleteAlertIndex(index);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon sx={{ color: theme.palette.secondary.main }} />
|
||||
<DeleteIcon sx={{ color: themeMode === "dark" ? "rgba(255,255,255,0.7)" : "#666666" }} />
|
||||
</Button>
|
||||
)}
|
||||
<Dialog
|
||||
@@ -2525,8 +2552,9 @@ const Billing = memo((props) => {
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ textTransform: 'none', fontSize: 16 }}
|
||||
color="secondary"
|
||||
style={{ textTransform: 'none', fontSize: 16 }}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
handleDeleteAlertThreshold(deleteAlertIndex);
|
||||
setDeleteAlertVerification(false);
|
||||
@@ -2569,13 +2597,64 @@ const Billing = memo((props) => {
|
||||
Utilization & Stats
|
||||
</Typography>
|
||||
</div>
|
||||
<BillingStats
|
||||
isCloud={isCloud}
|
||||
clickedFromOrgTab={clickedFromOrgTab}
|
||||
globalUrl={globalUrl}
|
||||
selectedOrganization={selectedOrganization}
|
||||
userdata={userdata}
|
||||
/>
|
||||
{isChildOrg ? (
|
||||
<BillingStats
|
||||
isCloud={isCloud}
|
||||
clickedFromOrgTab={clickedFromOrgTab}
|
||||
globalUrl={globalUrl}
|
||||
selectedOrganization={selectedOrganization}
|
||||
userdata={userdata}
|
||||
/>
|
||||
): (
|
||||
<>
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={(event, newValue) => setCurrentTab(newValue)}
|
||||
style={{ marginTop: 20 }}
|
||||
TabIndicatorProps={{
|
||||
style: {
|
||||
height: 3,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tab
|
||||
label="Parent Organization"
|
||||
style={{ textTransform: 'none', fontSize: 16, minWidth: 'auto', paddingLeft: 12, paddingRight: 12 }}
|
||||
/>
|
||||
<Tab
|
||||
label="Child Organization"
|
||||
style={{ textTransform: 'none', fontSize: 16, minWidth: 'auto', paddingLeft: 12, paddingRight: 12 }}
|
||||
/>
|
||||
</Tabs>
|
||||
|
||||
{currentTab === 0 ? (
|
||||
<div style={{ marginTop: 30,}}>
|
||||
<BillingStats
|
||||
isCloud={isCloud}
|
||||
clickedFromOrgTab={clickedFromOrgTab}
|
||||
globalUrl={globalUrl}
|
||||
selectedOrganization={selectedOrganization}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
): (
|
||||
<BillingStatsChildOrg
|
||||
isCloud={isCloud}
|
||||
clickedFromOrgTab={clickedFromOrgTab}
|
||||
globalUrl={globalUrl}
|
||||
selectedOrganization={selectedOrganization}
|
||||
userdata={userdata}
|
||||
allChildOrgs={allChildOrgs}
|
||||
setAllChildOrgs={setAllChildOrgs}
|
||||
allChildOrgsStats={allChildOrgsStats}
|
||||
setAllChildOrgsStats={setAllChildOrgsStats}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
)
|
||||
@@ -2583,19 +2662,473 @@ const Billing = memo((props) => {
|
||||
|
||||
export default memo(Billing);
|
||||
|
||||
const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => {
|
||||
const BillingStatsChildOrg = memo(({ userdata, globalUrl, selectedOrganization, allChildOrgs, setAllChildOrgs, allChildOrgsStats, setAllChildOrgsStats }) => {
|
||||
const [subOrgStats, setSubOrgStats] = useState([]);
|
||||
const [subOrgs, setSubOrgs] = useState([]);
|
||||
const [subOrgStatsRows, setSubOrgStatsRows] = useState([]);
|
||||
const [subOrgStatsColumns, setSubOrgStatsColumns] = useState([]);
|
||||
const [allOrgLoaded, setAllOrgLoaded] = useState(false);
|
||||
const [allOrgStatsLoaded, setAllOrgStatsLoaded] = useState(false);
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState("")
|
||||
const [editingOrgId, setEditingOrgId] = useState("")
|
||||
const [limit, setLimit] = useState("")
|
||||
const [tableCreated, setTableCreated] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filteredRows, setFilteredRows] = useState([]);
|
||||
const { themeMode, brandColor, supportEmail } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
// Handle page change
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
const HanldeLoadStats = async () => {
|
||||
const childOrgs = selectedOrganization.child_orgs;
|
||||
if (allChildOrgsStats.length > 0){
|
||||
setSubOrgStats(allChildOrgsStats)
|
||||
setAllOrgStatsLoaded(true)
|
||||
if (allChildOrgsStats.length > 0 && allChildOrgs.length > 0 && subOrgStatsRows.length === 0 && subOrgStatsColumns.length === 0) {
|
||||
HandleCreateTable(allChildOrgsStats, allChildOrgs)
|
||||
}
|
||||
return
|
||||
}
|
||||
const promises = childOrgs.map((org) => {
|
||||
// get org stats base on region url
|
||||
const baseUrl = org?.region_url?.length > 0 && !window?.location?.origin?.includes("localhost") ? org?.region_url : globalUrl;
|
||||
const url = `${baseUrl}/api/v1/orgs/${org.id}/stats`;
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
});
|
||||
|
||||
try {
|
||||
const responses = await Promise.all(promises);
|
||||
setSubOrgStats(responses);
|
||||
setAllChildOrgsStats(responses);
|
||||
setAllOrgStatsLoaded(true);
|
||||
} catch (error) {
|
||||
console.error("Error loading stats:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const HandleGetSuborg = async () => {
|
||||
const childOrgs = selectedOrganization.child_orgs
|
||||
|
||||
if (allChildOrgs.length > 0){
|
||||
setSubOrgs(allChildOrgs)
|
||||
setAllOrgLoaded(true)
|
||||
if (allChildOrgsStats.length > 0 && allChildOrgs.length > 0 && subOrgStatsRows.length === 0 && subOrgStatsColumns.length === 0) {
|
||||
HandleCreateTable(allChildOrgsStats, allChildOrgs)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const promises = childOrgs.map((org) => {
|
||||
const baseUrl = org?.region_url?.length > 0 && !window?.location?.origin?.includes("localhost") ? org?.region_url : globalUrl;
|
||||
const url = `${baseUrl}/api/v1/orgs/${org.id}`;
|
||||
return fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
});
|
||||
try {
|
||||
const responses = await Promise.all(promises);
|
||||
setSubOrgs(responses);
|
||||
setAllChildOrgs(responses);
|
||||
setAllOrgLoaded(true);
|
||||
} catch (error) {
|
||||
console.error("Error loading suborgs:", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (subOrgStats.length === 0 && selectedOrganization && selectedOrganization?.child_orgs?.length > 0) {
|
||||
HanldeLoadStats()
|
||||
}
|
||||
if (subOrgs.length === 0 && selectedOrganization && selectedOrganization?.child_orgs?.length > 0) {
|
||||
HandleGetSuborg()
|
||||
}
|
||||
|
||||
}, [selectedOrganization?.child_orgs]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (allOrgLoaded && allOrgStatsLoaded && !tableCreated) {
|
||||
HandleCreateTable(subOrgStats, subOrgs)
|
||||
}
|
||||
}
|
||||
, [allOrgLoaded, allOrgStatsLoaded, tableCreated])
|
||||
|
||||
|
||||
const HandleCreateTable = (subOrgStats, subOrgs) => {
|
||||
|
||||
if (subOrgStats.length === 0 || subOrgs.length === 0) return;
|
||||
|
||||
// check whether all of the suborg.success is false
|
||||
const allSubOrgStatsSuccess = subOrgStats.every((stat) => stat.success === false);
|
||||
const allSubOrgsSuccess = subOrgs.every((org) => org.success === false);
|
||||
|
||||
if (allSubOrgStatsSuccess || allSubOrgsSuccess) {
|
||||
setSubOrgStats([])
|
||||
setSubOrgs([])
|
||||
setTableCreated(true)
|
||||
return
|
||||
}
|
||||
|
||||
const rows = subOrgStats.map((stat, index) => {
|
||||
const subOrg = subOrgs[index]
|
||||
if (!subOrg) return null;
|
||||
return {
|
||||
id: index,
|
||||
name: subOrg.name,
|
||||
orgId: subOrg.id,
|
||||
limit: subOrg?.sync_features?.app_executions?.limit || "N/A",
|
||||
usage: stat?.monthly_app_executions || "N/A",
|
||||
workflows_usage: stat?.total_workflow_executions || "N/A",
|
||||
workflow_usage_limit: subOrg?.sync_features?.workflow_executions?.limit || "N/A",
|
||||
}
|
||||
})
|
||||
|
||||
setSubOrgStatsRows(rows)
|
||||
|
||||
const columns = [
|
||||
{ field: "id", headerName: "ID", width: 100 },
|
||||
{ field: "name", headerName: "Name", width: 200 },
|
||||
{ field: "usage", headerName: "App Execution Usage", width: 200 },
|
||||
{
|
||||
field: "limit", headerName: "App Execution Limit", width: 200, renderCell: (params) => {
|
||||
return (
|
||||
<>
|
||||
<Typography style={{ fontSize: 16 }}>
|
||||
{params.value}
|
||||
<IconButton
|
||||
style={{ color: theme.palette.primary.main }}
|
||||
onClick={() => {
|
||||
setOpen(true)
|
||||
setEditingOrgId(params.row.orgId)
|
||||
setEditing("app_executions")
|
||||
if (params.value === "N/A") {
|
||||
setLimit("")
|
||||
} else {
|
||||
setLimit(params.value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Edit/>
|
||||
</IconButton>
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
}
|
||||
},
|
||||
{ field: "workflows_usage", headerName: "Workflow Execution Usage", width: 200 },
|
||||
{ field: "workflow_usage_limit", headerName: "Workflow Execution Limit", width: 200, renderCell: (params) => {
|
||||
return (
|
||||
<>
|
||||
<Typography style={{ fontSize: 16 }}>
|
||||
{params.value}
|
||||
</Typography>
|
||||
<IconButton
|
||||
style={{ color: theme.palette.primary.main }}
|
||||
onClick={() => {
|
||||
setOpen(true)
|
||||
setEditingOrgId(params.row.orgId)
|
||||
setEditing("workflow_executions")
|
||||
if (params.value === "N/A") {
|
||||
setLimit("")
|
||||
} else {
|
||||
setLimit(params.value)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Edit/>
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
},
|
||||
]
|
||||
|
||||
setSubOrgStatsColumns(columns)
|
||||
|
||||
if (allOrgLoaded && allOrgStatsLoaded && !tableCreated) {
|
||||
setTableCreated(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const HandleEditLimit = (orgId, editing, limit) => {
|
||||
|
||||
|
||||
// change limit as number if string
|
||||
if (typeof limit === "string") {
|
||||
limit = parseInt(limit, 10)
|
||||
}
|
||||
if (isNaN(limit)) {
|
||||
toast.error("Please enter a valid number")
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedOrganization.sync_features.app_executions.limit <= 10000) {
|
||||
toast.error("Insufficient app execution limit to increase child org limit")
|
||||
return
|
||||
}
|
||||
|
||||
// check whether limit is greater than than parent org limit
|
||||
if (editing === "app_executions" && limit > selectedOrganization.sync_features.app_executions.limit && !userdata.support) {
|
||||
toast.error("App execution limit cannot be greater than parent org limit")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (editing === "workflow_executions" && limit > selectedOrganization.sync_features.workflow_executions.limit && !userdata.support) {
|
||||
toast.error("Workflow execution limit cannot be greater than parent org limit")
|
||||
return
|
||||
}
|
||||
|
||||
// find the org in the subOrgs array
|
||||
const orgIndex = subOrgs.findIndex((org) => org.id === orgId)
|
||||
if (orgIndex === -1) {
|
||||
toast.error("Organization not found")
|
||||
return
|
||||
}
|
||||
|
||||
const org = subOrgs[orgIndex]
|
||||
|
||||
org.sync_features[editing].limit = limit
|
||||
org.sync_features.editing = true
|
||||
const sync_features = org.sync_features
|
||||
const data = {
|
||||
org_id: orgId,
|
||||
sync_features: sync_features,
|
||||
}
|
||||
|
||||
const url = `${globalUrl}/api/v1/orgs/${orgId}`;
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
}).then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
toast("Successfully change suborg limit!");
|
||||
if (editing === "app_executions") {
|
||||
setSubOrgStatsRows((prevRows) => {
|
||||
const newRows = [...prevRows];
|
||||
newRows[orgIndex].limit = limit;
|
||||
return newRows;
|
||||
});
|
||||
}else if (editing === "workflow_executions") {
|
||||
setSubOrgStatsRows((prevRows) => {
|
||||
const newRows = [...prevRows];
|
||||
newRows[orgIndex].workflow_usage_limit = limit;
|
||||
return newRows;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const HandleClosePopUP = () => {
|
||||
setOpen(false)
|
||||
setEditing("")
|
||||
setEditingOrgId("")
|
||||
setLimit("")
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", marginTop: 30, minHeight: 300, marginBottom: 200, }}>
|
||||
{open && (
|
||||
<IncreaseLimitPopUp open={open} onClose={HandleClosePopUP} limit={limit} HandleEditLimit={HandleEditLimit} setLimit={setLimit} editing={editing} setEditing={setEditing} editingOrgId={editingOrgId} setEditingOrgId={setEditingOrgId}/>
|
||||
)}
|
||||
<Typography style={{ marginBottom: 5, fontSize: 24, fontWeight: "bold" }}>
|
||||
Child Organizations
|
||||
</Typography>
|
||||
<div style={{display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10}}>
|
||||
<Typography color="textSecondary" style={{ marginTop: 10, fontSize: 16 }}>
|
||||
View and configure execution limits for child organizations. Click the edit icon to modify app and workflow execution limits.
|
||||
</Typography>
|
||||
</div>
|
||||
{tableCreated ? (
|
||||
subOrgStatsRows.length > 0 && subOrgStatsColumns.length > 0 ? (
|
||||
<>
|
||||
<TextField
|
||||
style={{ marginBottom: 10, width: 500, marginTop: 20 }}
|
||||
variant="outlined"
|
||||
placeholder="Search organizations by name or ID"
|
||||
value={searchQuery}
|
||||
fullWidth
|
||||
InputProps={{
|
||||
style: {
|
||||
fontSize: "1em",
|
||||
height: 51,
|
||||
width: 693,
|
||||
borderRadius: 4,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{ marginLeft: 5}} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value.toLowerCase());
|
||||
const filtered = subOrgStatsRows.filter((row) =>
|
||||
row.name.toLowerCase().includes(e.target.value.toLowerCase().trim()) ||
|
||||
row.orgId.toLowerCase().includes(e.target.value.toLowerCase().trim())
|
||||
);
|
||||
setFilteredRows(filtered);
|
||||
}}
|
||||
/>
|
||||
<DataGrid
|
||||
rows={searchQuery ? filteredRows : subOrgStatsRows}
|
||||
columns={subOrgStatsColumns}
|
||||
pageSize={rowsPerPage}
|
||||
rowsPerPageOptions={[5, 10, 25, 50]}
|
||||
onPageChange={handleChangePage}
|
||||
onPageSizeChange={handleChangeRowsPerPage}
|
||||
autoHeight
|
||||
style={{ height: "300px", width: "100%", backgroundColor: theme.palette.platformColor, color: theme.palette.text.primary }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography variant="h6" color="secondary" style={{ margin: "auto",}}>
|
||||
{selectedOrganization.child_orgs.length === 0 ? "No child organizations exist." : "Unable to load child organization stats. Statistics may not be initialized yet." }
|
||||
</Typography>
|
||||
)
|
||||
) : (
|
||||
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100%", margin: 'auto'}}>
|
||||
<CircularProgress style={{ color: "#FF8444" }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const IncreaseLimitPopUp = memo(({ open, onClose, limit, setLimit, HandleEditLimit, editingOrgId, editing}) => {
|
||||
const [currentLimit, setCurrentLimit] = useState(limit)
|
||||
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
return(
|
||||
<Dialog open={open} onClose={onClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
'& .MuiDialogContent-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{ fontSize: 24, fontWeight: "bold" }}>
|
||||
Increase {editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
value={currentLimit}
|
||||
onChange={(e) => setCurrentLimit(e.target.value)}
|
||||
label={`${editing.replaceAll("_", " ").replace(/\b\w/g, c => c.toUpperCase())} Limit`}
|
||||
type="string"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
margin="normal"
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
HandleEditLimit(editingOrgId, editing, currentLimit)
|
||||
setLimit(currentLimit)
|
||||
onClose()
|
||||
}}
|
||||
}
|
||||
></TextField>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginRight: 5, color: theme.palette.primary.main }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" onClick={() => {
|
||||
HandleEditLimit(editingOrgId, editing, currentLimit)
|
||||
setLimit(currentLimit)
|
||||
onClose()
|
||||
}} style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginRight: 10 }}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => {
|
||||
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
const wrapperStyle = useMemo(() => ({
|
||||
width: clickedFromOrgTab
|
||||
? "100%"
|
||||
: "auto",
|
||||
padding: "27px 10px 19px 27px",
|
||||
backgroundColor: '#212121',
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'hidden',
|
||||
maxHeight: "1700px", overflowY: "auto",scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'
|
||||
}), [clickedFromOrgTab]);
|
||||
maxHeight: "1700px",
|
||||
overflowY: "auto",
|
||||
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
||||
scrollbarWidth: 'thin'
|
||||
}), [clickedFromOrgTab, theme]);
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
@@ -2604,9 +3137,9 @@ const PaddingWrapper = memo(({ clickedFromOrgTab, children }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const Wrapper = memo(({ children, clickedFromOrgTab }) => {
|
||||
const Wrapper = memo(({ children, clickedFromOrgTab }) => {
|
||||
return (
|
||||
<PaddingWrapper clickedFromOrgTab={clickedFromOrgTab}>
|
||||
<PaddingWrapper clickedFromOrgTab={clickedFromOrgTab}>
|
||||
{children}
|
||||
</PaddingWrapper>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user