Merge branch 'develop' of github.com:bluewave-labs/bluewave-uptime into develop
This commit is contained in:
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>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user