From e6947be2f1af21b81d8913f9ef50f21c7c3df0a7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 5 Feb 2025 14:59:08 -0800 Subject: [PATCH] add components --- .../src/Components/Charts/ChartBox/index.jsx | 9 +- Client/src/Components/Image/index.jsx | 3 + Client/src/Components/StatBox/index.jsx | 33 ++- .../Details/Components/DeviceTicker/index.jsx | 71 ++++++ .../DistributedUptimeMapStyle.json | 169 ++++++++++++++ .../Components/DistributedUptimeMap/index.jsx | 88 +++++++ .../DistributedUptimeResponseChart/index.jsx | 214 ++++++++++++++++++ .../Details/Components/Footer/index.jsx | 31 +++ .../Details/Components/LastUpdate/index.jsx | 15 ++ .../Components/NextExpectedCheck/index.jsx | 30 +++ 10 files changed, 653 insertions(+), 10 deletions(-) create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx diff --git a/Client/src/Components/Charts/ChartBox/index.jsx b/Client/src/Components/Charts/ChartBox/index.jsx index eed3efd0d..bd27d4775 100644 --- a/Client/src/Components/Charts/ChartBox/index.jsx +++ b/Client/src/Components/Charts/ChartBox/index.jsx @@ -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)} > - {icon} - {header} + {icon && {icon}} + {header && {header}} {children} @@ -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, }; diff --git a/Client/src/Components/Image/index.jsx b/Client/src/Components/Image/index.jsx index 778675f92..9304a4385 100644 --- a/Client/src/Components/Image/index.jsx +++ b/Client/src/Components/Image/index.jsx @@ -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; diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 4b1fd2454..58fdb7ebf 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -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 ( - ...sx, }} > - {heading} - {subHeading} - + {img && ( + {alt} + )} + + {heading} + {subHeading} + + ); }; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx new file mode 100644 index 000000000..22ef5596d --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx @@ -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 ( + + + + + + {connectionStatus === "up" ? "Connected" : "No connection"} + + + + {data.slice(Math.max(data.length - 5, 0)).map((dataPoint) => { + const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null; + const flag = countryCode ? `fi fi-${countryCode}` : null; + return ( + + + + {flag && } + {dataPoint?.city || "Unknown"} + + {`Response time: ${Math.floor(dataPoint?.responseTime ?? 0)} ms`} + {`UPT burned: ${dataPoint.uptBurnt}`} + {`${dataPoint?.device?.manufacturer} ${dataPoint?.device?.model}`} + + + ); + })} + + + ); +}; + +export default DeviceTicker; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json new file mode 100644 index 000000000..d8ee81d5b --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json @@ -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" + } +} \ No newline at end of file diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx new file mode 100644 index 000000000..e7c0c961a --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx @@ -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 ( +
+ ); +}; + +DistributedUptimeMap.propTypes = { + checks: PropTypes.array, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), +}; + +export default DistributedUptimeMap; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx new file mode 100644 index 000000000..56d2f9618 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx @@ -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 ( + + + {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} + + + + + + Response Time + {" "} + + {Math.floor(responseTime)} + + {" "} + ms + + + + + {/* Display original value */} + + ); + } + 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 ( + + {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)} + + ); +}; + +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 ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + + + + + + } + minTickGap={0} + axisLine={false} + tickLine={false} + height={20} + /> + } + wrapperStyle={{ pointerEvents: "none" }} + /> + + + + ); +}; + +DistributedUptimeResponseChart.propTypes = { + checks: PropTypes.array, +}; + +export default DistributedUptimeResponseChart; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx new file mode 100644 index 000000000..8032b6527 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx @@ -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 ( + + Made with ❤️ by UpRock & Bluewave Labs + + Built on + + Solana + + + ); +}; + +export default Footer; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx new file mode 100644 index 000000000..a095b5feb --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx @@ -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; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx new file mode 100644 index 000000000..03949a9f7 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx @@ -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 ( + + ); +}; + +export default NextExpectedCheck;