Page Speed (#470)

* Added page speed route and fallback UI

* Added first batch of components and route to create a page-speed monitor

* Added basic styling

* Added checkboxes and removed old component

* Added monitor card and some styling

* Moved time functions to separate file and added formatDate function

* Added status label

* Changed sidebar item priority and small styling fix

* Small styling changes
This commit is contained in:
Daniel C authored and GitHub committed 2024-07-28 13:51:47 -07:00
1 parent 954d11b7c4
commit db66ba849b
17 files changed
+423 -156

No files matched your search

+10
View File
@@ -22,6 +22,8 @@ import Details from "./Pages/Monitors/Details";
import Maintenance from "./Pages/Maintenance";
import withAdminCheck from "./HOC/withAdminCheck";
import Configure from "./Pages/Monitors/Configure";
import PageSpeed from "./Pages/PageSpeed";
import CreatePageSpeed from "./Pages/PageSpeed/CreatePageSpeed";
function App() {
const AdminCheckedRegister = withAdminCheck(Register);
@@ -83,6 +85,14 @@ function App() {
path="account/team"
element={<ProtectedRoute Component={Account} open="team" />}
/>
<Route
path="page-speed"
element={<ProtectedRoute Component={PageSpeed} />}
/>
<Route
path="page-speed/create"
element={<ProtectedRoute Component={CreatePageSpeed} />}
/>
</Route>
<Route exact path="/login" element={<Login />} />
@@ -1,36 +0,0 @@
.checkbox-holder {
height: var(--env-var-img-width-1);
display: flex;
align-items: center;
}
.checkbox {
width: var(--env-var-font-size-medium);
height: var(--env-var-font-size-medium);
margin: 0;
padding: 0;
cursor: pointer;
}
.custom-checkbox {
width: var(--env-var-img-width-1);
height: var(--env-var-img-width-1);
margin: 0;
padding: 0;
cursor: pointer;
}
.checkbox-label {
margin-left: 5px;
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: 600;
cursor: pointer;
}
.custom-checkbox-label {
margin-left: var(--env-var-font-size-large);
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-5);
font-weight: 500;
}
@@ -1,13 +0,0 @@
import React from "react";
import "./Checkbox.css";
const CheckBox = () => {
return (
<div className="checkbox-holder">
<input className="checkbox" type="checkbox" id="checkbox" />
<label className="checkbox-label">Remember for 30 days</label>
</div>
);
};
export default CheckBox;
@@ -1,24 +0,0 @@
import React from "react";
import "./Checkbox.css";
import PropsTypes from "prop-types";
const CustomizableCheckbox = (props) => {
return (
<div className="checkbox-holder">
<input
checked={props.isChecked}
onChange={props.handleChange}
className="custom-checkbox"
type="checkbox"
id={props.id}
/>
<label className="custom-checkbox-label" htmlFor={props.id}>{props.title}</label>
</div>
);
};
CustomizableCheckbox.propsTypes = {
title: PropsTypes.string.isRequired,
};
export default CustomizableCheckbox;
+1 -1
View File
@@ -29,7 +29,7 @@ const Fallback = ({ title, checks, link = "/" }) => {
gap={theme.gap.xl}
>
<Skeleton />
<Stack gap={theme.gap.small}>
<Stack gap={theme.gap.small} maxWidth={"275px"}>
<Typography component="h1" marginY={theme.gap.medium}>
A {title} is used to:
</Typography>
@@ -7,7 +7,6 @@
.checkbox-wrapper span.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
color: var(--env-var-color-2);
line-height: 1;
}
.checkbox-wrapper .MuiButtonBase-root {
margin-right: var(--env-var-spacing-1);
+1 -1
View File
@@ -94,7 +94,7 @@ const Field = ({
borderRight: `solid 1px ${theme.palette.section.borderColor}`,
}}
>
<Typography component="h5">
<Typography component="h5" sx={{ lineHeight: 1 }}>
{https ? "https" : "http"}://
</Typography>
</Stack>
+1 -1
View File
@@ -25,9 +25,9 @@ const menu = [
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Integrations", path: "integrations", icon: <Integrations /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
{ name: "Page speed", path: "page-speed", icon: <PageSpeed /> },
{ name: "Integrations", path: "integrations", icon: <Integrations /> },
{ name: "Support", path: "support", icon: <Support /> },
{ name: "Settings", path: "settings", icon: <Settings /> },
];
+6
View File
@@ -12,3 +12,9 @@
.integrations button {
height: var(--env-var-height-2);
}
.integrations .MuiGrid-item > .MuiStack-root {
border: solid 1px var(--env-var-color-6);
border-radius: var(--env-var-radius-1);
background-color: var(--env-var-color-8);
}
+5 -7
View File
@@ -23,11 +23,7 @@ const IntegrationsComponent = ({ url, header, info, onClick }) => {
direction="row"
justifyContent="space-between"
gap={theme.gap.large}
p={theme.gap.large}
border={1}
borderColor={theme.palette.divider}
borderRadius={`${theme.shape.borderRadius}px`}
sx={{ backgroundColor: theme.palette.otherColors.white }}
p={theme.gap.ml}
>
<Box
component="img"
@@ -99,8 +95,10 @@ const Integrations = () => {
return (
<Stack className="integrations" gap={theme.gap.xs}>
<Typography component="h1">Integrations</Typography>
<Typography>Connect Uptime Genie to your favorite service</Typography>
<Grid container spacing={theme.gap.large} mt={theme.gap.xs}>
<Typography mb={theme.gap.large}>
Connect Uptime Genie to your favorite service
</Typography>
<Grid container spacing={theme.gap.large}>
{integrations.map((integration, index) => (
<IntegrationsComponent
key={index}
+1 -27
View File
@@ -14,33 +14,7 @@ import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import "./index.css";
import { monitorValidation } from "../../../Validation/validation";
import Select from "../../../Components/Inputs/Select";
const formatDurationRounded = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let time = "";
if (days > 0) {
time += `${days} day${days !== 1 ? "s" : ""}`;
return time;
}
if (hours > 0) {
time += `${hours} hour${hours !== 1 ? "s" : ""}`;
return time;
}
if (minutes > 0) {
time += `${minutes} minute${minutes !== 1 ? "s" : ""}`;
return time;
}
if (seconds > 0) {
time += `${seconds} second${seconds !== 1 ? "s" : ""}`;
return time;
}
return time;
};
import { formatDurationRounded } from "../../../Utils/timeUtils";
/**
* Helper function to get duration since last check
+1 -45
View File
@@ -13,53 +13,9 @@ 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";
import SettingsIcon from "../../../assets/icons/settings.svg?react";
import { formatDuration, formatDurationRounded } from "../../../Utils/timeUtils";
import "./index.css";
const formatDuration = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let dateStr = "";
days && (dateStr += `${days}d `);
hours && (dateStr += `${hours % 24}h `);
minutes && (dateStr += `${minutes % 60}m `);
seconds && (dateStr += `${seconds % 60}s `);
dateStr === "" && (dateStr = "0s");
return dateStr;
};
const formatDurationRounded = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let time = "";
if (days > 0) {
time += `${days} day${days !== 1 ? "s" : ""}`;
return time;
}
if (hours > 0) {
time += `${hours} hour${hours !== 1 ? "s" : ""}`;
return time;
}
if (minutes > 0) {
time += `${minutes} minute${minutes !== 1 ? "s" : ""}`;
return time;
}
if (seconds > 0) {
time += `${seconds} second${seconds !== 1 ? "s" : ""}`;
return time;
}
return time;
};
const StatBox = ({ title, value }) => {
return (
<Box className="stat-box">
@@ -0,0 +1,49 @@
.create-page-speed > .MuiStack-root {
position: relative;
margin: auto;
margin-top: var(--env-var-default-2);
background-color: var(--env-var-color-8);
width: 100%;
max-width: 500px;
box-shadow: var(--env-var-shadow-1);
border-radius: var(--env-var-radius-1);
padding: var(--env-var-default-2);
}
.create-page-speed h1.MuiTypography-root,
.create-page-speed h2.MuiTypography-root,
.create-page-speed p.MuiTypography-root {
color: var(--env-var-color-5);
}
.create-page-speed h1.MuiTypography-root,
.create-page-speed h2.MuiTypography-root {
font-weight: 600;
}
.create-page-speed h2.MuiTypography-root {
font-size: var(--env-var-font-size-medium-plus);
}
.create-page-speed p.MuiTypography-root,
.create-page-speed button {
font-size: var(--env-var-font-size-medium);
}
.create-page-speed .MuiBox-root > .field + p.MuiTypography-root {
font-size: var(--env-var-font-size-small-plus);
opacity: 0.6;
}
.create-page-speed .checkbox-wrapper {
margin-bottom: 1px;
}
.create-page-speed button:not(.MuiIconButton-root) {
height: var(--env-var-height-2);
}
.create-page-speed .MuiIconButton-root {
position: absolute;
top: 0;
right: 0;
margin: var(--env-var-spacing-1);
}
body:has(.create-page-speed) .select-dropdown .MuiMenuItem-root,
.create-page-speed .select-wrapper .select-component > .MuiSelect-select {
text-transform: lowercase;
}
@@ -0,0 +1,106 @@
import { Box, IconButton, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import { useNavigate } from "react-router";
import Field from "../../../Components/Inputs/Field";
import Select from "../../../Components/Inputs/Select";
import Button from "../../../Components/Button";
import Checkbox from "../../../Components/Inputs/Checkbox";
import "./index.css";
const CreatePageSpeed = () => {
const theme = useTheme();
const navigate = useNavigate();
const frequencies = [
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 3, name: "3 minutes" },
{ _id: 4, name: "4 minutes" },
{ _id: 5, name: "5 minutes" },
];
return (
<Box className="create-page-speed">
<Stack gap={theme.gap.xl}>
<IconButton
aria-label="close modal"
onClick={() => navigate("/page-speed")}
sx={{
p: "5px",
opacity: 0.6,
"&:focus": { outline: "none" },
}}
>
<CloseRoundedIcon />
</IconButton>
<Stack gap={theme.gap.large}>
<Typography component="h1">Create a page speed monitor</Typography>
<Field
type="text"
id="monitor-name"
label="Monitor friendly name"
placeholder="Example monitor"
value=""
onChange={() => console.log("disabled")}
error=""
/>
<Field
type="url"
id="monitor-url"
label="URL"
placeholder="random.website.com"
value=""
onChange={() => console.log("disabled")}
error=""
/>
<Select
id="monitor-frequency"
label="Check frequency"
items={frequencies}
value={1}
onChange={() => console.log("disabled")}
/>
</Stack>
<Stack gap={theme.gap.small}>
<Typography component="h2">Incidents notifications</Typography>
<Typography>When there is a new incident,</Typography>
<Checkbox
id="notify-sms"
label="Notify via SMS (coming soon)"
isChecked={false}
isDisabled={true}
/>
<Checkbox
id="notify-email"
label="Notify via email (to gorkem.cetin@bluewavelabs.ca)"
isChecked={true}
/>
<Checkbox
id="notify-emails"
label="Notify via email to following emails"
isChecked={true}
/>
<Box mx={`calc(${theme.gap.ml} * 2)`}>
<Field
id="notify-emails-list"
placeholder="notifications@gmail.com"
value=""
onChange={() => console.log("disabled")}
error=""
/>
<Typography mt={theme.gap.small}>
You can separate multiple emails with a comma
</Typography>
</Box>
</Stack>
<Stack direction="row" justifyContent="flex-end" gap={theme.gap.small}>
<Button level="tertiary" label="Cancel" />
<Button level="primary" label="Create" />
</Stack>
</Stack>
</Box>
);
};
export default CreatePageSpeed;
+30
View File
@@ -0,0 +1,30 @@
.page-speed h1.MuiTypography-root,
.page-speed span.MuiTypography-root,
.page-speed p.MuiTypography-root,
.page-speed .label {
color: var(--env-var-color-5);
}
.page-speed h2.MuiTypography-root {
color: var(--env-var-color-3);
line-height: 1.1;
}
.page-speed h1.MuiTypography-root,
.page-speed h2.MuiTypography-root {
font-size: var(--env-var-font-size-large);
font-weight: 600;
}
.page-speed span.MuiTypography-root,
.page-speed p.MuiTypography-root {
font-size: var(--env-var-font-size-medium);
}
.page-speed .MuiGrid-item > .MuiStack-root {
border: solid 1px var(--env-var-color-6);
border-radius: var(--env-var-radius-1);
background-color: var(--env-var-color-8);
}
.page-speed .label {
padding: 0 5px;
height: 22px;
font-size: var(--env-var-font-size-small);
border-color: var(--env-var-color-29);
}
+153
View File
@@ -0,0 +1,153 @@
import { Box, Grid, Stack, Typography } from "@mui/material";
import Fallback from "../../Components/Fallback";
import { useTheme } from "@emotion/react";
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
import "./index.css";
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
import { StatusLabel } from "../../Components/Label";
const Card = ({ data }) => {
const theme = useTheme();
/**
* Helper function to get duration since last check or the last date checked
* @param {Array} checks Array of check objects.
* @param {boolean} duration Whether the function should return the duration since last checked or the date itself
* @returns {number} Timestamp of the most recent check.
*/
const getLastChecked = (checks, duration = true) => {
if (!checks || checks.length === 0) {
return 0; // Handle case when no checks are available
}
// Data is sorted oldest -> newest, so last check is the most recent
if (!duration) {
return new Date(checks[checks.length - 1].createdAt);
}
return new Date() - new Date(checks[checks.length - 1].createdAt);
};
return (
<Grid item lg={6} flexGrow={1}>
<Stack direction="row" gap={theme.gap.medium} p={theme.gap.ml}>
<PageSpeedIcon style={{ width: theme.gap.ml, height: theme.gap.ml }} />
<Box flex={1}>
<Stack direction="row" justifyContent="space-between">
<Typography component="h2" mb={theme.gap.xs}>
{data.name}
</Typography>
{/* TODO - fix prop-type error */}
<StatusLabel
status={data.isActive ? "Live (collecting data)" : "Inactive"}
dot={
data.isActive
? "var(--env-var-color-17)"
: "var(--env-var-color-19)"
}
customStyles={{
backgroundColor: data.isActive
? "var(--env-var-color-20)"
: "var(--env-var-color-21)",
}}
/>
</Stack>
<Typography>{data.url}</Typography>
<Typography mt={theme.gap.large}>
<Typography component="span" fontWeight={600}>
Last checked:{" "}
</Typography>
{formatDate(getLastChecked(data.checks, false))}{" "}
<Typography component="span" fontStyle="italic">
({formatDurationRounded(getLastChecked(data.checks))} ago)
</Typography>
</Typography>
</Box>
</Stack>
</Grid>
);
};
const PageSpeed = () => {
const theme = useTheme();
// sample data, remove later
let monitors = [
{
success: true,
msg: 'Got monitor for 66a3d58ecd42ab3ed1171cf1 successfully"',
data: [
{
_id: "66a3ef558943628c59aabf00",
userId: "66a3d58ecd42ab3ed1171cf1",
name: "Google",
description: "Google",
status: true,
type: "pagespeed",
url: "https://www.google.com",
isActive: true,
interval: 10000,
createdAt: "2024-07-26T18:47:49.212Z",
updatedAt: "2024-07-26T18:47:49.212Z",
__v: 0,
checks: [
{
_id: "66a3f2266a073a2ff8dd0f7f",
monitorId: "66a3ef558943628c59aabf00",
status: true,
accessibility: 90,
bestPractices: 93,
seo: 92,
performance: 93,
createdAt: "2024-07-26T18:59:50.103Z",
updatedAt: "2024-07-26T18:59:50.103Z",
__v: 0,
},
{
_id: "66a3f2226a073a2ff8dd0f7d",
monitorId: "66a3ef558943628c59aabf00",
status: true,
accessibility: 90,
bestPractices: 93,
seo: 92,
performance: 87,
createdAt: "2024-07-26T18:59:46.280Z",
updatedAt: "2024-07-26T18:59:46.280Z",
__v: 0,
},
],
},
],
},
];
return (
<Box className="page-speed">
{monitors ? (
<Stack gap={theme.gap.xs}>
<Typography component="h1">All page speed monitors</Typography>
<Typography mb={theme.gap.large}>
Click on one of the monitors to get more site speed information.
</Typography>
<Grid container spacing={theme.gap.large}>
{monitors[0].data?.map((monitor) => (
<Card data={monitor} key={`monitor-${monitor._id}`} />
))}
</Grid>
</Stack>
) : (
<Fallback
title="page speed"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/page-speed/create"
/>
)}
</Box>
);
};
export default PageSpeed;
+59
View File
@@ -0,0 +1,59 @@
export const formatDuration = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let dateStr = "";
days && (dateStr += `${days}d `);
hours && (dateStr += `${hours % 24}h `);
minutes && (dateStr += `${minutes % 60}m `);
seconds && (dateStr += `${seconds % 60}s `);
dateStr === "" && (dateStr = "0s");
return dateStr;
};
export const formatDurationRounded = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let time = "";
if (days > 0) {
time += `${days} day${days !== 1 ? "s" : ""}`;
return time;
}
if (hours > 0) {
time += `${hours} hour${hours !== 1 ? "s" : ""}`;
return time;
}
if (minutes > 0) {
time += `${minutes} minute${minutes !== 1 ? "s" : ""}`;
return time;
}
if (seconds > 0) {
time += `${seconds} second${seconds !== 1 ? "s" : ""}`;
return time;
}
return time;
};
export const formatDate = (date) => {
const options = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
hour12: true,
};
// Return the date using the specified options
return date.toLocaleString("en-US", options);
};