Merge pull request #1795 from bluewave-labs/feat/du-status

feat: DePIN status page
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-02-24 09:11:58 -08:00
commit a8ffba710d
12 files changed
+490 -61

No files matched your search

+45
View File
@@ -28,6 +28,7 @@
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
@@ -9381,6 +9382,14 @@
"integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -10443,6 +10452,14 @@
"tiny-invariant": "^1.0.6"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -12441,6 +12458,18 @@
"void-elements": "3.1.0"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -17849,6 +17878,14 @@
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
"integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -18379,6 +18416,14 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+1
View File
@@ -31,6 +31,7 @@
"axios": "^1.7.4",
"dayjs": "1.11.13",
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
"immutability-helper": "^3.1.1",
"joi": "17.13.3",
+42
View File
@@ -0,0 +1,42 @@
import { RowContainer } from "../StandardContainer";
import { Stack, Typography } from "@mui/material";
import Image from "../Image";
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;
+67
View File
@@ -0,0 +1,67 @@
import html2canvas from "html2canvas";
import ShareIcon from "@mui/icons-material/Share";
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={<ShareIcon sx={{ color: theme.palette.success.main }} />}
color="success"
onClick={captureAndShare}
>
Share
</Button>
);
};
export default ShareComponent;
@@ -0,0 +1,51 @@
// 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>
);
};
@@ -0,0 +1,48 @@
// Components
import { Stack, Typography } from "@mui/material";
import { ColContainer } from "../../../../../Components/StandardContainer";
import SmartToyIcon from "@mui/icons-material/SmartToy";
import Dot from "../../../../../Components/Dot";
// Utils
import { useTheme } from "@emotion/react";
const MESSAGES = [
"I've checked the network status, and we're seeing excellent performance across all regions.",
"The network is stable and functioning optimally. All connections are active and stable.",
"I've reviewed the network status, and everything looks great. No issues detected.",
"The network is up and running smoothly. All connections are active and stable.",
"I've checked the network status, and everything is looking good. No issues detected.",
];
const ChatBot = ({ sx }) => {
const theme = useTheme();
return (
<ColContainer
backgroundColor={theme.palette.chatbot.background}
sx={{ ...sx }}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<SmartToyIcon sx={{ color: theme.palette.chatbot.textAccent }} />
<Typography color={theme.palette.chatbot.textAccent}>Status Bot</Typography>
<Dot
color={theme.palette.chatbot.textAccent}
style={{ opacity: 0.4 }}
/>
<Typography
variant="body2"
color={theme.palette.chatbot.textAccent}
sx={{ opacity: 0.4 }}
>
Now
</Typography>
</Stack>
<Typography>{MESSAGES[Math.floor(Math.random() * MESSAGES.length)]}</Typography>
</ColContainer>
);
};
export default ChatBot;
@@ -1,7 +1,8 @@
import { Stack, Typography, List, ListItem } from "@mui/material";
import { Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import "flag-icons/css/flag-icons.min.css";
import { ColContainer } from "../../../../../Components/StandardContainer";
const BASE_BOX_PADDING_VERTICAL = 16;
const BASE_BOX_PADDING_HORIZONTAL = 8;
@@ -14,21 +15,9 @@ const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
};
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,
}}
>
<ColContainer>
<Stack
direction="row"
justifyContent={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[connectionStatus]} />
@@ -41,30 +30,53 @@ const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
{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>
<table>
<thead>
<tr>
<th style={{ textAlign: "left" }}>
<Typography>COUNTRY</Typography>
</th>
<th style={{ textAlign: "left" }}>
<Typography>CITY</Typography>
</th>
<th style={{ textAlign: "right" }}>
<Typography>RESPONSE</Typography>
</th>
<th style={{ textAlign: "right" }}>
<Typography>UPT BURNED</Typography>
</th>
</tr>
</thead>
<tbody>
{data.map((dataPoint) => {
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
const flag = countryCode ? `fi fi-${countryCode}` : null;
const city = dataPoint?.city !== "" ? dataPoint?.city : "Unknown";
return (
<tr key={Math.random()}>
<td style={{ padding: theme.spacing(4) }}>
<Typography>
{flag ? <span className={flag} /> : null}{" "}
{countryCode?.toUpperCase() ?? "N/A"}
</Typography>
</td>
<td>
<Typography>{city}</Typography>
</td>
<td style={{ textAlign: "right" }}>
<Typography>{Math.floor(dataPoint.responseTime)} ms</Typography>
</td>
<td style={{ textAlign: "right" }}>
<Typography color={theme.palette.warning.main}>
+{dataPoint.uptBurnt}
</Typography>
</td>
</tr>
);
})}
</tbody>
</table>
</ColContainer>
);
};
@@ -28,12 +28,17 @@ const DistributedUptimeMap = ({ width = "100%", checks }) => {
useEffect(() => {
if (mapContainer.current && !map.current) {
const initialStyle = buildStyle(initialTheme.current, initialMode.current);
map.current = new maplibregl.Map({
container: mapContainer.current,
style: initialStyle,
center: [0, 20],
zoom: 0.8,
attributionControl: false,
canvasContextAttributes: {
antialias: true,
preserveDrawingBuffer: true,
},
});
}
map.current.on("load", () => {
@@ -87,6 +92,10 @@ const DistributedUptimeMap = ({ width = "100%", checks }) => {
ref={mapContainer}
style={{
width: width,
borderRadius: theme.spacing(4),
borderColor: theme.palette.primary.lowContrast,
borderStyle: "solid",
borderWidth: 1,
}}
/>
);
@@ -1,6 +1,6 @@
// Components
import { Stack } from "@mui/material";
import StatBox from "../../../../../Components/StatBox";
import InfoBox from "../../../../../Components/InfoBox";
import LastUpdate from "../LastUpdate";
import UptLogo from "../../../../../assets/icons/upt_logo.png";
@@ -14,17 +14,21 @@ const StatBoxes = ({ monitor, lastUpdateTrigger }) => {
return (
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(8)}
>
<StatBox
<InfoBox
sx={{ flex: 1 }}
heading="Avg Response Time"
subHeading={`${Math.floor(monitor?.avgResponseTime ?? 0)} ms`}
/>
<StatBox
<InfoBox
sx={{ flex: 1 }}
heading="Checking every"
subHeading={`${(monitor?.interval ?? 0) / 1000} seconds`}
/>
<StatBox
<InfoBox
sx={{ flex: 1 }}
heading={"Last check"}
subHeading={
<LastUpdate
@@ -33,7 +37,8 @@ const StatBoxes = ({ monitor, lastUpdateTrigger }) => {
/>
}
/>
<StatBox
<InfoBox
sx={{ flex: 1 }}
heading="Last server push"
subHeading={
<LastUpdate
@@ -43,13 +48,6 @@ const StatBoxes = ({ monitor, lastUpdateTrigger }) => {
/>
}
/>
<StatBox
heading="UPT Burned"
subHeading={monitor?.totalUptBurnt ?? 0}
img={UptLogo}
alt="Upt Logo"
/>
</Stack>
);
};
@@ -0,0 +1,92 @@
// Components
import { ColContainer } from "../../../../../Components/StandardContainer";
import { Stack, Typography } from "@mui/material";
import PulseDot from "../../../../../Components/Animated/PulseDot";
import LastUpdate from "../LastUpdate";
import ChatBot from "../Chatbot";
import ShareComponent from "../../../../../Components/ShareComponent";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const StatusHeader = ({ monitor, connectionStatus, elementToCapture }) => {
const theme = useTheme();
const COLOR_MAP = {
up: theme.palette.successSecondary.main,
down: theme.palette.error.lowContrast,
};
const MSG_MAP = {
up: "All Systems Operational",
down: "Last Check Failed",
};
const PULSE_COLOR = {
up: theme.palette.success.main,
down: theme.palette.error.main,
};
let bgColor = COLOR_MAP[connectionStatus];
return (
<ColContainer backgroundColor={bgColor}>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(8)}
>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<PulseDot color={PULSE_COLOR[connectionStatus]} />
<Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Typography
variant="h1"
color={theme.palette.success.lowContrast}
>
{MSG_MAP[connectionStatus]}
</Typography>
<Typography
variant="body2"
borderRadius={theme.spacing(8)}
padding={theme.spacing(4)}
backgroundColor={theme.palette.successSecondary.lowContrast}
color={theme.palette.success.lowContrast}
>
Uptime: {(monitor.totalUptime * 100).toFixed(2)}%
</Typography>
</Stack>
<Typography
variant="body2"
color={theme.palette.success.lowContrast}
>
Last updated{" "}
<LastUpdate
suffix={"seconds ago"}
lastUpdateTime={monitor.timeSinceLastCheck}
/>
</Typography>
</Stack>
</Stack>
<ShareComponent
elementToCapture={elementToCapture}
fileName={monitor.name}
/>
</Stack>
<ChatBot sx={{ marginTop: theme.spacing(10) }} />
</ColContainer>
);
};
StatusHeader.propTypes = {
monitor: PropTypes.object,
connectionStatus: PropTypes.string,
};
export default StatusHeader;
@@ -12,9 +12,14 @@ import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import GenericFallback from "../../../Components/GenericFallback";
import Dialog from "../../../Components/Dialog";
import SkeletonLayout from "./Components/Skeleton";
import UptLogo from "../../../assets/icons/upt_logo.png";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import InfoBox from "../../../Components/InfoBox";
import StatusHeader from "../../DistributedUptime/Details/Components/StatusHeader";
//Utils
import { useTheme } from "@mui/material/styles";
import { useState } from "react";
import { useState, useRef } from "react";
import { useParams } from "react-router-dom";
import { useSubscribeToDetails } from "../../DistributedUptime/Details/Hooks/useSubscribeToDetails";
import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl";
@@ -26,6 +31,7 @@ const DistributedUptimeStatus = () => {
const location = useLocation();
const isPublic = location.pathname.startsWith("/status/distributed/public");
const elementToCapture = useRef(null);
// Local State
const [dateRange, setDateRange] = useState("day");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
@@ -130,9 +136,12 @@ const DistributedUptimeStatus = () => {
return (
<Stack
ref={elementToCapture}
direction="column"
gap={theme.spacing(10)}
sx={sx}
sx={{
...sx,
}}
>
{!isPublic && <Breadcrumbs list={BREADCRUMBS} />}
<ControlsHeader
@@ -144,10 +153,13 @@ const DistributedUptimeStatus = () => {
url={url}
type="distributed"
/>
<StatBoxes
<StatusHeader
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
connectionStatus={connectionStatus}
elementToCapture={elementToCapture}
/>
<NextExpectedCheck
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
interval={monitor?.interval ?? 0}
@@ -157,7 +169,6 @@ const DistributedUptimeStatus = () => {
dateRange={dateRange}
setDateRange={setDateRange}
/>
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
<Stack
direction="row"
gap={theme.spacing(8)}
@@ -165,14 +176,43 @@ const DistributedUptimeStatus = () => {
<DistributedUptimeMap
checks={monitor?.groupedMapChecks ?? []}
height={"100%"}
width={"100%"}
/>
<DeviceTicker
width={"25vw"}
data={monitor?.latestChecks ?? []}
connectionStatus={connectionStatus}
width={"50%"}
/>
<Stack
width={"50%"}
gap={theme.spacing(8)}
>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<InfoBox
heading="Devices"
subHeading={monitor?.totalChecks ?? 0}
icon={PeopleAltOutlinedIcon}
alt="Upt Logo"
sx={{ width: "50%" }}
/>
<InfoBox
heading="UPT Burned"
subHeading={monitor?.totalUptBurnt ?? 0}
img={UptLogo}
alt="Upt Logo"
sx={{ width: "50%" }}
/>
</Stack>
<DeviceTicker
data={monitor?.latestChecks ?? []}
connectionStatus={connectionStatus}
/>
</Stack>
</Stack>
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
<StatBoxes
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
/>
<Footer />
<Dialog
// open={isOpen.deleteStats}
+24
View File
@@ -200,6 +200,20 @@ const newSemanticColors = {
dark: newColors.green200,
},
},
successSecondary: {
main: {
light: undefined,
dark: "#073C2D",
},
contrastText: {
light: undefined,
dark: "#34D399",
},
lowContrast: {
light: undefined,
dark: "#094434",
},
},
warning: {
main: {
light: newColors.orange700,
@@ -266,6 +280,16 @@ const newSemanticColors = {
dark: undefined,
},
},
chatbot: {
background: {
light: "#112B2B",
dark: "#112B2B",
},
textAccent: {
light: "#C084FC",
dark: "#C084FC",
},
},
};
export { typographyLevels, semanticColors as colors, newSemanticColors };