Merge branch 'develop' into fix/incidents-remove-checks

This commit is contained in:
Alex Holliday committed 2024-09-17 08:35:53 +08:00
commit 5c94229994
13 files changed
+468 -109

No files matched your search

+1 -1
View File
@@ -67,7 +67,7 @@ export const update = createAsyncThunk(
fd.append("deleteProfileImage", form.deleteProfileImage);
const res = await networkService.updateUser(token, user._id, fd);
console.log(res);
return res.data;
} catch (error) {
if (error.response && error.response.data) {
+6 -1
View File
@@ -96,7 +96,12 @@ const Monitors = ({ isAdmin }) => {
alignItems="center"
mb={theme.spacing(8)}
>
<Typography component="h2" variant="h2" letterSpacing={-0.5}>
<Typography
component="h2"
variant="h2"
fontWeight={500}
letterSpacing={-0.2}
>
Actively monitoring
</Typography>
<Box
+23
View File
@@ -52,12 +52,35 @@ const useUtils = () => {
"& h2": { color: theme.palette.warning.main },
},
};
const pagespeedStyles = {
up: {
bg: theme.palette.success.bg,
light: theme.palette.success.light,
stroke: theme.palette.success.main,
},
down: {
bg: theme.palette.error.bg,
light: theme.palette.error.light,
stroke: theme.palette.error.main,
},
paused: {
bg: theme.palette.warning.bg,
light: theme.palette.warning.light,
stroke: theme.palette.warning.main,
},
pending: {
bg: theme.palette.warning.bg,
light: theme.palette.warning.light,
stroke: theme.palette.warning.main,
},
};
return {
determineState,
statusColor,
statusMsg,
pagespeedStatusMsg,
pagespeedStyles,
statusStyles,
};
};
+280 -45
View File
@@ -1,32 +1,220 @@
import PropTypes from "prop-types";
import PageSpeedIcon from "../../assets/icons/page-speed.svg?react";
import { StatusLabel } from "../../Components/Label";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useNavigate } from "react-router";
import { useTheme } from "@emotion/react";
import { formatDate, formatDurationRounded } from "../../Utils/timeUtils";
import { getLastChecked } from "../../Utils/monitorUtils";
import { IconBox } from "./Details/styled";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
} from "recharts";
import { useSelector } from "react-redux";
import {
formatDateWithTz,
formatDurationRounded,
formatDurationSplit,
} from "../../Utils/timeUtils";
import useUtils from "../Monitors/utils";
import PropTypes from "prop-types";
/**
* CustomToolTip displays a tooltip with formatted date and score information.
* @param {Object} props
* @param {Array} props.payload - Data to display in the tooltip
* @returns {JSX.Element} The rendered tooltip component
*/
const CustomToolTip = ({ payload }) => {
const theme = useTheme();
const uiTimezone = useSelector((state) => state.ui.timezone);
return (
<Box
sx={{
backgroundColor: theme.palette.background.main,
border: 1,
borderColor: theme.palette.border.dark,
borderRadius: theme.shape.borderRadius,
py: theme.spacing(2),
px: theme.spacing(4),
}}
>
<Typography
sx={{
color: theme.palette.text.tertiary,
fontSize: 12,
fontWeight: 500,
}}
>
{formatDateWithTz(
payload[0]?.payload.createdAt,
"ddd, MMMM D, YYYY, h:mm A",
uiTimezone
)}
</Typography>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={payload[0]?.color}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
textTransform="capitalize"
sx={{ opacity: 0.8 }}
>
{payload[0]?.name}
</Typography>{" "}
<Typography component="span">{payload[0]?.payload.score}</Typography>
</Stack>
</Box>
);
};
CustomToolTip.propTypes = {
payload: PropTypes.array,
};
/**
* Processes the raw data to include a score for each entry.
* @param {Array<Object>} data - The raw data array.
* @returns {Array<Object>} - The formatted data array with scores.
*/
const processData = (data) => {
if (data.length === 0) return [];
let formattedData = [];
const calculateScore = (entry) => {
return (
(entry.accessibility +
entry.bestPractices +
entry.performance +
entry.seo) /
4
);
};
data.forEach((entry) => {
entry = { ...entry, score: calculateScore(entry) };
formattedData.push(entry);
});
return formattedData;
};
/**
* Renders an area chart displaying page speed scores.
* @param {Object} props
* @param {Array<Object>} props.data - The raw data to be displayed in the chart.
* @param {string} props.status - The status of the page speed which determines the chart's color scheme.
* @returns {JSX.Element} - The rendered area chart.
*/
const PagespeedAreaChart = ({ data, status }) => {
const theme = useTheme();
const { pagespeedStyles } = useUtils();
const formattedData = processData(data);
return (
<ResponsiveContainer width="100%" minWidth={25} height={85}>
<AreaChart
width="100%"
height="100%"
data={formattedData}
margin={{ top: 10, bottom: -5 }}
style={{ cursor: "pointer" }}
>
<CartesianGrid
stroke={theme.palette.border.light}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
content={<CustomToolTip />}
/>
<defs>
<linearGradient
id={`pagespeed-chart-${status}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={pagespeedStyles[status].stroke}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={pagespeedStyles[status].light}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
dataKey="score"
stroke={pagespeedStyles[status].stroke}
strokeWidth={1.5}
fill={`url(#pagespeed-chart-${status})`}
activeDot={{
stroke: pagespeedStyles[status].light,
fill: pagespeedStyles[status].stroke,
r: 4.5,
}}
/>
</AreaChart>
</ResponsiveContainer>
);
};
PagespeedAreaChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
accessibility: PropTypes.number.isRequired,
bestPractices: PropTypes.number.isRequired,
performance: PropTypes.number.isRequired,
seo: PropTypes.number.isRequired,
})
).isRequired,
status: PropTypes.string.isRequired,
};
/**
* Renders a card displaying monitor details and an area chart.
* @param {Object} props
* @param {Object} props.monitor - The monitor data to be displayed in the card.
* @returns {JSX.Element} - The rendered card.
*/
const Card = ({ monitor }) => {
const { determineState, pagespeedStatusMsg } = useUtils();
const theme = useTheme();
const navigate = useNavigate();
const monitorState = determineState(monitor);
return (
<Grid
item
lg={6}
flexGrow={1}
sx={{
"&:hover > .MuiStack-root": {
backgroundColor: theme.palette.background.accent,
},
}}
>
<Stack
direction="row"
gap={theme.spacing(6)}
<Grid item lg={6} flexGrow={1}>
<Box
position="relative"
p={theme.spacing(8)}
onClick={() => navigate(`/pagespeed/${monitor._id}`)}
border={1}
@@ -34,41 +222,88 @@ const Card = ({ monitor }) => {
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.background.main}
sx={{
display: "grid",
gridTemplateColumns: "34px 2fr 1fr",
columnGap: theme.spacing(5),
gridTemplateRows: "34px 1fr 3fr",
cursor: "pointer",
"& svg path": { stroke: theme.palette.other.icon, strokeWidth: 0.8 },
"&:hover": {
backgroundColor: theme.palette.background.accent,
},
}}
>
<PageSpeedIcon
style={{ width: theme.spacing(8), height: theme.spacing(8) }}
<IconBox>
<PageSpeedIcon />
</IconBox>
<Typography
component="h2"
variant="h2"
fontWeight={500}
alignSelf="center"
>
{monitor.name}
</Typography>
<StatusLabel
status={monitorState}
text={pagespeedStatusMsg[monitorState] || "Pending..."}
customStyles={{
width: "max-content",
textTransform: "capitalize",
alignSelf: "flex-start",
justifySelf: "flex-end",
}}
/>
<Box flex={1}>
<Stack direction="row" justifyContent="space-between">
<Typography
component="h2"
mb={theme.spacing(2)}
fontSize={16}
color={theme.palette.primary.main}
>
{monitor.name}
</Typography>
<StatusLabel
status={monitorState}
text={pagespeedStatusMsg[monitorState] || "Pending..."}
customStyles={{ textTransform: "capitalize" }}
/>
</Stack>
<Typography>{monitor.url.replace(/^https?:\/\//, "")}</Typography>
<Typography mt={theme.spacing(12)}>
<Typography component="span" variant="body2" fontWeight={600}>
Last checked:{" "}
</Typography>
{formatDate(getLastChecked(monitor.checks, false))}{" "}
<Typography component="span" variant="body2" fontStyle="italic">
({formatDurationRounded(getLastChecked(monitor.checks))} ago)
</Typography>
<Typography
variant="body2"
mt={theme.spacing(-2)}
sx={{ gridColumnStart: 2 }}
>
{monitor.url}
</Typography>
<Box
mx={theme.spacing(-8)}
mt={theme.spacing(4)}
mb={theme.spacing(-8)}
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
>
<PagespeedAreaChart data={monitor.checks} status={monitorState} />
</Box>
<Box
position="absolute"
bottom={0}
py={theme.spacing(1)}
px={theme.spacing(4)}
borderTop={1}
borderRight={1}
borderColor={theme.palette.border.light}
backgroundColor={theme.palette.background.accent}
sx={{
pointerEvents: "none",
userSelect: "none",
borderTopRightRadius: 8,
borderBottomLeftRadius: 4,
}}
>
<Typography fontSize={11} color={theme.palette.text.accent}>
Checking every{" "}
{(() => {
const { time, format } = formatDurationSplit(monitor?.interval);
return (
<>
<Typography
component="span"
fontSize={12}
color={theme.palette.text.primary}
>
{time}{" "}
</Typography>
{format}
</>
);
})()}
</Typography>
</Box>
</Stack>
</Box>
</Grid>
);
};
+1 -4
View File
@@ -1,9 +1,6 @@
.page-speed h2.MuiTypography-root {
line-height: 1.1;
}
.page-speed .label {
padding: 7px;
height: 22px;
height: 24px;
font-size: var(--env-var-font-size-small);
}
.page-speed:not(:has([class*="fallback__"])) button {
+2 -8
View File
@@ -56,7 +56,6 @@ const PageSpeed = ({ isAdmin }) => {
// 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(monitors);
return (
<Box
className="page-speed"
@@ -75,13 +74,7 @@ const PageSpeed = ({ isAdmin }) => {
{isActuallyLoading ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Box
sx={{
"& p": {
color: theme.palette.text.secondary,
},
}}
>
<Box>
<Box mb={theme.spacing(12)}>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
@@ -95,6 +88,7 @@ const PageSpeed = ({ isAdmin }) => {
variant="contained"
color="primary"
onClick={() => navigate("/pagespeed/create")}
sx={{ whiteSpace: "nowrap" }}
>
Create new
</Button>
+84 -40
View File
@@ -1,5 +1,5 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, styled, Typography } from "@mui/material";
import { Box, Stack, Typography } from "@mui/material";
import Field from "../../Components/Inputs/Field";
import Link from "../../Components/Link";
import Select from "../../Components/Inputs/Select";
@@ -12,20 +12,92 @@ import {
addDemoMonitors,
deleteAllMonitors,
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { update } from "../../Features/Auth/authSlice";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { setTimezone } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState } from "react";
import { ConfigBox } from "./styled";
import { networkService } from "../../main";
import { settingsValidation } from "../../Validation/validation";
const SECONDS_PER_DAY = 86400;
const Settings = ({ isAdmin }) => {
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
const { checkTTL } = user;
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
const { timezone } = useSelector((state) => state.ui);
const [checksIsLoading, setChecksIsLoading] = useState(false);
const [form, setForm] = useState({
ttl: (checkTTL / SECONDS_PER_DAY).toString(),
});
const [errors, setErrors] = useState({});
const dispatch = useDispatch();
const handleChange = (event) => {
const { value, id } = event.target;
const { error } = settingsValidation.validate(
{ [id]: value },
{
abortEarly: false,
}
);
if (!error || error.details.length === 0) {
setErrors({});
} else {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
console.log(newErrors);
logger.error("Validation errors:", error.details);
}
let inputValue = value;
id === "ttl" && (inputValue = value.replace(/[^0-9]/g, ""));
setForm((prev) => ({
...prev,
[id]: inputValue,
}));
};
// TODO Handle saving
const handleSave = async () => {};
const handleSave = async () => {
try {
setChecksIsLoading(true);
await networkService.updateChecksTTL(authToken, form.ttl);
const updatedUser = { ...user, checkTTL: form.ttl };
const action = await dispatch(
update({ authToken, localData: updatedUser })
);
if (action.payload.success) {
createToast({
body: "Settings saved successfully",
});
} else {
if (action.payload) {
// dispatch errors
createToast({
body: action.payload.msg,
});
} else {
// unknown errors
createToast({
body: "Unknown error.",
});
}
}
} catch (error) {
console.log(error);
createToast({ body: "Failed to save settings" });
} finally {
setChecksIsLoading(false);
}
};
const handleClearStats = async () => {
try {
@@ -72,33 +144,6 @@ const Settings = ({ isAdmin }) => {
}
};
const ConfigBox = styled("div")({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
paddingTop: theme.spacing(12),
paddingInline: theme.spacing(15),
paddingBottom: theme.spacing(25),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
"& > div:first-of-type": {
flex: 0.7,
},
"& > div:last-of-type": {
flex: 1,
},
"& h1, & h2": {
color: theme.palette.text.secondary,
},
"& p": {
color: theme.palette.text.tertiary,
},
});
return (
<Box
className="settings"
@@ -143,21 +188,19 @@ const Settings = ({ isAdmin }) => {
</Box>
<Stack gap={theme.spacing(20)}>
<Field
type="text"
id="history-monitoring"
id="ttl"
label="The days you want to keep monitoring history."
isOptional={true}
optionalLabel="0 for infinite"
placeholder="90"
value=""
onChange={() => logger.warn("Disabled")}
value={form.ttl}
onChange={handleChange}
error={errors.ttl}
/>
<Box>
<Typography>Clear all stats. This is irreversible.</Typography>
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleClearStats}
sx={{ mt: theme.spacing(4) }}
>
@@ -181,7 +224,7 @@ const Settings = ({ isAdmin }) => {
<LoadingButton
variant="contained"
color="primary"
loading={isLoading}
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleInsertDemoMonitors}
sx={{ mt: theme.spacing(4) }}
>
@@ -193,7 +236,7 @@ const Settings = ({ isAdmin }) => {
<LoadingButton
variant="contained"
color="error"
loading={isLoading}
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleDeleteAllMonitors}
sx={{ mt: theme.spacing(4) }}
>
@@ -223,7 +266,8 @@ const Settings = ({ isAdmin }) => {
</ConfigBox>
<Stack direction="row" justifyContent="flex-end">
<LoadingButton
loading={false}
loading={isLoading || authIsLoading || checksIsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="primary"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
+28
View File
@@ -0,0 +1,28 @@
import { Stack, styled } from "@mui/material";
export const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
paddingTop: theme.spacing(12),
paddingInline: theme.spacing(15),
paddingBottom: theme.spacing(25),
backgroundColor: theme.palette.background.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: theme.spacing(2),
"& > div:first-of-type": {
flex: 0.7,
},
"& > div:last-of-type": {
flex: 1,
},
"& h1, & h2": {
color: theme.palette.text.secondary,
},
"& p": {
color: theme.palette.text.tertiary,
},
}));
+13
View File
@@ -582,6 +582,19 @@ class NetworkService {
}
);
}
async updateChecksTTL(authToken, ttl) {
return this.axiosInstance.put(
`/checks/ttl`,
{ ttl },
{
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
},
}
);
}
}
export default NetworkService;
+7
View File
@@ -146,9 +146,16 @@ const maintenanceWindowValidation = joi.object({
}),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
"string.empty": "TTL is required",
}),
});
export {
credentials,
imageValidation,
monitorValidation,
maintenanceWindowValidation,
settingsValidation,
};
+9 -2
View File
@@ -10,6 +10,8 @@ const {
updateChecksTTLBodyValidation,
} = require("../validation/joi");
const { successMessages } = require("../utils/messages");
const jwt = require("jsonwebtoken");
const { getTokenFromHeaders } = require("../utils/utils");
const SERVICE_NAME = "checkController";
const createCheck = async (req, res, next) => {
@@ -144,6 +146,8 @@ const deleteChecksByTeamId = async (req, res, next) => {
};
const updateChecksTTL = async (req, res, next) => {
const SECONDS_PER_DAY = 86400;
try {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
@@ -157,8 +161,11 @@ const updateChecksTTL = async (req, res, next) => {
}
try {
const ttl = req.body.ttl;
await req.db.updateChecksTTL(1, ttl);
// Get user's teamId
const token = getTokenFromHeaders(req.headers);
const { teamId } = jwt.verify(token, process.env.JWT_SECRET);
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await req.db.updateChecksTTL(teamId, ttl);
return res.status(200).json({
success: true,
msg: successMessages.CHECK_UPDATE_TTL,
+11 -8
View File
@@ -1,5 +1,6 @@
const Check = require("../../../models/Check");
const Monitor = require("../../../models/Monitor");
const User = require("../../../models/user");
const logger = require("../../../utils/logger");
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
@@ -248,6 +249,14 @@ const deleteChecksByTeamId = async (teamId) => {
const updateChecksTTL = async (teamId, ttl) => {
try {
await Check.collection.dropIndex("expiry_1");
} catch (error) {
logger.error("Failed to drop index", {
service: SERVICE_NAME,
method: "updateChecksTTL",
});
}
try {
await Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
@@ -257,15 +266,9 @@ const updateChecksTTL = async (teamId, ttl) => {
error.method = "updateChecksTTL";
throw error;
}
};
const updateChecksTTL = async (teamId, ttl) => {
// Update user
try {
await Check.collection.dropIndex("expiry_1");
await Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
);
await User.updateMany({ teamId: teamId }, { checkTTL: ttl });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
+3
View File
@@ -45,6 +45,9 @@ const UserSchema = mongoose.Schema(
ref: "Team",
immutable: true,
},
checkTTL: {
type: Number,
},
},
{
timestamps: true,