This commit is contained in:
Alex Holliday committed 2025-12-12 12:17:18 -08:00
1 parent c9dea5c0ae
commit 901b5a43c8
20 files changed
+393 -137

No files matched your search

+205
View File
@@ -1,6 +1,10 @@
{
"common": {
"appName": "Checkmate",
"toasts": {
"recoveryEmailSent": "If the email exists, a recovery link has been sent",
"passwordResetSuccessful": "Password reset successful!"
},
"table": {
"headers": {
"name": "Name",
@@ -19,6 +23,14 @@
"settings": "Settings"
},
"buttons": {
"acceptInvitation": "Accept invitation",
"register": "Register",
"submitNewPassword": "Submit new password",
"requestRecovery": "Request recovery email",
"sendInvite": "Send invite",
"testTransport": "Test Transport config",
"saveEmailSettings": "Save Email Settings",
"saveRetentionSettings": "Save Retention Settings",
"incidents": "Incidents",
"delete": "Delete",
"confirm": "Confirm",
@@ -32,7 +44,11 @@
"configure": "Configure",
"createNew": "Create new",
"testNotification": "Send test notification",
"updateProfile": "Update profile",
"removeAllMonitors": "Remove all monitors",
"resolve": "Resolve",
"exportJson": "Export to JSON",
"importJson": "Import from JSON",
"range": {
"all": "All",
"recent": "Recent",
@@ -87,6 +103,7 @@
}
}
},
"menu": {
"uptime": "Uptime",
"pagespeed": "PageSpeed",
@@ -96,11 +113,59 @@
"checks": "Checks",
"statusPages": "Status Pages",
"maintenance": "Maintenance",
"diagnostics": "Diagnostics",
"support": "Support",
"discussions": "Discussions",
"docs": "Documentation",
"changelog": "Changelog"
},
"auth": {
"register": {
"header": {
"title": "Welcome to Checkmate!",
"description": "Create an account to get started"
}
},
"login": {
"header": {
"title": "Welcome back to Checkmate",
"description": "Log in to continue"
},
"links": {
"forgotPassword": {
"text": "Forgot password?",
"linkText": "Reset password"
},
"register": {
"text": "Don't have an account?",
"linkText": "Register"
}
}
},
"recovery": {
"header": {
"title": "Enter your email to recover your account"
}
},
"reset": {
"header": {
"title": "Set a new password"
}
},
"common": {
"form": {
"optionEmail": "Email",
"optionEmailPlaceholder": "test@test.com",
"optionFirstName": "First Name",
"optionFirstNamePlaceholder": "Your first Name",
"optionLastName": "Last Name",
"optionLastNamePlaceholder": "Your last Name",
"optionPassword": "Password",
"optionConfirmPassword": "Confirm Password",
"optionConfirmPasswordPlaceholder": "Re-enter password to confirm"
}
}
},
"monitors": {
"common": {
@@ -478,5 +543,145 @@
],
"title": "A maintenance window is used to:"
}
},
"export": {
"form": {
"exportMonitors": {
"title": "Export current team's monitors",
"description": "This will only export the basic monitor settings, not the check history, incidents history, or notifications",
"optionExport": "Click here to export your monitors to JSON"
},
"importMonitors": {
"title": "Import monitors",
"description": "Import monitors from a JSON file. Sample:",
"optionImport": "Click here to import your monitors from JSON"
},
"exportIncidents": {
"title": "Export incidents",
"description": "Export current team's incidents as JSON",
"optionExport": "Click here to export your incidents to JSON"
}
}
},
"profile": {
"infoBox": {
"title": "View and edit your profile",
"description": "You can set your name and password here"
}
},
"settings": {
"form": {
"ui": {
"title": "UI Settings",
"description": "Configure time zone, UI mode, and language preferences",
"optionTimezone": "Time zone",
"optionUIMode": "UI Mode",
"valueUIModeLight": "Light",
"valueUIModeDark": "Dark",
"optionLanguage": "Language"
},
"chart": {
"title": "Chart settings",
"description": "Choose what type of Uptime chart you want to see on the dashboard and status page",
"optionChartType": "Chart Type",
"valueChartTypeHeatmap": "Heatmap",
"descriptionChartTypeHeatmap": "Compact tiles; color shows response speed, top strip shows availability.",
"valueChartTypeHistogram": "Histogram",
"descriptionChartTypeHistogram": "One bar per check; height shows response time, color shows status."
},
"monitors": {
"title": "Monitors",
"description": "Monitor related settings",
"descriptionRemoveAllMonitors": "Remove all monitors from your organization"
}
}
},
"adminSettings": {
"form": {
"email": {
"title": "Email",
"description": "Configure the email settings for your system. This is used to send notifications and alerts.",
"descriptionTransport": "This builds an SMTP transport for NodeMailer",
"linkTransport": "See specifications here",
"optionHost": "Email host - Hostname or IP address to connect to",
"optionPort": "Email port - Port to connect to",
"optionUser": "Email user - Username for authentication, overrides email address if specified",
"optionAddress": "Email address - Used for authentication",
"optionPassword": "Email password - Password for authentication",
"optionTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP",
"optionConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting",
"optionSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS",
"optionPool": "Enable connection pooling: Reuse existing connections to improve performance",
"optionIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it",
"optionRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported",
"optionRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates"
},
"retention": {
"title": "Global data retention settings",
"description": "Set data retention policy. This affects all organizations and teams",
"optionRetentionDays": "Days to retain data"
}
}
},
"teams": {
"infoBox": {
"title": "Team Collaboration",
"description": "Organize your monitoring workspace by creating teams. Control who can view and manage monitors, set up role-based permissions, and collaborate effectively."
},
"table": {
"headers": {
"teamDescription": "Team description"
}
},
"details": {
"infoHeader": {
"name": "Name",
"description": "Description"
},
"table": {
"title": "Team members",
"headers": {
"role": "Role",
"permissions": "Permissions"
}
}
}
},
"teamMember": {
"breadcrumbOverrideConfigTeams": "Teams",
"breadcrumbOverrideConfigTeamMember": "Configure team member",
"breadcrumbOverrideCreateTeamMember": "Create team member",
"form": {
"teamMember": {
"title": "Team member",
"description": "Team member selection",
"optionTeamMember": "Team member"
},
"role": {
"title": "Role",
"description": "Choose your role for this new team. You will be the first member",
"optionRole": "Role",
"optionIncludedPermissions": "Included permissions"
}
}
},
"invite": {
"form": {
"required": {
"title": "Required",
"description": "These fields are required",
"optionTeam": "Team",
"optionTeamRole": "Team role",
"optionPermissions": "Permissions"
},
"optional": {
"title": "Optional",
"description": "You can also assign an organization role",
"optionOrganizationRole": "Organization role"
},
"inviteLink": {
"title": "Invite link"
}
}
}
}
+15 -12
View File
@@ -71,8 +71,8 @@ const Invite = () => {
return (
<BasePage component="form" onSubmit={handleSubmit(onSubmit)}>
<ConfigBox
title="Required"
subtitle="These fields are required"
title={t("invite.form.required.title")}
subtitle={t("invite.form.required.description")}
rightContent={
<Stack gap={theme.spacing(4)} maxWidth={400}>
<Controller
@@ -82,9 +82,9 @@ const Invite = () => {
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.email.label")}
fieldLabel={t("auth.common.form.optionEmail")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
placeholder={t("auth.common.form.optionEmailPlaceholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
@@ -98,7 +98,7 @@ const Invite = () => {
<Stack gap={theme.spacing(8)}>
<Select
value={field.value}
fieldLabel="Team"
fieldLabel={t("invite.form.required.optionTeam")}
error={!!errors.teamId}
onChange={field.onChange}
>
@@ -125,7 +125,7 @@ const Invite = () => {
<Stack gap={theme.spacing(8)}>
<Select
value={field.value}
fieldLabel="Team role"
fieldLabel={t("invite.form.required.optionTeamRole")}
error={!!errors.teamRoleId}
onChange={field.onChange}
>
@@ -139,7 +139,8 @@ const Invite = () => {
</Select>
<Stack>
<Typography mb={theme.spacing(4)}>
{currentRole && `Permissions:`}
{currentRole &&
`${t("invite.form.required.optionPermissions")}:`}
</Typography>
{currentRole?.permissions?.map((perm: string) => (
<Typography variant="body2" key={perm}>
@@ -156,8 +157,8 @@ const Invite = () => {
/>
<ConfigBox
title="Optional"
subtitle="You can also assign an organization role"
title={t("invite.form.optional.title")}
subtitle={t("invite.form.optional.description")}
rightContent={
<Stack gap={theme.spacing(4)} maxWidth={400}>
<Controller
@@ -171,7 +172,9 @@ const Invite = () => {
<Stack gap={theme.spacing(8)}>
<Select
value={field.value}
fieldLabel="Organization role"
fieldLabel={t(
"invite.form.optional.optionOrganizationRole"
)}
error={!!errors.orgRoleId}
onChange={field.onChange}
>
@@ -202,7 +205,7 @@ const Invite = () => {
/>
{token && (
<Stack>
<Typography variant="h2">Invite link:</Typography>
<Typography variant="h2">{`${t("invite.form.inviteLink.title")}:`}</Typography>
<Typography>{`${HOST}/register/${token}`}</Typography>
</Stack>
)}
@@ -213,7 +216,7 @@ const Invite = () => {
color="primary"
type="submit"
>
Send Invite
{t("common.buttons.sendInvite")}
</Button>
</Box>
</BasePage>
+9 -9
View File
@@ -72,8 +72,8 @@ const Login = () => {
return (
<AuthBasePage
title={t("auth.login.welcome")}
subtitle={t("auth.login.heading")}
title={t("auth.login.header.title")}
subtitle={t("auth.login.header.description")}
>
<Stack
width={"100%"}
@@ -103,9 +103,9 @@ const Login = () => {
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.email.label")}
fieldLabel={t("auth.common.form.optionEmail")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
placeholder={t("auth.common.form.optionEmailPlaceholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
@@ -119,7 +119,7 @@ const Login = () => {
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.password.label")}
fieldLabel={t("auth.common.form.optionPassword")}
fullWidth
placeholder="••••••••••"
error={!!errors.password}
@@ -138,13 +138,13 @@ const Login = () => {
</Button>
</Stack>
<TextLink
text={t("auth.login.links.forgotPassword")}
linkText={t("auth.login.links.forgotPasswordLink")}
text={t("auth.login.links.forgotPassword.text")}
linkText={t("auth.login.links.forgotPassword.linkText")}
href="/recovery"
/>
<TextLink
text={t("auth.login.links.register")}
linkText={t("auth.login.links.registerLink")}
text={t("auth.login.links.register.text")}
linkText={t("auth.login.links.register.linkText")}
href="/register"
/>
</Stack>
+2 -2
View File
@@ -52,8 +52,8 @@ const Register = () => {
return (
<AuthBasePage
title={t("auth.registration.welcome")}
subtitle={t("auth.registration.heading.user")}
title={t("auth.register.header.title")}
subtitle={t("auth.register.header.description")}
>
<Stack alignItems={"center"} width={"100%"}>
<RegisterForm onSubmit={onSubmit} loading={loading} error={error} />
+12 -10
View File
@@ -88,9 +88,9 @@ export const RegisterForm = ({
<TextInput
disabled={mode === "invite"}
{...field}
fieldLabel={t("auth.common.inputs.email.label")}
fieldLabel={t("auth.common.form.optionEmail")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
placeholder={t("auth.common.form.optionEmailPlaceholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
@@ -104,9 +104,9 @@ export const RegisterForm = ({
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.firstName.label")}
fieldLabel={t("auth.common.form.optionFirstName")}
fullWidth
placeholder={t("auth.common.inputs.firstName.placeholder")}
placeholder={t("auth.common.form.optionFirstNamePlaceholder")}
error={!!errors.firstName}
helperText={errors.firstName ? errors.firstName.message : ""}
/>
@@ -120,9 +120,9 @@ export const RegisterForm = ({
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.lastName.label")}
fieldLabel={t("auth.common.form.optionLastName")}
fullWidth
placeholder={t("auth.common.inputs.lastName.placeholder")}
placeholder={t("auth.common.form.optionLastNamePlaceholder")}
error={!!errors.lastName}
helperText={errors.lastName ? errors.lastName.message : ""}
/>
@@ -136,7 +136,7 @@ export const RegisterForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.password.label")}
fieldLabel={t("auth.common.form.optionPassword")}
fullWidth
placeholder="••••••••••"
error={!!errors.password}
@@ -152,9 +152,9 @@ export const RegisterForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.passwordConfirm.label")}
fieldLabel={t("auth.common.form.optionConfirmPassword")}
fullWidth
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
placeholder={t("auth.common.form.optionConfirmPasswordPlaceholder")}
error={!!errors.confirmPassword}
helperText={
errors.confirmPassword ? errors.confirmPassword.message : ""
@@ -169,7 +169,9 @@ export const RegisterForm = ({
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
{mode === "invite" ? "Accept invitation" : "Register"}
{mode === "invite"
? t("common.buttons.acceptInvitation")
: t("common.buttons.register")}
</Button>
{error && <Typography color="error">{error}</Typography>}
</Stack>
+2 -2
View File
@@ -67,8 +67,8 @@ const RegisterInvite = () => {
return (
<AuthBasePage
title={t("auth.registration.welcome")}
subtitle={t("auth.registration.heading.user")}
title={t("auth.register.header.title")}
subtitle={t("auth.register.header.description")}
>
<Stack alignItems={"center"} width={"100%"}>
<RegisterForm
+18 -13
View File
@@ -6,10 +6,11 @@ import { Typography } from "@mui/material";
import { useGetOnDemand, usePost } from "@/hooks/UseApi";
import { useTheme } from "@mui/material/styles";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
const ExportPage = () => {
const fileInputRef = useRef<HTMLInputElement | null>(null);
const theme = useTheme();
const { t } = useTranslation();
const { get, loading } = useGetOnDemand();
const { post, loading: isPosting } = usePost();
@@ -67,11 +68,13 @@ const ExportPage = () => {
return (
<BasePage>
<ConfigBox
title="Export current team's monitors"
subtitle="This will only export the basic monitor settings, not the check history, incidents history, or notifications"
title={t("export.form.exportMonitors.title")}
subtitle={t("export.form.exportMonitors.description")}
rightContent={
<Stack gap={theme.spacing(4)}>
<Typography>Click here to export your monitors to JSON</Typography>
<Typography>
{t("export.form.exportMonitors.optionExport")}
</Typography>
<Box>
<Button
loading={loading}
@@ -79,15 +82,15 @@ const ExportPage = () => {
color="primary"
onClick={() => exportJson("/monitors/export", "monitors")}
>
Export to JSON
{t("common.buttons.exportJson")}
</Button>
</Box>
</Stack>
}
/>
<ConfigBox
title="Import monitors"
subtitle="Import monitors from a JSON file. Sample:"
title={t("export.form.importMonitors.title")}
subtitle={t("export.form.importMonitors.description")}
leftContent={
<Box
component={"pre"}
@@ -125,7 +128,7 @@ const ExportPage = () => {
rightContent={
<Stack gap={theme.spacing(4)}>
<Typography>
Click here to import your monitors from JSON
{t("export.form.importMonitors.optionImport")}
</Typography>
<Box>
<Button
@@ -134,7 +137,7 @@ const ExportPage = () => {
color="primary"
onClick={handleImport}
>
Import from JSON
{t("common.buttons.importJson")}
</Button>
<input
ref={fileInputRef}
@@ -148,11 +151,13 @@ const ExportPage = () => {
/>
<ConfigBox
title="Export incidents"
subtitle="Export current team's incidents as JSON"
title={t("export.form.exportIncidents.title")}
subtitle={t("export.form.exportIncidents.description")}
rightContent={
<Stack gap={theme.spacing(4)}>
<Typography>Click here to export your incidents to JSON</Typography>
<Typography>
{t("export.form.exportIncidents.optionExport")}
</Typography>
<Box>
<Button
loading={loading}
@@ -160,7 +165,7 @@ const ExportPage = () => {
color="primary"
onClick={() => exportJson("/incidents/export", "incidents")}
>
Export to JSON
{t("common.buttons.exportJson")}
</Button>
</Box>
</Stack>
+4 -2
View File
@@ -7,9 +7,11 @@ import type { ApiResponse } from "@/hooks/UseApi";
import { useGet, usePatch } from "@/hooks/UseApi";
import { z } from "zod";
import { profileSchema } from "@/validation/zod";
import { useTranslation } from "react-i18next";
type FormValues = z.infer<typeof profileSchema>;
const Profile = () => {
const { t } = useTranslation();
const [user, setUser] = useState<Partial<IUser> | null>(null);
const { response, loading } = useGet<ApiResponse<any>>(`/profile`, {}, {});
useEffect(() => {
@@ -32,8 +34,8 @@ const Profile = () => {
return (
<BasePage>
<InfoBox
title="View and edit your profile"
description="You can set your name and password here"
title={t("profile.infoBox.title")}
description={t("profile.infoBox.description")}
/>
<ProfileForm
user={user}
+9 -9
View File
@@ -71,9 +71,9 @@ export const ProfileForm = ({
<TextInput
value={user?.email || ""}
disabled
fieldLabel={t("auth.common.inputs.email.label")}
fieldLabel={t("auth.common.form.optionEmail")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
placeholder={t("auth.common.form.optionEmailPlaceholder")}
/>
<Controller
name="firstName"
@@ -82,9 +82,9 @@ export const ProfileForm = ({
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.firstName.label")}
fieldLabel={t("auth.common.form.optionFirstName")}
fullWidth
placeholder={t("auth.common.inputs.firstName.placeholder")}
placeholder={t("auth.common.form.optionFirstNamePlaceholder")}
error={!!errors.firstName}
helperText={errors.firstName ? errors.firstName.message : ""}
/>
@@ -97,9 +97,9 @@ export const ProfileForm = ({
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.lastName.label")}
fieldLabel={t("auth.common.form.optionLastName")}
fullWidth
placeholder={t("auth.common.inputs.lastName.placeholder")}
placeholder={t("auth.common.form.optionLastNamePlaceholder")}
error={!!errors.lastName}
helperText={errors.lastName ? errors.lastName.message : ""}
/>
@@ -113,7 +113,7 @@ export const ProfileForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.password.label")}
fieldLabel={t("auth.common.form.optionPassword")}
fullWidth
error={!!errors.password}
helperText={errors.password ? errors.password.message : ""}
@@ -128,7 +128,7 @@ export const ProfileForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.passwordConfirm.label")}
fieldLabel={t("auth.common.form.optionConfirmPassword")}
fullWidth
error={!!errors.confirmPassword}
helperText={
@@ -144,7 +144,7 @@ export const ProfileForm = ({
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Update profile
{t("common.buttons.updateProfile")}
</Button>
</Stack>
);
+4 -2
View File
@@ -1,5 +1,6 @@
import { AuthBasePage } from "@/components/auth";
import { useTranslation } from "react-i18next";
import { RecoveryForm } from "./RecoveryForm";
import { recoverySchema } from "@/validation/zod";
import { z } from "zod";
@@ -9,16 +10,17 @@ import { useToast } from "@/hooks/UseToast";
type FormData = z.infer<typeof recoverySchema>;
const Recovery = () => {
const { t } = useTranslation();
const { post, loading } = usePost<FormData>();
const { showToast } = useToast();
const onSubmit = async (data: FormData) => {
await post("/recovery", data);
showToast("If the email exists, a recovery link has been sent");
showToast(t("common.toasts.recoveryEmailSent"));
};
return (
<AuthBasePage title="Enter your email to recover your account">
<AuthBasePage title={t("auth.recovery.header.title")}>
<RecoveryForm onSubmit={onSubmit} loading={loading} />
</AuthBasePage>
);
+3 -3
View File
@@ -66,9 +66,9 @@ export const RecoveryForm = ({
render={({ field }) => (
<TextInput
{...field}
fieldLabel={t("auth.common.inputs.email.label")}
fieldLabel={t("auth.common.form.optionEmail")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
placeholder={t("auth.common.form.optionEmailPlaceholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
@@ -82,7 +82,7 @@ export const RecoveryForm = ({
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Request recovery email
{t("common.buttons.requestRecovery")}
</Button>
</Stack>
);
+4 -2
View File
@@ -1,5 +1,6 @@
import { AuthBasePage } from "@/components/auth";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router";
import { ResetForm } from "@/pages/recovery/ResetForm";
import { resetSchema } from "@/validation/zod";
@@ -18,6 +19,7 @@ import {
type FormData = z.infer<typeof resetSchema>;
const Reset = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { post, loading } = usePost<{ password: string; token: string }>();
@@ -37,7 +39,7 @@ const Reset = () => {
return;
}
toastSuccess("Password reset successful!");
toastSuccess(t("common.toasts.passwordResetSuccessful"));
dispatch(setAuthenticated(true));
dispatch(setUser(user));
dispatch(setSelectedTeamId(user.teams[0]?.id || null));
@@ -45,7 +47,7 @@ const Reset = () => {
};
return (
<AuthBasePage title="Set a new password">
<AuthBasePage title={t("auth.reset.header.title")}>
<ResetForm onSubmit={onSubmit} loading={loading} />
</AuthBasePage>
);
+4 -4
View File
@@ -69,7 +69,7 @@ export const ResetForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.password.label")}
fieldLabel={t("auth.common.form.optionPassword")}
fullWidth
placeholder="••••••••••"
error={!!errors.password}
@@ -85,9 +85,9 @@ export const ResetForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("auth.common.inputs.passwordConfirm.label")}
fieldLabel={t("auth.common.form.optionConfirmPassword")}
fullWidth
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
placeholder={t("auth.common.form.optionConfirmPasswordPlaceholder")}
error={!!errors.confirmPassword}
helperText={
errors.confirmPassword ? errors.confirmPassword.message : ""
@@ -102,7 +102,7 @@ export const ResetForm = ({
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Submit new password
{t("common.buttons.submitNewPassword")}
</Button>
</Stack>
);
+26 -18
View File
@@ -61,12 +61,12 @@ const SettingsPage = () => {
return (
<BasePage>
<ConfigBox
title="UI Settings"
subtitle="Configure time zone, UI mode, and language preferences"
title={t("settings.form.ui.title")}
subtitle={t("settings.form.ui.description")}
rightContent={
<Stack spacing={theme.spacing(10)}>
<Stack spacing={theme.spacing(2)}>
<Typography>Time zone</Typography>
<Typography>{t("settings.form.ui.optionTimezone")}</Typography>
<AutoComplete
value={selectedTimezone}
@@ -78,47 +78,55 @@ const SettingsPage = () => {
/>
</Stack>
<Stack spacing={theme.spacing(2)}>
<Typography>UI Mode</Typography>
<Typography>{t("settings.form.ui.optionUIMode")}</Typography>
<Select
value={mode}
onChange={(e: SelectChangeEvent<string>) => {
dispatch(setMode(e.target.value));
}}
>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
<MenuItem value="light">
{t("settings.form.ui.valueUIModeLight")}
</MenuItem>
<MenuItem value="dark">
{t("settings.form.ui.valueUIModeDark")}
</MenuItem>
</Select>
</Stack>
<Stack spacing={theme.spacing(2)}>
<Typography>Language</Typography>
<Typography>{t("settings.form.ui.optionLanguage")}</Typography>
<LanguageSelector />
</Stack>
</Stack>
}
/>
<ConfigBox
title="Chart settings"
subtitle="Choose what type of Uptime chart you want to see on the dashboard and status page"
title={t("settings.form.chart.title")}
subtitle={t("settings.form.chart.description")}
leftContent={<DummyChart type={chartType} />}
rightContent={
<Stack gap={theme.spacing(4)}>
<Typography>Chart Type</Typography>
<Typography>{t("settings.form.chart.optionChartType")}</Typography>
<Stack gap={theme.spacing(4)}>
<RadioWithDescription
checked={chartType === "heatmap"}
onChange={() => dispatch(setChartType("heatmap"))}
value="heatmap"
label="Heatmap"
description="Compact tiles; color shows response speed, top strip shows availability."
label={t("settings.form.chart.valueChartTypeHeatmap")}
description={t(
"settings.form.chart.descriptionChartTypeHeatmap"
)}
name="chart-type"
/>
<RadioWithDescription
checked={chartType === "histogram"}
onChange={() => dispatch(setChartType("histogram"))}
value="histogram"
label="Histogram"
description="One bar per check; height shows response time, color shows status."
label={t("settings.form.chart.valueChartTypeHistogram")}
description={t(
"settings.form.chart.descriptionChartTypeHistogram"
)}
name="chart-type"
/>
</Stack>
@@ -127,17 +135,17 @@ const SettingsPage = () => {
/>
{hasDelete && (
<ConfigBox
title="Monitors"
subtitle="Monitor related settings"
title={t("settings.form.monitors.title")}
subtitle={t("settings.form.monitors.description")}
rightContent={
<Stack gap={theme.spacing(4)}>
<Box>
<Button variant="contained" color="error" onClick={handleOpen}>
Remove all monitors
{t("common.buttons.removeAllMonitors")}
</Button>
</Box>
<Typography>
Removes all monitors from your organization.
{t("settings.form.monitors.descriptionRemoveAllMonitors")}
</Typography>
</Stack>
}
+23 -21
View File
@@ -57,13 +57,13 @@ export const SettingsForm = ({
onSubmit={handleSubmit(onSubmitEmail)}
>
<ConfigBox
title={t("settingsPage.emailSettings.title")}
subtitle={t("settingsPage.emailSettings.description")}
title={t("adminSettings.form.email.title")}
subtitle={t("adminSettings.form.email.description")}
leftContent={
<Stack>
<TextLink
text={t("settingsPage.emailSettings.descriptionTransport")}
linkText={t("settingsPage.emailSettings.linkTransport")}
text={t("adminSettings.form.email.descriptionTransport")}
linkText={t("adminSettings.form.email.linkTransport")}
href="https://nodemailer.com/smtp"
target="_blank"
/>
@@ -114,7 +114,7 @@ export const SettingsForm = ({
<TextInput
{...field}
type="text"
fieldLabel={t("settingsPage.emailSettings.labelHost")}
fieldLabel={t("adminSettings.form.email.optionHost")}
fullWidth
error={!!errors.systemEmailHost}
helperText={
@@ -132,7 +132,7 @@ export const SettingsForm = ({
<TextInput
{...field}
type="number"
fieldLabel={t("settingsPage.emailSettings.labelPort")}
fieldLabel={t("adminSettings.form.email.optionPort")}
fullWidth
error={!!errors.systemEmailPort}
helperText={
@@ -150,7 +150,7 @@ export const SettingsForm = ({
<TextInput
{...field}
type="text"
fieldLabel={t("settingsPage.emailSettings.labelUser")}
fieldLabel={t("adminSettings.form.email.optionUser")}
fullWidth
error={!!errors.systemEmailUser}
helperText={
@@ -168,7 +168,7 @@ export const SettingsForm = ({
<TextInput
{...field}
type="text"
fieldLabel={t("settingsPage.emailSettings.labelAddress")}
fieldLabel={t("adminSettings.form.email.optionAddress")}
fullWidth
error={!!errors.systemEmailAddress}
helperText={
@@ -186,7 +186,7 @@ export const SettingsForm = ({
<TextInput
{...field}
type="password"
fieldLabel={t("settingsPage.emailSettings.labelPassword")}
fieldLabel={t("adminSettings.form.email.optionPassword")}
fullWidth
error={!!errors.systemEmailPassword}
helperText={
@@ -205,7 +205,7 @@ export const SettingsForm = ({
{...field}
type="text"
fieldLabel={t(
"settingsPage.emailSettings.labelTLSServername"
"adminSettings.form.email.optionTLSServername"
)}
fullWidth
error={!!errors.systemEmailTLSServername}
@@ -225,7 +225,7 @@ export const SettingsForm = ({
{...field}
type="text"
fieldLabel={t(
"settingsPage.emailSettings.labelConnectionHost"
"adminSettings.form.email.optionConnectionHost"
)}
fullWidth
error={!!errors.systemEmailConnectionHost}
@@ -244,7 +244,7 @@ export const SettingsForm = ({
<Stack direction="row" alignItems={"center"}>
<Switch {...field} checked={field.value} />
<Typography>
{t("settingsPage.emailSettings.labelSecure")}
{t("adminSettings.form.email.optionSecure")}
</Typography>
</Stack>
)}
@@ -257,7 +257,7 @@ export const SettingsForm = ({
<Switch {...field} checked={field.value} />
<Typography>
{t("settingsPage.emailSettings.labelPool")}
{t("adminSettings.form.email.optionPool")}
</Typography>
</Stack>
)}
@@ -270,7 +270,7 @@ export const SettingsForm = ({
<Switch {...field} checked={field.value} />
<Typography>
{t("settingsPage.emailSettings.labelIgnoreTLS")}
{t("adminSettings.form.email.optionIgnoreTLS")}
</Typography>
</Stack>
)}
@@ -283,7 +283,7 @@ export const SettingsForm = ({
<Switch {...field} checked={field.value} />
<Typography>
{t("settingsPage.emailSettings.labelRequireTLS")}
{t("adminSettings.form.email.optionRequireTLS")}
</Typography>
</Stack>
)}
@@ -296,7 +296,7 @@ export const SettingsForm = ({
<Switch {...field} checked={field.value} />
<Typography>
{t("settingsPage.emailSettings.labelRejectUnauthorized")}
{t("adminSettings.form.email.optionRejectUnauthorized")}
</Typography>
</Stack>
)}
@@ -314,7 +314,7 @@ export const SettingsForm = ({
onTest(formValues);
}}
>
Test transport config
{t("common.buttons.testTransport")}
</Button>
<Button
loading={loading}
@@ -322,7 +322,7 @@ export const SettingsForm = ({
variant="contained"
color="primary"
>
Save e-mail settings
{t("common.buttons.saveEmailSettings")}
</Button>
</Stack>
</Stack>
@@ -335,8 +335,8 @@ export const SettingsForm = ({
spacing={theme.spacing(10)}
>
<ConfigBox
title="Global Data retention"
subtitle="Set data retention policy. This affects all organizations and teams"
title={t("adminSettings.form.retention.title")}
subtitle={t("adminSettings.form.retention.description")}
rightContent={
<Stack spacing={theme.spacing(10)}>
<Controller
@@ -346,7 +346,9 @@ export const SettingsForm = ({
<TextInput
{...field}
type="number"
fieldLabel="Retention days"
fieldLabel={t(
"adminSettings.form.retention.optionRetentionDays"
)}
fullWidth
error={!!errors.checksRetentionDays}
helperText={
+12 -6
View File
@@ -5,6 +5,7 @@ import { Table } from "@/components/design-elements";
import { HeaderCreate } from "@/components/common";
import type { Header } from "@/components/design-elements/Table";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { useTheme } from "@mui/material/styles";
import { useParams } from "react-router";
@@ -13,6 +14,7 @@ import type { ApiResponse } from "@/hooks/UseApi";
import type { ITeamMember } from "@/types/team-member";
const TeamDetailsPage = () => {
const { t } = useTranslation();
const theme = useTheme();
const navigate = useNavigate();
const { id } = useParams();
@@ -29,7 +31,7 @@ const TeamDetailsPage = () => {
const headers: Header<ITeamMember>[] = [
{
id: "name",
content: "Name",
content: t("common.table.headers.name"),
render: (row) => {
return row?.userId?.firstName + " " + row?.userId?.lastName;
},
@@ -37,14 +39,14 @@ const TeamDetailsPage = () => {
{
id: "role",
content: "Role",
content: t("teams.details.table.headers.role"),
render: (row) => {
return row?.roleId?.name;
},
},
{
id: "permissions",
content: "Permissions",
content: t("teams.details.table.headers.permissions"),
render: (row) => {
return (
<Stack>
@@ -65,11 +67,15 @@ const TeamDetailsPage = () => {
<BasePage>
<Stack>
<Stack alignItems="baseline" direction="row" gap={theme.spacing(2)}>
<Typography variant="h2">{"Name:"} </Typography>
<Typography variant="h2">
{`${t("teams.details.infoHeader.name")}:`}{" "}
</Typography>
<Typography>{team?.name}</Typography>
</Stack>
<Stack alignItems="baseline" direction="row" gap={theme.spacing(2)}>
<Typography variant="h2">{"Description:"} </Typography>
<Typography variant="h2">
{`${t("teams.details.infoHeader.description")}:`}{" "}
</Typography>
<Typography>{team?.description}</Typography>
</Stack>
</Stack>
@@ -81,7 +87,7 @@ const TeamDetailsPage = () => {
entitlementCount={teamMembers?.length || 0}
/>
<Typography variant="h2" marginTop={theme.spacing(4)}>
Team Members
{t("teams.details.table.title")}
</Typography>
<Table
headers={headers}
+8 -4
View File
@@ -67,15 +67,19 @@ const TeamMemberConfig = () => {
onClick={() => setDialogOpen(true)}
disabled={isDeleting}
>
{t("delete")}
{t("common.buttons.delete")}
</Button>
}
breadcrumbOverride={["teams", id || "", "configure team member"]}
breadcrumbOverride={[
t("teamMember.breadcrumbOverrideConfigTeams"),
id || "",
t("teamMember.breadcrumbOverrideConfigTeamMember"),
]}
/>
<Dialog
open={dialogOpen}
title={"Are you sure?"}
content={"Delete team member"}
title={t("common.dialog.delete.title")}
content={t("common.dialog.delete.description")}
onConfirm={handleConfirm}
onCancel={handleCancel}
loading={isDeleting}
+7 -1
View File
@@ -4,8 +4,10 @@ import { useParams } from "react-router";
import { useGet, usePost } from "@/hooks/UseApi";
import type { ApiResponse } from "@/hooks/UseApi";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
import { mutate } from "swr";
const TeamMemberCreate = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { id } = useParams();
const { response } = useGet<ApiResponse<any>>("/team-members?type=org");
@@ -34,7 +36,11 @@ const TeamMemberCreate = () => {
roles={roles}
onSubmit={onSubmit}
loading={false}
breadcrumbOverride={["teams", id || "", "create team member"]}
breadcrumbOverride={[
t("teamMember.breadcrumbOverrideConfigTeams"),
id || "",
t("teamMember.breadcrumbOverrideCreateTeamMember"),
]}
/>
);
};
+10 -8
View File
@@ -61,8 +61,8 @@ export const TeamMemberForm = ({
breadcrumbOverride={breadcrumbOverride}
>
<ConfigBox
title="Team member"
subtitle="Team member selection"
title={t("teamMember.form.teamMember.title")}
subtitle={t("teamMember.form.teamMember.description")}
rightContent={
<Controller
name="userId"
@@ -73,7 +73,9 @@ export const TeamMemberForm = ({
<Select
disabled={mode !== "create"}
value={field.value}
fieldLabel="Team member"
fieldLabel={t(
"teamMember.form.teamMember.optionTeamMember"
)}
error={!!errors.userId}
onChange={field.onChange}
>
@@ -92,8 +94,8 @@ export const TeamMemberForm = ({
}
/>
<ConfigBox
title="Role"
subtitle="Choose your role for this new team. You will be the first member"
title={t("teamMember.form.role.title")}
subtitle={t("teamMember.form.role.description")}
rightContent={
<Controller
name="roleId"
@@ -106,7 +108,7 @@ export const TeamMemberForm = ({
<Stack gap={theme.spacing(8)}>
<Select
value={field.value}
fieldLabel="Role"
fieldLabel={t("teamMember.form.role.optionRole")}
error={!!errors.roleId}
onChange={field.onChange}
>
@@ -120,7 +122,7 @@ export const TeamMemberForm = ({
</Select>
<Stack>
<Typography variant={"h2"} mb={theme.spacing(4)}>
Included permissions:
{`${t("teamMember.form.role.optionIncludedPermissions")}:`}
</Typography>
{selectedRole?.permissions?.map((perm: string) => (
<Typography key={perm}>- {perm}</Typography>
@@ -139,7 +141,7 @@ export const TeamMemberForm = ({
variant="contained"
color="primary"
>
{t("settingsSave")}
{t("common.buttons.save")}
</Button>
{deleteButton}
</Stack>
+16 -9
View File
@@ -3,6 +3,7 @@ import { Table } from "@/components/design-elements";
import { HeaderCreate } from "@/components/common";
import { ActionsMenu } from "@/components/actions-menu";
import { Dialog } from "@/components/inputs";
import Typography from "@mui/material/Typography";
import { useNavigate } from "react-router";
import { useTranslation } from "react-i18next";
@@ -13,8 +14,10 @@ import { useGet, useDelete } from "@/hooks/UseApi";
import type { ITeam } from "@/types/team";
import { useState } from "react";
import { mutate } from "swr";
import { useTheme } from "@mui/material/styles";
const TeamsPage = () => {
const theme = useTheme();
const navigate = useNavigate();
const [selectedTeam, setSelectedTeam] = useState<ITeam | null>(null);
const isDialogOpen = Boolean(selectedTeam);
@@ -37,7 +40,7 @@ const TeamsPage = () => {
return [
{
id: 1,
label: "Configure",
label: t("monitors.common.actions.configure"),
action: () => {
navigate(`/teams/${team._id}/configure`);
},
@@ -45,7 +48,11 @@ const TeamsPage = () => {
},
{
id: 2,
label: "Delete",
label: (
<Typography color={theme.palette.error.main}>
{t("common.buttons.delete")}
</Typography>
),
action: () => {
setSelectedTeam(team);
},
@@ -58,21 +65,21 @@ const TeamsPage = () => {
const headers: Header<ITeam>[] = [
{
id: "name",
content: "Team name",
content: t("common.table.headers.name"),
render: (row) => {
return row.name;
},
},
{
id: "description",
content: "Team description",
content: t("teams.table.headers.teamDescription"),
render: (row) => {
return row.description;
},
},
{
id: "actions",
content: "Team description",
content: t("common.table.headers.actions"),
render: (row) => {
return <ActionsMenu items={getActions(row)} />;
},
@@ -86,8 +93,8 @@ const TeamsPage = () => {
return (
<BasePage>
<InfoBox
title="Team Collaboration"
description="Organize your monitoring workspace by creating teams. Control who can view and manage monitors, set up role-based permissions, and collaborate effectively."
title={t("teams.infoBox.title")}
description={t("teams.infoBox.description")}
/>
<HeaderCreate
label="Create new team"
@@ -105,8 +112,8 @@ const TeamsPage = () => {
/>
<Dialog
open={isDialogOpen}
title={t("deleteDialogTitle")}
content={t("deleteDialogDescription")}
title={t("common.dialog.delete.title")}
content={t("common.dialog.delete.description")}
onConfirm={handleConfirm}
onCancel={handleCancel}
loading={isDeleting}