add components

This commit is contained in:
Alex Holliday committed 2025-02-05 14:59:08 -08:00
1 parent a5ba103148
commit e6947be2f1
10 files changed
+653 -10

No files matched your search

@@ -11,6 +11,7 @@ const ChartBox = ({
justifyContent = "space-between",
Legend,
borderRadiusRight = 4,
sx,
}) => {
const theme = useTheme();
return (
@@ -70,8 +71,8 @@ const ChartBox = ({
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
{icon && <IconBox>{icon}</IconBox>}
{header && <Typography component="h2">{header}</Typography>}
</Stack>
{children}
</Stack>
@@ -84,7 +85,7 @@ export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
icon: PropTypes.node,
header: PropTypes.string,
height: PropTypes.string,
};
+3
View File
@@ -14,6 +14,7 @@ const Image = ({
maxWidth = "auto",
maxHeight = "auto",
base64,
sx,
}) => {
if (shouldRender === false) {
return null;
@@ -36,6 +37,7 @@ const Image = ({
maxHeight={maxHeight}
width={width}
height={height}
sx={{ ...sx }}
/>
);
};
@@ -49,6 +51,7 @@ Image.propTypes = {
maxWidth: PropTypes.string,
maxHeight: PropTypes.string,
base64: PropTypes.string,
sx: PropTypes.object,
};
export default Image;
+27 -6
View File
@@ -1,4 +1,5 @@
import { Box, Typography } from "@mui/material";
import { Stack, Typography } from "@mui/material";
import Image from "../Image";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
@@ -29,7 +30,15 @@ import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
* @returns {React.ReactElement} A styled box containing the statistic
*/
const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) => {
const StatBox = ({
img,
alt,
heading,
subHeading,
gradient = false,
status = "",
sx,
}) => {
const theme = useTheme();
const { statusToTheme } = useUtils();
const themeColor = statusToTheme[status];
@@ -70,7 +79,8 @@ const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) =>
};
return (
<Box
<Stack
direction="row"
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
/* TODO why are we using width and min width here? */
@@ -95,9 +105,20 @@ const StatBox = ({ heading, subHeading, gradient = false, status = "", sx }) =>
...sx,
}}
>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</Box>
{img && (
<Image
src={img}
height={"30px"}
width={"30px"}
alt={alt}
sx={{ marginRight: theme.spacing(8) }}
/>
)}
<Stack>
<Typography component="h2">{heading}</Typography>
<Typography>{subHeading}</Typography>
</Stack>
</Stack>
);
};
@@ -0,0 +1,71 @@
import { Stack, Typography, List, ListItem } from "@mui/material";
import { useTheme } from "@emotion/react";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import "/node_modules/flag-icons/css/flag-icons.min.css";
const BASE_BOX_PADDING_VERTICAL = 16;
const BASE_BOX_PADDING_HORIZONTAL = 8;
const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
const theme = useTheme();
const statusColor = {
up: theme.palette.success.main,
down: theme.palette.error.main,
};
return (
<Stack
direction="column"
gap={theme.spacing(2)}
width={width}
sx={{
padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
}}
>
<Stack
direction="row"
justifyContent={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[connectionStatus]} />
<Typography
variant="h1"
mb={theme.spacing(8)}
sx={{ alignSelf: "center" }}
>
{connectionStatus === "up" ? "Connected" : "No connection"}
</Typography>
</Stack>
<List>
{data.slice(Math.max(data.length - 5, 0)).map((dataPoint) => {
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
const flag = countryCode ? `fi fi-${countryCode}` : null;
return (
<ListItem key={Math.random()}>
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
{flag && <span className={flag} />}
<Typography variant="h2">{dataPoint?.city || "Unknown"}</Typography>
</Stack>
<Typography variant="p">{`Response time: ${Math.floor(dataPoint?.responseTime ?? 0)} ms`}</Typography>
<Typography variant="p">{`UPT burned: ${dataPoint.uptBurnt}`}</Typography>
<Typography variant="p">{`${dataPoint?.device?.manufacturer} ${dataPoint?.device?.model}`}</Typography>
</Stack>
</ListItem>
);
})}
</List>
</Stack>
);
};
export default DeviceTicker;
@@ -0,0 +1,169 @@
{
"id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
"name": "MapLibre",
"zoom": 0.861983335785597,
"pitch": 0,
"center": [17.6543171043124, 32.9541203267468],
"glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#121217"
},
"filter": ["all"],
"layout": {
"visibility": "visible"
},
"maxzoom": 24
},
{
"id": "coastline",
"type": "line",
"paint": {
"line-blur": 0.5,
"line-color": "#000000",
"line-width": {
"stops": [
[0, 2],
[6, 6],
[14, 9],
[22, 18]
]
}
},
"filter": ["all"],
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"minzoom": 0,
"source-layer": "countries"
},
{
"id": "countries-fill",
"type": "fill",
"paint": {
"fill-color": "#292929"
},
"filter": ["all"],
"layout": {
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"source-layer": "countries"
},
{
"id": "countries-boundary",
"type": "line",
"paint": {
"line-color": "#484848",
"line-width": {
"stops": [
[1, 1],
[6, 2],
[14, 6],
[22, 12]
]
},
"line-opacity": {
"stops": [
[3, 0.5],
[6, 1]
]
}
},
"layout": {
"line-cap": "round",
"line-join": "round",
"visibility": "visible"
},
"source": "maplibre",
"maxzoom": 24,
"source-layer": "countries"
},
{
"id": "countries-label",
"type": "symbol",
"paint": {
"text-color": "rgba(8, 37, 77, 1)",
"text-halo-blur": {
"stops": [
[2, 0.2],
[6, 0]
]
},
"text-halo-color": "rgba(255, 255, 255, 1)",
"text-halo-width": {
"stops": [
[2, 1],
[6, 1.6]
]
}
},
"filter": ["all"],
"layout": {
"text-font": ["Open Sans Semibold"],
"text-size": {
"stops": [
[2, 10],
[4, 12],
[6, 16]
]
},
"text-field": {
"stops": [
[2, "{ABBREV}"],
[4, "{NAME}"]
]
},
"visibility": "visible",
"text-max-width": 10,
"text-transform": {
"stops": [
[0, "uppercase"],
[2, "none"]
]
}
},
"source": "maplibre",
"maxzoom": 24,
"minzoom": 2,
"source-layer": "centroids"
},
{
"id": "data-dots",
"type": "circle",
"source": "data-dots",
"paint": {
"circle-radius": 3,
"circle-color": ["get", "color"],
"circle-opacity": 0.5
}
}
],
"bearing": 0,
"sources": {
"maplibre": {
"url": "https://demotiles.maplibre.org/tiles/tiles.json",
"type": "vector"
},
"data-dots": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
}
},
"version": 8,
"metadata": {
"maptiler:copyright": "This style was generated on MapTiler Cloud. Usage is governed by the license terms in https://github.com/maplibre/demotiles/blob/gh-pages/LICENSE",
"openmaptiles:version": "3.x"
}
}
@@ -0,0 +1,88 @@
import "maplibre-gl/dist/maplibre-gl.css";
import PropTypes from "prop-types";
import { useRef, useState, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import style from "./DistributedUptimeMapStyle.json";
import maplibregl from "maplibre-gl";
const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
const mapContainer = useRef(null);
const map = useRef(null);
const theme = useTheme();
const [mapLoaded, setMapLoaded] = useState(false);
const colorLookup = (avgResponseTime) => {
if (avgResponseTime <= 150) {
return "#00FF00"; // Green
} else if (avgResponseTime <= 250) {
return "#FFFF00"; // Yellow
} else {
return "#FF0000"; // Red
}
};
useEffect(() => {
if (mapContainer.current && !map.current) {
map.current = new maplibregl.Map({
container: mapContainer.current,
style,
center: [0, 20],
zoom: 0.8,
});
}
map.current.on("load", () => {
setMapLoaded(true);
});
return () => {
if (map.current) {
map.current.remove();
map.current = null;
}
};
}, []);
useEffect(() => {
if (map.current && checks?.length > 0) {
// Convert dots to GeoJSON
const geojson = {
type: "FeatureCollection",
features: checks.map((check) => {
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: [check.lng, check.lat],
},
properties: {
color: theme.palette.accent.main,
// color: colorLookup(check.avgResponseTime) || "blue", // Default to blue if no color specified
},
};
}),
};
// Update the source with new dots
const source = map.current.getSource("data-dots");
if (source) {
source.setData(geojson);
}
}
}, [checks, theme, mapLoaded]);
return (
<div
ref={mapContainer}
style={{
width: width,
height: height,
}}
/>
);
};
DistributedUptimeMap.propTypes = {
checks: PropTypes.array,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default DistributedUptimeMap;
@@ -0,0 +1,214 @@
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 { useState } from "react";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
const CustomToolTip = ({ active, payload, label }) => {
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.background.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.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", 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.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Response Time
</Typography>{" "}
<Typography 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]),
};
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
// Render nothing for the first tick
if (index === 0) return null;
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
const DistributedUptimeResponseChart = ({ checks }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
if (checks.length === 0) return null;
return (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<AreaChart
width="100%"
height="100%"
data={checks}
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.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
);
};
DistributedUptimeResponseChart.propTypes = {
checks: PropTypes.array,
};
export default DistributedUptimeResponseChart;
@@ -0,0 +1,31 @@
import { Stack, Typography, Box } from "@mui/material";
import SolanaLogo from "../../../../../assets/icons/solana_logo.svg?react";
import { useTheme } from "@mui/material/styles";
const Footer = () => {
const theme = useTheme();
return (
<Stack
justifyContent="space-between"
alignItems="center"
spacing={2}
>
<Typography variant="h2">Made with ❤️ by UpRock & Bluewave Labs</Typography>
<Stack
width="100%"
direction="row"
gap={theme.spacing(2)}
justifyContent="center"
alignItems="center"
>
<Typography variant="h2">Built on</Typography>
<SolanaLogo
width={15}
height={15}
/>
<Typography variant="h2">Solana</Typography>
</Stack>
</Stack>
);
};
export default Footer;
@@ -0,0 +1,15 @@
import { useState, useEffect } from "react";
const LastUpdate = ({ suffix, lastUpdateTime, trigger }) => {
const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
useEffect(() => {
setElapsedMs(lastUpdateTime);
const timer = setInterval(() => {
setElapsedMs((prev) => prev + 1000);
}, 1000);
return () => clearInterval(timer);
}, [lastUpdateTime, trigger]);
return `${Math.floor(elapsedMs / 1000)} ${suffix}`;
};
export default LastUpdate;
@@ -0,0 +1,30 @@
import { LinearProgress } from "@mui/material";
import { useState, useEffect } from "react";
const NextExpectedCheck = ({ lastUpdateTime, interval, trigger }) => {
const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
useEffect(() => {
setElapsedMs(lastUpdateTime);
const timer = setInterval(() => {
setElapsedMs((prev) => {
const newElapsedMs = prev + 100;
return newElapsedMs;
});
}, 100);
return () => clearInterval(timer);
}, [interval, trigger]);
return (
<LinearProgress
variant="determinate"
color="accent"
value={Math.min((elapsedMs / interval) * 100, 100)}
sx={{
transition: "width 1s linear", // Smooth transition over 1 second
}}
/>
);
};
export default NextExpectedCheck;