implement adjustments and merge with develop

This commit is contained in:
karenvicent committed 2025-07-02 10:02:01 -04:00
1 parent 1cb730764a
commit c8bf571f7a
17 files changed
+373 -26

No files matched your search

+13 -6
View File
@@ -87,11 +87,17 @@ const Fallback = ({
sx={{
display: "flex",
borderStyle: "dashed",
height: { sm: "50vh", md: "60vh" },
width: { md: "45%", lg: "40%" },
minWidth: theme.spacing(200),
maxHeight: theme.spacing(350),
maxWidth: theme.spacing(450),
height: {
xs: theme.spacing(200),
sm: theme.spacing(300),
md: theme.spacing(300),
},
width: {
xs: theme.spacing(100),
sm: theme.spacing(210),
md: theme.spacing(250),
},
padding: theme.spacing(10),
}}
>
<Stack
@@ -116,10 +122,11 @@ const Fallback = ({
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
maxWidth={theme.spacing(180)}
zIndex={1}
>
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
@@ -214,7 +214,7 @@ const PasswordPanel = () => {
<TextInput
type="password"
id="edit-confirm-password"
placeholder={t("confirmPassword", "Confirm password")}
placeholder={t("confirmPassword")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
+27 -1
View File
@@ -122,4 +122,30 @@ const useFetchChecksByMonitor = ({
return [checks, checksCount, isLoading, networkError];
};
export { useFetchChecksByMonitor, useFetchChecksTeam };
const useFetchChecksSummaryByTeamId = () => {
const [summary, setSummary] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
useEffect(() => {
const fetchSummary = async () => {
try {
setIsLoading(true);
const res = await networkService.getChecksAndSummaryByTeamId();
setSummary(res.data.data);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchSummary();
}, []);
return [summary, isLoading, networkError];
};
export { useFetchChecksByMonitor, useFetchChecksTeam, useFetchChecksSummaryByTeamId };
+2 -2
View File
@@ -385,9 +385,9 @@ const useAddDemoMonitors = () => {
try {
setIsLoading(true);
await networkService.addDemoMonitors();
createToast({ body: t("settingsDemoMonitorsAdded") });
createToast({ body: t("monitorHooks.successAddDemoMonitors") });
} catch (error) {
createToast({ body: t("settingsFailedToAddDemoMonitors") });
createToast({ body: t("monitorHooks.failureAddDemoMonitors") });
} finally {
setIsLoading(false);
}
@@ -87,13 +87,13 @@ const OptionsHeader = ({
>
{t("incidentsOptionsHeaderFilterCannotResolve")}
</Button>
{/* <Button
<Button
variant="group"
filled={(filter === "resolved").toString()}
onClick={() => setFilter("resolved")}
>
{t("incidentsOptionsHeaderFilterResolved")}
</Button> */}
</Button>
</ButtonGroup>
</Stack>
<Stack {...stackStyles}>
@@ -0,0 +1,116 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography } from "@mui/material";
import Background from "../../../../assets/Images/background-grid.svg?react";
import MonitorHeartOutlinedIcon from "@mui/icons-material/MonitorHeartOutlined";
import TaskAltOutlinedIcon from "@mui/icons-material/TaskAltOutlined";
import CancelOutlinedIcon from "@mui/icons-material/CancelOutlined";
import WarningAmberRoundedIcon from "@mui/icons-material/WarningAmberRounded";
const StatusBox = ({ title, value, status }) => {
const theme = useTheme();
let sharedStyles = {
position: "absolute",
right: 8,
opacity: 0.5,
"& svg path": { stroke: theme.palette.primary.contrastTextTertiary },
};
let color;
let icon;
if (status === "up") {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<TaskAltOutlinedIcon fontSize="small" />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<CancelOutlinedIcon fontSize="small" />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<WarningAmberRoundedIcon fontSize="small" />
</Box>
);
} else {
color = theme.palette.accent.main;
icon = (
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<MonitorHeartOutlinedIcon fontSize="small" />
</Box>
);
}
return (
<Box
position="relative"
flex={1}
border={1}
backgroundColor={theme.palette.primary.main}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
p={theme.spacing(8)}
overflow="hidden"
>
<Box
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
<Stack direction="column">
<Stack
direction="row"
alignItems="center"
mb={theme.spacing(8)}
>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
{icon}
</Stack>
<Stack
direction="row"
alignItems="flex-start"
fontSize={theme.typography.h1.fontSize}
fontWeight={600}
color={color}
gap={theme.spacing(1)}
>
{value}
<Typography
fontSize={theme.typography.label.fontSize}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
opacity: 0.3,
}}
>
#
</Typography>
</Stack>
</Stack>
</Box>
);
};
StatusBox.propTypes = {
title: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
status: PropTypes.string,
};
export default StatusBox;
@@ -0,0 +1,46 @@
import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./StatusBox";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import SkeletonLayout from "./skeleton";
const StatusBoxes = ({ isLoading, summary }) => {
const theme = useTheme();
const { t } = useTranslation();
if (isLoading) return <SkeletonLayout shouldRender={isLoading} />;
return (
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<StatusBox
title={t("incidentsOptionsHeaderTotalIncidents")}
value={summary?.totalChecks || 0}
/>
<StatusBox
title={t("incidentsOptionsHeaderFilterResolved")}
status="up"
value={summary?.resolvedChecks || 0}
/>
<StatusBox
title={t("incidentsOptionsHeaderFilterCannotResolve")}
status="paused"
value={summary?.cannotResolveChecks || 0}
/>
<StatusBox
title={t("incidentsOptionsHeaderFilterDown")}
status="down"
value={summary?.downChecks || 0}
/>
</Stack>
);
};
StatusBoxes.propTypes = {
isLoading: PropTypes.bool,
summary: PropTypes.object,
};
export default StatusBoxes;
@@ -0,0 +1,36 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(12)}
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
</Stack>
);
};
export default SkeletonLayout;
+9 -1
View File
@@ -4,10 +4,12 @@ import Breadcrumbs from "../../Components/Breadcrumbs";
import GenericFallback from "../../Components/GenericFallback";
import IncidentTable from "./Components/IncidentTable";
import OptionsHeader from "./Components/OptionsHeader";
import StatusBoxes from "./Components/StatusBoxes";
//Utils
import { useTheme } from "@emotion/react";
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks";
import { useState, useEffect } from "react";
import NetworkError from "../../Components/GenericFallback/NetworkError";
import { useTranslation } from "react-i18next";
@@ -31,6 +33,8 @@ const Incidents = () => {
//Utils
const theme = useTheme();
const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
const [summary, isLoadingSummary, networkErrorSummary] =
useFetchChecksSummaryByTeamId();
const { monitorId } = useParams();
useEffect(() => {
@@ -51,7 +55,7 @@ const Incidents = () => {
setMonitorLookup(monitorLookup);
}, [monitors]);
if (networkError) {
if (networkError || networkErrorSummary) {
return (
<GenericFallback>
<NetworkError />
@@ -62,6 +66,10 @@ const Incidents = () => {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<StatusBoxes
isLoading={isLoadingSummary}
summary={summary}
/>
<OptionsHeader
shouldRender={!isLoading}
monitors={monitorLookup}
+1 -4
View File
@@ -87,10 +87,7 @@ const SettingsEmail = ({
!emailConfig.systemEmailPassword
) {
createToast({
body: t(
"settingsEmailRequiredFields",
"Email address, host, port and password are required"
),
body: t("settingsPage.emailSettings.toastEmailRequiredFieldsError"),
variant: "error",
});
return;
@@ -19,21 +19,21 @@ const StatusBox = ({ title, value, status }) => {
if (status === "up") {
color = theme.palette.success.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: 8 }}>
<Box sx={{ ...sharedStyles, top: theme.spacing(4) }}>
<Arrow />
</Box>
);
} else if (status === "down") {
color = theme.palette.error.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: theme.spacing(2) }}>
<Arrow />
</Box>
);
} else if (status === "paused") {
color = theme.palette.warning.lowContrast;
icon = (
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
<Box sx={{ ...sharedStyles, top: theme.spacing(6), right: theme.spacing(6) }}>
<ClockSnooze />
</Box>
);
@@ -75,15 +75,15 @@ const StatusBox = ({ title, value, status }) => {
<Stack
direction="row"
alignItems="flex-start"
fontSize={36}
fontSize={theme.typography.h1.fontSize}
fontWeight={600}
color={color}
gap="2px"
gap={theme.spacing(1)}
>
{value}
<Typography
fontSize={20}
fontSize={theme.typography.label.fontSize}
fontWeight={300}
color={theme.palette.primary.contrastTextSecondary}
sx={{
+12
View File
@@ -601,6 +601,18 @@ class NetworkService {
return this.axiosInstance.get(`/checks/team?${params.toString()}`);
};
/**
* ************************************
* Get summary of checks by team ID
* ************************************
*
* @async
* @returns {Promise<AxiosResponse>} The response from the axios GET request.
*/
getChecksAndSummaryByTeamId = async () => {
return this.axiosInstance.get(`/checks/team/summary`);
};
/**
* ************************************
* Update the status of a check
+15 -3
View File
@@ -197,12 +197,14 @@
"basicInformation": "Basic Information",
"bulkImport": {
"fallbackPage": "Import a file to upload a list of servers in bulk",
"invalidFileType": "Invalid file type",
"noFileSelected": "No file selected",
"parsingFailed": "Parsing failed",
"selectFile": "Select File",
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
"selectFileTips": "Select CSV file to upload",
"title": "Bulk Import",
"uploadFailed": "Upload failed",
"uploadSuccess": "Monitors created successfully!",
"validationFailed": "Validation failed"
},
@@ -226,6 +228,7 @@
"commonSaving": "Saving...",
"companyName": "Company name",
"configure": "Configure",
"confirmPassword": "Confirm password",
"cores": "Cores",
"country": "COUNTRY",
"cpu": "CPU",
@@ -385,7 +388,8 @@
"incidentsOptionsHeader": "Incidents for:",
"incidentsOptionsHeaderFilterAll": "All",
"incidentsOptionsHeaderFilterBy": "Filter by:",
"incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve",
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
"incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve",
"incidentsOptionsHeaderFilterDown": "Down",
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsOptionsHeaderLastDay": "Last day",
@@ -488,6 +492,11 @@
"title": "Export/Import"
},
"monitorDisplayName": "Monitor display name",
"monitorHooks": {
"failureAddDemoMonitors": "Failed to add demo monitors",
"successAddDemoMonitors": "Successfully added demo monitors"
},
"monitorState": {
"active": "Active",
"paused": "Paused",
@@ -661,12 +670,14 @@
"selectAll": "Select all",
"sendTestNotifications": "Send test notifications",
"seperateEmails": "You can separate multiple emails with a comma",
"settingsAppearance": "Appearance",
"settingsDisabled": "Disabled",
"settingsDisplayTimezone": "Display timezone",
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsFailedToClearStats": "Failed to clear stats",
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
"settingsFailedToSave": "Failed to save settings",
"settingsGeneralSettings": "General settings",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
"settingsPage": {
"aboutSettings": {
@@ -698,7 +709,8 @@
"labelUser": "Email user - Username for authentication, overrides email address if specified",
"linkTransport": "See specifications here",
"placeholderUser": "Leave empty if not required",
"title": "Email"
"title": "Email",
"toastEmailRequiredFieldsError": "Email address, host, port and password are required"
},
"pageSpeedSettings": {
"description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
+13
View File
@@ -111,6 +111,19 @@ class CheckController {
}
};
getChecksSummaryByTeamId = async (req, res, next) => {
try {
const { teamId } = req.user;
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
return res.success({
msg: this.stringService.checkGetSummary,
data: summary,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId"));
}
};
ackCheck = async (req, res, next) => {
try {
await ackCheckBodyValidation.validateAsync(req.body);
+25
View File
@@ -5,6 +5,7 @@ import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import User from "../../models/User.js";
import logger from "../../../utils/logger.js";
import { ObjectId } from "mongodb";
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
@@ -304,6 +305,29 @@ const ackAllChecks = async (monitorId, teamId, ack, path) => {
}
};
/**
* Get checks and summary by team ID
* @async
* @param {string} teamId
* @returns {Promise<Object>}
* @throws {Error}
*/
const getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
};
const checks = await Check.aggregate(
buildChecksSummaryByTeamIdPipeline({ matchStage })
);
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksSummaryByTeamId";
throw error;
}
};
/**
* Delete all checks for a monitor
* @async
@@ -387,6 +411,7 @@ export {
getChecksByTeam,
ackCheck,
ackAllChecks,
getChecksSummaryByTeamId,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
@@ -0,0 +1,48 @@
const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
resolvedChecks: {
$sum: {
$cond: [{ $eq: ["$ack", true] }, 1, 0],
},
},
downChecks: {
$sum: {
$cond: [
{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] },
1,
0,
],
},
},
cannotResolveChecks: {
$sum: {
$cond: [{ $eq: ["$statusCode", 5000] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
},
},
];
};
export { buildChecksSummaryByTeamIdPipeline };
+1
View File
@@ -14,6 +14,7 @@ class CheckRoutes {
initRoutes() {
this.router.get("/team", this.checkController.getChecksByTeam);
this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId);
this.router.delete(
"/team",
isAllowed(["admin", "superadmin"]),