From 41170f16bc2f1a84f33d15ae26505d06a134d58d Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:49:01 -0500 Subject: [PATCH 01/69] Update CONTRIBUTING.md --- CONTRIBUTING.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d96c99d0b..8db035b8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,7 @@ Once it's filed: This section guides you through submitting an enhancement suggestion for the application, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community understand your enhancements and find related suggestions. - Make sure that you are using the latest version. -- Make sure you run the code locally. The Checkmate [documentation](https://bluewavelabs.gitbook.io/checkmate) covers it. +- Make sure you run the code locally. The Checkmate [documentation](https://docs.checkmate.so) covers it. - Find out if the functionality is already covered, maybe by an individual configuration. - Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. @@ -92,14 +92,7 @@ Enhancement suggestions are tracked as [GitHub issues](/issues). - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. - **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration. -We have a Figma file that includes: - -- All the dashboard elements and components -- The design guideline for the app - -You can see it [here](https://www.figma.com/design/RPSfaw66HjzSwzntKcgDUV/Uptime-Genie?node-id=0-1&t=WqOFv9jqNTFGItpL-1). Since it is read-only, we encourage you to copy to your own Figma page, then work on it. - -[This document](https://docs.google.com/document/d/1Gy3LiimGUNoSiWAMbwyK3SeMADcCMjCLu6cQYoawtSE/edit#heading=h.1lj2lgut6m7h) outlines the process every developer should follow for managing the issues lifecycle. Also make sure you read the [document about how to make a good pull request](/PULLREQUESTS.md). +Also make sure you read the [document about how to make a good pull request](/PULLREQUESTS.md). ## Attribution This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)! From 27ea9b2aedc011d51c00647dbb3214969f77b99c Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Fri, 9 May 2025 22:07:20 +1000 Subject: [PATCH 02/69] fix: button font size is now 13px --- client/src/Utils/Theme/globalTheme.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 6a6724150..13442ace1 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -147,6 +147,7 @@ const baseTheme = (palette) => ({ ], height: 34, fontWeight: 400, + fontSize: "var(--env-var-font-size-medium)", borderRadius: 4, boxShadow: "none", textTransform: "none", From 2ee381f0b7680c9f0e5d07e0c42adcd90e24abb6 Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Fri, 9 May 2025 22:12:50 +1000 Subject: [PATCH 03/69] fix: changed from space indentation to tabs --- client/src/Utils/Theme/globalTheme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 13442ace1..ca32a3ad6 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -147,7 +147,7 @@ const baseTheme = (palette) => ({ ], height: 34, fontWeight: 400, - fontSize: "var(--env-var-font-size-medium)", + fontSize: "var(--env-var-font-size-medium)", borderRadius: 4, boxShadow: "none", textTransform: "none", From 6304212645d232ae8f421fc2d9a03e51c81dc9fd Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Sat, 10 May 2025 02:31:16 +1000 Subject: [PATCH 04/69] fix: removed css reference, theme fontSize changed --- client/src/Utils/Theme/globalTheme.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index ca32a3ad6..f7feed7fd 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -9,7 +9,7 @@ const shadow = const baseTheme = (palette) => ({ typography: { fontFamily: fontFamilyPrimary, - fontSize: 14, + fontSize: typographyLevels.base, h1: { fontSize: typographyLevels.xl, color: palette.primary.contrastText, @@ -147,7 +147,6 @@ const baseTheme = (palette) => ({ ], height: 34, fontWeight: 400, - fontSize: "var(--env-var-font-size-medium)", borderRadius: 4, boxShadow: "none", textTransform: "none", From 3a574b2f4ed0f34c0c6ab126529faddf03f20e6c Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Fri, 9 May 2025 14:40:07 -0400 Subject: [PATCH 05/69] translate menu items --- client/src/Components/Sidebar/index.jsx | 44 ++++++++++++---------- client/src/Pages/Uptime/Monitors/index.jsx | 7 +++- client/src/locales/gb.json | 25 +++++++++++- client/src/locales/ru.json | 25 +++++++++++- client/src/locales/tr.json | 23 +++++++++++ 5 files changed, 101 insertions(+), 23 deletions(-) diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index 81edcd3cc..dd548476a 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/Sidebar/index.jsx @@ -47,46 +47,47 @@ import "./index.css"; import { useLocation, useNavigate } from "react-router"; import { useTheme } from "@emotion/react"; import { useDispatch, useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; import { clearAuthState } from "../../Features/Auth/authSlice"; import { toggleSidebar } from "../../Features/UI/uiSlice"; import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -const menu = [ - { name: "Uptime", path: "uptime", icon: }, - { name: "Pagespeed", path: "pagespeed", icon: }, - { name: "Infrastructure", path: "infrastructure", icon: }, +const getMenu = (t) => [ + { name: t("menu.uptime"), path: "uptime", icon: }, + { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + { name: t("menu.infrastructure"), path: "infrastructure", icon: }, { - name: "Distributed uptime", + name: t("menu.distributedUptime"), path: "distributed-uptime", icon: , }, - { name: "Incidents", path: "incidents", icon: }, + { name: t("menu.incidents"), path: "incidents", icon: }, - { name: "Status pages", path: "status", icon: }, - { name: "Maintenance", path: "maintenance", icon: }, - // { name: "Integrations", path: "integrations", icon: }, + { name: t("menu.statusPages"), path: "status", icon: }, + { name: t("menu.maintenance"), path: "maintenance", icon: }, + // { name: t("menu.integrations"), path: "integrations", icon: }, { - name: "Settings", + name: t("menu.settings"), icon: , path: "settings", }, ]; -const otherMenuItems = [ - { name: "Support", path: "support", icon: }, +const getOtherMenuItems = (t) => [ + { name: t("menu.support"), path: "support", icon: }, { - name: "Discussions", + name: t("menu.discussions"), path: "discussions", icon: , }, - { name: "Docs", path: "docs", icon: }, - { name: "Changelog", path: "changelog", icon: }, + { name: t("menu.docs"), path: "docs", icon: }, + { name: t("menu.changelog"), path: "changelog", icon: }, ]; -const accountMenuItems = [ - { name: "Profile", path: "account/profile", icon: }, - { name: "Password", path: "account/password", icon: }, - { name: "Team", path: "account/team", icon: }, +const getAccountMenuItems = (t) => [ + { name: t("menu.profile"), path: "account/profile", icon: }, + { name: t("menu.password"), path: "account/password", icon: }, + { name: t("menu.team"), path: "account/team", icon: }, ]; /* TODO this could be a key in nested Path would be the link */ @@ -118,7 +119,12 @@ function Sidebar() { const navigate = useNavigate(); const location = useLocation(); const dispatch = useDispatch(); + const { t } = useTranslation(); const authState = useSelector((state) => state.auth); + + const menu = getMenu(t); + const otherMenuItems = getOtherMenuItems(t); + const accountMenuItems = getAccountMenuItems(t); const collapsed = useSelector((state) => state.ui.sidebar.collapsed); const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false }); const [anchorEl, setAnchorEl] = useState(null); diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 60a308141..ce9123816 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -31,11 +31,11 @@ import PropTypes from "prop-types"; import useFetchMonitorsWithSummary from "../../../Hooks/useFetchMonitorsWithSummary"; import useFetchMonitorsWithChecks from "../../../Hooks/useFetchMonitorsWithChecks"; import { useTranslation } from "react-i18next"; -const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; const TYPES = ["http", "ping", "docker", "port"]; const CreateMonitorButton = ({ shouldRender }) => { // Utils const navigate = useNavigate(); + const { t } = useTranslation(); if (shouldRender === false) { return; } @@ -49,7 +49,7 @@ const CreateMonitorButton = ({ shouldRender }) => { navigate("/uptime/create"); }} > - Create new + {t("createNew")} ); @@ -78,10 +78,13 @@ const UptimeMonitors = () => { // Utils const theme = useTheme(); + const navigate = useNavigate(); const isAdmin = useIsAdmin(); const dispatch = useDispatch(); const { t } = useTranslation(); + const BREADCRUMBS = [{ name: t("menu.uptime"), path: "/uptime" }]; + // Handlers const handleChangePage = (event, newPage) => { setPage(newPage); diff --git a/client/src/locales/gb.json b/client/src/locales/gb.json index 49993eb89..dacf84eb8 100644 --- a/client/src/locales/gb.json +++ b/client/src/locales/gb.json @@ -439,6 +439,29 @@ "pageSpeedApiKeyFieldLabel": "PageSpeed API key", "pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.", "pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.", - "reset": "Reset" + "reset": "Reset", + "createNew": "Create new", + "monitorState": { + "paused": "paused", + "resumed": "resumed" + }, + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastructure", + "distributedUptime": "Distributed uptime", + "incidents": "Incidents", + "statusPages": "Status pages", + "maintenance": "Maintenance", + "integrations": "Integrations", + "settings": "Settings", + "support": "Support", + "discussions": "Discussions", + "docs": "Docs", + "changelog": "Changelog", + "profile": "Profile", + "password": "Password", + "team": "Team" + } } diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index 5536b5e45..a37923557 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -393,5 +393,28 @@ "maintenanceTableActionMenuDialogTitle": "", "pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.", "pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать", - "pageSpeedAddApiKey": "как добавить ваш ключ API." + "pageSpeedAddApiKey": "как добавить ваш ключ API.", + "createNew": "Создать новый", + "monitorState": { + "paused": "приостановлен", + "resumed": "возобновлен" + }, + "menu": { + "uptime": "Аптайм", + "pagespeed": "Скорость страницы", + "infrastructure": "Инфраструктура", + "distributedUptime": "Распределенный аптайм", + "incidents": "Инциденты", + "statusPages": "Страницы статуса", + "maintenance": "Обслуживание", + "integrations": "Интеграции", + "settings": "Настройки", + "support": "Поддержка", + "discussions": "Обсуждения", + "docs": "Документация", + "changelog": "История изменений", + "profile": "Профиль", + "password": "Пароль", + "team": "Команда" + } } \ No newline at end of file diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 5df644fdd..e7e95a339 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -427,5 +427,28 @@ "validationFailed": "", "noFileSelected": "", "fallbackPage": "" + }, + "createNew": "Yeni oluştur", + "monitorState": { + "paused": "duraklatıldı", + "resumed": "devam ettirildi" + }, + "menu": { + "uptime": "Çalışma Süresi", + "pagespeed": "Sayfa Hızı", + "infrastructure": "Altyapı", + "distributedUptime": "Dağıtılmış Çalışma Süresi", + "incidents": "Olaylar", + "statusPages": "Durum Sayfaları", + "maintenance": "Bakım", + "integrations": "Entegrasyonlar", + "settings": "Ayarlar", + "support": "Destek", + "discussions": "Tartışmalar", + "docs": "Belgeler", + "changelog": "Değişiklik Günlüğü", + "profile": "Profil", + "password": "Şifre", + "team": "Takım" } } From 13c4390f967512f7fccd39ee54d621d808136a58 Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Fri, 9 May 2025 14:58:13 -0400 Subject: [PATCH 06/69] fix: translate UI elements in menu and uptime page - Added translations for sidebar menu items in all languages - Fixed "Create new" button to use translation instead of hardcoded text - Added missing translations for "Bulk Import" in Russian and Turkish - Translated status box labels (UP, DOWN, PAUSED) in the uptime monitors page - Made greeting component fully translatable with proper context - Fixed lint warnings in components --- .../Components/MonitorCreateHeader/index.jsx | 6 ++-- .../Monitors/Components/StatusBoxes/index.jsx | 9 ++++-- client/src/Utils/greeting.jsx | 8 ++++-- client/src/locales/gb.json | 10 +++++++ client/src/locales/ru.json | 23 ++++++++++++++- client/src/locales/tr.json | 28 +++++++++++++------ 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx index 05a9b7f9b..15d6d5c0f 100644 --- a/client/src/Components/MonitorCreateHeader/index.jsx +++ b/client/src/Components/MonitorCreateHeader/index.jsx @@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react"; const CreateMonitorHeader = ({ isAdmin, - label = "Create new", + label, isLoading = true, path, bulkPath, @@ -14,6 +14,8 @@ const CreateMonitorHeader = ({ const navigate = useNavigate(); const { t } = useTranslation(); const theme = useTheme(); + + // Use the provided label or fall back to the translated default if (!isAdmin) return null; @@ -30,7 +32,7 @@ const CreateMonitorHeader = ({ color="accent" onClick={() => navigate(path)} > - {label} + {label || t("createNew")} {bulkPath && ( + + + + + {t("settingsSystemReset")} + + {t("settingsSystemResetDescription")} + + + + {t("settingsRemoveAllMonitors")} + + + setIsOpen(false)} + confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} + onConfirm={() => { + const syntheticEvent = { + target: { + name: "deleteMonitors", + }, + }; + handleChange(syntheticEvent); + setIsOpen(false); + }} + isLoading={isLoading || authIsLoading || checksIsLoading} + /> + + + ); +}; + +SettingsDemoMonitors.propTypes = { + isLoading: PropTypes.bool, + authIsLoading: PropTypes.bool, + checksIsLoading: PropTypes.bool, + handleChange: PropTypes.func, +}; + +export default SettingsDemoMonitors; diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx new file mode 100644 index 000000000..87f558f32 --- /dev/null +++ b/client/src/Pages/Settings/SettingsPagespeed.jsx @@ -0,0 +1,81 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; + +const SettingsPagespeed = ({ HEADING_SX, settings, setSettings }) => { + const { t } = useTranslation(); + const theme = useTheme(); + + // Local state + const [apiKey, setApiKey] = useState(""); + const [isApiKeySet, setIsApiKeySet] = useState(false); + const [hasBeenReset, setHasBeenReset] = useState(false); + + // Handler + const handleChange = (e) => { + setApiKey(e.target.value); + setSettings({ ...settings, pagespeedApiKey: e.target.value }); + }; + + useEffect(() => { + console.log(settings); + setIsApiKeySet(Boolean(settings.pagespeedApiKey) && apiKey === ""); + }, [settings, apiKey]); + + return ( + + + {t("pageSpeedApiKeyFieldTitle")} + {t("pageSpeedApiKeyFieldDescription")} + + + {(isApiKeySet === false || hasBeenReset === true) && ( + } + /> + )} + + {isApiKeySet === true && hasBeenReset === false && ( + + {t("pageSpeedApiKeyFieldResetLabel")} + + + )} + + + ); +}; + +SettingsPagespeed.propTypes = { + HEADING_SX: PropTypes.object, + settings: PropTypes.object, + setSettings: PropTypes.func, +}; + +export default SettingsPagespeed; diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx new file mode 100644 index 000000000..c9178b1da --- /dev/null +++ b/client/src/Pages/Settings/SettingsStats.jsx @@ -0,0 +1,84 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import Dialog from "../../Components/Dialog"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; + +const SettingsStats = ({ HEADING_SX, handleChange, settings, errors }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + return ( + + + + {t("settingsHistoryAndMonitoring")} + + + {t("settingsHistoryAndMonitoringDescription")} + + + + + + {t("settingsClearAllStats")} + + + + setIsOpen(false)} + confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} + onConfirm={() => { + const syntheticEvent = { + target: { + name: "deleteStats", + }, + }; + handleChange(syntheticEvent); + setIsOpen(false); + }} + isLoading={false} + /> + + ); +}; + +SettingsStats.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + settings: PropTypes.object, + errors: PropTypes.object, +}; + +export default SettingsStats; diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx new file mode 100644 index 000000000..e6ba1a77b --- /dev/null +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -0,0 +1,43 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import Select from "../../Components/Inputs/Select"; +import timezones from "../../Utils/timezones.json"; + +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useTranslation } from "react-i18next"; +const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + {t("settingsGeneralSettings")} + + {t("settingsDisplayTimezone")}-{" "} + {t("settingsDisplayTimezoneDescription")} + + + + + + + + ); +}; + +SettingsUI.propTypes = { + HEADING_SX: PropTypes.object, + handleChange: PropTypes.func, + mode: PropTypes.string, + language: PropTypes.string, +}; + +export default SettingsUI; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 79e5ea657..e1e8c3859 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -1,116 +1,67 @@ -// Components -import { Box, Stack, Typography, Button } from "@mui/material"; -import TextInput from "../../Components/Inputs/TextInput"; -import Link from "../../Components/Link"; -import Select from "../../Components/Inputs/Select"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; -import Dialog from "../../Components/Dialog"; -import ConfigBox from "../../Components/ConfigBox"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; -import { getAppSettings } from "../../Features/Settings/settingsSlice"; -// import { -// WalletMultiButton, -// WalletDisconnectButton, -// } from "@solana/wallet-adapter-react-ui"; - -//Utils -import { useTheme } from "@emotion/react"; -import { logger } from "../../Utils/Logger"; -import { useDispatch, useSelector } from "react-redux"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Breadcrumbs from "../../Components/Breadcrumbs"; +import SettingsTimeZone from "./SettingsTimeZone"; +import SettingsUI from "./SettingsUI"; +import SettingsPagespeed from "./SettingsPagespeed"; +import SettingsDemoMonitors from "./SettingsDemoMonitors"; +import SettingsAbout from "./SettingsAbout"; +import Button from "@mui/material/Button"; +// Utils +import { settingsValidation } from "../../Validation/validation"; import { createToast } from "../../Utils/toastUtils"; +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useSelector, useDispatch } from "react-redux"; +import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; +import { getAppSettings } from "../../Features/Settings/settingsSlice"; +import SettingsStats from "./SettingsStats"; import { deleteMonitorChecksByTeamId, addDemoMonitors, deleteAllMonitors, } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import { update } from "../../Features/Auth/authSlice"; -import PropTypes from "prop-types"; -import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice"; -import timezones from "../../Utils/timezones.json"; -import { useState, useEffect } from "react"; -import { networkService } from "../../main"; -import { settingsValidation } from "../../Validation/validation"; -import { updateAppSettings } from "../../Features/Settings/settingsSlice"; -import { useTranslation } from "react-i18next"; +import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings"; +import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats"; // Constants -const SECONDS_PER_DAY = 86400; +const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { - const theme = useTheme(); - const { t, i18n } = useTranslation(); - const isAdmin = useIsAdmin(); + // Redux state + const { mode, language, timezone } = useSelector((state) => state.ui); const { user } = useSelector((state) => state.auth); - const { language } = useSelector((state) => state.ui); - const { checkTTL } = user; - const { isLoading } = useSelector((state) => state.uptimeMonitors); - const { isLoading: authIsLoading } = useSelector((state) => state.auth); - const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui); - const { mode } = useSelector((state) => state.ui); - const { pagespeedApiKey } = useSelector((state) => state.settings); - const [checksIsLoading, setChecksIsLoading] = useState(false); - const [form, setForm] = useState({ - enableDistributedUptime: distributedUptimeEnabled, - ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0, - pagespeedApiKey: pagespeedApiKey, + + // Local state + const [settings, setSettings] = useState({}); + const [errors, setErrors] = useState({}); + + // Network + const [isSettingsLoading, settingsError] = useFetchSettings({ + settings, + setSettings, }); - const [version, setVersion] = useState("unknown"); - const [apiKeyFieldType, setApiKeyFieldType] = useState("password"); - const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false); - const [tempPSKey, setTempPSKey] = useState(""); - const [errors, setErrors] = useState({}); - const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false }; - const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState); + const [isSaving, saveError, saveSettings] = useSaveSettings(); + + const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats(); + + // Setup + const theme = useTheme(); + const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) }; + const { t, i18n } = useTranslation(); const dispatch = useDispatch(); - //Fetching latest release version from github - useEffect(() => { - const fetchLatestVersion = async () => { - let version = "unknown"; - try { - const response = await networkService.fetchGithubLatestRelease(); - if (!response.status === 200) { - throw new Error("Failed to fetch latest version"); - } - version = response.data.tag_name; - } catch (error) { - createToast({ body: error.message || "Error fetching latest version" }); // Set error message - } finally { - setVersion(version); - } - }; - fetchLatestVersion(); - }, []); + // Handlers + const handleChange = async (e) => { + const { name, value } = e.target; - useEffect(() => { - dispatch(getAppSettings()); - }, [dispatch]); - - const handleChange = (event) => { - const { type, checked, value, id } = event.target; - - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [id]: checked, - })); - return; - } - - if (id === "pagespeedApiKey") { - setTempPSKey(value); - return; - } - - let inputValue = value; - if (id === "ttl") { - inputValue = value.replace(/[^0-9]/g, ""); - } - - const updatedForm = { ...form, [id]: inputValue }; - const { error } = settingsValidation.validate(updatedForm, { abortEarly: false }); + // Build next state early + const newSettings = { ...settings, [name]: value }; + // Validate + const { error } = settingsValidation.validate(newSettings, { abortEarly: false }); if (!error || error.details.length === 0) { setErrors({}); } else { @@ -119,397 +70,122 @@ const Settings = () => { newErrors[err.path[0]] = err.message; }); setErrors(newErrors); - logger.error("Validation errors:", error.details); } - setForm(updatedForm); - }; - - // TODO Handle saving - const handleSave = async () => { - try { - setChecksIsLoading(true); - await networkService.updateChecksTTL({ - ttl: form.ttl, - }); - const updatedUser = { ...user, checkTTL: form.ttl }; - const [userAction, settingsAction] = await Promise.all([ - dispatch(update({ localData: updatedUser })), - dispatch( - updateAppSettings({ - settings: { - language: language, - pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey, - }, - }) - ), - ]); - - if (userAction.payload.success && settingsAction.payload.success) { - createToast({ body: t("settingsSuccessSaved") }); - } else { - throw new Error("Failed to save settings"); - } - } catch (error) { - createToast({ body: t("settingsFailedToSave") }); - } finally { - setChecksIsLoading(false); + if (name === "timezone") { + dispatch(setTimezone({ timezone: value })); } - }; - const handleClearStats = async () => { - try { - const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId })); - - if (deleteMonitorChecksByTeamId.fulfilled.match(action)) { - createToast({ body: t("settingsStatsCleared") }); - } else { - createToast({ body: t("settingsFailedToClearStats") }); - } - } catch (error) { - logger.error(error); - createToast({ body: t("settingsFailedToClearStats") }); - } finally { - setIsOpen(deleteStatsMonitorsInitState); + if (name === "mode") { + dispatch(setMode(value)); } - }; - const handleInsertDemoMonitors = async () => { - try { - const action = await dispatch(addDemoMonitors()); - if (addDemoMonitors.fulfilled.match(action)) { - createToast({ body: t("settingsDemoMonitorsAdded") }); - } else { + if (name === "language") { + dispatch(setLanguage(value)); + i18n.changeLanguage(value); + } + + if (name === "deleteStats") { + await deleteMonitorStats({ teamId: user.teamId }); + return; + } + + if (name === "demo") { + try { + const action = await dispatch(addDemoMonitors()); + if (addDemoMonitors.fulfilled.match(action)) { + createToast({ body: t("settingsDemoMonitorsAdded") }); + } else { + createToast({ body: t("settingsFailedToAddDemoMonitors") }); + } + } catch (error) { createToast({ body: t("settingsFailedToAddDemoMonitors") }); } - } catch (error) { - logger.error(error); - createToast({ Body: t("settingsFailedToAddDemoMonitors") }); + return; } - }; - const handleDeleteAllMonitors = async () => { - try { - const action = await dispatch(deleteAllMonitors()); - if (deleteAllMonitors.fulfilled.match(action)) { - createToast({ body: t("settingsMonitorsDeleted") }); - } else { + if (name === "deleteMonitors") { + try { + const action = await dispatch(deleteAllMonitors()); + if (deleteAllMonitors.fulfilled.match(action)) { + createToast({ body: t("settingsMonitorsDeleted") }); + } else { + createToast({ body: t("settingsFailedToDeleteMonitors") }); + } + } catch (error) { createToast({ body: t("settingsFailedToDeleteMonitors") }); } - } catch (error) { - logger.error(error); - createToast({ Body: t("settingsFailedToDeleteMonitors") }); - } finally { - setIsOpen(deleteStatsMonitorsInitState); + return; } + + setSettings(newSettings); }; - const handleResetApiKey = () => { - setIsApiKeySet(false); - setForm((prev) => ({ - ...prev, - pagespeedApiKey: "", - })); + const handleSave = () => { + const { error } = settingsValidation.validate(settings, { abortEarly: false }); + if (!error || error.details.length === 0) { + setErrors({}); + } else { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors(newErrors); + } + saveSettings(settings); }; - const languages = Object.keys(i18n.options.resources || {}); - return ( - + + + Settings + + + + + + - - - {t("settingsGeneralSettings")} - - {t("settingsDisplayTimezone")}-{" "} - {t("settingsDisplayTimezoneDescription")} - - - - { - dispatch(setMode(e.target.value)); - }} - items={[ - { _id: "light", name: "Light" }, - { _id: "dark", name: "Dark" }, - ]} - > - - - - {/* {isAdmin && ( - - - {t("settingsDistributedUptime")} - - {t("settingsDistributedUptimeDescription")} - - - - { - dispatch(setDistributedUptimeEnabled(e.target.checked)); - }} - /> - {distributedUptimeEnabled === true - ? t("settingsEnabled") - : t("settingsDisabled")} - - - )} */} - - - {t("pageSpeedApiKeyFieldTitle")} - - {t("pageSpeedApiKeyFieldDescription")} - - - - - ) - } - /> - {isApiKeySet && ( - - {t("pageSpeedApiKeyFieldResetLabel")} - - - )} - - - {/* {isAdmin && ( - - - {t("settingsWallet")} - - {t("settingsWalletDescription")} - - - - - - - - - - )} */} - {isAdmin && ( - - - {t("settingsHistoryAndMonitoring")} - - {t("settingsHistoryAndMonitoringDescription")} - - - - - - {t("settingsClearAllStats")} - - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")} - onConfirm={handleClearStats} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - )} - {isAdmin && ( - <> - {/* Demo Monitors Section */} - - - {t("settingsDemoMonitors")} - - {t("settingsDemoMonitorsDescription")} - - - - {t("settingsAddDemoMonitors")} - - - - - {/* System Reset Section */} - - - {t("settingsSystemReset")} - - {t("settingsSystemResetDescription")} - - - - {t("settingsRemoveAllMonitors")} - - - setIsOpen(deleteStatsMonitorsInitState)} - confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")} - onConfirm={handleDeleteAllMonitors} - isLoading={isLoading || authIsLoading || checksIsLoading} - /> - - - )} - - - - {t("settingsAbout")} - - - Checkmate {version} - - {t("settingsDevelopedBy")} - - - - - 0} + variant="contained" + color="accent" + sx={{ px: theme.spacing(12), mt: theme.spacing(20) }} + onClick={handleSave} > - - + {t("settingsSave")} + - + ); }; -Settings.propTypes = { - isAdmin: PropTypes.bool, -}; export default Settings; From 10d575a7ce1f2a5605e32387aceedcf0a49b9510 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 13:29:05 -0700 Subject: [PATCH 16/69] add hooks --- client/src/Hooks/useDeleteMonitorStats.js | 24 ++++++++++ client/src/Hooks/useFetchSettings.js | 55 +++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 client/src/Hooks/useDeleteMonitorStats.js create mode 100644 client/src/Hooks/useFetchSettings.js diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js new file mode 100644 index 000000000..64138929f --- /dev/null +++ b/client/src/Hooks/useDeleteMonitorStats.js @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const UseDeleteMonitorStats = () => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const deleteMonitorStats = async ({ teamId }) => { + setIsLoading(true); + try { + const res = await networkService.deleteChecksByTeamId({ teamId }); + createToast({ body: t("settingsStatsCleared") }); + } catch (error) { + createToast({ body: t("settingsFailedToClearStats") }); + } finally { + setIsLoading(false); + } + }; + + return [deleteMonitorStats, isLoading]; +}; + +export { UseDeleteMonitorStats }; diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/useFetchSettings.js new file mode 100644 index 000000000..15859795f --- /dev/null +++ b/client/src/Hooks/useFetchSettings.js @@ -0,0 +1,55 @@ +import { useState, useEffect } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTranslation } from "react-i18next"; + +const useFetchSettings = ({ setSettings }) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + useEffect(() => { + const fetchSettings = async () => { + setIsLoading(true); + try { + const response = await networkService.getAppSettings(); + setSettings(response?.data?.data); + } catch (error) { + createToast({ body: "Failed to fetch settings" }); + setError(error); + } finally { + setIsLoading(false); + } + }; + fetchSettings(); + }, []); + + return [isLoading, error]; +}; + +const useSaveSettings = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const { t } = useTranslation(); + + const saveSettings = async (settings) => { + setIsLoading(true); + try { + await networkService.updateAppSettings({ settings }); + if (settings.checkTTL) { + await networkService.updateChecksTTL({ + ttl: settings.checkTTL, + }); + } + createToast({ body: t("settingsSuccessSaved") }); + } catch (error) { + createToast({ body: t("settingsFailedToSave") }); + + setError(error); + } finally { + setIsLoading(false); + } + }; + + return [isLoading, error, saveSettings]; +}; + +export { useFetchSettings, useSaveSettings }; From f7d780a8be597a5408cc6c76ead859337efad3f9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:09 -0700 Subject: [PATCH 17/69] add email settings component --- client/src/Pages/Settings/SettingsEmail.jsx | 126 ++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 client/src/Pages/Settings/SettingsEmail.jsx diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx new file mode 100644 index 000000000..ecc2a51f2 --- /dev/null +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -0,0 +1,126 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import ConfigBox from "../../Components/ConfigBox"; +import TextInput from "../../Components/Inputs/TextInput"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +// Utils +import { useTheme } from "@emotion/react"; +import { PropTypes } from "prop-types"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +const SettingsEmail = ({ + HEADER_SX, + handleChange, + settingsData, + setSettingsData, + isPasswordSet, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [password, setPassword] = useState(""); + const [hasBeenReset, setHasBeenReset] = useState(false); + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + setSettingsData({ + ...settingsData, + settings: { ...settingsData.settings, systemEmailPassword: e.target.value }, + }); + }; + + return ( + + + {t("settingsEmail")} + {t("settingsEmailDescription")} + + + + + {t("settingsEmailHost")} + + + + {t("settingsEmailPort")} + + + + {t("settingsEmailUser")} + + + + {t("settingsEmailAddress")} + + + {(isPasswordSet === false || hasBeenReset === true) && ( + + {t("settingsEmailPassword")} + } + /> + + )} + {isPasswordSet === true && hasBeenReset === false && ( + + {t("settingsEmailFieldResetLabel")} + + + )} + + + + ); +}; + +SettingsEmail.propTypes = { + settingsData: PropTypes.object, + setSettingsData: PropTypes.func, + handleChange: PropTypes.func, + HEADER_SX: PropTypes.object, + isPasswordSet: PropTypes.bool, +}; + +export default SettingsEmail; From d0ec67236d304b53cfb10e5d5b1dc802a5f0febd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:24 -0700 Subject: [PATCH 18/69] allow systememail user --- server/validation/joi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server/validation/joi.js b/server/validation/joi.js index b28ed3850..997d35d65 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -431,6 +431,7 @@ const updateAppSettingsBodyValidation = joi.object({ systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), systemEmailPassword: joi.string().allow(""), + systemEmailUser: joi.string().allow(""), }); //**************************************** From 0a723572a2de8c57925263287f85e5c1d243880c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:52:44 -0700 Subject: [PATCH 19/69] move transporter creation --- server/service/emailService.js | 49 +++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/server/service/emailService.js b/server/service/emailService.js index c9e34eeda..57f8c78e6 100755 --- a/server/service/emailService.js +++ b/server/service/emailService.js @@ -29,7 +29,10 @@ class EmailService { this.mjml2html = mjml2html; this.nodemailer = nodemailer; this.logger = logger; + this.init(); + } + init = async () => { /** * Loads an email template from the filesystem. * @@ -67,34 +70,14 @@ class EmailService { serverIsUpTemplate: this.loadTemplate("serverIsUp"), passwordResetTemplate: this.loadTemplate("passwordReset"), hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"), - testEmailTemplate: this.loadTemplate("testEmailTemplate") + testEmailTemplate: this.loadTemplate("testEmailTemplate"), }; /** * The email transporter used to send emails. * @type {Object} */ - - const { - systemEmailHost, - systemEmailPort, - systemEmailUser, - systemEmailAddress, - systemEmailPassword, - } = this.settingsService.getSettings(); - - const emailConfig = { - host: systemEmailHost, - port: systemEmailPort, - secure: true, - auth: { - user: systemEmailUser || systemEmailAddress, - pass: systemEmailPassword, - }, - }; - - this.transporter = this.nodemailer.createTransport(emailConfig); - } + }; /** * Asynchronously builds and sends an email using a specified template and context. @@ -106,6 +89,28 @@ class EmailService { * @returns {Promise} A promise that resolves to the messageId of the sent email. */ buildAndSendEmail = async (template, context, to, subject) => { + // TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change + const { + systemEmailHost, + systemEmailPort, + systemEmailUser, + systemEmailAddress, + systemEmailPassword, + } = await this.settingsService.getDBSettings(); + + const emailConfig = { + host: systemEmailHost, + port: systemEmailPort, + secure: true, + auth: { + user: systemEmailUser || systemEmailAddress, + pass: systemEmailPassword, + }, + connectionTimeout: 5000, + }; + + this.transporter = this.nodemailer.createTransport(emailConfig); + const buildHtml = async (template, context) => { try { const mjml = this.templateLookup[template](context); From 0f80524127b80bb757126145cc7e0aea4ee206ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:03 -0700 Subject: [PATCH 20/69] unset email password --- server/db/mongo/modules/settingsModule.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js index 2c1b402c3..d3568ad32 100755 --- a/server/db/mongo/modules/settingsModule.js +++ b/server/db/mongo/modules/settingsModule.js @@ -21,6 +21,11 @@ const updateAppSettings = async (newSettings) => { delete update.$set.pagespeedApiKey; } + if (newSettings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + const settings = await AppSettings.findOneAndUpdate({}, update, { new: true, upsert: true, From fcd6af259eb873b12fe1c1e3b75d746f6da695a8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 May 2025 14:53:20 -0700 Subject: [PATCH 21/69] add optional email user --- server/db/models/AppSettings.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js index 1113bbe46..06ba8c1df 100755 --- a/server/db/models/AppSettings.js +++ b/server/db/models/AppSettings.js @@ -25,6 +25,9 @@ const AppSettingsSchema = mongoose.Schema( systemEmailPassword: { type: String, }, + systemEmailUser: { + type: String, + }, singleton: { type: Boolean, required: true, From 3ccfc462c11e6fecddb721049a92e3e6ff450d69 Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Fri, 9 May 2025 17:53:21 -0400 Subject: [PATCH 22/69] Transalated all strings in TeamPanel --- .../TabPanels/Account/TeamPanel.jsx | 40 +++++++++---------- client/src/locales/gb.json | 23 +++++++++++ client/src/locales/ru.json | 23 +++++++++++ client/src/locales/tr.json | 23 +++++++++++ 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/client/src/Components/TabPanels/Account/TeamPanel.jsx b/client/src/Components/TabPanels/Account/TeamPanel.jsx index 9448c632b..fe4b201dd 100644 --- a/client/src/Components/TabPanels/Account/TeamPanel.jsx +++ b/client/src/Components/TabPanels/Account/TeamPanel.jsx @@ -40,7 +40,7 @@ const TeamPanel = () => { const headers = [ { id: "name", - content: "Name", + content: t("teamPanel.table.name"), render: (row) => { return ( @@ -48,16 +48,16 @@ const TeamPanel = () => { {row.firstName + " " + row.lastName} - Created {new Date(row.createdAt).toLocaleDateString()} + {t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()} ); }, }, - { id: "email", content: "Email", render: (row) => row.email }, + { id: "email", content: t("teamPanel.table.email"), render: (row) => row.email }, { id: "role", - content: "Role", + content: t("teamPanel.table.role"), render: (row) => row.role, }, ]; @@ -198,7 +198,7 @@ const TeamPanel = () => { spellCheck="false" gap={SPACING_GAP} > - Team members + {t("teamPanel.teamMembers")} { filled={(filter === "all").toString()} onClick={() => setFilter("all")} > - All + {t("teamPanel.filter.all")} @@ -238,22 +238,20 @@ const TeamPanel = () => { color="accent" onClick={() => setIsOpen(true)} > - Invite a team member + {t("teamPanel.inviteTeamMember")} { marginBottom={SPACING_GAP} type="email" id="input-team-member" - placeholder="Email" + placeholder={t("teamPanel.email")} value={toInvite.email} onChange={handleChange} error={errors.email ? true : false} @@ -270,7 +268,7 @@ const TeamPanel = () => { />