Merge pull request #3266 from bluewave-labs/fix/cleanup

fix; cleanup
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-02-10 09:33:33 -08:00
commit a2dfbd29c7
90 files changed
+202 -7725

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@
<div id="root"></div>
<script
type="module"
src="/src/main.jsx"
src="/src/main.tsx"
></script>
</body>
</html>
+10 -8
View File
@@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, type CSSProperties } from "react";
import { useSelector } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
@@ -7,18 +7,17 @@ import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import AppLayout from "@/Components/v1/Layouts/AppLayout";
import type { RootState } from "@/Types/state";
function App() {
const mode = useSelector((state) => state.ui.mode);
const mode = useSelector((state: RootState) => state.ui.mode);
// Cleanup
useEffect(() => {
return () => {
logger.cleanup();
networkService.cleanup();
};
}, []);
@@ -30,6 +29,7 @@ function App() {
<GlobalStyles
styles={{
body: {
// @ts-ignore
backgroundColor: theme.palette.background.main,
},
}}
@@ -40,10 +40,12 @@ function App() {
<ToastContainer
newestOnTop={true}
theme={mode}
style={{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
}}
style={
{
"--toastify-color-progress-light": "#7C8BA1",
"--toastify-color-progress-dark": "#7C8BA1",
} as CSSProperties
}
/>
</ThemeProvider>
);
@@ -1,228 +0,0 @@
// Components
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Icon from "../Icon";
import Dialog from "../Dialog/index.jsx";
// Utils
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils.jsx";
import PropTypes from "prop-types";
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js";
const ActionsMenu = ({
monitor,
isAdmin,
updateRowCallback,
pauseCallback,
setIsLoading = () => {},
}) => {
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
const theme = useTheme();
const [pauseMonitor, isPausing, error] = usePauseMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { id: actions.id };
await deleteMonitor({ monitor });
updateRowCallback();
};
const handlePause = async () => {
try {
setIsLoading(true);
await pauseMonitor({ monitorId: monitor.id });
pauseCallback();
} catch (error) {
createToast({ body: "Failed to pause monitor." });
} finally {
setIsLoading(false);
}
};
const openMenu = (event, id, url) => {
event.preventDefault();
event.stopPropagation();
setAnchorEl(event.currentTarget);
setActions({ id: id, url: url });
};
const openRemove = (e) => {
closeMenu(e);
setIsOpen(true);
};
const closeMenu = (e) => {
e.stopPropagation();
setAnchorEl(null);
};
const navigate = useNavigate();
return (
<>
<IconButton
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url);
}}
sx={{
"&:focus": {
outline: "none",
},
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Icon
name="Settings"
size={20}
/>
</IconButton>
<Menu
className="actions-menu"
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={(e) => closeMenu(e)}
disableScrollLock
slotProps={{
paper: {
sx: {
"& ul": {
p: theme.spacing(2.5),
backgroundColor: theme.palette.primary.main,
},
"& li": { m: 0, color: theme.palette.primary.contrastTextSecondary },
/*
This should not be set automatically on the last of type
"& li:last-of-type": {
color: theme.palette.error.main,
}, */
},
},
}}
>
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/${actions.id}`);
}}
>
Details
</MenuItem>
{/* TODO - pass monitor id to Incidents page */}
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/incidents/${actions.id}`);
}}
>
Incidents
</MenuItem>
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/uptime/create/${actions.id}`);
}}
>
Clone
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
openRemove(e);
}}
sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }}
>
Remove
</MenuItem>
)}
</Menu>
<Dialog
open={isOpen}
theme={theme}
title="Do you really want to delete this monitor?"
description="Once deleted, this monitor cannot be retrieved."
/* Do we need stop propagation? */
onCancel={(e) => {
e.stopPropagation();
setIsOpen(false);
}}
confirmationButtonLabel="Delete"
/* Do we need stop propagation? */
onConfirm={(e) => {
console.log(e);
e.stopPropagation();
handleRemove(e);
}}
isLoading={isDeleting}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
</>
);
};
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
isActive: PropTypes.bool,
}).isRequired,
isAdmin: PropTypes.bool,
updateRowCallback: PropTypes.func,
pauseCallback: PropTypes.func,
setIsLoading: PropTypes.func,
};
export default ActionsMenu;
@@ -1,68 +0,0 @@
import PropTypes from "prop-types";
import { Box, Stack, useTheme } from "@mui/material";
// import useUtils from "../../Pages/Uptime/utils";
/**
* A component that renders a pulsating dot with a specified color.
*
* @component
* @example
* // Example usage:
* <PulseDot color="#f00" />
*
* @param {Object} props
* @param {string} props.color - The color of the dot.
* @returns {JSX.Element} The PulseDot component.
*/
const PulseDot = ({ color }) => {
const theme = useTheme();
// const { statusToTheme } = useUtils();
/* TODO refactor so it gets status and gets theme color. Then uses theme.palette.[themeColor].lowContrast */
/* const themeColor = statusToTheme[status]; */
return (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: theme.palette.accent.contrastText,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};
PulseDot.propTypes = {
color: PropTypes.string.isRequired,
};
export default PulseDot;
@@ -1,30 +0,0 @@
import React from "react";
import Button from "@mui/material/Button";
import { styled } from "@mui/material/styles";
const RoundGradientButton = styled(Button)(({ theme }) => ({
position: "relative",
border: "5px solid transparent",
backgroundClip: "padding-box",
borderRadius: 30,
fontSize: "1.2rem",
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.background.main,
"&:after": {
position: "absolute",
top: -3,
left: -3,
right: -3,
bottom: -3,
background:
theme.palette.mode === "dark"
? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)"
: "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)",
content: '""',
zIndex: -1,
borderRadius: 30,
},
}));
export default RoundGradientButton;
@@ -1,214 +0,0 @@
/**
* CustomAreaChart component for rendering an area chart with optional gradient and custom ticks.
*
* @param {Object} props - The properties object.
* @param {Array} props.data - The data array for the chart.
* @param {Array} props.dataKeys - An array of data keys to be plotted as separate areas.
* @param {string} props.xKey - The key for the x-axis data.
* @param {string} [props.yKey] - The key for the y-axis data (optional).
* @param {Object} [props.xTick] - Custom tick component for the x-axis.
* @param {Object} [props.yTick] - Custom tick component for the y-axis.
* @param {string} [props.strokeColor] - The base stroke color for the areas.
* If not provided, uses a predefined color palette.
* @param {string} [props.fillColor] - The base fill color for the areas.
* @param {boolean} [props.gradient=false] - Whether to apply a gradient fill to the areas.
* @param {string} [props.gradientDirection="vertical"] - The direction of the gradient.
* @param {string} [props.gradientStartColor] - The start color of the gradient.
* Defaults to the area's stroke color if not provided.
* @param {string} [props.gradientEndColor] - The end color of the gradient.
* @param {Object} [props.customTooltip] - Custom tooltip component for the chart.
* @param {string|number} [props.height="100%"] - Height of the chart container.
*
* @returns {JSX.Element} The rendered area chart component.
*
* @example
* // Single series chart
* <CustomAreaChart
* data={temperatureData}
* dataKeys={["temperature"]}
* xKey="date"
* yKey="temperature"
* gradient={true}
* gradientStartColor="#ff6b6b"
* gradientEndColor="#4ecdc4"
* />
*
* @example
* // Multi-series chart with custom tooltip
* <CustomAreaChart
* data={performanceData}
* dataKeys={["cpu.usage", "memory.usage"]}
* xKey="timestamp"
* xTick={<CustomTimeTick />}
* yTick={<PercentageTick />}
* gradient={true}
* customTooltip={({ active, payload, label }) => (
* <CustomTooltip
* label={label}
* payload={payload}
* active={active}
* />
* )}
* />
*/
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import { createGradient } from "../Utils/gradientUtils.jsx";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { useId } from "react";
import { Fragment } from "react";
const CustomAreaChart = ({
data,
dataKeys,
xKey,
xDomain,
yKey,
yDomain,
xTick,
yTick,
strokeColor,
fillColor,
gradient = false,
gradientDirection = "vertical",
gradientStartColor,
gradientEndColor,
customTooltip,
height = "100%",
}) => {
const theme = useTheme();
const uniqueId = useId();
const AREA_COLORS = [
// Blues
"#3182bd", // Deep blue
"#6baed6", // Medium blue
"#9ecae1", // Light blue
// Greens
"#74c476", // Soft green
"#a1d99b", // Light green
"#c7e9c0", // Pale green
// Oranges
"#fdae6b", // Warm orange
"#fdd0a2", // Light orange
"#feedde", // Pale orange
// Purples
"#9467bd", // Lavender
"#a55194", // Deep magenta
"#c994c7", // Soft magenta
// Reds
"#ff9896", // Soft red
"#de2d26", // Deep red
"#fc9272", // Medium red
// Cyans/Teals
"#17becf", // Cyan
"#7fcdbb", // Teal
"#a1dab4", // Light teal
// Yellows
"#fec44f", // Mustard
"#fee391", // Light yellow
"#ffffd4", // Pale yellow
// Additional colors
"#e377c2", // Soft pink
"#bcbd22", // Olive
"#2ca02c", // Vibrant green
];
return (
<ResponsiveContainer
width="100%"
height={height}
// FE team HELP! Why does this overflow if set to 100%?
>
<AreaChart data={data}>
<XAxis
dataKey={xKey}
{...(xDomain && { domain: xDomain })}
{...(xTick && { tick: xTick })}
/>
<YAxis
dataKey={yKey}
{...(yDomain && { domain: yDomain })}
{...(yTick && { tick: yTick })}
/>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
{dataKeys.map((dataKey, index) => {
const gradientId = `gradient-${uniqueId}-${index}`;
return (
<Fragment key={dataKey}>
{gradient === true &&
createGradient({
id: gradientId,
startColor: gradientStartColor || AREA_COLORS[index],
endColor: gradientEndColor,
direction: gradientDirection,
})}
<Area
yKey={dataKey}
key={dataKey}
type="monotone"
dataKey={dataKey}
stroke={strokeColor || AREA_COLORS[index]}
fill={gradient === true ? `url(#${gradientId})` : fillColor}
/>
</Fragment>
);
})}
{customTooltip ? (
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={customTooltip}
wrapperStyle={{ pointerEvents: "none" }}
/>
) : (
<Tooltip />
)}
</AreaChart>
</ResponsiveContainer>
);
};
CustomAreaChart.propTypes = {
data: PropTypes.array.isRequired,
dataKeys: PropTypes.array.isRequired,
xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself
xKey: PropTypes.string.isRequired,
xDomain: PropTypes.array,
yKey: PropTypes.string,
yDomain: PropTypes.array,
fillColor: PropTypes.string,
strokeColor: PropTypes.string,
gradient: PropTypes.bool,
gradientDirection: PropTypes.string,
gradientStartColor: PropTypes.string,
gradientEndColor: PropTypes.string,
customTooltip: PropTypes.object,
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default CustomAreaChart;
Whitespace-only changes.
@@ -1,181 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState } from "react";
import "./index.css";
import { useSelector } from "react-redux";
/* TODO add prop validation and jsdocs */
const BarChart = ({ checks = [] }) => {
const theme = useTheme();
const [animate, setAnimate] = useState(false);
const uiTimezone = useSelector((state) => state.ui.timezone);
useEffect(() => {
setAnimate(true);
});
// set responseTime to average if there's only one check
if (checks.length === 1) {
checks[0] = { ...checks[0], responseTime: 50 };
}
if (checks.length !== 25) {
const placeholders = Array(25 - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
flexWrap="nowrap"
gap={theme.spacing(1.5)}
height="50px"
width="fit-content"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{checks.map((check, index) =>
check === "placeholder" ? (
/* TODO what is the purpose of this box? */
// CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars
// even if there are less than 25 checks
<Box
key={`${check}-${index}`}
position="relative"
width={theme.spacing(4.5)}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
) : (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(
check.updatedAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
<Box mt={theme.spacing(2)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(2)}
gap={theme.spacing(12)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>
<Typography component="span">
{check.originalResponseTime}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?.id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Box
position="relative"
width="9px"
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
/*
TODO this doesn't seem to be used
"&:hover > .MuiBox-root": {
filter: "brightness(0.8)",
}, */
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.responseTime : 0}%`}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Tooltip>
)
)}
</Stack>
);
};
export default BarChart;
@@ -1,106 +0,0 @@
// Components
import { Typography, Stack } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox/index.jsx";
/**
* `EmptyView` is a functional React component that displays an empty state view with an optional icon, header, and message.
*
* @component
* @param {Object} props - The properties that define the `EmptyView` component.
* @param {React.ReactNode} [props.icon] - An optional icon to display at the top of the empty view.
* @param {string} [props.header] - An optional header text displayed next to the icon.
* @param {string} [props.message="No Data"] - The message to be displayed in the empty view.
* @param {'h1' | 'h2' | 'h3'} [props.headingLevel="h2"] - The heading level for the message text.
* @param {string} [props.justifyContent="flex-start"] - The CSS `justify-content` value to align elements vertically.
* @param {string} [props.height="100%"] - The height of the empty view container.
*
* @example
* // Example usage of EmptyView component:
* <EmptyView
* icon={<SomeIcon />}
* header="Average Response Time"
* message="No Response Time Available"
* headingLevel="h2"
* justifyContent="center"
* height="50%"
* />
*
* @returns {React.Element} The `EmptyView` component with customizable icon, header, and message.
*/
const EmptyView = ({
icon,
header,
message = "No Data",
headingLevel = "h2",
justifyContent = "flex-start",
height = "100%",
}) => {
const theme = useTheme();
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: 4,
borderBottomRightRadius: 4,
}}
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
<Stack
flex={1}
justifyContent="center"
alignItems="center"
>
<Typography component={headingLevel}>{message}</Typography>
</Stack>
</Stack>
</Stack>
);
};
EmptyView.propTypes = {
message: PropTypes.string,
icon: PropTypes.node,
header: PropTypes.string,
headingLevel: PropTypes.oneOf(["h1", "h2", "h3"]),
justifyContent: PropTypes.string,
height: PropTypes.string,
};
export default EmptyView;
@@ -1,104 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox/index.jsx";
import EmptyView from "./EmptyView.jsx";
import PropTypes from "prop-types";
const ChartBox = ({
children,
icon,
header,
height = "300px",
justifyContent = "space-between",
Legend,
borderRadiusRight = 4,
sx,
noDataMessage,
isEmpty = false,
}) => {
const theme = useTheme();
if (isEmpty) {
return (
<EmptyView
icon={icon}
header={header}
message={noDataMessage}
/>
);
}
return (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: borderRadiusRight,
borderBottomRightRadius: borderRadiusRight,
}}
>
<Stack
flex={1}
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
minWidth: 250,
"& h2": {
color: theme.palette.primary.contrastTextSecondary,
fontSize: 15,
fontWeight: 500,
},
"& .MuiBox-root:not(.area-tooltip) p": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 13,
},
"& .MuiBox-root > span": {
color: theme.palette.primary.contrastText,
fontSize: 20,
"& span": {
opacity: 0.8,
marginLeft: 2,
fontSize: 15,
},
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
{children}
</Stack>
{Legend && Legend}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
header: PropTypes.string,
height: PropTypes.string,
noDataMessage: PropTypes.string,
isEmpty: PropTypes.bool,
};
@@ -1,14 +0,0 @@
.radial-chart {
position: relative;
display: inline-block;
}
.radial-chart-base {
opacity: 0.3;
}
.radial-chart-progress {
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 1.5s ease-in-out;
}
@@ -1,148 +0,0 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState, useMemo } from "react";
import Stack from "@mui/material/Stack";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
import CircularProgress from "@mui/material/CircularProgress";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
/**
* A Performant SVG based circular gauge
*
* @component
* @param {Object} props - Component properties
* @param {number} [props.progress=0] - Progress percentage (0-100)
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
* @param {number} [props.precision=1] - Precision of the progress percentage
* @param {string} [props.unit="%"] - Unit of progress
*
* @example
* <CustomGauge
* progress={75}
* radius={50}
* strokeWidth={10}
* />
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
precision = 1,
unit = "%",
}) => {
const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
() => ({
circumference: 2 * Math.PI * radius,
totalSize: radius * 2 + strokeWidth * 2,
strokeLength: (progress / 100) * (2 * Math.PI * radius),
}),
[radius, strokeWidth, progress]
);
// Handle initial animation
const [offset, setOffset] = useState(circumference);
useEffect(() => {
setOffset(circumference);
const timer = setTimeout(() => {
setOffset(circumference - strokeLength);
}, 100);
return () => clearTimeout(timer);
}, [progress, circumference, strokeLength]);
const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE));
let fillColor;
if (progressWithinRange < 50) {
fillColor = theme.palette.success.main;
} else if (progressWithinRange < 80) {
fillColor = theme.palette.warning.lowContrast;
} else {
fillColor = theme.palette.error.lowContrast;
}
if (isLoading) {
return (
<Stack
className="radial-chart"
width={radius}
height={radius}
alignItems="center"
justifyContent="center"
>
<CircularProgress color="accent" />
</Stack>
);
}
return (
<Box
className="radial-chart"
width={radius}
height={radius}
sx={{ backgroundColor: theme.palette.primary.main, borderRadius: "50%" }}
>
<svg
viewBox={`0 0 ${totalSize} ${totalSize}`}
width={radius}
height={radius}
>
<circle
className="radial-chart-base"
stroke={theme.palette.secondary.light}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
cy={totalSize / 2} // Center the circle
r={radius}
/>
<circle
className="radial-chart-progress"
stroke={fillColor}
strokeWidth={strokeWidth}
strokeDasharray={`${circumference} ${circumference}`}
strokeDashoffset={offset}
strokeLinecap="round"
fill="none"
cx={totalSize / 2}
cy={totalSize / 2}
r={radius}
/>
</svg>
<Typography
className="radial-chart-text"
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
...theme.typography.h2,
fill: theme.typography.h2.color,
}}
>
{`${progressWithinRange.toFixed(precision)}${unit}`}
</Typography>
</Box>
);
};
export default CustomGauge;
CustomGauge.propTypes = {
isLoading: PropTypes.bool,
progress: PropTypes.number,
radius: PropTypes.number,
strokeWidth: PropTypes.number,
precision: PropTypes.number,
unit: PropTypes.string,
};
@@ -1,181 +0,0 @@
// Components
import { Stack, Box, Tooltip, Typography } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useSelector } from "react-redux";
const PlaceholderCheck = ({ daysToShow }) => {
const theme = useTheme();
return (
<Box
width={`calc(30vw / ${daysToShow})`}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
);
};
PlaceholderCheck.propTypes = {
daysToShow: PropTypes.number,
};
const Check = ({ check, daysToShow }) => {
const [animate, setAnimate] = useState(false);
const theme = useTheme();
useEffect(() => {
setAnimate(true);
}, []);
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(check._id, "ddd, MMMM D, YYYY", uiTimezone)}
</Typography>
<Box mt={theme.spacing(2)}>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
gap={theme.spacing(4)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Uptime percentage
</Typography>
<Typography component="span">
{check.upPercentage.toFixed(2)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
%
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?._id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Box
position="relative"
width={`calc(30vw / ${daysToShow})`}
height="100%"
backgroundColor={theme.palette.error.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.upPercentage : 0}%`}
backgroundColor={theme.palette.success.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Tooltip>
);
};
Check.propTypes = {
check: PropTypes.object,
daysToShow: PropTypes.number,
};
const DePINStatusPageBarChart = ({ checks = [], daysToShow = 30 }) => {
if (checks.length !== daysToShow) {
const placeholders = Array(daysToShow - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
justifyContent="space-between"
width="100%"
flexWrap="nowrap"
height="50px"
>
{checks.map((check) => {
if (check === "placeholder") {
return (
<PlaceholderCheck
key={Math.random()}
daysToShow={daysToShow}
/>
);
}
return (
<Check
key={Math.random()}
check={check}
daysToShow={daysToShow}
/>
);
})}
</Stack>
);
};
DePINStatusPageBarChart.propTypes = {
checks: PropTypes.array,
daysToShow: PropTypes.number,
};
export default DePINStatusPageBarChart;
@@ -1,42 +0,0 @@
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import IconBox from "../../IconBox/index.jsx";
import PropTypes from "prop-types";
const LegendBox = ({ children, icon, header, sx }) => {
const theme = useTheme();
return (
<Stack
direction="column"
gap={theme.spacing(4)}
borderRadius={theme.spacing(8)}
sx={{
...sx,
"& label": { pl: theme.spacing(6) },
borderLeftStyle: "solid",
borderLeftWidth: 1,
borderLeftColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(8),
background: `linear-gradient(325deg, ${theme.palette.tertiary.main} 20%, ${theme.palette.primary.main} 45%)`,
}}
>
<Stack
direction="row"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
LegendBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
header: PropTypes.string,
};
export default LegendBox;
Whitespace-only changes.
@@ -1,222 +0,0 @@
import PropTypes from "prop-types";
import {
AreaChart,
Area,
XAxis,
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import {
tooltipDateFormatLookup,
tickDateFormatLookup,
} from "../Utils/chartUtilFunctions.js";
import "./index.css";
const CustomToolTip = ({ active, payload, label, dateRange }) => {
const format = tooltipDateFormatLookup(dateRange);
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
if (active && payload && payload.length) {
const responseTime = payload[0]?.payload?.originalAvgResponseTime
? payload[0]?.payload?.originalAvgResponseTime
: (payload[0]?.payload?.avgResponseTime ?? 0);
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, format, uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={theme.palette.primary.main}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response time:
</Typography>
<Typography
ml={theme.spacing(4)}
component="span"
>
{Math.floor(responseTime)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.number,
payload: PropTypes.shape({
_id: PropTypes.string,
avgResponseTime: PropTypes.number,
originalAvgResponseTime: PropTypes.number,
}),
})
),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
dateRange: PropTypes.string,
};
const CustomTick = ({ x, y, payload, dateRange }) => {
const format = tickDateFormatLookup(dateRange);
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
dateRange: PropTypes.string,
};
const MonitorDetailsAreaChart = ({ checks, dateRange }) => {
const theme = useTheme();
const memoizedChecks = useMemo(() => checks, [checks[0]]);
const [isHovered, setIsHovered] = useState(false);
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<AreaChart
width="100%"
height="100%"
data={memoizedChecks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.main}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.light}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick dateRange={dateRange} />}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip dateRange={dateRange} />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.accent.main} // CAIO_REVIEW
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.accent.main, r: 5 }} // CAIO_REVIEW
/>
</AreaChart>
</ResponsiveContainer>
);
};
MonitorDetailsAreaChart.propTypes = {
checks: PropTypes.array,
dateRange: PropTypes.string,
};
export default MonitorDetailsAreaChart;
@@ -1,217 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
/**
* A customizable Bar component that renders a colored bar with optional children.
* @component
*
* @param {string} width The width of the bar (e.g., "100px").
* @param {string} height The height of the bar (e.g., "50px").
* @param {string} backgroundColor The background color of the bar (e.g., "#FF5733").
* @param {string} [borderRadius] Optional border radius for the bar (e.g., "8px").
* @param {node} children The content to be rendered inside the bar.
* @returns {JSX.Element} The Bar component.
*/
const Bar = forwardRef(
({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => {
const theme = useTheme();
return (
<Box
ref={ref}
position="relative"
width={width}
height={height}
backgroundColor={backgroundColor}
sx={{
borderRadius: borderRadius || theme.spacing(1.5),
}}
{...otherProps}
>
{children}
</Box>
);
}
);
Bar.displayName = "Bar";
Bar.propTypes = {
width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
height: PropTypes.string.isRequired,
backgroundColor: PropTypes.string.isRequired,
borderRadius: PropTypes.string,
children: PropTypes.node,
};
/* TODO add prop validation and jsdocs */
const StatusPageBarChart = ({ checks = [] }) => {
const theme = useTheme();
const [animate, setAnimate] = useState(false);
const uiTimezone = useSelector((state) => state.ui.timezone);
const barWidth = {
xs: "calc(60% / 25)",
xl: "calc(40% / 25)",
};
useEffect(() => {
setAnimate(true);
}, []);
// set responseTime to average if there's only one check
if (checks.length === 1) {
checks[0] = { ...checks[0], responseTime: 50 };
}
if (checks.length !== 25) {
const placeholders = Array(25 - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
justifyContent="space-between"
width="100%"
flexWrap="nowrap"
height="50px"
onClick={(event) => event.stopPropagation()}
sx={{
cursor: "default",
}}
>
{checks.map((check, index) =>
check === "placeholder" ? (
/* TODO what is the purpose of this box? */
// CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars
// even if there are less than 25 checks
<Bar
key={`${check}-${index}`}
width={barWidth}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
/>
) : (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(
check.createdAt,
"ddd, MMMM D, YYYY, HH:mm A",
uiTimezone
)}
</Typography>
<Box mt={theme.spacing(2)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(2)}
gap={theme.spacing(12)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>
<Typography component="span">
{check.originalResponseTime}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
ms
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?.id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Bar
width={barWidth}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.responseTime : 0}%`}
backgroundColor={
check.status
? theme.palette.success.lowContrast
: theme.palette.error.lowContrast
}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Bar>
</Tooltip>
)
)}
</Stack>
);
};
export default StatusPageBarChart;
@@ -1,27 +0,0 @@
export const tooltipDateFormatLookup = (dateRange) => {
const dateFormatLookup = {
recent: "ddd. MMMM D, YYYY, hh:mm A",
day: "ddd. MMMM D, YYYY, hh:mm A",
week: "ddd. MMMM D, YYYY, hh:mm A",
month: "ddd. MMMM D, YYYY",
};
const format = dateFormatLookup[dateRange];
if (format === undefined) {
return "";
}
return format;
};
export const tickDateFormatLookup = (dateRange) => {
const tickFormatLookup = {
recent: "h:mm A",
day: "h:mm A",
week: "MM/D, h:mm A",
month: "ddd. M/D",
};
const format = tickFormatLookup[dateRange];
if (format === undefined) {
return "";
}
return format;
};
@@ -1,337 +0,0 @@
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
import { useTheme } from "@mui/material";
import { Text } from "recharts";
import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js";
import { Box, Stack, Typography } from "@mui/material";
import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js";
/**
* Custom tick component for rendering time with timezone.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element} The rendered tick component.
*/
export const TzTick = ({ x, y, payload, index, dateRange }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
const format = tickDateFormatLookup(dateRange);
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, format, uiTimezone)}
</Text>
);
};
TzTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
dateRange: PropTypes.string,
};
/**
* Custom tick component for rendering percentage values.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
*/
export const PercentTick = ({ x, y, payload, index }) => {
const theme = useTheme();
if (index === 0) return null;
return (
<Text
x={x - 20}
y={y}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{`${(payload?.value * 100).toFixed()}%`}
</Text>
);
};
PercentTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
/**
* Converts a decimal value to a formatted percentage string.
*
* @param {number} value - The decimal value to convert (e.g., 0.75)
* @returns {string} Formatted percentage string (e.g., "75.00%") or original input if not a number
*
* @example
* getFormattedPercentage(0.7543) // Returns "75.43%"
* getFormattedPercentage(1) // Returns "100.00%"
* getFormattedPercentage("test") // Returns "test"
*/
const getFormattedPercentage = (value) => {
if (typeof value !== "number") return value;
return `${(value * 100).toFixed(2)}%`;
};
/**
* Custom tick component for rendering network bytes per second.
*
* @param {Object} props - The properties object.
* @param {number} props.x - The x-coordinate for the tick.
* @param {number} props.y - The y-coordinate for the tick.
* @param {Object} props.payload - The payload object containing tick data.
* @param {number} props.index - The index of the tick.
* @returns {JSX.Element|null} The rendered tick component or null for the first tick.
*/
export const NetworkTick = ({ x, y, payload, index, formatter }) => {
const theme = useTheme();
if (index === 0) return null;
if (formatter === undefined) {
formatter = (value, space = false) => {
if (typeof value !== "number") return value;
// need to add space between value and unit
return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`;
};
}
return (
<Text
x={x - 20}
y={y}
textAnchor="middle"
fill={theme.palette.primary.contrastTextTertiary}
fontSize={11}
fontWeight={400}
>
{formatter(payload?.value, true)}
</Text>
);
};
NetworkTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
formatter: PropTypes.func,
};
/**
* Custom tooltip component for displaying infrastructure data.
*
* @param {Object} props - The properties object.
* @param {boolean} props.active - Indicates if the tooltip is active.
* @param {Array} props.payload - The payload array containing tooltip data.
* @param {string} props.label - The label for the tooltip.
* @param {string} props.yKey - The key for the y-axis data.
* @param {string} props.yLabel - The label for the y-axis data.
* @param {string} props.dotColor - The color of the dot in the tooltip.
* @returns {JSX.Element|null} The rendered tooltip component or null if inactive.
*/
export const InfrastructureTooltip = ({
active,
payload,
label,
yKey,
yIdx = -1,
yLabel,
dotColor,
dateRange,
formatter = getFormattedPercentage,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const format = tooltipDateFormatLookup(dateRange);
if (active && payload && payload.length) {
const [hardwareType, metric] = yKey.split(".");
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, format, uiTimezone)}
</Typography>
<Box mt={theme.spacing(1)}>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{yIdx >= 0
? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}`
: `${yLabel} ${formatter(payload[0].payload[yKey])}`}
</Typography>
</Stack>
</Box>
{/* Display original value */}
</Box>
);
}
return null;
};
InfrastructureTooltip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.array,
label: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string,
PropTypes.number,
]),
yKey: PropTypes.string,
yIdx: PropTypes.number,
yLabel: PropTypes.string,
dotColor: PropTypes.string,
dateRange: PropTypes.string,
};
export const TemperatureTooltip = ({
active,
payload,
label,
keys,
dotColor,
dateRange,
}) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const format = tooltipDateFormatLookup(dateRange);
const formatCoreKey = (key) => {
return key.replace(/^core(\d+)$/, "Core $1");
};
if (active && payload && payload.length) {
return (
<Box
className="area-tooltip"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.primary.contrastTextTertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, format, uiTimezone)}
</Typography>
<Stack direction="column">
{keys.map((key) => {
return (
<Stack
key={key}
display="inline-flex"
direction="row"
justifyContent="space-between"
ml={theme.spacing(3)}
sx={{
"& span": {
color: theme.palette.primary.contrastTextTertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Box
display="inline-block"
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{`${formatCoreKey(key)}: ${payload[0].payload[key]} °C`}
</Typography>
</Stack>
<Typography component="span"></Typography>
</Stack>
);
})}
</Stack>
</Box>
);
}
return null;
};
TemperatureTooltip.propTypes = {
active: PropTypes.bool,
keys: PropTypes.array,
payload: PropTypes.array,
label: PropTypes.oneOfType([
PropTypes.instanceOf(Date),
PropTypes.string,
PropTypes.number,
]),
dotColor: PropTypes.string,
dateRange: PropTypes.string,
};
@@ -1,47 +0,0 @@
/**
* Creates an SVG gradient definition for use in charts
* @param {Object} params - The gradient parameters
* @param {string} [params.id="colorUv"] - Unique identifier for the gradient
* @param {string} params.startColor - Starting color of the gradient (hex, rgb, or color name)
* @param {string} params.endColor - Ending color of the gradient (hex, rgb, or color name)
* @param {number} [params.startOpacity=0.8] - Starting opacity (0-1)
* @param {number} [params.endOpacity=0] - Ending opacity (0-1)
* @param {('vertical'|'horizontal')} [params.direction="vertical"] - Direction of the gradient
* @returns {JSX.Element} SVG gradient definition element
* @example
* createCustomGradient({
* startColor: "#1976D2",
* endColor: "#42A5F5",
* direction: "horizontal"
* })
*/
export const createGradient = ({
id,
startColor,
endColor,
startOpacity = 0.8,
endOpacity = 0,
direction = "vertical", // or "horizontal"
}) => (
<defs>
<linearGradient
id={id}
x1={direction === "vertical" ? "0" : "0"}
y1={direction === "vertical" ? "0" : "0"}
x2={direction === "vertical" ? "0" : "1"}
y2={direction === "vertical" ? "1" : "0"}
>
<stop
offset="0%"
stopColor={startColor}
stopOpacity={startOpacity}
/>
<stop
offset="100%"
stopColor={endColor}
stopOpacity={endOpacity}
/>
</linearGradient>
</defs>
);
-80
View File
@@ -1,80 +0,0 @@
import "./check.css";
import PropTypes from "prop-types";
import Icon from "../Icon";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
/**
* `Check` is a functional React component that displays a check icon and a label.
*
* @component
* @param {Object} props - The properties that define the `Check` component.
* @param {string} props.text - The text to be displayed as the label next to the check icon.
* @param {'info' | 'error' | 'success'} [props.variant='info'] - The variant of the check component, affecting its styling.
* @param {boolean} [props.outlined] - Whether the check icon should be outlined or not.
*
* @example
* // To use this component, import it and use it in your JSX like this:
* <Check text="Your Text Here" />
*
* @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop.
*/
const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => {
const theme = useTheme();
const colors = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.info.border,
};
return (
<Stack
direction="row"
className="check"
gap={outlined ? theme.spacing(6) : theme.spacing(4)}
alignItems="center"
>
{outlined ? (
<Icon
name="CheckCircle"
size={20}
/>
) : (
<Icon
name="Check"
size={16}
color={colors[variant]}
/>
)}
<Typography
component="span"
sx={{
color:
variant === "info"
? theme.palette.primary.contrastTextSecondary
: colors[variant],
opacity: 0.9,
fontWeight: 450,
}}
>
{noHighlightText && (
<Typography
component="span"
sx={{ color: "inherit" }}
>
{noHighlightText}
</Typography>
)}{" "}
{text}
</Typography>
</Stack>
);
};
Check.propTypes = {
text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
noHighlightText: PropTypes.string,
variant: PropTypes.oneOf(["info", "error", "success"]),
outlined: PropTypes.bool,
};
export default Check;
Whitespace-only changes.
@@ -1,29 +0,0 @@
import { Box } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const CircularCount = ({ count }) => {
const theme = useTheme();
return (
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50rem",
fontSize: "12px",
fontWeight: 500,
}}
>
{count}
</Box>
);
};
CircularCount.propTypes = {
count: PropTypes.number,
};
export default CircularCount;
-220
View File
@@ -1,220 +0,0 @@
import { useState } from "react";
import { styled, alpha } from "@mui/material/styles";
import Box from "@mui/material/Box";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Container from "@mui/material/Container";
import MenuItem from "@mui/material/MenuItem";
import Drawer from "@mui/material/Drawer";
import Icon from "../Icon";
import ThemeSwitch from "../ThemeSwitch/index.jsx";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
const StyledToolbar = styled(Toolbar)(({ theme, mode }) => ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
flexShrink: 0,
borderRadius: `calc(${theme.shape.borderRadius}px + 4px)`,
backdropFilter: "blur(24px)",
border: "1px solid",
borderColor:
mode === "light"
? alpha(theme.palette.common.black, 0.1)
: alpha(theme.palette.common.white, 0.1),
backgroundColor:
mode === "light"
? alpha(theme.palette.common.white, 0.4)
: alpha(theme.palette.common.black, 0.4),
boxShadow: theme.shadows[3],
padding: "8px 12px",
}));
const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
fontSize: "1.1rem",
margin: theme.spacing(4, 2),
}));
const AppAppBar = () => {
const [open, setOpen] = useState(false);
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
const location = useLocation();
const navigate = useNavigate();
const logoSrc =
mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png";
const toggleDrawer = (newOpen) => () => {
setOpen(newOpen);
};
const handleScroll = (id) => {
if (location.pathname === "/") {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
} else {
navigate(`/#${id}`);
}
};
return (
<AppBar
position="fixed"
sx={{
boxShadow: 0,
bgcolor: "transparent",
backgroundImage: "none",
border: "none",
mt: "calc(var(--template-frame-height, 0px) + 28px)",
}}
>
<Container maxWidth="lg">
<StyledToolbar
variant="dense"
disableGutters
mode={mode}
>
<Box sx={{ flexGrow: 1, display: "flex", alignItems: "center", px: 0 }}>
<img
src={logoSrc}
alt="Prism Logo"
style={{
height: "auto",
width: "auto",
marginRight: "10px",
maxHeight: "32px",
}}
/>
<Box sx={{ display: { xs: "none", md: "flex" } }}>
<Button
variant="text"
color="info"
size="large"
onClick={() => handleScroll("features")}
>
Features
</Button>
<Button
variant="text"
color="info"
size="large"
onClick={() => handleScroll("highlights")}
>
Highlights
</Button>
<Button
variant="text"
color="info"
size="large"
onClick={() => handleScroll("faq")}
>
FAQ
</Button>
<Button
variant="text"
color="info"
size="large"
href="https://uprock.com/blog"
>
Blog
</Button>
</Box>
</Box>
<Box
sx={{
display: { xs: "none", md: "flex" },
gap: 1,
alignItems: "center",
}}
>
{/* <Button color="primary" variant="text" size="small">
Sign in
</Button>
<Button color="primary" variant="contained" size="small">
Sign up
</Button> */}
</Box>
<Box
sx={{
display: { xs: "flex", md: "none" },
gap: 1,
}}
>
<IconButton
aria-label="Menu button"
onClick={toggleDrawer(true)}
>
<Icon
name="Menu"
size={24}
color={theme.palette.text.primary}
/>
</IconButton>
<Drawer
anchor="top"
open={open}
onClose={toggleDrawer(false)}
PaperProps={{
sx: {
top: 0,
marginTop: 0,
borderRadius: 0,
backgroundColor: theme.palette.background.paper,
},
}}
>
<Box sx={{ p: 4, backgroundColor: theme.palette.background.main }}>
<Box
sx={{
display: "flex",
justifyContent: "flex-end",
}}
>
<IconButton onClick={toggleDrawer(false)}>
<Icon
name="X"
size={24}
color={theme.palette.text.primary}
/>
</IconButton>
</Box>
<StyledMenuItem>Features</StyledMenuItem>
<StyledMenuItem>Testimonials</StyledMenuItem>
<StyledMenuItem>Highlights</StyledMenuItem>
<StyledMenuItem>FAQ</StyledMenuItem>
<StyledMenuItem
component="a"
href="https://uprock.com/blog"
>
Blog
</StyledMenuItem>
{/* <MenuItem>
<Button color="primary" variant="contained" fullWidth>
Sign up
</Button>
</MenuItem>
<MenuItem>
<Button color="primary" variant="outlined" fullWidth>
Sign in
</Button>
</MenuItem> */}
</Box>
</Drawer>
</Box>
<ThemeSwitch />
</StyledToolbar>
</Container>
</AppBar>
);
};
export default AppAppBar;
-255
View File
@@ -1,255 +0,0 @@
import * as React from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import IconButton from "@mui/material/IconButton";
import Link from "@mui/material/Link";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from "react-icons/fa";
function Copyright() {
return (
<Typography sx={{ color: "text.secondary", mt: 1 }}>
{"Copyright © "}
<Link
color="text.secondary"
href="https://prism.uprock.com/"
>
UpRock
</Link>
&nbsp;
{new Date().getFullYear()}
</Typography>
);
}
export default function Footer() {
return (
<Container
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: { xs: 4, sm: 8 },
py: { xs: 24, sm: 24 },
px: { xs: 12, sm: 12 },
textAlign: { sm: "center", md: "left" },
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
pt: { xs: 4, sm: 8 },
width: "100%",
borderColor: "divider",
}}
>
<div>
<Link
color="text.secondary"
href="https://uprock.com/privacy-policy"
>
Privacy Policy
</Link>
<Typography sx={{ display: "inline", mx: 0.5, opacity: 0.5 }}>
&nbsp;•&nbsp;
</Typography>
<Link
color="text.secondary"
href="https://uprock.com/terms-of-use"
>
Terms of Service
</Link>
<Copyright />
</div>
<Stack
direction="row"
spacing={1}
useFlexGap
sx={{ justifyContent: "left", color: "text.secondary" }}
>
<IconButton
color="inherit"
size="small"
href="mailto:prism@uprock.com?subject=Interested%20in%20UpRock%20Prism"
aria-label="Contact Us"
sx={{ alignSelf: "center" }}
>
<FaEnvelope />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://facebook.com/uprockcom"
aria-label="Facebook"
sx={{ alignSelf: "center" }}
>
<FaFacebook />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://x.com/uprockcom"
aria-label="X"
sx={{ alignSelf: "center" }}
>
<FaTwitter />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://www.linkedin.com/company/uprock/"
aria-label="LinkedIn"
sx={{ alignSelf: "center" }}
>
<FaLinkedin />
</IconButton>
<IconButton
color="inherit"
size="small"
href="https://github.com/uprockcom"
aria-label="GitHub"
sx={{ alignSelf: "center" }}
>
<FaGithub />
</IconButton>
</Stack>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
mt: 4,
}}
>
<Typography
variant="h2"
sx={{ color: "text.secondary" }}
>
Made with ❤️ by&nbsp;
<Link
href="https://uprock.com"
color="inherit"
sx={{ mx: 0.5 }}
>
UpRock&nbsp;
</Link>
&&nbsp;
<Link
href="https://bluewavelabs.ca"
color="inherit"
sx={{ mx: 0.5 }}
>
Bluewave Labs
</Link>
</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
mt: 2,
}}
>
<Typography
variant="h2"
sx={{ color: "text.secondary", mr: 1 }}
>
Built on&nbsp;
</Typography>
<svg
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 397.7 311.7"
xmlSpace="preserve"
width="15"
height="15"
>
<style type="text/css">
{`.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}`}
</style>
<linearGradient
id="SVGID_1_"
gradientUnits="userSpaceOnUse"
x1="360.8791"
y1="351.4553"
x2="141.213"
y2="-69.2936"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st0"
d="M64.6,237.9c2.4-2.4,5.7-3.8,9.2-3.8h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,237.9z"
/>
<linearGradient
id="SVGID_2_"
gradientUnits="userSpaceOnUse"
x1="264.8291"
y1="401.6014"
x2="45.163"
y2="-19.1475"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st1"
d="M64.6,3.8C67.1,1.4,70.4,0,73.8,0h317.4c5.8,0,8.7,7,4.6,11.1l-62.7,62.7c-2.4,2.4-5.7,3.8-9.2,3.8H6.5 c-5.8,0-8.7-7-4.6-11.1L64.6,3.8z"
/>
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="312.5484"
y1="376.688"
x2="92.8822"
y2="-44.061"
gradientTransform="matrix(1 0 0 -1 0 314)"
>
<stop
offset="0"
style={{ stopColor: "rgb(0, 255, 163)" }}
/>
<stop
offset="1"
style={{ stopColor: "rgb(220, 31, 255)" }}
/>
</linearGradient>
<path
className="st2"
d="M333.1,120.1c-2.4-2.4-5.7-3.8-9.2-3.8H6.5c-5.8,0-8.7,7-4.6,11.1l62.7,62.7c2.4,2.4,5.7,3.8,9.2,3.8h317.4 c5.8,0,8.7-7,4.6-11.1L333.1,120.1z"
/>
</svg>
<Typography
variant="h2"
sx={{ color: "text.secondary", ml: 1 }}
>
&nbsp;Solana
</Typography>
</Box>
</Box>
</Container>
);
}
@@ -1,58 +0,0 @@
import { Box, Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import ConfigBox from "../ConfigBox/index.jsx";
/**
* ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing.
* It serves as the wrapper for ConfigBox, with the left section displaying the title and description,
* and the right section displaying the children.
*
* @component
* @example
* return (
* <ConfigBox>
* <div>Left content (Title + Description)</div>
* <div>Right content (Children)</div>
* </ConfigBox>
* );
*/
const ConfigRow = ({ title, description, children }) => {
const theme = useTheme();
return (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{title}
</Typography>
{description && (
<Typography
variant="body2"
mt={theme.spacing(2)}
>
{description}
</Typography>
)}
</Box>
<Stack
gap={theme.spacing(15)}
mt={theme.spacing(4)}
>
{children}
</Stack>
</ConfigBox>
);
};
ConfigRow.propTypes = {
title: PropTypes.string.isRequired,
description: PropTypes.string,
children: PropTypes.node,
};
export default ConfigRow;
-24
View File
@@ -1,24 +0,0 @@
import PropTypes from "prop-types";
const Dot = ({ color = "gray", size = "4px", style }) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.string,
};
export default Dot;
@@ -1,44 +0,0 @@
import { Button, Stack } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const FallbackActionButtons = ({ link, type }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<Button
variant="contained"
color="accent"
sx={{ fontWeight: 700 }}
onClick={() => navigate(link)}
>
{t(`${type}.fallback.actionButton`)}
</Button>
{type === "uptimeMonitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
</Stack>
);
};
FallbackActionButtons.propTypes = {
title: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackActionButtons;
@@ -1,39 +0,0 @@
import { useTheme } from "@emotion/react";
import Box from "@mui/material/Box";
import Background from "../../../assets/Images/background-grid.svg?react";
import OutputAnimation from "../../../assets/Animations/output.gif";
import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif";
import { useSelector } from "react-redux";
const FallbackBackground = () => {
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
return (
<>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
background="transparent"
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
transform: "scale(0.6667)",
}}
/>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
</>
);
};
export default FallbackBackground;
@@ -1,35 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Check from "../Check/Check.jsx";
const FallbackCheckList = ({ checks, type }) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(2),
alignItems: "flex-start",
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{checks?.map((check, index) => (
<Check
text={check}
key={`${type.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Box>
);
};
FallbackCheckList.propTypes = {
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackCheckList;
@@ -1,44 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import PropTypes from "prop-types";
const FallbackContainer = ({ children, type }) => {
const theme = useTheme();
return (
<Box
border={1}
borderColor={theme.palette.tertiary.border}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.tertiary.background}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: "fit-content",
minHeight: "60vh",
width: {
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
},
padding: `${theme.spacing(20)} ${theme.spacing(10)}`,
}}
>
<Stack
className={`fallback__${type?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
{children}
</Stack>
</Box>
);
};
FallbackContainer.propTypes = {
children: PropTypes.node,
type: PropTypes.string,
};
export default FallbackContainer;
@@ -1,69 +0,0 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import Link from "@mui/material/Link";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Alert from "../Alert/index.jsx";
import PropTypes from "prop-types";
const renderWarningMessage = (t) => {
return (
<>
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
const FallbackPageSpeedWarning = ({ settingsData }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage(t)}
/>
)}
</Box>
</Box>
);
};
FallbackPageSpeedWarning.propTypes = {
settingsData: PropTypes.shape({
pagespeedKeySet: PropTypes.bool,
}),
};
export default FallbackPageSpeedWarning;
@@ -1,21 +0,0 @@
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
import PropTypes from "prop-types";
const FallbackTitle = ({ title }) => {
const theme = useTheme();
return (
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
);
};
FallbackTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default FallbackTitle;
@@ -1,40 +0,0 @@
[class*="fallback__"] {
width: fit-content;
margin: auto;
}
[class*="fallback__"] h1.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
}
[class*="fallback__"] button.MuiButtonBase-root,
[class*="fallback__"] .check {
width: max-content;
}
[class*="fallback__"] button.MuiButtonBase-root {
min-height: 34px;
}
[class*="fallback__"] .check span.MuiTypography-root,
[class*="fallback__"] button.MuiButtonBase-root {
font-size: var(--env-var-font-size-medium);
}
[class*="fallback__"] .background-pattern-svg {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
z-index: 0;
width: 100%;
max-width: 800px;
height: 100%;
max-height: 800px;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
@@ -1,75 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import "./index.css";
import FallbackTitle from "./FallbackTitle.jsx";
import FallbackCheckList from "./FallbackCheckList.jsx";
import FallbackActionButtons from "./FallBackActionButtons.jsx";
import FallbackBackground from "./FallbackBackground.jsx";
import FallbackContainer from "./FallbackContainer.jsx";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
*
* @param {Object} props - The component props.
* @param {string} props.title - The title to be displayed in the fallback UI.
* @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications").
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
* @param {string} [props.link="/"] - The link to navigate to.
* @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning.
* @returns {JSX.Element} The rendered fallback UI.
*/
const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => {
const theme = useTheme();
return (
<Box
position="relative"
sx={{
height: "100%",
width: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<FallbackContainer type={type}>
<FallbackBackground />
<Stack
gap={theme.spacing(5)}
zIndex={1}
alignItems="center"
>
<FallbackTitle title={title} />
<FallbackCheckList
checks={checks}
title={title}
type={type}
/>
</Stack>
{isAdmin && (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<FallbackActionButtons
title={title}
link={link}
type={type}
/>
{children}
</Stack>
)}
</FallbackContainer>
</Box>
);
};
Fallback.propTypes = {
title: PropTypes.string.isRequired,
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
link: PropTypes.string,
isAdmin: PropTypes.bool,
showPageSpeedWarning: PropTypes.bool,
type: PropTypes.string.isRequired,
children: PropTypes.node,
};
export default Fallback;
@@ -1,23 +0,0 @@
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const NetworkError = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</>
);
};
export default NetworkError;
@@ -1,85 +0,0 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import OutputAnimation from "../../../assets/Animations/output.gif";
import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif";
import Background from "../../../assets/Images/background-grid.svg?react";
import { useSelector } from "react-redux";
/**
* Fallback component to display a fallback UI for network errors
*
* @returns {JSX.Element} The rendered fallback UI.
*/
const GenericFallback = ({ children }) => {
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
return (
<Box
padding={theme.spacing(16)}
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
>
<Stack
alignItems="center"
gap={theme.spacing(20)}
sx={{
width: "fit-content",
margin: "auto",
marginTop: "100px",
}}
>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
backgroundColor: "transparent",
}}
/>
<Box
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
position: "absolute",
top: "0",
left: "50%",
transform: "translate(-50%, -50%)",
width: "100%",
maxWidth: "800px",
height: "100%",
maxHeight: "800px",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
alignItems="center"
maxWidth={"300px"}
zIndex={1}
>
{children}
</Stack>
</Stack>
</Box>
);
};
export default GenericFallback;
@@ -2,19 +2,19 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { logger } from "../../../Utils/Logger.js";
import { networkService } from "../../../main.jsx";
import { useLazyGet } from "@/Hooks/UseApi";
const withAdminCheck = (WrappedComponent) => {
const WithAdminCheck = (props) => {
const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true);
const [superAdminExists, setSuperAdminExists] = useState(false);
const [hasChecked, setHasChecked] = useState(false);
const { get: checkSuperAdmin, loading: isChecking } = useLazyGet();
useEffect(() => {
networkService
.doesSuperAdminExist()
checkSuperAdmin("/auth/users/superadmin")
.then((response) => {
if (response?.data?.data === true) {
if (response?.data === true) {
navigate("/login");
} else {
setSuperAdminExists(false);
@@ -24,11 +24,11 @@ const withAdminCheck = (WrappedComponent) => {
logger.error(error);
})
.finally(() => {
setIsChecking(false);
setHasChecked(true);
});
}, [navigate]);
}, [navigate, checkSuperAdmin]);
if (isChecking) {
if (!hasChecked || isChecking) {
return null;
}
@@ -1,32 +0,0 @@
import { Typography } from "@mui/material";
import PropTypes from "prop-types";
/**
* A heading component that renders text with a specific heading level.
*
* @param {Object} props - The properties passed to the Heading component.
* @param {('h1'|'h2'|'h3')} props.component - The heading level for the component.
* @param {Object} props.style - Custom styles to apply to the heading.
* @param {string} props.children - The content to display inside the heading.
* @returns {JSX.Element} The Typography component with specified heading properties.
*/
function Heading({ component, style, children }) {
return (
<Typography
component={component}
variant="h2"
fontWeight={600}
style={style}
>
{children}
</Typography>
);
}
Heading.propTypes = {
component: PropTypes.oneOf(["h1", "h2", "h3"]).isRequired,
style: PropTypes.object,
children: PropTypes.string.isRequired,
};
export { Heading };
-73
View File
@@ -1,73 +0,0 @@
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import Dot from "../Dot/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
*
* @component
* @param {Object} params - An object containing the following properties:
* @param {string} params.url - The URL of the host.
* @param {string} params.title - The name of the host.
* @param {string} params.percentageColor - The color of the percentage text.
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
const theme = useTheme();
return (
<Stack>
<Stack
direction="row"
position="relative"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography
component="span"
sx={{
fontWeight: 500,
}}
>
{title}
</Typography>
{percentageColor && percentage && (
<>
<Dot />
<Typography
component="span"
sx={{
fontWeight: 500,
color: percentageColor,
}}
>
{percentage}%
</Typography>
{status && (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
)}
</>
)}
</Stack>
{showURL && <span style={{ opacity: 0.6 }}>{url}</span>}
</Stack>
);
};
Host.propTypes = {
title: PropTypes.string,
percentageColor: PropTypes.string,
percentage: PropTypes.string,
url: PropTypes.string,
showURL: PropTypes.bool,
status: PropTypes.string,
};
export default Host;
@@ -1,67 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material";
import { BaseLabel } from "../Label/index.jsx";
/**
* @component
* @param {Object} props
* @param {number} props.status - The http status for the label
* @param {Styles} props.customStyles - CSS Styles passed from parent component
* @returns {JSX.Element}
* @example
* // Render a http status label
* <HttpStatusLabel status={404} />
*/
const DEFAULT_CODE = 9999; // Default code for unknown status
const handleStatusCode = (status) => {
if (status) {
return status;
}
return DEFAULT_CODE;
};
const getRoundedStatusCode = (status) => {
return Math.floor(status / 100) * 100;
};
const HttpStatusLabel = ({ status, customStyles }) => {
const theme = useTheme();
const colors = {
400: {
color: theme.palette.warning.main,
borderColor: theme.palette.warning.lowContrast,
},
500: {
color: theme.palette.error.main,
borderColor: theme.palette.error.lowContrast,
},
default: {
color: theme.palette.primary.contrastText,
borderColor: theme.palette.primary.contrastText,
},
};
const statusCode = handleStatusCode(status);
const { borderColor, color } =
colors[getRoundedStatusCode(statusCode)] || colors.default;
return (
<BaseLabel
label={String(statusCode)}
styles={{
color: color,
borderColor: borderColor,
...customStyles,
}}
/>
);
};
HttpStatusLabel.propTypes = {
status: PropTypes.number,
customStyles: PropTypes.object,
};
export { HttpStatusLabel };
@@ -1,77 +0,0 @@
import { Box, styled } from "@mui/material";
import PropTypes from "prop-types";
/**
* IconBox - A styled box component for rendering icons with consistent sizing and styling
*
* @component
* @param {Object} [props] - Configuration options for the IconBox
* @param {number} [props.height=34] - Height of the icon box
* @param {number} [props.width=34] - Width of the icon box
* @param {number} [props.minWidth=34] - Minimum width of the icon box
* @param {number} [props.borderRadius=4] - Border radius of the icon box
* @param {number} [props.svgWidth=20] - Width of the SVG icon
* @param {number} [props.svgHeight=20] - Height of the SVG icon
*
* @example
* // Basic usage
* <IconBox>
* <SomeIcon />
* </IconBox>
*
* @example
* // Customized usage
* <IconBox
* height={40}
* width={40}
* svgWidth={24}
* svgHeight={24}
* >
* <CustomIcon />
* </IconBox>
*
* @returns {React.ReactElement} A styled box containing an icon
*/
const IconBox = styled(Box)(
({
theme,
height = 34,
width = 34,
minWidth = 34,
borderRadius = 4,
svgWidth = 20,
svgHeight = 20,
}) => ({
height: height,
minWidth: minWidth,
width: width,
position: "relative",
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: borderRadius,
backgroundColor: theme.palette.tertiary.main,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: svgWidth,
height: svgHeight,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
})
);
IconBox.propTypes = {
height: PropTypes.number,
width: PropTypes.number,
minWidth: PropTypes.number,
borderRadius: PropTypes.number,
svgWidth: PropTypes.number,
svgHeight: PropTypes.number,
};
export default IconBox;
-72
View File
@@ -1,72 +0,0 @@
import { Box } from "@mui/material";
import PropTypes from "prop-types";
const isValidBase64Image = (data) => {
return /^[A-Za-z0-9+/=]+$/.test(data);
};
const Image = ({
shouldRender = true,
src,
alt,
width = "auto",
height = "auto",
minWidth = "auto",
minHeight = "auto",
maxWidth = "auto",
maxHeight = "auto",
base64,
placeholder,
sx,
}) => {
if (shouldRender === false) {
return null;
}
if (typeof src !== "undefined" && typeof base64 !== "undefined") {
console.warn("base64 takes precedence over src and overwrites it");
}
if (typeof base64 !== "undefined" && isValidBase64Image(base64)) {
src = `data:image/png;base64,${base64}`;
}
if (
typeof src === "undefined" &&
typeof base64 === "undefined" &&
typeof placeholder !== "undefined"
) {
src = placeholder;
}
return (
<Box
component="img"
src={src}
alt={alt}
minWidth={minWidth}
minHeight={minHeight}
maxWidth={maxWidth}
maxHeight={maxHeight}
width={width}
height={height}
sx={{ ...sx }}
/>
);
};
Image.propTypes = {
shouldRender: PropTypes.bool,
src: PropTypes.string,
alt: PropTypes.string.isRequired,
width: PropTypes.string,
height: PropTypes.string,
minWidth: PropTypes.string,
minHeight: PropTypes.string,
maxWidth: PropTypes.string,
maxHeight: PropTypes.string,
base64: PropTypes.string,
sx: PropTypes.object,
};
export default Image;
@@ -1,42 +0,0 @@
import { RowContainer } from "../StandardContainer/index.jsx";
import { Stack, Typography } from "@mui/material";
import Image from "../Image/index.jsx";
import { useTheme } from "@mui/material/styles";
const InfoBox = ({
img,
icon: Icon,
alt,
heading,
headingLevel = 2,
subHeading,
subHeadingLevel = "",
sx,
}) => {
const theme = useTheme();
return (
<RowContainer sx={{ ...sx }}>
{img && (
<Image
src={img}
height={"30px"}
width={"30px"}
alt={alt}
sx={{ marginRight: theme.spacing(8) }}
/>
)}
{Icon && (
<Icon sx={{ width: "30px", height: "30px", marginRight: theme.spacing(8) }} />
)}
<Stack>
<Typography variant={`h${headingLevel}`}>{heading}</Typography>
<Typography variant={subHeadingLevel ? `h${subHeadingLevel}` : "body1"}>
{subHeading}
</Typography>
</Stack>
</RowContainer>
);
};
export default InfoBox;
@@ -1,44 +0,0 @@
import { Stack, Button } from "@mui/material";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
// Use the provided label or fall back to the translated default
if (!isAdmin) return null;
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
{label || t("createNew")}
</Button>
{/* {bulkPath && <MonitorActions isLoading={isLoading} />} */}
</Stack>
);
};
export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
isLoading: PropTypes.bool,
path: PropTypes.string.isRequired,
label: PropTypes.string,
bulkPath: PropTypes.string,
};
@@ -1,19 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
>
<Skeleton
variant="rectangular"
width={100}
height={36}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,85 +0,0 @@
import { Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton.jsx";
import PropTypes from "prop-types";
const MonitorTimeFrameHeader = ({
isLoading = false,
hasDateRange = true,
dateRange,
setDateRange,
}) => {
const theme = useTheme();
if (isLoading) {
return <SkeletonLayout />;
}
let timeFramePicker = null;
if (hasDateRange) {
timeFramePicker = (
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "recent").toString()}
onClick={() => setDateRange("recent")}
>
Recent
</Button>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
);
}
return (
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "recent"
? "2 hours"
: dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
{timeFramePicker}
</Stack>
);
};
MonitorTimeFrameHeader.propTypes = {
isLoading: PropTypes.bool,
hasDateRange: PropTypes.bool,
dateRange: PropTypes.string,
setDateRange: PropTypes.func,
};
export default MonitorTimeFrameHeader;
@@ -1,23 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,66 +0,0 @@
import GenericFallback from "../GenericFallback/index.jsx";
import Fallback from "../Fallback/index.jsx";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import PropTypes from "prop-types";
const PageStateWrapper = ({
networkError,
isLoading,
items,
type,
fallbackLink,
fallbackChildren,
children,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const isEmpty = items === null || (Array.isArray(items) && items.length === 0);
const hasInitialized = items !== undefined && !(isLoading && isEmpty);
const showEmpty = !isLoading && isEmpty;
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!hasInitialized) {
return null;
}
if (showEmpty) {
return (
<Fallback
type={type}
title={t(`${type}.fallback.title`)}
checks={t(`${type}.fallback.checks`, { returnObjects: true })}
link={fallbackLink}
isAdmin={isAdmin}
>
{fallbackChildren}
</Fallback>
);
}
return <>{children}</>;
};
PageStateWrapper.propTypes = {
networkError: PropTypes.any,
isLoading: PropTypes.bool,
items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]),
type: PropTypes.string,
fallbackLink: PropTypes.string,
fallbackChildren: PropTypes.node,
children: PropTypes.node,
};
export default PageStateWrapper;
@@ -1,10 +0,0 @@
.progress-bar-container h2.MuiTypography-root,
.progress-bar-container p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
.progress-bar-container p.MuiTypography-root:has(span) {
font-size: 12px;
}
.progress-bar-container p.MuiTypography-root span {
padding-left: 2px;
}
@@ -1,180 +0,0 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
import Icon from "../Icon";
import "./index.css";
/**
* @param {Object} props - The component props.
* @param {JSX.Element} props.icon - The icon element to display (optional).
* @param {string} props.label - The label text for the progress item.
* @param {string} props.size - The size information for the progress item.
* @param {number} props.progress - The current progress value (0-100).
* @param {function} props.onClick - The function to handle click events on the remove button.
* @param {string} props.error - Error message to display if there's an error (optional).
* @returns {JSX.Element} The rendered component.
*/
const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) => {
const theme = useTheme();
return (
<Box
className="progress-bar-container"
mt={theme.spacing(10)}
p={theme.spacing(8)}
sx={{
minWidth: "200px",
height: "fit-content",
borderRadius: theme.shape.borderRadius,
border: 1,
borderColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.lowContrast,
"&:has(.input-error)": {
borderColor: theme.palette.error.main,
backgroundColor: theme.palette.error.lowContrast,
py: theme.spacing(4),
px: theme.spacing(8),
"& > .MuiStack-root > svg": {
fill: theme.palette.error.contrastText,
width: "20px",
height: "20px",
},
},
}}
>
<Stack
direction="row"
mb={error ? 0 : theme.spacing(5)}
gap={theme.spacing(5)}
alignItems={error ? "center" : "flex-start"}
>
{error ? (
<Icon
name="AlertTriangle"
size={20}
/>
) : icon ? (
<Box
sx={{
position: "relative",
height: 30,
minWidth: 30,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
backgroundColor: theme.palette.primary.main,
"& svg": {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 23,
height: 23,
"& path": {
fill: theme.palette.primary.contrastTextTertiary,
},
},
}}
>
{icon}
</Box>
) : (
""
)}
{error ? (
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{error}
</Typography>
) : (
<Box color={theme.palette.primary.contrastTextTertiary}>
<Typography
component="h2"
mb={theme.spacing(1.5)}
sx={{ wordBreak: "break-all" }}
>
{error ? error : label}
</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
>
{!error && size}
</Typography>
</Box>
)}
<IconButton
onClick={onClick}
sx={
!error
? {
alignSelf: "flex-start",
ml: "auto",
mr: theme.spacing(-2.5),
mt: theme.spacing(-2.5),
padding: theme.spacing(2.5),
"&:focus": {
outline: "none",
},
}
: {
ml: "auto",
"&:focus": {
outline: "none",
},
}
}
>
<Icon
name="X"
size={20}
/>
</IconButton>
</Stack>
{!error ? (
<Stack
direction="row"
alignItems="center"
>
<Box sx={{ width: "100%", mr: theme.spacing(5) }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
width: "100%",
height: "10px",
borderRadius: theme.shape.borderRadius,
maxWidth: "500px",
backgroundColor: theme.palette.primary.lowContrast,
}}
/>
</Box>
<Typography
component="p"
sx={{ minWidth: "max-content", opacity: 0.6 }}
>
{progress}
<span>%</span>
</Typography>
</Stack>
) : (
""
)}
</Box>
);
};
ProgressUpload.propTypes = {
icon: PropTypes.element, // JSX element for the icon (optional)
label: PropTypes.string, // Label text for the progress item
size: PropTypes.string.isRequired, // Size information for the progress item
progress: PropTypes.number.isRequired, // Current progress value (0-100)
onClick: PropTypes.func.isRequired, // Function to handle click events on the remove button
error: PropTypes.string, // Error message to display if there's an error (optional)
};
export default ProgressUpload;
Whitespace-only changes.
@@ -1,74 +0,0 @@
import React from "react";
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
import Icon from "../Icon";
import PropTypes from "prop-types";
const CustomStepIcon = (props) => {
const { completed, active } = props;
return completed ? (
<Icon
name="CheckCircle2"
size={24}
color="accent.main"
/>
) : (
<Icon
name="Circle"
size={24}
color={active ? "accent.main" : "text.disabled"}
/>
);
};
CustomStepIcon.propTypes = {
completed: PropTypes.bool.isRequired,
active: PropTypes.bool.isRequired,
};
/**
* @component
* @param {Object} props
* @param {Array} props.steps
*/
const ProgressStepper = ({ steps }) => {
const [activeStep, setActiveStep] = React.useState(1);
return (
<Stepper
activeStep={activeStep}
alternativeLabel
>
{steps.map((step, index) => {
const color = activeStep === index ? "primary" : "inherit";
return (
<Step
key={step.label}
onClick={() => setActiveStep(index)}
>
<StepLabel StepIconComponent={CustomStepIcon}>
<Typography
variant="body1"
color={color}
sx={{ fontWeight: "bold" }}
>
{step.label}
</Typography>
</StepLabel>
<Typography
variant="body1"
color={color}
>
{step.content}
</Typography>
</Step>
);
})}
</Stepper>
);
};
ProgressStepper.propTypes = {
steps: PropTypes.array.isRequired,
};
export default ProgressStepper;
@@ -1,73 +0,0 @@
import html2canvas from "html2canvas";
import Icon from "../Icon";
import { Button } from "@mui/material";
import { useTheme } from "@emotion/react";
const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
const theme = useTheme();
const captureAndShare = async () => {
try {
// Temporarily apply styles directly to the element
const originalBackground = elementToCapture.current.style.background;
const originalPadding = elementToCapture.current.style.padding;
elementToCapture.current.style.background = `radial-gradient(circle, ${theme.palette.gradient.color1}, ${theme.palette.gradient.color2}, ${theme.palette.gradient.color3}, ${theme.palette.gradient.color4}, ${theme.palette.gradient.color5})`;
elementToCapture.current.style.padding = `${theme.spacing(20)}`;
// Capture the element directly
const canvas = await html2canvas(elementToCapture.current, {
useCORS: true,
scale: 2,
allowTaint: true,
backgroundColor: null,
});
// Restore original styles
elementToCapture.current.style.background = originalBackground;
elementToCapture.current.style.padding = originalPadding;
const imageBlob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/png")
);
const file = new File([imageBlob], `${fileName}.png`, {
type: "image/png",
});
if (navigator.share) {
await navigator.share({
files: [file],
title: "Screenshot",
text: "Check out this screenshot!",
});
} else {
const url = URL.createObjectURL(imageBlob);
const a = document.createElement("a");
a.href = url;
a.download = `${fileName}.png`;
a.click();
URL.revokeObjectURL(url);
}
} catch (error) {
console.error(error);
}
};
return (
<Button
variant="outlined"
startIcon={
<Icon
name="Share2"
size={18}
color="success.main"
/>
}
color="success"
onClick={captureAndShare}
>
Share
</Button>
);
};
export default ShareComponent;
@@ -1,14 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,51 +0,0 @@
// Components
import { Stack } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
const Container = ({ children, direction, backgroundColor, sx }) => {
const theme = useTheme();
let bgColor =
typeof backgroundColor !== "undefined"
? backgroundColor
: theme.palette.background.main;
return (
<Stack
direction={direction}
padding={theme.spacing(8)}
gap={theme.spacing(2)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.spacing(4)}
backgroundColor={bgColor}
sx={{ ...sx }}
>
{children}
</Stack>
);
};
export const ColContainer = ({ children, backgroundColor, sx }) => {
return (
<Container
direction="column"
backgroundColor={backgroundColor}
sx={{ ...sx }}
>
{children}
</Container>
);
};
export const RowContainer = ({ children, backgroundColor, sx }) => {
return (
<Container
direction="row"
backgroundColor={backgroundColor}
sx={{ ...sx }}
>
{children}
</Container>
);
};
-149
View File
@@ -1,149 +0,0 @@
import { Stack, Typography } from "@mui/material";
import Image from "../Image/index.jsx";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
/**
* StatBox Component
*
* A reusable component that displays a statistic with a heading and subheading
* in a styled box with a gradient background.
*
* @component
* @param {Object} props - The component props
* @param {string} props.heading - The primary heading/title of the statistic
* @param {string|React.ReactNode} props.subHeading - The value or description of the statistic
* @param {boolean} [props.gradient=false] - Determines if the box should have a gradient background
* @param {string} [props.status] - The status of the statistic
* @param {Object} [props.sx] - Additional custom styling to be applied to the box
*
* @example
* return (
* <StatBox
* heading="Total Users"
* subHeading="1,234"
* sx={{ width: 300 }}
* />
* )
*
* @returns {React.ReactElement} A styled box containing the statistic
*/
const StatBox = ({
img,
icon: Icon,
alt,
heading,
subHeading,
gradient = false,
status = "",
sx,
}) => {
const theme = useTheme();
const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const statusBoxStyles = gradient
? {
background: `linear-gradient(to bottom right, ${theme.palette[themeColor].main} 30%, ${theme.palette[themeColor].lowContrast} 70%)`,
borderColor: theme.palette[themeColor].lowContrast,
}
: {
background: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`,
borderColor: theme.palette.primary.lowContrast,
};
const headingStyles = gradient
? {
color: theme.palette[themeColor].contrastText,
}
: {
color: theme.palette.primary.contrastTextSecondary,
};
const spanFixedStyles = { marginLeft: theme.spacing(2), fontSize: 15, fontWeight: 600 };
const detailTextStyles = gradient
? {
color: theme.palette[themeColor].contrastText,
"& span": {
color: theme.palette[themeColor].contrastText,
...spanFixedStyles,
},
}
: {
color: theme.palette.primary.contrastText,
"& span": {
color: theme.palette.primary.contrastTextTertiary,
...spanFixedStyles,
},
};
const responsiveWidths = {
default: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
md: `calc(50% - (1 * ${theme.spacing(8)} / 2))`,
};
return (
<Stack
direction="row"
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
/* TODO why are we using width and min width here? */
minWidth: 200,
// this is for status box to be reponsive on diff screens
width: responsiveWidths.default, // Default: 4 items per row
[theme.breakpoints.down("md")]: {
width: responsiveWidths.md, // 2 items per row
},
border: 1,
borderStyle: "solid",
borderRadius: 4,
...statusBoxStyles,
"& h2": {
/* TODO font size should come from theme */
fontSize: 13,
fontWeight: 500,
textTransform: "uppercase",
...headingStyles,
},
"& p": {
fontSize: 18,
marginTop: theme.spacing(2),
...detailTextStyles,
},
...sx,
}}
>
{img && (
<Image
src={img}
height={"30px"}
width={"30px"}
alt={alt}
sx={{ marginRight: theme.spacing(8) }}
/>
)}
{Icon && (
<Icon sx={{ width: "30px", height: "30px", marginRight: theme.spacing(8) }} />
)}
<Stack>
<Typography component="h2">{heading}</Typography>
<Typography sx={{ fontWeight: 500 }}>{subHeading}</Typography>
</Stack>
</Stack>
);
};
StatBox.propTypes = {
heading: PropTypes.string,
subHeading: PropTypes.node,
gradient: PropTypes.bool,
status: PropTypes.string,
sx: PropTypes.object,
icon: PropTypes.elementType,
img: PropTypes.node,
alt: PropTypes.string,
};
export default StatBox;
@@ -1,37 +0,0 @@
// Components
import { Stack } from "@mui/material";
import SkeletonLayout from "./skeleton.jsx";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
<SkeletonLayout
numBoxes={children?.length ?? 1}
flexWrap={flexWrap}
/>
);
}
return (
<Stack
direction="row"
flexWrap={flexWrap}
gap={theme.spacing(8)}
justifyContent="flex-start"
display="flex"
>
{children}
</Stack>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
flexWrap: PropTypes.string,
children: PropTypes.node,
};
export default StatusBoxes;
@@ -1,33 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const SkeletonLayout = ({ numBoxes, flexWrap }) => {
const theme = useTheme();
return (
<Stack
direction="row"
flexWrap={flexWrap}
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
{Array.from({ length: numBoxes }).map((_, index) => {
const width = `${200 / numBoxes}%`;
return (
<Skeleton
variant="rounded"
width={width}
height={100}
key={index}
/>
);
})}
</Stack>
);
};
SkeletonLayout.propTypes = {
flexWrap: PropTypes.string,
numBoxes: PropTypes.number,
};
export default SkeletonLayout;
@@ -1,83 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, Typography } from "@mui/material";
/**
*
* @component
* @example
*
* @param {boolean} props.shouldRender - Whether to render the subheader
* @param {string} props.direction - Direction of the subheader
* @param {string} props.headerText - Header text
* @param {number} props.headerLevel - Font characteristic of the header
* @param {string} props.subHeaderText - Subheader text
* @param {number} props.subHeaderLevel - Font characteristic of the subheader
* @param {string} props.alignItems - Align items
* @param {node} props.children - Children
* @returns {JSX.Element} The rendered component
*/
const SubHeader = ({
shouldRender = true,
direction = "row",
headerText,
headerLevel = 1,
subHeaderText,
subHeaderLevel = 1,
alignItems = "center",
children,
...props
}) => {
const theme = useTheme();
if (!shouldRender) {
return null;
}
return (
<Stack
direction={direction}
alignItems={alignItems}
justifyContent="space-between"
{...props}
>
<Stack direction={"column"}>
<Typography
component={`h${headerLevel}`}
variant={`h${headerLevel}`}
mb={theme.spacing(1)}
>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.primary.contrastTextTertiary}
>
{headerText}
</Typography>
</Typography>
<Typography
variant={`body${subHeaderLevel}`}
color={theme.palette.primary.contrastTextTertiary}
>
{subHeaderText}
</Typography>
</Stack>
{children}
</Stack>
);
};
SubHeader.propTypes = {
shouldRender: PropTypes.bool,
direction: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
headerText: PropTypes.string,
headerLevel: PropTypes.number,
subHeaderText: PropTypes.string,
subHeaderLevel: PropTypes.number,
alignItems: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.node,
};
export default SubHeader;
-41
View File
@@ -1,41 +0,0 @@
import { Box, useTheme } from "@mui/material";
import { TabList } from "@mui/lab";
import PropTypes from "prop-types";
/**
* CustomTabList component
* @param {string} value - The currently selected tab's value.
* @param {function} onChange - Callback when a different tab is selected.
* @param {React.ReactNode} children - Tab components to render inside the TabList.
* @param {object} props - Additional props passed to the TabList component.
*/
const CustomTabList = ({ value, onChange, children, ...props }) => {
const theme = useTheme();
return (
<Box
sx={{
marginBottom: theme.spacing(12),
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
>
<TabList
value={value}
onChange={onChange}
{...props}
>
{children}
</TabList>
</Box>
);
};
CustomTabList.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
children: PropTypes.node,
};
export default CustomTabList;
@@ -1,81 +0,0 @@
import PropTypes from "prop-types";
import { Box, Button } from "@mui/material";
import LeftArrow from "../../../ArrowLeft/index.jsx";
import RightArrow from "../../../ArrowRight/index.jsx";
import { useTheme } from "@emotion/react";
TablePaginationActions.propTypes = {
count: PropTypes.number.isRequired,
page: PropTypes.number.isRequired,
rowsPerPage: PropTypes.number.isRequired,
onPageChange: PropTypes.func.isRequired,
};
/**
* Component for pagination actions (first, previous, next, last).
*
* @component
* @param {Object} props
* @param {number} props.count - Total number of items.
* @param {number} props.page - Current page number.
* @param {number} props.rowsPerPage - Number of rows per page.
* @param {function} props.onPageChange - Callback function to handle page change.
*
* @returns {JSX.Element} Pagination actions component.
*/
function TablePaginationActions({ count, page, rowsPerPage, onPageChange }) {
const handleFirstPageButtonClick = (event) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
const theme = useTheme();
return (
<Box sx={{ flexShrink: 0, ml: "24px", display: "flex", gap: theme.spacing(2) }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrow type="double" />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrow type="double" />
</Button>
</Box>
);
}
export { TablePaginationActions };
@@ -1,144 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, TablePagination, Typography } from "@mui/material";
import { TablePaginationActions } from "./Actions/index.jsx";
import Icon from "../../Icon";
Pagination.propTypes = {
paginationLabel: PropTypes.string, // Label for the pagination.
itemCount: PropTypes.number, // Total number of items for pagination.
page: PropTypes.number, // Current page index.
rowsPerPage: PropTypes.number, // Number of rows displayed per page.
handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page.
};
const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
// Determine whether pagination buttons should be shown
const shouldShowPaginationButtons = (itemCount, rowsPerPage) => {
return Math.ceil(itemCount / rowsPerPage) > 1;
};
// Empty action component for pagination (no-op component)
const EmptyPaginationActions = () => <div />;
/**
* Pagination component for table navigation with customized styling and behavior.
*
* @param {object} props - Component properties.
* @param {string} props.paginationLabel - Label for the pagination.
* @param {number} props.monitorCount - Total number of monitors to paginate.
* @param {number} props.page - Current page index (0-based).
* @param {number} props.rowsPerPage - Number of rows to display per page.
* @param {function} props.handleChangePage - Callback for handling page changes.
* @param {function} props.handleChangeRowsPerPage - Callback for handling changes to rows per page.
* @returns {JSX.Element} The Pagination component.
*/
function Pagination({
paginationLabel,
itemCount = 0,
page = 0,
rowsPerPage = 5,
handleChangePage,
handleChangeRowsPerPage,
}) {
const theme = useTheme();
const start = page * rowsPerPage + 1;
const end = Math.min(page * rowsPerPage + rowsPerPage, itemCount);
const range = `${start} - ${end}`;
const showPaginationButtons = shouldShowPaginationButtons(itemCount, rowsPerPage);
return (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
marginTop={8}
>
<Typography
px={theme.spacing(2)}
variant="body2"
sx={{ opacity: 0.7 }}
>
Showing {range} of {itemCount} {paginationLabel}
</Typography>
<TablePagination
component="div"
count={itemCount}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={
showPaginationButtons ? TablePaginationActions : EmptyPaginationActions
}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}`
}
slotProps={{
select: {
MenuProps: {
keepMounted: true,
disableScrollLock: true,
PaperProps: {
className: "pagination-dropdown",
sx: {
mt: 0,
mb: theme.spacing(2),
},
},
transformOrigin: { vertical: "bottom", horizontal: "left" },
anchorOrigin: { vertical: "top", horizontal: "left" },
sx: {
mt: theme.spacing(-2),
},
},
inputProps: { id: "pagination-dropdown" },
IconComponent: () => (
<Icon
name="ChevronsUpDown"
size={16}
/>
),
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
minWidth: theme.spacing(20),
textAlign: "left",
"&.Mui-focused > div": {
backgroundColor: theme.palette.primary.main,
},
"& .MuiSelect-icon": {
// Add this style override
position: "absolute",
right: 0,
top: "50%",
transform: "translateY(-50%)",
},
},
},
}}
sx={{
color: theme.palette.primary.contrastTextSecondary,
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.3,
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
}}
/>
</Stack>
);
}
export default Pagination;
@@ -1,11 +0,0 @@
export const createHeaderFactory = (getCellSx = () => {}) => {
return ({ id, content, onClick = () => {}, render = () => {} }) => {
return {
id,
content,
onClick,
getCellSx,
render,
};
};
};
-180
View File
@@ -1,180 +0,0 @@
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import SkeletonLayout from "./skeleton.jsx";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
/**
* @typedef {Object} Header
* @property {number|string} id - The unique identifier for the header.
* @property {React.ReactNode} content - The content to display in the header cell.
* @property {Function} onClick - A function to be called when this cell is clicked, receiving the event and row data as an argument.
* @property {Object} getCellSx - Function that takes a row and returns a style object for the table cell.
* @property {Function} render - A function to render the cell content for a given row.
*/
/**
* @typedef {Object} Config
* @property {Function} onRowClick - A function to be called when a row is clicked, receiving the row data as an argument.
* @property {Object} rowSX - Style object for the table row.
*/
/**
* DataTable component renders a table with headers and data.
*
* @param {Object} props - The component props.
* @param {Header[]} props.headers - An array of header objects, each containing an `id`, `content`, and `render` function.
* @param {Array} props.data - An array of data objects, each representing a row.
* @returns {JSX.Element} The rendered table component.
*/
const DataTable = ({
shouldRender = true,
headers = [],
data = [],
config = {
emptyView: "No data",
tooltipContent: null,
onRowClick: () => {},
},
}) => {
const theme = useTheme();
if (!shouldRender) {
return <SkeletonLayout />;
}
if ((headers?.length ?? 0) === 0) {
return "No data";
}
return (
<TableContainer component={Paper}>
<Table
stickyHeader
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(8),
},
"& :is(th)": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
},
}}
>
<TableHead>
<TableRow>
{headers.map((header, index) => (
<TableCell
key={header.id}
align={index === 0 ? "left" : "center"}
>
{header.content}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{(data?.length ?? 0) === 0 ? (
<TableRow>
<TableCell
colSpan={headers.length}
align="center"
>
{config.emptyView}
</TableCell>
</TableRow>
) : (
data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<Tooltip
key={key}
followCursor
enterDelay={500}
enterNextDelay={500}
title={
typeof config.tooltipContent === "function"
? config.tooltipContent(row)
: config.tooltipContent
}
slotProps={{
tooltip: {
sx: {
background: "unset",
},
},
popper: {
modifiers: [
{
name: "offset",
options: {
offset: ({ popper }) => {
return [popper.width / 2 + 20, -popper.height / 8];
},
},
},
],
},
}}
>
<TableRow
sx={config?.rowSX ?? {}}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
sx={header.getCellSx ? header.getCellSx(row) : {}}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
</Tooltip>
);
})
)}
</TableBody>
</Table>
</TableContainer>
);
};
DataTable.propTypes = {
shouldRender: PropTypes.bool,
headers: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
content: PropTypes.node.isRequired,
render: PropTypes.func.isRequired,
})
).isRequired,
data: PropTypes.array,
config: PropTypes.shape({
onRowClick: PropTypes.func,
rowSX: PropTypes.object,
emptyView: PropTypes.node,
}),
};
export default DataTable;
@@ -1,14 +0,0 @@
import { Skeleton } from "@mui/material";
const TableSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height="80%"
flex={1}
/>
);
};
export default TableSkeleton;
-262
View File
@@ -1,262 +0,0 @@
import { networkService } from "../../main";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const initialState = {
isLoading: false,
authToken: "",
user: "",
success: null,
msg: null,
};
export const register = createAsyncThunk(
"auth/register",
async ({ user, token }, thunkApi) => {
try {
const res = await networkService.registerUser(user, token);
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
try {
const res = await networkService.loginUser(form);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
});
export const update = createAsyncThunk("auth/update", async (data, thunkApi) => {
const { localData: form } = data;
try {
const fd = new FormData();
form.firstName && fd.append("firstName", form.firstName);
form.lastName && fd.append("lastName", form.lastName);
form.password && fd.append("password", form.password);
form.newPassword && fd.append("newPassword", form.newPassword);
if (form.file && form.file !== "") {
const imageResult = await axios.get(form.file, {
responseType: "blob",
});
fd.append("profileImage", imageResult.data);
}
form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage);
const res = await networkService.updateUser({
form: fd,
});
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
});
export const deleteUser = createAsyncThunk("auth/delete", async (_, thunkApi) => {
try {
const res = await networkService.deleteUser();
return res.data;
} catch (error) {
if (error.response && error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
});
export const forgotPassword = createAsyncThunk(
"auth/forgotPassword",
async (form, thunkApi) => {
try {
const res = await networkService.forgotPassword(form);
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
export const setNewPassword = createAsyncThunk(
"auth/setNewPassword",
async (data, thunkApi) => {
const { token, form } = data;
try {
await networkService.validateRecoveryToken({ recoveryToken: token });
const res = await networkService.setNewPassword({
recoveryToken: token,
form: form,
});
return res.data;
} catch (error) {
if (error.response.data) {
return thunkApi.rejectWithValue(error.response.data);
}
const payload = {
status: false,
msg: error.message ? error.message : "Unknown error",
};
return thunkApi.rejectWithValue(payload);
}
}
);
const handleAuthFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.authToken = action.payload.data.token;
state.user = action.payload.data.user;
};
const handleAuthRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to login or register";
};
const handleUpdateFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.user = action.payload.data;
};
const handleUpdateRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to update profile data.";
};
const handleDeleteFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
};
const handleDeleteRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to delete account.";
};
const handleForgotFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
};
const handleForgotRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to send reset instructions.";
};
const handleNewPasswordRejected = (state, action) => {
state.isLoading = false;
state.success = false;
state.msg = action.payload ? action.payload.msg : "Failed to reset password.";
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
clearAuthState: (state) => {
state.authToken = "";
state.user = "";
state.isLoading = false;
state.success = true;
state.msg = "Logged out successfully";
},
setAuthState: (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.authToken = action.payload.data.token;
state.user = action.payload.data.user;
},
setUser: (state, action) => {
state.user = action.payload;
},
},
extraReducers: (builder) => {
// Register thunk
builder
.addCase(register.pending, (state) => {
state.isLoading = true;
})
.addCase(register.fulfilled, handleAuthFulfilled)
.addCase(register.rejected, handleAuthRejected);
// Login thunk
builder
.addCase(login.pending, (state) => {
state.isLoading = true;
})
.addCase(login.fulfilled, handleAuthFulfilled)
.addCase(login.rejected, handleAuthRejected);
// Update thunk
builder
.addCase(update.pending, (state) => {
state.isLoading = true;
})
.addCase(update.fulfilled, handleUpdateFulfilled)
.addCase(update.rejected, handleUpdateRejected);
// Delete thunk
builder
.addCase(deleteUser.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteUser.fulfilled, handleDeleteFulfilled)
.addCase(deleteUser.rejected, handleDeleteRejected);
// Forgot password thunk
builder
.addCase(forgotPassword.pending, (state) => {
state.isLoading = true;
})
.addCase(forgotPassword.fulfilled, handleForgotFulfilled)
.addCase(forgotPassword.rejected, handleForgotRejected);
// Set new password thunk
builder
.addCase(setNewPassword.pending, (state) => {
state.isLoading = true;
})
.addCase(setNewPassword.fulfilled, handleAuthFulfilled)
.addCase(setNewPassword.rejected, handleNewPasswordRejected);
},
});
export default authSlice.reducer;
export const { clearAuthState, setAuthState, setUser } = authSlice.actions;
+53
View File
@@ -0,0 +1,53 @@
import { createSlice, type PayloadAction } from "@reduxjs/toolkit";
import type { User } from "@/Types/User";
interface AuthState {
isLoading: boolean;
authToken: string;
user: User | null;
success: boolean | null;
msg: string | null;
}
const initialState: AuthState = {
isLoading: false,
authToken: "",
user: null,
success: null,
msg: null,
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
clearAuthState: (state) => {
state.authToken = "";
state.user = null;
state.isLoading = false;
state.success = true;
state.msg = "Logged out successfully";
},
setAuthState: (
state,
action: PayloadAction<{
success: boolean;
msg: string;
data: { token: string; user: User };
}>
) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.authToken = action.payload.data.token;
state.user = action.payload.data.user;
},
setUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
},
},
});
export type { AuthState };
export default authSlice.reducer;
export const { clearAuthState, setAuthState, setUser } = authSlice.actions;
-2
View File
@@ -4,8 +4,6 @@ const initialMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matche
? "dark"
: "light";
// Initial state for UI settings.
// Add more settings as needed (e.g., theme preferences, user settings)
const initialState = {
monitors: {
rowsPerPage: 10,
+34
View File
@@ -179,3 +179,37 @@ export const useDelete = <R = any>() => {
return { deleteFn, loading, error };
};
export const useLazyGet = <R = any>() => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { toastError } = useToast();
const getFn = async (
endpoint: string,
config?: AxiosRequestConfig
): Promise<ApiResponse<R> | null> => {
setLoading(true);
setError(null);
try {
const res = await get<ApiResponse<R>>(endpoint, {
...config,
headers: {
...config?.headers,
},
});
return res.data;
} catch (err: any) {
console.error(err);
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
toastError(errMsg);
setError(errMsg);
return null;
} finally {
setLoading(false);
}
};
return { get: getFn, loading, error };
};
-193
View File
@@ -1,193 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main.jsx";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useFetchChecksTeam = ({
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled = true,
updateTrigger,
}) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchChecks = async () => {
if (!enabled) {
return;
}
const config = {
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
};
try {
setIsLoading(true);
const res = await networkService.getChecksByTeam(config);
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchChecks();
}, [
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled,
updateTrigger,
]);
return [checks, checksCount, isLoading, networkError];
};
const useFetchChecksByMonitor = ({
monitorId,
type,
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled = true,
updateTrigger,
}) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchChecks = async () => {
if (!enabled || !type) {
return;
}
const config = {
monitorId,
type,
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
};
try {
setIsLoading(true);
const res = await networkService.getChecksByMonitor(config);
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchChecks();
}, [
monitorId,
type,
status,
sortOrder,
limit,
dateRange,
filter,
ack,
page,
rowsPerPage,
enabled,
updateTrigger,
]);
return [checks, checksCount, isLoading, networkError];
};
const useFetchChecksSummaryByTeamId = ({ dateRange, updateTrigger } = {}) => {
const [summary, setSummary] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchSummary = async () => {
try {
setIsLoading(true);
const res = await networkService.getChecksAndSummaryByTeamId({ dateRange });
setSummary(res.data.data);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchSummary();
}, [dateRange, updateTrigger]);
return [summary, isLoading, networkError];
};
const useResolveIncident = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const resolveIncident = async (checkId, setUpdateTrigger) => {
try {
setIsLoading(true);
await networkService.updateCheckStatus({
checkId,
ack: true,
});
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveOne") });
} finally {
setIsLoading(false);
}
};
return [resolveIncident, isLoading];
};
export {
useFetchChecksByMonitor,
useFetchChecksTeam,
useFetchChecksSummaryByTeamId,
useResolveIncident,
};
-65
View File
@@ -1,65 +0,0 @@
import { useState } from "react";
import { networkService } from "../main.jsx";
import { useTranslation } from "react-i18next";
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
const useGetInviteToken = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const [token, setToken] = useState(undefined);
const clearToken = () => {
setToken(undefined);
};
const fetchToken = async (email, role) => {
const response = await networkService.requestInvitationToken({ email, role });
const token = response?.data?.data?.token;
if (typeof token === "undefined") {
throw new Error(t("inviteNoTokenFound"));
}
return token;
};
const getInviteToken = async ({ email, role }) => {
try {
setIsLoading(true);
const token = await fetchToken(email, role);
let inviteLink = token;
if (typeof CLIENT_HOST !== "undefined") {
inviteLink = `${CLIENT_HOST}/register/${token}`;
}
setToken(inviteLink);
return token;
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
const addTeamMember = async (formData, role) => {
try {
setIsLoading(true);
const token = await fetchToken(formData.email, role);
const toSubmit = {
...formData,
inviteToken: token,
};
delete toSubmit.confirm;
const responseRegister = await networkService.registerUser(toSubmit);
return responseRegister;
} catch (error) {
setError(error);
throw error;
} finally {
setIsLoading(false);
}
};
return [getInviteToken, clearToken, isLoading, error, token, addTeamMember];
};
export { useGetInviteToken };
-111
View File
@@ -1,111 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main.jsx";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useFetchLogs = () => {
const { t } = useTranslation();
const [logs, setLogs] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchLogs = async () => {
try {
setIsLoading(true);
const response = await networkService.getLogs();
setLogs(response.data.data);
} catch (error) {
setError(error);
createToast({
message: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchLogs();
}, [t]);
return [logs, isLoading, error];
};
const useFetchQueueData = (trigger) => {
const [jobs, setJobs] = useState(undefined);
const [metrics, setMetrics] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchJobs = async () => {
try {
setIsLoading(true);
const response = await networkService.getQueueData();
if (response.status === 200) {
setJobs(response.data.data.jobs);
setMetrics(response.data.data.metrics);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchJobs();
}, [trigger]);
return [jobs, metrics, isLoading, error];
};
const useFlushQueue = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const flushQueue = async (trigger, setTrigger) => {
try {
setIsLoading(true);
await networkService.flushQueue();
createToast({
body: "Queue flushed",
});
} catch (error) {
setError(error);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
setTrigger(!trigger);
}
};
return [flushQueue, isLoading, error];
};
const useFetchDiagnostics = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const [diagnostics, setDiagnostics] = useState(undefined);
const fetchDiagnostics = async () => {
try {
setIsLoading(true);
const response = await networkService.getDiagnostics();
setDiagnostics(response.data.data);
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchDiagnostics();
}, []);
return [diagnostics, fetchDiagnostics, isLoading, error];
};
export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics };
-170
View File
@@ -1,170 +0,0 @@
import { useEffect, useState } from "react";
import { networkService } from "../main.jsx";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
types,
filter,
});
if (res?.data?.data) {
setMonitors(res.data.data);
}
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [types, filter, updateTrigger]);
return [monitors, isLoading, networkError];
};
export const useDeleteMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteMonitor = async (monitorId, successCallback = () => {}) => {
try {
setIsLoading(true);
await networkService.deleteMonitorById({ monitorId });
successCallback();
createToast({ body: t("monitorDeleted") });
} catch (error) {
createToast({ body: t("failedDeleteMonitor") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitor, isLoading];
};
export const usePauseMonitor = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const pauseMonitor = async (monitorId, successCallback = () => {}) => {
try {
setIsLoading(true);
const res = await networkService.pauseMonitorById({ monitorId });
successCallback();
if (res.data.data.isActive === false) {
createToast({ body: t("monitorPaused") });
} else {
createToast({ body: t("monitorResumed") });
}
} catch (error) {
setError(error.message);
createToast({ body: t("failedPauseMonitor") });
} finally {
setIsLoading(false);
}
};
return [pauseMonitor, isLoading, error];
};
export const useAddDemoMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const addDemoMonitors = async () => {
try {
setIsLoading(true);
await networkService.addDemoMonitors();
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
} catch (error) {
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
} finally {
setIsLoading(false);
}
};
return [addDemoMonitors, isLoading];
};
export const useDeleteAllMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const deleteAllMonitors = async () => {
try {
setIsLoading(true);
await networkService.deleteAllMonitors();
createToast({ body: t("settingsMonitorsDeleted") });
} catch (error) {
createToast({ body: t("settingsFailedToDeleteMonitors") });
} finally {
setIsLoading(false);
}
};
return [deleteAllMonitors, isLoading];
};
export const useDeleteMonitorStats = () => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState(false);
const deleteMonitorStats = async () => {
setIsLoading(true);
try {
await networkService.deleteChecksByTeamId();
createToast({ body: t("settingsStatsCleared") });
} catch (error) {
createToast({ body: t("settingsFailedToClearStats") });
} finally {
setIsLoading(false);
}
};
return [deleteMonitorStats, isLoading];
};
export const useCreateBulkMonitors = () => {
const [isLoading, setIsLoading] = useState(false);
const createBulkMonitors = async (file, user) => {
setIsLoading(true);
const formData = new FormData();
formData.append("csvFile", file);
try {
const response = await networkService.createBulkMonitors(formData);
return [true, response.data, null];
} catch (err) {
const errorMessage = err?.response?.data?.msg || err.message;
return [false, null, errorMessage];
} finally {
setIsLoading(false);
}
};
return [createBulkMonitors, isLoading];
};
export const useFetchJson = () => {
const [isLoading, setIsLoading] = useState(false);
const fetchJson = async () => {
try {
setIsLoading(true);
const res = await networkService.fetchJson();
createToast({ body: "JSON fetched successfully" });
return res?.data?.data ?? [];
} catch (error) {
createToast({ body: "Failed to create monitor." });
} finally {
setIsLoading(false);
}
};
return [fetchJson, isLoading];
};
-73
View File
@@ -1,73 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main.jsx";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchSettings = async () => {
setIsLoading(true);
try {
const response = await networkService.getAppSettings();
setSettingsData(response?.data?.data);
setIsApiKeySet(response?.data?.data?.pagespeedKeySet);
setIsEmailPasswordSet(response?.data?.data?.emailPasswordSet);
} catch (error) {
createToast({ body: "Failed to fetch settings" });
setError(error);
} finally {
setIsLoading(false);
}
};
fetchSettings();
}, [setSettingsData]);
return [isLoading, error];
};
const useSaveSettings = ({
setSettingsData,
setIsApiKeySet,
setApiKeyHasBeenReset,
setIsEmailPasswordSet,
setEmailPasswordHasBeenReset,
}) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
const saveSettings = async (settings) => {
setIsLoading(true);
try {
const settingsResponse = await networkService.updateAppSettings({ settings });
// if (settings.checkTTL) {
// await networkService.updateChecksTTL({
// ttl: settings.checkTTL,
// });
// }
setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet);
setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet);
if (settingsResponse.data.data.pagespeedKeySet === true) {
setApiKeyHasBeenReset(false);
}
if (settingsResponse.data.data.emailPasswordSet === true) {
setEmailPasswordHasBeenReset(false);
}
setSettingsData(settingsResponse.data.data);
createToast({ body: t("settingsSuccessSaved") });
} catch (error) {
createToast({ body: t("settingsFailedToSave") });
setError(error);
} finally {
setIsLoading(false);
}
};
return [isLoading, error, saveSettings];
};
export { useFetchSettings, useSaveSettings };
-17
View File
@@ -1,17 +0,0 @@
import { useSelector } from "react-redux";
const useIsAdmin = () => {
const { user } = useSelector((state) => state.auth);
const isAdmin =
(user?.role?.includes("admin") ?? false) ||
(user?.role?.includes("superadmin") ?? false);
return isAdmin;
};
const useIsSuperAdmin = () => {
const { user } = useSelector((state) => state.auth);
const isSuperAdmin = user?.role?.includes("superadmin");
return isSuperAdmin;
};
export { useIsAdmin, useIsSuperAdmin };
+18
View File
@@ -0,0 +1,18 @@
import type { RootState } from "@/Types/state";
import { useSelector } from "react-redux";
const useIsAdmin = (): boolean => {
const { user } = useSelector((state: RootState) => state.auth);
const isAdmin =
(user?.role?.includes("admin") ?? false) ||
(user?.role?.includes("superadmin") ?? false);
return isAdmin;
};
const useIsSuperAdmin = (): boolean => {
const { user } = useSelector((state: RootState) => state.auth);
const isSuperAdmin = user?.role?.includes("superadmin") ?? false;
return isSuperAdmin;
};
export { useIsAdmin, useIsSuperAdmin };
-72
View File
@@ -1,72 +0,0 @@
import { useCallback } from "react";
import { useTheme } from "@mui/material";
const useMonitorUtils = () => {
const getMonitorWithPercentage = useCallback((monitor, theme) => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor?.uptimePercentage !== undefined) {
uptimePercentage =
monitor?.uptimePercentage === 0
? "0"
: (monitor?.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor?.uptimePercentage < 0.25
? theme.palette.error.main
: monitor?.uptimePercentage < 0.5
? theme.palette.warning.main
: monitor?.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
return {
...monitor,
percentage: uptimePercentage,
percentageColor,
monitor: monitor,
};
}, []);
const determineState = useCallback((monitor) => {
if (typeof monitor === "undefined") return "pending";
if (monitor?.isActive === false) return "paused";
if (monitor?.status === undefined) return "pending";
return monitor?.status == true ? "up" : "down";
}, []);
const theme = useTheme();
const statusColor = {
up: theme.palette.success.lowContrast,
down: theme.palette.error.lowContrast,
paused: theme.palette.warning.lowContrast,
pending: theme.palette.warning.lowContrast,
};
const statusToTheme = {
up: "success",
down: "error",
paused: "warning",
pending: "secondary",
"cannot resolve": "tertiary",
};
const pagespeedStatusMsg = {
up: "Live (collecting data)",
down: "Inactive",
paused: "Paused",
};
return {
getMonitorWithPercentage,
determineState,
statusColor,
statusToTheme,
pagespeedStatusMsg,
};
};
export { useMonitorUtils };
-58
View File
@@ -1,58 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import { createToast } from "../Utils/toastUtils.jsx";
import { networkService } from "../main.jsx";
import { useTranslation } from "react-i18next";
const useGetNotificationsByTeamId = (updateTrigger) => {
const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { t } = useTranslation();
const getNotifications = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getNotificationsByTeamId();
setNotifications(response?.data?.data ?? []);
} catch (error) {
setError(error);
createToast({
body: t("notifications.fetch.failed"),
});
} finally {
setIsLoading(false);
}
}, [t]);
useEffect(() => {
getNotifications();
}, [getNotifications, updateTrigger]);
return [notifications, isLoading, error];
};
const useTestAllNotifications = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
const testAllNotifications = async ({ monitorId }) => {
try {
setIsLoading(true);
await networkService.testAllNotifications({ monitorId });
createToast({
body: t("notifications.test.success"),
});
} catch (error) {
createToast({
body: error.response?.data?.msg || error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
};
return [testAllNotifications, isLoading, error];
};
export { useGetNotificationsByTeamId, useTestAllNotifications };
-60
View File
@@ -1,60 +0,0 @@
import { useState } from "react";
import { networkService } from "../main.jsx";
import { useSelector } from "react-redux";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
const useSendTestEmail = () => {
const [isSending, setIsSending] = useState(false);
const [error, setError] = useState(null);
const user = useSelector((state) => state.auth.user);
const { t } = useTranslation();
/**
* Send a test email with optional email configuration
* @param {Object} emailConfig - Optional email configuration parameters
*/
const sendTestEmail = async (emailConfig = null) => {
try {
setIsSending(true);
setError(null);
// Send the test email with or without configuration
const response = await networkService.sendTestEmail({
to: user.email,
emailConfig,
});
if (typeof response?.data?.data?.messageId !== "undefined") {
createToast({
body: t("settingsTestEmailSuccess", "Test email sent successfully"),
});
} else {
throw new Error(
response?.data?.error ||
t("settingsTestEmailFailed", "Failed to send test email")
);
}
} catch (error) {
createToast({
body: t("failedToSendEmail"),
});
setError(error);
createToast({
body: t(
"settingsTestEmailFailedWithReason",
"Failed to send test email: {{reason}}",
{
reason: error.message || t("settingsTestEmailUnknownError", "Unknown error"),
}
),
variant: "error",
});
} finally {
setIsSending(false);
}
};
return [isSending, error, sendTestEmail];
};
export { useSendTestEmail };
-81
View File
@@ -1,81 +0,0 @@
import { useEffect, useState, useCallback } from "react";
import { networkService } from "../main.jsx";
import { createToast } from "../Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
export const useGetUser = (userId) => {
const [user, setUser] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUser = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getUserById({ userId });
setUser(response?.data?.data);
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
}, [userId]);
useEffect(() => {
if (userId) {
fetchUser();
}
}, [userId, fetchUser]);
return [user, isLoading, error];
};
export const useEditUser = (userId) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const editUser = useCallback(
async (user) => {
try {
setIsLoading(true);
await networkService.editUser({ userId, user });
createToast({
body: t("editUserPage.toast.successUserUpdate"),
});
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
},
[userId, t]
);
const changePassword = useCallback(
async (passwordForm) => {
try {
setIsLoading(true);
await networkService.changePasswordByAdmin({ userId, passwordForm });
createToast({
body: t("teamPanel.changeTeamPassword.success"),
});
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
},
[userId, t]
);
return [editUser, isLoading, error, changePassword];
};
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { Box, Typography, Button, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { networkService } from "@/Utils/NetworkService.js";
import { get } from "@/Utils/ApiClient";
import Alert from "@/Components/v1/Alert/index.jsx";
import { createToast } from "@/Utils/toastUtils.jsx";
import { useTranslation } from "react-i18next";
@@ -24,7 +24,7 @@ const ServerUnreachable = () => {
try {
// Try to connect to the backend with a simple API call
// We'll use any lightweight endpoint that doesn't require authentication
await networkService.axiosInstance.get("/health", { timeout: 5000 });
await get("/health", { timeout: 5000 });
// If successful, show toast and navigate to login page
createToast({
+32 -24
View File
@@ -12,7 +12,8 @@ import { PropTypes } from "prop-types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import { useSendTestEmail } from "@/Hooks/useSendTestEmail.js";
import { usePost } from "@/Hooks/UseApi";
import { useSelector } from "react-redux";
import { createToast } from "@/Utils/toastUtils.jsx";
const SettingsEmail = ({
@@ -48,7 +49,8 @@ const SettingsEmail = ({
const [password, setPassword] = useState("");
// Network
const [isSending, , sendTestEmail] = useSendTestEmail(); // Using empty placeholder for unused error variable
const { post: sendTestEmailFn, loading: isSending } = usePost();
const user = useSelector((state) => state.auth.user);
// Handlers
const handlePasswordChange = (e) => {
@@ -63,28 +65,12 @@ const SettingsEmail = ({
* Handle sending test email with current form values
*/
const handleSendTestEmail = () => {
// Collect current form values
const emailConfig = {
systemEmailHost,
systemEmailPort,
systemEmailSecure,
systemEmailPool,
systemEmailUser,
systemEmailAddress,
systemEmailPassword: password || systemEmailPassword,
systemEmailTLSServername,
systemEmailConnectionHost,
systemEmailIgnoreTLS,
systemEmailRequireTLS,
systemEmailRejectUnauthorized,
};
// Basic validation
if (
!emailConfig.systemEmailHost ||
!emailConfig.systemEmailPort ||
!emailConfig.systemEmailAddress ||
!emailConfig.systemEmailPassword
!systemEmailHost ||
!systemEmailPort ||
!systemEmailAddress ||
!(password || systemEmailPassword)
) {
createToast({
body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"),
@@ -93,8 +79,22 @@ const SettingsEmail = ({
return;
}
// Send test email with current form values
sendTestEmail(emailConfig);
// Send test email - only include optional fields if they have values
sendTestEmailFn("/settings/test-email", {
to: user.email,
systemEmailHost,
systemEmailPort,
systemEmailAddress,
systemEmailPassword: password || systemEmailPassword,
systemEmailSecure,
systemEmailPool,
systemEmailIgnoreTLS,
systemEmailRequireTLS,
systemEmailRejectUnauthorized,
...(systemEmailUser && { systemEmailUser }),
...(systemEmailTLSServername && { systemEmailTLSServername }),
...(systemEmailConnectionHost && { systemEmailConnectionHost }),
});
};
if (!isAdmin) {
@@ -297,6 +297,14 @@ const SettingsEmail = ({
</Box>
</Box>
<pre>
{JSON.stringify({
systemEmailHost,
systemEmailAddress,
systemEmailPassword,
})}
</pre>
<Box>
{systemEmailHost &&
systemEmailPort &&
+38 -28
View File
@@ -13,7 +13,7 @@ import SettingsExport from "./SettingsExport.jsx";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "@/Validation/validation.js";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
@@ -26,14 +26,8 @@ import {
} from "@/Features/UI/uiSlice.js";
import SettingsStats from "./SettingsStats.jsx";
import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks.js";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import {
useAddDemoMonitors,
useDeleteAllMonitors,
useDeleteMonitorStats,
useFetchJson,
} from "@/Hooks/monitorHooks.js";
import { useGet, usePost, useDelete, useLazyGet, usePut } from "@/Hooks/UseApi";
// Constants
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
@@ -58,24 +52,39 @@ const Settings = () => {
const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false);
// Network
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
setIsApiKeySet,
setIsEmailPasswordSet,
});
const { data: fetchedSettings, isLoading: isSettingsLoading } = useGet("/settings");
const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors();
useEffect(() => {
if (fetchedSettings) {
setSettingsData(fetchedSettings);
setIsApiKeySet(fetchedSettings?.pagespeedKeySet);
setIsEmailPasswordSet(fetchedSettings?.emailPasswordSet);
}
}, [fetchedSettings]);
const [isSaving, saveError, saveSettings] = useSaveSettings({
setSettingsData,
setIsApiKeySet,
setApiKeyHasBeenReset,
setIsEmailPasswordSet,
setEmailPasswordHasBeenReset,
});
const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors();
const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats();
const [fetchJson, isFetchingJson] = useFetchJson();
const { put: saveSettingsFn, loading: isSaving } = usePut();
const saveSettings = async (settings) => {
const response = await saveSettingsFn("/settings", { settings });
if (response?.data) {
const data = response.data;
setIsApiKeySet(data.pagespeedKeySet);
setIsEmailPasswordSet(data.emailPasswordSet);
if (data.pagespeedKeySet === true) {
setApiKeyHasBeenReset(false);
}
if (data.emailPasswordSet === true) {
setEmailPasswordHasBeenReset(false);
}
setSettingsData(data);
}
};
// New API hooks to replace monitorHooks
const { post: postDemoMonitors, loading: isAddingDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitorsFn, loading: isDeletingMonitors } = useDelete();
const { deleteFn: deleteMonitorStatsFn, loading: isDeletingMonitorStats } = useDelete();
const { get: fetchJson, loading: isFetchingJson } = useLazyGet();
// Setup
const isAdmin = useIsAdmin();
@@ -130,22 +139,23 @@ const Settings = () => {
}
if (name === "deleteStats") {
await deleteMonitorStats();
await deleteMonitorStatsFn("/checks/team");
return;
}
if (name === "demo") {
await addDemoMonitors();
await postDemoMonitors("/monitors/demo", {});
return;
}
if (name === "deleteMonitors") {
await deleteAllMonitors();
await deleteAllMonitorsFn("/monitors/");
return;
}
if (name === "export") {
const json = await fetchJson();
const res = await fetchJson("/monitors/export/json");
const json = res?.data ?? [];
if (!json || json.length === 0) {
return;
}
@@ -1,73 +0,0 @@
import { useTheme } from "@emotion/react";
import { useState, useRef } from "react";
import { Button, Typography } from "@mui/material";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const UploadFile = ({ onFileSelect }) => {
// Changed prop to onFileSelect
const theme = useTheme();
const [file, setFile] = useState();
const [error, setError] = useState("");
const inputRef = useRef();
const { t } = useTranslation();
const handleSelectFile = () => {
inputRef.current.click();
};
const handleFileChange = (e) => {
setError("");
const selectedFile = e.target.files[0];
// Basic file validation
if (!selectedFile) return;
if (!selectedFile.name.toLowerCase().endsWith(".csv")) {
setError(t("bulkImport.invalidFileType"));
return;
}
setFile(selectedFile);
onFileSelect(selectedFile); // Pass the file directly to parent
};
return (
<div>
<input
ref={inputRef}
type="file"
accept=".csv"
style={{ display: "none" }}
onChange={handleFileChange}
/>
<Typography
component="h2"
mb={theme.spacing(1.5)}
sx={{ wordBreak: "break-all" }}
>
{file?.name || t("bulkImport.noFileSelected")}
</Typography>
<Typography
component="div"
mb={theme.spacing(1.5)}
color={theme.palette.error.main}
>
{error}
</Typography>
<Button
variant="contained"
color="accent"
onClick={handleSelectFile}
>
{t("bulkImport.selectFile")}
</Button>
</div>
);
};
UploadFile.prototype = {
onFileSelect: PropTypes.func.isRequired,
};
export default UploadFile;
@@ -1,117 +0,0 @@
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { useState } from "react";
// MUI
import { Box, Stack, Typography, Button, Link } from "@mui/material";
//Components
import { createToast } from "../../../Utils/toastUtils.jsx";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import UploadFile from "./Upload.jsx";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks.js";
const BulkImport = () => {
const theme = useTheme();
const { user } = useSelector((state) => state.auth);
const navigate = useNavigate();
const { t } = useTranslation();
const [selectedFile, setSelectedFile] = useState(null);
const crumbs = [
{ name: t("uptime"), path: "/uptime" },
{ name: t("bulkImport.title"), path: `/uptime/bulk-import` },
];
const [createBulkMonitors, hookLoading] = useCreateBulkMonitors();
const handleSubmit = async () => {
if (!selectedFile) {
createToast({ body: t("bulkImport.noFileSelected") });
return;
}
const [success, data, error] = await createBulkMonitors(selectedFile, user);
if (success) {
// You can use `data` here if needed
createToast({ body: t("bulkImport.uploadSuccess") });
navigate("/uptime");
} else {
createToast({ body: error || t("bulkImport.uploadFailed") });
}
};
return (
<Box className="bulk-import-monitor">
<Breadcrumbs list={crumbs} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
>
<Typography
component="h1"
variant="h1"
>
{t("bulkImport.title")}
</Typography>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("bulkImport.selectFileTips")}
</Typography>
<Typography component="p">
<Trans
i18nKey="bulkImport.selectFileDescription"
components={{
template: (
<Link
color="info"
download
href="/bulk_import_monitors_template.csv"
/>
),
sample: (
<Link
color="info"
download
href="/bulk_import_monitors_sample.csv"
/>
),
}}
/>
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<UploadFile onFileSelect={(file) => setSelectedFile(file)} />
</Stack>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={handleSubmit}
disabled={hookLoading}
loading={hookLoading}
>
{t("submit")}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default BulkImport;
+1 -5
View File
@@ -53,7 +53,6 @@ import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create";
import ProtectedRoute from "../Components/v1/ProtectedRoute";
import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute";
import withAdminCheck from "@/Components/v1/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport/index.jsx";
import Logs from "../Pages/Logs";
import CreateMonitor from "@/Pages/CreateMonitor";
@@ -87,10 +86,7 @@ const Routes = () => {
}
/>
<Route
path="/uptime/bulk-import"
element={<BulkImport />}
/>
<Route path="/uptime/bulk-import" />
<Route
path="/uptime/create"
File diff suppressed because it is too large. Load diff
@@ -1,16 +0,0 @@
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { setNetworkService } from "./NetworkService";
import NetworkService, { networkService } from "./NetworkService";
import { store } from "../store";
const NetworkServiceProvider = ({ children }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
if (!networkService) {
setNetworkService(new NetworkService(store, dispatch, navigate));
}
return children;
};
export default NetworkServiceProvider;
+5 -10
View File
@@ -3,17 +3,14 @@ import App from "./App.jsx";
import "./index.css";
import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import { persistor, store } from "./store";
import { persistor, store } from "./store.js";
import { PersistGate } from "redux-persist/integration/react";
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
import { networkService } from "./Utils/NetworkService";
export { networkService };
import I18nLoader from "./Components/v1/I18nLoader";
import { initApiClient } from "./Utils/ApiClient";
import I18nLoader from "./Components/v1/I18nLoader/index.jsx";
import { initApiClient } from "./Utils/ApiClient.js";
initApiClient(store);
ReactDOM.createRoot(document.getElementById("root")).render(
ReactDOM.createRoot(document.getElementById("root")!).render(
<Provider store={store}>
<PersistGate
loading={null}
@@ -21,9 +18,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
>
<I18nLoader />
<Router>
<NetworkServiceProvider>
<App />
</NetworkServiceProvider>
<App />
</Router>
</PersistGate>
</Provider>