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:
1 parent
954d11b7c4
commit
db66ba849b
17 files changed
+423
-156
No files matched your search
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 /> },
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
Reference in new issue
Block a user