v1 cleanup
This commit is contained in:
1 parent
a20fdb423a
commit
28dc61e1a6
61 files changed
-4705
No files matched your search
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
{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 }}>
|
||||
•
|
||||
</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
|
||||
<Link
|
||||
href="https://uprock.com"
|
||||
color="inherit"
|
||||
sx={{ mx: 0.5 }}
|
||||
>
|
||||
UpRock
|
||||
</Link>
|
||||
&
|
||||
<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
|
||||
</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 }}
|
||||
>
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user