Merge pull request #1632 from bluewave-labs/fix/fe/uptime-refactor

Fix/fe/uptime refactor
This commit is contained in:
Alexander Holliday
2025-01-27 07:29:40 -08:00
committed by GitHub
28 changed files with 596 additions and 533 deletions
@@ -0,0 +1,29 @@
import { Box } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const CircularCount = ({ count }) => {
const theme = useTheme();
return (
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50rem",
fontSize: "12px",
fontWeight: 500,
}}
>
{count}
</Box>
);
};
CircularCount.propTypes = {
count: PropTypes.number,
};
export default CircularCount;
+1 -1
View File
@@ -1,7 +1,7 @@
import { Box, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import useUtils from "../../Pages/Uptime/utils";
import useUtils from "../../Pages/Uptime/Home/Hooks/useUtils";
/**
* StatBox Component
@@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
import PulseDot from "../../../Components/Animated/PulseDot";
import useUtils from "../../Uptime/utils";
import useUtils from "../../Uptime/Home/Hooks/useUtils";
import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
@@ -625,7 +625,8 @@ const InfrastructureDetails = () => {
</ButtonGroup>
</Stack>
<Stack
direction="row"
direction={"row"}
// height={chartContainerHeight} // FE team HELP! Possibly no longer needed?
gap={theme.spacing(8)} // FE team HELP!
flexWrap="wrap" // //FE team HELP! Better way to do this?
sx={{
+2 -2
View File
@@ -2,7 +2,7 @@ import { useEffect, useState, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { /* useDispatch, */ useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useUtils from "../Uptime/utils.jsx";
import useUtils from "../Uptime/Home/Hooks/useUtils.jsx";
import { jwtDecode } from "jwt-decode";
import SkeletonLayout from "./skeleton";
import Fallback from "../../Components/Fallback";
@@ -17,7 +17,7 @@ import Pagination from "../../Components/Table/TablePagination/index.jsx";
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { networkService } from "../../Utils/NetworkService.js";
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
import Host from "../Uptime/Home/host.jsx";
import Host from "../Uptime/Home/Components/Host";
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
import { InfrastructureMenu } from "./components/Menu";
@@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import LoadingButton from "@mui/lab/LoadingButton";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton";
import useUtils from "../../Uptime/utils";
import useUtils from "../../Uptime/Home/Hooks/useUtils";
import "./index.css";
import Dialog from "../../../Components/Dialog";
+1 -1
View File
@@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PieChart from "./Charts/PieChart";
import useUtils from "../../Uptime/utils";
import useUtils from "../../Uptime/Home/Hooks/useUtils";
import "./index.css";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import StatBox from "../../../Components/StatBox";
+1 -1
View File
@@ -7,7 +7,7 @@ import { useTheme } from "@emotion/react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils";
import useUtils from "../Uptime/utils";
import useUtils from "../Uptime/Home/Hooks/useUtils";
import { useState } from "react";
import IconBox from "../../Components/IconBox";
/**
+1 -1
View File
@@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import { ChartBox } from "./styled";
import SkeletonLayout from "./skeleton";
import "./index.css";
import useUtils from "../utils";
import useUtils from "../Home/Hooks/useUtils";
import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import IconBox from "../../../Components/IconBox";
@@ -2,16 +2,16 @@ import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { createToast } from "../../../../../Utils/toastUtils";
import { logger } from "../../../../../Utils/Logger";
import { IconButton, Menu, MenuItem } from "@mui/material";
import {
deleteUptimeMonitor,
pauseUptimeMonitor,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../assets/icons/settings-bold.svg?react";
} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../../../assets/icons/settings-bold.svg?react";
import PropTypes from "prop-types";
import Dialog from "../../../Components/Dialog";
import Dialog from "../../../../../Components/Dialog";
const ActionsMenu = ({
monitor,
@@ -1,5 +1,6 @@
import { Box, Typography } from "@mui/material";
import { Stack, Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
@@ -13,44 +14,42 @@ import PropTypes from "prop-types";
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ url, title, percentageColor, percentage }) => {
const noTitle = title === undefined || title === url;
const theme = useTheme();
return (
<Box className="host">
<Box
display="inline-block"
<Stack>
<Stack
direction="row"
position="relative"
sx={{
fontWeight: 500,
"&:before": {
position: "absolute",
content: `""`,
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
right: "-10px",
top: "42%",
},
}}
alignItems="center"
gap={theme.spacing(4)}
>
{title}
</Box>
{percentageColor && percentage && (
<Typography
component="span"
sx={{
color: percentageColor,
fontWeight: 500,
/* TODO point font weight to theme */
ml: "15px",
}}
>
{percentage}%
</Typography>
)}
{!noTitle && <Box sx={{ opacity: 0.6 }}>{url}</Box>}
</Box>
{percentageColor && percentage && (
<>
<span
style={{
content: '""',
width: "4px",
height: "4px",
borderRadius: "50%",
backgroundColor: "gray",
opacity: 0.8,
}}
/>
<Typography
component="span"
sx={{
color: percentageColor,
fontWeight: 500,
}}
>
{percentage}%
</Typography>
</>
)}
</Stack>
<span style={{ opacity: 0.6 }}>{url}</span>
</Stack>
);
};
@@ -0,0 +1,46 @@
import { CircularProgress, Box } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const LoadingSpinner = ({ shouldRender }) => {
const theme = useTheme();
if (shouldRender === false) {
return;
}
return (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.primary.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="50%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.accent.main,
}}
/>
</Box>
</>
);
};
LoadingSpinner.propTypes = {
shouldRender: PropTypes.bool,
};
export default LoadingSpinner;
@@ -0,0 +1,43 @@
import { useState } from "react";
import Search from "../../../../../Components/Inputs/Search";
import { Box } from "@mui/material";
import useDebounce from "../../Hooks/useDebounce";
import { useEffect } from "react";
import PropTypes from "prop-types";
const SearchComponent = ({ monitors, onSearchChange, setIsSearching }) => {
const [localSearch, setLocalSearch] = useState("");
const debouncedSearch = useDebounce(localSearch, 500);
useEffect(() => {
onSearchChange(debouncedSearch);
setIsSearching(false);
}, [debouncedSearch, onSearchChange, setIsSearching]);
const handleSearch = (value) => {
setLocalSearch(value);
setIsSearching(true);
};
return (
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={localSearch}
handleInputChange={handleSearch}
/>
</Box>
);
};
SearchComponent.propTypes = {
monitors: PropTypes.array,
onSearchChange: PropTypes.func,
setIsSearching: PropTypes.func,
};
export default SearchComponent;
@@ -0,0 +1,36 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./statusBox";
import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton";
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
const theme = useTheme();
if (!shouldRender) return <SkeletonLayout shouldRender={shouldRender} />;
return (
<Stack
gap={theme.spacing(8)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title="up"
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
monitorsSummary: PropTypes.object.isRequired,
};
export default StatusBoxes;
@@ -0,0 +1,31 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -1,9 +1,9 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Arrow from "../../../assets/icons/top-right-arrow.svg?react";
import Background from "../../../assets/Images/background-grid.svg?react";
import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react";
import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react";
import Background from "../../../../../assets/Images/background-grid.svg?react";
import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react";
const StatusBox = ({ title, value }) => {
const theme = useTheme();
@@ -52,47 +52,48 @@ const StatusBox = ({ title, value }) => {
overflow="hidden"
>
<Box
sx={{
position: "absolute",
top: "-10%",
left: "5%",
pointerEvents: "none",
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
<Box
textTransform="uppercase"
fontSize={15}
letterSpacing={0.5}
color={theme.palette.primary.contrastTextTertiary}
mb={theme.spacing(8)}
>
{title}
</Box>
{icon}
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
component="span"
fontSize={20}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{ opacity: 0.3 }}
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
#
</Typography>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
{icon}
</Stack>
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontWeight={600}
color={color}
gap="2px"
>
{value}
<Typography
fontSize={20}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
opacity: 0.3,
}}
>
#
</Typography>
</Stack>
</Stack>
</Box>
);
@@ -1,57 +1,51 @@
// Components
import { Box, Stack, CircularProgress } from "@mui/material";
import Search from "../../../../Components/Inputs/Search";
import { Heading } from "../../../../Components/Heading";
import DataTable from "../../../../Components/Table";
import { Heading } from "../../../../../Components/Heading";
import DataTable from "../../../../../Components/Table";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
import Host from "../host";
import { StatusLabel } from "../../../../Components/Label";
import BarChart from "../../../../Components/Charts/BarChart";
import ActionsMenu from "../actionsMenu";
import Host from "../Host";
import { StatusLabel } from "../../../../../Components/Label";
import BarChart from "../../../../../Components/Charts/BarChart";
import ActionsMenu from "../ActionsMenu";
import { useState } from "react";
import SearchComponent from "../SearchComponent";
import CircularCount from "../../../../../Components/CircularCount";
import LoadingSpinner from "../LoadingSpinner";
import UptimeDataTableSkeleton from "./skeleton";
// Utils
import { useTheme } from "@emotion/react";
import useUtils from "../../utils";
import { useState, memo, useCallback } from "react";
import useUtils from "../../Hooks/useUtils";
import { useNavigate } from "react-router-dom";
import "../index.css";
import PropTypes from "prop-types";
const SearchComponent = memo(
({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => {
const [localSearch, setLocalSearch] = useState(debouncedSearch);
const handleSearch = useCallback(
(value) => {
setIsSearching(true);
setLocalSearch(value);
onSearchChange(value);
},
[onSearchChange, setIsSearching]
);
const MonitorDataTable = ({ shouldRender, isSearching, headers, filteredMonitors }) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={localSearch}
handleInputChange={handleSearch}
/>
</Box>
);
}
);
SearchComponent.displayName = "SearchComponent";
SearchComponent.propTypes = {
monitors: PropTypes.array,
debouncedSearch: PropTypes.string,
onSearchChange: PropTypes.func,
setIsSearching: PropTypes.func,
if (!shouldRender) return null;
return (
<Box position="relative">
<LoadingSpinner shouldRender={isSearching} />
<DataTable
headers={headers}
data={filteredMonitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
},
emptyView: "No monitors found",
}}
/>
</Box>
);
};
/**
@@ -79,31 +73,32 @@ SearchComponent.propTypes = {
* @param {string} props.search - Current search query
* @param {Function} props.setSearch - Callback to update search query
* @param {boolean} props.isSearching - Whether a search is in progress
* @param {Function} props.setIsSearching - Callback to update search state
* @param {Function} props.setIsLoading - Callback to update loading state
* @param {Function} props.triggerUpdate - Callback to trigger a data refresh
* @returns {JSX.Element} Rendered component
*/
const UptimeDataTable = ({
isAdmin,
isLoading,
monitors,
filteredMonitors,
monitorCount,
sort,
setSort,
debouncedSearch,
setSearch,
isSearching,
setIsSearching,
setIsLoading,
triggerUpdate,
}) => {
const { determineState } = useUtils();
const UptimeDataTable = (props) => {
// Utils
const {
isAdmin,
setIsLoading,
monitors,
filteredMonitors,
monitorCount,
sort,
setSort,
setSearch,
triggerUpdate,
monitorsAreLoading,
} = props;
const { determineState } = useUtils();
const theme = useTheme();
const navigate = useNavigate();
// Local state
const [isSearching, setIsSearching] = useState(false);
// Handlers
const handleSort = (field) => {
let order = "";
if (sort.field !== field) {
@@ -212,7 +207,6 @@ const UptimeDataTable = ({
),
},
];
return (
<Box
flex={1}
@@ -225,87 +219,35 @@ const UptimeDataTable = ({
gap={theme.spacing(2)}
>
<Heading component="h2">Uptime monitors</Heading>
{/* TODO Same as the one in Infrastructure. Create component */}
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "10000px",
fontSize: "12px",
fontWeight: 500,
}}
>
{monitorCount}
</Box>
<CircularCount count={monitorCount} />
<SearchComponent
monitorsAreLoading={monitorsAreLoading}
monitors={monitors}
debouncedSearch={debouncedSearch}
onSearchChange={setSearch}
setIsSearching={setIsSearching}
/>
</Stack>
<Box position="relative">
{(isSearching || isLoading) && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.primary.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="50%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.accent.main,
}}
/>
</Box>
</>
)}
<DataTable
headers={headers}
data={filteredMonitors}
config={{
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
},
emptyView: "No monitors found",
}}
/>
</Box>
<MonitorDataTable
shouldRender={!monitorsAreLoading}
isSearching={isSearching}
headers={headers}
filteredMonitors={filteredMonitors}
/>
<UptimeDataTableSkeleton shouldRender={monitorsAreLoading} />
</Box>
);
};
const MemoizedUptimeDataTable = memo(UptimeDataTable);
export default MemoizedUptimeDataTable;
UptimeDataTable.propTypes = {
isSearching: PropTypes.bool,
setIsSearching: PropTypes.func,
setSort: PropTypes.func,
setSearch: PropTypes.func,
setIsLoading: PropTypes.func,
triggerUpdate: PropTypes.func,
debouncedSearch: PropTypes.string,
onSearchChange: PropTypes.func,
isAdmin: PropTypes.bool,
isLoading: PropTypes.bool,
monitors: PropTypes.array,
@@ -315,11 +257,6 @@ UptimeDataTable.propTypes = {
field: PropTypes.string,
order: PropTypes.oneOf(["asc", "desc"]),
}),
setSort: PropTypes.func,
debouncedSearch: PropTypes.string,
setSearch: PropTypes.func,
isSearching: PropTypes.bool,
setIsSearching: PropTypes.func,
setIsLoading: PropTypes.func,
triggerUpdate: PropTypes.func,
};
export default UptimeDataTable;
@@ -0,0 +1,21 @@
import { Skeleton } from "@mui/material";
import PropTypes from "prop-types";
const UptimeDataTableSkeleton = ({ shouldRender }) => {
if (!shouldRender) return null;
return (
<Skeleton
variant="rounded"
width="100%"
height="100%"
flex={1}
/>
);
};
UptimeDataTableSkeleton.propTypes = {
shouldRender: PropTypes.bool.isRequired,
};
export default UptimeDataTableSkeleton;
@@ -0,0 +1,18 @@
import { useState, useEffect } from "react";
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default useDebounce;
@@ -0,0 +1,98 @@
import { useEffect, useState } from "react";
import { networkService } from "../../../../main";
import { createToast } from "../../../../Utils/toastUtils";
import { useTheme } from "@emotion/react";
const getMonitorWithPercentage = (monitor, theme) => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0 ? "0" : (monitor.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
? theme.palette.warning.main
: monitor.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
return {
id: monitor._id,
name: monitor.name,
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
monitor: monitor,
};
};
export const useMonitorFetch = ({
authToken,
teamId,
limit,
page,
rowsPerPage,
filter,
field,
order,
triggerUpdate,
}) => {
const [monitorsAreLoading, setMonitorsAreLoading] = useState(false);
const [monitors, setMonitors] = useState([]);
const [filteredMonitors, setFilteredMonitors] = useState([]);
const [monitorsSummary, setMonitorsSummary] = useState({});
const theme = useTheme();
useEffect(() => {
const fetchMonitors = async () => {
try {
setMonitorsAreLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken,
teamId,
limit,
types: ["http", "ping", "docker", "port"],
page,
rowsPerPage,
filter,
field,
order,
});
const { monitors, filteredMonitors, summary } = res.data.data;
const mappedMonitors = filteredMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(monitors);
setFilteredMonitors(mappedMonitors);
setMonitorsSummary(summary);
} catch (error) {
createToast({
body: error.message,
});
} finally {
setMonitorsAreLoading(false);
}
};
fetchMonitors();
}, [
authToken,
teamId,
limit,
field,
filter,
order,
page,
rowsPerPage,
theme,
triggerUpdate,
]);
return { monitors, filteredMonitors, monitorsSummary, monitorsAreLoading };
};
export default useMonitorFetch;
@@ -1,47 +0,0 @@
import { Skeleton } from "@mui/material";
import DataTable from "../../../../../Components/Table";
const ROWS_NUMBER = 7;
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
const TableSkeleton = () => {
/* TODO Skeleton does not follow light and dark theme */
const headers = [
{
id: "name",
content: "Host",
render: () => <Skeleton />,
},
{
id: "status",
content: "Status",
render: () => <Skeleton />,
},
{
id: "responseTime",
content: "Response Time",
render: () => <Skeleton />,
},
{
id: "type",
content: "Type",
render: () => <Skeleton />,
},
{
id: "actions",
content: "Actions",
render: () => <Skeleton />,
},
];
return (
<DataTable
headers={headers}
data={ROWS_ARRAY}
/>
);
};
export { TableSkeleton };
-59
View File
@@ -1,59 +0,0 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
import PropTypes from "prop-types";
const Fallback = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const mode = useSelector((state) => state.ui.mode);
return (
<Stack
alignItems="center"
backgroundColor={theme.palette.primary.main}
p={theme.spacing(30)}
pt={theme.spacing(25)}
gap={theme.spacing(2)}
border={1}
borderRadius={theme.shape.borderRadius}
borderColor={theme.palette.primary.lowContrast}
color={theme.palette.primary.contrastTextSecondary}
>
<Box pb={theme.spacing(20)}>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
component="h2"
variant="h2"
fontWeight={500}
>
No monitors found to display
</Typography>
<Typography variant="body1">
It looks like you dont have any monitors set up yet.
</Typography>
{isAdmin && (
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
sx={{ mt: theme.spacing(12) }}
>
Create your first monitor
</Button>
)}
</Stack>
);
};
Fallback.propTypes = {
isAdmin: PropTypes.bool,
};
export default Fallback;
-6
View File
@@ -1,6 +0,0 @@
/* TODO take these from here and declare using emotion. Plus, the values should live in the theme */
.monitors .MuiStack-root > button:not(.MuiIconButton-root) {
font-size: var(--env-var-font-size-medium);
height: var(--env-var-height-2);
min-width: fit-content;
}
+88 -189
View File
@@ -1,126 +1,70 @@
// Components
import { Box, Stack, Button } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Greeting from "../../../Utils/greeting";
import SkeletonLayout from "./skeleton";
import Fallback from "./fallback";
import StatusBox from "./StatusBox";
import UptimeDataTable from "./UptimeDataTable";
import StatusBoxes from "./Components/StatusBoxes";
import UptimeDataTable from "./Components/UptimeDataTable";
import Pagination from "../../../Components/Table/TablePagination";
// MUI Components
import { Stack, Box, Button } from "@mui/material";
// Utils
import { useTheme } from "@emotion/react";
import { useEffect, useState, useCallback, useMemo } from "react";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import { useState, useCallback } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import useDebounce from "../../../Utils/debounce";
import { networkService } from "../../../main";
import useMonitorFetch from "./Hooks/useMonitorFetch";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import PropTypes from "prop-types";
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
const CreateMonitorButton = ({ shouldRender }) => {
// Utils
const navigate = useNavigate();
if (shouldRender === false) {
return;
}
return (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
>
Create new
</Button>
</Box>
);
};
CreateMonitorButton.propTypes = {
shouldRender: PropTypes.bool,
};
const UptimeMonitors = () => {
// Redux state
const { authToken, user } = useSelector((state) => state.auth);
const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage);
// Local state
const [sort, setSort] = useState({});
const [search, setSearch] = useState("");
const [page, setPage] = useState(0);
const [sort, setSort] = useState({});
const [isSearching, setIsSearching] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
const [monitors, setMonitors] = useState([]);
const [filteredMonitors, setFilteredMonitors] = useState([]);
const [monitorsSummary, setMonitorsSummary] = useState({});
// Utils
const debouncedFilter = useDebounce(search, 500);
const dispatch = useDispatch();
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const dispatch = useDispatch();
const authState = useSelector((state) => state.auth);
const fetchParams = useMemo(
() => ({
authToken: authState.authToken,
teamId: authState.user.teamId,
sort: { field: sort.field, order: sort.order },
filter: debouncedFilter,
page,
rowsPerPage,
}),
[authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage]
);
const getMonitorWithPercentage = useCallback((monitor, theme) => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
? theme.palette.warning.main
: theme.palette.success.main;
}
return {
id: monitor._id,
name: monitor.name,
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
monitor: monitor,
};
}, []);
const fetchMonitors = useCallback(async () => {
try {
setIsLoading(true);
const config = fetchParams;
const res = await networkService.getMonitorsByTeamId({
authToken: config.authToken,
teamId: config.teamId,
limit: 25,
types: ["http", "ping", "docker", "port"],
page: config.page,
rowsPerPage: config.rowsPerPage,
filter: config.filter,
field: config.sort.field,
order: config.sort.order,
});
const { monitors, filteredMonitors, summary } = res.data.data;
const mappedMonitors = filteredMonitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setMonitors(monitors);
setFilteredMonitors(mappedMonitors);
setMonitorsSummary(summary);
} catch (error) {
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
setIsSearching(false);
}
}, [fetchParams, getMonitorWithPercentage, theme]);
useEffect(() => {
fetchMonitors();
}, [fetchMonitors, monitorUpdateTrigger]);
// Handlers
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
@@ -138,103 +82,58 @@ const UptimeMonitors = () => {
const triggerUpdate = useCallback(() => {
setMonitorUpdateTrigger((prev) => !prev);
}, []);
const teamId = user.teamId;
const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } =
useMonitorFetch({
authToken,
teamId,
limit: 25,
page,
rowsPerPage: rowsPerPage,
filter: search,
field: sort.field,
order: sort.order,
triggerUpdate: monitorUpdateTrigger,
});
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
const hasMonitors = totalMonitors > 0;
const canAddMonitor = isAdmin && hasMonitors;
return (
<Stack
className="monitors"
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
{canAddMonitor && (
<>
<Button
variant="contained"
color="accent"
onClick={() => {
navigate("/uptime/create");
}}
sx={{
fontWeight: 500,
whiteSpace: "nowrap",
/* TODO IMPORTANT this should be applied to all buttons */
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.contrastText}`,
outlineOffset: 4,
},
}}
>
Create new
</Button>
</>
)}
</Stack>
<Greeting type="uptime" />
</Box>
{
<>
{!isLoading && !hasMonitors && <Fallback isAdmin={isAdmin} />}
{isLoading ? (
<SkeletonLayout />
) : (
hasMonitors && (
<>
<Stack
gap={theme.spacing(8)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title="up"
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
<UptimeDataTable
isAdmin={isAdmin}
isLoading={isLoading}
filteredMonitors={filteredMonitors}
monitors={monitors}
monitorCount={totalMonitors}
sort={sort}
setSort={setSort}
debouncedSearch={debouncedFilter}
setSearch={setSearch}
isSearching={isSearching}
setIsSearching={setIsSearching}
setIsLoading={setIsLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
itemCount={totalMonitors}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</>
)
)}
</>
}
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorButton shouldRender={true} />
<Greeting type="uptime" />
<StatusBoxes
monitorsSummary={monitorsSummary}
shouldRender={!monitorsAreLoading}
/>
<UptimeDataTable
isAdmin={isAdmin}
isLoading={isLoading}
setIsLoading={setIsLoading}
filteredMonitors={filteredMonitors}
monitors={monitors}
monitorCount={totalMonitors}
sort={sort}
setSort={setSort}
setSearch={setSearch}
isSearching={isSearching}
setIsSearching={setIsSearching}
monitorsAreLoading={monitorsAreLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
itemCount={totalMonitors}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
);
};
@@ -8,7 +8,6 @@ class DistributedUptimeController {
async resultsCallback(req, res, next) {
try {
console.log(req.body);
res.status(200).json({ message: "OK" });
} catch (error) {
throw handleError(error, SERVICE_NAME, "resultsCallback");
+1 -1
View File
@@ -436,7 +436,7 @@ class MonitorController {
monitor.isActive = !monitor.isActive;
monitor.status = undefined;
monitor.save();
return res.ssuccess({
return res.success({
msg: monitor.isActive
? successMessages.MONITOR_RESUME
: successMessages.MONITOR_PAUSE,
+19 -2
View File
@@ -497,7 +497,6 @@ const getMonitorById = async (monitorId) => {
const getMonitorsByTeamId = async (req) => {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
@@ -512,7 +511,6 @@ const getMonitorsByTeamId = async (req) => {
}
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const results = await Monitor.aggregate([
{ $match: matchStage },
@@ -839,3 +837,22 @@ export {
groupChecksByTime,
calculateGroupStats,
};
// limit 25
// page 1
// rowsPerPage 25
// filter undefined
// field name
// order asc
// skip 25
// sort { name: 1 }
// filteredMonitors []
// limit 25
// page NaN
// rowsPerPage 25
// filter undefined
// field name
// order asc
// skip 0
// sort { name: 1 }