mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-17 23:29:42 -06:00
Merge latest changes from develop branch
This commit is contained in:
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -52,42 +52,42 @@ 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 */
|
||||
@@ -121,6 +121,10 @@ function Sidebar() {
|
||||
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);
|
||||
@@ -728,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
client/src/Hooks/useDeleteMonitorStats.js
Normal file
24
client/src/Hooks/useDeleteMonitorStats.js
Normal 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
client/src/Hooks/useFetchSettings.js
Normal file
55
client/src/Hooks/useFetchSettings.js
Normal 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 };
|
||||
32
client/src/Pages/Settings/SettingsAbout.jsx
Normal file
32
client/src/Pages/Settings/SettingsAbout.jsx
Normal file
@@ -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.0}</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;
|
||||
90
client/src/Pages/Settings/SettingsDemoMonitors.jsx
Normal file
90
client/src/Pages/Settings/SettingsDemoMonitors.jsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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 = ({ HEADER_SX, handleChange, isLoading }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
// Local state
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
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 = {
|
||||
handleChange: PropTypes.func,
|
||||
HEADER_SX: PropTypes.object,
|
||||
};
|
||||
|
||||
export default SettingsDemoMonitors;
|
||||
126
client/src/Pages/Settings/SettingsEmail.jsx
Normal file
126
client/src/Pages/Settings/SettingsEmail.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Button from "@mui/material/Button";
|
||||
import Stack from "@mui/material/Stack";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
const SettingsEmail = ({
|
||||
HEADER_SX,
|
||||
handleChange,
|
||||
settingsData,
|
||||
setSettingsData,
|
||||
isPasswordSet,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [hasBeenReset, setHasBeenReset] = useState(false);
|
||||
|
||||
const handlePasswordChange = (e) => {
|
||||
setPassword(e.target.value);
|
||||
setSettingsData({
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, systemEmailPassword: e.target.value },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsEmail.propTypes = {
|
||||
settingsData: PropTypes.object,
|
||||
setSettingsData: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
HEADER_SX: PropTypes.object,
|
||||
isPasswordSet: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SettingsEmail;
|
||||
87
client/src/Pages/Settings/SettingsPagespeed.jsx
Normal file
87
client/src/Pages/Settings/SettingsPagespeed.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const SettingsPagespeed = ({
|
||||
HEADING_SX,
|
||||
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 },
|
||||
});
|
||||
};
|
||||
|
||||
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 = {
|
||||
HEADING_SX: PropTypes.object,
|
||||
settingsData: PropTypes.object,
|
||||
setSettingsData: PropTypes.func,
|
||||
isApiKeySet: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SettingsPagespeed;
|
||||
84
client/src/Pages/Settings/SettingsStats.jsx
Normal file
84
client/src/Pages/Settings/SettingsStats.jsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { PropTypes } from "prop-types";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
const SettingsStats = ({ HEADING_SX, handleChange, settingsData, errors }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
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 = {
|
||||
HEADING_SX: PropTypes.object,
|
||||
handleChange: PropTypes.func,
|
||||
settingsData: PropTypes.object,
|
||||
errors: PropTypes.object,
|
||||
};
|
||||
|
||||
export default SettingsStats;
|
||||
43
client/src/Pages/Settings/SettingsTimeZone.jsx
Normal file
43
client/src/Pages/Settings/SettingsTimeZone.jsx
Normal file
@@ -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
client/src/Pages/Settings/SettingsUI.jsx
Normal file
52
client/src/Pages/Settings/SettingsUI.jsx
Normal 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;
|
||||
@@ -1,134 +1,69 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography, Button, Switch } 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,
|
||||
setDistributedUptimeEnabled,
|
||||
setLanguage,
|
||||
} from "../../Features/UI/uiSlice";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../main";
|
||||
import { settingsValidation } from "../../Validation/validation";
|
||||
import { updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings";
|
||||
import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats";
|
||||
|
||||
// Constants
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
|
||||
|
||||
const Settings = () => {
|
||||
const theme = useTheme();
|
||||
const { t, i18n } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
// Redux state
|
||||
const { mode, language, timezone } = useSelector((state) => state.ui);
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { language } = useSelector((state) => state.ui);
|
||||
const { checkTTL } = user;
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
|
||||
const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui);
|
||||
const { mode } = useSelector((state) => state.ui);
|
||||
const [checksIsLoading, setChecksIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
enableDistributedUptime: distributedUptimeEnabled,
|
||||
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
const [version, setVersion] = useState("unknown");
|
||||
const [apiKeyFieldType, setApiKeyFieldType] = useState("password");
|
||||
const [isApiKeySet, setIsApiKeySet] = useState(false);
|
||||
|
||||
// Local state
|
||||
const [settingsData, setSettingsData] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
|
||||
const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
|
||||
// Network
|
||||
const [isSettingsLoading, settingsError] = useFetchSettings({
|
||||
setSettingsData,
|
||||
});
|
||||
|
||||
const [isSaving, saveError, saveSettings] = useSaveSettings();
|
||||
const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats();
|
||||
|
||||
// Setup
|
||||
const theme = useTheme();
|
||||
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
|
||||
const { t, i18n } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
//Fetching latest release version from github
|
||||
useEffect(() => {
|
||||
const fetchLatestVersion = async () => {
|
||||
let version = "unknown";
|
||||
try {
|
||||
const response = await networkService.fetchGithubLatestRelease();
|
||||
if (!response.status === 200) {
|
||||
throw new Error("Failed to fetch latest version");
|
||||
}
|
||||
version = response.data.tag_name;
|
||||
} catch (error) {
|
||||
createToast({ body: error.message || "Error fetching latest version" }); // Set error message
|
||||
} finally {
|
||||
setVersion(version);
|
||||
}
|
||||
// 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());
|
||||
}, []);
|
||||
|
||||
const { pagespeedApiKey } = useSelector((state) => state.settings);
|
||||
|
||||
useEffect(() => {
|
||||
if (pagespeedApiKey) {
|
||||
setIsApiKeySet(true);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
pagespeedApiKey: t("maskedPageSpeedKeyPlaceholder"),
|
||||
}));
|
||||
} else {
|
||||
setIsApiKeySet(false);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
pagespeedApiKey: "",
|
||||
}));
|
||||
}
|
||||
}, [pagespeedApiKey]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { type, checked, value, id } = event.target;
|
||||
|
||||
if (type === "checkbox") {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[id]: checked,
|
||||
}));
|
||||
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 {
|
||||
@@ -137,385 +72,130 @@ 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: 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 languages = Object.keys(i18n.options.resources || {});
|
||||
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);
|
||||
};
|
||||
|
||||
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
|
||||
HEADING_SX={HEADING_SX}
|
||||
settingsData={settingsData}
|
||||
setSettingsData={setSettingsData}
|
||||
isApiKeySet={settingsData?.pagespeedKeySet ?? false}
|
||||
/>
|
||||
<SettingsStats
|
||||
HEADING_SX={HEADING_SX}
|
||||
settingsData={settingsData}
|
||||
handleChange={handleChange}
|
||||
errors={errors}
|
||||
/>
|
||||
<SettingsDemoMonitors
|
||||
HEADER_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
isLoading={isSettingsLoading || isSaving || isDeletingMonitorStats}
|
||||
/>
|
||||
<SettingsEmail
|
||||
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: t("settingsThemeModeLight") },
|
||||
{ _id: "dark", name: t("settingsThemeModeDark") },
|
||||
]}
|
||||
></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}
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,8 @@ 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 passwordSchema = joi
|
||||
@@ -152,20 +153,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 +255,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 +370,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 +387,5 @@ export {
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
statusPageValidation,
|
||||
logoImageValidation
|
||||
logoImageValidation,
|
||||
};
|
||||
|
||||
@@ -1,451 +1,515 @@
|
||||
{
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
|
||||
"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",
|
||||
"statusBreadCrumbsStatusPages": "Status Pages",
|
||||
"statusBreadCrumbsDetails": "Details",
|
||||
"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.",
|
||||
"navControls": "Controls"
|
||||
"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",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"commonBack": "Назад",
|
||||
"authForgotPasswordTitle": "Забыли пароль?",
|
||||
"authForgotPasswordResetPassword": "Сбросить пароль",
|
||||
"authForgotPasswordInstructions": "Не волнуйтесь, мы отправим вам инструкции по сбросу пароля.",
|
||||
"createPassword": "Создайте свой пароль",
|
||||
"createAPassword": "Пароль",
|
||||
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
|
||||
@@ -66,7 +65,18 @@
|
||||
"authRegisterLastName": "Фамилия",
|
||||
"authRegisterEmail": "Эл. почта",
|
||||
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
|
||||
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
|
||||
"authRegisterEmailInvalid": "Пожалуйста, введите действительный адрес электронной почты",
|
||||
"bulkImport": {
|
||||
"title": "Массовый импорт",
|
||||
"selectFileTips": "Выберите CSV-файл для загрузки",
|
||||
"selectFileDescription": "Вы можете скачать наш <template>шаблон</template> или <sample>пример</sample>",
|
||||
"selectFile": "Выбрать файл",
|
||||
"parsingFailed": "Ошибка анализа",
|
||||
"uploadSuccess": "Мониторы успешно созданы!",
|
||||
"validationFailed": "Ошибка проверки",
|
||||
"noFileSelected": "Файл не выбран",
|
||||
"fallbackPage": "Импортируйте файл для загрузки списка серверов"
|
||||
},
|
||||
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
|
||||
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
|
||||
"settingsGeneralSettings": "Общие настройки",
|
||||
@@ -75,8 +85,6 @@
|
||||
"settingsAppearance": "Внешний вид",
|
||||
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
|
||||
"settingsThemeMode": "Тема",
|
||||
"settingsThemeModeLight": "Светлая",
|
||||
"settingsThemeModeDark": "Темная",
|
||||
"settingsLanguage": "Язык",
|
||||
"settingsDistributedUptime": "Distributed uptime",
|
||||
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
|
||||
@@ -397,11 +405,67 @@
|
||||
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
|
||||
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
|
||||
"pageSpeedAddApiKey": "как добавить ваш ключ API.",
|
||||
"pageSpeedApiKeyFieldDescription": "Введите ваш API-ключ Google PageSpeed для включения мониторинга скорости страницы. Нажмите Сбросить, чтобы обновить ключ.",
|
||||
"pageSpeedApiKeyFieldResetLabel": "API-ключ установлен. Нажмите Сбросить, чтобы изменить его.",
|
||||
"reset": "Сбросить",
|
||||
"statusBreadCrumbsStatusPages": "Страницы статуса",
|
||||
"statusBreadCrumbsDetails": "Подробности",
|
||||
"uptimeCreateSelectURL": "Введите URL или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет показано на панели управления.",
|
||||
"navControls": "Управление"
|
||||
"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": "Поддержка",
|
||||
"discussions": "Обсуждения",
|
||||
"docs": "Документация",
|
||||
"changelog": "История изменений",
|
||||
"profile": "Профиль",
|
||||
"password": "Пароль",
|
||||
"team": "Команда"
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,8 @@
|
||||
"authLoginEnterPassword": "Parolanızı girin",
|
||||
"commonPassword": "Parola",
|
||||
"commonBack": "Geri",
|
||||
"authForgotPasswordTitle": "Şifrenizi mi unuttunuz?",
|
||||
"authForgotPasswordResetPassword": "Şifreyi sıfırla",
|
||||
"authForgotPasswordInstructions": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.",
|
||||
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
|
||||
"authForgotPasswordResetPassword": "Parola sıfırla",
|
||||
"createPassword": "Parolanızı oluşturun",
|
||||
"createAPassword": "Parola",
|
||||
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
|
||||
@@ -70,8 +69,6 @@
|
||||
"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.",
|
||||
@@ -421,18 +418,77 @@
|
||||
"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"
|
||||
},
|
||||
"statusBreadCrumbsStatusPages": "Durum Sayfaları",
|
||||
"statusBreadCrumbsDetails": "Detaylar",
|
||||
"uptimeCreateSelectURL": "İzlenecek URL veya IP adresini girin (örneğin, https://example.com/ veya 192.168.1.100) ve kontrol panelinde görünecek net bir görüntü adı ekleyin.",
|
||||
"navControls": "Kontroller"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ services:
|
||||
ports:
|
||||
- "80:80"
|
||||
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"
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d/
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
image: uptime_server:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
@@ -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:
|
||||
|
||||
@@ -12,7 +12,7 @@ server {
|
||||
}
|
||||
|
||||
# location /api/ {
|
||||
# proxy_pass http://server:5000/api/;
|
||||
# proxy_pass http://server:52345/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -24,12 +24,12 @@ server {
|
||||
# proxy_cache off;
|
||||
# }
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
# location /api-docs/ {
|
||||
# proxy_pass http://server:52345/api-docs/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
}
|
||||
@@ -8,6 +8,6 @@ RUN npm install
|
||||
|
||||
COPY ./server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
6
docker/dist/docker-compose.yaml
vendored
6
docker/dist/docker-compose.yaml
vendored
@@ -14,7 +14,7 @@ services:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-dist
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
@@ -27,8 +27,6 @@ services:
|
||||
redis:
|
||||
image: ghcr.io/bluewave-labs/checkmate:redis-dist
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ./redis/data:/data
|
||||
healthcheck:
|
||||
@@ -43,8 +41,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
|
||||
|
||||
2
docker/dist/server.Dockerfile
vendored
2
docker/dist/server.Dockerfile
vendored
@@ -8,6 +8,6 @@ RUN npm install
|
||||
|
||||
COPY ./server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-demo
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -25,7 +25,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -50,7 +50,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -59,7 +59,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -10,6 +10,6 @@ RUN npm install
|
||||
|
||||
COPY ./server ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-staging
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
|
||||
@@ -16,7 +16,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -29,7 +29,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -54,7 +54,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -67,7 +67,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -10,6 +10,6 @@ RUN npm install
|
||||
|
||||
COPY ./server ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -649,9 +649,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;
|
||||
|
||||
@@ -11,16 +11,28 @@ class SettingsController {
|
||||
}
|
||||
|
||||
getAppSettings = async (req, res, next) => {
|
||||
try {
|
||||
const settings = { ...(await this.settingsService.getSettings()) };
|
||||
delete settings.jwtSecret;
|
||||
return res.success({
|
||||
msg: this.stringService.getAppSettings,
|
||||
data: settings,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAppSettings"));
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
const sanitizedSettings = { ...dbSettings };
|
||||
|
||||
const returnSettings = {
|
||||
pagespeedKeySet: false,
|
||||
emailPasswordSet: false,
|
||||
};
|
||||
|
||||
if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") {
|
||||
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: returnSettings,
|
||||
});
|
||||
};
|
||||
|
||||
updateAppSettings = async (req, res, next) => {
|
||||
|
||||
@@ -2,64 +2,31 @@ import mongoose from "mongoose";
|
||||
|
||||
const AppSettingsSchema = mongoose.Schema(
|
||||
{
|
||||
apiBaseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "http://localhost:5000/api/v1",
|
||||
checkTTL: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
},
|
||||
logLevel: {
|
||||
language: {
|
||||
type: String,
|
||||
default: "debug",
|
||||
enum: ["debug", "none", "error", "warn"],
|
||||
},
|
||||
clientHost: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "http://localhost:5173",
|
||||
},
|
||||
jwtSecret: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "my_secret",
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "MongoDB",
|
||||
},
|
||||
dbConnectionString: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "mongodb://localhost:27017/uptime_db",
|
||||
},
|
||||
redisUrl: {
|
||||
type: String,
|
||||
default: "redis://127.0.0.1:6379",
|
||||
},
|
||||
jwtTTL: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "2h",
|
||||
default: "gb",
|
||||
},
|
||||
pagespeedApiKey: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailHost: {
|
||||
type: String,
|
||||
default: "smtp.gmail.com",
|
||||
},
|
||||
systemEmailPort: {
|
||||
type: Number,
|
||||
default: 465,
|
||||
},
|
||||
systemEmailAddress: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailPassword: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailUser: {
|
||||
type: String,
|
||||
},
|
||||
singleton: {
|
||||
type: Boolean,
|
||||
@@ -73,4 +40,4 @@ const AppSettingsSchema = mongoose.Schema(
|
||||
}
|
||||
);
|
||||
|
||||
export default mongoose.model("AppSettings", AppSettingsSchema);
|
||||
export default mongoose.model("AppSettings", AppSettingsSchema);
|
||||
|
||||
@@ -76,7 +76,8 @@ import * as diagnosticModule from "./modules/diagnosticModule.js";
|
||||
class MongoDB {
|
||||
static SERVICE_NAME = "MongoDB";
|
||||
|
||||
constructor() {
|
||||
constructor({ appSettings }) {
|
||||
this.appSettings = appSettings;
|
||||
Object.assign(this, userModule);
|
||||
Object.assign(this, inviteModule);
|
||||
Object.assign(this, recoveryModule);
|
||||
@@ -95,8 +96,7 @@ class MongoDB {
|
||||
connect = async () => {
|
||||
try {
|
||||
const connectionString =
|
||||
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
|
||||
console.log("Connecting to MongoDB with connection string:", connectionString);
|
||||
this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
|
||||
await mongoose.connect(connectionString);
|
||||
// If there are no AppSettings, create one
|
||||
await AppSettings.findOneAndUpdate(
|
||||
|
||||
@@ -14,11 +14,22 @@ const getAppSettings = async () => {
|
||||
|
||||
const updateAppSettings = async (newSettings) => {
|
||||
try {
|
||||
const settings = await AppSettings.findOneAndUpdate(
|
||||
{},
|
||||
{ $set: newSettings },
|
||||
{ new: 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;
|
||||
|
||||
@@ -100,8 +100,6 @@ const openApiSpec = JSON.parse(
|
||||
|
||||
let server;
|
||||
|
||||
const PORT = 5000;
|
||||
|
||||
const shutdown = async () => {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
@@ -139,19 +137,21 @@ const shutdown = async () => {
|
||||
// Need to wrap server setup in a function to handle async nature of JobQueue
|
||||
const startApp = async () => {
|
||||
const app = express();
|
||||
const allowedOrigin = process.env.CLIENT_HOST;
|
||||
// Create and Register Primary services
|
||||
const translationService = new TranslationService(logger);
|
||||
const stringService = new StringService(translationService);
|
||||
ServiceRegistry.register(StringService.SERVICE_NAME, stringService);
|
||||
|
||||
// Create DB
|
||||
const db = new MongoDB();
|
||||
await db.connect();
|
||||
|
||||
// Create services
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
await settingsService.loadSettings();
|
||||
const appSettings = settingsService.loadSettings();
|
||||
|
||||
// Create DB
|
||||
const db = new MongoDB({ appSettings });
|
||||
await db.connect();
|
||||
|
||||
// Set allowed origin
|
||||
const allowedOrigin = appSettings.clientHost;
|
||||
|
||||
const networkService = new NetworkService(
|
||||
axios,
|
||||
@@ -215,8 +215,9 @@ const startApp = async () => {
|
||||
|
||||
await translationService.initialize();
|
||||
|
||||
server = app.listen(PORT, () => {
|
||||
logger.info({ message: `server started on port:${PORT}` });
|
||||
const port = appSettings.port || 52345;
|
||||
server = app.listen(port, () => {
|
||||
logger.info({ message: `Server started on port:${port}` });
|
||||
});
|
||||
|
||||
process.on("SIGUSR2", shutdown);
|
||||
@@ -237,7 +238,7 @@ const startApp = async () => {
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
ServiceRegistry.get(JobQueue.SERVICE_NAME),
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME),
|
||||
ServiceRegistry.get(EmailService.SERVICE_NAME),
|
||||
ServiceRegistry.get(EmailService.SERVICE_NAME)
|
||||
);
|
||||
|
||||
const settingsController = new SettingsController(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
const SERVICE_NAME = "SettingsService";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
const envConfig = {
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
logLevel: process.env.LOG_LEVEL,
|
||||
clientHost: process.env.CLIENT_HOST,
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
dbType: process.env.DB_TYPE,
|
||||
dbConnectionString: process.env.DB_CONNECTION_STRING,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
jwtTTL: process.env.TOKEN_TTL,
|
||||
pagespeedApiKey: process.env.PAGESPEED_API_KEY,
|
||||
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
|
||||
systemEmailPort: process.env.SYSTEM_EMAIL_PORT,
|
||||
systemEmailUser: process.env.SYSTEM_EMAIL_USER,
|
||||
systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS,
|
||||
systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD,
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
jwtTTL: process.env.TOKEN_TTL,
|
||||
clientHost: process.env.CLIENT_HOST,
|
||||
dbConnectionString: process.env.DB_CONNECTION_STRING,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
callbackUrl: process.env.CALLBACK_URL,
|
||||
port: process.env.PORT,
|
||||
pagespeedApiKey: process.env.PAGESPEED_API_KEY,
|
||||
uprockApiKey: process.env.UPROCK_API_KEY,
|
||||
};
|
||||
/**
|
||||
* SettingsService
|
||||
*
|
||||
* This service is responsible for loading and managing the application settings.
|
||||
* It gives priority to environment variables and will only load settings
|
||||
* from the database if they are not set in the environment.
|
||||
*/
|
||||
class SettingsService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -34,40 +33,17 @@ class SettingsService {
|
||||
this.settings = { ...envConfig };
|
||||
}
|
||||
/**
|
||||
* Load settings from the database and merge with environment settings.
|
||||
* If there are any settings that weren't set by environment variables, use user settings from the database.
|
||||
* @returns {Promise<Object>} The merged settings.
|
||||
* @throws Will throw an error if settings are not found in the database or if settings have not been loaded.
|
||||
*/ async loadSettings() {
|
||||
try {
|
||||
const dbSettings = await this.appSettings.findOne();
|
||||
if (!this.settings) {
|
||||
throw new Error("Settings not found");
|
||||
}
|
||||
|
||||
// If there are any settings that weren't set by environment variables, use user settings from DB
|
||||
for (const key in envConfig) {
|
||||
if (
|
||||
typeof envConfig?.[key] === "undefined" &&
|
||||
typeof dbSettings?.[key] !== "undefined"
|
||||
) {
|
||||
this.settings[key] = dbSettings[key];
|
||||
}
|
||||
}
|
||||
|
||||
await this.appSettings.updateOne({}, { $set: this.settings }, { upsert: true });
|
||||
return this.settings;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "loadSettings") : null;
|
||||
throw error;
|
||||
}
|
||||
* Load settings from env settings
|
||||
* @returns {Object>} The settings.
|
||||
*/
|
||||
loadSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
/**
|
||||
* Reload settings by calling loadSettings.
|
||||
* @returns {Promise<Object>} The reloaded settings.
|
||||
*/
|
||||
async reloadSettings() {
|
||||
reloadSettings() {
|
||||
return this.loadSettings();
|
||||
}
|
||||
/**
|
||||
@@ -81,6 +57,21 @@ class SettingsService {
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
async getDBSettings() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsService;
|
||||
|
||||
@@ -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(""),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
|
||||
Reference in New Issue
Block a user