mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-04-24 18:08:21 -05:00
Merge branch 'develop' of github.com:bluewave-labs/Checkmate into bug/fix-form-errors
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -68,8 +68,16 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
),
|
||||
},
|
||||
{ id: "cpu", content: t("cpu"), render: (row) => <CustomGauge progress={row.cpu} /> },
|
||||
{ id: "memory", content: t("memory"), render: (row) => <CustomGauge progress={row.mem} /> },
|
||||
{ id: "disk", content: t("disk"), render: (row) => <CustomGauge progress={row.disk} /> },
|
||||
{
|
||||
id: "memory",
|
||||
content: t("memory"),
|
||||
render: (row) => <CustomGauge progress={row.mem} />,
|
||||
},
|
||||
{
|
||||
id: "disk",
|
||||
content: t("disk"),
|
||||
render: (row) => <CustomGauge progress={row.disk} />,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
content: t("actions"),
|
||||
@@ -129,6 +137,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -73,7 +73,7 @@ const InfrastructureMonitors = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoading && monitors?.length === 0) {
|
||||
if (!isLoading && typeof summary?.totalMonitors === "undefined" ) {
|
||||
return (
|
||||
<Fallback
|
||||
vowelStart={true}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "../../Components/Link";
|
||||
|
||||
const SettingsAbout = () => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsAbout")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">Checkmate {2.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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
+150
-464
@@ -1,116 +1,69 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
import { getAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
// import {
|
||||
// WalletMultiButton,
|
||||
// WalletDisconnectButton,
|
||||
// } from "@solana/wallet-adapter-react-ui";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { logger } from "../../Utils/Logger";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Breadcrumbs from "../../Components/Breadcrumbs";
|
||||
import SettingsTimeZone from "./SettingsTimeZone";
|
||||
import SettingsUI from "./SettingsUI";
|
||||
import SettingsPagespeed from "./SettingsPagespeed";
|
||||
import SettingsDemoMonitors from "./SettingsDemoMonitors";
|
||||
import SettingsAbout from "./SettingsAbout";
|
||||
import SettingsEmail from "./SettingsEmail";
|
||||
import Button from "@mui/material/Button";
|
||||
// Utils
|
||||
import { settingsValidation } from "../../Validation/validation";
|
||||
import { createToast } from "../../Utils/toastUtils";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
|
||||
import SettingsStats from "./SettingsStats";
|
||||
import {
|
||||
deleteMonitorChecksByTeamId,
|
||||
addDemoMonitors,
|
||||
deleteAllMonitors,
|
||||
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { update } from "../../Features/Auth/authSlice";
|
||||
import PropTypes from "prop-types";
|
||||
import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../main";
|
||||
import { settingsValidation } from "../../Validation/validation";
|
||||
import { updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings";
|
||||
import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats";
|
||||
|
||||
// Constants
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
|
||||
|
||||
const Settings = () => {
|
||||
const theme = useTheme();
|
||||
const { t, i18n } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
// Redux state
|
||||
const { mode, language, timezone } = useSelector((state) => state.ui);
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { language } = useSelector((state) => state.ui);
|
||||
const { checkTTL } = user;
|
||||
const { isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
|
||||
const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui);
|
||||
const { mode } = useSelector((state) => state.ui);
|
||||
const { pagespeedApiKey } = useSelector((state) => state.settings);
|
||||
const [checksIsLoading, setChecksIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
enableDistributedUptime: distributedUptimeEnabled,
|
||||
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
|
||||
pagespeedApiKey: pagespeedApiKey,
|
||||
|
||||
// Local state
|
||||
const [settingsData, setSettingsData] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
// Network
|
||||
const [isSettingsLoading, settingsError] = useFetchSettings({
|
||||
setSettingsData,
|
||||
});
|
||||
|
||||
const [version, setVersion] = useState("unknown");
|
||||
const [apiKeyFieldType, setApiKeyFieldType] = useState("password");
|
||||
const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false);
|
||||
const [tempPSKey, setTempPSKey] = useState("");
|
||||
const [errors, setErrors] = useState({});
|
||||
const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
|
||||
const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
|
||||
const [isSaving, saveError, saveSettings] = useSaveSettings();
|
||||
const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats();
|
||||
|
||||
// Setup
|
||||
const theme = useTheme();
|
||||
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
|
||||
const { t, i18n } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
//Fetching latest release version from github
|
||||
useEffect(() => {
|
||||
const fetchLatestVersion = async () => {
|
||||
let version = "unknown";
|
||||
try {
|
||||
const response = await networkService.fetchGithubLatestRelease();
|
||||
if (!response.status === 200) {
|
||||
throw new Error("Failed to fetch latest version");
|
||||
}
|
||||
version = response.data.tag_name;
|
||||
} catch (error) {
|
||||
createToast({ body: error.message || "Error fetching latest version" }); // Set error message
|
||||
} finally {
|
||||
setVersion(version);
|
||||
}
|
||||
// Handlers
|
||||
const handleChange = async (e) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
// Build next state early
|
||||
const newSettingsData = {
|
||||
...settingsData,
|
||||
settings: { ...settingsData.settings, [name]: value },
|
||||
};
|
||||
fetchLatestVersion();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAppSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { type, checked, value, id } = event.target;
|
||||
|
||||
if (type === "checkbox") {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[id]: checked,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === "pagespeedApiKey") {
|
||||
setTempPSKey(value);
|
||||
return;
|
||||
}
|
||||
|
||||
let inputValue = value;
|
||||
if (id === "ttl") {
|
||||
inputValue = value.replace(/[^0-9]/g, "");
|
||||
}
|
||||
|
||||
const updatedForm = { ...form, [id]: inputValue };
|
||||
const { error } = settingsValidation.validate(updatedForm, { abortEarly: false });
|
||||
|
||||
// Validate
|
||||
const { error } = settingsValidation.validate(newSettingsData.settings, {
|
||||
abortEarly: false,
|
||||
});
|
||||
if (!error || error.details.length === 0) {
|
||||
setErrors({});
|
||||
} else {
|
||||
@@ -119,397 +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: tempPSKey ? tempPSKey : form.pagespeedApiKey,
|
||||
},
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
if (userAction.payload.success && settingsAction.payload.success) {
|
||||
createToast({ body: t("settingsSuccessSaved") });
|
||||
} else {
|
||||
throw new Error("Failed to save settings");
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToSave") });
|
||||
} finally {
|
||||
setChecksIsLoading(false);
|
||||
if (name === "timezone") {
|
||||
dispatch(setTimezone({ timezone: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearStats = async () => {
|
||||
try {
|
||||
const action = await dispatch(deleteMonitorChecksByTeamId({ teamId: user.teamId }));
|
||||
|
||||
if (deleteMonitorChecksByTeamId.fulfilled.match(action)) {
|
||||
createToast({ body: t("settingsStatsCleared") });
|
||||
} else {
|
||||
createToast({ body: t("settingsFailedToClearStats") });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ body: t("settingsFailedToClearStats") });
|
||||
} finally {
|
||||
setIsOpen(deleteStatsMonitorsInitState);
|
||||
if (name === "mode") {
|
||||
dispatch(setMode(value));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInsertDemoMonitors = async () => {
|
||||
try {
|
||||
const action = await dispatch(addDemoMonitors());
|
||||
if (addDemoMonitors.fulfilled.match(action)) {
|
||||
createToast({ body: t("settingsDemoMonitorsAdded") });
|
||||
} else {
|
||||
if (name === "language") {
|
||||
dispatch(setLanguage(value));
|
||||
i18n.changeLanguage(value);
|
||||
}
|
||||
|
||||
if (name === "deleteStats") {
|
||||
await deleteMonitorStats({ teamId: user.teamId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (name === "demo") {
|
||||
try {
|
||||
const action = await dispatch(addDemoMonitors());
|
||||
if (addDemoMonitors.fulfilled.match(action)) {
|
||||
createToast({ body: t("settingsDemoMonitorsAdded") });
|
||||
} else {
|
||||
createToast({ body: t("settingsFailedToAddDemoMonitors") });
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToAddDemoMonitors") });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ Body: t("settingsFailedToAddDemoMonitors") });
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAllMonitors = async () => {
|
||||
try {
|
||||
const action = await dispatch(deleteAllMonitors());
|
||||
if (deleteAllMonitors.fulfilled.match(action)) {
|
||||
createToast({ body: t("settingsMonitorsDeleted") });
|
||||
} else {
|
||||
if (name === "deleteMonitors") {
|
||||
try {
|
||||
const action = await dispatch(deleteAllMonitors());
|
||||
if (deleteAllMonitors.fulfilled.match(action)) {
|
||||
createToast({ body: t("settingsMonitorsDeleted") });
|
||||
} else {
|
||||
createToast({ body: t("settingsFailedToDeleteMonitors") });
|
||||
}
|
||||
} catch (error) {
|
||||
createToast({ body: t("settingsFailedToDeleteMonitors") });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
createToast({ Body: t("settingsFailedToDeleteMonitors") });
|
||||
} finally {
|
||||
setIsOpen(deleteStatsMonitorsInitState);
|
||||
return;
|
||||
}
|
||||
|
||||
setSettingsData(newSettingsData);
|
||||
};
|
||||
|
||||
const handleResetApiKey = () => {
|
||||
setIsApiKeySet(false);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
pagespeedApiKey: "",
|
||||
}));
|
||||
const handleSave = () => {
|
||||
const { error } = settingsValidation.validate(settingsData.settings, {
|
||||
abortEarly: false,
|
||||
});
|
||||
if (!error || error.details.length === 0) {
|
||||
setErrors({});
|
||||
} else {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
}
|
||||
saveSettings(settingsData?.settings);
|
||||
};
|
||||
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="settings"
|
||||
style={{
|
||||
paddingBottom: 0,
|
||||
}}
|
||||
>
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h1">Settings</Typography>
|
||||
<SettingsTimeZone
|
||||
HEADING_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<SettingsUI
|
||||
HEADING_SX={HEADING_SX}
|
||||
handleChange={handleChange}
|
||||
mode={mode}
|
||||
language={language}
|
||||
/>
|
||||
<SettingsPagespeed
|
||||
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: "Light" },
|
||||
{ _id: "dark", name: "Dark" },
|
||||
]}
|
||||
></Select>
|
||||
<Select
|
||||
id="language"
|
||||
label={t("settingsLanguage")}
|
||||
value={language}
|
||||
onChange={(e) => {
|
||||
dispatch(setLanguage(e.target.value));
|
||||
i18n.changeLanguage(e.target.value);
|
||||
}}
|
||||
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
|
||||
></Select>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{/* {isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
|
||||
{t("settingsDistributedUptimeDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Switch
|
||||
id="enableDistributedUptime"
|
||||
color="accent"
|
||||
checked={distributedUptimeEnabled}
|
||||
onChange={(e) => {
|
||||
dispatch(setDistributedUptimeEnabled(e.target.checked));
|
||||
}}
|
||||
/>
|
||||
{distributedUptimeEnabled === true
|
||||
? t("settingsEnabled")
|
||||
: t("settingsDisabled")}
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)} */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
|
||||
{t("pageSpeedApiKeyFieldDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
id="pagespeedApiKey"
|
||||
label={t("pageSpeedApiKeyFieldLabel")}
|
||||
value={form.pagespeedApiKey ? form.pagespeedApiKey : tempPSKey}
|
||||
type={apiKeyFieldType}
|
||||
onChange={handleChange}
|
||||
disabled={isApiKeySet}
|
||||
optionalLabel="(Optional)"
|
||||
error={!!errors.pagespeedApiKey}
|
||||
helperText={errors.pagespeedApiKey}
|
||||
endAdornment={
|
||||
!isApiKeySet && (
|
||||
<PasswordEndAdornment
|
||||
fieldType={apiKeyFieldType}
|
||||
setFieldType={setApiKeyFieldType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isApiKeySet && (
|
||||
<Box>
|
||||
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
|
||||
<Button
|
||||
onClick={handleResetApiKey}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{/* {isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsWallet")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsWalletDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "flex-start",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
>
|
||||
<WalletMultiButton />
|
||||
<WalletDisconnectButton />
|
||||
</Stack>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)} */}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsHistoryAndMonitoring")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsHistoryAndMonitoringDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
id="ttl"
|
||||
label={t("settingsTTLLabel")}
|
||||
optionalLabel={t("settingsTTLOptionalLabel")}
|
||||
value={form.ttl}
|
||||
onChange={handleChange}
|
||||
error={errors.ttl ? true : false}
|
||||
helperText={errors.ttl}
|
||||
/>
|
||||
<Box>
|
||||
<Typography>{t("settingsClearAllStats")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() =>
|
||||
setIsOpen({ ...deleteStatsMonitorsInitState, deleteStats: true })
|
||||
}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("settingsClearAllStatsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Dialog
|
||||
open={isOpen.deleteStats}
|
||||
theme={theme}
|
||||
title={t("settingsClearAllStatsDialogTitle")}
|
||||
description={t("settingsClearAllStatsDialogDescription")}
|
||||
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
|
||||
confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
|
||||
onConfirm={handleClearStats}
|
||||
isLoading={isLoading || authIsLoading || checksIsLoading}
|
||||
/>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<>
|
||||
{/* Demo Monitors Section */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsDemoMonitorsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsAddDemoMonitors")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isLoading || authIsLoading || checksIsLoading}
|
||||
onClick={handleInsertDemoMonitors}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("settingsAddDemoMonitorsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
|
||||
{/* System Reset Section */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsSystemReset")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsSystemResetDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isLoading || authIsLoading || checksIsLoading}
|
||||
onClick={() =>
|
||||
setIsOpen({ ...deleteStatsMonitorsInitState, deleteMonitors: true })
|
||||
}
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("settingsRemoveAllMonitorsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
<Dialog
|
||||
open={isOpen.deleteMonitors}
|
||||
theme={theme}
|
||||
title={t("settingsRemoveAllMonitorsDialogTitle")}
|
||||
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
|
||||
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
|
||||
onConfirm={handleDeleteAllMonitors}
|
||||
isLoading={isLoading || authIsLoading || checksIsLoading}
|
||||
/>
|
||||
</ConfigBox>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsAbout")}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography component="h2">Checkmate {version}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
|
||||
{t("settingsDevelopedBy")}
|
||||
</Typography>
|
||||
<Link
|
||||
level="secondary"
|
||||
url="https://github.com/bluewave-labs/checkmate"
|
||||
label="https://github.com/bluewave-labs/checkmate"
|
||||
/>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
<Button
|
||||
loading={isSaving || isDeletingMonitorStats || isSettingsLoading}
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
<Button
|
||||
loading={isLoading || authIsLoading || checksIsLoading}
|
||||
disabled={Object.keys(errors).length > 0}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Settings.propTypes = {
|
||||
isAdmin: PropTypes.bool,
|
||||
};
|
||||
export default Settings;
|
||||
|
||||
@@ -420,7 +420,9 @@ const Configure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateIncidentNotification")}</Typography>
|
||||
<Typography component="h2">
|
||||
{t("distributedUptimeCreateIncidentNotification")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateIncidentDescription")}
|
||||
</Typography>
|
||||
@@ -491,6 +493,7 @@ const Configure = () => {
|
||||
</Box>
|
||||
<Stack>
|
||||
<FormControlLabel
|
||||
sx={{ marginLeft: 0 }}
|
||||
control={
|
||||
<Switch
|
||||
name="ignore-error"
|
||||
@@ -611,7 +614,7 @@ const Configure = () => {
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<NotificationIntegrationModal
|
||||
<NotificationIntegrationModal
|
||||
open={isNotificationModalOpen}
|
||||
onClose={handleClosenNotificationModal}
|
||||
monitor={monitor}
|
||||
|
||||
@@ -422,6 +422,7 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
<Stack>
|
||||
<FormControlLabel
|
||||
sx={{ marginLeft: 0 }}
|
||||
control={
|
||||
<Switch
|
||||
name="ignore-error"
|
||||
|
||||
@@ -253,6 +253,7 @@ const baseTheme = (palette) => ({
|
||||
MuiTableCell: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
fontSize: typographyLevels.base,
|
||||
borderBottomColor: theme.palette.primary.lowContrast,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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 lastnameSchema = joi
|
||||
@@ -163,20 +164,21 @@ const monitorValidation = joi.object({
|
||||
"string.invalidUrl": "Please enter a valid URL with optional port",
|
||||
"string.pattern.base": "Please enter a valid container ID.",
|
||||
}),
|
||||
port: joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.when("type", {
|
||||
is: "port",
|
||||
then: joi.required().messages({
|
||||
"number.base": "Port must be a number.",
|
||||
"number.min": "Port must be at least 1.",
|
||||
"number.max": "Port must be at most 65535.",
|
||||
"any.required": "Port is required for port monitors.",
|
||||
port: joi
|
||||
.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.when("type", {
|
||||
is: "port",
|
||||
then: joi.required().messages({
|
||||
"number.base": "Port must be a number.",
|
||||
"number.min": "Port must be at least 1.",
|
||||
"number.max": "Port must be at most 65535.",
|
||||
"any.required": "Port is required for port monitors.",
|
||||
}),
|
||||
otherwise: joi.optional(),
|
||||
}),
|
||||
otherwise: joi.optional(),
|
||||
}),
|
||||
name: joi.string().trim().max(50).allow("").messages({
|
||||
"string.max": "This field should not exceed the 50 characters limit.",
|
||||
}),
|
||||
@@ -264,14 +266,19 @@ const statusPageValidation = joi.object({
|
||||
showCharts: joi.boolean(),
|
||||
});
|
||||
const settingsValidation = joi.object({
|
||||
ttl: joi.number().required().messages({
|
||||
checkTTL: joi.number().required().messages({
|
||||
"string.empty": "Please enter a value",
|
||||
"number.base": "Please enter a valid number",
|
||||
"any.required": "Please enter a value"
|
||||
"any.required": "Please enter a value",
|
||||
}),
|
||||
pagespeedApiKey: joi.string().allow("").optional(),
|
||||
})
|
||||
.unknown(true);
|
||||
language: joi.string().required(),
|
||||
systemEmailHost: joi.string().allow(""),
|
||||
systemEmailPort: joi.number().allow(null, ""),
|
||||
systemEmailAddress: joi.string().allow(""),
|
||||
systemEmailPassword: joi.string().allow(""),
|
||||
systemEmailUser: joi.string().allow(""),
|
||||
});
|
||||
|
||||
const dayjsValidator = (value, helpers) => {
|
||||
if (!dayjs(value).isValid()) {
|
||||
@@ -374,9 +381,12 @@ const infrastructureMonitorValidation = joi.object({
|
||||
notifications: joi.array().items(
|
||||
joi.object({
|
||||
type: joi.string().valid("email").required(),
|
||||
address: joi.string().email({ tlds: { allow: false } }).required(),
|
||||
address: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export {
|
||||
@@ -388,5 +398,5 @@ export {
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
statusPageValidation,
|
||||
logoImageValidation
|
||||
logoImageValidation,
|
||||
};
|
||||
|
||||
+514
-504
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
Vendored
+1
-5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,12 +13,25 @@ class SettingsController {
|
||||
getAppSettings = async (req, res, next) => {
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
const sanitizedSettings = { ...dbSettings };
|
||||
|
||||
const returnSettings = {
|
||||
pagespeedKeySet: false,
|
||||
emailPasswordSet: false,
|
||||
};
|
||||
|
||||
if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") {
|
||||
sanitizedSettings.pagespeedApiKey = "********";
|
||||
returnSettings.pagespeedKeySet = true;
|
||||
delete sanitizedSettings.pagespeedApiKey;
|
||||
}
|
||||
if (typeof sanitizedSettings.systemEmailPassword !== "undefined") {
|
||||
returnSettings.emailPasswordSet = true;
|
||||
delete sanitizedSettings.systemEmailPassword;
|
||||
}
|
||||
|
||||
returnSettings.settings = sanitizedSettings;
|
||||
return res.success({
|
||||
msg: this.stringService.getAppSettings,
|
||||
data: sanitizedSettings,
|
||||
data: returnSettings,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@ import mongoose from "mongoose";
|
||||
|
||||
const AppSettingsSchema = mongoose.Schema(
|
||||
{
|
||||
checkTTL: {
|
||||
type: Number,
|
||||
default: 30,
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: "gb",
|
||||
},
|
||||
pagespeedApiKey: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailHost: {
|
||||
type: String,
|
||||
@@ -22,6 +25,9 @@ const AppSettingsSchema = mongoose.Schema(
|
||||
systemEmailPassword: {
|
||||
type: String,
|
||||
},
|
||||
systemEmailUser: {
|
||||
type: String,
|
||||
},
|
||||
singleton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -14,12 +14,22 @@ const getAppSettings = async () => {
|
||||
|
||||
const updateAppSettings = async (newSettings) => {
|
||||
try {
|
||||
console.log(newSettings);
|
||||
const settings = await AppSettings.findOneAndUpdate(
|
||||
{},
|
||||
{ $set: newSettings },
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
const update = { $set: { ...newSettings } };
|
||||
|
||||
if (newSettings.pagespeedApiKey === "") {
|
||||
update.$unset = { pagespeedApiKey: "" };
|
||||
delete update.$set.pagespeedApiKey;
|
||||
}
|
||||
|
||||
if (newSettings.systemEmailPassword === "") {
|
||||
update.$unset = { systemEmailPassword: "" };
|
||||
delete update.$set.systemEmailPassword;
|
||||
}
|
||||
|
||||
const settings = await AppSettings.findOneAndUpdate({}, update, {
|
||||
new: true,
|
||||
upsert: true,
|
||||
});
|
||||
return settings;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,7 +59,17 @@ class SettingsService {
|
||||
}
|
||||
|
||||
async getDBSettings() {
|
||||
const settings = await this.appSettings.findOne({ singleton: true }).lean();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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