diff --git a/package-lock.json b/package-lock.json
index e9e82ce06..6bfb9c149 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index dc30cf825..52b2eb0f0 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/Components/InfoBox/index.jsx b/src/Components/InfoBox/index.jsx
new file mode 100644
index 000000000..6797d6c9c
--- /dev/null
+++ b/src/Components/InfoBox/index.jsx
@@ -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 (
+
+ {img && (
+
+ )}
+ {Icon && (
+
+ )}
+
+ {heading}
+
+ {subHeading}
+
+
+
+ );
+};
+
+export default InfoBox;
diff --git a/src/Components/ShareComponent/index.jsx b/src/Components/ShareComponent/index.jsx
new file mode 100644
index 000000000..073ed2243
--- /dev/null
+++ b/src/Components/ShareComponent/index.jsx
@@ -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 (
+ }
+ color="success"
+ onClick={captureAndShare}
+ >
+ Share
+
+ );
+};
+
+export default ShareComponent;
diff --git a/src/Components/StandardContainer/index.jsx b/src/Components/StandardContainer/index.jsx
new file mode 100644
index 000000000..c736b3ecc
--- /dev/null
+++ b/src/Components/StandardContainer/index.jsx
@@ -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 (
+
+ {children}
+
+ );
+};
+
+export const ColContainer = ({ children, backgroundColor, sx }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const RowContainer = ({ children, backgroundColor, sx }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/Pages/DistributedUptime/Details/Components/Chatbot/index.jsx b/src/Pages/DistributedUptime/Details/Components/Chatbot/index.jsx
new file mode 100644
index 000000000..00af46f6b
--- /dev/null
+++ b/src/Pages/DistributedUptime/Details/Components/Chatbot/index.jsx
@@ -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 (
+
+
+
+ Status Bot
+
+
+ Now
+
+
+ {MESSAGES[Math.floor(Math.random() * MESSAGES.length)]}
+
+ );
+};
+
+export default ChatBot;
diff --git a/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx b/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
index dc5604a47..9194eefe5 100644
--- a/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
+++ b/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
@@ -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 (
-
+
@@ -41,30 +30,53 @@ const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
{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}`}
-
-
- );
- })}
-
-
+
+
+
+ |
+ COUNTRY
+ |
+
+ CITY
+ |
+
+ RESPONSE
+ |
+
+ UPT BURNED
+ |
+
+
+
+ {data.map((dataPoint) => {
+ const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
+ const flag = countryCode ? `fi fi-${countryCode}` : null;
+ const city = dataPoint?.city !== "" ? dataPoint?.city : "Unknown";
+ return (
+
+ |
+
+ {flag ? : null}{" "}
+ {countryCode?.toUpperCase() ?? "N/A"}
+
+ |
+
+ {city}
+ |
+
+ {Math.floor(dataPoint.responseTime)} ms
+ |
+
+
+ +{dataPoint.uptBurnt}
+
+ |
+
+ );
+ })}
+
+
+
);
};
diff --git a/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx b/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
index f4e6a2b19..7617666eb 100644
--- a/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
+++ b/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
@@ -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,
}}
/>
);
diff --git a/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx b/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx
index a4fd3b03d..df0492bd2 100644
--- a/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx
+++ b/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx
@@ -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 (
-
-
- {
/>
}
/>
- {
/>
}
/>
-
-
);
};
diff --git a/src/Pages/DistributedUptime/Details/Components/StatusHeader/index.jsx b/src/Pages/DistributedUptime/Details/Components/StatusHeader/index.jsx
new file mode 100644
index 000000000..5db44a559
--- /dev/null
+++ b/src/Pages/DistributedUptime/Details/Components/StatusHeader/index.jsx
@@ -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 (
+
+
+
+
+
+
+
+ {MSG_MAP[connectionStatus]}
+
+
+ Uptime: {(monitor.totalUptime * 100).toFixed(2)}%
+
+
+
+ Last updated{" "}
+
+
+
+
+
+
+
+
+ );
+};
+
+StatusHeader.propTypes = {
+ monitor: PropTypes.object,
+ connectionStatus: PropTypes.string,
+};
+
+export default StatusHeader;
diff --git a/src/Pages/DistributedUptimeStatus/Status/index.jsx b/src/Pages/DistributedUptimeStatus/Status/index.jsx
index ef77b3032..a1c6b473f 100644
--- a/src/Pages/DistributedUptimeStatus/Status/index.jsx
+++ b/src/Pages/DistributedUptimeStatus/Status/index.jsx
@@ -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 (
{!isPublic && }
{
url={url}
type="distributed"
/>
-
+
{
dateRange={dateRange}
setDateRange={setDateRange}
/>
-
{
-
+
+
+
+
+
+
+
+
+
+