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:
gorkem-bwl committed 2026-08-28 21:31:09 -04:00
1 parent 46211ac61d
commit 9d0f0116f3
2 files changed
+497 -384

No files matched your search

+488 -382
View File
@@ -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}
+9 -2
View File
@@ -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": {