Files
Checkmate/client/src/Pages/Settings/index.tsx
T
Alexander Holliday bdf1e13ce8 Merge pull request #3607 from bluewave-labs/feat/smtp-from-name
feat(smtp): add optional sender display name to SMTP configuration
2026-04-29 13:01:08 -07:00

1085 lines
31 KiB
TypeScript

import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements";
import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs";
import { logger } from "@/Utils/logger";
import { LAYOUT } from "@/Utils/Theme/constants";
import {
Stack,
useTheme,
MenuItem,
Link,
Alert,
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, useLazyGet } from "@/Hooks/UseApi";
import { useToast } from "@/Hooks/UseToast";
import { useSettingsForm } from "@/Hooks/useSettingsForm";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings";
import { useState } from "react";
import { Controller } from "react-hook-form";
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
import { languageNames } from "@/Components/inputs/LanguageSelector";
import { Box, Typography } from "@mui/material";
import { useDelete } from "@/Hooks/UseApi";
import {
setTimezone,
setMode,
setLanguage,
setChartType,
type ThemeMode,
type ChartType,
} from "@/Features/UI/uiSlice.js";
import timezones from "@/Utils/timezones.json";
import type { RootState } from "@/Types/state";
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
interface Timezone {
id: string;
name: string;
}
interface SettingsResponse {
settings: any;
pagespeedKeySet: boolean;
emailPasswordSet: boolean;
}
export const SettingsPage = () => {
const theme = useTheme();
const { t, i18n } = useTranslation();
const dispatch = useDispatch();
const isAdmin = useIsAdmin();
const { toastError } = useToast();
// Local state for demo monitors dialog
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
// Import monitors functionality
const { post: importMonitors, loading: isImportingMonitors } = usePost();
// Fetch settings data from API
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
// Form submission
const { patch, loading: isSaving } = usePatch<SettingsFormData, SettingsResponse>();
// Local state for API key reset
const [isApiKeySet, setIsApiKeySet] = useState(
fetchedSettings?.pagespeedKeySet ?? false
);
const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false);
// Local state for email password reset
const [isEmailPasswordSet, setIsEmailPasswordSet] = useState(
fetchedSettings?.emailPasswordSet ?? false
);
const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false);
// Test email functionality
const { post: sendTestEmail, loading: isSendingTestEmail } = usePost();
// 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 });
const form = useForm<SettingsFormInput, unknown, SettingsFormData>({
resolver: zodResolver(schema),
defaultValues: defaults,
mode: "onChange",
});
// Reset form when defaults change
useEffect(() => {
form.reset(defaults);
}, [defaults, form]);
// Update isApiKeySet when fetchedSettings changes
useEffect(() => {
if (fetchedSettings) {
setIsApiKeySet(fetchedSettings.pagespeedKeySet);
setIsEmailPasswordSet(fetchedSettings.emailPasswordSet);
}
}, [fetchedSettings]);
const {
timezone: selectedTimezoneId,
mode,
language = "en",
chartType = "histogram",
} = useSelector((state: RootState) => state.ui);
const user = useSelector((state: RootState) => state.auth.user);
// Convert timezones to match AutoComplete format (id instead of _id)
const timezoneOptions: Timezone[] = timezones.map((tz) => ({
id: tz._id,
name: tz.name,
}));
const selectedTimezone =
timezoneOptions.find((tz) => tz.id === selectedTimezoneId) ?? null;
const handleTimezoneChange = (newValue: Timezone | null) => {
if (!newValue?.id) return;
dispatch(setTimezone({ timezone: newValue.id }));
};
const handleModeChange = (e: SelectChangeEvent<ThemeMode>) => {
dispatch(setMode(e.target.value));
};
const handleLanguageChange = (e: SelectChangeEvent<string>) => {
dispatch(setLanguage(e.target.value));
};
const handleChartTypeChange = (e: SelectChangeEvent<ChartType>) => {
dispatch(setChartType(e.target.value));
};
const handleResetApiKey = () => {
form.setValue("pagespeedApiKey", "");
setApiKeyHasBeenReset(true);
};
const handleResetEmailPassword = () => {
form.setValue("systemEmailPassword", "");
setEmailPasswordHasBeenReset(true);
};
const handleSendTestEmail = async () => {
const formValues = form.getValues();
if (!user) {
alert("User not authenticated");
return;
}
if (
!formValues.systemEmailHost ||
!formValues.systemEmailPort ||
!formValues.systemEmailAddress ||
!formValues.systemEmailPassword
) {
alert("Please fill in all required email fields before testing.");
return;
}
await sendTestEmail("/settings/test-email", {
to: user.email,
systemEmailHost: formValues.systemEmailHost,
systemEmailPort: formValues.systemEmailPort,
systemEmailAddress: formValues.systemEmailAddress,
systemEmailPassword: formValues.systemEmailPassword,
systemEmailSecure: formValues.systemEmailSecure,
systemEmailPool: formValues.systemEmailPool,
systemEmailIgnoreTLS: formValues.systemEmailIgnoreTLS,
systemEmailRequireTLS: formValues.systemEmailRequireTLS,
systemEmailRejectUnauthorized: formValues.systemEmailRejectUnauthorized,
...(formValues.systemEmailUser && { systemEmailUser: formValues.systemEmailUser }),
...(formValues.systemEmailDisplayName && {
systemEmailDisplayName: formValues.systemEmailDisplayName,
}),
...(formValues.systemEmailTLSServername && {
systemEmailTLSServername: formValues.systemEmailTLSServername,
}),
...(formValues.systemEmailConnectionHost && {
systemEmailConnectionHost: formValues.systemEmailConnectionHost,
}),
});
};
const handleClearStats = async () => {
await deleteStats("/checks/team");
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 handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
if (file.type !== "application/json") {
toastError("Please select a valid JSON file");
event.target.value = "";
}
try {
const text = await file.text();
const monitors = JSON.parse(text);
if (!Array.isArray(monitors)) {
toastError("Invalid file format: expected an array of monitors");
event.target.value = "";
return;
}
await importMonitors("/monitors/import/json", { monitors });
event.target.value = "";
} catch (error) {
toastError("Error parsing JSON file. Please check the file format.");
event.target.value = "";
}
};
const onSubmit = async (data: SettingsFormData) => {
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
const dataToSend = { ...data };
if (isApiKeySet && !apiKeyHasBeenReset) {
delete (dataToSend as any).pagespeedApiKey;
}
if (isEmailPasswordSet && !emailPasswordHasBeenReset) {
delete (dataToSend as any).systemEmailPassword;
}
const result = await patch("/settings", dataToSend as SettingsFormData);
if (result?.success) {
// Update API key state from response
if (result.data) {
setIsApiKeySet(result.data.pagespeedKeySet);
setApiKeyHasBeenReset(false);
setIsEmailPasswordSet(result.data.emailPasswordSet);
setEmailPasswordHasBeenReset(false);
}
}
};
const onError = (errors: unknown) => {
logger.debug("Form validation errors", errors);
};
const languages = Object.keys(i18n.options.resources || {});
return (
<BasePage
headerKey="settings"
component="form"
onSubmit={form.handleSubmit(onSubmit, onError)}
>
<Stack gap={theme.spacing(LAYOUT.MD)}>
<ConfigBox
title={t("pages.settings.form.timezone.title")}
subtitle={t("pages.settings.form.timezone.description")}
rightContent={
<Autocomplete
value={selectedTimezone}
options={timezoneOptions}
getOptionLabel={(option: Timezone) => option.name}
isOptionEqualToValue={(option: Timezone, value: Timezone) =>
option.id === value.id
}
onChange={(_, newValue) => {
handleTimezoneChange(newValue as Timezone | null);
}}
fieldLabel={t("pages.settings.form.timezone.option.timezone.label")}
/>
}
/>
<ConfigBox
title={t("pages.settings.form.ui.title")}
subtitle={t("pages.settings.form.ui.description")}
rightContent={
<Stack gap={theme.spacing(LAYOUT.MD)}>
<Select
value={mode}
onChange={handleModeChange}
fieldLabel={t("pages.settings.form.ui.option.theme.label")}
>
<MenuItem value="light">
{t("pages.settings.form.ui.option.theme.light")}
</MenuItem>
<MenuItem value="dark">
{t("pages.settings.form.ui.option.theme.dark")}
</MenuItem>
</Select>
<Select
value={language}
onChange={handleLanguageChange}
fieldLabel={t("pages.settings.form.ui.option.language.label")}
>
{languages.map((lang) => (
<MenuItem
key={lang}
value={lang}
>
{languageNames[lang] ?? lang}
</MenuItem>
))}
</Select>
<Select
value={chartType}
onChange={handleChartTypeChange}
fieldLabel={t("pages.settings.form.ui.option.chartType.label")}
>
<MenuItem value="histogram">
{t("pages.settings.form.ui.option.chartType.histogram")}
</MenuItem>
<MenuItem value="heatmap">
{t("pages.settings.form.ui.option.chartType.heatmap")}
</MenuItem>
</Select>
<DummyChart chartType={chartType} />
</Stack>
}
/>
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.pagespeed.title")}
subtitle={t("pages.settings.form.pagespeed.description")}
rightContent={
<>
{(isApiKeySet === false || apiKeyHasBeenReset === true) && (
<Controller
name="pagespeedApiKey"
control={form.control}
defaultValue={defaults.pagespeedApiKey}
render={({ field, fieldState }) => (
<TextField
{...field}
fieldLabel={t(
"pages.settings.form.pagespeed.option.apiKey.label"
)}
type="password"
placeholder={t(
"pages.settings.form.pagespeed.option.apiKey.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
)}
{isApiKeySet === true && apiKeyHasBeenReset === false && (
<Box>
<FieldLabel>
{t("pages.settings.form.pagespeed.option.apiKey.labelSet")}
</FieldLabel>
<Button
onClick={handleResetApiKey}
variant="contained"
color="error"
>
{t("common.buttons.reset")}
</Button>
</Box>
)}
</>
}
/>
)}
{/* URL Settings */}
<ConfigBox
title={t("pages.settings.form.url.title")}
subtitle={t("pages.settings.form.url.description")}
rightContent={
<Controller
name="showURL"
control={form.control}
defaultValue={defaults.showURL}
render={({ field, fieldState }) => (
<Select
{...field}
value={field.value === undefined ? "false" : field.value.toString()}
onChange={(e) => {
const value = e.target.value === "true";
field.onChange(value);
}}
fieldLabel={t("pages.settings.form.url.option.showURL.label")}
error={!!fieldState.error}
>
<MenuItem value="true">
{t("pages.settings.form.url.option.showURL.enabled")}
</MenuItem>
<MenuItem value="false">
{t("pages.settings.form.url.option.showURL.disabled")}
</MenuItem>
</Select>
)}
/>
}
/>
{/* Clear All Stats */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.stats.title")}
subtitle={t("pages.settings.form.stats.description")}
rightContent={
<Button
variant="contained"
color="error"
onClick={() => setIsStatsDialogOpen(true)}
>
{t("common.buttons.clear")}
</Button>
}
/>
)}
{/* Check Retention */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.retention.title")}
subtitle={t("pages.settings.form.retention.description")}
rightContent={
<Controller
name="checkTTL"
control={form.control}
defaultValue={defaults.checkTTL}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.retention.option.days.label")}
min={1}
max={CHECK_TTL_SENTINEL}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 30}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
valueLabelFormat={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
formatDisplayValue={(value: number) =>
value >= CHECK_TTL_SENTINEL
? t("pages.settings.form.retention.option.days.unlimited")
: `${value}`
}
/>
)}
/>
}
/>
)}
{/* Global Thresholds */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.thresholds.title")}
subtitle={t("pages.settings.form.thresholds.description")}
rightContent={
<Stack spacing={2}>
<Controller
name="globalThresholds.cpu"
control={form.control}
defaultValue={defaults.globalThresholds?.cpu}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
min={1}
max={100}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 1}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
/>
)}
/>
<Controller
name="globalThresholds.memory"
control={form.control}
defaultValue={defaults.globalThresholds?.memory}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
min={1}
max={100}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 1}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
/>
)}
/>
<Controller
name="globalThresholds.disk"
control={form.control}
defaultValue={defaults.globalThresholds?.disk}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
min={1}
max={100}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 1}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
/>
)}
/>
<Controller
name="globalThresholds.temperature"
control={form.control}
defaultValue={defaults.globalThresholds?.temperature}
render={({ field }) => (
<SliderWithLabel
{...field}
fieldLabel={t(
"pages.settings.form.thresholds.option.temperature.label"
)}
min={1}
max={150}
sliderMaxWidth={{ xs: "100%", md: "50%" }}
value={field.value || 1}
onChange={(_, value) => field.onChange(value)}
valueLabelDisplay="auto"
/>
)}
/>
</Stack>
}
/>
)}
</Stack>
{/* Email Settings - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.email.title")}
subtitle={t("pages.settings.form.email.description")}
leftContent={
<Stack gap={theme.spacing(LAYOUT.MD)}>
<TextLink
text={t("pages.settings.form.email.descriptionTransport")}
linkText={t("pages.settings.form.email.descriptionTransportLink")}
href="https://nodemailer.com/smtp/"
target="_blank"
/>
{(() => {
const address = form.watch("systemEmailAddress") || "";
const displayName = form.watch("systemEmailDisplayName")?.trim();
return (
<>
<Box
component="pre"
p={2}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.action.hover}
sx={{
fontFamily: theme.typography.fontFamilyMonospace,
overflow: "auto",
}}
>
<code>
{JSON.stringify(
{
host: form.watch("systemEmailHost") || "",
port: form.watch("systemEmailPort") || "",
secure: form.watch("systemEmailSecure") ?? false,
auth: {
user: form.watch("systemEmailUser") || address,
pass: "<your_password>",
},
name: form.watch("systemEmailConnectionHost") || "localhost",
pool: form.watch("systemEmailPool") ?? false,
tls: {
rejectUnauthorized:
form.watch("systemEmailRejectUnauthorized") ?? true,
ignoreTLS: form.watch("systemEmailIgnoreTLS") ?? false,
requireTLS: form.watch("systemEmailRequireTLS") ?? false,
servername: form.watch("systemEmailTLSServername") || "",
},
},
null,
2
)}
</code>
</Box>
{address && (
<Box
component="pre"
p={2}
borderRadius={theme.shape.borderRadius}
bgcolor={theme.palette.action.hover}
sx={{
fontFamily: theme.typography.fontFamilyMonospace,
overflow: "auto",
}}
>
<code>
{`From: ${displayName ? `"${displayName}" <${address}>` : address}`}
</code>
</Box>
)}
</>
);
})()}
</Stack>
}
rightContent={
<Stack gap={theme.spacing(LAYOUT.MD)}>
{/* Email Host */}
<Controller
name="systemEmailHost"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.email.option.host.label")}
placeholder={t("pages.settings.form.email.option.host.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Email Port */}
<Controller
name="systemEmailPort"
control={form.control}
render={({ field, fieldState }) => (
<TextField
name={field.name}
ref={field.ref}
onBlur={field.onBlur}
value={
field.value === undefined || field.value === 0 ? "" : field.value
}
onChange={(e) => {
const val = e.target.value;
field.onChange(val === "" ? 0 : Number(val));
}}
fieldLabel={t("pages.settings.form.email.option.port.label")}
type="number"
inputProps={{ min: 0 }}
placeholder={t("pages.settings.form.email.option.port.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Email Address */}
<Controller
name="systemEmailAddress"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.email.option.address.label")}
placeholder={t(
"pages.settings.form.email.option.address.placeholder"
)}
type="email"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Email Display Name (Optional) */}
<Controller
name="systemEmailDisplayName"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.email.option.displayName.label")}
placeholder={t(
"pages.settings.form.email.option.displayName.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Email User (Optional) */}
<Controller
name="systemEmailUser"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.email.option.user.label")}
placeholder={t("pages.settings.form.email.option.user.placeholder")}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Email Password with Reset Pattern */}
{isEmailPasswordSet && !emailPasswordHasBeenReset ? (
<Box>
<FieldLabel>
{t("pages.settings.form.email.option.password.labelSet")}
</FieldLabel>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(LAYOUT.XS)}
>
<Button
variant="contained"
color="error"
size="small"
onClick={handleResetEmailPassword}
>
{t("common.buttons.reset")}
</Button>
</Stack>
</Box>
) : (
<Controller
name="systemEmailPassword"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.email.option.password.label")}
type="password"
placeholder={t(
"pages.settings.form.email.option.password.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
)}
{/* TLS Servername (Optional) */}
<Controller
name="systemEmailTLSServername"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t("pages.settings.form.email.option.tlsServername.label")}
placeholder={t(
"pages.settings.form.email.option.tlsServername.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Connection Host (Optional) */}
<Controller
name="systemEmailConnectionHost"
control={form.control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.settings.form.email.option.connectionHost.label"
)}
placeholder={t(
"pages.settings.form.email.option.connectionHost.placeholder"
)}
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>
{/* Boolean Switches */}
<Box
sx={{
display: "flex",
flexDirection: "column",
gap: theme.spacing(LAYOUT.XS),
}}
>
{[
{
name: "systemEmailSecure",
label: t("pages.settings.form.email.option.secure.label"),
},
{
name: "systemEmailPool",
label: t("pages.settings.form.email.option.pool.label"),
},
{
name: "systemEmailIgnoreTLS",
label: t("pages.settings.form.email.option.ignoreTLS.label"),
},
{
name: "systemEmailRequireTLS",
label: t("pages.settings.form.email.option.requireTLS.label"),
},
{
name: "systemEmailRejectUnauthorized",
label: t("pages.settings.form.email.option.rejectUnauthorized.label"),
},
].map(({ name, label }) => (
<Controller
key={name}
name={name as any}
control={form.control}
render={({ field }) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Typography>{label}</Typography>
<SwitchComponent
checked={field.value ?? false}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
field.onChange(e.target.checked)
}
/>
</Box>
)}
/>
))}
</Box>
{/* Test Email Button */}
<Box>
<Button
variant="contained"
loading={isSendingTestEmail}
onClick={handleSendTestEmail}
disabled={
!form.watch("systemEmailHost") ||
!form.watch("systemEmailPort") ||
!form.watch("systemEmailAddress") ||
!form.watch("systemEmailPassword")
}
>
{t("common.buttons.sendTestEmail")}
</Button>
</Box>
</Stack>
}
/>
)}
{/* Demo Monitors - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.demoMonitors.title")}
subtitle={t("pages.settings.form.demoMonitors.description")}
rightContent={
<Box>
<Button
variant="contained"
loading={isPostingDemoMonitors}
onClick={async () => {
await postDemoMonitors("/monitors/demo", {});
}}
>
{t("common.buttons.addDemo")}
</Button>
</Box>
}
/>
)}
{/* Remove All Monitors - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.removeMonitors.title")}
subtitle={t("pages.settings.form.removeMonitors.description")}
rightContent={
<Box>
<Button
variant="contained"
color="error"
loading={isDeletingAllMonitors}
onClick={() => setIsDemoMonitorsDialogOpen(true)}
>
{t("common.buttons.removeMonitors")}
</Button>
</Box>
}
/>
)}
{/* Export Monitors - Admin Only */}
{isAdmin && (
<ConfigBox
title={t("pages.settings.form.importExportMonitors.title")}
subtitle={t("pages.settings.form.importExportMonitors.description")}
rightContent={
<Stack
gap={theme.spacing(LAYOUT.MD)}
direction={"row"}
>
<input
id="monitor-import-input"
type="file"
accept=".json"
style={{ display: "none" }}
onChange={handleFileSelect}
/>
<Button
variant="contained"
onClick={() => document.getElementById("monitor-import-input")?.click()}
disabled={isImportingMonitors}
>
{t("common.buttons.importFromJSON")}
</Button>
<Button
variant="contained"
onClick={handleExportMonitors}
>
{t("common.buttons.exportToJSON")}
</Button>
</Stack>
}
/>
)}
{/* 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}
title={t("pages.settings.form.stats.dialog.title")}
content={t("pages.settings.form.stats.dialog.description")}
onCancel={() => setIsStatsDialogOpen(false)}
onConfirm={handleClearStats}
loading={isDeletingStats}
/>
{/* Delete All Monitors Confirmation Dialog */}
<Dialog
open={isDemoMonitorsDialogOpen}
title={t("pages.settings.form.removeMonitors.dialog.title")}
content={t("pages.settings.form.removeMonitors.dialog.description")}
onCancel={() => setIsDemoMonitorsDialogOpen(false)}
onConfirm={async () => {
await deleteAllMonitors("/monitors/");
setIsDemoMonitorsDialogOpen(false);
}}
loading={isDeletingAllMonitors}
confirmColor="error"
confirmText={t("common.buttons.removeMonitors")}
>
<Typography variant="body1">
{t("pages.settings.form.removeMonitors.dialog.paragraph")}
</Typography>
</Dialog>
<Stack
direction="row"
justifyContent="flex-end"
sx={{
position: "sticky",
bottom: 0,
padding: theme.spacing(LAYOUT.MD),
zIndex: 1000,
}}
>
{/* Validation Error Display */}
{Object.keys(form.formState.errors).length > 0 && (
<Alert
severity="error"
sx={{ mb: 2, flexGrow: 1, mr: 2 }}
>
<Typography
variant="body2"
sx={{ fontWeight: 600, mb: 1 }}
>
{t("pages.settings.form.validation.errorMessage")}
</Typography>
<Box
component="ul"
sx={{ m: 0, pl: 2 }}
>
{Object.entries(form.formState.errors).map(([field, error]) => {
const message =
typeof error === "object" && error?.message
? error.message
: "Invalid value";
return (
<li key={field}>
<Typography variant="body2">
<strong>{field}:</strong> {message}
</Typography>
</li>
);
})}
</Box>
</Alert>
)}
<Button
loading={isSaving}
type="submit"
variant="contained"
color="primary"
disabled={!form.formState.isValid}
>
{t("common.buttons.save")}
</Button>
</Stack>
</BasePage>
);
};
export default SettingsPage;