diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/Fallback/index.jsx index 004ab4300..f35229494 100644 --- a/client/src/Components/Fallback/index.jsx +++ b/client/src/Components/Fallback/index.jsx @@ -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), }} > { { + 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 }; diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 013e7fc25..d59d88c74 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -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); } diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx index 457c6fd96..08a9bcf1c 100644 --- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx @@ -87,13 +87,13 @@ const OptionsHeader = ({ > {t("incidentsOptionsHeaderFilterCannotResolve")} - {/* */} + diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx new file mode 100644 index 000000000..b19f017e7 --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -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 = ( + + + + ); + } else if (status === "down") { + color = theme.palette.error.lowContrast; + icon = ( + + + + ); + } else if (status === "paused") { + color = theme.palette.warning.lowContrast; + icon = ( + + + + ); + } else { + color = theme.palette.accent.main; + icon = ( + + + + ); + } + + return ( + + + + + + + + {title} + + {icon} + + + {value} + + + # + + + + + ); +}; + +StatusBox.propTypes = { + title: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + status: PropTypes.string, +}; + +export default StatusBox; diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..6ba2f1820 --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx @@ -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 ; + return ( + + + + + + + ); +}; + +StatusBoxes.propTypes = { + isLoading: PropTypes.bool, + summary: PropTypes.object, +}; + +export default StatusBoxes; diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx new file mode 100644 index 000000000..b9aa016b7 --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx @@ -0,0 +1,36 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 09d4f0e25..11946ae63 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -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 ( @@ -62,6 +66,10 @@ const Incidents = () => { return ( + { if (status === "up") { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); @@ -75,15 +75,15 @@ const StatusBox = ({ title, value, status }) => { {value} } The response from the axios GET request. + */ + getChecksAndSummaryByTeamId = async () => { + return this.axiosInstance.get(`/checks/team/summary`); + }; + /** * ************************************ * Update the status of a check diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8d26f75fd..9e6c9fe91 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -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 or 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.", diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js index 8f9d0fbb5..e50d5b7fc 100755 --- a/server/controllers/checkController.js +++ b/server/controllers/checkController.js @@ -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); diff --git a/server/db/mongo/modules/checkModule.js b/server/db/mongo/modules/checkModule.js index b7d9f4fb2..fb0cc539e 100755 --- a/server/db/mongo/modules/checkModule.js +++ b/server/db/mongo/modules/checkModule.js @@ -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} + * @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, diff --git a/server/db/mongo/modules/checkModuleQueries.js b/server/db/mongo/modules/checkModuleQueries.js new file mode 100644 index 000000000..7795fb6dc --- /dev/null +++ b/server/db/mongo/modules/checkModuleQueries.js @@ -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 }; diff --git a/server/routes/checkRoute.js b/server/routes/checkRoute.js index 7f91dbe31..4ac1e3730 100755 --- a/server/routes/checkRoute.js +++ b/server/routes/checkRoute.js @@ -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"]),