Compare commits

...
54 Commits
Author SHA1 Message Date
Alexander Holliday a924227805 Merge pull request #2964 from bluewave-labs/develop
develop -> master
2025-09-18 13:40:45 -07:00
Alexander Holliday e4e75632c5 Merge pull request #2963 from bluewave-labs/translation-update-78
🌐 Updated Translations from POEditor
2025-09-18 13:40:10 -07:00
GitHub Action ddb7c24832 feat: translations updated from POEditor 2025-09-18 20:25:11 +00:00
Alexander Holliday 2144d3f8c0 Merge pull request #2962 from bluewave-labs/develop
develop -> master
2025-09-18 12:04:51 -07:00
Alexander Holliday 4a824dec81 Merge pull request #2960 from bluewave-labs/feat/bump-version
bump version
2025-09-18 12:03:26 -07:00
Alex Holliday b35fee1be8 bump version 2025-09-18 12:02:49 -07:00
Alexander Holliday 1b167af12b Merge pull request #2959 from Owaiseimdad/Addition-of-sliding-window-implementation
Addition of incident tab in creation and config page which is same.
2025-09-18 11:50:55 -07:00
Owaise f2962e34ae Fixed the formatting. 2025-09-19 00:06:31 +05:30
Owaise 386db5029a Addition of incident tab in creation and config page which is same. 2025-09-19 00:03:18 +05:30
Alexander Holliday 0e6e54a124 Merge pull request #2957 from FrancocDev/develop
Allow jsonPath to be used along expectedValue and validate the response.
2025-09-18 09:47:53 -07:00
Franco Carrara c666cb3879 fix formatting issues 2025-09-17 23:46:20 -03:00
Franco Carrara 4039517683 Add response messages to string service 2025-09-17 23:33:45 -03:00
Franco Carrara 00fc7b8038 Allow jsonPath to be used along expectedValue and validate the response. 2025-09-17 23:23:16 -03:00
Alexander Holliday 81d283ec8d Merge pull request #2956 from ajhollid/bump-dependencies
chore: bump deps
2025-09-17 15:17:14 -07:00
Alex Holliday 105100e349 bump deps 2025-09-17 15:07:55 -07:00
Alexander Holliday 4598690dbf Merge pull request #2954 from brownyroll/develop
feat: Support Thailand language
2025-09-16 09:00:51 -07:00
Bamz Kittisak Udomsri f68341c315 feat: thailang 2025-09-16 20:41:13 +07:00
Alexander Holliday e8a8cea909 Merge pull request #2953 from bluewave-labs/fix-skip-maintenance-notifications
Fix: Skip notifications during maintenance windows
2025-09-15 11:13:28 -07:00
karenvicent bf53f107e3 Merge branch 'develop' into fix-skip-maintenance-notifications 2025-09-15 12:29:47 -04:00
karenvicent 00de974528 Fix: Skip notifications for monitors during active maintenance windows 2025-09-15 12:28:37 -04:00
Alexander Holliday 0445398e94 Merge pull request #2952 from bluewave-labs/feat-notifications-body
Add monitor name and URL formatting for hardware notifications
2025-09-15 09:15:10 -07:00
karenvicent 272e2ba955 add monitor name and url to infrastructure alert 2025-09-15 08:34:51 -04:00
Alexander Holliday 0a2adc2d92 Merge pull request #2948 from bluewave-labs/feat-discord-format
Add Discord embed message formatting for notifications
2025-09-12 09:03:19 -07:00
karenvicent 7008fc1770 Merge branch 'develop' into feat-discord-format 2025-09-12 10:14:07 -04:00
karenvicent 041a261540 add llama sugestions 2025-09-12 10:13:38 -04:00
Gorkem Cetin (BWL) 1008d819f7 Update README.md 2025-09-11 22:40:11 -04:00
karenvicent fb84a79a04 Add Discord embed message formatting for notifications 2025-09-11 16:29:08 -04:00
Alexander Holliday fcd6786149 Merge pull request #2936 from bluewave-labs/develop
develop -> master
2025-09-08 20:43:21 -07:00
Alexander Holliday 45bfdbdce0 Merge pull request #2935 from bluewave-labs/fix/mono-dist-ping
fix vite.config
2025-09-08 20:40:33 -07:00
Alex Holliday 1537c2141f fix vite.config 2025-09-08 20:39:45 -07:00
Alexander Holliday ea968ebf3c Merge pull request #2933 from bluewave-labs/develop
develop -> master
2025-09-08 20:33:35 -07:00
Alexander Holliday 3ba1a52644 Merge pull request #2932 from bluewave-labs/fix/mono-dist-ping
fix: mono dist ping
2025-09-08 20:31:01 -07:00
Alex Holliday a015cc6706 fix dist-arm, bump version 2025-09-08 20:29:38 -07:00
Alex Holliday 5bad362793 fix server build order 2025-09-08 20:27:20 -07:00
Alexander Holliday 5520756edd Merge pull request #2923 from bluewave-labs/feat-page-initialization-hook
Add PageStateWrapper and fix initial render flash
2025-09-08 09:12:14 -07:00
karenvicent ba954a1d6e run format 2025-09-08 11:15:14 -04:00
karenvicent 2944ff298c Merge branch 'develop' into feat-page-initialization-hook 2025-09-08 10:50:15 -04:00
karenvicent 80d121e87a add llama suggestions 2025-09-08 10:49:42 -04:00
Alexander Holliday 8a7b852974 Merge pull request #2929 from bluewave-labs/develop
develop -> master
2025-09-08 07:24:16 -07:00
Alexander Holliday ed8352d857 Merge pull request #2928 from bluewave-labs/fix/install-ping
fix: install ping
2025-09-08 07:11:34 -07:00
Alex Holliday 41f64ca9fe bump version 2025-09-08 07:10:29 -07:00
Alex Holliday 738e49dc92 Merge branch 'develop' into fix/install-ping 2025-09-08 07:10:15 -07:00
Alex Holliday 68a16257c6 add ping to dist 2025-09-08 07:09:39 -07:00
Alex Holliday 5dcbae55f4 install ping 2025-09-08 06:57:10 -07:00
Alexander Holliday e0d963256c Merge pull request #2926 from bluewave-labs/fix/bump-version
fix: bump version
2025-09-08 06:45:15 -07:00
Alexander Holliday 2bff4bbd15 Merge pull request #2927 from bluewave-labs/fix/ping-error-logging
fix: throw ping error if present
2025-09-08 06:45:00 -07:00
Alex Holliday a0eba89f34 bump version 2025-09-08 06:43:35 -07:00
Alex Holliday 4e78de9da2 throw error if present 2025-09-08 06:42:36 -07:00
karenvicent 85f00eb7e8 Merge branch 'develop' into feat-page-initialization-hook 2025-09-05 15:59:14 -04:00
karenvicent 8e13855b66 feat create pagestate wrapper 2025-09-05 15:53:22 -04:00
Alexander Holliday 06f49e24c2 Merge pull request #2867 from antonstrover/fix/uptime%-charts-toggles
fix(status-page): toggles for charts and uptime %
2025-09-05 09:08:40 -07:00
antonstrover e598ae45f7 Merge branch 'develop' into fix/uptime%-charts-toggles 2025-09-05 16:46:04 +01:00
anton.strover ad7f469e06 add display name for bar 2025-09-05 16:27:48 +01:00
anton.strover 27d613b594 remove carried away-ness 2025-08-29 10:49:22 +01:00
50 changed files with 3504 additions and 545 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ import { URLSearchParams } from "url";
const API_TOKEN = process.env.POEDITOR_API; const API_TOKEN = process.env.POEDITOR_API;
const PROJECT_ID = process.env.POEDITOR_PROJECT_ID; const PROJECT_ID = process.env.POEDITOR_PROJECT_ID;
const LANGUAGES = ( const LANGUAGES = (
process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn" process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
).split(","); ).split(",");
const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json"; const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json";
+1 -1
View File
@@ -7,7 +7,7 @@ on:
languages: languages:
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
required: false required: false
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn" default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
format: format:
description: "Export format (key_value_json or json)" description: "Export format (key_value_json or json)"
required: false required: false
+1 -1
View File
@@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
### Using a Custom CA ### Using a Custom CA
+22 -8
View File
@@ -45,7 +45,7 @@
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"vite": "6.3.5" "vite": "6.3.6"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
@@ -2422,13 +2422,13 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.9.0", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
"form-data": "^4.0.0", "form-data": "^4.0.4",
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
@@ -3756,6 +3756,20 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6375,9 +6389,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.5", "version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
+1 -1
View File
@@ -58,6 +58,6 @@
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"vite": "6.3.5" "vite": "6.3.6"
} }
} }
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material"; import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../Utils/timeUtils"; import { formatDateWithTz } from "../../../Utils/timeUtils";
import { useEffect, useState } from "react"; import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
@@ -17,23 +17,29 @@ import PropTypes from "prop-types";
* @returns {JSX.Element} The Bar component. * @returns {JSX.Element} The Bar component.
*/ */
const Bar = ({ width, height, backgroundColor, borderRadius, children }) => { const Bar = forwardRef(
const theme = useTheme(); ({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => {
const theme = useTheme();
return ( return (
<Box <Box
width={width} ref={ref}
position="relative" position="relative"
height={height} width={width}
backgroundColor={backgroundColor} height={height}
sx={{ backgroundColor={backgroundColor}
borderRadius: borderRadius || theme.spacing(1.5), sx={{
}} borderRadius: borderRadius || theme.spacing(1.5),
> }}
{children} {...otherProps}
</Box> >
); {children}
}; </Box>
);
}
);
Bar.displayName = "Bar";
Bar.propTypes = { Bar.propTypes = {
width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
@@ -0,0 +1,66 @@
import GenericFallback from "../GenericFallback";
import Fallback from "../Fallback";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import PropTypes from "prop-types";
const PageStateWrapper = ({
networkError,
isLoading,
items,
type,
fallbackLink,
fallbackChildren,
children,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin();
const isEmpty = items === null || (Array.isArray(items) && items.length === 0);
const hasInitialized = items !== undefined && !(isLoading && isEmpty);
const showEmpty = !isLoading && isEmpty;
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!hasInitialized) {
return null;
}
if (showEmpty) {
return (
<Fallback
type={type}
title={t(`${type}.fallback.title`)}
checks={t(`${type}.fallback.checks`, { returnObjects: true })}
link={fallbackLink}
isAdmin={isAdmin}
>
{fallbackChildren}
</Fallback>
);
}
return <>{children}</>;
};
PageStateWrapper.propTypes = {
networkError: PropTypes.any,
isLoading: PropTypes.bool,
items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]),
type: PropTypes.string,
fallbackLink: PropTypes.string,
fallbackChildren: PropTypes.node,
children: PropTypes.node,
};
export default PageStateWrapper;
+1 -1
View File
@@ -34,7 +34,7 @@ const useCreateNotification = () => {
const useGetNotificationsByTeamId = (updateTrigger) => { const useGetNotificationsByTeamId = (updateTrigger) => {
const [notifications, setNotifications] = useState([]); const [notifications, setNotifications] = useState([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useCallback } from "react";
const useInfrastructureMonitorForm = () => { const useInfrastructureMonitorForm = () => {
const [infrastructureMonitor, setInfrastructureMonitor] = useState({ const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "", url: "",
@@ -6,6 +6,8 @@ const useInfrastructureMonitorForm = () => {
notifications: [], notifications: [],
notify_email: false, notify_email: false,
interval: 0.25, interval: 0.25,
statusWindowSize: 5,
statusWindowThreshold: 60,
cpu: false, cpu: false,
usage_cpu: "", usage_cpu: "",
memory: false, memory: false,
@@ -29,9 +31,10 @@ const useInfrastructureMonitorForm = () => {
[event.target.name]: event.target.checked, [event.target.name]: event.target.checked,
}); });
}; };
const initializeInfrastructureMonitorForCreate = (globalSettings) => { const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => {
const gt = globalSettings?.data?.settings?.globalThresholds || {}; const gt = globalSettings?.data?.settings?.globalThresholds || {};
setInfrastructureMonitor({ setInfrastructureMonitor((prev) => ({
...prev,
url: "", url: "",
name: "", name: "",
notifications: [], notifications: [],
@@ -45,17 +48,20 @@ const useInfrastructureMonitorForm = () => {
temperature: gt.temperature !== undefined, temperature: gt.temperature !== undefined,
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "", secret: "",
}); }));
}; }, []);
const initializeInfrastructureMonitorForUpdate = (monitor) => { const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => {
const MS_PER_MINUTE = 60000; const MS_PER_MINUTE = 60000;
const { thresholds = {} } = monitor; const { thresholds = {} } = monitor;
setInfrastructureMonitor({ setInfrastructureMonitor((prev) => ({
...prev,
url: monitor.url.replace(/^https?:\/\//, ""), url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "", name: monitor.name || "",
notifications: monitor.notifications || [], notifications: monitor.notifications || [],
interval: monitor.interval / MS_PER_MINUTE, interval: monitor.interval / MS_PER_MINUTE,
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
cpu: thresholds.usage_cpu !== undefined, cpu: thresholds.usage_cpu !== undefined,
usage_cpu: usage_cpu:
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
@@ -75,8 +81,8 @@ const useInfrastructureMonitorForm = () => {
? (thresholds.usage_temperature * 100).toString() ? (thresholds.usage_temperature * 100).toString()
: "", : "",
secret: monitor.secret || "", secret: monitor.secret || "",
}); }));
}; }, []);
return { return {
infrastructureMonitor, infrastructureMonitor,
setInfrastructureMonitor, setInfrastructureMonitor,
@@ -12,6 +12,8 @@ const useInfrastructureSubmit = () => {
? infrastructureMonitor.url ? infrastructureMonitor.url
: infrastructureMonitor.name, : infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE, interval: infrastructureMonitor.interval * MS_PER_MINUTE,
statusWindowSize: infrastructureMonitor.statusWindowSize,
statusWindowThreshold: infrastructureMonitor.statusWindowThreshold,
cpu: infrastructureMonitor.cpu, cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu ...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu } ? { usage_cpu: infrastructureMonitor.usage_cpu }
@@ -86,7 +86,14 @@ const CreateInfrastructureMonitor = () => {
setHttps(monitor.url.startsWith("https")); setHttps(monitor.url.startsWith("https"));
initializeInfrastructureMonitorForUpdate(monitor); initializeInfrastructureMonitorForUpdate(monitor);
} }
}, [isCreate, monitor, globalSettings, globalSettingsLoading]); }, [
isCreate,
monitor,
globalSettings,
globalSettingsLoading,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
]);
// Handlers // Handlers
const onSubmit = async (event) => { const onSubmit = async (event) => {
@@ -281,6 +288,39 @@ const CreateInfrastructureMonitor = () => {
setNotifications={infrastructureMonitor.notifications} setNotifications={infrastructureMonitor.notifications}
/> />
</ConfigBox> </ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={infrastructureMonitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={infrastructureMonitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<CustomAlertsSection <CustomAlertsSection
errors={errors} errors={errors}
onChange={onChange} onChange={onChange}
@@ -1,12 +1,11 @@
// Components // Components
import { Stack, Typography } from "@mui/material"; import { Stack } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs"; import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorCountHeader from "../../../Components/MonitorCountHeader"; import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
import MonitorsTable from "./Components/MonitorsTable"; import MonitorsTable from "./Components/MonitorsTable";
import Pagination from "../../..//Components/Table/TablePagination"; import Pagination from "../../..//Components/Table/TablePagination";
import GenericFallback from "../../../Components/GenericFallback"; import PageStateWrapper from "../../../Components/PageStateWrapper";
import Fallback from "../../../Components/Fallback";
import Filter from "./Components/Filters"; import Filter from "./Components/Filters";
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent"; import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent";
// Utils // Utils
@@ -81,75 +80,58 @@ const InfrastructureMonitors = () => {
updateTrigger, updateTrigger,
}); });
if (networkError === true) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!isLoading && typeof summary?.totalMonitors === "undefined") {
return (
<Fallback
type="infrastructureMonitor"
title={t("infrastructureMonitor.fallback.title")}
checks={t("infrastructureMonitor.fallback.checks", { returnObjects: true })}
link="/infrastructure/create"
isAdmin={isAdmin}
/>
);
}
return ( return (
<Stack gap={theme.spacing(10)}> <>
<Breadcrumbs list={BREADCRUMBS} /> <PageStateWrapper
<MonitorCreateHeader networkError={networkError}
isAdmin={isAdmin}
isLoading={isLoading} isLoading={isLoading}
path="/infrastructure/create" items={monitors}
/> type="infrastructureMonitor"
<Stack direction={"row"}> fallbackLink="/infrastructure/create"
<MonitorCountHeader >
isLoading={isLoading} <Stack gap={theme.spacing(10)}>
monitorCount={summary?.totalMonitors ?? 0} <Breadcrumbs list={BREADCRUMBS} />
/> <MonitorCreateHeader
<Filter isAdmin={isAdmin}
selectedStatus={selectedStatus} isLoading={isLoading}
setSelectedStatus={setSelectedStatus} path="/infrastructure/create"
setToFilterStatus={setToFilterStatus} />
handleReset={handleReset} <Stack direction={"row"}>
/> <MonitorCountHeader
<SearchComponent isLoading={isLoading}
monitors={monitors} monitorCount={summary?.totalMonitors ?? 0}
onSearchChange={setSearch} />
setIsSearching={setIsSearching} <Filter
/> selectedStatus={selectedStatus}
</Stack> setSelectedStatus={setSelectedStatus}
setToFilterStatus={setToFilterStatus}
handleReset={handleReset}
/>
<SearchComponent
monitors={monitors}
onSearchChange={setSearch}
setIsSearching={setIsSearching}
/>
</Stack>
<MonitorsTable <MonitorsTable
isLoading={isLoading} isLoading={isLoading}
monitors={monitors} monitors={monitors}
isAdmin={isAdmin} isAdmin={isAdmin}
handleActionMenuDelete={handleActionMenuDelete} handleActionMenuDelete={handleActionMenuDelete}
isSearching={isSearching} isSearching={isSearching}
/> />
<Pagination <Pagination
itemCount={summary?.totalMonitors} itemCount={summary?.totalMonitors}
paginationLabel={t("monitors")} paginationLabel={t("monitors")}
page={page} page={page}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage} handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage} handleChangeRowsPerPage={handleChangeRowsPerPage}
/> />
</Stack> </Stack>
</PageStateWrapper>
</>
); );
}; };
+47 -66
View File
@@ -1,6 +1,5 @@
import { Stack, Button } from "@mui/material"; import { Stack, Button } from "@mui/material";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import Fallback from "../../Components/Fallback";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import "./index.css"; import "./index.css";
import MaintenanceTable from "./MaintenanceTable"; import MaintenanceTable from "./MaintenanceTable";
@@ -8,24 +7,21 @@ import { useSelector } from "react-redux";
import { networkService } from "../../main"; import { networkService } from "../../main";
import Breadcrumbs from "../../Components/Breadcrumbs"; import Breadcrumbs from "../../Components/Breadcrumbs";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Typography } from "@mui/material"; import PageStateWrapper from "../../Components/PageStateWrapper";
import GenericFallback from "../../Components/GenericFallback";
const Maintenance = () => { const Maintenance = () => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5); const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5);
const isAdmin = useIsAdmin(); const [maintenanceWindows, setMaintenanceWindows] = useState(undefined);
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [sort, setSort] = useState({}); const [sort, setSort] = useState({});
const [updateTrigger, setUpdateTrigger] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false);
const [networkError, setNetworkError] = useState(false); const [networkError, setNetworkError] = useState(false);
const [isDataFetched, setIsDataFetched] = useState(false); const [isLoading, setIsLoading] = useState(true);
const handleActionMenuDelete = () => { const handleActionMenuDelete = () => {
setUpdateTrigger((prev) => !prev); setUpdateTrigger((prev) => !prev);
@@ -34,80 +30,65 @@ const Maintenance = () => {
useEffect(() => { useEffect(() => {
const fetchMaintenanceWindows = async () => { const fetchMaintenanceWindows = async () => {
try { try {
setNetworkError(false);
setIsLoading(true);
const response = await networkService.getMaintenanceWindowsByTeamId({ const response = await networkService.getMaintenanceWindowsByTeamId({
page: page, page: page,
rowsPerPage: rowsPerPage, rowsPerPage: rowsPerPage,
}); });
const { maintenanceWindows, maintenanceWindowCount } = response.data.data; const { maintenanceWindows, maintenanceWindowCount } = response.data.data;
setMaintenanceWindows(maintenanceWindows); setMaintenanceWindows(maintenanceWindows ?? []);
setMaintenanceWindowCount(maintenanceWindowCount); setMaintenanceWindowCount(maintenanceWindowCount ?? 0);
} catch (error) { } catch (error) {
setNetworkError(true); setNetworkError(true);
} finally { } finally {
setIsDataFetched(true); setIsLoading(false);
} }
}; };
fetchMaintenanceWindows(); fetchMaintenanceWindows();
}, [page, rowsPerPage, updateTrigger]); }, [page, rowsPerPage, updateTrigger]);
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
// Only show the fallback if we've fetched data and there are no maintenance windows
if (isDataFetched && maintenanceWindows.length === 0) {
return (
<Fallback
type="maintenanceWindow"
title={t("maintenanceWindow.fallback.title")}
checks={t("maintenanceWindow.fallback.checks", { returnObjects: true })}
link="/maintenance/create"
isAdmin={isAdmin}
/>
);
}
return ( return (
<Stack gap={theme.spacing(10)}> <>
<Stack <PageStateWrapper
direction="row" networkError={networkError}
justifyContent="space-between" isLoading={isLoading}
alignItems="center" items={maintenanceWindows}
mt={theme.spacing(5)} type="maintenanceWindow"
fallbackLink="/maintenance/create"
> >
<Breadcrumbs list={[{ name: "maintenance", path: "/maintenance" }]} /> <Stack gap={theme.spacing(10)}>
<Button <Stack
variant="contained" direction="row"
color="accent" justifyContent="space-between"
onClick={() => { alignItems="center"
navigate("/maintenance/create"); mt={theme.spacing(5)}
}} >
sx={{ fontWeight: 500 }} <Breadcrumbs list={[{ name: "maintenance", path: "/maintenance" }]} />
> <Button
{t("createMaintenanceWindow")} variant="contained"
</Button> color="accent"
</Stack> onClick={() => {
<MaintenanceTable navigate("/maintenance/create");
page={page} }}
setPage={setPage} sx={{ fontWeight: 500 }}
rowsPerPage={rowsPerPage} >
sort={sort} {t("createMaintenanceWindow")}
setSort={setSort} </Button>
maintenanceWindows={maintenanceWindows} </Stack>
maintenanceWindowCount={maintenanceWindowCount} <MaintenanceTable
updateCallback={handleActionMenuDelete} page={page}
/> setPage={setPage}
</Stack> rowsPerPage={rowsPerPage}
sort={sort}
setSort={setSort}
maintenanceWindows={maintenanceWindows}
maintenanceWindowCount={maintenanceWindowCount}
updateCallback={handleActionMenuDelete}
/>
</Stack>
</PageStateWrapper>
</>
); );
}; };
+40 -44
View File
@@ -4,11 +4,10 @@ import Typography from "@mui/material/Typography";
import Breadcrumbs from "../../Components/Breadcrumbs"; import Breadcrumbs from "../../Components/Breadcrumbs";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import DataTable from "../../Components/Table"; import DataTable from "../../Components/Table";
import Fallback from "../../Components/Fallback";
import ActionMenu from "./components/ActionMenu"; import ActionMenu from "./components/ActionMenu";
import PageStateWrapper from "../../Components/PageStateWrapper";
// Utils // Utils
import { useIsAdmin } from "../../Hooks/useIsAdmin";
import { useState } from "react"; import { useState } from "react";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -23,7 +22,6 @@ const Notifications = () => {
const theme = useTheme(); const theme = useTheme();
const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }]; const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }];
const [updateTrigger, setUpdateTrigger] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false);
const isAdmin = useIsAdmin();
const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger); const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger);
const [deleteNotification, isDeleting, deleteError] = useDeleteNotification(); const [deleteNotification, isDeleting, deleteError] = useDeleteNotification();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -76,49 +74,47 @@ const Notifications = () => {
}, },
]; ];
if (notifications?.length === 0) {
return (
<Fallback
type="notifications"
title={t("notifications.fallback.title")}
checks={t("notifications.fallback.checks", { returnObjects: true })}
link="/notifications/create"
isAdmin={isAdmin}
/>
);
}
return ( return (
<Stack gap={theme.spacing(10)}> <>
<Breadcrumbs list={BREADCRUMBS} /> <PageStateWrapper
<Stack networkError={error}
direction="row" isLoading={isLoading}
justifyContent="flex-end" items={notifications}
type="notifications"
fallbackLink="/notifications/create"
> >
<Button <Stack gap={theme.spacing(10)}>
variant="contained" <Breadcrumbs list={BREADCRUMBS} />
color="accent" <Stack
onClick={() => navigate("/notifications/create")} direction="row"
> justifyContent="flex-end"
{t("notifications.createButton")} >
</Button> <Button
</Stack> variant="contained"
<Typography variant="h1">{t("notifications.createTitle")}</Typography> color="accent"
<DataTable onClick={() => navigate("/notifications/create")}
config={{ >
onRowClick: (row) => navigate(`/notifications/${row._id}`), {t("notifications.createButton")}
rowSX: { </Button>
cursor: "pointer", </Stack>
"&:hover td": { <Typography variant="h1">{t("notifications.createTitle")}</Typography>
backgroundColor: theme.palette.tertiary.main, <DataTable
transition: "background-color .3s ease", config={{
}, onRowClick: (row) => navigate(`/notifications/${row._id}`),
}, rowSX: {
}} cursor: "pointer",
headers={headers} "&:hover td": {
data={notifications} backgroundColor: theme.palette.tertiary.main,
/> transition: "background-color .3s ease",
</Stack> },
},
}}
headers={headers}
data={notifications}
/>
</Stack>
</PageStateWrapper>
</>
); );
}; };
+36 -53
View File
@@ -1,18 +1,16 @@
// Components // Components
import { useState } from "react"; import { useState } from "react";
import Breadcrumbs from "../../../Components/Breadcrumbs"; import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material"; import { Stack } from "@mui/material";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
import MonitorCountHeader from "../../../Components/MonitorCountHeader"; import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid"; import MonitorGrid from "./Components/MonitorGrid";
import Fallback from "../../../Components/Fallback"; import PageStateWrapper from "../../../Components/PageStateWrapper";
import GenericFallback from "../../../Components/GenericFallback";
import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning"; import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning";
// Utils // Utils
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks"; import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
import { useFetchSettings } from "../../../Hooks/settingsHooks"; import { useFetchSettings } from "../../../Hooks/settingsHooks";
// Constants // Constants
@@ -20,7 +18,6 @@ const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const TYPES = ["pagespeed"]; const TYPES = ["pagespeed"];
const PageSpeed = () => { const PageSpeed = () => {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin(); const isAdmin = useIsAdmin();
const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({ const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({
@@ -40,56 +37,42 @@ const PageSpeed = () => {
setIsEmailPasswordSet: () => {}, setIsEmailPasswordSet: () => {},
}); });
if (networkError === true) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
type="pageSpeed"
title={t("pageSpeed.fallback.title")}
checks={t("pageSpeed.fallback.checks", { returnObjects: true })}
link="/pagespeed/create"
isAdmin={isAdmin}
>
{isAdmin && settingsData && !settingsData.pagespeedApiKey && (
<FallbackPageSpeedWarning settingsData={settingsData} />
)}
</Fallback>
);
}
return ( return (
<Stack gap={theme.spacing(10)}> <>
<Breadcrumbs list={BREADCRUMBS} /> <PageStateWrapper
<CreateMonitorHeader networkError={networkError}
isAdmin={isAdmin}
isLoading={isLoading} isLoading={isLoading}
path="/pagespeed/create" items={monitors}
/> type="pageSpeed"
<MonitorCountHeader fallbackLink="/pagespeed/create"
isLoading={isLoading} fallbackChildren={
monitorCount={monitorsSummary?.totalMonitors} isAdmin &&
sx={{ mb: theme.spacing(8) }} settingsData &&
/> !settingsData.pagespeedApiKey && (
<MonitorGrid <FallbackPageSpeedWarning settingsData={settingsData} />
size={6} )
shouldRender={!isLoading} }
monitors={monitors} >
/> <Stack gap={theme.spacing(10)}>
</Stack> <Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
isLoading={isLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader
isLoading={isLoading}
monitorCount={monitorsSummary?.totalMonitors}
sx={{ mb: theme.spacing(8) }}
/>
<MonitorGrid
size={6}
shouldRender={!isLoading}
monitors={monitors}
/>
</Stack>
</PageStateWrapper>
</>
); );
}; };
@@ -11,7 +11,12 @@ import PropTypes from "prop-types";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => { const MonitorsList = ({
isLoading = false,
shouldRender = true,
monitors = [],
statusPage = {},
}) => {
const theme = useTheme(); const theme = useTheme();
const { determineState } = useMonitorUtils(); const { determineState } = useMonitorUtils();
@@ -40,10 +45,12 @@ const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] })
alignItems="center" alignItems="center"
gap={theme.spacing(20)} gap={theme.spacing(20)}
> >
<Box flex={9}> {statusPage.showCharts !== false && (
<StatusPageBarChart checks={monitor?.checks?.slice().reverse()} /> <Box flex={9}>
</Box> <StatusPageBarChart checks={monitor?.checks?.slice().reverse()} />
<Box flex={1}> </Box>
)}
<Box flex={statusPage.showCharts !== false ? 1 : 10}>
<StatusLabel <StatusLabel
status={status} status={status}
text={status} text={status}
@@ -60,6 +67,7 @@ const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] })
MonitorsList.propTypes = { MonitorsList.propTypes = {
monitors: PropTypes.array.isRequired, monitors: PropTypes.array.isRequired,
statusPage: PropTypes.object,
}; };
export default MonitorsList; export default MonitorsList;
+4 -1
View File
@@ -154,7 +154,10 @@ const PublicStatus = () => {
/> />
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography> <Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
<StatusBar monitors={monitors} /> <StatusBar monitors={monitors} />
<MonitorsList monitors={monitors} /> <MonitorsList
monitors={monitors}
statusPage={statusPage}
/>
{link} {link}
</Stack> </Stack>
); );
@@ -1,14 +1,11 @@
// Components // Components
import { Stack, Typography } from "@mui/material"; import { Stack } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs"; import Breadcrumbs from "../../../Components/Breadcrumbs";
import Fallback from "../../../Components/Fallback";
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
import GenericFallback from "../../../Components/GenericFallback";
import StatusPagesTable from "./Components/StatusPagesTable"; import StatusPagesTable from "./Components/StatusPagesTable";
import SkeletonLayout from "../../../Components/Skeletons/FullPage"; import PageStateWrapper from "../../../Components/PageStateWrapper";
// Utils // Utils
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch"; import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch";
import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useIsAdmin } from "../../../Hooks/useIsAdmin";
const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; const BREADCRUMBS = [{ name: `Status Pages`, path: "" }];
@@ -16,52 +13,30 @@ const BREADCRUMBS = [{ name: `Status Pages`, path: "" }];
const StatusPages = () => { const StatusPages = () => {
// Utils // Utils
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation();
const isAdmin = useIsAdmin(); const isAdmin = useIsAdmin();
const [isLoading, networkError, statusPages] = useStatusPagesFetch(); const [isLoading, networkError, statusPages] = useStatusPagesFetch();
if (isLoading) {
return <SkeletonLayout />;
}
if (networkError === true) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (!isLoading && typeof statusPages !== "undefined" && statusPages.length === 0) {
return (
<Fallback
title={t("statusPage.fallback.title")}
checks={t("statusPage.fallback.checks", { returnObjects: true })}
type="statusPage"
link="/status/uptime/create"
isAdmin={isAdmin}
/>
);
}
return ( return (
<Stack gap={theme.spacing(10)}> <>
<Breadcrumbs list={BREADCRUMBS} /> <PageStateWrapper
<MonitorCreateHeader networkError={networkError}
label="Create status page"
isAdmin={isAdmin}
path="/status/uptime/create"
isLoading={isLoading} isLoading={isLoading}
/> items={statusPages}
<StatusPagesTable data={statusPages} /> type="statusPage"
</Stack> fallbackLink="/status/uptime/create"
>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
label="Create status page"
isAdmin={isAdmin}
path="/status/uptime/create"
isLoading={isLoading}
/>
<StatusPagesTable data={statusPages} />
</Stack>
</PageStateWrapper>
</>
); );
}; };
+70 -90
View File
@@ -10,16 +10,14 @@ import StatusBoxes from "./Components/StatusBoxes";
import UptimeDataTable from "./Components/UptimeDataTable"; import UptimeDataTable from "./Components/UptimeDataTable";
import Pagination from "../../../Components/Table/TablePagination"; import Pagination from "../../../Components/Table/TablePagination";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
import Fallback from "../../../Components/Fallback";
import GenericFallback from "../../../Components/GenericFallback";
import SearchComponent from "./Components/SearchComponent"; import SearchComponent from "./Components/SearchComponent";
import Filter from "./Components/Filter"; import Filter from "./Components/Filter";
import PageStateWrapper from "../../../Components/PageStateWrapper";
import MonitorCountHeader from "../../../Components/MonitorCountHeader"; import MonitorCountHeader from "../../../Components/MonitorCountHeader";
// MUI Components // MUI Components
import { Stack, Box, Button, Typography } from "@mui/material"; import { Stack, Box, Button } from "@mui/material";
// Utils // Utils
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useIsAdmin } from "../../../Hooks/useIsAdmin";
@@ -153,93 +151,75 @@ const UptimeMonitors = () => {
}, [isSearching]); }, [isSearching]);
const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading; const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading;
if (networkError) {
return (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{t("common.toasts.networkError")}
</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</GenericFallback>
);
}
if (
!isLoading &&
(monitorsSummary?.totalMonitors === 0 ||
typeof monitorsSummary?.totalMonitors === "undefined")
) {
return (
<Fallback
type="uptimeMonitor"
title={t("uptimeMonitor.fallback.title")}
checks={t("uptimeMonitor.fallback.checks", { returnObjects: true })}
link="/uptime/create"
isAdmin={isAdmin}
/>
);
}
return (
<Stack
className="monitors"
gap={theme.spacing(10)}
>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
isLoading={isLoading}
path="/uptime/create"
bulkPath="/uptime/bulk-import"
/>
<Greeting type="uptime" />
<StatusBoxes
monitorsSummary={monitorsSummary}
shouldRender={!monitorsWithSummaryIsLoading}
/>
<Stack direction={"row"}> return (
<MonitorCountHeader <>
isLoading={monitorsWithSummaryIsLoading} <PageStateWrapper
monitorCount={monitorsSummary?.totalMonitors} networkError={networkError}
/> isLoading={isLoading}
<Filter items={monitors}
selectedTypes={selectedTypes} type="uptimeMonitor"
setSelectedTypes={setSelectedTypes} fallbackLink="/uptime/create"
selectedStatus={selectedStatus} >
setSelectedStatus={setSelectedStatus} <Stack
selectedState={selectedState} className="monitors"
setSelectedState={setSelectedState} gap={theme.spacing(10)}
setToFilterStatus={setToFilterStatus} >
setToFilterActive={setToFilterActive} <Breadcrumbs list={BREADCRUMBS} />
handleReset={handleReset} <CreateMonitorHeader
/> isAdmin={isAdmin}
<SearchComponent isLoading={isLoading}
monitors={monitors} path="/uptime/create"
onSearchChange={setSearch} bulkPath="/uptime/bulk-import"
setIsSearching={setIsSearching} />
/> <Greeting type="uptime" />
</Stack> <StatusBoxes
<UptimeDataTable monitorsSummary={monitorsSummary}
isAdmin={isAdmin} shouldRender={!monitorsWithSummaryIsLoading}
isSearching={isSearching} />
filteredMonitors={monitorsWithChecks}
sort={sort} <Stack direction={"row"}>
setSort={setSort} <MonitorCountHeader
monitorsAreLoading={monitorsWithChecksIsLoading} isLoading={monitorsWithSummaryIsLoading}
triggerUpdate={triggerUpdate} monitorCount={monitorsSummary?.totalMonitors}
/> />
<Pagination <Filter
itemCount={monitorsWithChecksCount} selectedTypes={selectedTypes}
paginationLabel="monitors" setSelectedTypes={setSelectedTypes}
page={page} selectedStatus={selectedStatus}
rowsPerPage={rowsPerPage} setSelectedStatus={setSelectedStatus}
handleChangePage={handleChangePage} selectedState={selectedState}
handleChangeRowsPerPage={handleChangeRowsPerPage} setSelectedState={setSelectedState}
/> setToFilterStatus={setToFilterStatus}
</Stack> setToFilterActive={setToFilterActive}
handleReset={handleReset}
/>
<SearchComponent
monitors={monitors}
onSearchChange={setSearch}
setIsSearching={setIsSearching}
/>
</Stack>
<UptimeDataTable
isAdmin={isAdmin}
isSearching={isSearching}
filteredMonitors={monitorsWithChecks}
sort={sort}
setSort={setSort}
monitorsAreLoading={monitorsWithChecksIsLoading}
triggerUpdate={triggerUpdate}
/>
<Pagination
itemCount={monitorsWithChecksCount}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
</PageStateWrapper>
</>
); );
}; };
+10
View File
@@ -437,6 +437,16 @@ const infrastructureMonitorValidation = joi.object({
"number.base": "Frequency must be a number.", "number.base": "Frequency must be a number.",
"any.required": "Frequency is required.", "any.required": "Frequency is required.",
}), }),
statusWindowSize: joi.number().min(1).max(20).messages({
"number.base": "Status window size must be a number.",
"number.min": "Status window size must be at least 1.",
"number.max": "Status window size cannot exceed 20.",
}),
statusWindowThreshold: joi.number().min(1).max(100).messages({
"number.base": "Status window threshold must be a number.",
"number.min": "Status window threshold must be at least 1%.",
"number.max": "Status window threshold cannot exceed 100%.",
}),
notifications: joi.array().items(joi.string()), notifications: joi.array().items(joi.string()),
}); });
+48 -2
View File
@@ -376,7 +376,52 @@
"email": "", "email": "",
"role": "", "role": "",
"created": "" "created": ""
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "", "paused": "",
@@ -742,7 +787,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "", "lastFinishedAtHeader": "",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
+48 -2
View File
@@ -376,7 +376,52 @@
"email": "", "email": "",
"role": "", "role": "",
"created": "" "created": ""
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "", "paused": "",
@@ -742,7 +787,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "", "lastFinishedAtHeader": "",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
+48 -2
View File
@@ -380,7 +380,52 @@
"email": "Email", "email": "Email",
"role": "Rolle", "role": "Rolle",
"created": "Angelegt" "created": "Angelegt"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "Pausiert", "paused": "Pausiert",
@@ -746,7 +791,8 @@
"failCountHeader": "Anzahl Fehler", "failCountHeader": "Anzahl Fehler",
"lastRunHeader": "Letzter Lauf um", "lastRunHeader": "Letzter Lauf um",
"lastFinishedAtHeader": "Zuletzt abgeschlossen um", "lastFinishedAtHeader": "Zuletzt abgeschlossen um",
"lastRunTookHeader": "Letzter Lauf dauerte" "lastRunTookHeader": "Letzter Lauf dauerte",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "Warteschlangenmetriken", "title": "Warteschlangenmetriken",
+15 -15
View File
@@ -360,12 +360,6 @@
"demoUser": "Demo user" "demoUser": "Demo user"
}, },
"teamPanel": { "teamPanel": {
"addTeamMember": {
"addMemberMenu": "Add Team Member",
"title": "Register new team member",
"description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.",
"addButton": "Add Member"
},
"teamMembers": "Team members", "teamMembers": "Team members",
"filter": { "filter": {
"all": "All", "all": "All",
@@ -381,6 +375,18 @@
"noMembers": "There are no team members with this role", "noMembers": "There are no team members with this role",
"getToken": "Get token", "getToken": "Get token",
"emailToken": "E-mail token", "emailToken": "E-mail token",
"table": {
"name": "Name",
"email": "Email",
"role": "Role",
"created": "Created"
},
"addTeamMember": {
"addMemberMenu": "Add Team Member",
"title": "Register new team member",
"description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.",
"addButton": "Add Member"
},
"register": "Register a team member", "register": "Register a team member",
"registerToast": { "registerToast": {
"success": "User created, share credentials with the member securely.", "success": "User created, share credentials with the member securely.",
@@ -419,13 +425,7 @@
} }
} }
}, },
"role": "Role", "role": "Role"
"table": {
"name": "Name",
"email": "Email",
"role": "Role",
"created": "Created"
}
}, },
"monitorState": { "monitorState": {
"paused": "Paused", "paused": "Paused",
@@ -785,14 +785,14 @@
"idHeader": "Monitor ID", "idHeader": "Monitor ID",
"urlHeader": "URL", "urlHeader": "URL",
"typeHeader": "Type", "typeHeader": "Type",
"intervalHeader": "Interval",
"activeHeader": "Active", "activeHeader": "Active",
"lockedAtHeader": "Locked at", "lockedAtHeader": "Locked at",
"runCountHeader": "Run count", "runCountHeader": "Run count",
"failCountHeader": "Fail count", "failCountHeader": "Fail count",
"lastRunHeader": "Last run at", "lastRunHeader": "Last run at",
"lastFinishedAtHeader": "Last finished at", "lastFinishedAtHeader": "Last finished at",
"lastRunTookHeader": "Last run took" "lastRunTookHeader": "Last run took",
"intervalHeader": "Interval"
}, },
"metricsTable": { "metricsTable": {
"title": "Queue metrics", "title": "Queue metrics",
+48 -2
View File
@@ -376,7 +376,52 @@
"email": "", "email": "",
"role": "", "role": "",
"created": "" "created": ""
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "", "paused": "",
@@ -742,7 +787,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "", "lastFinishedAtHeader": "",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
+48 -2
View File
@@ -376,7 +376,52 @@
"email": "Sähköposti", "email": "Sähköposti",
"role": "Rooli", "role": "Rooli",
"created": "Luotu" "created": "Luotu"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "Tauota", "paused": "Tauota",
@@ -742,7 +787,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "", "lastFinishedAtHeader": "",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
+48 -2
View File
@@ -380,7 +380,52 @@
"email": "Email", "email": "Email",
"role": "Rôle", "role": "Rôle",
"created": "Créée" "created": "Créée"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "Pause", "paused": "Pause",
@@ -746,7 +791,8 @@
"failCountHeader": "Nombre d'échecs", "failCountHeader": "Nombre d'échecs",
"lastRunHeader": "Dernière tentative le", "lastRunHeader": "Dernière tentative le",
"lastFinishedAtHeader": "Dernière finalisation à", "lastFinishedAtHeader": "Dernière finalisation à",
"lastRunTookHeader": "Dernière exécution effectuée" "lastRunTookHeader": "Dernière exécution effectuée",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "Métriques des files", "title": "Métriques des files",
+48 -2
View File
@@ -380,7 +380,52 @@
"email": "メール", "email": "メール",
"role": "役割", "role": "役割",
"created": "作成日" "created": "作成日"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "一時停止", "paused": "一時停止",
@@ -746,7 +791,8 @@
"failCountHeader": "失敗回数", "failCountHeader": "失敗回数",
"lastRunHeader": "最終実行日時", "lastRunHeader": "最終実行日時",
"lastFinishedAtHeader": "最終完了日時", "lastFinishedAtHeader": "最終完了日時",
"lastRunTookHeader": "最終実行時間" "lastRunTookHeader": "最終実行時間",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "キューメトリック", "title": "キューメトリック",
+48 -2
View File
@@ -380,7 +380,52 @@
"email": "E-mail", "email": "E-mail",
"role": "Cargo", "role": "Cargo",
"created": "Criado" "created": "Criado"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "Pausado", "paused": "Pausado",
@@ -746,7 +791,8 @@
"failCountHeader": "Contagem de falhas", "failCountHeader": "Contagem de falhas",
"lastRunHeader": "Última corrida em", "lastRunHeader": "Última corrida em",
"lastFinishedAtHeader": "Última conclusão em", "lastFinishedAtHeader": "Última conclusão em",
"lastRunTookHeader": "A última corrida aconteceu" "lastRunTookHeader": "A última corrida aconteceu",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "Métricas de fila", "title": "Métricas de fila",
+111 -65
View File
@@ -376,7 +376,52 @@
"email": "Email", "email": "Email",
"role": "Роль", "role": "Роль",
"created": "Создано" "created": "Создано"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "Остановлено", "paused": "Остановлено",
@@ -421,23 +466,23 @@
"passwordRequirements": "Новый пароль должен содержать не менее 8 символов и содержать как минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ.", "passwordRequirements": "Новый пароль должен содержать не менее 8 символов и содержать как минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ.",
"saving": "Сохранение..." "saving": "Сохранение..."
}, },
"emailSent": "", "emailSent": "Письмо успешно отправлено",
"failedToSendEmail": "", "failedToSendEmail": "Не удалось отправить письмо",
"settingsTestEmailSuccess": "", "settingsTestEmailSuccess": "Тестовое письмо успешно отправлено",
"settingsTestEmailFailed": "", "settingsTestEmailFailed": "Не удалось отправить тестовое письмо",
"settingsTestEmailFailedWithReason": "", "settingsTestEmailFailedWithReason": "Не удалось отправить тестовое письмо: {{reason}}",
"settingsTestEmailUnknownError": "Неизвестная ошибка", "settingsTestEmailUnknownError": "Неизвестная ошибка",
"statusMsg": { "statusMsg": {
"paused": "", "paused": "Мониторинг приостановлен",
"up": "", "up": "",
"down": "", "down": "",
"pending": "" "pending": "Ожидание..."
}, },
"uptimeGeneralInstructions": { "uptimeGeneralInstructions": {
"http": "", "http": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное имя, которое будет отображаться на панели инструментов.",
"ping": "", "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.",
"docker": "", "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect <short_id>, чтобы получить полный ID контейнера.",
"port": "", "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.",
"game": "" "game": ""
}, },
"common": { "common": {
@@ -447,24 +492,24 @@
"toggleTheme": "" "toggleTheme": ""
}, },
"toasts": { "toasts": {
"networkError": "", "networkError": "Ошибка сети",
"checkConnection": "", "checkConnection": "Пожалуйста, проверьте свое сетевое соединение",
"unknownError": "Неизвестная ошибка" "unknownError": "Неизвестная ошибка"
} }
}, },
"auth": { "auth": {
"common": { "common": {
"navigation": { "navigation": {
"continue": "", "continue": "Продолжить",
"back": "Назад" "back": "Назад"
}, },
"inputs": { "inputs": {
"email": { "email": {
"label": "", "label": "Электронная почта",
"placeholder": "", "placeholder": "",
"errors": { "errors": {
"empty": "Для продолжения введите свой адрес электронной почты", "empty": "Для продолжения введите свой адрес электронной почты",
"invalid": "" "invalid": "Пожалуйста, проверьте правильность введённого адреса электронной почты"
} }
}, },
"password": { "password": {
@@ -491,49 +536,49 @@
"highlighted": "одну прописную букву" "highlighted": "одну прописную букву"
}, },
"match": { "match": {
"beginning": "", "beginning": "Пароли",
"highlighted": "" "highlighted": "должны совпадать"
} }
}, },
"errors": { "errors": {
"empty": "", "empty": "Пожалуйста, введите пароль",
"length": "", "length": "Пароль должен быть длиной не менее 8 символов",
"uppercase": "", "uppercase": "Пароль должен содержать минимум 1 прописную букву",
"lowercase": "", "lowercase": "Пароль должен содержать минимум 1 строчную букву",
"number": "", "number": "Пароль должен содержать минимум 1 цифру",
"special": "", "special": "Пароль должен содержать минимум 1 спецсимвол",
"incorrect": "" "incorrect": "Предоставленный вами пароль не соответствует нашим записям"
} }
}, },
"passwordConfirm": { "passwordConfirm": {
"label": "", "label": "Подтвердите пароль",
"placeholder": "", "placeholder": "Введите пароль еще раз для подтверждения",
"errors": { "errors": {
"empty": "", "empty": "Пожалуйста, введите свой пароль еще раз для подтверждения (помогает избежать опечаток)",
"different": "" "different": "Введённые пароли не совпадают, вероятно, один из них напечатан с ошибкой"
} }
}, },
"firstName": { "firstName": {
"label": "", "label": "Имя",
"placeholder": "", "placeholder": "",
"errors": { "errors": {
"empty": "", "empty": "Пожалуйста, введите свое имя",
"length": "", "length": "Имя должно быть короче 50 символов",
"pattern": "" "pattern": "Имя должно содержать только буквы, пробелы, апострофы или дефисы"
} }
}, },
"lastName": { "lastName": {
"label": "", "label": "Фамилия",
"placeholder": "", "placeholder": "",
"errors": { "errors": {
"empty": "", "empty": "Пожалуйста, введите свою фамилию",
"length": "", "length": "Фамилия должна быть короче 50 символов",
"pattern": "" "pattern": "Фамилия должна содержать только буквы, пробелы, апострофы или дефисы"
} }
} }
}, },
"errors": { "errors": {
"validation": "" "validation": "Ошибка при проверке данных."
}, },
"fields": { "fields": {
"password": { "password": {
@@ -549,20 +594,20 @@
} }
}, },
"login": { "login": {
"heading": "", "heading": "Войдите, чтобы продолжить",
"subheadings": { "subheadings": {
"stepOne": "", "stepOne": "Введите свою электронную почту",
"stepTwo": "" "stepTwo": "Введите свой пароль"
}, },
"links": { "links": {
"forgotPassword": "", "forgotPassword": "Забыли пароль?",
"register": "", "register": "У вас нет учетной записи?",
"forgotPasswordLink": "", "forgotPasswordLink": "",
"registerLink": "" "registerLink": ""
}, },
"toasts": { "toasts": {
"success": "", "success": "С возвращением! Вы успешно вошли в систему.",
"incorrectPassword": "" "incorrectPassword": "Неверный пароль"
}, },
"errors": { "errors": {
"password": { "password": {
@@ -573,42 +618,42 @@
}, },
"registration": { "registration": {
"heading": { "heading": {
"superAdmin": "", "superAdmin": "Создать супер-администратора",
"user": "" "user": "Зарегистрироваться"
}, },
"subheadings": { "subheadings": {
"stepOne": "", "stepOne": "Введите свои личные данные",
"stepTwo": "", "stepTwo": "Введите вашу электронную почту",
"stepThree": "" "stepThree": "Создайте свой пароль"
}, },
"description": { "description": {
"superAdmin": "", "superAdmin": "Создайте свой аккаунт супер-администратора, чтобы начать",
"user": "" "user": "Зарегистрируйтесь как пользователь и обратитесь к супер-администратору для доступа к вашим мониторам"
}, },
"gettingStartedButton": { "gettingStartedButton": {
"superAdmin": "", "superAdmin": "Создать учетную запись супер-администратора",
"user": "" "user": "Зарегистрироваться как обычный пользователь"
}, },
"termsAndPolicies": "", "termsAndPolicies": "Создавая учетную запись, вы соглашаетесь с нашими <a1>Условиями предоставления услуг</a1> и <a2>Политикой конфиденциальности</a2>.",
"links": { "links": {
"login": "" "login": "Уже есть учетная запись? <a>Войти</a>"
}, },
"toasts": { "toasts": {
"success": "" "success": "Добро пожаловать! Ваша учетная запись была создана успешно."
}, },
"welcome": "" "welcome": ""
}, },
"forgotPassword": { "forgotPassword": {
"heading": "", "heading": "Забыли пароль?",
"subheadings": { "subheadings": {
"stepOne": "", "stepOne": "Не волнуйтесь, мы вышлем вам инструкции для сброса.",
"stepTwo": "", "stepTwo": "Мы выслали ссылку для сброса пароля на <email/>",
"stepThree": "", "stepThree": "Ваш новый пароль должен отличаться от ваших предыдущих пароей.",
"stepFour": "" "stepFour": "Ваш пароль был успешно сброшен. Нажмите ниже, чтобы волшебным образом войти в систему."
}, },
"buttons": { "buttons": {
"openEmail": "", "openEmail": "",
"resetPassword": "" "resetPassword": "Сбросить пароль"
}, },
"imageAlts": { "imageAlts": {
"passwordKey": "", "passwordKey": "",
@@ -742,7 +787,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "", "lastFinishedAtHeader": "",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
File diff suppressed because it is too large. Load diff
+48 -2
View File
@@ -376,7 +376,52 @@
"email": "Eposta", "email": "Eposta",
"role": "Rol", "role": "Rol",
"created": "Oluşturulma" "created": "Oluşturulma"
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "Durduruldu", "paused": "Durduruldu",
@@ -742,7 +787,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "Son tamamlama", "lastFinishedAtHeader": "Son tamamlama",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
File diff suppressed because it is too large. Load diff
+48 -2
View File
@@ -380,7 +380,52 @@
"email": "", "email": "",
"role": "", "role": "",
"created": "" "created": ""
} },
"addTeamMember": {
"addMemberMenu": "",
"title": "",
"description": "",
"addButton": ""
},
"register": "",
"registerToast": {
"success": "",
"dbUserExists": "",
"unknownError": ""
},
"registerTeamMember": {
"title": "",
"auth": {
"common": {
"inputs": {
"firstName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"lastName": {
"errors": {
"empty": "",
"pattern": ""
}
},
"email": {
"errors": {
"empty": "",
"invalid": ""
}
},
"role": {
"errors": {
"empty": ""
}
}
}
}
}
},
"role": ""
}, },
"monitorState": { "monitorState": {
"paused": "", "paused": "",
@@ -746,7 +791,8 @@
"failCountHeader": "", "failCountHeader": "",
"lastRunHeader": "", "lastRunHeader": "",
"lastFinishedAtHeader": "", "lastFinishedAtHeader": "",
"lastRunTookHeader": "" "lastRunTookHeader": "",
"intervalHeader": ""
}, },
"metricsTable": { "metricsTable": {
"title": "", "title": "",
+1 -1
View File
@@ -5,7 +5,7 @@ import { execSync } from "child_process";
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), ""); const env = loadEnv(mode, process.cwd(), "");
let version = "3.1.5"; let version = "3.2.0";
return { return {
base: "/", base: "/",
+5
View File
@@ -1,5 +1,10 @@
FROM node:20-slim FROM node:20-slim
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY ../../package*.json ./ COPY ../../package*.json ./
+5
View File
@@ -1,5 +1,10 @@
FROM node:20-slim FROM node:20-slim
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY ./server/package*.json ./ COPY ./server/package*.json ./
+7
View File
@@ -3,6 +3,8 @@
# --------------------- # ---------------------
FROM node:24-slim AS frontend-build FROM node:24-slim AS frontend-build
WORKDIR /app/client WORKDIR /app/client
COPY client/package.json ./ COPY client/package.json ./
@@ -20,6 +22,11 @@ RUN npm run build
# --------------------- # ---------------------
FROM node:24-slim AS backend FROM node:24-slim AS backend
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app/server WORKDIR /app/server
COPY server/package.json ./ COPY server/package.json ./
+1
View File
@@ -1,6 +1,7 @@
services: services:
server: server:
image: ghcr.io/bluewave-labs/checkmate-backend-mono:latest image: ghcr.io/bluewave-labs/checkmate-backend-mono:latest
pull_policy: always
restart: always restart: always
ports: ports:
- "52345:52345" - "52345:52345"
+5
View File
@@ -10,6 +10,11 @@ RUN npm run build
FROM node:20-slim AS app FROM node:20-slim AS app
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app/server WORKDIR /app/server
COPY server ./ COPY server ./
+5
View File
@@ -1,5 +1,10 @@
FROM node:20-slim FROM node:20-slim
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY ./server/package*.json ./ COPY ./server/package*.json ./
+5
View File
@@ -1,5 +1,10 @@
FROM node:20-slim FROM node:20-slim
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_OPTIONS="--max-old-space-size=2048" ENV NODE_OPTIONS="--max-old-space-size=2048"
WORKDIR /app WORKDIR /app
+5
View File
@@ -1,5 +1,10 @@
FROM node:20-slim FROM node:20-slim
# Install ping
RUN apt-get update \
&& apt-get install -y iputils-ping \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_OPTIONS="--max-old-space-size=2048" ENV NODE_OPTIONS="--max-old-space-size=2048"
WORKDIR /app WORKDIR /app
@@ -162,6 +162,14 @@ class StatusPageModule {
as: "monitors.checks", as: "monitors.checks",
}, },
}, },
{
$lookup: {
from: "monitorstats",
localField: "monitors._id",
foreignField: "monitorId",
as: "monitors.stats",
},
},
{ {
$addFields: { $addFields: {
"monitors.orderIndex": { "monitors.orderIndex": {
@@ -181,6 +189,9 @@ class StatusPageModule {
}, },
}, },
}, },
"monitors.uptimePercentage": {
$arrayElemAt: ["$monitors.stats.uptimePercentage", 0],
},
}, },
}, },
{ $match: { "monitors.orderIndex": { $ne: -1 } } }, { $match: { "monitors.orderIndex": { $ne: -1 } } },
+16 -1
View File
@@ -149,10 +149,12 @@
"statusPageByTeamId": "Got status pages by team id successfully", "statusPageByTeamId": "Got status pages by team id successfully",
"httpNetworkError": "Network error", "httpNetworkError": "Network error",
"httpNotJson": "Response data is not json", "httpNotJson": "Response data is not json",
"httpExpectedValueFail": "Expected value did not match",
"httpJsonPathError": "Failed to parse json data", "httpJsonPathError": "Failed to parse json data",
"httpEmptyResult": "Result is empty", "httpEmptyResult": "Result is empty",
"httpMatchSuccess": "Response data match successfully", "httpMatchSuccess": "Response data match successfully",
"httpMatchFail": "Failed to match response data", "httpMatchFail": "Failed to match response data",
"httpJsonPathFail": "JSONPath did not match expected value",
"webhookSendSuccess": "Webhook notification sent successfully", "webhookSendSuccess": "Webhook notification sent successfully",
"telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId", "telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId",
"webhookUrlRequired": "Webhook URL is required", "webhookUrlRequired": "Webhook URL is required",
@@ -162,5 +164,18 @@
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n", "monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
"sendTestEmail": "Test email sent successfully", "sendTestEmail": "Test email sent successfully",
"errorForValidEmailAddress": "A valid recipient email address is required.", "errorForValidEmailAddress": "A valid recipient email address is required.",
"testEmailSubject": "Test E-mail from Checkmate" "testEmailSubject": "Test E-mail from Checkmate",
"discordNotification": {
"monitor": "Monitor",
"status": "Status",
"statusCode": "Status Code",
"time": "Time",
"up": "UP",
"down": "DOWN",
"checkmate": "Checkmate",
"url": "URL",
"unknown": "Unknown",
"uptimeAlert": "Uptime Alert: One of your monitors is back online",
"downtimeAlert": "Downtime Alert: One of your monitors went offline"
}
} }
@@ -28,11 +28,12 @@ class SuperSimpleQueueHelper {
return async (monitor) => { return async (monitor) => {
try { try {
const monitorId = monitor._id; const monitorId = monitor._id;
const teamId = monitor.teamId;
if (!monitorId) { if (!monitorId) {
throw new Error("No monitor id"); throw new Error("No monitor id");
} }
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId); const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) { if (maintenanceWindowActive) {
this.logger.info({ this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`, message: `Monitor ${monitorId} is in maintenance window`,
@@ -77,8 +78,11 @@ class SuperSimpleQueueHelper {
}; };
}; };
async isInMaintenanceWindow(monitorId) { async isInMaintenanceWindow(monitorId, teamId) {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId); const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({
monitorId: monitorId.toString(),
teamId: teamId.toString(),
});
// Check for active maintenance window: // Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) { if (window.active) {
@@ -1,5 +1,3 @@
import { match } from "assert";
const SERVICE_NAME = "NetworkService"; const SERVICE_NAME = "NetworkService";
class NetworkService { class NetworkService {
@@ -75,6 +73,9 @@ class NetworkService {
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url)); const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url));
if (!response) { if (!response) {
if (error) {
throw error;
}
throw new Error("Ping failed - no result returned"); throw new Error("Ping failed - no result returned");
} }
@@ -104,7 +105,7 @@ class NetworkService {
} }
async requestHttp(monitor) { async requestHttp(monitor) {
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = { const httpResponse = {
monitorId: _id, monitorId: _id,
teamId: teamId, teamId: teamId,
@@ -153,33 +154,61 @@ class NetworkService {
return httpResponse; return httpResponse;
} }
if (expectedValue) { if (expectedValue && !jsonPath) {
let ok = false; let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue; if (matchMethod === "equal") ok = payload === expectedValue;
if (matchMethod === "include") ok = payload.includes(expectedValue); if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
if (matchMethod === "regex") ok = new RegExp(expectedValue).test(payload); if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
if (ok === true) { if (ok === true) {
return httpResponse; return httpResponse;
} else { } else {
httpResponse.code = 500; httpResponse.code = 500;
httpResponse.status = false; httpResponse.status = false;
httpResponse.message = this.stringService.httpExpectedValueFail;
return httpResponse; return httpResponse;
} }
} }
if (jsonPath) { if (jsonPath) {
const contentType = response.headers["content-type"]; const contentType = response.headers["content-type"];
const isJson = contentType?.includes("application/json"); const isJson = contentType?.includes("application/json");
if (!isJson) { if (!isJson) {
httpResponse.status = false; httpResponse.status = false;
httpResponse.message = this.stringService.httpNotJson; httpResponse.message = this.stringService.httpNotJson;
return httpResponse; return httpResponse;
} }
try { try {
this.jmespath.search(payload, jsonPath); const extracted = this.jmespath.search(payload, jsonPath);
if (expectedValue) {
let ok = false;
if (matchMethod === "equal") ok = extracted === expectedValue;
if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
if (ok) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = this.stringService.httpJsonPathFail;
httpResponse.extracted = extracted;
return httpResponse;
}
} else {
const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
if (!isFalsey) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = this.stringService.httpJsonPathFail;
httpResponse.extracted = extracted;
return httpResponse;
}
}
} catch { } catch {
httpResponse.status = false; httpResponse.status = false;
httpResponse.message = this.stringService.httpJsonPathError; httpResponse.message = this.stringService.httpJsonPathError;
@@ -16,7 +16,7 @@ class NotificationService {
return NotificationService.SERVICE_NAME; return NotificationService.SERVICE_NAME;
} }
sendNotification = async ({ notification, subject, content, html }) => { sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => {
const { type, address } = notification; const { type, address } = notification;
if (type === "email") { if (type === "email") {
@@ -28,9 +28,11 @@ class NotificationService {
// Create a body for webhooks // Create a body for webhooks
let body = { text: content }; let body = { text: content };
if (type === "discord") { if (type === "discord") {
body = { content }; body = !discordContent ? { content } : discordContent;
}
if (type === "webhook") {
body = !webhookBody ? { content } : webhookBody;
} }
if (type === "slack" || type === "discord" || type === "webhook") { if (type === "slack" || type === "discord" || type === "webhook") {
const response = await this.networkService.requestWebhook(type, address, body); const response = await this.networkService.requestWebhook(type, address, body);
return response.status; return response.status;
@@ -55,7 +57,6 @@ class NotificationService {
const notificationIDs = networkResponse.monitor?.notifications ?? []; const notificationIDs = networkResponse.monitor?.notifications ?? [];
if (notificationIDs.length === 0) return false; if (notificationIDs.length === 0) return false;
if (networkResponse.monitor.type === "hardware") { if (networkResponse.monitor.type === "hardware") {
const thresholds = networkResponse?.monitor?.thresholds; const thresholds = networkResponse?.monitor?.thresholds;
@@ -63,30 +64,29 @@ class NotificationService {
const metrics = networkResponse?.payload?.data ?? null; const metrics = networkResponse?.payload?.data ?? null;
if (metrics === null) return false; // No metrics, we're done if (metrics === null) return false; // No metrics, we're done
const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse); const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse);
if (alerts.length === 0) return false; if (alerts.length === 0) return false;
const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts);
const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts); const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor);
const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor);
const success = await this.notifyAll({ notificationIDs, subject, html, content }); const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody });
return success; return success;
} }
// Status monitors // Status monitors
const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse);
const content = await this.notificationUtils.buildWebhookMessage(networkResponse); const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse);
const success = this.notifyAll({ notificationIDs, subject, html, content }); const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent });
return success; return success;
} }
async notifyAll({ notificationIDs, subject, html, content }) { async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) {
const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs);
// Map each notification to a test promise // Map each notification to a test promise
const promises = notifications.map(async (notification) => { const promises = notifications.map(async (notification) => {
try { try {
await this.sendNotification({ notification, subject, content, html }); await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody });
return true; return true;
} catch (err) { } catch (err) {
return false; return false;
@@ -70,7 +70,25 @@ class NotificationUtils {
.replace("{time}", formattedTime) .replace("{time}", formattedTime)
.replace("{code}", code || "Unknown"); .replace("{code}", code || "Unknown");
} }
return messageText; const dn = this.stringService.discordNotification;
let discordMessageText = {
embeds: [
{
title: status ? dn.uptimeAlert : dn.downtimeAlert,
color: status ? 5763719 : 15548997,
fields: [
{ name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true },
{ name: dn.status, value: status ? dn.up : dn.down, inline: true },
{ name: dn.statusCode, value: String(code ?? dn.unknown), inline: true },
{ name: dn.time, value: formattedTime, inline: true },
{ name: dn.url, value: monitor?.url ?? dn.unknown, inline: false },
],
footer: { text: dn.checkmate },
},
],
};
return [messageText, discordMessageText];
}; };
buildHardwareAlerts = async (networkResponse) => { buildHardwareAlerts = async (networkResponse) => {
@@ -88,6 +106,43 @@ class NotificationUtils {
}; };
const alertsToSend = []; const alertsToSend = [];
const discordEmbeds = [];
const monitorInfoFields = [
{ name: "Monitor", value: monitor.name, inline: true },
{ name: "URL", value: monitor.url, inline: false },
];
const formatDiscordAlert = {
cpu: () => ({
title: "CPU alert",
description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
color: 15548997,
fields: monitorInfoFields,
footer: { text: "Checkmate" },
}),
memory: () => ({
title: "Memory alert",
description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
color: 15548997,
fields: monitorInfoFields,
footer: { text: "Checkmate" },
}),
disk: () => ({
title: "Disk alert",
description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
color: 15548997,
footer: { text: "Checkmate" },
fields: [
...monitorInfoFields,
...(Array.isArray(disk) ? disk : []).map((d, idx) => ({
name: `Disk ${idx}`,
value: `${(d?.usage_percent * 100).toFixed(0)}%`,
inline: true,
})),
],
}),
};
const alertTypes = ["cpu", "memory", "disk"]; const alertTypes = ["cpu", "memory", "disk"];
for (const type of alertTypes) { for (const type of alertTypes) {
// Iterate over each alert type to see if any need to be decremented // Iterate over each alert type to see if any need to be decremented
@@ -108,11 +163,13 @@ class NotificationUtils {
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
}; };
alertsToSend.push(formatAlert[type]()); alertsToSend.push(formatAlert[type]());
discordEmbeds.push(formatDiscordAlert[type]());
} }
} }
} }
await monitor.save(); await monitor.save();
return alertsToSend; const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
return [alertsToSend, discordPayload];
}; };
buildHardwareEmail = async (networkResponse, alerts) => { buildHardwareEmail = async (networkResponse, alerts) => {
@@ -124,8 +181,15 @@ class NotificationUtils {
return { subject, html }; return { subject, html };
}; };
buildHardwareNotificationMessage = (alerts) => { buildHardwareNotificationMessage = (alerts, monitor) => {
return alerts.map((alert) => alert).join("\n"); const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`];
const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts] : [];
return alertText.map((alert) => alert).join("\n");
};
buildHardwareWebhookBody = (alerts, monitor) => {
const content = alerts.map((alert) => alert).join("\n");
const body = { text: content, name: monitor.name, url: monitor.url };
return body;
}; };
} }
@@ -216,6 +216,10 @@ class StringService {
return this.translationService.getTranslation("monitorDownAlert"); return this.translationService.getTranslation("monitorDownAlert");
} }
get discordNotification() {
return this.translationService.getTranslation("discordNotification");
}
getWebhookUnsupportedPlatform(platform) { getWebhookUnsupportedPlatform(platform) {
return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform); return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform);
} }