refactor: created current monitoring component and changed isLoading initial to true because of glitch

This commit is contained in:
Caio Cabral
2024-10-18 17:29:11 -04:00
parent 5adca16b55
commit 704348d1f9
5 changed files with 371 additions and 338 deletions

View File

@@ -1,12 +1,5 @@
import PropTypes from "prop-types";
import {
Box,
ListItem,
Autocomplete,
TextField,
Stack,
Typography,
} from "@mui/material";
import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
@@ -24,172 +17,172 @@ import SearchIcon from "../../../assets/icons/search.svg?react";
*/
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
);
const theme = useTheme();
return (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.text.tertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
);
};
//TODO keep search state inside of component
const Search = ({
id,
options,
filteredBy,
secondaryLabel,
value,
inputValue,
handleInputChange,
handleChange,
sx,
multiple = false,
isAdorned = true,
error,
disabled,
id,
options,
filteredBy,
secondaryLabel,
value,
inputValue,
handleInputChange,
handleChange,
sx,
multiple = false,
isAdorned = true,
error,
disabled,
}) => {
const theme = useTheme();
const theme = useTheme();
return (
<Autocomplete
multiple={multiple}
id={id}
value={value}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
onChange={(_, newValue) => {
handleChange && handleChange(newValue);
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
options={options}
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<Stack>
<TextField
{...params}
error={Boolean(error)}
placeholder="Type to search"
InputProps={{
...params.InputProps,
...(isAdorned && { startAdornment: <SearchAdornment /> }),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.border.light,
},
}}
/>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.text}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</Stack>
)}
filterOptions={(options, { inputValue }) => {
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
return (
<Autocomplete
multiple={multiple}
id={id}
value={value}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
onChange={(_, newValue) => {
handleChange && handleChange(newValue);
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
options={options}
getOptionLabel={(option) => option[filteredBy]}
renderInput={(params) => (
<Stack>
<TextField
{...params}
error={Boolean(error)}
placeholder="Type to search"
InputProps={{
...params.InputProps,
...(isAdorned && { startAdornment: <SearchAdornment /> }),
}}
sx={{
"& fieldset": {
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
},
"& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
{
borderColor: theme.palette.border.light,
},
}}
/>
{error && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.text}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</Stack>
)}
filterOptions={(options, { inputValue }) => {
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
if (filtered.length === 0) {
return [{ [filteredBy]: "No monitors found", noOptions: true }];
}
return filtered;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
sx={
option.noOptions
? {
pointerEvents: "none",
backgroundColor: theme.palette.background.main,
}
: {}
}
>
{option[filteredBy] +
(secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
</ListItem>
);
}}
slotProps={{
popper: {
keepMounted: true,
sx: {
"& ul": { p: 2 },
"& li.MuiAutocomplete-option": {
color: theme.palette.text.secondary,
px: 4,
borderRadius: theme.shape.borderRadius,
},
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
{
backgroundColor: theme.palette.background.fill,
},
"& .MuiAutocomplete-noOptions": {
px: theme.spacing(6),
py: theme.spacing(5),
},
},
},
}}
sx={{
height: 34,
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
...sx,
}}
/>
);
if (filtered.length === 0) {
return [{ [filteredBy]: "No monitors found", noOptions: true }];
}
return filtered;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
sx={
option.noOptions
? {
pointerEvents: "none",
backgroundColor: theme.palette.background.main,
}
: {}
}
>
{option[filteredBy] + (secondaryLabel ? ` (${option[secondaryLabel]})` : "")}
</ListItem>
);
}}
slotProps={{
popper: {
keepMounted: true,
sx: {
"& ul": { p: 2 },
"& li.MuiAutocomplete-option": {
color: theme.palette.text.secondary,
px: 4,
borderRadius: theme.shape.borderRadius,
},
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
{
backgroundColor: theme.palette.background.fill,
},
"& .MuiAutocomplete-noOptions": {
px: theme.spacing(6),
py: theme.spacing(5),
},
},
},
}}
sx={{
height: 34,
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
...sx,
}}
/>
);
};
Search.propTypes = {
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.array,
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isAdorned: PropTypes.bool,
sx: PropTypes.object,
error: PropTypes.string,
disabled: PropTypes.bool,
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.array,
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isAdorned: PropTypes.bool,
sx: PropTypes.object,
error: PropTypes.string,
disabled: PropTypes.bool,
};
export default Search;

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

@@ -109,7 +109,7 @@ TablePaginationActions.propTypes = {
onPageChange: PropTypes.func.isRequired,
};
const MonitorTable = ({ isAdmin, filter, setLoading, isSearching }) => {
const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching }) => {
const theme = useTheme();
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -162,15 +162,25 @@ const MonitorTable = ({ isAdmin, filter, setLoading, isSearching }) => {
});
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(() => {
@@ -459,7 +469,7 @@ const MonitorTable = ({ isAdmin, filter, setLoading, isSearching }) => {
MonitorTable.propTypes = {
isAdmin: PropTypes.bool,
filter: PropTypes.string,
setLoading: PropTypes.func,
setIsSearching: PropTypes.func,
isSearching: PropTypes.bool,
};

View File

@@ -1,19 +1,17 @@
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, 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();
@@ -22,20 +20,12 @@ const Monitors = ({ isAdmin }) => {
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
//TODO create components, and lower these states.
const [search, setSearch] = useState("");
const [isSearching, setIsSearching] = useState(false);
const debouncedFilter = useDebounce(search, 500);
const handleSearch = (value) => {
setIsSearching(true);
setSearch(value);
};
useEffect(() => {
dispatch(getUptimeMonitorsByTeamId(authState.authToken));
}, [authState.authToken, dispatch]);
//TODO bring fetching to this component, like on pageSpeed
const loading = monitorState?.isLoading;
const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total;
@@ -100,57 +90,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}
>
{totalMonitors}
</Box>
<Box
width="25%"
minWidth={150}
ml="auto"
>
<Search
options={monitorState.monitorsSummary.monitors}
filteredBy="name"
inputValue={search}
handleInputChange={handleSearch}
/>
</Box>
</Stack>
<MonitorTable
isAdmin={isAdmin}
filter={debouncedFilter}
setLoading={setIsSearching}
isSearching={isSearching}
/>
</Box>
<CurrentMonitoring
isAdmin={isAdmin}
monitors={monitorState.monitorsSummary.monitors}
totalMonitors={totalMonitors}
/>
</>
)}
</>

View File

@@ -14,114 +14,121 @@ import Card from "./card";
import { networkService } from "../../main";
const PageSpeed = ({ isAdmin }) => {
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const dispatch = useDispatch();
const navigate = useNavigate();
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(false);
const [monitors, setMonitors] = useState([]);
useEffect(() => {
dispatch(getPageSpeedByTeamId(authToken));
}, [authToken, dispatch]);
const { user, authToken } = useSelector((state) => state.auth);
const [isLoading, setIsLoading] = useState(true);
const [monitors, setMonitors] = useState([]);
useEffect(() => {
dispatch(getPageSpeedByTeamId(authToken));
}, [authToken, dispatch]);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authToken,
teamId: user.teamId,
limit: 10,
types: ["pagespeed"],
status: null,
checkOrder: "desc",
normalize: true,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
if (res?.data?.data?.monitors) {
setMonitors(res.data.data.monitors);
}
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authToken,
teamId: user.teamId,
limit: 10,
types: ["pagespeed"],
status: null,
checkOrder: "desc",
normalize: true,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
if (res?.data?.data?.monitors) {
setMonitors(res.data.data.monitors);
}
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, []);
fetchMonitors();
}, []);
// will show skeletons only on initial load
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
let isActuallyLoading = isLoading && monitors?.length === 0;
return (
<Box
className="page-speed"
sx={{
':has(> [class*="fallback__"])': {
position: "relative",
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
borderStyle: "dashed",
backgroundColor: theme.palette.background.main,
overflow: "hidden",
},
}}
>
{isActuallyLoading ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Box>
<Box mb={theme.spacing(12)}>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="pagespeed" />
{isAdmin && (
<Button
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
sx={{ whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Grid container spacing={theme.spacing(12)}>
{monitors?.map((monitor) => (
<Card monitor={monitor} key={monitor._id} />
))}
</Grid>
</Box>
) : (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
// will show skeletons only on initial load
// since monitor state is being added to redux persist, there's no reason to display skeletons on every render
let isActuallyLoading = isLoading && monitors?.length === 0;
console.log({ isActuallyLoading });
return (
<Box
className="page-speed"
sx={{
':has(> [class*="fallback__"])': {
position: "relative",
border: 1,
borderColor: theme.palette.border.light,
borderRadius: theme.shape.borderRadius,
borderStyle: "dashed",
backgroundColor: theme.palette.background.main,
overflow: "hidden",
},
}}
>
<Box mb={theme.spacing(12)}>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
mt={theme.spacing(5)}
>
<Greeting type="pagespeed" />
{isAdmin && (
<Button
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
sx={{ whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
{isActuallyLoading ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Box>
<Grid
container
spacing={theme.spacing(12)}
>
{monitors?.map((monitor) => (
<Card
monitor={monitor}
key={monitor._id}
/>
))}
</Grid>
</Box>
) : (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
};
PageSpeed.propTypes = {
isAdmin: PropTypes.bool,
isAdmin: PropTypes.bool,
};
export default PageSpeed;