Merge branch 'develop' of github.com:bluewave-labs/bluewave-uptime into develop

This commit is contained in:
Alex Holliday committed 2025-03-04 22:26:36 -08:00
commit 5f8e43900f
5 files changed
+273 -115

No files matched your search

@@ -3,6 +3,45 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import { useEffect, useState } 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 = ({ width, height, backgroundColor, borderRadius, children }) => {
const theme = useTheme();
return (
<Box
position="relative"
width={width}
height={height}
backgroundColor={backgroundColor}
sx={{
borderRadius: borderRadius || theme.spacing(1.5),
}}
>
{children}
</Box>
);
};
Bar.propTypes = {
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
backgroundColor: PropTypes.string.isRequired,
borderRadius: PropTypes.string,
children: PropTypes.node,
};
/* TODO add prop validation and jsdocs */
const StatusPageBarChart = ({ checks = [] }) => {
@@ -10,6 +49,11 @@ const StatusPageBarChart = ({ checks = [] }) => {
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);
}, []);
@@ -41,15 +85,11 @@ const StatusPageBarChart = ({ checks = [] }) => {
/* 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
<Bar
key={`${check}-${index}`}
position="relative"
width="calc(30vw / 25)"
width={barWidth}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
) : (
<Tooltip
@@ -140,14 +180,10 @@ const StatusPageBarChart = ({ checks = [] }) => {
},
}}
>
<Box
position="relative"
width="calc(30vw / 25)"
<Bar
width={barWidth}
height="100%"
backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW
sx={{
borderRadius: theme.spacing(1.5),
}}
backgroundColor={theme.palette.primary.lowContrast}
>
<Box
position="absolute"
@@ -164,7 +200,7 @@ const StatusPageBarChart = ({ checks = [] }) => {
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Bar>
</Tooltip>
)
)}
+120 -35
View File
@@ -64,34 +64,28 @@ const menu = [
{ name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{
name: "Account",
icon: <Account />,
nested: [
{ name: "Profile", path: "account/profile", icon: <UserSvg /> },
{ name: "Password", path: "account/password", icon: <LockSvg /> },
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
],
},
{
name: "Settings",
icon: <Settings />,
path: "settings",
},
];
const otherMenuItems = [
{ name: "Support", path: "support", icon: <Support /> },
{
name: "Other",
icon: <Folder />,
nested: [
{ name: "Support", path: "support", icon: <Support /> },
{
name: "Discussions",
path: "discussions",
icon: <Discussions />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
],
name: "Discussions",
path: "discussions",
icon: <Discussions />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
];
const accountMenuItems = [
{ name: "Profile", path: "account/profile", icon: <UserSvg /> },
{ name: "Password", path: "account/password", icon: <LockSvg /> },
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
];
/* TODO this could be a key in nested Path would be the link */
@@ -133,12 +127,38 @@ function Sidebar() {
(state) => state.ui.distributedUptimeEnabled
);
const accountMenuItem = menu.find((item) => item.name === "Account");
if (user.role?.includes("demo") && accountMenuItem) {
accountMenuItem.nested = accountMenuItem.nested.filter((item) => {
return item.name !== "Password";
});
}
const renderAccountMenuItems = () => {
let filteredAccountMenuItems = [...accountMenuItems];
// If the user is in demo mode, remove the "Password" option
if (user.role?.includes("demo")) {
filteredAccountMenuItems = filteredAccountMenuItems.filter(
(item) => item.name !== "Password"
);
}
// If the user is NOT a superadmin, remove the "Team" option
if (user.role && !user.role.includes("superadmin")) {
filteredAccountMenuItems = filteredAccountMenuItems.filter(
(item) => item.name !== "Team"
);
}
return filteredAccountMenuItems.map((item) => (
<MenuItem
key={item.name}
onClick={() => navigate(item.path)}
sx={{
gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
}}
>
{item.icon}
{item.name}
</MenuItem>
));
};
const openPopup = (event, id) => {
setAnchorEl(event.currentTarget);
@@ -260,7 +280,6 @@ function Sidebar() {
>
{collapsed ? <ArrowRight /> : <ArrowLeft />}
</IconButton>
{/* TODO Alignment done using padding. Use single source of truth to that*/}
<Stack
pt={theme.spacing(6)}
@@ -588,8 +607,59 @@ function Sidebar() {
})}
</List>
</Box>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<List
component="nav"
disablePadding
sx={{ px: theme.spacing(6) }}
>
{otherMenuItems.map((item) => {
return item.path ? (
<Tooltip
key={item.path}
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
className={
location.pathname.startsWith(`/${item.path}`) ? "selected-path" : ""
}
onClick={() => {
const url = URL_MAP[item.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${item.path}`);
}
}}
sx={{
height: "37px",
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon} </ListItemIcon>
<ListItemText>{item.name} </ListItemText>{" "}
</ListItemButton>
</Tooltip>
) : null;
})}
</List>
<Divider sx={{ mt: "auto", borderColor: theme.palette.primary.lowContrast }} />
<Stack
direction="row"
height="50px"
@@ -711,14 +781,19 @@ function Sidebar() {
},
}}
sx={{
ml: theme.spacing(8),
ml: theme.spacing(4),
}}
>
{collapsed && (
<MenuItem sx={{ cursor: "default", minWidth: "50%" }}>
<Box
mb={theme.spacing(2)}
sx={{ maxWidth: "50%", overflow: "hidden" }}
sx={{
minWidth: "50%",
maxWidth: "max-content",
overflow: "visible",
whiteSpace: "nowrap",
}}
>
<Typography
component="span"
@@ -727,13 +802,22 @@ function Sidebar() {
sx={{
display: "block",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
overflow: "visible",
// wordBreak: "break-word",
textOverflow: "clip",
}}
>
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize", fontSize: 12 }}>
<Typography
sx={{
textTransform: "capitalize",
fontSize: 12,
whiteSpace: "nowrap",
overflow: "visible",
// wordBreak: "break-word",
}}
>
{authState.user?.role}
</Typography>
</Box>
@@ -741,7 +825,8 @@ function Sidebar() {
)}
{/* TODO Do we need two dividers? */}
{collapsed && <Divider />}
<Divider />
{/* <Divider /> */}
{renderAccountMenuItems()}
<MenuItem
onClick={logout}
sx={{
@@ -1,49 +1,7 @@
import { useState, useEffect, useRef } from "react";
import { useState, useEffect } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
const getRandomDevice = () => {
const manufacturers = {
Apple: ["iPhone 15 Pro Max", "iPhone 15", "iPhone 14 Pro", "iPhone 14", "iPhone 13"],
Samsung: [
"Galaxy S23 Ultra",
"Galaxy S23+",
"Galaxy S23",
"Galaxy Z Fold5",
"Galaxy Z Flip5",
],
Google: ["Pixel 8 Pro", "Pixel 8", "Pixel 7a", "Pixel 7", "Pixel 6a"],
OnePlus: [
"OnePlus 11",
"OnePlus 10T",
"OnePlus Nord 3",
"OnePlus 10 Pro",
"OnePlus Nord 2T",
],
Xiaomi: ["13 Pro", "13", "Redmi Note 12", "POCO F5", "Redmi 12"],
Huawei: ["P60 Pro", "Mate X3", "Nova 11", "P50 Pro", "Mate 50"],
Sony: ["Xperia 1 V", "Xperia 5 V", "Xperia 10 V", "Xperia Pro-I", "Xperia 1 IV"],
Motorola: ["Edge 40 Pro", "Edge 40", "G84", "G54", "Razr 40 Ultra"],
ASUS: [
"ROG Phone 7",
"Zenfone 10",
"ROG Phone 6",
"Zenfone 9",
"ROG Phone 7 Ultimate",
],
};
const manufacturerNames = Object.keys(manufacturers);
const randomManufacturer =
manufacturerNames[Math.floor(Math.random() * manufacturerNames.length)];
const models = manufacturers[randomManufacturer];
const randomModel = models[Math.floor(Math.random() * models.length)];
return {
manufacturer: randomManufacturer,
model: randomModel,
};
};
const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) => {
const [isLoading, setIsLoading] = useState(true);
const [connectionStatus, setConnectionStatus] = useState(undefined);
@@ -51,9 +9,6 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange })
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0);
const [devices, setDevices] = useState(Array.from({ length: 5 }, getRandomDevice));
const prevDateRangeRef = useRef(dateRange);
useEffect(() => {
if (typeof monitorId === "undefined") {
@@ -64,6 +19,35 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange })
return;
}
// Get initial data
const fetchInitialData = async () => {
try {
const res = await networkService.getDistributedUptimeDetails({
monitorId,
dateRange: dateRange,
normalize: true,
});
const responseData = res?.data?.data;
if (typeof responseData === "undefined") {
throw new Error("No data");
}
setLastUpdateTrigger(Date.now());
setMonitor(responseData);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchInitialData();
try {
const cleanup = networkService.subscribeToDistributedUptimeDetails({
monitorId,
@@ -77,15 +61,7 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange })
setNetworkError(false);
}
setLastUpdateTrigger(Date.now());
const latestChecksWithDevice = data?.monitor?.latestChecks.map((check, idx) => {
check.device = devices[idx];
return check;
});
const monitorWithDevice = {
...data.monitor,
latestChecks: latestChecksWithDevice,
};
setMonitor(monitorWithDevice);
setMonitor(data.monitor);
},
onOpen: () => {
setConnectionStatus("up");
@@ -107,21 +83,11 @@ const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange })
retryCount,
setConnectionStatus,
networkError,
devices,
isLoading,
isPublic,
isPublished,
]);
useEffect(() => {
const hasDateRangeChanged = prevDateRangeRef.current !== dateRange;
prevDateRangeRef.current = dateRange; // Update the ref to the current dateRange
if (!hasDateRangeChanged) {
setDevices(Array.from({ length: 5 }, getRandomDevice));
}
}, [dateRange]);
return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
};
@@ -20,6 +20,43 @@ const useSubscribeToMonitors = (page, rowsPerPage) => {
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
const fetchInitialData = async () => {
try {
const initialDataRes = await networkService.getDistributedUptimeMonitors({
teamId: user.teamId,
limit: 25,
types: [
typeof import.meta.env.VITE_DEPIN_TESTING === "undefined"
? "distributed_http"
: "distributed_test",
],
page,
rowsPerPage,
});
const responseData = initialDataRes?.data?.data;
if (typeof responseData === "undefined") throw new Error("No data");
const { monitors, filteredMonitors, summary } = responseData;
const mappedMonitors = filteredMonitors?.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(monitors);
setMonitorsSummary(summary);
setFilteredMonitors(mappedMonitors);
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchInitialData();
try {
const cleanup = networkService.subscribeToDistributedUptimeMonitors({
teamId: user.teamId,
+34
View File
@@ -807,6 +807,31 @@ class NetworkService {
);
}
getDistributedUptimeMonitors(config) {
const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config;
const params = new URLSearchParams();
if (limit) params.append("limit", limit);
if (types) {
types.forEach((type) => {
params.append("type", type);
});
}
if (page) params.append("page", page);
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
if (filter) params.append("filter", filter);
if (field) params.append("field", field);
if (order) params.append("order", order);
if (this.eventSource) {
this.eventSource.close();
}
const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}/initial?${params.toString()}`;
return this.axiosInstance.get(url);
}
subscribeToDistributedUptimeMonitors(config) {
const {
teamId,
@@ -872,6 +897,15 @@ class NetworkService {
};
}
getDistributedUptimeDetails(config) {
const params = new URLSearchParams();
const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config;
if (dateRange) params.append("dateRange", dateRange);
if (normalize) params.append("normalize", normalize);
const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}/initial?${params.toString()}`;
return this.axiosInstance.get(url);
}
subscribeToDistributedUptimeDetails(config) {
const params = new URLSearchParams();
const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config;