feat(client): group settings into five tabs
The settings page rendered all 13 ConfigBox sections in one column, so finding anything meant scrolling past everything else. The sections are now grouped under five tabs by subject: General timezone, appearance, status page URL, about Monitoring thresholds, check retention, demo monitors Email email settings Integrations global proxy, PageSpeed API key Data import/export, monitor history, system reset No section changed. Every ConfigBox moved verbatim with its props, children and isAdmin guard intact; the only edit inside one is Prettier rewrapping an arrow function that shifted indentation level. Tabs are derived from a filtered array rather than conditional JSX, so indices stay correct when admin-only tabs drop out. Nine of the thirteen sections are admin-gated, which leaves a non-admin four sections in General alone -- the bar is hidden entirely in that case rather than showing four empty tabs. Panels stay mounted and toggle with `hidden`. Conditionally rendering them would unmount inputs belonging to the shared react-hook-form, and since Save is disabled on !isValid, a partial tree can compute a stale isValid and lock saving. The active tab lives in the URL as ?tab=<key> so reloads and shared links land in the right place, following the useSearchParams pattern in StatusPage/Status. An unknown tab, or an admin tab requested by a non-admin, falls back to the first visible tab. Both confirmation dialogs and the sticky save bar stay outside the panels: the dialogs would unmount mid-request otherwise, and the save bar serves the whole form. Because the save bar lists validation errors by field name, an error could name a field on a hidden tab. onError now switches to the tab owning the first error before the alert renders. Two titles are corrected to sentence case while their sections move: "Global Thresholds" and "Monitor IP/URL on Status Page". Claude-Session: https://claude.ai/code/session_01AbYLvwpEaZFXktHVcqrFRd
This commit is contained in:
1 parent
46211ac61d
commit
9d0f0116f3
2 files changed
+497
-384
No files matched your search
+488
-382
@@ -1,4 +1,4 @@
|
||||
import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements";
|
||||
import { BasePage, ConfigBox, TextLink, Tabs, Tab } from "@/Components/design-elements";
|
||||
import { Autocomplete, Select, Dialog } from "@/Components/inputs";
|
||||
import { logger } from "@/Utils/logger";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
@@ -46,6 +46,33 @@ import { FormNumberField } from "@/Components/inputs/forms/FormNumberField";
|
||||
import { FormSelectField } from "@/Components/inputs/forms/FormSelectField";
|
||||
import type { ProxyResponse } from "@/Types/Proxy";
|
||||
import type { AppSettingsResponse } from "@/Types/Settings";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const SETTINGS_TABS = [
|
||||
{ key: "general", labelKey: "pages.settings.tabs.general" },
|
||||
{ key: "monitoring", labelKey: "pages.settings.tabs.monitoring", adminOnly: true },
|
||||
{ key: "email", labelKey: "pages.settings.tabs.email", adminOnly: true },
|
||||
{ key: "integrations", labelKey: "pages.settings.tabs.integrations", adminOnly: true },
|
||||
{ key: "data", labelKey: "pages.settings.tabs.data", adminOnly: true },
|
||||
] as const;
|
||||
|
||||
type SettingsTabKey = (typeof SETTINGS_TABS)[number]["key"];
|
||||
|
||||
// Maps a validation error back to the tab holding it, so a failed save can
|
||||
// reveal the offending field instead of naming one on a hidden tab.
|
||||
const FIELD_TAB: Record<string, SettingsTabKey> = {
|
||||
checkTTL: "monitoring",
|
||||
globalThresholds: "monitoring",
|
||||
pagespeedApiKey: "integrations",
|
||||
globalProxyEnabled: "integrations",
|
||||
globalProxyId: "integrations",
|
||||
showURL: "general",
|
||||
};
|
||||
|
||||
const tabForField = (field: string): SettingsTabKey =>
|
||||
field.startsWith("systemEmail")
|
||||
? "email"
|
||||
: (FIELD_TAB[field.split(".")[0]] ?? "general");
|
||||
|
||||
export const SettingsPage = () => {
|
||||
const theme = useTheme();
|
||||
@@ -53,6 +80,28 @@ export const SettingsPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { toastError } = useToast();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const visibleTabs = useMemo(
|
||||
() => SETTINGS_TABS.filter((tab) => !("adminOnly" in tab) || isAdmin),
|
||||
[isAdmin]
|
||||
);
|
||||
|
||||
// An unknown tab, or an admin tab requested by a non-admin, falls back to the
|
||||
// first tab the current user can actually see.
|
||||
const requestedTab = searchParams.get("tab");
|
||||
const activeTab: SettingsTabKey =
|
||||
visibleTabs.find((tab) => tab.key === requestedTab)?.key ?? visibleTabs[0].key;
|
||||
|
||||
const handleTabChange = (tab: SettingsTabKey) => {
|
||||
const updated = new URLSearchParams(searchParams);
|
||||
if (tab === visibleTabs[0].key) {
|
||||
updated.delete("tab");
|
||||
} else {
|
||||
updated.set("tab", tab);
|
||||
}
|
||||
setSearchParams(updated, { replace: true });
|
||||
};
|
||||
// Local state for demo monitors dialog
|
||||
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
|
||||
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
|
||||
@@ -298,6 +347,15 @@ export const SettingsPage = () => {
|
||||
|
||||
const onError = (errors: unknown) => {
|
||||
logger.debug("Form validation errors", errors);
|
||||
// The save bar lists errors by field name, so reveal the tab owning the
|
||||
// first one rather than naming a field the user cannot see.
|
||||
const firstField = Object.keys(errors ?? {})[0];
|
||||
if (firstField) {
|
||||
const target = tabForField(firstField);
|
||||
if (visibleTabs.some((tab) => tab.key === target)) {
|
||||
handleTabChange(target);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
@@ -324,7 +382,26 @@ export const SettingsPage = () => {
|
||||
component="form"
|
||||
onSubmit={form.handleSubmit(onSubmit, onError)}
|
||||
>
|
||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||
{visibleTabs.length > 1 && (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChange={(_, value: SettingsTabKey) => handleTabChange(value)}
|
||||
>
|
||||
{visibleTabs.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
value={tab.key}
|
||||
label={t(tab.labelKey)}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{/* general tab */}
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
hidden={activeTab !== "general"}
|
||||
>
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.timezone.title")}
|
||||
subtitle={t("pages.settings.form.timezone.description")}
|
||||
@@ -390,6 +467,360 @@ export const SettingsPage = () => {
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
{/* URL Settings */}
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.url.title")}
|
||||
subtitle={t("pages.settings.form.url.description")}
|
||||
rightContent={
|
||||
<FormSwitchField
|
||||
name="showURL"
|
||||
label={t("pages.settings.form.url.option.showURL.label")}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* 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>
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* monitoring tab */}
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
hidden={activeTab !== "monitoring"}
|
||||
>
|
||||
{/* Global Thresholds */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.thresholds.title")}
|
||||
subtitle={t("pages.settings.form.thresholds.description")}
|
||||
rightContent={
|
||||
<Stack spacing={2}>
|
||||
<FormSliderField
|
||||
name="globalThresholds.cpu"
|
||||
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
<FormSliderField
|
||||
name="globalThresholds.memory"
|
||||
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
<FormSliderField
|
||||
name="globalThresholds.disk"
|
||||
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
|
||||
<FormSliderField
|
||||
name="globalThresholds.temperature"
|
||||
fieldLabel={t(
|
||||
"pages.settings.form.thresholds.option.temperature.label"
|
||||
)}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={150}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Check Retention */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.retention.title")}
|
||||
subtitle={t("pages.settings.form.retention.description")}
|
||||
rightContent={
|
||||
<FormSliderField
|
||||
name="checkTTL"
|
||||
fieldLabel={t("pages.settings.form.retention.option.days.label")}
|
||||
min={1}
|
||||
max={CHECK_TTL_SENTINEL}
|
||||
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}`
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* email tab */}
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
hidden={activeTab !== "email"}
|
||||
>
|
||||
{/* 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 */}
|
||||
<FormTextField
|
||||
name="systemEmailHost"
|
||||
fieldLabel={t("pages.settings.form.email.option.host.label")}
|
||||
placeholder={t("pages.settings.form.email.option.host.placeholder")}
|
||||
/>
|
||||
|
||||
{/* Email Port */}
|
||||
<FormNumberField
|
||||
name="systemEmailPort"
|
||||
fieldLabel={t("pages.settings.form.email.option.port.label")}
|
||||
placeholder={t("pages.settings.form.email.option.port.placeholder")}
|
||||
/>
|
||||
|
||||
{/* Email Address */}
|
||||
<FormTextField
|
||||
name="systemEmailAddress"
|
||||
type="email"
|
||||
fieldLabel={t("pages.settings.form.email.option.address.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.address.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email Display Name (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailDisplayName"
|
||||
fieldLabel={t("pages.settings.form.email.option.displayName.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.displayName.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email User (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailUser"
|
||||
fieldLabel={t("pages.settings.form.email.option.user.label")}
|
||||
placeholder={t("pages.settings.form.email.option.user.placeholder")}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
) : (
|
||||
<FormTextField
|
||||
name="systemEmailPassword"
|
||||
type="password"
|
||||
fieldLabel={t("pages.settings.form.email.option.password.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.password.placeholder"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TLS Servername (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailTLSServername"
|
||||
fieldLabel={t("pages.settings.form.email.option.tlsServername.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.tlsServername.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Connection Host (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailConnectionHost"
|
||||
fieldLabel={t(
|
||||
"pages.settings.form.email.option.connectionHost.label"
|
||||
)}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.connectionHost.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Boolean Switches */}
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
{emailSwitches.map(({ name, label }) => (
|
||||
<FormSwitchField
|
||||
key={name}
|
||||
name={name}
|
||||
label={label}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
))}
|
||||
</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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* integrations tab */}
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
hidden={activeTab !== "integrations"}
|
||||
>
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.globalProxy.title")}
|
||||
@@ -457,20 +888,49 @@ export const SettingsPage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* URL Settings */}
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.url.title")}
|
||||
subtitle={t("pages.settings.form.url.description")}
|
||||
rightContent={
|
||||
<FormSwitchField
|
||||
name="showURL"
|
||||
label={t("pages.settings.form.url.option.showURL.label")}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* data tab */}
|
||||
<Stack
|
||||
gap={theme.spacing(LAYOUT.MD)}
|
||||
hidden={activeTab !== "data"}
|
||||
>
|
||||
{/* 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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* Clear All Stats */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
@@ -488,381 +948,27 @@ export const SettingsPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Check Retention */}
|
||||
{/* Remove All Monitors - Admin Only */}
|
||||
{isAdmin && (
|
||||
<ConfigBox
|
||||
title={t("pages.settings.form.retention.title")}
|
||||
subtitle={t("pages.settings.form.retention.description")}
|
||||
title={t("pages.settings.form.removeMonitors.title")}
|
||||
subtitle={t("pages.settings.form.removeMonitors.description")}
|
||||
rightContent={
|
||||
<FormSliderField
|
||||
name="checkTTL"
|
||||
fieldLabel={t("pages.settings.form.retention.option.days.label")}
|
||||
min={1}
|
||||
max={CHECK_TTL_SENTINEL}
|
||||
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}>
|
||||
<FormSliderField
|
||||
name="globalThresholds.cpu"
|
||||
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
<FormSliderField
|
||||
name="globalThresholds.memory"
|
||||
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
<FormSliderField
|
||||
name="globalThresholds.disk"
|
||||
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={100}
|
||||
/>
|
||||
|
||||
<FormSliderField
|
||||
name="globalThresholds.temperature"
|
||||
fieldLabel={t(
|
||||
"pages.settings.form.thresholds.option.temperature.label"
|
||||
)}
|
||||
valueLabelDisplay="auto"
|
||||
min={1}
|
||||
max={150}
|
||||
/>
|
||||
</Stack>
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
loading={isDeletingAllMonitors}
|
||||
onClick={() => setIsDemoMonitorsDialogOpen(true)}
|
||||
>
|
||||
{t("common.buttons.removeMonitors")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</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 */}
|
||||
<FormTextField
|
||||
name="systemEmailHost"
|
||||
fieldLabel={t("pages.settings.form.email.option.host.label")}
|
||||
placeholder={t("pages.settings.form.email.option.host.placeholder")}
|
||||
/>
|
||||
|
||||
{/* Email Port */}
|
||||
<FormNumberField
|
||||
name="systemEmailPort"
|
||||
fieldLabel={t("pages.settings.form.email.option.port.label")}
|
||||
placeholder={t("pages.settings.form.email.option.port.placeholder")}
|
||||
/>
|
||||
|
||||
{/* Email Address */}
|
||||
<FormTextField
|
||||
name="systemEmailAddress"
|
||||
type="email"
|
||||
fieldLabel={t("pages.settings.form.email.option.address.label")}
|
||||
placeholder={t("pages.settings.form.email.option.address.placeholder")}
|
||||
/>
|
||||
|
||||
{/* Email Display Name (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailDisplayName"
|
||||
fieldLabel={t("pages.settings.form.email.option.displayName.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.displayName.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Email User (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailUser"
|
||||
fieldLabel={t("pages.settings.form.email.option.user.label")}
|
||||
placeholder={t("pages.settings.form.email.option.user.placeholder")}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
) : (
|
||||
<FormTextField
|
||||
name="systemEmailPassword"
|
||||
type="password"
|
||||
fieldLabel={t("pages.settings.form.email.option.password.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.password.placeholder"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TLS Servername (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailTLSServername"
|
||||
fieldLabel={t("pages.settings.form.email.option.tlsServername.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.tlsServername.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Connection Host (Optional) */}
|
||||
<FormTextField
|
||||
name="systemEmailConnectionHost"
|
||||
fieldLabel={t("pages.settings.form.email.option.connectionHost.label")}
|
||||
placeholder={t(
|
||||
"pages.settings.form.email.option.connectionHost.placeholder"
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Boolean Switches */}
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
>
|
||||
{emailSwitches.map(({ name, label }) => (
|
||||
<FormSwitchField
|
||||
key={name}
|
||||
name={name}
|
||||
label={label}
|
||||
labelPlacement="start"
|
||||
/>
|
||||
))}
|
||||
</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}
|
||||
|
||||
@@ -1333,6 +1333,13 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"tabs": {
|
||||
"general": "General",
|
||||
"monitoring": "Monitoring",
|
||||
"email": "Email",
|
||||
"integrations": "Integrations",
|
||||
"data": "Data"
|
||||
},
|
||||
"form": {
|
||||
"timezone": {
|
||||
"title": "Display timezone",
|
||||
@@ -1390,7 +1397,7 @@
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"title": "Monitor IP/URL on Status Page",
|
||||
"title": "Monitor IP/URL on status page",
|
||||
"description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
|
||||
"option": {
|
||||
"showURL": {
|
||||
@@ -1422,7 +1429,7 @@
|
||||
}
|
||||
},
|
||||
"thresholds": {
|
||||
"title": "Global Thresholds",
|
||||
"title": "Global thresholds",
|
||||
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
|
||||
"option": {
|
||||
"cpu": {
|
||||
|
||||
Reference in new issue
Block a user