Merge pull request #2263 from bluewave-labs/develop

develop->demo
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-05-13 13:35:45 -07:00
commit 881fbdeeb4
56 files changed
+1962 -1184

No files matched your search

+1 -1
View File
@@ -18,7 +18,7 @@ Fixes #123
const { t } = useTranslation();
<div>{t('add')}</div>
```
- [ ] The issue I am working on is assigned to me.
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions.
- [ ] My PR is granular and targeted to one specific feature.
+9
View File
@@ -123,9 +123,15 @@ If one or more of these are missing, we may ask you to update your pull request
## Branching model
- Code contributions should go to the `develop` branch.
- `master` is used for stable releases.
- Use descriptive branch names, like `fix/login-error` or `feat/add-alerts`.
- Make sure that you are using the latest version.
- 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.
---
@@ -133,8 +139,11 @@ If one or more of these are missing, we may ask you to update your pull request
Thanks for making Checkmate better. We mean it. Whether it’s your first pull request or your 50th, we’re excited to build with you.
PS: feel free to introduce yourself on [Discord](https://discord.gg/YOUR-DISCORD-LINK) and say hi.
-- Checkmate team
Also make sure you read the [document about how to make a good pull request](/PULLREQUESTS.md).
+1
View File
@@ -8,6 +8,7 @@
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
+1 -1
View File
@@ -111,7 +111,7 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
Alert.propTypes = {
variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired,
title: PropTypes.string,
body: PropTypes.string,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
isToast: PropTypes.bool,
hasIcon: PropTypes.bool,
onClick: function (props, propName, componentName) {
+3 -1
View File
@@ -2,6 +2,7 @@ import PropTypes from "prop-types";
import { Button, Stack } from "@mui/material";
import { GenericDialog } from "./genericDialog";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const Dialog = ({
title,
@@ -13,6 +14,7 @@ const Dialog = ({
isLoading,
}) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<GenericDialog
@@ -33,7 +35,7 @@ const Dialog = ({
color="secondary"
onClick={onCancel}
>
Cancel
{t("cancel", "Cancel")}
</Button>
<Button
variant="contained"
+41 -27
View File
@@ -11,7 +11,8 @@ import { useSelector } from "react-redux";
import Alert from "../Alert";
import { useTranslation } from "react-i18next";
import "./index.css";
import { useFetchSettings } from "../../Hooks/useFetchSettings";
import { useState } from "react";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
*
@@ -24,32 +25,41 @@ import "./index.css";
* @returns {JSX.Element} The rendered fallback UI.
*/
const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, showPageSpeedWarning = false }) => {
const Fallback = ({
title,
checks,
link = "/",
isAdmin,
vowelStart = false,
showPageSpeedWarning = false,
}) => {
const theme = useTheme();
const navigate = useNavigate();
const mode = useSelector((state) => state.ui.mode);
const { t } = useTranslation();
const [settingsData, setSettingsData] = useState(undefined);
const [isLoading, error] = useFetchSettings({ setSettingsData });
// Custom warning message with clickable link
const renderWarningMessage = () => {
return (
<>
{t("pageSpeedWarning")} {" "}
<Link
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
}
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>
{" "}{t("pageSpeedAddApiKey")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
@@ -120,7 +130,7 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, show
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
@@ -129,27 +139,31 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, show
{t("bulkImport.fallbackPage")}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{(title === "pagespeed monitor" && showPageSpeedWarning) && (
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box sx={{
'& .alert.row-stack': {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
'& .MuiTypography-root': {
color: theme.palette.warningSecondary.contrastText
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
'& .MuiBox-root > svg': {
color: theme.palette.warningSecondary.contrastText
}
}
}}>
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
</Box>
</Box>
)}
@@ -156,7 +156,7 @@ const Select = ({
};
Select.propTypes = {
id: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.string,
placeholder: PropTypes.string,
@@ -141,9 +141,9 @@ TextInput.displayName = "TextInput";
TextInput.propTypes = {
type: PropTypes.string,
id: PropTypes.string.isRequired,
id: PropTypes.string,
name: PropTypes.string,
value: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
placeholder: PropTypes.string,
isRequired: PropTypes.bool,
isOptional: PropTypes.bool,
@@ -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")}
</Button>
{bulkPath && (
<Button
+32 -22
View File
@@ -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: <Monitors /> },
{ name: "Pagespeed", path: "pagespeed", icon: <PageSpeed /> },
{ name: "Infrastructure", path: "infrastructure", icon: <Integrations /> },
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: "Distributed uptime",
name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: <DistributedUptimeIcon />,
},
{ name: "Incidents", path: "incidents", icon: <Incidents /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: "Status pages", path: "status", icon: <StatusPages /> },
{ name: "Maintenance", path: "maintenance", icon: <Maintenance /> },
// { name: "Integrations", path: "integrations", icon: <Integrations /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
// { name: t("menu.integrations"), path: "integrations", icon: <Integrations /> },
{
name: "Settings",
name: t("menu.settings"),
icon: <Settings />,
path: "settings",
},
];
const otherMenuItems = [
{ name: "Support", path: "support", icon: <Support /> },
const getOtherMenuItems = (t) => [
{ name: t("menu.support"), path: "support", icon: <Support /> },
{
name: "Discussions",
name: t("menu.discussions"),
path: "discussions",
icon: <Discussions />,
},
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
];
const accountMenuItems = [
{ name: "Profile", path: "account/profile", icon: <UserSvg /> },
{ name: "Password", path: "account/password", icon: <LockSvg /> },
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
const getAccountMenuItems = (t) => [
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
];
/* 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);
@@ -726,7 +732,11 @@ function Sidebar() {
{authState.user?.firstName} {authState.user?.lastName}
</Typography>
<Typography sx={{ textTransform: "capitalize" }}>
{authState.user?.role}
{authState.user?.role?.includes("superadmin") ? t("roles.superAdmin") :
authState.user?.role?.includes("admin") ? t("roles.admin") :
authState.user?.role?.includes("user") ? t("roles.teamMember") :
authState.user?.role?.includes("demo") ? t("roles.demoUser") :
authState.user?.role}
</Typography>
</Box>
<Stack
@@ -737,7 +747,7 @@ function Sidebar() {
>
<ThemeSwitch color={iconColor} />
<Tooltip
title="Controls"
title={t("navControls")}
disableInteractive
>
<IconButton
@@ -851,7 +861,7 @@ function Sidebar() {
}}
>
<LogoutSvg />
Log out
{t("menu.logOut", "Log out")}
</MenuItem>
</Menu>
</Stack>
@@ -94,7 +94,7 @@ const PasswordPanel = () => {
const action = await dispatch(update({ localData }));
if (action.payload.success) {
createToast({
body: "Your password was changed successfully.",
body: t("passwordPanel.passwordChangedSuccess", "Your password was changed successfully."),
});
setLocalData({
password: "",
@@ -104,7 +104,7 @@ const PasswordPanel = () => {
} else {
// TODO: Check for other errors?
createToast({
body: "Your password input was incorrect.",
body: t("passwordPanel.passwordInputIncorrect", "Your password input was incorrect."),
});
setErrors({ password: "*" + action.payload.msg + "." });
}
@@ -148,12 +148,12 @@ const PasswordPanel = () => {
component="h1"
width="20ch"
>
Current password
{t("passwordPanel.currentPassword", "Current password")}
</Typography>
<TextInput
type="password"
id="edit-current-password"
placeholder="Enter your current password"
placeholder={t("passwordPanel.enterCurrentPassword", "Enter your current password")}
autoComplete="current-password"
value={localData.password}
onChange={handleChange}
@@ -173,13 +173,13 @@ const PasswordPanel = () => {
component="h1"
width="20ch"
>
New password
{t("passwordPanel.newPassword", "New password")}
</Typography>
<TextInput
type="password"
id="edit-new-password"
placeholder="Enter your new password"
placeholder={t("passwordPanel.enterNewPassword", "Enter your new password")}
autoComplete="new-password"
value={localData.newPassword}
onChange={handleChange}
@@ -199,13 +199,13 @@ const PasswordPanel = () => {
component="h1"
width="20ch"
>
Confirm new password
{t("passwordPanel.confirmNewPassword", "Confirm new password")}
</Typography>
<TextInput
type="password"
id="edit-confirm-password"
placeholder={t("confirmPassword")}
placeholder={t("confirmPassword", "Confirm password")}
autoComplete="new-password"
value={localData.confirm}
onChange={handleChange}
@@ -219,7 +219,7 @@ const PasswordPanel = () => {
<Box sx={{ maxWidth: "70ch" }}>
<Alert
variant="warning"
body="New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character."
body={t("passwordPanel.passwordRequirements", "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.")}
/>
</Box>
)}
@@ -232,7 +232,7 @@ const PasswordPanel = () => {
color="accent"
type="submit"
loading={isLoading}
loadingIndicator="Saving..."
loadingIndicator={t("commonSaving", "Saving...")}
disabled={
Object.keys(errors).length > 0 ||
Object.values(localData).filter((value) => value === "").length > 0
@@ -242,7 +242,7 @@ const PasswordPanel = () => {
mt: theme.spacing(20),
}}
>
Save
{t("commonSave", "Save")}
</Button>
</Stack>
</Stack>
@@ -2,11 +2,11 @@ import { useTheme } from "@emotion/react";
import TabPanel from "@mui/lab/TabPanel";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
import Select from "../../Inputs/Select";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
@@ -21,6 +21,7 @@ import { useGetInviteToken } from "../../../Hooks/inviteHooks";
const TeamPanel = () => {
const theme = useTheme();
const { t } = useTranslation();
const SPACING_GAP = theme.spacing(12);
const [toInvite, setToInvite] = useState({
@@ -39,7 +40,7 @@ const TeamPanel = () => {
const headers = [
{
id: "name",
content: "Name",
content: t("teamPanel.table.name"),
render: (row) => {
return (
<Stack>
@@ -47,16 +48,16 @@ const TeamPanel = () => {
{row.firstName + " " + row.lastName}
</Typography>
<Typography>
Created {new Date(row.createdAt).toLocaleDateString()}
{t("teamPanel.table.created")} {new Date(row.createdAt).toLocaleDateString()}
</Typography>
</Stack>
);
},
},
{ 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,
},
];
@@ -78,10 +79,10 @@ const TeamPanel = () => {
useEffect(() => {
const ROLE_MAP = {
superadmin: "Super admin",
admin: "Admin",
user: "Team member",
demo: "Demo User",
superadmin: t("roles.superAdmin"),
admin: t("roles.admin"),
user: t("roles.teamMember"),
demo: t("roles.demoUser"),
};
let team = members;
if (filter !== "all")
@@ -98,7 +99,7 @@ const TeamPanel = () => {
role: member.role.map((role) => ROLE_MAP[role]).join(","),
}));
setData(team);
}, [filter, members]);
}, [members, filter, t]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
@@ -197,7 +198,7 @@ const TeamPanel = () => {
spellCheck="false"
gap={SPACING_GAP}
>
<Typography component="h1">Team members</Typography>
<Typography component="h1">{t("teamPanel.teamMembers")}</Typography>
<Stack
direction="row"
justifyContent="space-between"
@@ -214,21 +215,21 @@ const TeamPanel = () => {
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
All
{t("teamPanel.filter.all")}
</Button>
<Button
variant="group"
filled={(filter === "admin").toString()}
onClick={() => setFilter("admin")}
>
Super admin
{t("roles.superAdmin")}
</Button>
<Button
variant="group"
filled={(filter === "user").toString()}
onClick={() => setFilter("user")}
>
Member
{t("teamPanel.filter.member")}
</Button>
</ButtonGroup>
</Stack>
@@ -237,22 +238,20 @@ const TeamPanel = () => {
color="accent"
onClick={() => setIsOpen(true)}
>
Invite a team member
{t("teamPanel.inviteTeamMember")}
</Button>
</Stack>
<DataTable
headers={headers}
data={data}
config={{ emptyView: "There are no team members with this role" }}
config={{ emptyView: t("teamPanel.noMembers") }}
/>
</Stack>
<GenericDialog
title={"Invite new team member"}
description={
"When you add a new team member, they will get access to all monitors."
}
title={t("teamPanel.inviteNewTeamMember")}
description={t("teamPanel.inviteDescription")}
open={isOpen}
onClose={closeInviteModal}
theme={theme}
@@ -261,7 +260,7 @@ const TeamPanel = () => {
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}
@@ -269,7 +268,7 @@ const TeamPanel = () => {
/>
<Select
id="team-member-role"
placeholder="Select role"
placeholder={t("teamPanel.selectRole")}
isHidden={true}
value={toInvite.role[0]}
onChange={(event) =>
@@ -279,11 +278,11 @@ const TeamPanel = () => {
}))
}
items={[
{ _id: "admin", name: "Admin" },
{ _id: "user", name: "User" },
{ _id: "admin", name: t("roles.admin") },
{ _id: "user", name: t("roles.teamMember") },
]}
/>
{token && <Typography>Invite link</Typography>}
{token && <Typography>{t("teamPanel.inviteLink")}</Typography>}
{token && (
<TextInput
id="invite-token"
@@ -302,7 +301,7 @@ const TeamPanel = () => {
color="error"
onClick={closeInviteModal}
>
Cancel
{t("teamPanel.cancel")}
</Button>
<Button
variant="contained"
@@ -311,7 +310,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
Get token
{t("teamPanel.getToken")}
</Button>
<Button
variant="contained"
@@ -320,7 +319,7 @@ const TeamPanel = () => {
loading={isSendingInvite}
disabled={isDisabled}
>
E-mail token
{t("teamPanel.emailToken")}
</Button>
</Stack>
</GenericDialog>
-1
View File
@@ -65,7 +65,6 @@ const DataTable = ({
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
fontSize: "13px",
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
+24
View File
@@ -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 };
+55
View File
@@ -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 = ({ setSettingsData }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchSettings = async () => {
setIsLoading(true);
try {
const response = await networkService.getAppSettings();
setSettingsData(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 };
+33
View File
@@ -0,0 +1,33 @@
import { useState } from "react";
import { networkService } from "../main";
import { useSelector } from "react-redux";
import { createToast } from "../Utils/toastUtils";
const useSendTestEmail = () => {
const [isSending, setIsSending] = useState(false);
const [error, setError] = useState(null);
const user = useSelector((state) => state.auth.user);
const sendTestEmail = async () => {
try {
setIsSending(true);
setError(null);
const response = await networkService.sendTestEmail({ to: user.email });
if (typeof response?.data?.data?.messageId !== "undefined") {
createToast({
body: "Test email sent successfully",
});
} else {
throw new Error("Failed to send test email");
}
} catch (error) {
setError(error);
} finally {
setIsSending(false);
}
};
return [isSending, error, sendTestEmail];
};
export { useSendTestEmail };
+20 -9
View File
@@ -8,6 +8,7 @@ import TabContext from "@mui/lab/TabContext";
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
import { useTranslation } from "react-i18next";
import "./index.css";
/**
@@ -24,24 +25,34 @@ const Account = ({ open = "profile" }) => {
navigate(`/account/${newTab}`);
};
const { user } = useSelector((state) => state.auth);
const { t } = useTranslation();
const requiredRoles = ["superadmin", "admin"];
let tabList = ["Profile", "Password", "Team"];
let tabList = [
{ name: t("menu.profile"), value: "profile" },
{ name: t("menu.password"), value: "password" },
{ name: t("menu.team"), value: "team" }
];
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
if (hideTeams) {
tabList = ["Profile", "Password"];
tabList = [
{ name: t("menu.profile"), value: "profile" },
{ name: t("menu.password"), value: "password" }
];
}
// Remove password for demo
if (user.role.includes("demo")) {
tabList = ["Profile"];
tabList = [
{ name: t("menu.profile"), value: "profile" }
];
}
const handleKeyDown = (event) => {
const currentIndex = tabList.findIndex((label) => label.toLowerCase() === tab);
const currentIndex = tabList.findIndex((item) => item.value === tab);
if (event.key === "Tab") {
const nextIndex = (currentIndex + 1) % tabList.length;
setFocusedTab(tabList[nextIndex].toLowerCase());
setFocusedTab(tabList[nextIndex].value);
} else if (event.key === "Enter") {
event.preventDefault();
navigate(`/account/${focusedTab}`);
@@ -60,13 +71,13 @@ const Account = ({ open = "profile" }) => {
>
<TabContext value={tab}>
<CustomTabList value={tab} onChange={handleTabChange} aria-label="account tabs">
{tabList.map((label, index) => (
{tabList.map((item, index) => (
<Tab
label={label}
label={item.name}
key={index}
value={label.toLowerCase()}
value={item.value}
onKeyDown={handleKeyDown}
onFocus={() => handleFocus(label.toLowerCase())}
onFocus={() => handleFocus(item.value)}
tabIndex={index}
/>
))}
+4 -4
View File
@@ -166,7 +166,7 @@ const ForgotPassword = () => {
</IconBox>
</Stack>
<Typography component="h1">{t("authForgotPasswordTitle")}</Typography>
<Typography>No worries, we&apos;ll send you reset instructions.</Typography>
<Typography>{t("authForgotPasswordInstructions")}</Typography>
</Box>
<Box
component="form"
@@ -179,13 +179,13 @@ const ForgotPassword = () => {
<TextInput
type="email"
id="forgot-password-email-input"
label="Email"
label={t("email")}
isRequired={true}
placeholder="Enter your email"
placeholder={t("enterEmail")}
value={form.email}
onChange={handleChange}
error={errors.email ? true : false}
helperText={errors.email}
helperText={t(errors.email)}
/>
<Button
variant="contained"
@@ -71,7 +71,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
helperText={errors.email && (
errors.email.includes("required") ? t("authRegisterEmailRequired") :
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") :
errors.email
t(errors.email)
)}
ref={inputRef}
/>
+4 -1
View File
@@ -11,12 +11,15 @@ import { useState } from "react";
import IncidentTable from "./Components/IncidentTable";
import GenericFallback from "../../Components/GenericFallback";
import NetworkError from "../../Components/GenericFallback/NetworkError";
import { useTranslation } from "react-i18next";
//Constants
const BREADCRUMBS = [{ name: `Incidents`, path: "/incidents" }];
const Incidents = () => {
// Redux state
const { user } = useSelector((state) => state.auth);
const { t } = useTranslation();
const BREADCRUMBS = [{ name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }];
// Local state
const [selectedMonitor, setSelectedMonitor] = useState("0");
@@ -68,8 +68,16 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
),
},
{ id: "cpu", content: t("cpu"), render: (row) => <CustomGauge progress={row.cpu} /> },
{ id: "memory", content: t("memory"), render: (row) => <CustomGauge progress={row.mem} /> },
{ id: "disk", content: t("disk"), render: (row) => <CustomGauge progress={row.disk} /> },
{
id: "memory",
content: t("memory"),
render: (row) => <CustomGauge progress={row.mem} />,
},
{
id: "disk",
content: t("disk"),
render: (row) => <CustomGauge progress={row.disk} />,
},
{
id: "actions",
content: t("actions"),
@@ -129,6 +137,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
},
},
onRowClick: (row) => openDetails(row.id),
emptyView: "No monitors found",
}}
/>
);
@@ -73,7 +73,7 @@ const InfrastructureMonitors = () => {
);
}
if (!isLoading && monitors?.length === 0) {
if (!isLoading && typeof summary?.totalMonitors === "undefined" ) {
return (
<Fallback
vowelStart={true}
@@ -0,0 +1,32 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import Link from "../../Components/Link";
const SettingsAbout = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAbout")}</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {2.1}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
);
};
export default SettingsAbout;
@@ -0,0 +1,96 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ConfigBox from "../../Components/ConfigBox";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import Dialog from "../../Components/Dialog";
import { useState } from "react";
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [isOpen, setIsOpen] = useState(false);
if (!isAdmin) {
return null;
}
return (
<>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={HEADER_SX}>{t("settingsDemoMonitorsDescription")}</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={() => {
const syntheticEvent = {
target: {
name: "demo",
},
};
handleChange(syntheticEvent);
}}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
variant="contained"
color="error"
loading={isLoading}
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteMonitors",
},
};
handleChange(syntheticEvent);
setIsOpen(false);
}}
isLoading={isLoading}
/>
</ConfigBox>
</>
);
};
SettingsDemoMonitors.propTypes = {
isAdmin: PropTypes.bool,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
};
export default SettingsDemoMonitors;
+150
View File
@@ -0,0 +1,150 @@
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";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
const SettingsEmail = ({
isAdmin,
HEADER_SX,
handleChange,
settingsData,
setSettingsData,
isPasswordSet,
}) => {
// Setup
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [password, setPassword] = useState("");
const [hasBeenReset, setHasBeenReset] = useState(false);
// Network
const [isSending, error, sendTestEmail] = useSendTestEmail();
// Handlers
const handlePasswordChange = (e) => {
setPassword(e.target.value);
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: e.target.value },
});
};
if (!isAdmin) {
return null;
}
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsEmail")}</Typography>
<Typography sx={HEADER_SX}>{t("settingsEmailDescription")}</Typography>
</Box>
<Box>
<Stack gap={theme.spacing(10)}>
<Box>
<Typography>{t("settingsEmailHost")}</Typography>
<TextInput
name="systemEmailHost"
placeholder="smtp.gmail.com"
value={settingsData?.settings?.systemEmailHost ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailPort")}</Typography>
<TextInput
name="systemEmailPort"
placeholder="425"
type="number"
value={settingsData?.settings?.systemEmailPort ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailUser")}</Typography>
<TextInput
name="systemEmailUser"
placeholder="Leave empty if not required"
value={settingsData?.settings?.systemEmailUser ?? ""}
onChange={handleChange}
/>
</Box>
<Box>
<Typography>{t("settingsEmailAddress")}</Typography>
<TextInput
name="systemEmailAddress"
placeholder="uptime@bluewavelabs.ca"
value={settingsData?.settings?.systemEmailAddress ?? ""}
onChange={handleChange}
/>
</Box>
{(isPasswordSet === false || hasBeenReset === true) && (
<Box>
<Typography>{t("settingsEmailPassword")}</Typography>
<TextInput
name="systemEmailPassword"
type="password"
placeholder="123 456 789 101112"
value={password}
onChange={handlePasswordChange}
endAdornment={<PasswordEndAdornment />}
/>
</Box>
)}
{isPasswordSet === true && hasBeenReset === false && (
<Box>
<Typography>{t("settingsEmailFieldResetLabel")}</Typography>
<Button
onClick={() => {
setPassword("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: "" },
});
setHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
<Box>
<Button
variant="contained"
color="accent"
loading={isSending}
onClick={sendTestEmail}
>
Send test e-mail
</Button>
</Box>
</Stack>
</Box>
</ConfigBox>
);
};
SettingsEmail.propTypes = {
isAdmin: PropTypes.bool,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
handleChange: PropTypes.func,
HEADER_SX: PropTypes.object,
isPasswordSet: PropTypes.bool,
};
export default SettingsEmail;
@@ -0,0 +1,93 @@
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 = ({
isAdmin,
HEADING_SX,
settingsData,
setSettingsData,
isApiKeySet,
}) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [apiKey, setApiKey] = useState("");
const [hasBeenReset, setHasBeenReset] = useState(false);
// Handler
const handleChange = (e) => {
setApiKey(e.target.value);
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: e.target.value },
});
};
if (!isAdmin) {
return null;
}
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography sx={HEADING_SX}>{t("pageSpeedApiKeyFieldDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
{(isApiKeySet === false || hasBeenReset === true) && (
<TextInput
name="pagespeedApiKey"
label={t("pageSpeedApiKeyFieldLabel")}
value={apiKey}
type={"password"}
onChange={handleChange}
optionalLabel="(Optional)"
endAdornment={<PasswordEndAdornment />}
/>
)}
{isApiKeySet === true && hasBeenReset === false && (
<Box>
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
<Button
onClick={() => {
setApiKey("");
setSettingsData({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: "" },
});
setHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
);
};
SettingsPagespeed.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
isApiKeySet: PropTypes.bool,
};
export default SettingsPagespeed;
@@ -0,0 +1,90 @@
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 = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors }) => {
const theme = useTheme();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
if (!isAdmin) {
return null;
}
return (
<ConfigBox>
<Box>
<Typography
component="h1"
sx={HEADING_SX}
>
{t("settingsHistoryAndMonitoring")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsHistoryAndMonitoringDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="checkTTL"
label={t("settingsTTLLabel")}
optionalLabel={t("settingsTTLOptionalLabel")}
value={settingsData?.settings?.checkTTL ?? ""}
onChange={handleChange}
type="number"
error={errors.checkTTL ? true : false}
helperText={errors.checkTTL}
/>
<Box>
<Typography>{t("settingsClearAllStats")}</Typography>
<Button
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsClearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen}
theme={theme}
title={t("settingsClearAllStatsDialogTitle")}
description={t("settingsClearAllStatsDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
name: "deleteStats",
},
};
handleChange(syntheticEvent);
setIsOpen(false);
}}
isLoading={false}
/>
</ConfigBox>
);
};
SettingsStats.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
settingsData: PropTypes.object,
errors: PropTypes.object,
};
export default SettingsStats;
@@ -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 (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
<Typography sx={HEADING_SX}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
label={t("settingsDisplayTimezone")}
name="timezone"
value={timezone}
onChange={handleChange}
items={timezones}
/>
</Stack>
</ConfigBox>
);
};
SettingsTimeZone.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
timezone: PropTypes.string,
};
export default SettingsTimeZone;
+52
View File
@@ -0,0 +1,52 @@
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";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => {
const { t, i18n } = useTranslation();
const theme = useTheme();
const languages = Object.keys(i18n.options.resources || {});
return (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAppearance")}</Typography>
<Typography sx={HEADING_SX}>{t("settingsAppearanceDescription")}</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="mode"
label={t("settingsThemeMode")}
value={mode}
onChange={handleChange}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
]}
></Select>
<Select
name="language"
label={t("settingsLanguage")}
value={language}
onChange={handleChange}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
</Stack>
</ConfigBox>
);
};
SettingsUI.propTypes = {
HEADING_SX: PropTypes.object,
handleChange: PropTypes.func,
mode: PropTypes.string,
language: PropTypes.string,
};
export default SettingsUI;
+157 -464
View File
@@ -1,116 +1,72 @@
// 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 SettingsEmail from "./SettingsEmail";
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 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";
import { useIsAdmin } from "../../Hooks/useIsAdmin";
// 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 [settingsData, setSettingsData] = useState({});
const [errors, setErrors] = useState({});
// Network
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
});
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 isAdmin = useIsAdmin();
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);
}
// Handlers
const handleChange = async (e) => {
const { name, value } = e.target;
// Build next state early
const newSettingsData = {
...settingsData,
settings: { ...settingsData.settings, [name]: value },
};
fetchLatestVersion();
}, []);
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 });
// Validate
const { error } = settingsValidation.validate(newSettingsData.settings, {
abortEarly: false,
});
if (!error || error.details.length === 0) {
setErrors({});
} else {
@@ -119,397 +75,134 @@ 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;
}
setSettingsData(newSettingsData);
};
const handleResetApiKey = () => {
setIsApiKeySet(false);
setForm((prev) => ({
...prev,
pagespeedApiKey: "",
}));
const handleSave = () => {
const { error } = settingsValidation.validate(settingsData.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(settingsData?.settings);
};
const languages = Object.keys(i18n.options.resources || {});
return (
<Box
className="settings"
style={{
paddingBottom: 0,
}}
>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">Settings</Typography>
<SettingsTimeZone
HEADING_SX={HEADING_SX}
handleChange={handleChange}
timezone={timezone}
/>
<SettingsUI
HEADING_SX={HEADING_SX}
handleChange={handleChange}
mode={mode}
language={language}
/>
<SettingsPagespeed
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
settingsData={settingsData}
setSettingsData={setSettingsData}
isApiKeySet={settingsData?.pagespeedKeySet ?? false}
/>
<SettingsStats
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
settingsData={settingsData}
handleChange={handleChange}
errors={errors}
/>
<SettingsDemoMonitors
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
isLoading={isSettingsLoading || isSaving || isDeletingMonitorStats}
/>
<SettingsEmail
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
settingsData={settingsData}
setSettingsData={setSettingsData}
isPasswordSet={settingsData?.emailPasswordSet ?? false}
/>
<SettingsAbout />
<Stack
component="form"
gap={theme.spacing(12)}
noValidate
spellCheck="false"
direction="row"
justifyContent="flex-end"
>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsGeneralSettings")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
<Typography component="span">{t("settingsDisplayTimezone")}</Typography>-{" "}
{t("settingsDisplayTimezoneDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="display-timezones"
label={t("settingsDisplayTimezone")}
value={timezone}
onChange={(e) => {
dispatch(setTimezone({ timezone: e.target.value }));
}}
items={timezones}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAppearance")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("settingsAppearanceDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="theme-mode"
label={t("settingsThemeMode")}
value={mode}
onChange={(e) => {
dispatch(setMode(e.target.value));
}}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
]}
></Select>
<Select
id="language"
label={t("settingsLanguage")}
value={language}
onChange={(e) => {
dispatch(setLanguage(e.target.value));
i18n.changeLanguage(e.target.value);
}}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
</Stack>
</ConfigBox>
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("settingsDistributedUptimeDescription")}
</Typography>
</Box>
<Box>
<Switch
id="enableDistributedUptime"
color="accent"
checked={distributedUptimeEnabled}
onChange={(e) => {
dispatch(setDistributedUptimeEnabled(e.target.checked));
}}
/>
{distributedUptimeEnabled === true
? t("settingsEnabled")
: t("settingsDisabled")}
</Box>
</ConfigBox>
)} */}
<ConfigBox>
<Box>
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
{t("pageSpeedApiKeyFieldDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="pagespeedApiKey"
label={t("pageSpeedApiKeyFieldLabel")}
value={form.pagespeedApiKey ? form.pagespeedApiKey : tempPSKey}
type={apiKeyFieldType}
onChange={handleChange}
disabled={isApiKeySet}
optionalLabel="(Optional)"
error={!!errors.pagespeedApiKey}
helperText={errors.pagespeedApiKey}
endAdornment={
!isApiKeySet && (
<PasswordEndAdornment
fieldType={apiKeyFieldType}
setFieldType={setApiKeyFieldType}
/>
)
}
/>
{isApiKeySet && (
<Box>
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
<Button
onClick={handleResetApiKey}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
{/* {isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsWallet")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsWalletDescription")}
</Typography>
</Box>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: "flex-start",
gap: 2,
}}
>
<Stack
direction="row"
spacing={2}
>
<WalletMultiButton />
<WalletDisconnectButton />
</Stack>
</Box>
</ConfigBox>
)} */}
{isAdmin && (
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsHistoryAndMonitoring")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsHistoryAndMonitoringDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
id="ttl"
label={t("settingsTTLLabel")}
optionalLabel={t("settingsTTLOptionalLabel")}
value={form.ttl}
onChange={handleChange}
error={errors.ttl ? true : false}
helperText={errors.ttl}
/>
<Box>
<Typography>{t("settingsClearAllStats")}</Typography>
<Button
variant="contained"
color="error"
onClick={() =>
setIsOpen({ ...deleteStatsMonitorsInitState, deleteStats: true })
}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsClearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen.deleteStats}
theme={theme}
title={t("settingsClearAllStatsDialogTitle")}
description={t("settingsClearAllStatsDialogDescription")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
onConfirm={handleClearStats}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
)}
{isAdmin && (
<>
{/* Demo Monitors Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsDemoMonitorsDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsAddDemoMonitors")}</Typography>
<Button
variant="contained"
color="accent"
loading={isLoading || authIsLoading || checksIsLoading}
onClick={handleInsertDemoMonitors}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsAddDemoMonitorsButton")}
</Button>
</Box>
</ConfigBox>
{/* System Reset Section */}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsSystemReset")}</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("settingsSystemResetDescription")}
</Typography>
</Box>
<Box>
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
<Button
variant="contained"
color="error"
loading={isLoading || authIsLoading || checksIsLoading}
onClick={() =>
setIsOpen({ ...deleteStatsMonitorsInitState, deleteMonitors: true })
}
sx={{ mt: theme.spacing(4) }}
>
{t("settingsRemoveAllMonitorsButton")}
</Button>
</Box>
<Dialog
open={isOpen.deleteMonitors}
theme={theme}
title={t("settingsRemoveAllMonitorsDialogTitle")}
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
onConfirm={handleDeleteAllMonitors}
isLoading={isLoading || authIsLoading || checksIsLoading}
/>
</ConfigBox>
</>
)}
<ConfigBox>
<Box>
<Typography component="h1">{t("settingsAbout")}</Typography>
</Box>
<Box>
<Typography component="h2">Checkmate {version}</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("settingsDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
<Button
loading={isSaving || isDeletingMonitorStats || isSettingsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
<Button
loading={isLoading || authIsLoading || checksIsLoading}
disabled={Object.keys(errors).length > 0}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
onClick={handleSave}
>
{t("settingsSave")}
</Button>
</Stack>
{t("settingsSave")}
</Button>
</Stack>
</Box>
</Stack>
);
};
Settings.propTypes = {
isAdmin: PropTypes.bool,
};
export default Settings;
+19 -19
View File
@@ -54,7 +54,7 @@ const CreateStatusPage = () => {
const [createStatusPage, createStatusIsLoading, createStatusPageNetworkError] =
useCreateStatusPage(isCreate);
const navigate = useNavigate();
const { t } = useTranslation();
const { t } = useTranslation();
const [statusPage, statusPageMonitors, statusPageIsLoading, statusPageNetworkError] =
useStatusPageFetch(isCreate, url);
@@ -89,29 +89,29 @@ const CreateStatusPage = () => {
const handleImageChange = useCallback((fileObj) => {
if (!fileObj || !fileObj.file) return;
setForm((prev) => ({
...prev,
logo: {
src: fileObj.src,
name: fileObj.name,
type: fileObj.file.type,
size: fileObj.file.size,
},
...prev,
logo: {
src: fileObj.src,
name: fileObj.name,
type: fileObj.file.type,
size: fileObj.file.size,
},
}));
intervalRef.current = setInterval(() => {
const buffer = 12;
setProgress((prev) => {
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
return { value: 100, isLoading: false };
}
return { ...prev, value: prev.value + buffer };
});
const buffer = 12;
setProgress((prev) => {
if (prev.value + buffer >= 100) {
clearInterval(intervalRef.current);
return { value: 100, isLoading: false };
}
return { ...prev, value: prev.value + buffer };
});
}, 120);
}, []);
const removeLogo = () => {
setForm((prev) => ({
...prev,
+5 -2
View File
@@ -420,7 +420,9 @@ const Configure = () => {
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
<Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
@@ -491,6 +493,7 @@ const Configure = () => {
</Box>
<Stack>
<FormControlLabel
sx={{ marginLeft: 0 }}
control={
<Switch
name="ignore-error"
@@ -611,7 +614,7 @@ const Configure = () => {
isLoading={isLoading}
/>
<NotificationIntegrationModal
<NotificationIntegrationModal
open={isNotificationModalOpen}
onClose={handleClosenNotificationModal}
monitor={monitor}
+1
View File
@@ -422,6 +422,7 @@ const CreateMonitor = () => {
</Box>
<Stack>
<FormControlLabel
sx={{ marginLeft: 0 }}
control={
<Switch
name="ignore-error"
@@ -27,22 +27,14 @@ import { useTranslation } from "react-i18next";
* @returns {JSX.Element} The rendered Filter component.
*/
const typeOptions = [
const getTypeOptions = () => [
{ value: "http", label: "HTTP(S)" },
{ value: "ping", label: "Ping" },
{ value: "docker", label: "Docker" },
{ value: "port", label: "Port" },
];
const statusOptions = [
{ value: "Up", label: "Up" },
{ value: "Down", label: "Down" },
];
const stateOptions = [
{ value: "Active", label: "Active" },
{ value: "Paused", label: "Paused" },
];
// These functions were moved inline to ensure translations are applied correctly
const Filter = ({
selectedTypes,
@@ -58,6 +50,18 @@ const Filter = ({
const theme = useTheme();
const { t } = useTranslation();
const typeOptions = getTypeOptions();
// Create status options with translations
const statusOptions = [
{ value: "Up", label: t("monitorStatus.up") },
{ value: "Down", label: t("monitorStatus.down") },
];
// Create state options with translations
const stateOptions = [
{ value: "Active", label: t("monitorState.active") },
{ value: "Paused", label: t("monitorState.paused") },
];
const handleTypeChange = (event) => {
const selectedValues = event.target.value;
setSelectedTypes(selectedValues.length > 0 ? selectedValues : undefined);
@@ -2,10 +2,12 @@ import PropTypes from "prop-types";
import { Stack } from "@mui/material";
import StatusBox from "./statusBox";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import SkeletonLayout from "./skeleton";
const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) return <SkeletonLayout shouldRender={shouldRender} />;
return (
<Stack
@@ -14,15 +16,15 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
justifyContent="space-between"
>
<StatusBox
title="up"
title={t("monitorStatus.up")}
value={monitorsSummary?.upMonitors ?? 0}
/>
<StatusBox
title="down"
title={t("monitorStatus.down")}
value={monitorsSummary?.downMonitors ?? 0}
/>
<StatusBox
title="paused"
title={t("monitorStatus.paused")}
value={monitorsSummary?.pausedMonitors ?? 0}
/>
</Stack>
@@ -31,6 +33,7 @@ const StatusBoxes = ({ shouldRender, monitorsSummary }) => {
StatusBoxes.propTypes = {
monitorsSummary: PropTypes.object,
shouldRender: PropTypes.bool,
};
export default StatusBoxes;
+5 -2
View File
@@ -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")}
</Button>
</Box>
);
@@ -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);
+8
View File
@@ -1152,6 +1152,14 @@ class NetworkService {
}
);
}
// ************************************
// Send tes t email
// ************************************
async sendTestEmail(config) {
const { to } = config;
return this.axiosInstance.post(`/monitors/test-email`, { to });
}
}
export default NetworkService;
+2 -2
View File
@@ -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,
@@ -253,6 +253,7 @@ const baseTheme = (palette) => ({
MuiTableCell: {
styleOverrides: {
root: ({ theme }) => ({
fontSize: typographyLevels.base,
borderBottomColor: theme.palette.primary.lowContrast,
}),
},
@@ -376,7 +377,6 @@ const baseTheme = (palette) => ({
MuiTab: {
styleOverrides: {
root: ({ theme }) => ({
fontSize: theme.typography.fontSize - 1,
color: theme.palette.tertiary.contrastText,
height: "34px",
minHeight: "34px",
+5 -3
View File
@@ -3,6 +3,7 @@ import { useTheme } from "@emotion/react";
import { Box, Typography } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { setGreeting } from "../Features/UI/uiSlice";
const early = [
@@ -133,6 +134,7 @@ const evening = [
const Greeting = ({ type = "" }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const { firstName } = useSelector((state) => state.auth.user);
const index = useSelector((state) => state.ui.greeting.index);
const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate);
@@ -147,7 +149,7 @@ const Greeting = ({ type = "" }) => {
let random = Math.floor(Math.random() * 5);
dispatch(setGreeting({ index: random, lastUpdate: hour }));
}
}, [dispatch, hour]);
}, [dispatch, hour, lastUpdate]);
let greetingArray =
hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening;
@@ -165,7 +167,7 @@ const Greeting = ({ type = "" }) => {
fontSize="inherit"
color={theme.palette.primary.contrastTextTertiary}
>
{prepend},{" "}
{t("greeting.prepend", { defaultValue: prepend })}, {" "}
</Typography>
<Typography
component="span"
@@ -181,7 +183,7 @@ const Greeting = ({ type = "" }) => {
lineHeight={1}
color={theme.palette.primary.contrastTextTertiary}
>
{append} — Here’s an overview of your {type} monitors.
{t("greeting.append", { defaultValue: append })} — {t("greeting.overview", { type: t(`menu.${type}`) })}
</Typography>
</Box>
);
+43 -22
View File
@@ -11,7 +11,19 @@ const nameSchema = joi
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
"string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
"string.pattern.base":
"Name must contain only letters, spaces, apostrophes, or hyphens",
});
const lastnameSchema = joi
.string()
.max(50)
.trim()
.pattern(/^[\p{L}\p{M}''\- ]+$/u)
.messages({
"string.empty": "Surname is required",
"string.max": "Surname must be less than 50 characters",
"string.pattern.base": "Surname must contain only letters, spaces, apostrophes, or hyphens"
});
const passwordSchema = joi
@@ -53,7 +65,7 @@ const passwordSchema = joi
const credentials = joi.object({
firstName: nameSchema,
lastName: nameSchema,
lastName: lastnameSchema,
email: joi
.string()
.trim()
@@ -152,20 +164,21 @@ const monitorValidation = joi.object({
"string.invalidUrl": "Please enter a valid URL with optional port",
"string.pattern.base": "Please enter a valid container ID.",
}),
port: joi.number()
.integer()
.min(1)
.max(65535)
.when("type", {
is: "port",
then: joi.required().messages({
"number.base": "Port must be a number.",
"number.min": "Port must be at least 1.",
"number.max": "Port must be at most 65535.",
"any.required": "Port is required for port monitors.",
port: joi
.number()
.integer()
.min(1)
.max(65535)
.when("type", {
is: "port",
then: joi.required().messages({
"number.base": "Port must be a number.",
"number.min": "Port must be at least 1.",
"number.max": "Port must be at most 65535.",
"any.required": "Port is required for port monitors.",
}),
otherwise: joi.optional(),
}),
otherwise: joi.optional(),
}),
name: joi.string().trim().max(50).allow("").messages({
"string.max": "This field should not exceed the 50 characters limit.",
}),
@@ -253,14 +266,19 @@ const statusPageValidation = joi.object({
showCharts: joi.boolean(),
});
const settingsValidation = joi.object({
ttl: joi.number().required().messages({
checkTTL: joi.number().required().messages({
"string.empty": "Please enter a value",
"number.base": "Please enter a valid number",
"any.required": "Please enter a value"
"any.required": "Please enter a value",
}),
pagespeedApiKey: joi.string().allow("").optional(),
})
.unknown(true);
language: joi.string().required(),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(null, ""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
});
const dayjsValidator = (value, helpers) => {
if (!dayjs(value).isValid()) {
@@ -363,9 +381,12 @@ const infrastructureMonitorValidation = joi.object({
notifications: joi.array().items(
joi.object({
type: joi.string().valid("email").required(),
address: joi.string().email({ tlds: { allow: false } }).required(),
address: joi
.string()
.email({ tlds: { allow: false } })
.required(),
})
)
),
});
export {
@@ -377,5 +398,5 @@ export {
advancedSettingsValidation,
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation
logoImageValidation,
};
+536 -441
View File
@@ -1,444 +1,539 @@
{
"dontHaveAccount": "Don't have account",
"doNotHaveAccount": "Do not have an account?",
"registerHere": "Register here",
"email": "E-mail",
"forgotPassword": "Forgot Password",
"password": "Password",
"signUp": "Sign Up",
"submit": "Submit",
"title": "Title",
"continue": "Continue",
"enterEmail": "Enter your email",
"authLoginTitle": "Log In",
"authLoginEnterPassword": "Enter your password",
"commonPassword": "Password",
"commonBack": "Back",
"authForgotPasswordTitle": "Forgot password?",
"authForgotPasswordResetPassword": "Reset password",
"createPassword": "Create your password",
"createAPassword": "Password",
"authRegisterAlreadyHaveAccount": "Already have an account?",
"authRegisterLoginLink": "Log In",
"commonAppName": "Checkmate",
"welcomeBack": "Welcome back! You're successfully logged in.",
"authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
"authRegisterStepOneDescription": "Enter your details to get started",
"authRegisterStepTwoTitle": "Set up your profile",
"authRegisterStepTwoDescription": "Tell us more about yourself",
"authRegisterStepThreeTitle": "Almost done!",
"authRegisterStepThreeDescription": "Review your information",
"authForgotPasswordDescription": "No worries, we'll send you reset instructions.",
"authForgotPasswordSendInstructions": "Send instructions",
"authForgotPasswordBackTo": "Back to",
"authCheckEmailTitle": "Check your email",
"authCheckEmailDescription": "We sent a password reset link to",
"authCheckEmailResendEmail": "Resend email",
"authCheckEmailBackTo": "Back to",
"goBackTo": "Go back to",
"authCheckEmailDidntReceiveEmail": "Didn't receive the email?",
"authCheckEmailClickToResend": "Click to resend",
"authSetNewPasswordTitle": "Set new password",
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
"authSetNewPasswordNewPassword": "New password",
"authSetNewPasswordConfirmPassword": "Confirm password",
"confirmPassword": "Re-enter password to confirm",
"authSetNewPasswordResetPassword": "Reset password",
"authSetNewPasswordBackTo": "Back to",
"authPasswordMustBeAtLeast": "Must be at least",
"authPasswordCharactersLong": "8 characters long",
"authPasswordMustContainAtLeast": "Must contain at least",
"authPasswordSpecialCharacter": "one special character",
"authPasswordOneNumber": "one number",
"authPasswordUpperCharacter": "one upper character",
"authPasswordLowerCharacter": "one lower character",
"authPasswordConfirmAndPassword": "Confirm password and password",
"authPasswordMustMatch": "Passwords must match",
"validationNameRequired": "Please enter your name",
"validationNameTooLong": "Name should be less than 50 characters",
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
"authRegisterCreateAccount": "Create your account to get started",
"authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
"authRegisterSignUpWithEmail": "Create super admin account",
"authRegisterBySigningUp": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
"distributedStatusHeaderText": "Real-time, real-device coverage",
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
"settingsDisplayTimezone": "Display timezone",
"settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
"settingsAppearance": "Appearance",
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
"settingsThemeMode": "Theme Mode",
"settingsLanguage": "Language",
"settingsDistributedUptime": "Distributed uptime",
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
"settingsEnabled": "Enabled",
"settingsDisabled": "Disabled",
"settingsHistoryAndMonitoring": "History of monitoring",
"settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
"settingsTTLLabel": "The days you want to keep monitoring history.",
"settingsTTLOptionalLabel": "0 for infinite",
"settingsClearAllStats": "Clear all stats. This is irreversible.",
"settingsClearAllStatsButton": "Clear all stats",
"settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
"settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
"settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
"settingsDemoMonitors": "Demo monitors",
"settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
"settingsAddDemoMonitors": "Adding demo monitors",
"settingsAddDemoMonitorsButton": "Add demo monitors",
"settingsSystemReset": "System reset",
"settingsSystemResetDescription": "Remove all monitors from your system.",
"settingsRemoveAllMonitors": "Removing all monitors",
"settingsRemoveAllMonitorsButton": "Remove all monitors",
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
"settingsWallet": "Wallet",
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
"settingsAbout": "About",
"settingsDevelopedBy": "Developed by Bluewave Labs.",
"settingsSave": "Save",
"settingsSuccessSaved": "Settings saved successfully",
"settingsFailedToSave": "Failed to save settings",
"settingsStatsCleared": "Stats cleared successfully",
"settingsFailedToClearStats": "Failed to clear stats",
"settingsDemoMonitorsAdded": "Successfully added demo monitors",
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
"backendUnreachable": "Server Connection Error",
"backendUnreachableMessage": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
"backendUnreachableError": "Cannot connect to the server. Please try again later.",
"retryConnection": "Retry connection",
"retryingConnection": "Connecting...",
"backendReconnected": "Successfully reconnected to the server.",
"backendStillUnreachable": "Server is still unreachable. Please try again later.",
"backendConnectionError": "Error connecting to the server. Please check your network connection.",
"starPromptTitle": "Star Checkmate",
"starPromptDescription": "See the latest releases and help grow the community on GitHub",
"https": "HTTPS",
"http": "HTTP",
"monitor": "monitor",
"aboutus": "About Us",
"dontHaveAccount": "Don't have account",
"doNotHaveAccount": "Do not have an account?",
"registerHere": "Register here",
"email": "E-mail",
"forgotPassword": "Forgot Password",
"password": "Password",
"signUp": "Sign Up",
"submit": "Submit",
"title": "Title",
"continue": "Continue",
"enterEmail": "Enter your email",
"authLoginTitle": "Log In",
"authLoginEnterPassword": "Enter your password",
"commonPassword": "Password",
"commonBack": "Back",
"authForgotPasswordTitle": "Forgot password?",
"authForgotPasswordResetPassword": "Reset password",
"authForgotPasswordInstructions": "No worries, we'll send you reset instructions.",
"createPassword": "Create your password",
"createAPassword": "Password",
"authRegisterAlreadyHaveAccount": "Already have an account?",
"authRegisterLoginLink": "Log In",
"commonAppName": "Checkmate",
"welcomeBack": "Welcome back! You're successfully logged in.",
"authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
"authRegisterStepOneDescription": "Enter your details to get started",
"authRegisterStepTwoTitle": "Set up your profile",
"authRegisterStepTwoDescription": "Tell us more about yourself",
"authRegisterStepThreeTitle": "Almost done!",
"authRegisterStepThreeDescription": "Review your information",
"authForgotPasswordDescription": "No worries, we'll send you reset instructions.",
"authForgotPasswordSendInstructions": "Send instructions",
"authForgotPasswordBackTo": "Back to",
"authCheckEmailTitle": "Check your email",
"authCheckEmailDescription": "We sent a password reset link to",
"authCheckEmailResendEmail": "Resend email",
"authCheckEmailBackTo": "Back to",
"goBackTo": "Go back to",
"authCheckEmailDidntReceiveEmail": "Didn't receive the email?",
"authCheckEmailClickToResend": "Click to resend",
"authSetNewPasswordTitle": "Set new password",
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
"authSetNewPasswordNewPassword": "New password",
"authSetNewPasswordConfirmPassword": "Confirm password",
"confirmPassword": "Re-enter password to confirm",
"authSetNewPasswordResetPassword": "Reset password",
"authSetNewPasswordBackTo": "Back to",
"authPasswordMustBeAtLeast": "Must be at least",
"authPasswordCharactersLong": "8 characters long",
"authPasswordMustContainAtLeast": "Must contain at least",
"authPasswordSpecialCharacter": "one special character",
"authPasswordOneNumber": "one number",
"authPasswordUpperCharacter": "one upper character",
"authPasswordLowerCharacter": "one lower character",
"authPasswordConfirmAndPassword": "Confirm password and password",
"authPasswordMustMatch": "Passwords must match",
"validationNameRequired": "Please enter your name",
"validationNameTooLong": "Name should be less than 50 characters",
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
"authRegisterCreateAccount": "Create your account to get started",
"authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
"authRegisterSignUpWithEmail": "Create super admin account",
"authRegisterBySigningUp": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
"distributedStatusHeaderText": "Real-time, real-device coverage",
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
"settingsDisplayTimezone": "Display timezone",
"settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
"settingsAppearance": "Appearance",
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
"settingsThemeMode": "Theme Mode",
"settingsThemeModeLight": "Light",
"settingsThemeModeDark": "Dark",
"settingsLanguage": "Language",
"settingsDistributedUptime": "Distributed uptime",
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
"settingsEnabled": "Enabled",
"settingsDisabled": "Disabled",
"settingsHistoryAndMonitoring": "History of monitoring",
"settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
"settingsTTLLabel": "The days you want to keep monitoring history.",
"settingsTTLOptionalLabel": "0 for infinite",
"settingsClearAllStats": "Clear all stats. This is irreversible.",
"settingsClearAllStatsButton": "Clear all stats",
"settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
"settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
"settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
"settingsClearAllStatsDialogCancel": "Cancel",
"settingsDemoMonitors": "Demo monitors",
"settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
"settingsAddDemoMonitors": "Adding demo monitors",
"settingsAddDemoMonitorsButton": "Add demo monitors",
"settingsSystemReset": "System reset",
"settingsSystemResetDescription": "Remove all monitors from your system.",
"settingsRemoveAllMonitors": "Removing all monitors",
"settingsRemoveAllMonitorsButton": "Remove all monitors",
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
"settingsWallet": "Wallet",
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
"settingsAbout": "About",
"settingsDevelopedBy": "Developed by Bluewave Labs.",
"settingsSave": "Save",
"settingsSuccessSaved": "Settings saved successfully",
"settingsFailedToSave": "Failed to save settings",
"settingsStatsCleared": "Stats cleared successfully",
"settingsFailedToClearStats": "Failed to clear stats",
"settingsDemoMonitorsAdded": "Successfully added demo monitors",
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
"settingsEmail": "Email settings",
"settingsEmailDescription": "Configure email settings",
"settingsEmailHost": "Email host",
"settingsEmailPort": "Email port",
"settingsEmailAddress": "Email address",
"settingsEmailPassword": "Email password",
"settingsEmailUser": "Email user",
"settingsEmailFieldResetLabel": "Password is set. Click Reset to change it.",
"backendUnreachable": "Server Connection Error",
"backendUnreachableMessage": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
"backendUnreachableError": "Cannot connect to the server. Please try again later.",
"retryConnection": "Retry connection",
"retryingConnection": "Connecting...",
"backendReconnected": "Successfully reconnected to the server.",
"backendStillUnreachable": "Server is still unreachable. Please try again later.",
"backendConnectionError": "Error connecting to the server. Please check your network connection.",
"starPromptTitle": "Star Checkmate",
"starPromptDescription": "See the latest releases and help grow the community on GitHub",
"https": "HTTPS",
"http": "HTTP",
"monitor": "monitor",
"aboutus": "About Us",
"state": "State",
"now": "Now",
"delete": "Delete",
"configure": "Configure",
"networkError": "Network error",
"responseTime": "Response time",
"ms": "ms",
"bar": "Bar",
"area": "Area",
"country": "COUNTRY",
"city": "CITY",
"response": "RESPONSE",
"checkConnection": "Please check your connection",
"passwordreset": "Password Reset",
"authRegisterStepOnePersonalDetails": "Enter your personal details",
"authCheckEmailOpenEmailButton": "Open email app",
"authNewPasswordConfirmed": "Your password has been successfully reset. Click below to log in magically.",
"monitorStatusUp": "Monitor {name} ({url}) is now UP and responding",
"monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding",
"webhookSendSuccess": "Webhook notification sent successfully",
"webhookSendError": "Error sending webhook notification to {platform}",
"webhookUnsupportedPlatform": "Unsupported platform: {platform}",
"distributedRightCategoryTitle": "Monitor",
"distributedStatusServerMonitors": "Server Monitors",
"distributedStatusServerMonitorsDescription": "Monitor status of related servers",
"distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.",
"distributedUptimeCreateChecks": "Checks to perform",
"distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.",
"distributedUptimeCreateIncidentNotification": "Incident notifications",
"distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.",
"distributedUptimeCreateAdvancedSettings": "Advanced settings",
"distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.",
"distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs",
"distributedUptimeDetailsFooterBuilt": "Built on",
"distributedUptimeDetailsFooterSolana": "Solana",
"distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Uptime:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated",
"notifications": {
"enableNotifications": "Enable {{platform}} notifications",
"testNotification": "Test notification",
"addOrEditNotifications": "Add or edit notifications",
"slack": {
"label": "Slack",
"description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.",
"webhookLabel": "Webhook URL",
"webhookPlaceholder": "https://hooks.slack.com/services/...",
"webhookRequired": "Slack webhook URL is required"
},
"discord": {
"label": "Discord",
"description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.",
"webhookLabel": "Discord Webhook URL",
"webhookPlaceholder": "https://discord.com/api/webhooks/...",
"webhookRequired": "Discord webhook URL is required"
},
"telegram": {
"label": "Telegram",
"description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.",
"tokenLabel": "Your bot token",
"tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chatIdLabel": "Your Chat ID",
"chatIdPlaceholder": "-1001234567890",
"fieldsRequired": "Telegram token and chat ID are required"
},
"webhook": {
"label": "Webhooks",
"description": "You can set up a custom webhook to receive notifications when incidents occur.",
"urlLabel": "Webhook URL",
"urlPlaceholder": "https://your-server.com/webhook",
"urlRequired": "Webhook URL is required"
},
"testNotificationDevelop": "Test notification 2",
"integrationButton": "Notification Integration",
"testSuccess": "Test notification sent successfully!",
"testFailed": "Failed to send test notification",
"unsupportedType": "Unsupported notification type",
"networkError": "Network error occurred"
},
"testLocale": "testLocale",
"add": "Add",
"monitors": "monitors",
"distributedUptimeStatusCreateStatusPage": "status page",
"distributedUptimeStatusCreateStatusPageAccess": "Access",
"distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.",
"distributedUptimeStatusBasicInfoHeader": "Basic Information",
"distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.",
"distributedUptimeStatusLogoHeader": "Logo",
"distributedUptimeStatusLogoDescription": "Upload a logo for your status page",
"distributedUptimeStatusLogoUploadButton": "Upload logo",
"distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors",
"distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.",
"distributedUptimeStatusCreateYour": "Create your",
"distributedUptimeStatusEditYour": "Edit your",
"distributedUptimeStatusPublishedLabel": "Published and visible to the public",
"distributedUptimeStatusCompanyNameLabel": "Company name",
"distributedUptimeStatusPageAddressLabel": "Your status page address",
"distributedUptimeStatus30Days": "30 days",
"distributedUptimeStatus60Days": "60 days",
"distributedUptimeStatus90Days": "90 days",
"distributedUptimeStatusPageNotSetUp": "A status page is not set up.",
"distributedUptimeStatusContactAdmin": "Please contact your administrator",
"distributedUptimeStatusPageNotPublic": "This status page is not public.",
"distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?",
"distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page",
"distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.",
"distributedUptimeStatusDevices": "Devices",
"distributedUptimeStatusUpt": "UPT",
"distributedUptimeStatusUptBurned": "UPT Burned",
"distributedUptimeStatusUptLogo": "Upt Logo",
"incidentsTableNoIncidents": "No incidents recorded",
"incidentsTablePaginationLabel": "incidents",
"incidentsTableMonitorName": "Monitor Name",
"incidentsTableStatus": "Status",
"incidentsTableDateTime": "Date & Time",
"incidentsTableStatusCode": "Status Code",
"incidentsTableMessage": "Message",
"incidentsOptionsHeader": "Incidents for:",
"incidentsOptionsHeaderFilterBy": "Filter by:",
"incidentsOptionsHeaderFilterAll": "All",
"incidentsOptionsHeaderFilterDown": "Down",
"incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve",
"incidentsOptionsHeaderShow": "Show:",
"incidentsOptionsHeaderLastHour": "Last hour",
"incidentsOptionsHeaderLastDay": "Last day",
"incidentsOptionsHeaderLastWeek": "Last week",
"incidentsOptionsPlaceholderAllServers": "All servers",
"infrastructureCreateYour": "Create your",
"infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.",
"infrastructureServerRequirement": "The server you are monitoring must be running the",
"infrastructureCustomizeAlerts": "Customize alerts",
"infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.",
"infrastructureCreateMonitor": "Create Infrastructure Monitor",
"infrastructureProtocol": "Protocol",
"infrastructureServerUrlLabel": "Server URL",
"infrastructureDisplayNameLabel": "Display name",
"infrastructureAuthorizationSecretLabel": "Authorization secret",
"gb": "GB",
"mb": "MB",
"mem": "Mem",
"memoryUsage": "Memory usage",
"cpu": "CPU",
"cpuUsage": "CPU usage",
"cpuTemperature": "CPU Temperature",
"diskUsage": "Disk Usage",
"used": "Used",
"total": "Total",
"cores": "Cores",
"frequency": "Frequency",
"status": "Status",
"cpuPhysical": "CPU (Physical)",
"cpuLogical": "CPU (Logical)",
"cpuFrequency": "CPU Frequency",
"avgCpuTemperature": "Average CPU Temperature",
"memory": "Memory",
"disk": "Disk",
"uptime": "Uptime",
"os": "OS",
"host": "Host",
"actions": "Actions",
"integrations": "Integrations",
"integrationsPrism": "Connect Prism to your favorite service.",
"integrationsSlack": "Slack",
"integrationsSlackInfo": "Connect with Slack and see incidents in a channel",
"integrationsDiscord": "Discord",
"integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel",
"integrationsZapier": "Zapier",
"integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere",
"commonSave": "Save",
"commonSaving": "Saving...",
"createYour": "Create your",
"createMonitor": "Create monitor",
"pause": "Pause",
"resume": "Resume",
"editing": "Editing...",
"url": "URL",
"access": "Access",
"timezone": "Timezone",
"features": "Features",
"administrator": "Administrator?",
"loginHere": "Login here",
"displayName": "Display name",
"urlMonitor": "URL to monitor",
"portToMonitor": "Port to monitor",
"websiteMonitoring": "Website monitoring",
"websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.",
"pingMonitoring": "Ping monitoring",
"pingMonitoringDescription": "Check whether your server is available or not.",
"dockerContainerMonitoring": "Docker container monitoring",
"dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.",
"portMonitoring": "Port monitoring",
"portMonitoringDescription": "Check whether your port is open or not.",
"createMaintenanceWindow": "Create maintenance window",
"createMaintenance": "Create maintenance",
"editMaintenance": "Edit maintenance",
"maintenanceWindowName": "Maintenance Window Name",
"friendlyNameInput": "Friendly name",
"friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes",
"maintenanceRepeat": "Maintenance Repeat",
"maintenance": "maintenance",
"duration": "Duration",
"addMonitors": "Add monitors",
"window": "window",
"cancel": "Cancel",
"message": "Message",
"low": "low",
"high": "high",
"statusCode": "Status code",
"date&Time": "Date & Time",
"type": "Type",
"statusPageName": "Status page name",
"publicURL": "Public URL",
"repeat": "Repeat",
"edit": "Edit",
"createA": "Create a",
"remove": "Remove",
"maintenanceWindowDescription": "Your pings won't be sent during this time frame",
"startTime": "Start time",
"timeZoneInfo": "All dates and times are in GMT+0 time zone.",
"monitorsToApply": "Monitors to apply maintenance window to",
"nextWindow": "Next window",
"notFoundButton": "Go to the main dashboard",
"pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.",
"monitorDisplayName": "Monitor display name",
"whenNewIncident": "When there is a new incident,",
"notifySMS": "Notify via SMS (coming soon)",
"notifyEmails": "Also notify via email to multiple addresses (coming soon)",
"seperateEmails": "You can separate multiple emails with a comma",
"checkFrequency": "Check frequency",
"matchMethod": "Match Method",
"expectedValue": "Expected value",
"deleteDialogTitle": "Do you really want to delete this monitor?",
"deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.",
"pageSpeedMonitor": "PageSpeed monitor",
"shown": "Shown",
"ago": "ago",
"companyName": "Company name",
"pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.",
"pageSpeedDetailsPerformanceReportCalculator": "See calculator",
"checkingEvery": "Checking every",
"statusPageCreateSettings": "If your status page is ready, you can mark it as published.",
"basicInformation": "Basic Information",
"statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.",
"statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.",
"statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.",
"statusPageCreateSettingsCheckboxLabel": "Published and visible to the public",
"statusPageCreateBasicInfoStatusPageAddress": "Your status page address",
"statusPageCreateTabsContent": "Status page servers",
"statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.",
"statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page",
"showCharts": "Show charts",
"showUptimePercentage": "Show uptime percentage",
"removeLogo": "Remove Logo",
"statusPageStatus": "A public status page is not set up.",
"statusPageStatusContactAdmin": "Please contact to your administrator",
"statusPageStatusNotPublic": "This status page is not public.",
"statusPageStatusNoPage": "There's no status page here.",
"statusPageStatusServiceStatus": "Service status",
"statusBreadCrumbsStatusPages": "Status Pages",
"statusBreadCrumbsDetails": "Details",
"deleteStatusPage": "Do you want to delete this status page?",
"deleteStatusPageConfirm": "Yes, delete status page",
"deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.",
"uptimeCreate": "The expected value is used to match against response result, and the match determines the status.",
"uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See",
"uptimeCreateJsonPathQuery": "for query language documentation.",
"maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?",
"infrastructureEditYour": "Edit your",
"infrastructureEditMonitor": "Save Infrastructure Monitor",
"infrastructureMonitorCreated": "Infrastructure monitor created successfully!",
"infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!",
"errorInvalidTypeId": "Invalid notification type provided",
"errorInvalidFieldId": "Invalid field ID provided",
"inviteNoTokenFound": "No invite token found",
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.",
"pageSpeedLearnMoreLink": "Click here",
"pageSpeedAddApiKey": "to add your API key.",
"update": "Update",
"invalidFileFormat": "Unsupported file format!",
"invalidFileSize": "File size is too large!",
"ClickUpload": "Click to upload",
"DragandDrop": "drag and drop",
"MaxSize": "Maximum Size",
"SupportedFormats": "Supported formats",
"FirstName": "First name",
"LastName": "Last name",
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
"ignoreTLSError": "Ignore TLS/SSL error",
"tlsErrorIgnored": "TLS/SSL errors ignored",
"ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability",
"YourPhoto": "Profile photo",
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
"save": "Save",
"DeleteAccountTitle": "Remove account",
"DeleteAccountButton": "Remove account",
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteWarningTitle": "Really remove this account?",
"authRegisterFirstName": "Name",
"authRegisterLastName": "Surname",
"authRegisterEmail": "Email",
"authRegisterEmailRequired": "To continue, please enter your email address",
"authRegisterEmailInvalid": "Please enter a valid email address",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
"selectFile": "Select File",
"parsingFailed": "Parsing failed",
"uploadSuccess": "Monitors created successfully!",
"validationFailed": "Validation failed",
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
},
"publicLink": "Public link",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
"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.",
"now": "Now",
"delete": "Delete",
"configure": "Configure",
"networkError": "Network error",
"responseTime": "Response time",
"ms": "ms",
"bar": "Bar",
"area": "Area",
"country": "COUNTRY",
"city": "CITY",
"response": "RESPONSE",
"checkConnection": "Please check your connection",
"passwordreset": "Password Reset",
"authRegisterStepOnePersonalDetails": "Enter your personal details",
"authCheckEmailOpenEmailButton": "Open email app",
"authNewPasswordConfirmed": "Your password has been successfully reset. Click below to log in magically.",
"monitorStatusUp": "Monitor {name} ({url}) is now UP and responding",
"monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding",
"webhookSendSuccess": "Webhook notification sent successfully",
"webhookSendError": "Error sending webhook notification to {platform}",
"webhookUnsupportedPlatform": "Unsupported platform: {platform}",
"distributedRightCategoryTitle": "Monitor",
"distributedStatusServerMonitors": "Server Monitors",
"distributedStatusServerMonitorsDescription": "Monitor status of related servers",
"distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.",
"distributedUptimeCreateChecks": "Checks to perform",
"distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.",
"distributedUptimeCreateIncidentNotification": "Incident notifications",
"distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.",
"distributedUptimeCreateAdvancedSettings": "Advanced settings",
"distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.",
"distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs",
"distributedUptimeDetailsFooterBuilt": "Built on",
"distributedUptimeDetailsFooterSolana": "Solana",
"distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Uptime:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated",
"notifications": {
"enableNotifications": "Enable {{platform}} notifications",
"testNotification": "Test notification",
"addOrEditNotifications": "Add or edit notifications",
"slack": {
"label": "Slack",
"description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.",
"webhookLabel": "Webhook URL",
"webhookPlaceholder": "https://hooks.slack.com/services/...",
"webhookRequired": "Slack webhook URL is required"
},
"discord": {
"label": "Discord",
"description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.",
"webhookLabel": "Discord Webhook URL",
"webhookPlaceholder": "https://discord.com/api/webhooks/...",
"webhookRequired": "Discord webhook URL is required"
},
"telegram": {
"label": "Telegram",
"description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.",
"tokenLabel": "Your bot token",
"tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chatIdLabel": "Your Chat ID",
"chatIdPlaceholder": "-1001234567890",
"fieldsRequired": "Telegram token and chat ID are required"
},
"webhook": {
"label": "Webhooks",
"description": "You can set up a custom webhook to receive notifications when incidents occur.",
"urlLabel": "Webhook URL",
"urlPlaceholder": "https://your-server.com/webhook",
"urlRequired": "Webhook URL is required"
},
"testNotificationDevelop": "Test notification 2",
"integrationButton": "Notification Integration",
"testSuccess": "Test notification sent successfully!",
"testFailed": "Failed to send test notification",
"unsupportedType": "Unsupported notification type",
"networkError": "Network error occurred"
},
"testLocale": "testLocale",
"add": "Add",
"monitors": "monitors",
"distributedUptimeStatusCreateStatusPage": "status page",
"distributedUptimeStatusCreateStatusPageAccess": "Access",
"distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.",
"distributedUptimeStatusBasicInfoHeader": "Basic Information",
"distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.",
"distributedUptimeStatusLogoHeader": "Logo",
"distributedUptimeStatusLogoDescription": "Upload a logo for your status page",
"distributedUptimeStatusLogoUploadButton": "Upload logo",
"distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors",
"distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.",
"distributedUptimeStatusCreateYour": "Create your",
"distributedUptimeStatusEditYour": "Edit your",
"distributedUptimeStatusPublishedLabel": "Published and visible to the public",
"distributedUptimeStatusCompanyNameLabel": "Company name",
"distributedUptimeStatusPageAddressLabel": "Your status page address",
"distributedUptimeStatus30Days": "30 days",
"distributedUptimeStatus60Days": "60 days",
"distributedUptimeStatus90Days": "90 days",
"distributedUptimeStatusPageNotSetUp": "A status page is not set up.",
"distributedUptimeStatusContactAdmin": "Please contact your administrator",
"distributedUptimeStatusPageNotPublic": "This status page is not public.",
"distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?",
"distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page",
"distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.",
"distributedUptimeStatusDevices": "Devices",
"distributedUptimeStatusUpt": "UPT",
"distributedUptimeStatusUptBurned": "UPT Burned",
"distributedUptimeStatusUptLogo": "Upt Logo",
"incidentsTableNoIncidents": "No incidents recorded",
"incidentsTablePaginationLabel": "incidents",
"incidentsTableMonitorName": "Monitor Name",
"incidentsTableStatus": "Status",
"incidentsTableDateTime": "Date & Time",
"incidentsTableStatusCode": "Status Code",
"incidentsTableMessage": "Message",
"incidentsOptionsHeader": "Incidents for:",
"incidentsOptionsHeaderFilterBy": "Filter by:",
"incidentsOptionsHeaderFilterAll": "All",
"incidentsOptionsHeaderFilterDown": "Down",
"incidentsOptionsHeaderFilterCannotResolve": "Cannot resolve",
"incidentsOptionsHeaderShow": "Show:",
"incidentsOptionsHeaderLastHour": "Last hour",
"incidentsOptionsHeaderLastDay": "Last day",
"incidentsOptionsHeaderLastWeek": "Last week",
"incidentsOptionsPlaceholderAllServers": "All servers",
"infrastructureCreateYour": "Create your",
"infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.",
"infrastructureServerRequirement": "The server you are monitoring must be running the",
"infrastructureCustomizeAlerts": "Customize alerts",
"infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.",
"infrastructureCreateMonitor": "Create Infrastructure Monitor",
"infrastructureProtocol": "Protocol",
"infrastructureServerUrlLabel": "Server URL",
"infrastructureDisplayNameLabel": "Display name",
"infrastructureAuthorizationSecretLabel": "Authorization secret",
"gb": "GB",
"mb": "MB",
"mem": "Mem",
"memoryUsage": "Memory usage",
"cpu": "CPU",
"cpuUsage": "CPU usage",
"cpuTemperature": "CPU Temperature",
"diskUsage": "Disk Usage",
"used": "Used",
"total": "Total",
"cores": "Cores",
"frequency": "Frequency",
"status": "Status",
"cpuPhysical": "CPU (Physical)",
"cpuLogical": "CPU (Logical)",
"cpuFrequency": "CPU Frequency",
"avgCpuTemperature": "Average CPU Temperature",
"memory": "Memory",
"disk": "Disk",
"uptime": "Uptime",
"os": "OS",
"host": "Host",
"actions": "Actions",
"integrations": "Integrations",
"integrationsPrism": "Connect Prism to your favorite service.",
"integrationsSlack": "Slack",
"integrationsSlackInfo": "Connect with Slack and see incidents in a channel",
"integrationsDiscord": "Discord",
"integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel",
"integrationsZapier": "Zapier",
"integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere",
"commonSave": "Save",
"createYour": "Create your",
"createMonitor": "Create monitor",
"pause": "Pause",
"resume": "Resume",
"editing": "Editing...",
"url": "URL",
"access": "Access",
"timezone": "Timezone",
"features": "Features",
"administrator": "Administrator?",
"loginHere": "Login here",
"displayName": "Display name",
"urlMonitor": "URL to monitor",
"portToMonitor": "Port to monitor",
"websiteMonitoring": "Website monitoring",
"websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.",
"pingMonitoring": "Ping monitoring",
"pingMonitoringDescription": "Check whether your server is available or not.",
"dockerContainerMonitoring": "Docker container monitoring",
"dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.",
"portMonitoring": "Port monitoring",
"portMonitoringDescription": "Check whether your port is open or not.",
"createMaintenanceWindow": "Create maintenance window",
"createMaintenance": "Create maintenance",
"editMaintenance": "Edit maintenance",
"maintenanceWindowName": "Maintenance Window Name",
"friendlyNameInput": "Friendly name",
"friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes",
"maintenanceRepeat": "Maintenance Repeat",
"maintenance": "maintenance",
"duration": "Duration",
"addMonitors": "Add monitors",
"window": "window",
"cancel": "Cancel",
"message": "Message",
"low": "low",
"high": "high",
"statusCode": "Status code",
"date&Time": "Date & Time",
"type": "Type",
"statusPageName": "Status page name",
"publicURL": "Public URL",
"repeat": "Repeat",
"edit": "Edit",
"createA": "Create a",
"remove": "Remove",
"maintenanceWindowDescription": "Your pings won't be sent during this time frame",
"startTime": "Start time",
"timeZoneInfo": "All dates and times are in GMT+0 time zone.",
"monitorsToApply": "Monitors to apply maintenance window to",
"nextWindow": "Next window",
"notFoundButton": "Go to the main dashboard",
"pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.",
"monitorDisplayName": "Monitor display name",
"whenNewIncident": "When there is a new incident,",
"notifySMS": "Notify via SMS (coming soon)",
"notifyEmails": "Also notify via email to multiple addresses (coming soon)",
"seperateEmails": "You can separate multiple emails with a comma",
"checkFrequency": "Check frequency",
"matchMethod": "Match Method",
"expectedValue": "Expected value",
"deleteDialogTitle": "Do you really want to delete this monitor?",
"deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.",
"pageSpeedMonitor": "PageSpeed monitor",
"shown": "Shown",
"ago": "ago",
"companyName": "Company name",
"pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.",
"pageSpeedDetailsPerformanceReportCalculator": "See calculator",
"checkingEvery": "Checking every",
"statusPageCreateSettings": "If your status page is ready, you can mark it as published.",
"basicInformation": "Basic Information",
"statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.",
"statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.",
"statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.",
"statusPageCreateSettingsCheckboxLabel": "Published and visible to the public",
"statusPageCreateBasicInfoStatusPageAddress": "Your status page address",
"statusPageCreateTabsContent": "Status page servers",
"statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.",
"statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page",
"showCharts": "Show charts",
"showUptimePercentage": "Show uptime percentage",
"removeLogo": "Remove Logo",
"statusPageStatus": "A public status page is not set up.",
"statusPageStatusContactAdmin": "Please contact to your administrator",
"statusPageStatusNotPublic": "This status page is not public.",
"statusPageStatusNoPage": "There's no status page here.",
"statusPageStatusServiceStatus": "Service status",
"deleteStatusPage": "Do you want to delete this status page?",
"deleteStatusPageConfirm": "Yes, delete status page",
"deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.",
"uptimeCreate": "The expected value is used to match against response result, and the match determines the status.",
"uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See",
"uptimeCreateJsonPathQuery": "for query language documentation.",
"maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?",
"infrastructureEditYour": "Edit your",
"infrastructureEditMonitor": "Save Infrastructure Monitor",
"infrastructureMonitorCreated": "Infrastructure monitor created successfully!",
"infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!",
"errorInvalidTypeId": "Invalid notification type provided",
"errorInvalidFieldId": "Invalid field ID provided",
"inviteNoTokenFound": "No invite token found",
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.",
"pageSpeedLearnMoreLink": "Click here",
"pageSpeedAddApiKey": "to add your API key.",
"update": "Update",
"invalidFileFormat": "Unsupported file format!",
"invalidFileSize": "File size is too large!",
"ClickUpload": "Click to upload",
"DragandDrop": "drag and drop",
"MaxSize": "Maximum Size",
"SupportedFormats": "Supported formats",
"FirstName": "First name",
"LastName": "Last name",
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
"ignoreTLSError": "Ignore TLS/SSL error",
"tlsErrorIgnored": "TLS/SSL errors ignored",
"ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability",
"YourPhoto": "Profile photo",
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
"save": "Save",
"DeleteAccountTitle": "Remove account",
"DeleteAccountButton": "Remove account",
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteWarningTitle": "Really remove this account?",
"authRegisterFirstName": "Name",
"authRegisterLastName": "Surname",
"authRegisterEmail": "Email",
"authRegisterEmailRequired": "To continue, please enter your email address",
"authRegisterEmailInvalid": "Please enter a valid email address",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
"selectFile": "Select File",
"parsingFailed": "Parsing failed",
"uploadSuccess": "Monitors created successfully!",
"validationFailed": "Validation failed",
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
},
"publicLink": "Public link",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
"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",
"greeting": {
"prepend": "Hey there",
"append": "The afternoon is your playground—let's make it epic!",
"overview": "Here's an overview of your {{type}} monitors."
},
"monitorStatus": {
"up": "up",
"down": "down",
"paused": "paused"
},
"roles": {
"superAdmin": "Super admin",
"admin": "Admin",
"teamMember": "Team member",
"demoUser": "Demo user"
},
"teamPanel": {
"teamMembers": "Team members",
"filter": {
"all": "All",
"member": "Member"
},
"inviteTeamMember": "Invite a team member",
"inviteNewTeamMember": "Invite new team member",
"inviteDescription": "When you add a new team member, they will get access to all monitors.",
"email": "Email",
"selectRole": "Select role",
"inviteLink": "Invite link",
"cancel": "Cancel",
"noMembers": "There are no team members with this role",
"getToken": "Get token",
"emailToken": "E-mail token",
"table": {
"name": "Name",
"email": "Email",
"role": "Role",
"created": "Created"
}
},
"monitorState": {
"paused": "Paused",
"resumed": "Resumed",
"active": "Active"
},
"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",
"logOut": "Log out"
},
"navControls": "Controls",
"statusBreadCrumbsStatusPages": "Status Pages",
"statusBreadCrumbsDetails": "Details",
"incidentsPageTitle": "Incidents",
"passwordPanel": {
"passwordChangedSuccess": "Your password was changed successfully.",
"passwordInputIncorrect": "Your password input was incorrect.",
"currentPassword": "Current password",
"enterCurrentPassword": "Enter your current password",
"newPassword": "New password",
"enterNewPassword": "Enter your new password",
"confirmNewPassword": "Confirm new password",
"passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.",
"saving": "Saving..."
},
"uptimeCreateSelectURL": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard."
}
+99 -16
View File
@@ -16,6 +16,7 @@
"commonBack": "Назад",
"authForgotPasswordTitle": "Забыли пароль?",
"authForgotPasswordResetPassword": "Сбросить пароль",
"authForgotPasswordInstructions": "Не волнуйтесь, мы отправим вам инструкции по сбросу.",
"createPassword": "Создайте свой пароль",
"createAPassword": "Пароль",
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
@@ -65,7 +66,18 @@
"authRegisterLastName": "Фамилия",
"authRegisterEmail": "Эл. почта",
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
"authRegisterEmailInvalid": "Пожалуйста, введите действительный адрес электронной почты",
"bulkImport": {
"title": "Массовый импорт",
"selectFileTips": "Выберите CSV-файл для загрузки",
"selectFileDescription": "Вы можете скачать наш <template>шаблон</template> или <sample>пример</sample>",
"selectFile": "Выбрать файл",
"parsingFailed": "Ошибка анализа",
"uploadSuccess": "Мониторы успешно созданы!",
"validationFailed": "Ошибка проверки",
"noFileSelected": "Файл не выбран",
"fallbackPage": "Импортируйте файл для загрузки списка серверов"
},
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
@@ -74,6 +86,8 @@
"settingsAppearance": "Внешний вид",
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
"settingsThemeMode": "Тема",
"settingsThemeModeLight": "Светлая",
"settingsThemeModeDark": "Тёмная",
"settingsLanguage": "Язык",
"settingsDistributedUptime": "Distributed uptime",
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
@@ -88,6 +102,7 @@
"settingsClearAllStatsDialogTitle": "Хотите очистить всю статистику?",
"settingsClearAllStatsDialogDescription": "После удаления история мониторинга и статистика не могут быть восстановлены.",
"settingsClearAllStatsDialogConfirm": "Да, очистить всю статистику",
"settingsClearAllStatsDialogCancel": "Отмена",
"settingsDemoMonitors": "Демо мониторы",
"settingsDemoMonitorsDescription": "Добавьте примеры мониторов для демонстрации.",
"settingsAddDemoMonitors": "Добавление демонстрационных мониторов",
@@ -103,6 +118,8 @@
"settingsAbout": "О",
"settingsDevelopedBy": "Developed by Bluewave Labs.",
"settingsSave": "Сохранить",
"commonSave": "Сохранить",
"commonSaving": "Сохранение...",
"settingsSuccessSaved": "Настройки успешно сохранены",
"settingsFailedToSave": "Не удалось сохранить настройки",
"settingsStatsCleared": "Статистика успешно очищена",
@@ -295,7 +312,6 @@
"integrationsDiscordInfo": "Подключитесь к Discord и просматривайте инциденты прямо в канале",
"integrationsZapier": "Zapier",
"integrationsZapierInfo": "Отправляйте все инциденты в Zapier, и тогда вы сможете видеть их везде",
"commonSave": "",
"createYour": "",
"createMonitor": "",
"pause": "",
@@ -315,19 +331,6 @@
"pingMonitoring": "",
"pingMonitoringDescription": "",
"dockerContainerMonitoring": "",
"dockerContainerMonitoringDescription": "",
"portMonitoring": "",
"portMonitoringDescription": "",
"createMaintenanceWindow": "",
"createMaintenance": "",
"editMaintenance": "",
"maintenanceWindowName": "",
"friendlyNameInput": "",
"friendlyNamePlaceholder": "",
"maintenanceRepeat": "",
"maintenance": "",
"duration": "",
"addMonitors": "",
"window": "",
"cancel": "",
"message": "",
@@ -391,7 +394,87 @@
"uptimeCreateJsonPath": "",
"uptimeCreateJsonPathQuery": "",
"maintenanceTableActionMenuDialogTitle": "",
"incidentsPageTitle": "Инциденты",
"passwordPanel": {
"passwordChangedSuccess": "Ваш пароль был успешно изменен.",
"passwordInputIncorrect": "Ваш пароль введен неверно.",
"currentPassword": "Текущий пароль",
"enterCurrentPassword": "Введите ваш текущий пароль",
"newPassword": "Новый пароль",
"enterNewPassword": "Введите ваш новый пароль",
"confirmNewPassword": "Подтвердите новый пароль",
"passwordRequirements": "Новый пароль должен содержать не менее 8 символов и должен иметь как минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ.",
"saving": "Сохранение..."
},
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
"pageSpeedAddApiKey": "как добавить ваш ключ API."
"pageSpeedAddApiKey": "как добавить ваш ключ API.",
"createNew": "Создать новый",
"greeting": {
"prepend": "Привет",
"append": "День прекрасен для новых достижений!",
"overview": "Вот обзор ваших мониторов {{type}}."
},
"monitorStatus": {
"up": "работает",
"down": "не работает",
"paused": "приостановлен"
},
"roles": {
"superAdmin": "Суперадминистратор",
"admin": "Администратор",
"teamMember": "Член команды",
"demoUser": "Демо-пользователь"
},
"teamPanel": {
"teamMembers": "Члены команды",
"filter": {
"all": "Все",
"member": "Член"
},
"inviteTeamMember": "Пригласить члена команды",
"inviteNewTeamMember": "Пригласить нового члена команды",
"inviteDescription": "Когда вы добавляете нового члена команды, он получит доступ ко всем мониторам.",
"email": "Эл. почта",
"selectRole": "Выберите роль",
"inviteLink": "Ссылка для приглашения",
"cancel": "Отмена",
"noMembers": "Нет членов команды с этой ролью",
"getToken": "Получить токен",
"emailToken": "Отправить токен по эл. почте",
"table": {
"name": "Имя",
"email": "Эл. почта",
"role": "Роль",
"created": "Создан"
}
},
"monitorState": {
"paused": "приостановлен",
"resumed": "возобновлен",
"active": "активный"
},
"menu": {
"uptime": "Аптайм",
"pagespeed": "Скорость страницы",
"infrastructure": "Инфраструктура",
"distributedUptime": "Распределенный Аптайм",
"incidents": "Инциденты",
"statusPages": "Страницы статуса",
"maintenance": "Обслуживание",
"integrations": "Интеграции",
"settings": "Настройки",
"support": "Поддержка",
"profile": "Профиль",
"password": "Пароль",
"team": "Команда",
"logOut": "Выйти",
"discussions": "Обсуждения",
"docs": "Документация",
"changelog": "История изменений"
},
"navControls": "Управление",
"statusBreadCrumbsStatusPages": "Страницы статуса",
"statusBreadCrumbsDetails": "Подробности",
"uptimeCreateSelectURL": "Введите URL или IP для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет показано на панели управления."
}
+101 -16
View File
@@ -16,6 +16,7 @@
"commonBack": "Geri",
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
"authForgotPasswordResetPassword": "Parola sıfırla",
"authForgotPasswordInstructions": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.",
"createPassword": "Parolanızı oluşturun",
"createAPassword": "Parola",
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
@@ -69,6 +70,8 @@
"settingsAppearance": "Görünüm",
"settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin",
"settingsThemeMode": "Tema",
"settingsThemeModeLight": "Açık",
"settingsThemeModeDark": "Koyu",
"settingsLanguage": "Dil",
"settingsDistributedUptime": "Dağıtılmış çalışma süresi",
"settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
@@ -83,6 +86,7 @@
"settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?",
"settingsClearAllStatsDialogDescription": "Kaldırıldıktan sonra, izleme geçmişi ve istatistikleri geri alınamaz.",
"settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle",
"settingsClearAllStatsDialogCancel": "İptal",
"settingsDemoMonitors": "Demo monitörler",
"settingsDemoMonitorsDescription": "Gösterim amaçlı örnek monitörler ekleyin.",
"settingsAddDemoMonitors": "Demo monitörler ekleniyor",
@@ -289,6 +293,7 @@
"integrationsZapier": "Zapier",
"integrationsZapierInfo": "",
"commonSave": "Kaydet",
"commonSaving": "Kaydediliyor...",
"createYour": "",
"createMonitor": "Monitör oluştur",
"pause": "Beklet",
@@ -389,12 +394,12 @@
"infrastructureMonitorCreated": "",
"infrastructureMonitorUpdated": "",
"errorInvalidTypeId": "",
"errorInvalidFieldId": "",
"inviteNoTokenFound": "",
"pageSpeedWarning": "",
"pageSpeedLearnMoreLink": "",
"pageSpeedAddApiKey": "",
"update": "",
"pageSpeedWarning": "Uyarı: Google PageSpeed API anahtarı eklemediniz. Bu olmadan, PageSpeed monitörü çalışmayacaktır.",
"pageSpeedLearnMoreLink": "Daha fazla bilgi için tıklayın",
"pageSpeedAddApiKey": "API anahtarınızı nasıl ekleyeceğinizi öğrenin.",
"pageSpeedApiKeyFieldDescription": "Sayfa hızı izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.",
"pageSpeedApiKeyFieldResetLabel": "API anahtarı ayarlandı. Değiştirmek için Sıfırla'ya tıklayın.",
"reset": "Sıfırla",
"invalidFileFormat": "",
"invalidFileSize": "",
"ClickUpload": "",
@@ -418,14 +423,94 @@
"authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
"authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin",
"bulkImport": {
"title": "",
"selectFileTips": "",
"selectFileDescription": "",
"selectFile": "",
"parsingFailed": "",
"uploadSuccess": "",
"validationFailed": "",
"noFileSelected": "",
"fallbackPage": ""
}
"title": "Toplu İçe Aktar",
"selectFileTips": "Yüklemek için CSV dosyası seçin",
"selectFileDescription": "<template>Şablonumuzu</template> veya <sample>örneğimizi</sample> indirebilirsiniz",
"selectFile": "Dosya Seç",
"parsingFailed": "Ayrıştırma başarısız oldu",
"uploadSuccess": "Monitörler başarıyla oluşturuldu!",
"validationFailed": "Doğrulama başarısız oldu",
"noFileSelected": "Dosya seçilmedi",
"fallbackPage": "Sunucuların listesini toplu olarak yüklemek için bir dosya içe aktarın"
},
"createNew": "Yeni oluştur",
"greeting": {
"prepend": "Merhaba",
"append": "Öğleden sonra senin oyun alanın—hadi onu muhteşem yapalım!",
"overview": "İşte {{type}} monitörlerinizin genel görünümü."
},
"monitorStatus": {
"up": "aktif",
"down": "devre dışı",
"paused": "duraklatıldı"
},
"roles": {
"superAdmin": "Süper yönetici",
"admin": "Yönetici",
"teamMember": "Takım üyesi",
"demoUser": "Demo kullanıcı"
},
"teamPanel": {
"teamMembers": "Takım üyeleri",
"filter": {
"all": "Tümü",
"member": "Üye"
},
"inviteTeamMember": "Takım üyesi davet et",
"inviteNewTeamMember": "Yeni takım üyesi davet et",
"inviteDescription": "Yeni bir takım üyesi eklediğinizde, tüm monitörlere erişim hakkı alacaktır.",
"email": "E-posta",
"selectRole": "Rol seçin",
"inviteLink": "Davet bağlantısı",
"cancel": "İptal",
"noMembers": "Bu role sahip takım üyesi bulunmamaktadır",
"getToken": "Token al",
"emailToken": "Token e-posta ile gönder",
"table": {
"name": "Ad",
"email": "E-posta",
"role": "Rol",
"created": "Oluşturuldu"
}
},
"monitorState": {
"paused": "duraklatıldı",
"resumed": "devam ettirildi",
"active": "aktif"
},
"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",
"logOut": "Çıkış"
},
"navControls": "Kontroller",
"statusBreadCrumbsStatusPages": "Durum Sayfaları",
"statusBreadCrumbsDetails": "Detaylar",
"incidentsPageTitle": "Olaylar",
"passwordPanel": {
"passwordChangedSuccess": "Şifreniz başarıyla değiştirildi.",
"passwordInputIncorrect": "Şifre girişiniz yanlış.",
"currentPassword": "Mevcut şifre",
"enterCurrentPassword": "Mevcut şifrenizi girin",
"newPassword": "Yeni şifre",
"enterNewPassword": "Yeni şifrenizi girin",
"confirmNewPassword": "Yeni şifreyi onaylayın",
"passwordRequirements": "Yeni şifre en az 8 karakter içermeli ve en az bir büyük harf, bir küçük harf, bir rakam ve bir özel karakter içermelidir.",
"saving": "Kaydediliyor..."
},
"uptimeCreateSelectURL": "İzlenecek URL veya IP adresini girin (örn. https://example.com/ veya 192.168.1.100) ve kontrol panelinde görünecek net bir görüntü adı ekleyin."
}
-4
View File
@@ -24,8 +24,6 @@ services:
redis:
image: uptime_redis:latest
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
healthcheck:
@@ -38,8 +36,6 @@ services:
image: uptime_mongo:latest
restart: always
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
healthcheck:
+3 -6
View File
@@ -3,7 +3,7 @@ services:
image: ghcr.io/bluewave-labs/checkmate:frontend-dist
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1"
UPTIME_APP_CLIENT_HOST: "http://localhost"
ports:
- "80:80"
@@ -14,7 +14,7 @@ services:
image: ghcr.io/bluewave-labs/checkmate:backend-dist
restart: always
ports:
- "5000:5000"
- "52345:52345"
depends_on:
- redis
- mongodb
@@ -22,13 +22,12 @@ services:
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
- REDIS_URL=redis://redis:6379
- CLIENT_HOST=http://localhost
- JWT_SECRET=my_secret
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: ghcr.io/bluewave-labs/checkmate:redis-dist
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
healthcheck:
@@ -43,8 +42,6 @@ services:
volumes:
- ./mongo/data:/data/db
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
interval: 5s
-4
View File
@@ -36,8 +36,6 @@ services:
redis:
image: ghcr.io/bluewave-labs/checkmate:redis-demo
restart: always
ports:
- "6379:6379"
volumes:
- ./redis/data:/data
healthcheck:
@@ -50,8 +48,6 @@ services:
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
restart: always
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
ports:
- "27017:27017"
volumes:
- ./mongo/data:/data/db
# - ./mongo/init/init.js:/docker-entrypoint-initdb.d/init.js // No longer needed
+12 -9
View File
@@ -582,14 +582,18 @@ class MonitorController {
}
try {
const monitor = await Monitor.findOneAndUpdate({ _id: req.params.monitorId }, [
{
$set: {
isActive: { $not: "$isActive" },
status: "$$REMOVE",
const monitor = await Monitor.findOneAndUpdate(
{ _id: req.params.monitorId },
[
{
$set: {
isActive: { $not: "$isActive" },
status: "$$REMOVE",
},
},
},
]);
],
{ new: true }
);
monitor.isActive === true
? await this.jobQueue.deleteJob(monitor)
: await this.jobQueue.addJob(monitor._id, monitor);
@@ -649,9 +653,8 @@ class MonitorController {
sendTestEmail = async (req, res, next) => {
try {
const { to } = req.body;
if (!to || typeof to !== "string") {
return res.error({ msg: this.stringService.errorForValidEmailAddress });
throw new Error(this.stringService.errorForValidEmailAddress);
}
const subject = this.stringService.testEmailSubject;
+15 -2
View File
@@ -13,12 +13,25 @@ class SettingsController {
getAppSettings = async (req, res, next) => {
const dbSettings = await this.settingsService.getDBSettings();
const sanitizedSettings = { ...dbSettings };
const returnSettings = {
pagespeedKeySet: false,
emailPasswordSet: false,
};
if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") {
sanitizedSettings.pagespeedApiKey = "********";
returnSettings.pagespeedKeySet = true;
delete sanitizedSettings.pagespeedApiKey;
}
if (typeof sanitizedSettings.systemEmailPassword !== "undefined") {
returnSettings.emailPasswordSet = true;
delete sanitizedSettings.systemEmailPassword;
}
returnSettings.settings = sanitizedSettings;
return res.success({
msg: this.stringService.getAppSettings,
data: sanitizedSettings,
data: returnSettings,
});
};
+11 -1
View File
@@ -2,13 +2,16 @@ import mongoose from "mongoose";
const AppSettingsSchema = mongoose.Schema(
{
checkTTL: {
type: Number,
default: 30,
},
language: {
type: String,
default: "gb",
},
pagespeedApiKey: {
type: String,
default: "",
},
systemEmailHost: {
type: String,
@@ -22,12 +25,19 @@ const AppSettingsSchema = mongoose.Schema(
systemEmailPassword: {
type: String,
},
systemEmailUser: {
type: String,
},
singleton: {
type: Boolean,
required: true,
unique: true,
default: true,
},
version: {
type: Number,
default: 1,
},
},
{
timestamps: true,
+16 -6
View File
@@ -14,12 +14,22 @@ const getAppSettings = async () => {
const updateAppSettings = async (newSettings) => {
try {
console.log(newSettings);
const settings = await AppSettings.findOneAndUpdate(
{},
{ $set: newSettings },
{ new: true, upsert: true }
);
const update = { $set: { ...newSettings } };
if (newSettings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
}
if (newSettings.systemEmailPassword === "") {
update.$unset = { systemEmailPassword: "" };
delete update.$set.systemEmailPassword;
}
const settings = await AppSettings.findOneAndUpdate({}, update, {
new: true,
upsert: true,
});
return settings;
} catch (error) {
error.service = SERVICE_NAME;
+2 -2
View File
@@ -158,8 +158,8 @@
"platformRequired": "Platform is required",
"testNotificationFailed": "Failed to send test notification",
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\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",
"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",
"errorForValidEmailAddress": "A valid recipient email address is required.",
"testEmailSubject": "Test Email from Monitoring System"
"testEmailSubject": "Test E-mail from Checkmate"
}
+28 -22
View File
@@ -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<string>} 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);
@@ -125,6 +130,7 @@ class EmailService {
try {
const info = await this.transporter.sendMail({
to: to,
from: systemEmailAddress,
subject: subject,
html: html,
});
+12 -3
View File
@@ -36,7 +36,6 @@ class NetworkService {
this.net = net;
this.stringService = stringService;
this.settingsService = settingsService;
this.settings = settingsService.getSettings();
}
/**
@@ -254,8 +253,18 @@ class NetworkService {
const url = job.data.url;
const updatedJob = { ...job };
let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (this.settings?.pagespeedApiKey) {
pagespeedUrl += `&key=${this.settings.pagespeedApiKey}`;
const dbSettings = await this.settingsService.getDBSettings();
if (dbSettings?.pagespeedApiKey) {
pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
} else {
this.logger.info({
message: "Pagespeed API key not found",
service: this.SERVICE_NAME,
method: "requestPagespeed",
details: { url },
});
return;
}
updatedJob.data.url = pagespeedUrl;
return await this.requestHttp(updatedJob);
+14 -1
View File
@@ -59,7 +59,20 @@ class SettingsService {
}
async getDBSettings() {
const settings = await this.appSettings.findOne({ singleton: true }).lean();
// Remove any old settings
await this.appSettings.deleteMany({ version: { $exists: false } });
let settings = await this.appSettings
.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
if (settings === null) {
await this.appSettings.create({});
settings = await this.appSettings
.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
}
return settings;
}
}
+4
View File
@@ -428,6 +428,10 @@ class StringService {
get dbUserExists() {
return this.translationService.getTranslation("dbUserExists");
}
get testEmailSubject() {
return this.translationService.getTranslation("testEmailSubject");
}
}
export default StringService;
+2 -9
View File
@@ -424,21 +424,14 @@ const editMaintenanceByIdWindowBodyValidation = joi.object({
// SettingsValidation
//****************************************
const updateAppSettingsBodyValidation = joi.object({
apiBaseUrl: joi.string().allow(""),
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
clientHost: joi.string().allow(""),
dbType: joi.string().allow(""),
dbConnectionString: joi.string().allow(""),
redisHost: joi.string().allow(""),
redisPort: joi.number().allow(null, ""),
redisUrl: joi.string().allow(""),
jwtTTL: joi.string().allow(""),
checkTTL: joi.number().allow(""),
pagespeedApiKey: joi.string().allow(""),
language: joi.string().allow(""),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(""),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
});
//****************************************