remove old, add about and export
This commit is contained in:
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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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={
|
||||
|
||||
Vendored
+2
@@ -1,5 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_API_V2_BASE_URL?: string;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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: "/",
|
||||
|
||||
Reference in new issue
Block a user