resolving conflicts

This commit is contained in:
Caio Cabral committed 2024-12-12 09:15:39 -05:00
commit 788ffc92c8
48 files changed
+593 -580

No files matched your search

@@ -1,6 +1,6 @@
## Describe your changes
Provide a brief description of the changes you’ve made and their purpose.
Briefly describe the changes you made and their purpose.
## Issue number
@@ -11,4 +11,11 @@ Mention the issue number(s) this PR addresses (e.g., #123).
- [ ] I have performed a self-review of my code.
- [ ] I have included the issue # in the PR.
- [ ] I have labelled the PR correctly.
- [ ] My PR is granular and targeted to one specific feature only.
- [ ] The issue I am working on is assigned to me.
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] I made sure font sizes, color choices etc are all referenced from the theme.
- [ ] My PR is granular and targeted to one specific feature.
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
-1
View File
@@ -3,7 +3,6 @@ import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import "react-toastify/dist/ReactToastify.css";
import { ToastContainer } from "react-toastify";
import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
+36 -17
View File
@@ -3,33 +3,50 @@ import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react";
import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react";
import "./index.css";
/**
* @param {Object} props
* @param {string} props.id - The id attribute for the checkbox input.
* @param {string} props.label - The label to display next to the checkbox.
* @param {('small' | 'medium' | 'large')} - The size of the checkbox.
* @param {boolean} props.isChecked - Whether the checkbox is checked or not.
* @param {string} [props.value] - The value of the checkbox input.
* @param {function} [props.onChange] - The function to call when the checkbox value changes.
* @param {boolean} [props.isDisabled] - Whether the checkbox is disabled or not.
* Checkbox Component
*
* @returns {JSX.Element}
* A customized checkbox component using Material-UI that supports custom sizing,
* disabled states, and custom icons.
*
* @component
* @param {Object} props - Component properties
* @param {string} props.id - Unique identifier for the checkbox input
* @param {string} [props.name] - Optional name attribute for the checkbox
* @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox
* @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon
* @param {boolean} props.isChecked - Current checked state of the checkbox
* @param {string} [props.value] - Optional value associated with the checkbox
* @param {Function} [props.onChange] - Callback function triggered when checkbox state changes
* @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled
*
* @returns {React.ReactElement} Rendered Checkbox component
*
* @example
* // Basic usage
* <Checkbox
* id="checkbox-id"
* label="Ping monitoring"
* isChecked={checks.type === "ping"}
* value="ping"
* onChange={handleChange}
* id="terms-checkbox"
* label="I agree to terms"
* isChecked={agreed}
* onChange={handleAgree}
* />
*
* @example
* // With custom size and disabled state
* <Checkbox
* id="advanced-checkbox"
* label="Advanced Option"
* size="large"
* isChecked={isAdvanced}
* isDisabled={!canModify}
* onChange={handleAdvancedToggle}
* />
*/
const Checkbox = ({
id,
name,
label,
size = "medium",
isChecked,
@@ -46,6 +63,7 @@ const Checkbox = ({
control={
<MuiCheckbox
checked={isDisabled ? false : isChecked}
name={name}
value={value}
onChange={onChange}
icon={<CheckboxOutline />}
@@ -54,7 +72,7 @@ const Checkbox = ({
"aria-label": "controlled checkbox",
id: id,
}}
sx={{
sx={{
"&:hover": { backgroundColor: "transparent" },
"& svg": { width: sizes[size], height: sizes[size] },
alignSelf: "flex-start",
@@ -89,6 +107,7 @@ const Checkbox = ({
Checkbox.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
isChecked: PropTypes.bool.isRequired,
+3 -3
View File
@@ -47,7 +47,7 @@ import Folder from "../../assets/icons/folder.svg?react";
import "./index.css";
const menu = [
{ name: "Monitors", path: "monitors", icon: <Monitors /> },
{ name: "Uptime", path: "uptime", icon: <Monitors /> },
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
@@ -262,10 +262,10 @@ function Sidebar() {
Menu
</ListSubheader>
}
sx={{
sx={{
px: theme.spacing(6),
height: "100%",
overflow: "hidden"
overflow: "hidden",
}}
>
{menu.map((item) =>
+2 -2
View File
@@ -388,7 +388,7 @@ const Login = () => {
useEffect(() => {
if (authToken) {
navigate("/monitors");
navigate("/uptime");
return;
}
networkService
@@ -454,7 +454,7 @@ const Login = () => {
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/monitors");
navigate("/uptime");
createToast({
body: "Welcome back! You're successfully logged in.",
});
+1 -1
View File
@@ -237,7 +237,7 @@ const Register = ({ isSuperAdmin }) => {
if (action.payload.success) {
const authToken = action.payload.data;
localStorage.setItem("token", authToken);
navigate("/monitors");
navigate("/uptime");
createToast({
body: "Welcome! Your account was created successfully.",
});
@@ -5,32 +5,58 @@ import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
/**
* `CustomThreshold` is a functional React component that displays a
* group of CheckBox with a label and its correspondant threshold input field.
* CustomThreshold Component
*
* @param {{ checkboxId: any; checkboxLabel: any; onCheckboxChange: any; fieldId: any; onFieldChange: any; onFieldBlur: any; alertUnit: any; infrastructureMonitor: any; errors: any; }} param0
* @param {string} param0.checkboxId - The text is the id of the checkbox.
* @param {string} param0.checkboxLabel - The text to be displayed as the label next to the check icon.
* @param {func} param0.onCheckboxChange - The function to invoke when checkbox is checked or unchecked.
* @param {string} param0.fieldId - The text is the id of the input field.
* @param {func} param0.onFieldChange - The function to invoke when input field is changed.
* @param {func} param0.onFieldBlur - The function to invoke when input field is losing focus.
* @param {string} param0.alertUnit the threshold unit such as usage percentage '%' etc
* @param {object} param0.infrastructureMonitor the form object of the create infrastrcuture monitor page
* @param {object} param0.errors the object that holds all the errors of the form page
* @returns A compound React component that renders the custom threshold alert section
* A reusable component that renders a checkbox with an associated numeric input field
* and an optional unit label. The input field can be enabled/disabled based on checkbox state.
*
* @component
* @param {Object} props - Component properties
* @param {string} [props.checkboxId] - Optional unique identifier for the checkbox
* @param {string} [props.checkboxName] - Optional name attribute for the checkbox
* @param {string} props.checkboxLabel - Label text for the checkbox
* @param {boolean} props.isChecked - Current checked state of the checkbox
* @param {Function} props.onCheckboxChange - Callback function when checkbox is toggled
* @param {string} props.fieldId - Unique identifier for the input field
* @param {string} [props.fieldName] - Optional name attribute for the input field
* @param {string} props.fieldValue - Current value of the input field
* @param {Function} props.onFieldChange - Callback function when input field value changes
* @param {Function} props.onFieldBlur - Callback function when input field loses focus
* @param {string} props.alertUnit - Unit label displayed next to the input field
* @param {Object} props.errors - Object containing validation errors for the field
* @param {Object} props.infrastructureMonitor - Infrastructure monitor configuration object
*
* @returns {React.ReactElement} Rendered CustomThreshold component
*
* @example
* <CustomThreshold
* checkboxId="cpu-threshold"
* checkboxName="cpu_threshold"
* checkboxLabel="Enable CPU Threshold"
* isChecked={true}
* onCheckboxChange={handleCheckboxToggle}
* fieldId="cpu-threshold-value"
* fieldName="cpu_threshold_value"
* fieldValue="80"
* onFieldChange={handleFieldChange}
* onFieldBlur={handleFieldBlur}
* alertUnit="%"
* errors={{}}
* infrastructureMonitor={monitorConfig}
* />
*/
export const CustomThreshold = ({
checkboxId,
checkboxName,
checkboxLabel,
onCheckboxChange,
isChecked,
fieldId,
fieldName,
fieldValue,
onFieldChange,
onFieldBlur,
alertUnit,
infrastructureMonitor,
errors,
}) => {
const theme = useTheme();
@@ -46,8 +72,9 @@ export const CustomThreshold = ({
<Box>
<Checkbox
id={checkboxId}
name={checkboxName}
label={checkboxLabel}
isChecked={infrastructureMonitor[checkboxId]}
isChecked={isChecked}
onChange={onCheckboxChange}
/>
</Box>
@@ -61,11 +88,12 @@ export const CustomThreshold = ({
maxWidth="var(--env-var-width-4)"
type="number"
id={fieldId}
value={infrastructureMonitor[fieldId]}
name={fieldName}
value={fieldValue}
onBlur={onFieldBlur}
onChange={onFieldChange}
error={errors[fieldId] ? true : false}
disabled={!infrastructureMonitor[checkboxId]}
disabled={!isChecked}
/>
<Typography
@@ -80,10 +108,14 @@ export const CustomThreshold = ({
};
CustomThreshold.propTypes = {
checkboxId: PropTypes.string.isRequired,
checkboxId: PropTypes.string,
checkboxName: PropTypes.string,
checkboxLabel: PropTypes.string.isRequired,
isChecked: PropTypes.bool.isRequired,
onCheckboxChange: PropTypes.func.isRequired,
fieldId: PropTypes.string.isRequired,
fieldName: PropTypes.string,
fieldValue: PropTypes.string.isRequired,
onFieldChange: PropTypes.func.isRequired,
onFieldBlur: PropTypes.func.isRequired,
alertUnit: PropTypes.string.isRequired,
@@ -1,27 +1,62 @@
import { useState } from "react";
import { Box, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { useSelector, useDispatch } from "react-redux";
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { parseDomainName } from "../../../Utils/monitorUtils";
import {
createInfrastructureMonitor,
checkInfrastructureEndpointResolution,
} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { useNavigate } from "react-router-dom";
// React, Redux, Router
import { useTheme } from "@emotion/react";
import { createToast } from "../../../Utils/toastUtils";
import Link from "../../../Components/Link";
import { ConfigBox } from "../../Monitors/styled";
import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
// Utility and Network
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { createInfrastructureMonitor } from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { CustomThreshold } from "../CreateMonitor/CustomThreshold";
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import Link from "../../../Components/Link";
import { ConfigBox } from "../../Uptime/styled";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import { CustomThreshold } from "./CustomThreshold";
const SELECT_VALUES = [
{ _id: 0.25, name: "15 seconds" },
{ _id: 0.5, name: "30 seconds" },
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
];
const METRICS = ["cpu", "memory", "disk", "temperature"];
const METRIC_PREFIX = "usage_";
const MS_PER_MINUTE = 60000;
const hasAlertError = (errors) => {
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
};
const getAlertError = (errors) => {
return Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))
? errors[Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX))]
: null;
};
const CreateInfrastructureMonitor = () => {
const theme = useTheme();
const { user, authToken } = useSelector((state) => state.auth);
const monitorState = useSelector((state) => state.infrastructureMonitor);
const dispatch = useDispatch();
const navigate = useNavigate();
// State
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
@@ -38,118 +73,73 @@ const CreateInfrastructureMonitor = () => {
secret: "",
});
const MS_PER_MINUTE = 60000;
const THRESHOLD_FIELD_PREFIX = "usage_";
const HARDWARE_MONITOR_TYPES = ["cpu", "memory", "disk", "temperature"];
const { user, authToken } = useSelector((state) => state.auth);
const monitorState = useSelector((state) => state.infrastructureMonitor);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const idMap = {
"notify-email-default": "notification-email",
};
const [errors, setErrors] = useState({});
const alertErrKeyLen = Object.keys(errors).filter((k) =>
k.startsWith(THRESHOLD_FIELD_PREFIX)
).length;
const handleCustomAlertCheckChange = (event) => {
const { value, id } = event.target;
setInfrastructureMonitor((prev) => {
const newState = {
[id]: prev[id] == undefined && value == "on" ? true : !prev[id],
};
return {
...prev,
...newState,
[THRESHOLD_FIELD_PREFIX + id]: newState[id]
? prev[THRESHOLD_FIELD_PREFIX + id]
: "",
};
});
// Remove the error if unchecked
setErrors((prev) => {
return buildErrors(prev, [THRESHOLD_FIELD_PREFIX + id]);
});
};
const handleBlur = (event, appendID) => {
// Handlers
const handleCreateInfrastructureMonitor = async (event) => {
event.preventDefault();
const { value, id } = event.target;
let name = idMap[id] ?? id;
if (name === "url" && infrastructureMonitor.name === "") {
setInfrastructureMonitor((prev) => ({
...prev,
name: parseDomainName(value),
}));
// Build the form
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
: {}),
memory: infrastructureMonitor.memory,
...(infrastructureMonitor.memory
? { usage_memory: infrastructureMonitor.usage_memory }
: {}),
disk: infrastructureMonitor.disk,
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
};
const { error } = infrastructureMonitorValidation.validate(form, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return;
}
if (id?.startsWith("notify-email-")) return;
const { error } = infrastructureMonitorValidation.validate(
{ [id ?? appendID]: value },
{
abortEarly: false,
}
);
setErrors((prev) => {
return buildErrors(prev, id ?? appendID, error);
});
};
// Build the thresholds for the form
const {
cpu,
usage_cpu,
memory,
usage_memory,
disk,
usage_disk,
temperature,
usage_temperature,
...rest
} = form;
const handleChange = (event, appendedId) => {
event.preventDefault();
const { value, id } = event.target;
let name = appendedId ?? idMap[id] ?? id;
if (name.includes("notification-")) {
name = name.replace("notification-", "");
let hasNotif = infrastructureMonitor.notifications.some(
(notification) => notification.type === name
);
setInfrastructureMonitor((prev) => {
const notifs = [...prev.notifications];
if (hasNotif) {
return {
...prev,
notifications: notifs.filter((notif) => notif.type !== name),
};
} else {
return {
...prev,
notifications: [
...notifs,
name === "email"
? { type: name, address: value }
: // TODO - phone number
{ type: name, phone: value },
],
};
}
});
} else {
setInfrastructureMonitor((prev) => ({
...prev,
[name]: value,
}));
}
};
const generatePayload = (form) => {
let thresholds = {};
Object.keys(form)
.filter((k) => k.startsWith(THRESHOLD_FIELD_PREFIX))
.map((k) => {
if (form[k]) thresholds[k] = form[k] / 100;
delete form[k];
delete form[k.substring(THRESHOLD_FIELD_PREFIX.length)];
});
const thresholds = {
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
...(memory ? { usage_memory: usage_memory / 100 } : {}),
...(disk ? { usage_disk: usage_disk / 100 } : {}),
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
};
form = {
...form,
...rest,
description: form.name,
teamId: user.teamId,
userId: user._id,
@@ -157,54 +147,69 @@ const CreateInfrastructureMonitor = () => {
notifications: infrastructureMonitor.notifications,
thresholds,
};
return form;
};
const handleCreateInfrastructureMonitor = async (event) => {
event.preventDefault();
let form = {
...infrastructureMonitor,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
};
delete form.notifications;
if (hasValidationErrors(form, infrastructureMonitorValidation, setErrors)) {
return;
const action = await dispatch(
createInfrastructureMonitor({ authToken, monitor: form })
);
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Infrastructure monitor created successfully!" });
navigate("/infrastructure");
} else {
const checkEndpointAction = await dispatch(
checkInfrastructureEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
const action = await dispatch(
createInfrastructureMonitor({ authToken, monitor: generatePayload(form) })
);
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Infrastructure monitor created successfully!" });
navigate("/infrastructure");
} else {
createToast({ body: "Failed to create monitor." });
}
createToast({ body: "Failed to create monitor." });
}
};
//select values
const frequencies = [
{ _id: 0.25, name: "15 seconds" },
{ _id: 0.5, name: "30 seconds" },
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
];
const handleChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleCheckboxChange = (event) => {
const { name } = event.target;
const { checked } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: checked,
});
};
const handleNotifications = (event, type) => {
const { value } = event.target;
let notifications = [...infrastructureMonitor.notifications];
const notificationExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationExists) {
notifications = notifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
notifications.push({ type, address: value });
}
setInfrastructureMonitor((prev) => ({
...prev,
notifications,
}));
};
return (
<Box className="create-infrastructure-monitor">
@@ -239,56 +244,75 @@ const CreateInfrastructureMonitor = () => {
fontSize="inherit"
fontWeight="inherit"
>
infrastructure monitor
monitor
</Typography>
</Typography>
<ConfigBox>
<Box>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the friendly name
and authorization secret to connect to the server agent.
</Typography>
<Typography component="p">
The server you are monitoring must be running the{" "}
<Link
level="primary"
url="https://github.com/bluewave-labs/checkmate-agent"
label="Checkmate Monitoring Agent"
/>
</Typography>
</Stack>
</Box>
<Stack gap={theme.spacing(6)}>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the friendly name and
authorization secret to connect to the server agent.
</Typography>
<Typography component="p">
The server you are monitoring must be running the{" "}
<Link
level="primary"
url="https://github.com/bluewave-labs/checkmate-agent"
label="Checkmate Monitoring Agent"
/>
</Typography>
</Stack>
<Stack gap={theme.spacing(15)}>
<TextInput
type="text"
type="url"
id="url"
name="url"
startAdornment={<HttpAdornment https={https} />}
placeholder={"localhost:59232/api/v1/metrics"}
label="Server URL"
placeholder="https://"
https={https}
value={infrastructureMonitor.url}
onBlur={handleBlur}
onChange={handleChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<Box>
<Typography component="p">Protocol</Typography>
<ButtonGroup>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
HTTPS
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
HTTP
</Button>
</ButtonGroup>
</Box>
<TextInput
type="text"
id="name"
name="name"
label="Display name"
placeholder="Google"
isOptional={true}
value={infrastructureMonitor.name}
onBlur={handleBlur}
onChange={handleChange}
error={errors["name"]}
/>
<TextInput
type="text"
id="secret"
name="secret"
label="Authorization secret"
value={infrastructureMonitor.secret}
onBlur={handleBlur}
onChange={handleChange}
error={errors["secret"] ? true : false}
helperText={errors["secret"]}
@@ -310,12 +334,10 @@ const CreateInfrastructureMonitor = () => {
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(e) => handleChange(e)}
onBlur={handleBlur}
onChange={(event) => handleNotifications(event, "email")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Customize alerts</Typography>
@@ -325,25 +347,31 @@ const CreateInfrastructureMonitor = () => {
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{HARDWARE_MONITOR_TYPES.map((type, idx) => (
<CustomThreshold
key={idx}
checkboxId={type}
checkboxLabel={
type !== "cpu" ? capitalizeFirstLetter(type) : type.toUpperCase()
}
onCheckboxChange={handleCustomAlertCheckChange}
fieldId={THRESHOLD_FIELD_PREFIX + type}
fieldValue={infrastructureMonitor[THRESHOLD_FIELD_PREFIX + type] ?? ""}
onFieldChange={handleChange}
onFieldBlur={handleBlur}
// TODO: need BE, maybe in another PR
alertUnit={type == "temperature" ? "°C" : "%"}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
/>
))}
{alertErrKeyLen > 0 && (
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu"
? capitalizeFirstLetter(metric)
: metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={infrastructureMonitor[METRIC_PREFIX + metric]}
onFieldChange={handleChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{/* Error text */}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
@@ -353,14 +381,7 @@ const CreateInfrastructureMonitor = () => {
opacity: 0.8,
}}
>
{
errors[
THRESHOLD_FIELD_PREFIX +
HARDWARE_MONITOR_TYPES.filter(
(type) => errors[THRESHOLD_FIELD_PREFIX + type]
)[0]
]
}
{getAlertError(errors)}
</Typography>
)}
</Stack>
@@ -372,11 +393,12 @@ const CreateInfrastructureMonitor = () => {
<Stack gap={theme.spacing(12)}>
<Select
id="interval"
name="interval"
label="Check frequency"
value={infrastructureMonitor.interval || 15}
onChange={(e) => handleChange(e, "interval")}
onBlur={(e) => handleBlur(e, "interval")}
items={frequencies}
onChange={handleChange}
onBlur={}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
@@ -8,7 +8,7 @@ import AreaChart from "../../../Components/Charts/AreaChart";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
import PulseDot from "../../../Components/Animated/PulseDot";
import useUtils from "../../Monitors/utils";
import useUtils from "../../Uptime/utils";
import { useNavigate } from "react-router-dom";
import Empty from "./empty";
import { logger } from "../../../Utils/Logger";
@@ -283,7 +283,7 @@ const InfrastructureDetails = () => {
{
type: "memory",
value: decimalToPercentage(memoryUsagePercent),
heading: "Memory Usage",
heading: "Memory usage",
metricOne: "Used",
valueOne: formatBytes(memoryUsedBytes),
metricTwo: "Total",
@@ -292,7 +292,7 @@ const InfrastructureDetails = () => {
{
type: "cpu",
value: decimalToPercentage(cpuUsagePercent),
heading: "CPU Usage",
heading: "CPU usage",
metricOne: "Cores",
valueOne: cpuPhysicalCores ?? 0,
metricTwo: "Frequency",
@@ -364,7 +364,7 @@ const InfrastructureDetails = () => {
heading: "Memory usage",
strokeColor: theme.palette.primary.main,
gradientStartColor: theme.palette.primary.main,
yLabel: "Memory Usage",
yLabel: "Memory usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
@@ -372,7 +372,7 @@ const InfrastructureDetails = () => {
<InfrastructureTooltip
dotColor={theme.palette.primary.main}
yKey={"memory.usage_percent"}
yLabel={"Memory Usage"}
yLabel={"Memory usage"}
/>
),
},
@@ -383,7 +383,7 @@ const InfrastructureDetails = () => {
heading: "CPU usage",
strokeColor: theme.palette.success.main,
gradientStartColor: theme.palette.success.main,
yLabel: "CPU Usage",
yLabel: "CPU usage",
yDomain: [0, 1],
yTick: <PercentTick />,
xTick: <TzTick />,
@@ -391,7 +391,7 @@ const InfrastructureDetails = () => {
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"cpu.usage_percent"}
yLabel={"CPU Usage"}
yLabel={"CPU usage"}
/>
),
},
@@ -109,88 +109,6 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
},
}}
>
{/*
Open site action. Not necessary for infrastructure?
{actions.url !== null ? (
<MenuItem
onClick={(e) => {
closeMenu(e);
e.stopPropagation();
window.open(actions.url, "_blank", "noreferrer");
}}
>
Open site
</MenuItem>
) : (
""
)}
*/}
<MenuItem onClick={() => openDetails(monitor.id)}>Details</MenuItem>
{/*
Incidents. Necessary?
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/incidents/${actions.id}`);
}}
>
Incidents
</MenuItem> */}
{/*
Configure. Necessary?
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
}}
>
Configure
</MenuItem>
)} */}
{/*
Clone. Necessary?
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/create/${actions.id}`);
}}
>
Clone
</MenuItem>
)} */}
{/*
Pause. Necessary?
const handlePause = async () => {
try {
const action = await dispatch(
pauseUptimeMonitor({ authToken, monitorId: monitor._id })
);
if (pauseUptimeMonitor.fulfilled.match(action)) {
updateCallback();
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else {
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
}
} catch (error) {
logger.error("Error pausing monitor:", monitor._id, error);
createToast({ body: "Failed to pause monitor." });
}
};
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)} */}
{isAdmin && <MenuItem onClick={openRemove}>Remove</MenuItem>}
</Menu>
<Dialog
+2 -2
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { /* useDispatch, */ useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import useUtils from "../Monitors/utils";
import useUtils from "../Uptime/utils.jsx";
import { jwtDecode } from "jwt-decode";
import SkeletonLayout from "./skeleton";
import Fallback from "../../Components/Fallback";
@@ -28,7 +28,7 @@ import { Pagination } from "./components/TablePagination";
// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
import { networkService } from "../../Utils/NetworkService.js";
import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx";
import Host from "../Monitors/Home/host.jsx";
import Host from "../Uptime/Home/host.jsx";
import { useIsAdmin } from "../../Hooks/useIsAdmin.js";
import { InfrastructureMenu } from "./components/Menu";
@@ -13,7 +13,7 @@ import {
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../../Monitors/styled";
import { ConfigBox } from "../../Uptime/styled";
import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
import Checkbox from "../../../Components/Inputs/Checkbox";
@@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import LoadingButton from "@mui/lab/LoadingButton";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton";
import useUtils from "../../Monitors/utils";
import useUtils from "../../Uptime/utils";
import "./index.css";
import Dialog from "../../../Components/Dialog";
@@ -1,42 +1,51 @@
import { useState } from "react";
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import { useSelector, useDispatch } from "react-redux";
import { monitorValidation } from "../../../Validation/validation";
// React, Redux, Router
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
// Utility and Network
import { monitorValidation } from "../../../Validation/validation";
import {
createPageSpeed,
checkEndpointResolution,
} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { ConfigBox } from "../../Monitors/styled";
import Radio from "../../../Components/Inputs/Radio";
import { parseDomainName } from "../../../Utils/monitorUtils";
// MUI
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import Select from "../../../Components/Inputs/Select";
import { ConfigBox } from "../../Monitors/styled";
import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
import Checkbox from "../../../Components/Inputs/Checkbox";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { parseDomainName } from "../../../Utils/monitorUtils";
import "./index.css";
import Select from "../../../Components/Inputs/Select";
const MS_PER_MINUTE = 60000;
const CRUMBS = [
{ name: "pagespeed", path: "/pagespeed" },
{ name: "create", path: `/pagespeed/create` },
];
const SELECT_VALUES = [
{ _id: 3, name: "3 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
{ _id: 20, name: "20 minutes" },
{ _id: 60, name: "1 hour" },
{ _id: 1440, name: "1 day" },
{ _id: 10080, name: "1 week" },
];
const CreatePageSpeed = () => {
const MS_PER_MINUTE = 60000;
const { user, authToken } = useSelector((state) => state.auth);
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
const idMap = {
"monitor-url": "url",
"monitor-name": "name",
"monitor-checks-http": "type",
"monitor-checks-ping": "type",
"notify-email-default": "notification-email",
};
// State
const [monitor, setMonitor] = useState({
url: "",
name: "",
@@ -44,71 +53,20 @@ const CreatePageSpeed = () => {
notifications: [],
interval: 3,
});
const [https, setHttps] = useState(true);
const [errors, setErrors] = useState({});
const { user, authToken } = useSelector((state) => state.auth);
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const handleChange = (event, name) => {
const { value, id } = event.target;
if (!name) name = idMap[id];
if (name.includes("notification-")) {
name = name.replace("notification-", "");
let hasNotif = monitor.notifications.some(
(notification) => notification.type === name
);
setMonitor((prev) => {
const notifs = [...prev.notifications];
if (hasNotif) {
return {
...prev,
notifications: notifs.filter((notif) => notif.type !== name),
};
} else {
return {
...prev,
notifications: [
...notifs,
name === "email"
? { type: name, address: value }
: // TODO - phone number
{ type: name, phone: value },
],
};
}
});
} else {
setMonitor((prev) => ({
...prev,
[name]: value,
}));
const { error } = monitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => {
const updatedErrors = { ...prev };
if (error) updatedErrors[name] = error.details[0].message;
else delete updatedErrors[name];
return updatedErrors;
});
}
};
const onUrlBlur = (event) => {
const { value } = event.target;
if (monitor.name === "") {
setMonitor((prev) => ({
...prev,
name: parseDomainName(value),
}));
}
};
// Setup
const dispatch = useDispatch();
const navigate = useNavigate();
const theme = useTheme();
// Handlers
const handleCreateMonitor = async (event) => {
event.preventDefault();
//obj to submit
let form = {
url: `http${https ? "s" : ""}://` + monitor.url,
name: monitor.name === "" ? monitor.url : monitor.name,
@@ -126,46 +84,97 @@ const CreatePageSpeed = () => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
createToast({ body: "Error validation data." });
} else {
const checkEndpointAction = await dispatch(
checkEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
createToast({ body: "Please check the form for errors." });
return;
}
form = {
...form,
description: form.name,
teamId: user.teamId,
userId: user._id,
notifications: monitor.notifications,
};
const action = await dispatch(createPageSpeed({ authToken, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
navigate("/pagespeed");
} else {
createToast({ body: "Failed to create monitor." });
}
const checkEndpointAction = await dispatch(
checkEndpointResolution({ authToken, monitorURL: form.url })
);
if (checkEndpointAction.meta.requestStatus === "rejected") {
createToast({
body: "The endpoint you entered doesn't resolve. Check the URL again.",
});
setErrors({ url: "The entered URL is not reachable." });
return;
}
form = {
...form,
description: form.name,
teamId: user.teamId,
userId: user._id,
notifications: monitor.notifications,
};
const action = await dispatch(createPageSpeed({ authToken, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
navigate("/pagespeed");
} else {
createToast({ body: "Failed to create monitor." });
}
};
const handleChange = (event) => {
const { value, name } = event.target;
setMonitor({
...monitor,
[name]: value,
});
const { error } = monitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleNotifications = (event, type) => {
const { value } = event.target;
let notifications = [...monitor.notifications];
const notificationExists = notifications.some((notification) => {
if (notification.type === type && notification.address === value) {
return true;
}
return false;
});
if (notificationExists) {
notifications = notifications.filter((notification) => {
if (notification.type === type && notification.address === value) {
return false;
}
return true;
});
} else {
notifications.push({ type, address: value });
}
setMonitor((prev) => ({
...prev,
notifications,
}));
};
const handleBlur = (event) => {
const { name } = event.target;
if (name === "url") {
const { value } = event.target;
if (monitor.name !== "") {
return;
}
setMonitor((prev) => ({
...prev,
name: parseDomainName(value),
}));
}
};
//select values
const frequencies = [
{ _id: 3, name: "3 minutes" },
{ _id: 5, name: "5 minutes" },
{ _id: 10, name: "10 minutes" },
{ _id: 20, name: "20 minutes" },
{ _id: 60, name: "1 hour" },
{ _id: 1440, name: "1 day" },
{ _id: 10080, name: "1 week" },
];
return (
<Box
className="create-monitor"
@@ -175,12 +184,7 @@ const CreatePageSpeed = () => {
},
}}
>
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "create", path: `/pagespeed/create` },
]}
/>
<Breadcrumbs list={CRUMBS} />
<Stack
component="form"
className="create-monitor-form"
@@ -206,7 +210,7 @@ const CreatePageSpeed = () => {
fontWeight="inherit"
color={theme.palette.text.secondary}
>
pagespeed monitor
PageSpeed monitor
</Typography>
</Typography>
<ConfigBox>
@@ -219,19 +223,21 @@ const CreatePageSpeed = () => {
<Stack gap={theme.spacing(15)}>
<TextInput
type={"url"}
name="url"
id="monitor-url"
label="URL to monitor"
startAdornment={<HttpAdornment https={https} />}
placeholder="google.com"
value={monitor.url}
onChange={handleChange}
onBlur={onUrlBlur}
onBlur={handleBlur}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
<TextInput
type="text"
id="monitor-name"
name="name"
label="Display name"
isOptional={true}
placeholder="Google"
@@ -253,12 +259,11 @@ const CreatePageSpeed = () => {
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
title="PageSpeed"
desc="Use the Lighthouse PageSpeed API to monitor your website"
size="small"
value="http"
checked={monitor.type === "pagespeed"}
onChange={(event) => handleChange(event)}
/>
<ButtonGroup sx={{ ml: "32px" }}>
<Button
@@ -269,8 +274,8 @@ const CreatePageSpeed = () => {
HTTPS
</Button>
<Button
variant="group"
filled={(!https).toString()}
variant="group" // Why does this work?
filled={(!https).toString()} // There's nothing in the docs about this either
onClick={() => setHttps(false)}
>
HTTP
@@ -308,26 +313,8 @@ const CreatePageSpeed = () => {
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(event) => handleChange(event)}
onChange={(event) => handleNotifications(event, "email")}
/>
{monitor.notifications.some(
(notification) => notification.type === "emails"
) ? (
<Box mx={theme.spacing(16)}>
<TextInput
id="notify-email-list"
type="text"
placeholder="name@gmail.com"
value=""
onChange={() => logger.warn("disabled")}
/>
<Typography mt={theme.spacing(4)}>
You can separate multiple emails with a comma
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
<ConfigBox>
@@ -337,10 +324,11 @@ const CreatePageSpeed = () => {
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
name="interval"
label="Check frequency"
value={monitor.interval || 3}
onChange={(event) => handleChange(event, "interval")}
items={frequencies}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
@@ -352,7 +340,7 @@ const CreatePageSpeed = () => {
variant="contained"
color="primary"
onClick={handleCreateMonitor}
disabled={Object.keys(errors).length !== 0 && true}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
Create monitor
@@ -362,5 +350,4 @@ const CreatePageSpeed = () => {
</Box>
);
};
export default CreatePageSpeed;
+1 -1
View File
@@ -19,7 +19,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
import Checkbox from "../../../Components/Inputs/Checkbox";
import PieChart from "./Charts/PieChart";
import useUtils from "../../Monitors/utils";
import useUtils from "../../Uptime/utils";
import "./index.css";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
+1 -1
View File
@@ -8,7 +8,7 @@ import { IconBox } from "./Details/styled";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils";
import useUtils from "../Monitors/utils";
import useUtils from "../Uptime/utils";
import { useState } from "react";
/**
@@ -166,7 +166,7 @@ const Configure = () => {
event.preventDefault();
const action = await dispatch(deleteUptimeMonitor({ authToken, monitor }));
if (action.meta.requestStatus === "fulfilled") {
navigate("/monitors");
navigate("/uptime");
} else {
createToast({ body: "Failed to delete monitor." });
}
@@ -207,9 +207,9 @@ const Configure = () => {
<>
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "details", path: `/monitors/${monitorId}` },
{ name: "configure", path: `/monitors/configure/${monitorId}` },
{ name: "uptime", path: "/uptime" },
{ name: "details", path: `/uptime/${monitorId}` },
{ name: "configure", path: `/uptime/configure/${monitorId}` },
]}
/>
<Stack
@@ -60,8 +60,8 @@ const CreateMonitor = () => {
const theme = useTheme();
const { monitorId } = useParams();
const crumbs = [
{ name: "monitors", path: "/monitors" },
{ name: "create", path: `/monitors/create` },
{ name: "uptime", path: "/uptime" },
{ name: "create", path: `/uptime/create` },
];
// State
@@ -125,7 +125,7 @@ const CreateMonitor = () => {
const action = await dispatch(createUptimeMonitor({ authToken, monitor: form }));
if (action.meta.requestStatus === "fulfilled") {
createToast({ body: "Monitor created successfully!" });
navigate("/monitors");
navigate("/uptime");
} else {
createToast({ body: "Failed to create monitor." });
}
@@ -102,7 +102,7 @@ const DetailsPage = () => {
}
};
fetchCertificate();
}, [authToken, monitorId, monitor]);
}, [authToken, monitorId, monitor, uiTimezone, dateFormat]);
const splitDuration = (duration) => {
const { time, format } = formatDurationSplit(duration);
@@ -119,18 +119,17 @@ const DetailsPage = () => {
const [hoveredUptimeData, setHoveredUptimeData] = useState(null);
const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null);
const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "details", path: `/uptime/${monitorId}` },
];
return (
<Box className="monitor-details">
{loading ? (
<SkeletonLayout />
) : (
<>
<Breadcrumbs
list={[
{ name: "monitors", path: "/monitors" },
{ name: "details", path: `/monitors/${monitorId}` },
]}
/>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
gap={theme.spacing(10)}
mt={theme.spacing(10)}
@@ -264,7 +263,7 @@ const DetailsPage = () => {
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/monitors/configure/${monitorId}`)}
onClick={() => navigate(`/uptime/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import Search from "../../../../Components/Inputs/Search";
import MemoizedMonitorTable from "../MonitorTable";
import MemoizedMonitorTable from "../UptimeTable";
import { useState } from "react";
import useDebounce from "../../../../Utils/debounce";
import PropTypes from "prop-types";
@@ -26,7 +26,7 @@ const CurrentMonitoring = ({ totalMonitors, monitors, isAdmin, handlePause }) =>
alignItems="center"
mb={theme.spacing(8)}
>
<Heading component="h2">Actively monitoring</Heading>
<Heading component="h2">Uptime monitors</Heading>
<Box
className="current-monitors-counter"
@@ -268,7 +268,7 @@ const MonitorTable = ({ isAdmin, filter, setIsSearching, isSearching, handlePaus
},
}}
onClick={() => {
navigate(`/monitors/${monitor._id}`);
navigate(`/uptime/${monitor._id}`);
}}
>
<TableCell>
@@ -131,7 +131,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/${actions.id}`);
navigate(`/uptime/${actions.id}`);
}}
>
Details
@@ -150,7 +150,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/configure/${actions.id}`);
navigate(`/uptime/configure/${actions.id}`);
}}
>
Configure
@@ -160,7 +160,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateRowCallback, pauseCallback }) =>
<MenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/monitors/create/${actions.id}`);
navigate(`/uptime/create/${actions.id}`);
}}
>
Clone
@@ -14,11 +14,13 @@ import Greeting from "../../../Utils/greeting";
import { CurrentMonitoring } from "./CurrentMonitoring";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
const Monitors = () => {
const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }];
const UptimeMonitors = () => {
const theme = useTheme();
const navigate = useNavigate();
const isAdmin = useIsAdmin();
const monitorState = useSelector((state) => state.uptimeMonitors);
const uptimeMonitorsState = useSelector((state) => state.uptimeMonitors);
const authState = useSelector((state) => state.auth);
const dispatch = useDispatch({});
const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false);
@@ -33,9 +35,9 @@ const Monitors = () => {
//TODO bring fetching to this component, like on pageSpeed
const loading = monitorState?.isLoading;
const loading = uptimeMonitorsState?.isLoading;
const totalMonitors = monitorState?.monitorsSummary?.monitorCounts?.total;
const totalMonitors = uptimeMonitorsState?.monitorsSummary?.monitorCounts?.total;
const hasMonitors = totalMonitors > 0;
const noMonitors = !hasMonitors;
@@ -47,28 +49,28 @@ const Monitors = () => {
gap={theme.spacing(8)}
>
<Box>
<Breadcrumbs list={[{ name: `monitors`, path: "/monitors" }]} />
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="space-between"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
gap={theme.spacing(6)}
>
<Greeting type="uptime" />
{canAddMonitor && (
<Button
variant="contained"
color="primary"
onClick={() => {
navigate("/monitors/create");
navigate("/uptime/create");
}}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create monitor
Create new
</Button>
)}
</Stack>
<Greeting type="uptime" />
</Box>
{noMonitors && <Fallback isAdmin={isAdmin} />}
{loading ? (
@@ -84,20 +86,20 @@ const Monitors = () => {
>
<StatusBox
title="up"
value={monitorState?.monitorsSummary?.monitorCounts?.up ?? 0}
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.up ?? 0}
/>
<StatusBox
title="down"
value={monitorState?.monitorsSummary?.monitorCounts?.down ?? 0}
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.down ?? 0}
/>
<StatusBox
title="paused"
value={monitorState?.monitorsSummary?.monitorCounts?.paused ?? 0}
value={uptimeMonitorsState?.monitorsSummary?.monitorCounts?.paused ?? 0}
/>
</Stack>
<CurrentMonitoring
isAdmin={isAdmin}
monitors={monitorState.monitorsSummary.monitors}
monitors={uptimeMonitorsState.monitorsSummary.monitors}
totalMonitors={totalMonitors}
handlePause={handlePause}
/>
@@ -109,7 +111,7 @@ const Monitors = () => {
);
};
Monitors.propTypes = {
UptimeMonitors.propTypes = {
isAdmin: PropTypes.bool,
};
export default Monitors;
export default UptimeMonitors;
File renamed without changes.
File renamed without changes.
+36 -8
View File
@@ -91,10 +91,24 @@ const credentials = joi.object({
});
const monitorValidation = joi.object({
url: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
}),
url: joi
.string()
.trim()
.custom((value, helpers) => {
const urlRegex =
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
}
return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -176,10 +190,24 @@ const advancedSettingsValidation = joi.object({
});
const infrastructureMonitorValidation = joi.object({
url: joi.string().uri({ allowRelative: true }).trim().messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
}),
url: joi
.string()
.trim()
.custom((value, helpers) => {
const urlRegex =
/^(https?:\/\/)?(([0-9]{1,3}\.){3}[0-9]{1,3}|[\da-z\.-]+)(\.[a-z\.]{2,6})?(:(\d+))?([\/\w \.-]*)*\/?$/i;
if (!urlRegex.test(value)) {
return helpers.error("string.invalidUrl");
}
return value;
})
.messages({
"string.empty": "This field is required.",
"string.uri": "The URL you provided is not valid.",
"string.invalidUrl": "Please enter a valid URL with optional port",
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
+1 -1
View File
@@ -2,7 +2,7 @@ server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
+1 -1
View File
@@ -2,7 +2,7 @@ services:
client:
image: uptime_client:latest
environment:
UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
ports:
- "80:80"
- "443:443"
+1 -1
View File
@@ -2,7 +2,7 @@ services:
client:
image: uptime_client:latest
environment:
UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1"
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
ports:
- "80:80"
- "443:443"
+4 -4
View File
@@ -2,7 +2,7 @@ server {
listen 80;
listen [::]:80;
server_name uptime-demo.bluewavelabs.ca;
server_name checkmate-demo.bluewavelabs.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
@@ -38,10 +38,10 @@ server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;
server_name uptime-demo.bluewavelabs.ca;
server_name checkmate-demo.bluewavelabs.ca;
ssl_certificate /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/uptime-demo.bluewavelabs.ca/privkey.pem;
ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem;
location / {
root /usr/share/nginx/html;
+1 -1
View File
@@ -72,7 +72,7 @@ We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-
## 🤝 Contributing
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 600+ stars and attracted 30+ contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 1300+ stars and attracted 30+ contributors from around the globe. So, don’t hold back — jump in, contribute and learn with us!
Here's how you can contribute:
+1 -1
View File
@@ -44,7 +44,7 @@
}
},
{
"url": "https://uptime-demo.bluewavelabs.ca/{API_PATH}",
"url": "https://checkmate-demo.bluewavelabs.ca/{API_PATH}",
"description": "Bluewave Demo Server",
"variables": {
"PORT": {
+1 -1
View File
@@ -26,7 +26,7 @@ It regularly checks whether a server/website is accessible and performs optimall
## Demo
We have a [demo](https://uptime-demo.bluewavelabs.ca/) where you can test how the Uptime Manager works. The username is [uptimedemo@demo.com](mailto:uptimedemo@demo.com) and the password is Demouser1!
We have a [demo](https://checkmate-demo.bluewavelabs.ca/) where you can test how the Uptime Manager works. The username is [uptimedemo@demo.com](mailto:uptimedemo@demo.com) and the password is Demouser1!
## Questions & ideas
@@ -20,7 +20,7 @@ When making changes to the Front end please always keep future developers in min
### Back end
The back end of this project is not especially complex and is built around Express. The back end is a RESTful API and the [documentation can be found here](https://uptime-demo.bluewavelabs.ca/api-docs).
The back end of this project is not especially complex and is built around Express. The back end is a RESTful API and the [documentation can be found here](https://checkmate-demo.bluewavelabs.ca/api-docs).
The application consists of several main conceptual models:
+1 -1
View File
@@ -201,7 +201,7 @@ Our API is documented in accordance with the [OpenAPI spec](https://www.openapis
You can see the documentation on your local development server at http://localhost:{port}/api-docs
You can also view the documentation on our demo server at [https://uptime-demo.bluewavelabs.ca/api-docs](https://uptime-demo.bluewavelabs.ca/api-docs)
You can also view the documentation on our demo server at [https://checkmate-demo.bluewavelabs.ca/api-docs](https://checkmate-demo.bluewavelabs.ca/api-docs)
### Error handling