remove old, add about and export

This commit is contained in:
Alex Holliday committed 2026-02-20 17:36:04 +00:00
1 parent 73f313e4d5
commit fc6300d057
16 files changed
+83 -1278

No files matched your search

@@ -1,39 +0,0 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import Link from "@/Components/v1/Link/index.jsx";
const SettingsAbout = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.aboutSettings.title")}
</Typography>
</Box>
<Box>
<Typography component="h2">
{t("common.appName")} {__APP_VERSION__}
</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(6), opacity: 0.6 }}>
{t("pages.settings.aboutSettings.labelDevelopedBy")}
</Typography>
<Link
level="secondary"
url="https://github.com/bluewave-labs/checkmate"
label="https://github.com/bluewave-labs/checkmate"
/>
</Box>
</ConfigBox>
);
};
export default SettingsAbout;
@@ -1,97 +0,0 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import Dialog from "@/Components/v1/Dialog/index.jsx";
import { useState } from "react";
import { useDelete, usePost } from "@/Hooks/UseApi";
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, isLoading }) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [isOpen, setIsOpen] = useState(false);
if (!isAdmin) {
return null;
}
const { post: postDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitorsFn } = useDelete();
return (
<>
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.demoMonitorsSettings.title")}
</Typography>
<Typography sx={HEADER_SX}>
{t("pages.settings.demoMonitorsSettings.description")}
</Typography>
</Box>
<Box>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={async () => {
await postDemoMonitors("/monitors/demo", {});
}}
sx={{ mt: theme.spacing(4) }}
>
{t("pages.settings.demoMonitorsSettings.buttonAddMonitors")}
</Button>
</Box>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.systemResetSettings.title")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("pages.settings.systemResetSettings.description")}
</Typography>
</Box>
<Box>
<Button
variant="contained"
color="error"
loading={isLoading}
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("pages.settings.systemResetSettings.buttonRemoveAllMonitors")}
</Button>
</Box>
<Dialog
open={isOpen}
theme={theme}
title={t("pages.settings.systemResetSettings.dialogTitle")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("pages.settings.systemResetSettings.dialogConfirm")}
onConfirm={async () => {
await deleteAllMonitorsFn("/monitors/");
setIsOpen(false);
}}
isLoading={isLoading}
/>
</ConfigBox>
</>
);
};
SettingsDemoMonitors.propTypes = {
isAdmin: PropTypes.bool,
HEADER_SX: PropTypes.object,
};
export default SettingsDemoMonitors;
-369
View File
@@ -1,369 +0,0 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { Switch } from "@mui/material";
import TextLink from "@/Components/v1/TextLink/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
import { usePost } from "@/Hooks/UseApi";
import { useSelector } from "react-redux";
import { createToast } from "@/Utils/toastUtils.jsx";
import { Controller } from "react-hook-form";
const SettingsEmail = ({
isAdmin,
HEADER_SX,
isEmailPasswordSet,
emailPasswordHasBeenReset,
setEmailPasswordHasBeenReset,
control,
defaults,
formValues,
setValue,
}) => {
// Setup
const { t } = useTranslation();
const theme = useTheme();
// Destructure settings with default values
const {
systemEmailHost = "",
systemEmailPort = "",
systemEmailSecure = false,
systemEmailPool = false,
systemEmailUser = "",
systemEmailAddress = "",
systemEmailPassword = "",
systemEmailTLSServername = "",
systemEmailConnectionHost = "localhost",
systemEmailIgnoreTLS = false,
systemEmailRequireTLS = false,
systemEmailRejectUnauthorized = true,
} = formValues || {};
// Network
const { post: sendTestEmailFn, loading: isSending } = usePost();
const user = useSelector((state) => state.auth.user);
// Handlers
/**
* Handle sending test email with current form values
*/
const handleSendTestEmail = () => {
// Basic validation
if (
!systemEmailHost ||
!systemEmailPort ||
!systemEmailAddress ||
!systemEmailPassword
) {
createToast({
body: t("pages.settings.emailSettings.toastEmailRequiredFieldsError"),
variant: "error",
});
return;
}
// Send test email - only include optional fields if they have values
sendTestEmailFn("/settings/test-email", {
to: user.email,
systemEmailHost,
systemEmailPort,
systemEmailAddress,
systemEmailPassword,
systemEmailSecure,
systemEmailPool,
systemEmailIgnoreTLS,
systemEmailRequireTLS,
systemEmailRejectUnauthorized,
...(systemEmailUser && { systemEmailUser }),
...(systemEmailTLSServername && { systemEmailTLSServername }),
...(systemEmailConnectionHost && { systemEmailConnectionHost }),
});
};
if (!isAdmin) {
return null;
}
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.emailSettings.title")}
</Typography>
<Typography sx={HEADER_SX}>
{t("pages.settings.emailSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(10)}>
<Box>
<Controller
name="systemEmailHost"
control={control}
defaultValue={defaults.systemEmailHost}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelHost")}
placeholder="smtp.gmail.com"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailPort"
control={control}
defaultValue={defaults.systemEmailPort}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelPort")}
placeholder="425"
type="number"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailUser"
control={control}
defaultValue={defaults.systemEmailUser}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelUser")}
placeholder={t("pages.settings.emailSettings.placeholderUser")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailAddress"
control={control}
defaultValue={defaults.systemEmailAddress}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelAddress")}
placeholder="uptime@bluewavelabs.ca"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
<Box>
<Controller
name="systemEmailPassword"
control={control}
defaultValue={defaults.systemEmailPassword}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelPassword")}
type="password"
placeholder="123 456 789 101112"
endAdornment={<PasswordEndAdornment />}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
)}
{isEmailPasswordSet === true && emailPasswordHasBeenReset === false && (
<Box>
<Typography>{t("pages.settings.emailSettings.labelPasswordSet")}</Typography>
<Button
onClick={() => {
setEmailPasswordHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
<Box>
<Controller
name="systemEmailTLSServername"
control={control}
defaultValue={defaults.systemEmailTLSServername}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelTLSServername")}
placeholder="bluewavelabs.ca"
/>
)}
/>
</Box>
<Box>
<Controller
name="systemEmailConnectionHost"
control={control}
defaultValue={defaults.systemEmailConnectionHost}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.emailSettings.labelConnectionHost")}
placeholder="bluewavelabs.ca"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(4),
}}
>
{[
[
"pages.settings.emailSettings.labelSecure",
"systemEmailSecure",
systemEmailSecure,
],
[
"pages.settings.emailSettings.labelPool",
"systemEmailPool",
systemEmailPool,
],
[
"pages.settings.emailSettings.labelIgnoreTLS",
"systemEmailIgnoreTLS",
systemEmailIgnoreTLS,
],
[
"pages.settings.emailSettings.labelRequireTLS",
"systemEmailRequireTLS",
systemEmailRequireTLS,
],
[
"pages.settings.emailSettings.labelRejectUnauthorized",
"systemEmailRejectUnauthorized",
systemEmailRejectUnauthorized,
],
].map(([labelKey, name, value]) => (
<Controller
key={name}
name={name}
control={control}
defaultValue={defaults[name]}
render={({ field }) => (
<Box
key={name}
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography>{t(labelKey)}</Typography>
<Switch
{...field}
checked={field.value}
/>
</Box>
)}
/>
))}
<TextLink
text={t("pages.settings.emailSettings.descriptionTransport")}
linkText={t("pages.settings.emailSettings.linkTransport")}
href="https://nodemailer.com/smtp"
target="_blank"
/>
<Box
component={"pre"}
sx={{
fontFamily: "monospace",
p: 2,
borderRadius: 1,
overflow: "auto",
}}
>
<code>
{JSON.stringify(
{
host: systemEmailHost,
port: systemEmailPort,
secure: systemEmailSecure,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: "<your_password>",
},
name: systemEmailConnectionHost || "localhost",
pool: systemEmailPool,
tls: {
rejectUnauthorized: systemEmailRejectUnauthorized,
ignoreTLS: systemEmailIgnoreTLS,
requireTLS: systemEmailRequireTLS,
servername: systemEmailTLSServername,
},
},
null,
2
)}
</code>
</Box>
</Box>
<Box>
{systemEmailHost &&
systemEmailPort &&
systemEmailAddress &&
systemEmailPassword && (
<Button
variant="contained"
color="accent"
loading={isSending}
onClick={handleSendTestEmail}
>
{t("pages.settings.emailSettings.buttonSendTestEmail")}
</Button>
)}
</Box>
</Stack>
</ConfigBox>
);
};
SettingsEmail.propTypes = {
isAdmin: PropTypes.bool,
HEADER_SX: PropTypes.object,
isPasswordSet: PropTypes.bool,
};
export default SettingsEmail;
@@ -1,81 +0,0 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import Dialog from "@/Components/v1/Dialog/index.jsx";
import { useState } from "react";
import { useLazyGet } from "@/Hooks/UseApi";
const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, isLoading }) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [isOpen, setIsOpen] = useState(false);
if (!isAdmin) {
return null;
}
const { get: fetchJson } = useLazyGet();
const handleExport = async () => {
const res = await fetchJson("/monitors/export/json");
const json = res?.data ?? [];
if (!json || json.length === 0) {
return;
}
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "monitors.json";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
return;
};
return (
<>
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
Export monitors to JSON
</Typography>
<Typography sx={HEADER_SX}>
Export your monitors data as a JSON file for backup or transfer.
</Typography>
</Box>
<Box>
<Button
variant="contained"
color="accent"
loading={isLoading}
onClick={handleExport}
sx={{ mt: theme.spacing(4) }}
>
Export Monitors to JSON
</Button>
</Box>
</ConfigBox>
</>
);
};
SettingsDemoMonitors.propTypes = {
isAdmin: PropTypes.bool,
HEADER_SX: PropTypes.object,
};
export default SettingsDemoMonitors;
@@ -1,79 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { Controller } from "react-hook-form";
const SettingsGlobalThresholds = ({
isAdmin,
HEADING_SX,
control,
defaults,
}) => {
const { t } = useTranslation(); // For language translation
const theme = useTheme(); // MUI theme access
// Only render this section for admins
if (!isAdmin) return null;
return (
<ConfigBox>
{/* Header and description */}
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.globalThresholds.title", "Global Thresholds")}
</Typography>
<Typography sx={HEADING_SX}>
{t(
"pages.settings.globalThresholds.description",
"Configure global CPU, Memory, Disk, and Temperature thresholds."
)}
</Typography>
</Box>
{/* Threshold inputs */}
<Stack gap={theme.spacing(20)}>
{[
["CPU Threshold (%)", "cpu", 1, 100],
["Memory Threshold (%)", "memory", 1, 100],
["Disk Threshold (%)", "disk", 1, 100],
["Temperature Threshold (°C)", "temperature", 1, 150],
].map(([label, name, min, max]) => (
<Controller
key={name}
name={`globalThresholds.${name}`}
control={control}
defaultValue={defaults.globalThresholds?.[name]}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={label}
placeholder={`${min} - ${max}`}
type="number"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
))}
</Stack>
</ConfigBox>
);
};
// Prop types
SettingsGlobalThresholds.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
};
export default SettingsGlobalThresholds;
@@ -1,100 +0,0 @@
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/v1/ConfigBox/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller } from "react-hook-form";
const SettingsPagespeed = ({
isAdmin,
HEADING_SX,
isApiKeySet,
apiKeyHasBeenReset,
setApiKeyHasBeenReset,
defaults,
control,
setValue,
}) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [apiKey, setApiKey] = useState("");
if (!isAdmin) {
return null;
}
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.pageSpeedSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
{t("pages.settings.pageSpeedSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
{(isApiKeySet === false || apiKeyHasBeenReset === true) && (
<Controller
name="pagespeedApiKey"
control={control}
defaultValue={defaults.pagespeedApiKey}
render={({ field, fieldState }) => (
<TextInput
{...field}
label={t("pages.settings.pageSpeedSettings.labelApiKey")}
type={"password"}
optionalLabel="(Optional)"
endAdornment={<PasswordEndAdornment />}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
)}
{isApiKeySet === true && apiKeyHasBeenReset === false && (
<Box>
<Typography>
{t("pages.settings.pageSpeedSettings.labelApiKeySet")}
</Typography>
<Button
onClick={() => {
setValue("pagespeedApiKey", "");
setApiKeyHasBeenReset(true);
}}
variant="contained"
color="error"
sx={{ mt: theme.spacing(4) }}
>
{t("reset")}
</Button>
</Box>
)}
</Stack>
</ConfigBox>
);
};
SettingsPagespeed.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
isApiKeySet: PropTypes.bool,
setIsApiKeySet: PropTypes.func,
apiKeyHasBeenReset: PropTypes.bool,
setApiKeyHasBeenReset: PropTypes.func,
};
export default SettingsPagespeed;
@@ -1,80 +0,0 @@
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/v1/ConfigBox/index.jsx";
import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx";
import Dialog from "@/Components/v1/Dialog/index.jsx";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useDelete } from "@/Hooks/UseApi";
const SettingsStats = ({ isAdmin, HEADING_SX, errors }) => {
const theme = useTheme();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
if (!isAdmin) {
return null;
}
const { deleteFn: deleteMonitorStatsFn } = useDelete();
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
sx={HEADING_SX}
>
{t("pages.settings.statsSettings.title")}
</Typography>
<Typography sx={{ mt: theme.spacing(2) }}>
{t("pages.settings.statsSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Box>
<Typography>
{t("pages.settings.statsSettings.clearAllStatsDescription")}
</Typography>
<Button
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
{t("pages.settings.statsSettings.clearAllStatsButton")}
</Button>
</Box>
</Stack>
<Dialog
open={isOpen}
theme={theme}
title={t("pages.settings.statsSettings.clearAllStatsDialogTitle")}
description={t("pages.settings.statsSettings.clearAllStatsDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t(
"pages.settings.statsSettings.clearAllStatsDialogConfirm"
)}
onConfirm={async () => {
await deleteMonitorStatsFn("/checks/team");
setIsOpen(false);
}}
isLoading={false}
/>
</ConfigBox>
);
};
SettingsStats.propTypes = {
isAdmin: PropTypes.bool,
HEADING_SX: PropTypes.object,
};
export default SettingsStats;
@@ -1,70 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Search from "@/Components/v1/Inputs/Search/index.jsx";
import timezones from "@/Utils/timezones.json";
// Utils
import { useTheme } from "@emotion/react";
import { useSelector, useDispatch } from "react-redux";
import { setTimezone } from "@/Features/UI/uiSlice.js";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useCallback, useMemo, useState } from "react";
const SettingsTimeZone = ({ HEADING_SX }) => {
const theme = useTheme();
const { t } = useTranslation();
const [rawInput, setRawInput] = useState("");
const dispatch = useDispatch();
const { timezone } = useSelector((state) => state.ui);
const selectedTimezone = useMemo(
() => timezones.find((tz) => tz._id === timezone) ?? null,
[timezone]
);
const handleTimezoneChange = (newValue) => {
setRawInput("");
const newId = newValue?._id ?? "";
dispatch(setTimezone({ timezone: newId }));
};
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.timezoneSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
<Typography component="span">
{t("pages.settings.timezoneSettings.description")}
</Typography>
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Search
id="timezone"
label={t("settingsDisplayTimezone")}
options={timezones}
filteredBy="name"
value={selectedTimezone}
inputValue={rawInput}
handleInputChange={(val) => setRawInput(val)}
handleChange={handleTimezoneChange}
isAdorned={true}
unit="timezone"
/>
</Stack>
</ConfigBox>
);
};
SettingsTimeZone.propTypes = {
HEADING_SX: PropTypes.object,
};
export default SettingsTimeZone;
-81
View File
@@ -1,81 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import DummyChart from "./DummyChart";
import { ThemeProvider } from "@mui/material/styles";
import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import { setMode, setLanguage, setChartType } from "@/Features/UI/uiSlice.js";
const SettingsUI = ({ HEADING_SX }) => {
const {
mode,
language = "en",
chartType = "histogram",
} = useSelector((state) => state.ui);
const { t, i18n } = useTranslation();
const theme = useTheme();
const languages = Object.keys(i18n.options.resources || {});
const dispatch = useDispatch();
const v2Theme = mode === "dark" ? darkTheme : lightTheme;
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.uiSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
{t("pages.settings.uiSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
name="mode"
label={t("pages.settings.uiSettings.labelTheme")}
value={mode}
onChange={(e) => dispatch(setMode(e.target.value))}
items={[
{ _id: "light", name: "Light" },
{ _id: "dark", name: "Dark" },
]}
></Select>
<Select
name="language"
label={t("pages.settings.uiSettings.labelLanguage")}
value={language}
onChange={(e) => dispatch(setLanguage(e.target.value))}
items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))}
></Select>
<Select
name="chartType"
label={t("pages.settings.uiSettings.labelChartType")}
value={chartType}
onChange={(e) => dispatch(setChartType(e.target.value))}
items={[
{ _id: "histogram", name: t("pages.settings.uiSettings.chartTypeHistogram") },
{ _id: "heatmap", name: t("pages.settings.uiSettings.chartTypeHeatmap") },
]}
></Select>
<ThemeProvider theme={v2Theme}>
<DummyChart chartType={chartType} />
</ThemeProvider>
</Stack>
</ConfigBox>
);
};
SettingsUI.propTypes = {
HEADING_SX: PropTypes.object,
};
export default SettingsUI;
-56
View File
@@ -1,56 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "@/Components/v1/ConfigBox/index.jsx";
import Select from "@/Components/v1/Inputs/Select/index.jsx";
import { Controller } from "react-hook-form";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
const SettingsURL = ({ HEADING_SX, control, defaults }) => {
const { t } = useTranslation();
const theme = useTheme();
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("pages.settings.urlSettings.title")}
</Typography>
<Typography sx={HEADING_SX}>
{t("pages.settings.urlSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Controller
name="showURL"
control={control}
defaultValue={defaults.showURL}
render={({ field, fieldState }) => (
<Select
{...field}
error={!!fieldState.error}
helperText={fieldState.error?.message}
label={t("pages.settings.urlSettings.label")}
items={[
{ _id: true, name: t("pages.settings.urlSettings.selectEnabled") },
{ _id: false, name: t("pages.settings.urlSettings.selectDisabled") },
]}
/>
)}
/>
</Stack>
</ConfigBox>
);
};
SettingsURL.propTypes = {
HEADING_SX: PropTypes.object,
};
export default SettingsURL;
+69 -2
View File
@@ -1,13 +1,13 @@
import { BasePage, ConfigBox, TextLink } from "@/Components/v2/design-elements";
import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/v2/inputs";
import { Stack, useTheme, MenuItem, type SelectChangeEvent } from "@mui/material";
import { Stack, useTheme, MenuItem, Link, type SelectChangeEvent } from "@mui/material";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import DummyChart from "@/Pages/Settings/DummyChart";
import { useGet, usePatch, usePost } from "@/Hooks/UseApi";
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData } from "@/Validation/settings";
@@ -69,6 +69,8 @@ export const SettingsPage = () => {
// Local state for clear stats dialog
const [isStatsDialogOpen, setIsStatsDialogOpen] = useState(false);
const { deleteFn: deleteStats, loading: isDeletingStats } = useDelete();
// Export monitors functionality
const { get: fetchMonitorsJson } = useLazyGet();
// Initialize form with schema and defaults
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
@@ -179,6 +181,27 @@ export const SettingsPage = () => {
setIsStatsDialogOpen(false);
};
const handleExportMonitors = async () => {
const res = await fetchMonitorsJson("/monitors/export/json");
const json = res?.data ?? [];
if (!json || json.length === 0) {
return;
}
const blob = new Blob([JSON.stringify(json, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "monitors.json";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const onSubmit = async (data: SettingsFormData) => {
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
const dataToSend = { ...data };
@@ -840,6 +863,50 @@ export const SettingsPage = () => {
/>
)}
{/* Export Monitors - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.exportMonitors.title")}
subtitle={t("pages.settings.form.exportMonitors.description")}
rightContent={
<Box>
<Button
variant="contained"
onClick={handleExportMonitors}
>
{t("common.buttons.exportToJSON")}
</Button>
</Box>
}
/>
)}
{/* About */}
<ConfigBox
title={t("pages.settings.form.about.title")}
subtitle=""
rightContent={
<Stack spacing={2}>
<Typography variant="body1">
{t("common.appName")} {__APP_VERSION__}
</Typography>
<Typography
variant="body2"
sx={{ opacity: 0.6 }}
>
{t("pages.settings.form.about.developedBy")}
</Typography>
<Link
href="https://github.com/bluewave-labs/checkmate"
target="_blank"
rel="noopener noreferrer"
>
https://github.com/bluewave-labs/checkmate
</Link>
</Stack>
}
/>
{/* Clear Stats Confirmation Dialog */}
<Dialog
open={isStatsDialogOpen}
-217
View File
@@ -1,217 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx";
import SettingsTimeZone from "./SettingsTimeZone.jsx";
import SettingsUI from "./SettingsUI.jsx";
import SettingsURL from "./SettingsURL.jsx";
import SettingsPagespeed from "./SettingsPagespeed.jsx";
import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx";
import SettingsAbout from "./SettingsAbout.jsx";
import SettingsEmail from "./SettingsEmail.jsx";
import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx";
import SettingsExport from "./SettingsExport.jsx";
import Button from "@mui/material/Button";
// Utils
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSettingsForm } from "@/Hooks/useSettingsForm.js";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import SettingsStats from "./SettingsStats.jsx";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import { useGet, usePost, useDelete, useLazyGet, usePatch } from "@/Hooks/UseApi";
// Constants
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
const Settings = () => {
const { data: fetchedSettings, isLoading: isSettingsLoading } = useGet("/settings");
// Local state
const [isApiKeySet, setIsApiKeySet] = useState(
fetchedSettings?.pagespeedKeySet ?? false
);
const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false);
const [isEmailPasswordSet, setIsEmailPasswordSet] = useState(
fetchedSettings?.emailPasswordSet ?? false
);
const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false);
// Network
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
const form = useForm({
resolver: zodResolver(schema),
defaultValues: defaults,
});
const {
control,
watch,
reset,
handleSubmit,
clearErrors,
trigger,
getValues,
setValue,
} = form;
useEffect(() => {
reset(defaults, { keepDirtyValues: false });
}, [defaults, reset]);
useEffect(() => {
if (fetchedSettings) {
setIsApiKeySet(fetchedSettings?.pagespeedKeySet);
setIsEmailPasswordSet(fetchedSettings?.emailPasswordSet);
}
}, [fetchedSettings]);
const { patch: saveSettingsFn, loading: isSaving } = usePatch();
const saveSettings = async (settings) => {
const response = await saveSettingsFn("/settings", settings);
if (response?.data) {
const data = response.data;
setIsApiKeySet(data.pagespeedKeySet);
setIsEmailPasswordSet(data.emailPasswordSet);
if (data.pagespeedKeySet === true) {
setApiKeyHasBeenReset(false);
}
if (data.emailPasswordSet === true) {
setEmailPasswordHasBeenReset(false);
}
}
};
const onSubmit = async (data) => {
// Convert undefined to empty strings for proper unsetting on backend
const toSubmit = Object.fromEntries(
Object.entries(data).map(([key, value]) => {
if (value === undefined) return [key, ""];
if (typeof value === "object" && value !== null) {
// Handle nested objects like globalThresholds
return [
key,
Object.fromEntries(
Object.entries(value).map(([k, v]) => [k, v === undefined ? "" : v])
),
];
}
return [key, value];
})
);
saveSettings(toSubmit);
};
// New API hooks to replace monitorHooks
const { loading: isAddingDemoMonitors } = usePost();
const { loading: isDeletingMonitors } = useDelete();
const { loading: isDeletingMonitorStats } = useDelete();
const { get: fetchJson, loading: isFetchingJson } = useLazyGet();
// Setup
const isAdmin = useIsAdmin();
const theme = useTheme();
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h1">{t("pages.settings.title")}</Typography>
<SettingsTimeZone HEADING_SX={HEADING_SX} />
<SettingsUI HEADING_SX={HEADING_SX} />
<SettingsPagespeed
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
isApiKeySet={isApiKeySet}
apiKeyHasBeenReset={apiKeyHasBeenReset}
setApiKeyHasBeenReset={setApiKeyHasBeenReset}
control={control}
defaults={defaults}
setValue={setValue}
/>
<SettingsURL
HEADING_SX={HEADING_SX}
control={control}
defaults={defaults}
/>
<SettingsStats
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
/>
<SettingsGlobalThresholds
isAdmin={isAdmin}
HEADING_SX={HEADING_SX}
control={control}
defaults={defaults}
/>
<SettingsDemoMonitors
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
isLoading={
isSettingsLoading || isSaving || isDeletingMonitorStats || isAddingDemoMonitors
}
/>
<SettingsEmail
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
isEmailPasswordSet={isEmailPasswordSet}
emailPasswordHasBeenReset={emailPasswordHasBeenReset}
setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
control={control}
defaults={defaults}
formValues={watch()}
setValue={setValue}
/>
<SettingsExport
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
isLoading={isSettingsLoading || isSaving || isFetchingJson}
/>
<SettingsAbout />
<Stack
direction="row"
justifyContent="flex-end"
sx={{
position: "sticky",
bottom: 0,
boxShadow: theme.shape.boxShadow,
zIndex: 1000,
mt: 3,
backgroundColor: theme.palette.primary.main,
display: "flex",
justifyContent: "flex-end",
pb: theme.spacing(4),
pr: theme.spacing(15),
pl: theme.spacing(5),
pt: theme.spacing(4),
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.spacing(2),
}}
>
<Button
loading={
isSaving || isDeletingMonitorStats || isSettingsLoading || isDeletingMonitors
}
disabled={!form.formState.isValid}
variant="contained"
color="accent"
sx={{ px: theme.spacing(12), py: theme.spacing(8) }}
onClick={handleSubmit(onSubmit)}
>
{t("pages.settings.saveButtonLabel")}
</Button>
</Stack>
</Stack>
);
};
export default Settings;
+1 -5
View File
@@ -44,7 +44,6 @@ import CreateNotifications from "@/Pages/Notifications/create";
import Account from "@/Pages/Account";
import EditUser from "@/Pages/Account/EditUser";
import Settings from "@/Pages/Settings";
import OldSettings from "@/Pages/Settings/old.jsx";
import Maintenance from "@/Pages/Maintenance";
import CreateNewMaintenanceWindow from "@/Pages/Maintenance/create";
@@ -328,10 +327,7 @@ const Routes = () => {
</>
}
/>
<Route
path="old-settings"
element={<OldSettings />}
/>
<Route
path="account/profile"
element={
+2
View File
@@ -1,5 +1,7 @@
/// <reference types="vite/client" />
declare const __APP_VERSION__: string;
interface ImportMetaEnv {
readonly VITE_APP_API_V2_BASE_URL?: string;
}
+10 -1
View File
@@ -43,7 +43,8 @@
"clear": "Clear",
"addDemo": "Add demo monitors",
"removeMonitors": "Remove monitors",
"sendTestEmail": "Send test email"
"sendTestEmail": "Send test email",
"exportToJSON": "Export to JSON"
},
"charts": {
"labels": {
@@ -1083,6 +1084,14 @@
"title": "Remove all monitors?",
"description": "This cannot be undone."
}
},
"exportMonitors": {
"title": "Export monitors",
"description": "Export your monitors data as a JSON file for backup or transfer."
},
"about": {
"title": "About",
"developedBy": "Developed by Bluewave Labs"
}
}
},
+1 -1
View File
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig(({}) => {
let version = "3.3";
let version = "3.4.0";
return {
base: "/",