mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-05 01:10:36 -06:00
Merge branch 'develop' into fix/2336-settings-select-value-type
This commit is contained in:
16
client/src/Components/I18nLoader/index.jsx
Normal file
16
client/src/Components/I18nLoader/index.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import i18n from "../../Utils/i18n";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
const I18nLoader = () => {
|
||||
const language = useSelector((state) => state.ui.language);
|
||||
|
||||
useEffect(() => {
|
||||
if (language && i18n.language !== language) {
|
||||
i18n.changeLanguage(language);
|
||||
}
|
||||
}, [language]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default I18nLoader;
|
||||
@@ -164,7 +164,9 @@ Select.propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).isRequired,
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||
.isRequired,
|
||||
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
|
||||
@@ -3,15 +3,17 @@ import { Box, MenuItem, Select, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setLanguage } from "../Features/UI/uiSlice";
|
||||
|
||||
const LanguageSelector = () => {
|
||||
const { i18n } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const { language } = useSelector((state) => state.ui);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const handleChange = (event) => {
|
||||
const newLang = event.target.value;
|
||||
i18n.changeLanguage(newLang);
|
||||
dispatch(setLanguage(newLang));
|
||||
};
|
||||
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
@@ -32,7 +34,6 @@ const LanguageSelector = () => {
|
||||
}
|
||||
if (parsedLang.includes("-")) {
|
||||
parsedLang = parsedLang.split("-")[1].toLowerCase();
|
||||
console.log("parsedLang", parsedLang);
|
||||
}
|
||||
|
||||
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
|
||||
|
||||
@@ -5,7 +5,6 @@ const initialState = {
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
language: "gb",
|
||||
pagespeedApiKey: "",
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ const initialState = {
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
distributedUptimeEnabled: false,
|
||||
language: "gb",
|
||||
language: "en",
|
||||
starPromptOpen: true,
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ const uiSlice = createSlice({
|
||||
setTimezone(state, action) {
|
||||
state.timezone = action.payload.timezone;
|
||||
},
|
||||
setLanguage(state, action) {
|
||||
setLanguage: (state, action) => {
|
||||
state.language = action.payload;
|
||||
},
|
||||
setStarPromptOpen: (state, action) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
|
||||
<Select
|
||||
name="showURL"
|
||||
label={t("settingsURLSelectTitle")}
|
||||
value={showURL}
|
||||
value={showURL || ""}
|
||||
onChange={handleChange}
|
||||
items={[
|
||||
{ _id: true, name: t("settingsURLEnabled") },
|
||||
|
||||
@@ -93,7 +93,6 @@ const Settings = () => {
|
||||
|
||||
if (name === "language") {
|
||||
dispatch(setLanguage(value));
|
||||
i18n.changeLanguage(value);
|
||||
}
|
||||
|
||||
if (name === "deleteStats") {
|
||||
|
||||
@@ -17,14 +17,17 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
|
||||
>
|
||||
<StatusBox
|
||||
title={t("monitorStatus.up")}
|
||||
status="up"
|
||||
value={monitorsSummary?.upMonitors ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("monitorStatus.down")}
|
||||
status="down"
|
||||
value={monitorsSummary?.downMonitors ?? 0}
|
||||
/>
|
||||
<StatusBox
|
||||
title={t("monitorStatus.paused")}
|
||||
status="paused"
|
||||
value={monitorsSummary?.pausedMonitors ?? 0}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
@@ -5,9 +5,8 @@ import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react";
|
||||
import Background from "../../../../../assets/Images/background-grid.svg?react";
|
||||
import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react";
|
||||
|
||||
const StatusBox = ({ title, value }) => {
|
||||
const StatusBox = ({ title, value, status }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
let sharedStyles = {
|
||||
position: "absolute",
|
||||
right: 8,
|
||||
@@ -17,21 +16,21 @@ const StatusBox = ({ title, value }) => {
|
||||
|
||||
let color;
|
||||
let icon;
|
||||
if (title === "up") {
|
||||
if (status === "up") {
|
||||
color = theme.palette.success.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 8 }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "down") {
|
||||
} else if (status === "down") {
|
||||
color = theme.palette.error.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, transform: "rotate(180deg)", top: 5 }}>
|
||||
<Arrow />
|
||||
</Box>
|
||||
);
|
||||
} else if (title === "paused") {
|
||||
} else if (status === "paused") {
|
||||
color = theme.palette.warning.lowContrast;
|
||||
icon = (
|
||||
<Box sx={{ ...sharedStyles, top: 12, right: 12 }}>
|
||||
@@ -100,8 +99,9 @@ const StatusBox = ({ title, value }) => {
|
||||
};
|
||||
|
||||
StatusBox.propTypes = {
|
||||
title: PropTypes.oneOf(["up", "down", "paused"]).isRequired,
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
status: PropTypes.string,
|
||||
};
|
||||
|
||||
export default StatusBox;
|
||||
|
||||
@@ -4,7 +4,6 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
|
||||
import { clearAuthState } from "../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
|
||||
class NetworkService {
|
||||
constructor(store, dispatch, navigate) {
|
||||
this.store = store;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { setLanguage } from "../Features/UI/uiSlice";
|
||||
import store from "../store";
|
||||
|
||||
const primaryLanguage = "en";
|
||||
|
||||
// Load all translation files eagerly
|
||||
const translations = import.meta.glob("../locales/*.json", { eager: true });
|
||||
|
||||
const resources = {};
|
||||
@@ -15,12 +14,9 @@ Object.keys(translations).forEach((path) => {
|
||||
};
|
||||
});
|
||||
|
||||
const savedLanguage = store.getState()?.ui?.language;
|
||||
const initialLanguage = savedLanguage;
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: initialLanguage,
|
||||
lng: primaryLanguage,
|
||||
fallbackLng: primaryLanguage,
|
||||
debug: import.meta.env.MODE === "development",
|
||||
ns: ["translation"],
|
||||
@@ -30,8 +26,4 @@ i18n.use(initReactI18next).init({
|
||||
},
|
||||
});
|
||||
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
store.dispatch(setLanguage(lng));
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -277,6 +277,7 @@ const settingsValidation = joi.object({
|
||||
}),
|
||||
pagespeedApiKey: joi.string().allow("").optional(),
|
||||
language: joi.string().required(),
|
||||
timezone: joi.string().allow("").optional(),
|
||||
systemEmailHost: joi.string().allow(""),
|
||||
systemEmailPort: joi.number().allow(null, ""),
|
||||
systemEmailAddress: joi.string().allow(""),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App.jsx";
|
||||
import "./index.css";
|
||||
import "./Utils/i18n";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import { persistor, store } from "./store";
|
||||
@@ -9,6 +8,7 @@ import { PersistGate } from "redux-persist/integration/react";
|
||||
import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx";
|
||||
import { networkService } from "./Utils/NetworkService";
|
||||
export { networkService };
|
||||
import I18nLoader from "./Components/I18nLoader";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<Provider store={store}>
|
||||
@@ -16,6 +16,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
loading={null}
|
||||
persistor={persistor}
|
||||
>
|
||||
<I18nLoader />
|
||||
<Router>
|
||||
<NetworkServiceProvider>
|
||||
<App />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
const SERVICE_NAME = "NotificationService";
|
||||
const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot";
|
||||
const PLATFORM_TYPES = ["telegram", "slack", "discord"];
|
||||
const PLATFORM_TYPES = ["telegram", "slack", "discord", "webhook"];
|
||||
|
||||
const MESSAGE_FORMATTERS = {
|
||||
telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }),
|
||||
slack: (messageText) => ({ text: messageText }),
|
||||
discord: (messageText) => ({ content: messageText }),
|
||||
webhook: (messageText) => ({ text: messageText }),
|
||||
};
|
||||
|
||||
class NotificationService {
|
||||
@@ -42,49 +43,57 @@ class NotificationService {
|
||||
formatNotificationMessage(monitor, status, platform, chatId, code, timestamp) {
|
||||
// Format timestamp using the local system timezone
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Get timezone abbreviation and format the date
|
||||
const timeZoneAbbr = date.toLocaleTimeString('en-US', { timeZoneName: 'short' })
|
||||
.split(' ').pop();
|
||||
|
||||
// Format the date with readable format
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}).replace(/(\d+)\/(\d+)\/(\d+),\s/, '$3-$1-$2 ') + ' ' + timeZoneAbbr;
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Get timezone abbreviation and format the date
|
||||
const timeZoneAbbr = date
|
||||
.toLocaleTimeString("en-US", { timeZoneName: "short" })
|
||||
.split(" ")
|
||||
.pop();
|
||||
|
||||
// Format the date with readable format
|
||||
return (
|
||||
date
|
||||
.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
.replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
|
||||
" " +
|
||||
timeZoneAbbr
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Get formatted time
|
||||
const formattedTime = timestamp
|
||||
? formatTime(timestamp)
|
||||
: formatTime(new Date().getTime());
|
||||
|
||||
const formattedTime = timestamp
|
||||
? formatTime(timestamp)
|
||||
: formatTime(new Date().getTime());
|
||||
|
||||
// Create different messages based on status with extra spacing
|
||||
let messageText;
|
||||
if (status === true) {
|
||||
messageText = this.stringService.monitorUpAlert
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || 'Unknown');
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || "Unknown");
|
||||
} else {
|
||||
messageText = this.stringService.monitorDownAlert
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || 'Unknown');
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || "Unknown");
|
||||
}
|
||||
|
||||
|
||||
if (!PLATFORM_TYPES.includes(platform)) {
|
||||
return undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
return MESSAGE_FORMATTERS[platform](messageText, chatId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a webhook notification to a specified platform.
|
||||
@@ -105,56 +114,56 @@ class NotificationService {
|
||||
const { monitor, status, code } = networkResponse;
|
||||
const { platform } = notification;
|
||||
const { webhookUrl, botToken, chatId } = notification.config;
|
||||
|
||||
|
||||
// Early return if platform is not supported
|
||||
if (!PLATFORM_TYPES.includes(platform)) {
|
||||
this.logger.warn({
|
||||
message: this.stringService.getWebhookUnsupportedPlatform(platform),
|
||||
service: this.SERVICE_NAME,
|
||||
method: "sendWebhookNotification",
|
||||
details: { platform }
|
||||
});
|
||||
return false;
|
||||
this.logger.warn({
|
||||
message: this.stringService.getWebhookUnsupportedPlatform(platform),
|
||||
service: this.SERVICE_NAME,
|
||||
method: "sendWebhookNotification",
|
||||
details: { platform },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Early return for telegram if required fields are missing
|
||||
if (platform === "telegram" && (!botToken || !chatId)) {
|
||||
this.logger.warn({
|
||||
message: "Missing required fields for Telegram notification",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "sendWebhookNotification",
|
||||
details: { platform }
|
||||
});
|
||||
return false;
|
||||
this.logger.warn({
|
||||
message: "Missing required fields for Telegram notification",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "sendWebhookNotification",
|
||||
details: { platform },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
let url = webhookUrl;
|
||||
if (platform === "telegram") {
|
||||
url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
|
||||
url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
|
||||
}
|
||||
|
||||
|
||||
const message = this.formatNotificationMessage(
|
||||
monitor,
|
||||
status,
|
||||
platform,
|
||||
chatId,
|
||||
code, // Pass the code field directly
|
||||
networkResponse.timestamp
|
||||
monitor,
|
||||
status,
|
||||
platform,
|
||||
chatId,
|
||||
code, // Pass the code field directly
|
||||
networkResponse.timestamp
|
||||
);
|
||||
|
||||
|
||||
try {
|
||||
const response = await this.networkService.requestWebhook(platform, url, message);
|
||||
return response.status;
|
||||
const response = await this.networkService.requestWebhook(platform, url, message);
|
||||
return response.status;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: this.stringService.getWebhookSendError(platform),
|
||||
service: this.SERVICE_NAME,
|
||||
method: "sendWebhookNotification",
|
||||
stack: error.stack,
|
||||
});
|
||||
return false;
|
||||
this.logger.error({
|
||||
message: this.stringService.getWebhookSendError(platform),
|
||||
service: this.SERVICE_NAME,
|
||||
method: "sendWebhookNotification",
|
||||
stack: error.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email notification for hardware infrastructure alerts
|
||||
@@ -197,33 +206,33 @@ class NotificationService {
|
||||
|
||||
async handleStatusNotifications(networkResponse) {
|
||||
try {
|
||||
// If status hasn't changed, we're done
|
||||
if (networkResponse.statusChanged === false) return false;
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (networkResponse.prevStatus === undefined) return false;
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
networkResponse.monitorId
|
||||
);
|
||||
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await this.sendEmail(networkResponse, notification.address);
|
||||
} else if (notification.type === "webhook") {
|
||||
await this.sendWebhookNotification(networkResponse, notification);
|
||||
// If status hasn't changed, we're done
|
||||
if (networkResponse.statusChanged === false) return false;
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (networkResponse.prevStatus === undefined) return false;
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
networkResponse.monitorId
|
||||
);
|
||||
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await this.sendEmail(networkResponse, notification.address);
|
||||
} else if (notification.type === "webhook") {
|
||||
await this.sendWebhookNotification(networkResponse, notification);
|
||||
}
|
||||
// Handle other types of notifications here
|
||||
}
|
||||
// Handle other types of notifications here
|
||||
}
|
||||
return true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleNotifications",
|
||||
stack: error.stack,
|
||||
});
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleNotifications",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handles status change notifications for a monitor
|
||||
*
|
||||
@@ -257,7 +266,13 @@ class NotificationService {
|
||||
const alerts = {
|
||||
cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
|
||||
memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
|
||||
disk: disk?.some(d => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false,
|
||||
disk:
|
||||
disk?.some(
|
||||
(d) =>
|
||||
diskThreshold !== -1 &&
|
||||
typeof d?.usage_percent === "number" &&
|
||||
d?.usage_percent > diskThreshold
|
||||
) ?? false,
|
||||
};
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
|
||||
@@ -427,6 +427,7 @@ const updateAppSettingsBodyValidation = joi.object({
|
||||
checkTTL: joi.number().allow(""),
|
||||
pagespeedApiKey: joi.string().allow(""),
|
||||
language: joi.string().allow(""),
|
||||
timezone: joi.string().allow(""),
|
||||
// showURL: joi.bool().required(),
|
||||
systemEmailHost: joi.string().allow(""),
|
||||
systemEmailPort: joi.number().allow(""),
|
||||
|
||||
Reference in New Issue
Block a user