Merge pull request #20 from uprockcom/feat/status-graph

Feat/status graph
This commit is contained in:
Alexander Holliday
2025-02-22 08:50:38 -08:00
committed by GitHub
7 changed files with 335 additions and 8 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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];
};

View File

@@ -16,7 +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 "../../StatusPage/Status/Components/MonitorsList";
import MonitorsList from "./Components/MonitorsList";
//Utils
import { useTheme } from "@mui/material/styles";
@@ -27,6 +27,7 @@ import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl";
import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete";
import { useNavigate } from "react-router-dom";
import { useLocation } from "react-router-dom";
import TimeFrameHeader from "./Components/TimeframeHeader";
const DistributedUptimeStatus = () => {
const { url } = useParams();
@@ -37,6 +38,7 @@ 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();
@@ -49,6 +51,7 @@ const DistributedUptimeStatus = () => {
isPublished,
] = useStatusPageFetchByUrl({
url,
timeFrame,
});
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
@@ -217,7 +220,14 @@ const DistributedUptimeStatus = () => {
monitor={monitor}
lastUpdateTrigger={lastUpdateTrigger}
/>
<MonitorsList monitors={statusPage?.subMonitors} />
<TimeFrameHeader
timeFrame={timeFrame}
setTimeFrame={setTimeFrame}
/>
<MonitorsList
monitors={statusPage?.subMonitors}
timeFrame={timeFrame}
/>
<Footer />
<Dialog
// open={isOpen.deleteStats}

View File

@@ -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}
/>

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;