mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-02 22:49:19 -05:00
Merge branch 'develop' into refactor/notification-service
This commit is contained in:
@@ -1,7 +1,17 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
ListItem,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Stack,
|
||||
Typography,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import SearchIcon from "../../../assets/icons/search.svg?react";
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
/**
|
||||
* Search component using Material UI's Autocomplete.
|
||||
@@ -60,24 +70,72 @@ const Search = ({
|
||||
onBlur,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [selectAll, setSelectAll] = React.useState(false);
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const enhancedOptions = React.useMemo(() => {
|
||||
return multiple && isAdorned
|
||||
? [
|
||||
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
|
||||
...options,
|
||||
]
|
||||
: options;
|
||||
}, [multiple, isAdorned, options, filteredBy]);
|
||||
const isOptionSelected = (option) => {
|
||||
if (!multiple && !isAdorned) return false;
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => item._id === option._id);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const handleSelectAll = (isSelectAll) => {
|
||||
const newValue = isSelectAll ? [...options] : [];
|
||||
handleChange(newValue);
|
||||
setSelectAll(isSelectAll);
|
||||
};
|
||||
useEffect(() => {
|
||||
const allSelected =
|
||||
Array.isArray(value) && Array.isArray(options) && value.length === options.length;
|
||||
if (selectAll !== allSelected) setSelectAll(allSelected);
|
||||
}, [value, options]);
|
||||
return (
|
||||
<Autocomplete
|
||||
onBlur={onBlur}
|
||||
multiple={multiple}
|
||||
id={id}
|
||||
value={value}
|
||||
open={open}
|
||||
onOpen={() => setOpen(true)}
|
||||
onClose={(event, reason) => {
|
||||
if (reason === "blur" || reason === "escape") {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
inputValue={inputValue}
|
||||
onInputChange={(_, newValue) => {
|
||||
handleInputChange(newValue);
|
||||
}}
|
||||
onChange={(_, newValue) => {
|
||||
handleChange(newValue);
|
||||
if (multiple && isAdorned) {
|
||||
const hasSelectAllSelected =
|
||||
Array.isArray(newValue) && newValue.some((item) => item.isSelectAll);
|
||||
if (hasSelectAllSelected) {
|
||||
handleSelectAll(!selectAll);
|
||||
} else {
|
||||
handleChange(newValue);
|
||||
setSelectAll(Array.isArray(newValue) && newValue.length === options.length);
|
||||
}
|
||||
} else {
|
||||
handleChange(newValue);
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
freeSolo
|
||||
disabled={disabled}
|
||||
disableClearable
|
||||
options={options}
|
||||
options={enhancedOptions}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
@@ -120,6 +178,9 @@ const Search = ({
|
||||
</Stack>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
if (inputValue.trim() === "" && multiple && isAdorned) {
|
||||
return enhancedOptions;
|
||||
}
|
||||
const filtered = options.filter((option) =>
|
||||
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
|
||||
);
|
||||
@@ -136,6 +197,7 @@ const Search = ({
|
||||
const { key, ...optionProps } = props;
|
||||
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
|
||||
const port = option["port"];
|
||||
const selected = isOptionSelected(option);
|
||||
return (
|
||||
<ListItem
|
||||
key={key}
|
||||
@@ -146,9 +208,29 @@ const Search = ({
|
||||
pointerEvents: "none",
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
}
|
||||
: {}
|
||||
: option.isSelectAll
|
||||
? {
|
||||
fontWeight: "bold",
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{multiple && isAdorned && !option.noOptions && (
|
||||
<Checkbox
|
||||
checked={option.isSelectAll ? selectAll : selected}
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
"&.Mui-checked": {
|
||||
color: theme.palette.secondary.main,
|
||||
},
|
||||
padding: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{option[filteredBy] +
|
||||
(hasSecondaryLabel
|
||||
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
|
||||
|
||||
@@ -68,6 +68,9 @@ const MonitorDetailsControlHeader = ({
|
||||
onClick={() => {
|
||||
testAllNotifications({ monitorId: monitor?._id });
|
||||
}}
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{t("sendTestNotifications")}
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatDurationRounded } from "../../Utils/timeUtils";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
|
||||
import { formatMonitorUrl } from "../../Utils/utils";
|
||||
/**
|
||||
* Status component displays the status information of a monitor.
|
||||
* It includes the monitor's name, URL, and check interval.
|
||||
@@ -33,9 +34,7 @@ const Status = ({ monitor }) => {
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography variant="monitorUrl">
|
||||
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography variant="monitorUrl">{formatMonitorUrl(monitor?.url)}</Typography>
|
||||
<Dot />
|
||||
<Typography>
|
||||
Checking every {formatDurationRounded(monitor?.interval)}.
|
||||
|
||||
@@ -2,8 +2,57 @@ import { useState, useEffect } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
|
||||
const useFetchChecks = ({
|
||||
teamId,
|
||||
const useFetchChecksTeam = ({
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled = true,
|
||||
}) => {
|
||||
const [checks, setChecks] = useState(undefined);
|
||||
const [checksCount, setChecksCount] = useState(undefined);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChecks = async () => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getChecksByTeam(config);
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChecks();
|
||||
}, [status, sortOrder, limit, dateRange, filter, page, rowsPerPage, enabled]);
|
||||
|
||||
return [checks, checksCount, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchChecksByMonitor = ({
|
||||
monitorId,
|
||||
type,
|
||||
status,
|
||||
@@ -13,6 +62,7 @@ const useFetchChecks = ({
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled = true,
|
||||
}) => {
|
||||
const [checks, setChecks] = useState(undefined);
|
||||
const [checksCount, setChecksCount] = useState(undefined);
|
||||
@@ -21,40 +71,25 @@ const useFetchChecks = ({
|
||||
|
||||
useEffect(() => {
|
||||
const fetchChecks = async () => {
|
||||
if (!type && !teamId) {
|
||||
if (!enabled || !type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const method = monitorId
|
||||
? networkService.getChecksByMonitor
|
||||
: networkService.getChecksByTeam;
|
||||
|
||||
const config = monitorId
|
||||
? {
|
||||
monitorId,
|
||||
type,
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
}
|
||||
: {
|
||||
status,
|
||||
teamId,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
const config = {
|
||||
monitorId,
|
||||
type,
|
||||
status,
|
||||
sortOrder,
|
||||
limit,
|
||||
dateRange,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await method(config);
|
||||
const res = await networkService.getChecksByMonitor(config);
|
||||
setChecks(res.data.data.checks);
|
||||
setChecksCount(res.data.data.checksCount);
|
||||
} catch (error) {
|
||||
@@ -68,7 +103,6 @@ const useFetchChecks = ({
|
||||
fetchChecks();
|
||||
}, [
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
status,
|
||||
sortOrder,
|
||||
@@ -77,9 +111,10 @@ const useFetchChecks = ({
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
enabled,
|
||||
]);
|
||||
|
||||
return [checks, checksCount, isLoading, networkError];
|
||||
};
|
||||
|
||||
export { useFetchChecks };
|
||||
export { useFetchChecksByMonitor, useFetchChecksTeam };
|
||||
|
||||
@@ -10,10 +10,11 @@ import NetworkError from "../../../../Components/GenericFallback/NetworkError";
|
||||
//Utils
|
||||
import { formatDateWithTz } from "../../../../Utils/timeUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchChecks } from "../../../../Hooks/checkHooks";
|
||||
import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
|
||||
import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
|
||||
|
||||
const IncidentTable = ({
|
||||
shouldRender,
|
||||
@@ -26,26 +27,41 @@ const IncidentTable = ({
|
||||
const uiTimezone = useSelector((state) => state.ui.timezone);
|
||||
|
||||
//Local state
|
||||
const [teamId, setTeamId] = useState(undefined);
|
||||
const [monitorId, setMonitorId] = useState(undefined);
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const selectedMonitorDetails = monitors?.[selectedMonitor];
|
||||
const selectedMonitorType = selectedMonitorDetails?.type;
|
||||
|
||||
const [checks, checksCount, isLoading, networkError] = useFetchChecks({
|
||||
status: false,
|
||||
monitorId,
|
||||
teamId,
|
||||
type: selectedMonitorType,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
});
|
||||
const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] =
|
||||
useFetchChecksByMonitor({
|
||||
monitorId: selectedMonitor === "0" ? undefined : selectedMonitor,
|
||||
type: selectedMonitorType,
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor !== "0",
|
||||
});
|
||||
|
||||
const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] =
|
||||
useFetchChecksTeam({
|
||||
status: false,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: filter,
|
||||
page: page,
|
||||
rowsPerPage: rowsPerPage,
|
||||
enabled: selectedMonitor === "0",
|
||||
});
|
||||
|
||||
const checks = selectedMonitor === "0" ? checksTeam : checksMonitor;
|
||||
const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor;
|
||||
const isLoading = isLoadingTeam || isLoadingMonitor;
|
||||
const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -58,16 +74,6 @@ const IncidentTable = ({
|
||||
setRowsPerPage(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMonitor === "0") {
|
||||
setTeamId("placeholder"); // TODO this isn't needed any longer, fix hook
|
||||
setMonitorId(undefined);
|
||||
} else {
|
||||
setMonitorId(selectedMonitor);
|
||||
setTeamId(undefined);
|
||||
}
|
||||
}, [selectedMonitor]);
|
||||
|
||||
const headers = [
|
||||
{
|
||||
id: "monitorName",
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks";
|
||||
import useCertificateFetch from "./Hooks/useCertificateFetch";
|
||||
import { useFetchChecks } from "../../../Hooks/checkHooks";
|
||||
import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Constants
|
||||
@@ -66,16 +66,17 @@ const UptimeDetails = () => {
|
||||
|
||||
const monitorType = monitor?.type;
|
||||
|
||||
const [checks, checksCount, checksAreLoading, checksNetworkError] = useFetchChecks({
|
||||
monitorId,
|
||||
type: monitorType,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: null,
|
||||
page,
|
||||
rowsPerPage,
|
||||
});
|
||||
const [checks, checksCount, checksAreLoading, checksNetworkError] =
|
||||
useFetchChecksByMonitor({
|
||||
monitorId,
|
||||
type: monitorType,
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange,
|
||||
filter: null,
|
||||
page,
|
||||
rowsPerPage,
|
||||
});
|
||||
|
||||
// Handlers
|
||||
const triggerUpdate = () => {
|
||||
|
||||
@@ -5,3 +5,11 @@ export const safelyParseFloat = (value) => {
|
||||
}
|
||||
return parsedValue;
|
||||
};
|
||||
|
||||
export const formatMonitorUrl = (url, maxLength = 55) => {
|
||||
if (!url) return "";
|
||||
const strippedUrl = url.replace(/^https?:\/\//, "");
|
||||
return strippedUrl.length > maxLength
|
||||
? `${strippedUrl.slice(0, maxLength)}…`
|
||||
: strippedUrl;
|
||||
};
|
||||
|
||||
@@ -744,5 +744,6 @@
|
||||
"settingsEmailRejectUnauthorized": "Reject Unauthorized",
|
||||
"settingsEmailSecure": "Secure - Use SSL",
|
||||
"settingsEmailPool": "Pool - Enable connection pooling",
|
||||
"sendTestNotifications": "Send test notifications"
|
||||
}
|
||||
"sendTestNotifications": "Send test notifications",
|
||||
"selectAll": "Select all"
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
recoveryTokenValidation,
|
||||
newPasswordValidation,
|
||||
} from "../validation/joi.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import crypto from "crypto";
|
||||
@@ -15,12 +14,13 @@ import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
const SERVICE_NAME = "authController";
|
||||
|
||||
class AuthController {
|
||||
constructor(db, settingsService, emailService, jobQueue, stringService) {
|
||||
constructor({ db, settingsService, emailService, jobQueue, stringService, logger }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.emailService = emailService;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,7 +117,7 @@ class SettingsController {
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
next(handleError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
+8
-7
@@ -239,13 +239,14 @@ const startApp = async () => {
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
//Create controllers
|
||||
const authController = new AuthController(
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
ServiceRegistry.get(EmailService.SERVICE_NAME),
|
||||
ServiceRegistry.get(JobQueue.SERVICE_NAME),
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME)
|
||||
);
|
||||
const authController = new AuthController({
|
||||
db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
emailService: ServiceRegistry.get(EmailService.SERVICE_NAME),
|
||||
jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME),
|
||||
stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
|
||||
logger: logger,
|
||||
});
|
||||
|
||||
const monitorController = new MonitorController(
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
|
||||
Reference in New Issue
Block a user