Merge pull request #987 from bluewave-labs/feat/loadingSpinners

Feat/loading spinners
This commit is contained in:
Alexander Holliday
2024-10-22 09:58:09 +08:00
committed by GitHub
11 changed files with 391 additions and 247 deletions

View File

@@ -38,6 +38,7 @@ const SearchAdornment = () => {
);
};
//TODO keep search state inside of component
const Search = ({
id,
options,

View File

@@ -0,0 +1,32 @@
import { useTheme } from "@emotion/react";
import PlaceholderLight from "../../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../../assets/Images/data_placeholder_dark.svg?react";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
const Empty = ({ styles, mode }) => {
const theme = useTheme();
return (
<Box sx={{ ...styles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.text.secondary}
>
No incidents recorded yet.
</Typography>
</Box>
);
};
Empty.propTypes = {
styles: PropTypes.object,
mode: PropTypes.string,
};
export { Empty };

View File

@@ -0,0 +1,21 @@
import { Skeleton /* , Stack */ } from "@mui/material";
const IncidentSkeleton = () => {
return (
<>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={100}
/>
</>
);
};
export { IncidentSkeleton };

View File

@@ -24,6 +24,8 @@ import { useTheme } from "@emotion/react";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
import { Empty } from "./Empty/Empty";
import { IncidentSkeleton } from "./Skeleton/Skeleton";
const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
@@ -37,6 +39,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
page: 0,
rowsPerPage: 14,
});
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setPaginationController((prevPaginationController) => ({
@@ -51,6 +54,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
return;
}
try {
setIsLoading(true);
let res;
if (selectedMonitor === "0") {
res = await networkService.getChecksByTeam({
@@ -79,6 +83,8 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
setChecksCount(res.data.data.checksCount);
} catch (error) {
logger.error(error);
} finally {
setIsLoading(false);
}
};
fetchPage();
@@ -129,24 +135,20 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
p: theme.spacing(30),
};
const hasChecks = checks?.length === 0;
const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0";
const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0";
return (
<>
{checks?.length === 0 && selectedMonitor === "0" ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.text.secondary}
>
No incidents recorded yet.
</Typography>
</Box>
) : checks?.length === 0 ? (
{isLoading ? (
<IncidentSkeleton />
) : noIncidentsRecordedYet ? (
<Empty
mode={mode}
styles={sharedStyles}
/>
) : noIncidentsForThatMonitor ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"

View File

@@ -17,56 +17,62 @@ const Incidents = () => {
const [monitors, setMonitors] = useState({});
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [loading, setLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
// TODO do something with these filters
const [filter, setFilter] = useState("all");
useEffect(() => {
const fetchMonitors = async () => {
setLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authState.authToken,
teamId: authState.user.teamId,
limit: -1,
types: null,
status: null,
checkOrder: null,
normalize: null,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
// Reduce to a lookup object for 0(1) lookup
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
return acc;
}, {});
setMonitors(monitorLookup);
monitorId !== undefined && setSelectedMonitor(monitorId);
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authState.authToken,
teamId: authState.user.teamId,
limit: -1,
types: null,
status: null,
checkOrder: null,
normalize: null,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
// Reduce to a lookup object for 0(1) lookup
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
return acc;
}, {});
setMonitors(monitorLookup);
monitorId !== undefined && setSelectedMonitor(monitorId);
}
} catch (error) {
console.info(error);
} finally {
setIsLoading(false);
}
setLoading(false);
};
fetchMonitors();
}, [authState]);
useEffect(() => {}, []);
useEffect(() => {}, [monitors]);
const handleSelect = (event) => {
setSelectedMonitor(event.target.value);
};
const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0;
return (
<Stack
className="incidents table-container"
className="incidents"
pt={theme.spacing(6)}
gap={theme.spacing(12)}
>
{loading ? (
{isActuallyLoading ? (
<SkeletonLayout />
) : (
<>

View File

@@ -0,0 +1,79 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Search from "../../../../Components/Inputs/Search";
import MemoizedMonitorTable from "../MonitorTable";
import { useState } from "react";
import useDebounce from "../../../../Utils/debounce";
import PropTypes from "prop-types";
const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin }) => {
const theme = useTheme();
const [search, setSearch] = useState("");
const [isSearching, setIsSearching] = useState(false);
const debouncedFilter = useDebounce(search, 500);
const handleSearch = (value) => {
setIsSearching(true);
setSearch(value);
};
return (
<Box
flex={1}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{totalMonitors}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitors}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<MemoizedMonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setIsSearching={setIsSearching}
isSearching={isSearching}
/>
</Box>
);
};
CurrentMonitoring.propTypes = {
totalMonitors: PropTypes.number,
monitors: PropTypes.array,
isAdmin: PropTypes.bool,
};
export { CurrentMonitoring };

View File

@@ -0,0 +1,31 @@
import { Skeleton, TableCell, TableRow } from "@mui/material";
const ROWS_NUMBER = 7;
const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i);
const TableBodySkeleton = () => {
return (
<>
{ROWS_ARRAY.map((row) => (
<TableRow key={row}>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
<TableCell>
<Skeleton />
</TableCell>
</TableRow>
))}
</>
);
};
export { TableBodySkeleton };

View File

@@ -12,6 +12,7 @@ import {
Stack,
Typography,
Button,
CircularProgress,
} from "@mui/material";
import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded";
import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded";
@@ -34,6 +35,7 @@ import RightArrow from "../../../../assets/icons/right-arrow.svg?react";
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
import ActionsMenu from "../actionsMenu";
import useUtils from "../../utils";
import { TableBodySkeleton } from "./Skeleton";
/**
* Component for pagination actions (first, previous, next, last).
@@ -107,17 +109,17 @@ TablePaginationActions.propTypes = {
onPageChange: PropTypes.func.isRequired,
};
const MonitorTable = ({ isAdmin, filter, setLoading }) => {
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
const { determineState } = useUtils();
const { rowsPerPage } = useSelector((state) => state.ui.monitors);
const authState = useSelector((state) => state.auth);
const [page, setPage] = useState(0);
const [monitors, setMonitors] = useState([]);
const [monitorCount, setMonitorCount] = useState(0);
const authState = useSelector((state) => state.auth);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [sort, setSort] = useState({});
const prevFilter = useRef(filter);
@@ -160,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
});
setMonitors(res?.data?.data?.monitors ?? []);
setMonitorCount(res?.data?.data?.monitorCount ?? 0);
setLoading(false);
} catch (error) {
logger.error(error);
} finally {
setIsSearching(false);
}
}, [authState, page, rowsPerPage, filter, sort, setLoading]);
}, [authState, page, rowsPerPage, filter, sort, setIsSearching]);
useEffect(() => {
fetchPage();
}, [updateTrigger, authState, page, rowsPerPage, filter, sort, setLoading, fetchPage]);
}, [
updateTrigger,
authState,
page,
rowsPerPage,
filter,
sort,
setIsSearching,
fetchPage,
]);
// Listen for changes in filter, if new value reset the page
useEffect(() => {
@@ -220,7 +232,37 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
};
return (
<>
<Box position="relative">
{isSearching && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.background.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="20%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.other.icon,
}}
/>
</Box>
</>
)}
<TableContainer component={Paper}>
<Table>
<TableHead>
@@ -271,77 +313,82 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
</TableRow>
</TableHead>
<TableBody>
{monitors.map((monitor) => {
let uptimePercentage = "";
let percentageColor = theme.palette.percentage.uptimeExcellent;
{/* TODO add empty state. Check if is searching, and empty => skeleton. Is empty, not searching => skeleton */}
{monitors.length > 0 ? (
monitors.map((monitor) => {
let uptimePercentage = "";
let percentageColor = theme.palette.percentage.uptimeExcellent;
// Determine uptime percentage and color based on the monitor's uptimePercentage value
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
// Determine uptime percentage and color based on the monitor's uptimePercentage value
if (monitor.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.percentage.uptimePoor
: monitor.uptimePercentage < 0.5
? theme.palette.percentage.uptimeFair
: monitor.uptimePercentage < 0.75
? theme.palette.percentage.uptimeGood
: theme.palette.percentage.uptimeExcellent;
}
percentageColor =
monitor.uptimePercentage < 0.25
? theme.palette.percentage.uptimePoor
: monitor.uptimePercentage < 0.5
? theme.palette.percentage.uptimeFair
: monitor.uptimePercentage < 0.75
? theme.palette.percentage.uptimeGood
: theme.palette.percentage.uptimeExcellent;
}
const params = {
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
status: determineState(monitor),
};
const params = {
url: monitor.url,
title: monitor.name,
percentage: uptimePercentage,
percentageColor,
status: determineState(monitor),
};
return (
<TableRow
key={monitor._id}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host
key={monitor._id}
params={params}
/>
</TableCell>
<TableCell>
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
<BarChart checks={monitor.checks.slice().reverse()} />
</TableCell>
<TableCell>
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
</TableCell>
<TableCell>
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</TableCell>
</TableRow>
);
})}
return (
<TableRow
key={monitor._id}
sx={{
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
}}
>
<TableCell>
<Host
key={monitor._id}
params={params}
/>
</TableCell>
<TableCell>
<StatusLabel
status={params.status}
text={params.status}
customStyles={{ textTransform: "capitalize" }}
/>
</TableCell>
<TableCell>
<BarChart checks={monitor.checks.slice().reverse()} />
</TableCell>
<TableCell>
<span style={{ textTransform: "uppercase" }}>{monitor.type}</span>
</TableCell>
<TableCell>
<ActionsMenu
monitor={monitor}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</TableCell>
</TableRow>
);
})
) : (
<TableBodySkeleton />
)}
</TableBody>
</Table>
</TableContainer>
@@ -415,14 +462,15 @@ const MonitorTable = ({ isAdmin, filter, setLoading }) => {
}}
/>
</Stack>
</>
</Box>
);
};
MonitorTable.propTypes = {
isAdmin: PropTypes.bool,
filter: PropTypes.string,
setLoading: PropTypes.func,
setIsSearching: PropTypes.func,
isSearching: PropTypes.bool,
};
const MemoizedMonitorTable = memo(MonitorTable);

View File

@@ -1,79 +1,74 @@
import "./index.css";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { getUptimeMonitorsByTeamId } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { Box, Button, CircularProgress, Stack, Typography } from "@mui/material";
import { Box, Button, Stack } from "@mui/material";
import PropTypes from "prop-types";
import SkeletonLayout from "./skeleton";
import Fallback from "./fallback";
import StatusBox from "./StatusBox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Greeting from "../../../Utils/greeting";
import MonitorTable from "./MonitorTable";
import Search from "../../../Components/Inputs/Search";
import useDebounce from "../../../Utils/debounce";
import { CurrentMonitoring } from "./CurrentMonitoring";
const Monitors = ({ isAdmin }) => {
const theme = useTheme();
const navigate = useNavigate();
const monitorState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const [search, setSearch] = useState("");
const [isSearching, setIsSearching] = useState(false);
const dispatch = useDispatch({});
const debouncedFilter = useDebounce(search, 500);
const handleSearch = (value) => {
setIsSearching(true);
setSearch(value);
};
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
let loading =
monitorState?.isLoading && monitorState?.monitorsSummary?.monitors?.length === 0;
//TODO bring fetching to this component, like on pageSpeed
const loading = monitorState?.isLoading;
const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total;
const hasMonitors = totalMonitors > 0;
const noMonitors = !hasMonitors;
const canAddMonitor = isAdmin && hasMonitors;
return (
<Stack
className="monitors table-container"
className="monitors"
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
<Greeting type="uptime" />
{canAddMonitor && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{loading ? (
<SkeletonLayout />
) : (
<>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="uptime" />
{isAdmin && monitorState?.monitorsSummary?.monitors?.length !== 0 && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
}}
sx={{ fontWeight: 500 }}
>
Create monitor
</Button>
)}
</Stack>
</Box>
{isAdmin && monitorState?.monitorsSummary?.monitors?.length === 0 && (
<Fallback isAdmin={isAdmin} />
)}
{monitorState?.monitorsSummary?.monitors?.length !== 0 && (
{hasMonitors && (
<>
<Stack
gap={theme.spacing(8)}
@@ -93,88 +88,11 @@ const Monitors = ({ isAdmin }) => {
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
/>
</Stack>
<Box
flex={1}
px={theme.spacing(10)}
py={theme.spacing(8)}
border={1}
borderColor={theme.palette.border.light}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
>
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
className="current-monitors-counter"
color={theme.palette.text.primary}
border={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
>
{monitorState?.monitorsSummary?.monitorCounts?.total || 0}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitorState?.monitorsSummary?.monitors ?? []}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<Box position="relative">
{isSearching && (
<>
<Box
width="100%"
height="100%"
position="absolute"
sx={{
backgroundColor: theme.palette.background.main,
opacity: 0.8,
zIndex: 100,
}}
/>
<Box
height="100%"
position="absolute"
top="20%"
left="50%"
sx={{
transform: "translateX(-50%)",
zIndex: 101,
}}
>
<CircularProgress
sx={{
color: theme.palette.other.icon,
}}
/>
</Box>
</>
)}
<MonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setLoading={setIsSearching}
/>
</Box>
</Box>
<CurrentMonitoring
isAdmin={isAdmin}
monitors={monitorState.monitorsSummary.monitors}
totalMonitors={totalMonitors}
/>
</>
)}
</>

View File

@@ -19,7 +19,7 @@ const PageSpeed = ({ isAdmin }) => {
const navigate = useNavigate();
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
useEffect(() => {
dispatch(getPageSpeedByTeamId(authToken));

6
package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "bluewave-uptime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}