Merge branch 'develop' into status-page-subheader

This commit is contained in:
tunckiral
2025-02-22 15:45:39 -05:00
12 changed files with 458 additions and 76 deletions
@@ -0,0 +1,181 @@
// Components
import { Stack, Box, Tooltip, Typography } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import { useSelector } from "react-redux";
const PlaceholderCheck = ({ daysToShow }) => {
const theme = useTheme();
return (
<Box
width={`calc(30vw / ${daysToShow})`}
height="100%"
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
/>
);
};
PlaceholderCheck.propTypes = {
daysToShow: PropTypes.number,
};
const Check = ({ check, daysToShow }) => {
const [animate, setAnimate] = useState(false);
const theme = useTheme();
useEffect(() => {
setAnimate(true);
}, []);
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Tooltip
title={
<>
<Typography>
{formatDateWithTz(check._id, "ddd, MMMM D, YYYY", uiTimezone)}
</Typography>
<Box mt={theme.spacing(2)}>
<Stack
display="inline-flex"
direction="row"
justifyContent="space-between"
gap={theme.spacing(4)}
>
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
Uptime percentage
</Typography>
<Typography component="span">
{check.upPercentage.toFixed(2)}
<Typography
component="span"
sx={{ opacity: 0.8 }}
>
{" "}
%
</Typography>
</Typography>
</Stack>
</Box>
</>
}
placement="top"
key={`check-${check?._id}`}
slotProps={{
popper: {
className: "bar-tooltip",
modifiers: [
{
name: "offset",
options: {
offset: [0, -10],
},
},
],
sx: {
"& .MuiTooltip-tooltip": {
backgroundColor: theme.palette.secondary.main,
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shape.boxShadow,
px: theme.spacing(4),
py: theme.spacing(3),
},
"& .MuiTooltip-tooltip p": {
/* TODO Font size should point to theme */
fontSize: 12,
color: theme.palette.secondary.contrastText,
fontWeight: 500,
},
"& .MuiTooltip-tooltip span": {
/* TODO Font size should point to theme */
fontSize: 11,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
},
},
}}
>
<Box
position="relative"
width={`calc(30vw / ${daysToShow})`}
height="100%"
backgroundColor={theme.palette.error.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
}}
>
<Box
position="absolute"
bottom={0}
width="100%"
height={`${animate ? check.upPercentage : 0}%`}
backgroundColor={theme.palette.success.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
transition: "height 600ms cubic-bezier(0.4, 0, 0.2, 1)",
}}
/>
</Box>
</Tooltip>
);
};
Check.propTypes = {
check: PropTypes.object,
daysToShow: PropTypes.number,
};
const DePINStatusPageBarChart = ({ checks = [], daysToShow = 30 }) => {
if (checks.length !== daysToShow) {
const placeholders = Array(daysToShow - checks.length).fill("placeholder");
checks = [...checks, ...placeholders];
}
return (
<Stack
direction="row"
justifyContent="space-between"
width="100%"
flexWrap="nowrap"
height="50px"
>
{checks.map((check) => {
if (check === "placeholder") {
return (
<PlaceholderCheck
key={Math.random()}
daysToShow={daysToShow}
/>
);
}
return (
<Check
key={Math.random()}
check={check}
daysToShow={daysToShow}
/>
);
})}
</Stack>
);
};
DePINStatusPageBarChart.propTypes = {
checks: PropTypes.array,
daysToShow: PropTypes.number,
};
export default DePINStatusPageBarChart;
@@ -0,0 +1,46 @@
import { useEffect, useState, useCallback } from "react";
import { networkService } from "../../../../main";
import { useSelector } from "react-redux";
import { createToast } from "../../../../Utils/toastUtils";
const useDUStatusPageFetch = (isCreate = false, url) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [statusPage, setStatusPage] = useState(undefined);
const { authToken } = useSelector((state) => state.auth);
const fetchStatusPage = useCallback(async () => {
try {
const response = await networkService.getStatusPageByUrl({
authToken,
url,
type: "distributed",
});
if (!response?.data?.data) return;
const statusPage = response.data.data;
setStatusPage(statusPage);
} catch (error) {
// If there is a 404, status page is not found
if (error?.response?.status === 404) {
setStatusPage(undefined);
return;
}
createToast({ body: error.message });
setNetworkError(true);
} finally {
setIsLoading(false);
}
}, [authToken, url]);
useEffect(() => {
if (isCreate === true) {
return;
}
fetchStatusPage();
}, [isCreate, fetchStatusPage]);
return [statusPage, isLoading, networkError, fetchStatusPage];
};
export { useDUStatusPageFetch };
@@ -7,26 +7,34 @@ import VisuallyHiddenInput from "./Components/VisuallyHiddenInput";
import Image from "../../../Components/Image";
import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Search from "../../../Components/Inputs/Search";
import MonitorList from "../../StatusPage/Create/Components/MonitorList";
// Utils
import { useTheme } from "@emotion/react";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useStatusPageFetch } from "../../StatusPage/Status/Hooks/useStatusPageFetch";
import { useDUStatusPageFetch } from "./Hooks/useDUStatusPageFetch";
import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
import { statusPageValidation } from "../../../Validation/validation";
import { buildErrors } from "../../../Validation/error";
import { createToast } from "../../../Utils/toastUtils";
import { useNavigate } from "react-router-dom";
import { useMonitorsFetch } from "../../StatusPage/Create/Hooks/useMonitorsFetch";
const CreateStatus = () => {
const theme = useTheme();
const { monitorId, url } = useParams();
const navigate = useNavigate();
const isCreate = typeof url === "undefined";
const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
useStatusPageFetch(isCreate, url);
const [statusPage, statusPageIsLoading, statusPageNetworkError] = useDUStatusPageFetch(
isCreate,
url
);
const [monitors, monitorsIsLoading, monitorsNetworkError] = useMonitorsFetch();
const BREADCRUMBS = [
{ name: "distributed uptime", path: "/distributed-uptime" },
@@ -43,6 +51,8 @@ const CreateStatus = () => {
monitors: [monitorId],
});
const [errors, setErrors] = useState({});
const [search, setSearch] = useState("");
const [selectedMonitors, setSelectedMonitors] = useState([]);
const handleFormChange = (e) => {
const { name, value, checked, type } = e.target;
@@ -62,6 +72,16 @@ const CreateStatus = () => {
setForm({ ...form, [name]: value });
};
const handleMonitorsChange = (selectedMonitors) => {
handleFormChange({
target: {
name: "subMonitors",
value: selectedMonitors.map((monitor) => monitor._id),
},
});
setSelectedMonitors(selectedMonitors);
};
const handleImageUpload = (e) => {
const img = e.target?.files?.[0];
setForm((prev) => ({
@@ -73,19 +93,20 @@ const CreateStatus = () => {
let logoToSubmit = undefined;
// Handle image
if (typeof form.logo !== "undefined") {
if (typeof form.logo !== "undefined" && typeof form.logo.src === "undefined") {
logoToSubmit = {
src: URL.createObjectURL(form.logo),
name: form.logo.name,
type: form.logo.type,
size: form.logo.size,
};
} else if (typeof form.logo !== "undefined") {
logoToSubmit = form.logo;
}
const formToSubmit = { ...form };
if (typeof logoToSubmit !== "undefined") {
formToSubmit.logo = logoToSubmit;
}
// Validate
const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false });
if (typeof error === "undefined") {
@@ -127,12 +148,20 @@ const CreateStatus = () => {
companyName: statusPage?.companyName,
isPublished: statusPage?.isPublished,
timezone: statusPage?.timezone,
monitors: statusPageMonitors.map((monitor) => monitor._id),
monitors: statusPage?.monitors,
subMonitors: statusPage?.subMonitors.map((monitor) => monitor._id),
color: statusPage?.color,
logo: newLogo,
};
});
}, [isCreate, statusPage, statusPageMonitors]);
setSelectedMonitors(statusPage?.subMonitors);
}, [isCreate, statusPage]);
const imgSrc = form?.logo?.src
? form.logo.src
: form.logo
? URL.createObjectURL(form.logo)
: undefined;
return (
<Stack gap={theme.spacing(10)}>
@@ -212,7 +241,7 @@ const CreateStatus = () => {
alignItems="center"
>
<Image
src={form.logo ? URL.createObjectURL(form.logo) : undefined}
src={imgSrc}
alt="Logo"
minWidth={"300px"}
minHeight={"100px"}
@@ -234,6 +263,30 @@ const CreateStatus = () => {
</Box>
</Stack>
</ConfigBox>
<ConfigBox>
<Stack>
<Typography component="h2">Standard Monitors</Typography>
<Typography component="p">
Attach standard monitors to your status page.
</Typography>
</Stack>
<Stack gap={theme.spacing(18)}>
<Search
options={monitors ?? []}
multiple={true}
filteredBy="name"
value={selectedMonitors}
inputValue={search}
handleInputChange={setSearch}
handleChange={handleMonitorsChange}
/>
<MonitorList
monitors={monitors}
selectedMonitors={selectedMonitors}
setSelectedMonitors={handleMonitorsChange}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
@@ -0,0 +1,66 @@
// Components
import { Stack, Box } from "@mui/material";
import Host from "../../../../../Components/Host";
import DePINStatusPageBarChart from "../../../../../Components/Charts/DePINStatusPageBarChart";
import { StatusLabel } from "../../../../../Components/Label";
//Utils
import { useTheme } from "@mui/material/styles";
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
import PropTypes from "prop-types";
const MonitorsList = ({
isLoading = false,
shouldRender = true,
monitors = [],
timeFrame,
}) => {
const theme = useTheme();
const { determineState } = useUtils();
return (
<>
{monitors?.map((monitor) => {
const status = determineState(monitor);
return (
<Stack
key={monitor._id}
width="100%"
gap={theme.spacing(2)}
>
<Host
key={monitor._id}
url={monitor.url}
title={monitor.name}
percentageColor={monitor.percentageColor}
percentage={monitor.percentage}
/>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(20)}
>
<Box flex={9}>
<DePINStatusPageBarChart
checks={monitor?.checks?.slice().reverse()}
daysToShow={timeFrame}
/>
</Box>
<Box flex={1}>
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
</Box>
</Stack>
</Stack>
);
})}
</>
);
};
MonitorsList.propTypes = {
monitors: PropTypes.array.isRequired,
};
export default MonitorsList;
@@ -0,0 +1,39 @@
import { Stack, Button, ButtonGroup } from "@mui/material";
import { RowContainer } from "../../../../../Components/StandardContainer";
import { useTheme } from "@emotion/react";
const TimeFrameHeader = ({ timeFrame, setTimeFrame }) => {
const theme = useTheme();
return (
<Stack
direction="row"
justifyContent="flex-end"
>
<ButtonGroup>
<Button
variant="group"
filled={(timeFrame === 30).toString()}
onClick={() => setTimeFrame(30)}
>
30 days
</Button>
<Button
variant="group"
filled={(timeFrame === 60).toString()}
onClick={() => setTimeFrame(60)}
>
60 days
</Button>
<Button
variant="group"
filled={(timeFrame === 90).toString()}
onClick={() => setTimeFrame(90)}
>
90 days
</Button>
</ButtonGroup>
</Stack>
);
};
export default TimeFrameHeader;
@@ -2,25 +2,41 @@ import { useState, useEffect } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
const useStatusPageFetchByUrl = ({ url }) => {
const useStatusPageFetchByUrl = ({ url, timeFrame }) => {
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [statusPage, setStatusPage] = useState(undefined);
const [monitorId, setMonitorId] = useState(undefined);
const [isPublished, setIsPublished] = useState(false);
const { authToken } = useSelector((state) => state.auth);
const theme = useTheme();
const { getMonitorWithPercentage } = useMonitorUtils();
useEffect(() => {
const fetchStatusPageByUrl = async () => {
try {
const response = await networkService.getStatusPageByUrl({
const response = await networkService.getDistributedStatusPageByUrl({
authToken,
url,
type: "distributed",
timeFrame,
});
if (!response?.data?.data) return;
const statusPage = response.data.data;
setStatusPage(statusPage);
const monitorsWithPercentage = statusPage?.subMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
const statusPageWithSubmonitorPercentages = {
...statusPage,
subMonitors: monitorsWithPercentage,
};
setStatusPage(statusPageWithSubmonitorPercentages);
setMonitorId(statusPage?.monitors[0]);
setIsPublished(statusPage?.isPublished);
} catch (error) {
@@ -33,7 +49,7 @@ const useStatusPageFetchByUrl = ({ url }) => {
}
};
fetchStatusPageByUrl();
}, [authToken, url]);
}, [authToken, url, getMonitorWithPercentage, theme, timeFrame]);
return [isLoading, networkError, statusPage, monitorId, isPublished];
};
@@ -16,6 +16,7 @@ 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";
import MonitorsList from "./Components/MonitorsList";
//Utils
import { useTheme } from "@mui/material/styles";
@@ -27,6 +28,8 @@ import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPage
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import TimeFrameHeader from "./Components/TimeframeHeader";
import SubHeader from "../../DistributedUptime/Details/Components/Subheader";
const DistributedUptimeStatus = () => {
const { url } = useParams();
@@ -37,9 +40,11 @@ const DistributedUptimeStatus = () => {
// Local State
const [dateRange, setDateRange] = useState("day");
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [timeFrame, setTimeFrame] = useState(30);
// Utils
const theme = useTheme();
const navigate = useNavigate();
const [
statusPageIsLoading,
statusPageNetworkError,
@@ -48,6 +53,7 @@ const DistributedUptimeStatus = () => {
isPublished,
] = useStatusPageFetchByUrl({
url,
timeFrame,
});
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
@@ -56,6 +62,7 @@ const DistributedUptimeStatus = () => {
const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => {
navigate("/distributed-uptime");
}, url);
// Constants
const BREADCRUMBS = [
{ name: "Distributed Uptime", path: "/distributed-uptime" },
@@ -223,6 +230,14 @@ const DistributedUptimeStatus = () => {
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
/>
<TimeFrameHeader
timeFrame={timeFrame}
setTimeFrame={setTimeFrame}
/>
<MonitorsList
monitors={statusPage?.subMonitors}
timeFrame={timeFrame}
/>
<Footer />
<Dialog
// open={isOpen.deleteStats}
+2 -2
View File
@@ -139,7 +139,6 @@ const Settings = () => {
}
}
} catch (error) {
console.log(error);
createToast({ body: "Failed to save settings" });
} finally {
setChecksIsLoading(false);
@@ -368,7 +367,8 @@ const Settings = () => {
<Box>
<Typography component="h1">Wallet</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.
Connect your wallet here. This is required for the Distributed Uptime
monitor to connect to multiple nodes globally.
</Typography>
</Box>
<Box>
@@ -8,7 +8,7 @@ import { StatusLabel } from "../../../../../Components/Label";
import { useTheme } from "@mui/material/styles";
import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
import PropTypes from "prop-types";
const MonitorsList = ({ monitors = [] }) => {
const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => {
const theme = useTheme();
const { determineState } = useUtils();
return (
@@ -24,7 +24,7 @@ const MonitorsList = ({ monitors = [] }) => {
<Host
key={monitor._id}
url={monitor.url}
title={monitor.title}
title={monitor.name}
percentageColor={monitor.percentageColor}
percentage={monitor.percentage}
/>
+25
View File
@@ -1011,6 +1011,21 @@ class NetworkService {
},
});
}
async getDistributedStatusPageByUrl(config) {
const { authToken, url, type, timeFrame } = config;
const params = new URLSearchParams();
params.append("type", type);
params.append("timeFrame", timeFrame);
return this.axiosInstance.get(
`/status-page/distributed/${url}?${params.toString()}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
async getStatusPagesByTeamId(config) {
const { authToken, teamId } = config;
@@ -1041,6 +1056,15 @@ class NetworkService {
form.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
});
// Handle subMonitors, even if it's an empty array
if (form.subMonitors && form.subMonitors.length > 0) {
form.subMonitors.forEach((monitorId) => {
fd.append("subMonitors[]", monitorId);
});
} else {
fd.append("deleteSubmonitors", true);
}
if (form?.logo?.src && form?.logo?.src !== "") {
const imageResult = await axios.get(form.logo.src, {
responseType: "blob",
@@ -1051,6 +1075,7 @@ class NetworkService {
URL.revokeObjectURL(form.logo.src);
}
}
if (isCreate) {
return this.axiosInstance.post(`/status-page`, fd, {
headers: {
+1
View File
@@ -226,6 +226,7 @@ const statusPageValidation = joi.object({
"array.empty": "At least one monitor is required",
"any.required": "At least one monitor is required",
}),
subMonitors: joi.array().optional(),
logo: logoImageValidation,
showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(),