Merge pull request #1663 from bluewave-labs/fix/fe/consistent-initial-state
Fix/fe/consistent initial state
This commit is contained in:
29 files changed
+313
-239
No files matched your search
@@ -19,6 +19,22 @@
|
||||
font-size: var(--env-var-font-size-medium);
|
||||
}
|
||||
|
||||
[class*="fallback__"] .background-pattern-svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.fallback__status > .MuiStack-root {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -26,20 +26,3 @@
|
||||
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-layout > div:has(> [class*="fallback__"]) .background-pattern-svg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 100%;
|
||||
max-height: 800px;
|
||||
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import ConfigButton from "./ConfigButton";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
|
||||
const MonitorStatusHeader = ({ shouldRender = true, isAdmin, monitor }) => {
|
||||
const theme = useTheme();
|
||||
const { statusColor, statusMsg, determineState } = useUtils();
|
||||
if (!shouldRender) {
|
||||
@@ -21,7 +21,7 @@ const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack>
|
||||
<Typography variant="h1">{monitor.name}</Typography>
|
||||
<Typography variant="h1">{monitor?.name}</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
@@ -39,16 +39,16 @@ const MonitorHeader = ({ shouldRender = true, isAdmin, monitor }) => {
|
||||
</Stack>
|
||||
<ConfigButton
|
||||
shouldRender={isAdmin}
|
||||
monitorId={monitor._id}
|
||||
monitorId={monitor?._id}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorHeader.propTypes = {
|
||||
MonitorStatusHeader.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
isAdmin: PropTypes.bool,
|
||||
monitor: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MonitorHeader;
|
||||
export default MonitorStatusHeader;
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
|
||||
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
/**
|
||||
* Fallback component to display a fallback UI for network errors
|
||||
*
|
||||
* @returns {JSX.Element} The rendered fallback UI.
|
||||
*/
|
||||
|
||||
const NetworkErrorFallback = () => {
|
||||
const theme = useTheme();
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="page-speed"
|
||||
position="relative"
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
overflow="hidden"
|
||||
sx={{
|
||||
borderStyle: "dashed",
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
alignItems="center"
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
width: "fit-content",
|
||||
margin: "auto",
|
||||
marginTop: "100px",
|
||||
}}
|
||||
>
|
||||
{mode === "light" ? (
|
||||
<Skeleton style={{ zIndex: 1 }} />
|
||||
) : (
|
||||
<SkeletonDark style={{ zIndex: 1 }} />
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "100%",
|
||||
maxWidth: "800px",
|
||||
height: "100%",
|
||||
maxHeight: "800px",
|
||||
backgroundPosition: "center",
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
alignItems="center"
|
||||
maxWidth={"300px"}
|
||||
zIndex={1}
|
||||
>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
Network error
|
||||
</Typography>
|
||||
<Typography>Please check your connection</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkErrorFallback;
|
||||
@@ -6,8 +6,8 @@ import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react"
|
||||
|
||||
Pagination.propTypes = {
|
||||
paginationLabel: PropTypes.string, // Label for the pagination.
|
||||
itemCount: PropTypes.number.isRequired, // Total number of items for pagination.
|
||||
page: PropTypes.number.isRequired, // Current page index.
|
||||
itemCount: PropTypes.number, // Total number of items for pagination.
|
||||
page: PropTypes.number, // Current page index.
|
||||
rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page.
|
||||
handleChangePage: PropTypes.func.isRequired, // Function to handle page changes.
|
||||
handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page.
|
||||
@@ -29,9 +29,9 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25];
|
||||
*/
|
||||
function Pagination({
|
||||
paginationLabel,
|
||||
itemCount,
|
||||
page,
|
||||
rowsPerPage,
|
||||
itemCount = 0,
|
||||
page = 0,
|
||||
rowsPerPage = 5,
|
||||
handleChangePage,
|
||||
handleChangeRowsPerPage,
|
||||
}) {
|
||||
|
||||
@@ -32,7 +32,14 @@ import { useTheme } from "@emotion/react";
|
||||
* @returns {JSX.Element} The rendered table component.
|
||||
*/
|
||||
|
||||
const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => {
|
||||
const DataTable = ({
|
||||
headers = [],
|
||||
data = [],
|
||||
config = {
|
||||
emptyView: "No data",
|
||||
onRowClick: () => {},
|
||||
},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
if ((headers?.length ?? 0) === 0) {
|
||||
return "No data";
|
||||
@@ -117,9 +124,9 @@ DataTable.propTypes = {
|
||||
render: PropTypes.func.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
data: PropTypes.array,
|
||||
config: PropTypes.shape({
|
||||
onRowClick: PropTypes.func.isRequired,
|
||||
onRowClick: PropTypes.func,
|
||||
rowSX: PropTypes.object,
|
||||
emptyView: PropTypes.node,
|
||||
}),
|
||||
|
||||
@@ -118,8 +118,6 @@ const PieChart = ({ audits }) => {
|
||||
const [highlightedItem, setHighLightedItem] = useState(null);
|
||||
const [expand, setExpand] = useState(false);
|
||||
|
||||
if (typeof audits === "undefined") return null;
|
||||
|
||||
/**
|
||||
* Retrieves color properties based on the performance value.
|
||||
*
|
||||
@@ -166,6 +164,8 @@ const PieChart = ({ audits }) => {
|
||||
*/
|
||||
let performance = 0;
|
||||
const getPieData = (audits) => {
|
||||
if (typeof audits === "undefined") return undefined;
|
||||
|
||||
let data = [];
|
||||
let startAngle = 0;
|
||||
const padding = 3; // padding between arcs
|
||||
|
||||
@@ -7,84 +7,84 @@ import PropTypes from "prop-types";
|
||||
const PieChartLegend = ({ audits }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (typeof audits === "undefined") return null;
|
||||
return (
|
||||
<LegendBox
|
||||
icon={<SpeedometerIcon />}
|
||||
header="Performance metrics"
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{Object.keys(audits).map((key) => {
|
||||
if (key === "_id") return;
|
||||
{typeof audits !== "undefined" &&
|
||||
Object.keys(audits).map((key) => {
|
||||
if (key === "_id") return;
|
||||
|
||||
let audit = audits[key];
|
||||
let score = audit.score * 100;
|
||||
let bg =
|
||||
score >= 90
|
||||
? theme.palette.success.main
|
||||
: score >= 50
|
||||
? theme.palette.warning.main
|
||||
: score >= 0
|
||||
? theme.palette.error.main
|
||||
: theme.palette.tertiary.main;
|
||||
let audit = audits[key];
|
||||
let score = audit.score * 100;
|
||||
let bg =
|
||||
score >= 90
|
||||
? theme.palette.success.main
|
||||
: score >= 50
|
||||
? theme.palette.warning.main
|
||||
: score >= 0
|
||||
? theme.palette.error.main
|
||||
: theme.palette.tertiary.main;
|
||||
|
||||
// Find the position where the number ends and the unit begins
|
||||
const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
|
||||
let value;
|
||||
let unit;
|
||||
if (match) {
|
||||
value = match[1];
|
||||
match[2] === "s" ? (unit = "seconds") : (unit = match[2]);
|
||||
} else {
|
||||
value = audit.displayValue;
|
||||
}
|
||||
// Find the position where the number ends and the unit begins
|
||||
const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
|
||||
let value;
|
||||
let unit;
|
||||
if (match) {
|
||||
value = match[1];
|
||||
match[2] === "s" ? (unit = "seconds") : (unit = match[2]);
|
||||
} else {
|
||||
value = audit.displayValue;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
key={`${key}-box`}
|
||||
justifyContent="space-between"
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
p={theme.spacing(3)}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
lineHeight={1}
|
||||
mb={1}
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{audit.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={14}
|
||||
fontWeight={500}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{value}
|
||||
return (
|
||||
<Stack
|
||||
flex={1}
|
||||
key={`${key}-box`}
|
||||
justifyContent="space-between"
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
p={theme.spacing(3)}
|
||||
border={1}
|
||||
borderColor={theme.palette.primary.lowContrast}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
fontSize={12}
|
||||
fontWeight={500}
|
||||
lineHeight={1}
|
||||
mb={1}
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{audit.title}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
ml={2}
|
||||
fontSize={14}
|
||||
fontWeight={500}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{unit}
|
||||
{value}
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
ml={2}
|
||||
>
|
||||
{unit}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
width={4}
|
||||
backgroundColor={bg}
|
||||
borderRadius={4}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Box
|
||||
width={4}
|
||||
backgroundColor={bg}
|
||||
borderRadius={4}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</LegendBox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,10 +15,6 @@ const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) =
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
if (typeof monitor === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,18 +5,14 @@ import { Typography } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PieChartLegend from "../Charts/PieChartLegend";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const PerformanceReport = ({ shouldRender, audits }) => {
|
||||
console.log("shouldRender", shouldRender);
|
||||
const theme = useTheme();
|
||||
|
||||
if (!shouldRender) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
if (typeof audits === "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartBox
|
||||
icon={<PerformanceIcon />}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Components
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import { Stack, Typography, Skeleton } from "@mui/material";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
|
||||
import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes";
|
||||
import PageSpeedAreaChart from "./Components/PageSpeedAreaChart";
|
||||
import PerformanceReport from "./Components/PerformanceReport";
|
||||
import Fallback from "../../../Components/Fallback";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
@@ -31,8 +30,6 @@ const PageSpeedDetails = () => {
|
||||
monitorId,
|
||||
});
|
||||
|
||||
console.log(monitor, audits, isLoading);
|
||||
|
||||
const [metrics, setMetrics] = useState({
|
||||
accessibility: true,
|
||||
bestPractices: true,
|
||||
|
||||
@@ -2,8 +2,6 @@ import { Grid, Grid2 } from "@mui/material";
|
||||
import Card from "../Card";
|
||||
|
||||
const MonitorGrid = ({ shouldRender, monitors }) => {
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
|
||||
@@ -6,6 +6,7 @@ const useMonitorsFetch = ({ authToken, teamId }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [summary, setSummary] = useState({});
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
@@ -27,6 +28,7 @@ const useMonitorsFetch = ({ authToken, teamId }) => {
|
||||
setSummary(res.data.data.summary);
|
||||
}
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
@@ -37,7 +39,7 @@ const useMonitorsFetch = ({ authToken, teamId }) => {
|
||||
|
||||
fetchMonitors();
|
||||
}, [authToken, teamId]);
|
||||
return { isLoading, monitors, summary };
|
||||
return { isLoading, monitors, summary, networkError };
|
||||
};
|
||||
|
||||
export default useMonitorsFetch;
|
||||
@@ -11,6 +11,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import useMonitorsFetch from "./Hooks/useMonitorsFetch";
|
||||
import NetworkErrorFallback from "../../../Components/NetworkErrorFallback";
|
||||
|
||||
// Constants
|
||||
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
|
||||
@@ -20,11 +21,15 @@ const PageSpeed = () => {
|
||||
const isAdmin = useIsAdmin();
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
|
||||
const { isLoading, monitors, summary } = useMonitorsFetch({
|
||||
const { isLoading, monitors, summary, networkError } = useMonitorsFetch({
|
||||
authToken: authToken,
|
||||
teamId: user.teamId,
|
||||
});
|
||||
|
||||
if (networkError === true) {
|
||||
return <NetworkErrorFallback />;
|
||||
}
|
||||
|
||||
if (!isLoading && monitors?.length === 0) {
|
||||
return (
|
||||
<Fallback
|
||||
|
||||
@@ -123,7 +123,7 @@ const ChartBoxes = ({
|
||||
icon={<AverageResponseIcon />}
|
||||
header="Average Response Time"
|
||||
>
|
||||
<ResponseGaugeChart avgResponseTime={monitor.avgResponseTime ?? 0} />
|
||||
<ResponseGaugeChart avgResponseTime={monitor?.avgResponseTime ?? 0} />
|
||||
</ChartBox>
|
||||
</Stack>
|
||||
);
|
||||
@@ -133,7 +133,7 @@ export default ChartBoxes;
|
||||
|
||||
ChartBoxes.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
monitor: PropTypes.object,
|
||||
dateRange: PropTypes.string.isRequired,
|
||||
uiTimezone: PropTypes.string.isRequired,
|
||||
dateFormat: PropTypes.string.isRequired,
|
||||
|
||||
@@ -19,7 +19,7 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => {
|
||||
<BarChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={monitor.groupedDownChecks}
|
||||
data={monitor?.groupedDownChecks}
|
||||
onMouseEnter={() => {
|
||||
setChartHovered(true);
|
||||
onBarHover({ time: null, totalChecks: 0 });
|
||||
|
||||
@@ -15,7 +15,7 @@ const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => {
|
||||
header="Response Times"
|
||||
>
|
||||
<MonitorDetailsAreaChart
|
||||
checks={monitor.groupedChecks ?? []}
|
||||
checks={monitor?.groupedChecks ?? []}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</ChartBox>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { formatDateWithTz } from "../../../../../Utils/timeUtils";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
const ResponseTable = ({
|
||||
shouldRender = true,
|
||||
checks,
|
||||
checks = [],
|
||||
checksCount,
|
||||
uiTimezone,
|
||||
page,
|
||||
@@ -77,8 +77,8 @@ const ResponseTable = ({
|
||||
|
||||
ResponseTable.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
checks: PropTypes.array.isRequired,
|
||||
checksCount: PropTypes.number.isRequired,
|
||||
checks: PropTypes.array,
|
||||
checksCount: PropTypes.number,
|
||||
uiTimezone: PropTypes.string.isRequired,
|
||||
page: PropTypes.number.isRequired,
|
||||
setPage: PropTypes.func.isRequired,
|
||||
|
||||
@@ -8,6 +8,7 @@ import useUtils from "../../../Monitors/Hooks/useUtils";
|
||||
const UptimeStatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => {
|
||||
const theme = useTheme();
|
||||
const { determineState } = useUtils();
|
||||
|
||||
const { time: streakTime, units: streakUnits } = getHumanReadableDuration(
|
||||
monitor?.uptimeStreak
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ const useCertificateFetch = ({
|
||||
certificateDateFormat,
|
||||
uiTimezone,
|
||||
}) => {
|
||||
const [certificateExpiry, setCertificateExpiry] = useState("N/A");
|
||||
const [certificateExpiry, setCertificateExpiry] = useState(undefined);
|
||||
const [certificateIsLoading, setCertificateIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,8 +10,8 @@ export const useChecksFetch = ({
|
||||
page,
|
||||
rowsPerPage,
|
||||
}) => {
|
||||
const [checks, setChecks] = useState([]);
|
||||
const [checksCount, setChecksCount] = useState(0);
|
||||
const [checks, setChecks] = useState(undefined);
|
||||
const [checksCount, setChecksCount] = useState(undefined);
|
||||
const [checksAreLoading, setChecksAreLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => {
|
||||
const [monitorIsLoading, setMonitorsIsLoading] = useState(false);
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,13 +2,18 @@ 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 { useEffect, useRef } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const SearchComponent = ({ monitors, onSearchChange, setIsSearching }) => {
|
||||
const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => {
|
||||
const isFirstRender = useRef(true);
|
||||
const [localSearch, setLocalSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(localSearch, 500);
|
||||
useEffect(() => {
|
||||
if (isFirstRender.current === true) {
|
||||
isFirstRender.current = false;
|
||||
return;
|
||||
}
|
||||
onSearchChange(debouncedSearch);
|
||||
setIsSearching(false);
|
||||
}, [debouncedSearch, onSearchChange, setIsSearching]);
|
||||
|
||||
@@ -7,12 +7,9 @@ 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";
|
||||
import { Heading } from "../../../../../Components/Heading";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -20,35 +17,6 @@ import useUtils from "../../Hooks/useUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorDataTable = ({ shouldRender, isSearching, headers, filteredMonitors }) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities
|
||||
* @param {Object} props - Component props
|
||||
@@ -78,34 +46,29 @@ const MonitorDataTable = ({ shouldRender, isSearching, headers, filteredMonitors
|
||||
* @param {Function} props.triggerUpdate - Callback to trigger a data refresh
|
||||
* @returns {JSX.Element} Rendered component
|
||||
*/
|
||||
const UptimeDataTable = (props) => {
|
||||
const UptimeDataTable = ({
|
||||
isAdmin,
|
||||
isSearching,
|
||||
setIsLoading,
|
||||
filteredMonitors,
|
||||
sort,
|
||||
setSort,
|
||||
triggerUpdate,
|
||||
monitorsAreLoading,
|
||||
}) => {
|
||||
// Utils
|
||||
|
||||
const {
|
||||
isAdmin,
|
||||
setIsLoading,
|
||||
monitors,
|
||||
filteredMonitors,
|
||||
monitorCount,
|
||||
sort,
|
||||
setSort,
|
||||
setSearch,
|
||||
triggerUpdate,
|
||||
monitorsAreLoading,
|
||||
} = props;
|
||||
const navigate = useNavigate();
|
||||
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) {
|
||||
if (sort?.field !== field) {
|
||||
order = "desc";
|
||||
} else {
|
||||
order = sort.order === "asc" ? "desc" : "asc";
|
||||
order = sort?.order === "asc" ? "desc" : "asc";
|
||||
}
|
||||
setSort({ field, order });
|
||||
};
|
||||
@@ -124,10 +87,10 @@ const UptimeDataTable = (props) => {
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
style={{
|
||||
visibility: sort.field === "name" ? "visible" : "hidden",
|
||||
visibility: sort?.field === "name" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
{sort?.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon />
|
||||
@@ -160,10 +123,10 @@ const UptimeDataTable = (props) => {
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
style={{
|
||||
visibility: sort.field === "status" ? "visible" : "hidden",
|
||||
visibility: sort?.field === "status" ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
{sort.order === "asc" ? (
|
||||
{sort?.order === "asc" ? (
|
||||
<ArrowUpwardRoundedIcon fontSize="18px" />
|
||||
) : (
|
||||
<ArrowDownwardRoundedIcon fontSize="18px" />
|
||||
@@ -210,41 +173,37 @@ const UptimeDataTable = (props) => {
|
||||
),
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Box
|
||||
flex={1}
|
||||
py={theme.spacing(8)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
mb={theme.spacing(8)}
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Heading component="h2">Uptime monitors</Heading>
|
||||
<CircularCount count={monitorCount} />
|
||||
<SearchComponent
|
||||
monitorsAreLoading={monitorsAreLoading}
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<MonitorDataTable
|
||||
shouldRender={!monitorsAreLoading}
|
||||
isSearching={isSearching}
|
||||
if (monitorsAreLoading) {
|
||||
return <UptimeDataTableSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box position="relative">
|
||||
<LoadingSpinner shouldRender={isSearching} />
|
||||
<DataTable
|
||||
headers={headers}
|
||||
filteredMonitors={filteredMonitors}
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
<UptimeDataTableSkeleton shouldRender={monitorsAreLoading} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
UptimeDataTable.propTypes = {
|
||||
isSearching: PropTypes.bool,
|
||||
setIsSearching: PropTypes.func,
|
||||
setSort: PropTypes.func,
|
||||
setSearch: PropTypes.func,
|
||||
setIsLoading: PropTypes.func,
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Skeleton } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const UptimeDataTableSkeleton = ({ shouldRender }) => {
|
||||
if (!shouldRender) return null;
|
||||
|
||||
const UptimeDataTableSkeleton = () => {
|
||||
return (
|
||||
<Skeleton
|
||||
variant="rounded"
|
||||
@@ -14,8 +11,4 @@ const UptimeDataTableSkeleton = ({ shouldRender }) => {
|
||||
);
|
||||
};
|
||||
|
||||
UptimeDataTableSkeleton.propTypes = {
|
||||
shouldRender: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default UptimeDataTableSkeleton;
|
||||
@@ -43,9 +43,10 @@ export const useMonitorFetch = ({
|
||||
triggerUpdate,
|
||||
}) => {
|
||||
const [monitorsAreLoading, setMonitorsAreLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [filteredMonitors, setFilteredMonitors] = useState([]);
|
||||
const [monitorsSummary, setMonitorsSummary] = useState({});
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [filteredMonitors, setFilteredMonitors] = useState(undefined);
|
||||
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -72,6 +73,7 @@ export const useMonitorFetch = ({
|
||||
setFilteredMonitors(mappedMonitors);
|
||||
setMonitorsSummary(summary);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
@@ -92,7 +94,13 @@ export const useMonitorFetch = ({
|
||||
theme,
|
||||
triggerUpdate,
|
||||
]);
|
||||
return { monitors, filteredMonitors, monitorsSummary, monitorsAreLoading };
|
||||
return {
|
||||
monitors,
|
||||
filteredMonitors,
|
||||
monitorsSummary,
|
||||
monitorsAreLoading,
|
||||
networkError,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMonitorFetch;
|
||||
@@ -2,6 +2,7 @@ import { useTheme } from "@mui/material";
|
||||
|
||||
const useUtils = () => {
|
||||
const determineState = (monitor) => {
|
||||
if (typeof monitor === "undefined") return "pending";
|
||||
if (monitor.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
|
||||
@@ -6,6 +6,11 @@ import UptimeDataTable from "./Components/UptimeDataTable";
|
||||
import Pagination from "../../../Components/Table/TablePagination";
|
||||
import CreateMonitorHeader from "../../../Components/CreateMonitorHeader";
|
||||
import Fallback from "../../../Components/Fallback";
|
||||
import NetworkErrorFallback from "../../../Components/NetworkErrorFallback";
|
||||
import SearchComponent from "./Components/SearchComponent";
|
||||
|
||||
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
|
||||
|
||||
// MUI Components
|
||||
import { Stack, Box, Button } from "@mui/material";
|
||||
|
||||
@@ -53,9 +58,9 @@ const UptimeMonitors = () => {
|
||||
const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage);
|
||||
|
||||
// Local state
|
||||
const [search, setSearch] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [sort, setSort] = useState({});
|
||||
const [search, setSearch] = useState(undefined);
|
||||
const [page, setPage] = useState(undefined);
|
||||
const [sort, setSort] = useState(undefined);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
|
||||
@@ -85,22 +90,28 @@ const UptimeMonitors = () => {
|
||||
}, []);
|
||||
|
||||
const teamId = user.teamId;
|
||||
|
||||
const { monitorsAreLoading, monitors, filteredMonitors, monitorsSummary } =
|
||||
useMonitorsFetch({
|
||||
authToken,
|
||||
teamId,
|
||||
limit: 25,
|
||||
page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: search,
|
||||
field: sort.field,
|
||||
order: sort.order,
|
||||
triggerUpdate: monitorUpdateTrigger,
|
||||
});
|
||||
const {
|
||||
monitorsAreLoading,
|
||||
monitors,
|
||||
filteredMonitors,
|
||||
monitorsSummary,
|
||||
networkError,
|
||||
} = useMonitorsFetch({
|
||||
authToken,
|
||||
teamId,
|
||||
limit: 25,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
filter: search,
|
||||
field: sort?.field,
|
||||
order: sort?.order,
|
||||
triggerUpdate: monitorUpdateTrigger,
|
||||
});
|
||||
const totalMonitors = monitorsSummary?.totalMonitors ?? 0;
|
||||
|
||||
if (!isLoading && !monitorsAreLoading && monitors?.length === 0) {
|
||||
if (networkError) return <NetworkErrorFallback />;
|
||||
|
||||
if (!isLoading && !monitorsAreLoading && totalMonitors === 0) {
|
||||
return (
|
||||
<Fallback
|
||||
vowelStart={true}
|
||||
@@ -128,6 +139,19 @@ const UptimeMonitors = () => {
|
||||
monitorsSummary={monitorsSummary}
|
||||
shouldRender={!monitorsAreLoading}
|
||||
/>
|
||||
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
shouldRender={monitors?.length > 0 && !monitorsAreLoading}
|
||||
monitorCount={totalMonitors}
|
||||
heading={"Uptime monitors"}
|
||||
></MonitorCountHeader>
|
||||
<SearchComponent
|
||||
monitors={monitors}
|
||||
onSearchChange={setSearch}
|
||||
setIsSearching={setIsSearching}
|
||||
/>
|
||||
</Stack>
|
||||
<UptimeDataTable
|
||||
isAdmin={isAdmin}
|
||||
isLoading={isLoading}
|
||||
@@ -139,7 +163,6 @@ const UptimeMonitors = () => {
|
||||
setSort={setSort}
|
||||
setSearch={setSearch}
|
||||
isSearching={isSearching}
|
||||
setIsSearching={setIsSearching}
|
||||
monitorsAreLoading={monitorsAreLoading}
|
||||
triggerUpdate={triggerUpdate}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,6 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
|
||||
import { clearAuthState } from "../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { createToast } from "./toastUtils";
|
||||
|
||||
class NetworkService {
|
||||
constructor(store, dispatch, navigate) {
|
||||
|
||||
Reference in new issue
Block a user