Added skeletons to monitor pages

This commit is contained in:
Daniel Cojocea committed 2024-08-07 16:45:49 -04:00
1 parent 0195d093d5
commit ecb6f6204c
5 files changed
+507 -325

No files matched your search

+245 -188
View File
@@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import Button from "../../../Components/Button";
import Field from "../../../Components/Inputs/Field";
import { Box, Modal, Stack, Typography } from "@mui/material";
import { Box, Modal, Skeleton, Stack, Typography } from "@mui/material";
import WestRoundedIcon from "@mui/icons-material/WestRounded";
import GreenCheck from "../../../assets/icons/checkbox-green.svg?react";
import RedCheck from "../../../assets/icons/checkbox-red.svg?react";
@@ -47,7 +47,7 @@ const Configure = () => {
const dispatch = useDispatch();
const { user, authToken } = useSelector((state) => state.auth);
const { monitors } = useSelector((state) => state.uptimeMonitors);
const [monitor, setMonitor] = useState();
const [monitor, setMonitor] = useState({});
const [duration, setDuration] = useState(0);
const [errors, setErrors] = useState({});
const { monitorId } = useParams();
@@ -164,53 +164,88 @@ const Configure = () => {
var lastChecked = formatDurationRounded(getLastChecked(monitor?.checks));
let loading = Object.keys(monitor).length === 0;
return (
<Box className="configure-monitor">
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate(-1)}
sx={{
backgroundColor: "#f4f4f4",
mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
{loading ? (
<Skeleton variant="rounded" width="15%" height={34} />
) : (
<Button
level="tertiary"
label="Back"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate(-1)}
sx={{
backgroundColor: "#f4f4f4",
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
/>
)}
<form
className="configure-monitor-form"
noValidate
spellCheck="false"
style={{
marginTop: theme.gap.medium,
}}
/>
<form className="configure-monitor-form" noValidate spellCheck="false">
>
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Box>
{parsedUrl?.host ? (
<Typography
component="h1"
mb={theme.gap.small}
sx={{ lineHeight: 1 }}
>
{parsedUrl.host || "..."}
</Typography>
{loading ? (
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
) : monitor?.status ? (
<GreenCheck />
) : (
<RedCheck />
)}
<Box width="80%">
{loading ? (
<>
<Skeleton
variant="rounded"
width="50%"
height={24}
sx={{ mb: theme.gap.small }}
/>
<Skeleton variant="rounded" width="50%" height={18} />
</>
) : (
""
<>
{parsedUrl?.host ? (
<Typography
component="h1"
mb={theme.gap.small}
sx={{ lineHeight: 1 }}
>
{parsedUrl.host || "..."}
</Typography>
) : (
""
)}
<Typography lineHeight={parsedUrl?.host ? 1 : theme.gap.large}>
<Typography
component="span"
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
}}
>
Your site is {monitor?.status ? "up" : "down"}.
</Typography>{" "}
Checking every {duration}.{" "}
{lastChecked ? `Last time checked ${lastChecked} ago.` : ""}
</Typography>
</>
)}
<Typography lineHeight={parsedUrl?.host ? 1 : theme.gap.large}>
<Typography
component="span"
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
}}
>
Your site is {monitor?.status ? "up" : "down"}.
</Typography>{" "}
Checking every {duration}.{" "}
{lastChecked ? `Last time checked ${lastChecked} ago.` : ""}
</Typography>
</Box>
<Stack
direction="row"
@@ -220,156 +255,178 @@ const Configure = () => {
alignSelf: "flex-end",
}}
>
<Button
level="tertiary"
label="Pause"
animate="rotate180"
img={<PauseCircleOutlineIcon />}
sx={{
backgroundColor: "#f4f4f4",
pl: theme.gap.small,
pr: theme.gap.medium,
"& svg": {
mr: theme.gap.xs,
},
}}
/>
<Button
level="error"
label="Remove"
sx={{
boxShadow: "none",
px: theme.gap.ml,
}}
onClick={() => setIsOpen(true)}
/>
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p" sx={{ mt: theme.gap.small }}>
Here you can select the URL of the host, together with the type of
monitor.
</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Field
type={monitor?.type === "http" ? "url" : "text"}
https={protocol === "https"}
id="monitor-url"
label="URL to monitor"
placeholder="google.com"
value={parsedUrl?.host || monitor?.url || ""}
disabled={true}
error={errors["url"]}
/>
<Field
type="text"
id="monitor-name"
label="Friendly name"
isOptional={true}
placeholder="Google"
value={monitor?.name || ""}
onChange={handleChange}
error={errors["name"]}
/>
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p" mt={theme.gap.small}>
When there is an incident, notify users.
</Typography>
</Box>
<Stack gap={theme.gap.medium}>
<Typography component="p" mt={theme.gap.small}>
When there is a new incident,
</Typography>
<Checkbox
id="notify-sms"
label="Notify via SMS (coming soon)"
isChecked={false}
value=""
onChange={() => console.log("disabled")}
isDisabled={true}
/>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
isChecked={
monitor?.notifications?.some(
(notification) => notification.type === "email"
) || false
}
value={user?.email}
onChange={(event) => handleChange(event)}
/>
<Checkbox
id="notify-email"
label="Also notify via email to multiple addresses (coming soon)"
isChecked={false}
value=""
onChange={() => console.log("disabled")}
isDisabled={true}
/>
{monitor?.notifications?.some(
(notification) => notification.type === "emails"
) ? (
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Field
id="notify-email-list"
type="text"
placeholder="name@gmail.com"
value=""
onChange={() => console.log("disabled")}
/>
<Typography mt={theme.gap.small}>
You can separate multiple emails with a comma
</Typography>
</Box>
{loading ? (
<>
<Skeleton variant="rounded" width={150} height={34} />
</>
) : (
""
<>
<Button
level="tertiary"
label="Pause"
animate="rotate180"
img={<PauseCircleOutlineIcon />}
sx={{
backgroundColor: "#f4f4f4",
pl: theme.gap.small,
pr: theme.gap.medium,
"& svg": {
mr: theme.gap.xs,
},
}}
/>
<Button
level="error"
label="Remove"
sx={{
boxShadow: "none",
px: theme.gap.ml,
}}
onClick={() => setIsOpen(true)}
/>
</>
)}
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">Advanced settings</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Select
id="monitor-interval-configure"
label="Check frequency"
value={monitor?.interval / MS_PER_MINUTE || 1}
onChange={(event) => handleChange(event, "interval")}
items={frequencies}
/>
</Stack>
</Stack>
{loading ? (
<>
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={200} />
</>
) : (
<>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p" sx={{ mt: theme.gap.small }}>
Here you can select the URL of the host, together with the
type of monitor.
</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Field
type={monitor?.type === "http" ? "url" : "text"}
https={protocol === "https"}
id="monitor-url"
label="URL to monitor"
placeholder="google.com"
value={parsedUrl?.host || monitor?.url || ""}
disabled={true}
error={errors["url"]}
/>
<Field
type="text"
id="monitor-name"
label="Friendly name"
isOptional={true}
placeholder="Google"
value={monitor?.name || ""}
onChange={handleChange}
error={errors["name"]}
/>
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">Incident notifications</Typography>
<Typography component="p" mt={theme.gap.small}>
When there is an incident, notify users.
</Typography>
</Box>
<Stack gap={theme.gap.medium}>
<Typography component="p" mt={theme.gap.small}>
When there is a new incident,
</Typography>
<Checkbox
id="notify-sms"
label="Notify via SMS (coming soon)"
isChecked={false}
value=""
onChange={() => console.log("disabled")}
isDisabled={true}
/>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
isChecked={
monitor?.notifications?.some(
(notification) => notification.type === "email"
) || false
}
value={user?.email}
onChange={(event) => handleChange(event)}
/>
<Checkbox
id="notify-email"
label="Also notify via email to multiple addresses (coming soon)"
isChecked={false}
value=""
onChange={() => console.log("disabled")}
isDisabled={true}
/>
{monitor?.notifications?.some(
(notification) => notification.type === "emails"
) ? (
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Field
id="notify-email-list"
type="text"
placeholder="name@gmail.com"
value=""
onChange={() => console.log("disabled")}
/>
<Typography mt={theme.gap.small}>
You can separate multiple emails with a comma
</Typography>
</Box>
) : (
""
)}
</Stack>
</Stack>
<Stack
className="config-box"
direction="row"
justifyContent="space-between"
gap={theme.gap.xxl}
>
<Box>
<Typography component="h2">Advanced settings</Typography>
</Box>
<Stack gap={theme.gap.xl}>
<Select
id="monitor-interval-configure"
label="Check frequency"
value={monitor?.interval / MS_PER_MINUTE || 1}
onChange={(event) => handleChange(event, "interval")}
items={frequencies}
/>
</Stack>
</Stack>
</>
)}
<Stack direction="row" justifyContent="flex-end">
<Button
level="primary"
label="Save"
sx={{ px: theme.gap.large }}
onClick={handleSubmit}
/>
{loading ? (
<Skeleton variant="rounded" width="15%" height={34} />
) : (
<Button
level="primary"
label="Save"
sx={{ px: theme.gap.large }}
onClick={handleSubmit}
/>
)}
</Stack>
</form>
<Modal
@@ -42,9 +42,6 @@
padding: var(--env-var-spacing-1) 18px;
background-color: var(--env-var-color-8);
}
.monitor-details .stat-box:not(:first-of-type) {
margin-left: var(--env-var-spacing-2);
}
.monitor-details .stat-box h6.MuiTypography-root {
margin-bottom: var(--env-var-spacing-1);
}
+199 -111
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Box, Stack, Typography, useTheme } from "@mui/material";
import { Box, Skeleton, Stack, Typography, useTheme } from "@mui/material";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import axiosInstance from "../../../Utils/axiosConfig";
@@ -30,6 +30,11 @@ const StatBox = ({ title, value }) => {
);
};
StatBox.propTypes = {
title: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
/**
* Details page component displaying monitor details and related information.
* @component
@@ -158,78 +163,135 @@ const DetailsPage = () => {
}, 0);
};
let loading = Object.keys(monitor).length === 0;
return (
<Box className="monitor-details">
<Button
level="tertiary"
label="Back to Monitors"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate("/monitors")}
sx={{
backgroundColor: "#f4f4f4",
mb: theme.gap.medium,
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
/>
<Stack gap={theme.gap.xl}>
{loading ? (
<Skeleton variant="rounded" width="20%" height={34} />
) : (
<Button
level="tertiary"
label="Back to Monitors"
animate="slideLeft"
img={<WestRoundedIcon />}
onClick={() => navigate("/monitors")}
sx={{
backgroundColor: "#f4f4f4",
px: theme.gap.ml,
"& svg.MuiSvgIcon-root": {
mr: theme.gap.small,
fill: theme.palette.otherColors.slateGray,
},
}}
/>
)}
<Stack gap={theme.gap.xl} mt={theme.gap.medium}>
<Stack direction="row" gap={theme.gap.small} mt={theme.gap.small}>
{monitor?.status ? <GreenCheck /> : <RedCheck />}
<Box>
<Typography component="h1" sx={{ lineHeight: 1 }}>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography mt={theme.gap.small}>
<Typography
component="span"
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
}}
>
Your site is {monitor?.status ? "up" : "down"}.
</Typography>{" "}
Checking every {formatDurationRounded(monitor?.interval)}. Last
time checked{" "}
{formatDurationRounded(getLastChecked(monitor?.checks))} ago.
</Typography>
{loading ? (
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
) : monitor?.status ? (
<GreenCheck />
) : (
<RedCheck />
)}
<Box width="80%">
{loading ? (
<>
<Skeleton variant="rounded" width="50%" height={24} />
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: theme.gap.small }}
/>
</>
) : (
<>
<Typography component="h1" sx={{ lineHeight: 1 }}>
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography mt={theme.gap.small}>
<Typography
component="span"
sx={{
color: monitor?.status
? "var(--env-var-color-17)"
: "var(--env-var-color-24)",
}}
>
Your site is {monitor?.status ? "up" : "down"}.
</Typography>{" "}
Checking every {formatDurationRounded(monitor?.interval)}.
Last time checked{" "}
{formatDurationRounded(getLastChecked(monitor?.checks))} ago.
</Typography>
</>
)}
</Box>
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{ width: theme.gap.mlplus, height: theme.gap.mlplus }}
/>
}
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
backgroundColor: "#f4f4f4",
px: theme.gap.medium,
"& svg": {
mr: "6px",
},
}}
/>
{loading ? (
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
) : (
<Button
level="tertiary"
label="Configure"
animate="rotate90"
img={
<SettingsIcon
style={{
minWidth: theme.gap.mlplus,
minHeight: theme.gap.mlplus,
}}
/>
}
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
backgroundColor: "#f4f4f4",
px: theme.gap.medium,
"& svg": {
mr: "6px",
},
}}
/>
)}
</Stack>
<Stack direction="row" justifyContent="space-between">
<StatBox
title="Currently up for"
value={formatDuration(calculateUptimeDuration(monitor.checks))}
/>
<StatBox
title="Last checked"
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
/>
<StatBox title="Incidents" value={countIncidents(monitor.checks)} />
<Stack
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
>
{loading ? (
<>
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
<Skeleton variant="rounded" width="100%" height={80} />
</>
) : (
<>
<StatBox
title="Currently up for"
value={formatDuration(calculateUptimeDuration(monitor.checks))}
/>
<StatBox
title="Last checked"
value={`${formatDuration(getLastChecked(monitor.checks))} ago`}
/>
<StatBox
title="Incidents"
value={countIncidents(monitor.checks)}
/>
</>
)}
</Stack>
<Box>
<Stack
@@ -237,56 +299,82 @@ const DetailsPage = () => {
justifyContent="space-between"
mb={theme.gap.ml}
>
<Typography component="h2" mb={theme.gap.small}>
Response Times
</Typography>
<ButtonGroup>
<Button
level="secondary"
label="Day"
onClick={() => setFilter("day")}
sx={{
backgroundColor:
filter === "day" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Week"
onClick={() => setFilter("week")}
sx={{
backgroundColor:
filter === "week" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Month"
onClick={() => setFilter("month")}
sx={{
backgroundColor:
filter === "month" && theme.palette.otherColors.fillGray,
}}
/>
</ButtonGroup>
{loading ? (
<>
<Skeleton
variant="rounded"
width="20%"
height={24}
sx={{ alignSelf: "flex-end" }}
/>
<Skeleton variant="rounded" width="20%" height={34} />
</>
) : (
<>
<Typography component="h2" mb={theme.gap.small}>
Response Times
</Typography>
<ButtonGroup>
<Button
level="secondary"
label="Day"
onClick={() => setFilter("day")}
sx={{
backgroundColor:
filter === "day" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Week"
onClick={() => setFilter("week")}
sx={{
backgroundColor:
filter === "week" && theme.palette.otherColors.fillGray,
}}
/>
<Button
level="secondary"
label="Month"
onClick={() => setFilter("month")}
sx={{
backgroundColor:
filter === "month" &&
theme.palette.otherColors.fillGray,
}}
/>
</ButtonGroup>
</>
)}
</Stack>
<Box sx={{ height: "200px" }}>
<MonitorDetailsAreaChart checks={monitor.checks} filter={filter} />
{loading ? (
<Skeleton variant="rounded" width="100%" height="100%" />
) : (
<MonitorDetailsAreaChart
checks={monitor.checks}
filter={filter}
/>
)}
</Box>
</Box>
<Box>
<Typography component="h2" mb={theme.gap.ml}>
History
</Typography>
<BasicTable data={data} paginated={true} reversed={true} />
</Box>
<Stack gap={theme.gap.ml}>
{loading ? (
<>
<Skeleton variant="rounded" width="20%" height={24} />
<Skeleton variant="rounded" width="100%" height={200} />
<Skeleton variant="rounded" width="100%" height={50} />
</>
) : (
<>
<Typography component="h2">History</Typography>
<BasicTable data={data} paginated={true} reversed={true} />
</>
)}
</Stack>
</Stack>
</Box>
);
};
StatBox.propTypes = {
title: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
export default DetailsPage;
+59 -23
View File
@@ -22,6 +22,7 @@ import {
Menu,
MenuItem,
Modal,
Skeleton,
Stack,
Typography,
} from "@mui/material";
@@ -204,46 +205,81 @@ const Monitors = () => {
};
});
let loading = monitorState.isLoading && monitorState.monitors.length === 0;
return (
<Stack className="monitors" gap={theme.gap.large}>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Typography component="h1">
Hello, {authState.user.firstName}
</Typography>
<Button
level="primary"
label="Create new monitor"
onClick={() => {
navigate("/monitors/create");
}}
/>
{loading ? (
<>
<Skeleton variant="rounded" width="50%" height={36} />
<Skeleton variant="rounded" width="15%" height={36} />
</>
) : (
<>
<Typography component="h1">
Hello, {authState.user.firstName}
</Typography>
<Button
level="primary"
label="Create new monitor"
onClick={() => {
navigate("/monitors/create");
}}
/>
</>
)}
</Stack>
<Stack
gap={theme.gap.large}
direction="row"
justifyContent="space-between"
>
<ServerStatus title="Up" value={up} state="up" />
<ServerStatus title="Down" value={down} state="down" />
<ServerStatus title="Paused" value={0} state="pause" />
{loading ? (
<>
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
<Skeleton variant="rounded" width="100%" height={100} />
</>
) : (
<>
<ServerStatus title="Up" value={up} state="up" />
<ServerStatus title="Down" value={down} state="down" />
<ServerStatus title="Paused" value={0} state="pause" />
</>
)}
</Stack>
<Stack
gap={theme.gap.large}
p={theme.gap.xl}
border={1}
borderColor={
loading ? "#f9fafb" : theme.palette.otherColors.graishWhite
}
backgroundColor={loading ? "#f9fafb" : "white"}
sx={{
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
borderRadius: `${theme.shape.borderRadius}px`,
backgroundColor: theme.palette.otherColors.white,
}}
>
<Stack direction="row" alignItems="center">
<Typography component="h2">Current monitors</Typography>
<Box className="current-monitors-counter">
{monitorState.monitors.length}
</Box>
{/* TODO - add search bar */}
</Stack>
<BasicTable data={data} paginated={true} />
{loading ? (
<Skeleton variant="rounded" width="50%" height={25} />
) : (
<Stack direction="row" alignItems="center">
<Typography component="h2">Current monitors</Typography>
<Box className="current-monitors-counter">
{monitorState.monitors.length}
</Box>
{/* TODO - add search bar */}
</Stack>
)}
{loading ? (
<>
<Skeleton variant="rounded" width="100%" height={400} />
<Skeleton variant="rounded" width="100%" height={100} />
</>
) : (
<BasicTable data={data} paginated={true} />
)}
</Stack>
<Menu
className="actions-menu"
+4
View File
@@ -161,6 +161,10 @@ button:focus-visible {
display: none;
}
body .MuiSkeleton-root {
background-color: var(--env-var-color-15);
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;