From f2363a7a09ba66e175ba906c0324047b981775f7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 2 Feb 2026 23:01:35 +0000 Subject: [PATCH 01/24] form initial commit --- .../src/Components/v2/inputs/ImageUpload.tsx | 164 ++++++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Pages/StatusPage/Create/index.tsx | 46 +++++ .../StatusPage/Create/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 10 +- client/src/locales/en.json | 46 +++++ 6 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 client/src/Components/v2/inputs/ImageUpload.tsx create mode 100644 client/src/Pages/StatusPage/Create/index.tsx rename client/src/Pages/StatusPage/Create/{index.jsx => old.jsx} (100%) diff --git a/client/src/Components/v2/inputs/ImageUpload.tsx b/client/src/Components/v2/inputs/ImageUpload.tsx new file mode 100644 index 000000000..6c9fc1783 --- /dev/null +++ b/client/src/Components/v2/inputs/ImageUpload.tsx @@ -0,0 +1,164 @@ +import { Box, Stack, Typography, IconButton } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { Upload, X } from "lucide-react"; +import { useState, useRef, useCallback } from "react"; +import { useTranslation } from "react-i18next"; + +interface FileObject { + src: string; + name: string; + file: File; +} + +interface ImageUploadProps { + src?: string; + onChange?: (file: FileObject | undefined) => void; + maxSize?: number; + accept?: string[]; + error?: string; +} + +export const ImageUpload = ({ + src, + onChange, + maxSize = 3 * 1024 * 1024, + accept = ["jpg", "jpeg", "png"], + error, +}: ImageUploadProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const inputRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + const [localError, setLocalError] = useState(null); + const [preview, setPreview] = useState(null); + + const handleFile = useCallback( + (file: File | undefined) => { + if (!file) return; + const isValidType = accept.some((type) => file.type.includes(type)); + const isValidSize = file.size <= maxSize; + if (!isValidType) { + setLocalError(t("common.errors.invalidFileFormat")); + return; + } + if (!isValidSize) { + setLocalError(t("common.errors.invalidFileSize")); + return; + } + setLocalError(null); + const fileObj: FileObject = { + src: URL.createObjectURL(file), + name: file.name, + file, + }; + setPreview(fileObj); + onChange?.(fileObj); + }, + [maxSize, accept, onChange, t] + ); + + const handleClear = () => { + setPreview(null); + setLocalError(null); + onChange?.(undefined); + if (inputRef.current) inputRef.current.value = ""; + }; + + const displaySrc = src || preview?.src; + const displayError = localError || error; + + return ( + + {displaySrc ? ( + + + + + + + ) : ( + { + e.preventDefault(); + setIsDragging(true); + }} + onDragLeave={() => setIsDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setIsDragging(false); + handleFile(e.dataTransfer.files?.[0]); + }} + alignItems="center" + justifyContent="center" + gap={1} + sx={{ + position: "relative", + width: "100%", + minHeight: 150, + border: "2px dashed", + borderRadius: 1, + borderColor: isDragging ? theme.palette.primary.main : theme.palette.divider, + backgroundColor: isDragging ? theme.palette.action.hover : "transparent", + transition: "0.2s", + cursor: "pointer", + "&:hover": { + borderColor: theme.palette.primary.main, + backgroundColor: theme.palette.action.hover, + }, + }} + > + `.${ext}`).join(",")} + onChange={(e) => handleFile(e.target.files?.[0])} + style={{ position: "absolute", inset: 0, opacity: 0, cursor: "pointer" }} + /> + + + + {t("common.imageUpload.clickToUpload")} + {" "} + {t("common.imageUpload.orDragAndDrop")} + + + {accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "} + {Math.round(maxSize / 1024 / 1024)}MB + + + )} + {displayError && ( + + {displayError} + + )} + + ); +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 1618daf76..ad7c984bd 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -12,3 +12,4 @@ export { DialogInput as Dialog } from "./Dialog"; export * from "./Radio"; export * from "./Switch"; export * from "./Slider"; +export * from "./ImageUpload"; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx new file mode 100644 index 000000000..25c4d03f8 --- /dev/null +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -0,0 +1,46 @@ +import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { ImageUpload, SwitchComponent } from "@/Components/v2/inputs"; + +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; + +const CreateStatusPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + field.onChange(e.target.checked)} + /> + + {t("pages.statusPages.form.access.option.published.name")} + + + } + /> + + + + } + /> + + ); +}; + +export default CreateStatusPage; diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/old.jsx similarity index 100% rename from client/src/Pages/StatusPage/Create/index.jsx rename to client/src/Pages/StatusPage/Create/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index bbba8eac0..0b24ac03e 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -36,7 +36,7 @@ import Checks from "../Pages/Checks/index"; import Incidents from "../Pages/Incidents/"; // Status pages -import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; +import CreateStatus from "../Pages/StatusPage/Create/"; import StatusPages from "../Pages/StatusPage/StatusPages"; import Status from "../Pages/StatusPage/Status"; @@ -249,7 +249,13 @@ const Routes = () => { } + element={ + <> + + + + + } /> Date: Mon, 2 Feb 2026 23:46:15 +0000 Subject: [PATCH 02/24] monitors --- client/src/Hooks/useStatusPageForm.ts | 27 +++ client/src/Pages/StatusPage/Create/index.tsx | 239 ++++++++++++++++++- client/src/Validation/statusPage.ts | 33 +++ client/src/locales/en.json | 35 +++ 4 files changed, 328 insertions(+), 6 deletions(-) create mode 100644 client/src/Hooks/useStatusPageForm.ts create mode 100644 client/src/Validation/statusPage.ts diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts new file mode 100644 index 000000000..0ea128e0d --- /dev/null +++ b/client/src/Hooks/useStatusPageForm.ts @@ -0,0 +1,27 @@ +import { useMemo } from "react"; +import { statusPageSchema, type StatusPageFormData } from "@/Validation/statusPage"; +import type { StatusPage } from "@/Types/StatusPage"; + +interface UseStatusPageFormOptions { + data?: StatusPage | null; +} + +export const useStatusPageForm = ({ data = null }: UseStatusPageFormOptions = {}) => { + return useMemo(() => { + const defaults: StatusPageFormData = { + companyName: data?.companyName || "", + url: data?.url || "", + timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + color: data?.color || "#4169E1", + monitors: data?.monitors || [], + isPublished: data?.isPublished ?? false, + showCharts: data?.showCharts ?? true, + showUptimePercentage: data?.showUptimePercentage ?? true, + showAdminLoginLink: data?.showAdminLoginLink ?? false, + customCSS: data?.customCSS || "", + logo: data?.logo || null, + }; + + return { schema: statusPageSchema, defaults }; + }, [data]); +}; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 25c4d03f8..7e4e94a9d 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -1,16 +1,68 @@ import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { ImageUpload, SwitchComponent } from "@/Components/v2/inputs"; +import IconButton from "@mui/material/IconButton"; +import Divider from "@mui/material/Divider"; +import { Trash2 } from "lucide-react"; +import { + ImageUpload, + SwitchComponent, + Button, + TextField, + Autocomplete, +} from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; +import { useEffect } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; +import type { StatusPageFormData } from "@/Validation/statusPage"; +import { useGet } from "@/Hooks/UseApi"; +import type { Monitor } from "@/Types/Monitor"; +import timezones from "@/Utils/timezones.json"; + +interface TimezoneOption { + _id: string; + name: string; +} const CreateStatusPage = () => { const theme = useTheme(); const { t } = useTranslation(); + + const { schema, defaults } = useStatusPageForm(); + + const { data: monitorsResponse } = useGet( + "/monitors/team?type=http&type=ping&type=port&type=docker" + ); + const monitors = monitorsResponse ?? []; + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: defaults, + }); + + const { control, reset, handleSubmit } = form; + + useEffect(() => { + reset(defaults); + }, [defaults, reset]); + + const onError = (errors: any) => { + console.log(errors); + }; + + const onSubmit = (data: StatusPageFormData) => { + console.log("Form Data Submitted: ", data); + }; + return ( - + { alignItems="center" spacing={theme.spacing(2)} > - field.onChange(e.target.checked)} + ( + field.onChange(e.target.checked)} + /> + )} /> {t("pages.statusPages.form.access.option.published.name")} @@ -30,15 +88,184 @@ const CreateStatusPage = () => { } /> + + ( + + )} + /> + ( + + )} + /> + + } + /> + { + const selectedMonitors = monitors.filter((m) => field.value.includes(m.id)); + return ( + + option.name} + value={selectedMonitors} + onChange={(_, newValue: Monitor[]) => { + field.onChange(newValue.map((m) => m.id)); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + fieldLabel={t( + "pages.statusPages.form.monitors.option.monitors.label" + )} + renderInput={(params) => ( + + )} + /> + {selectedMonitors.length > 0 && ( + + {selectedMonitors.map((monitor, index) => ( + + {monitor.name} + { + field.onChange( + field.value.filter((id: string) => id !== monitor.id) + ); + }} + aria-label="Remove monitor" + > + + + {index < selectedMonitors.length - 1 && } + + ))} + + )} + + ); + }} + /> + } + /> + ( + option.name} + value={ + timezones.find((tz: TimezoneOption) => tz._id === field.value) ?? null + } + onChange={(_, newValue: TimezoneOption | null) => { + field.onChange(newValue?._id ?? ""); + }} + fieldLabel={t("pages.statusPages.form.timezone.option.timezone.label")} + renderInput={(params) => ( + + )} + /> + )} + /> + } + /> - + ( + { + if (file) { + field.onChange({ + data: file.src, + contentType: file.file.type, + }); + } else { + field.onChange(null); + } + }} + /> + )} + /> } /> + + + ); }; diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts new file mode 100644 index 000000000..cb148e633 --- /dev/null +++ b/client/src/Validation/statusPage.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; + +export const statusPageSchema = z.object({ + companyName: z + .string() + .min(1, "Company name is required") + .max(100, "Company name must be at most 100 characters"), + url: z + .string() + .min(1, "URL is required") + .max(50, "URL must be at most 50 characters") + .regex( + /^[a-z0-9-]+$/, + "URL can only contain lowercase letters, numbers, and hyphens" + ), + timezone: z.string().optional(), + color: z.string().min(1, "Color is required"), + monitors: z.array(z.string()), + isPublished: z.boolean(), + showCharts: z.boolean(), + showUptimePercentage: z.boolean(), + showAdminLoginLink: z.boolean(), + customCSS: z.string().optional(), + logo: z + .object({ + data: z.string(), + contentType: z.string(), + }) + .nullable() + .optional(), +}); + +export type StatusPageFormData = z.infer; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 2d1819696..2d3eb614c 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -508,6 +508,41 @@ "title": "Appearance", "description": "Define the default look and feel of your public status page", "option": {} + }, + "basicInfo": { + "title": "Basic information", + "description": "Name and address of your status page", + "option": { + "name": { + "label": "Name", + "placeholder": "My status page" + }, + "url": { + "label": "URL", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Monitors", + "description": "Select which monitors to display on your status page", + "noMonitors": "No monitors available. Create uptime monitors first.", + "option": { + "monitors": { + "label": "Monitors", + "placeholder": "Select monitors..." + } + } + }, + "timezone": { + "title": "Timezone", + "description": "Select the timezone for your status page", + "option": { + "timezone": { + "label": "Timezone", + "placeholder": "Search timezones..." + } + } } }, "settings": { From ae7524f507e2b7199a40c676f3e7891fa20056eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 2 Feb 2026 23:57:14 +0000 Subject: [PATCH 03/24] monitors details --- client/src/Hooks/useStatusPageForm.ts | 4 +- client/src/Pages/StatusPage/Create/index.tsx | 101 +++++++++++++------ client/src/Validation/statusPage.ts | 2 +- 3 files changed, 76 insertions(+), 31 deletions(-) diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts index 0ea128e0d..69401138e 100644 --- a/client/src/Hooks/useStatusPageForm.ts +++ b/client/src/Hooks/useStatusPageForm.ts @@ -6,11 +6,13 @@ interface UseStatusPageFormOptions { data?: StatusPage | null; } +const generateDefaultUrl = () => Math.floor(Math.random() * 1000000).toString(); + export const useStatusPageForm = ({ data = null }: UseStatusPageFormOptions = {}) => { return useMemo(() => { const defaults: StatusPageFormData = { companyName: data?.companyName || "", - url: data?.url || "", + url: data?.url || generateDefaultUrl(), timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, color: data?.color || "#4169E1", monitors: data?.monitors || [], diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 7e4e94a9d..ec5245e41 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -2,8 +2,9 @@ import { BasePage, ConfigBox } from "@/Components/v2/design-elements"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import IconButton from "@mui/material/IconButton"; -import Divider from "@mui/material/Divider"; -import { Trash2 } from "lucide-react"; +import { Trash2, GripVertical } from "lucide-react"; +import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; +import type { DropResult } from "@hello-pangea/dnd"; import { ImageUpload, SwitchComponent, @@ -135,8 +136,11 @@ const CreateStatusPage = () => { { - const selectedMonitors = monitors.filter((m) => field.value.includes(m.id)); + render={({ field, fieldState }) => { + // Preserve order from field.value (IDs) when building selectedMonitors + const selectedMonitors = field.value + .map((id: string) => monitors.find((m) => m.id === id)) + .filter((m): m is Monitor => m !== undefined); return ( { placeholder={t( "pages.statusPages.form.monitors.option.monitors.placeholder" )} + error={!!fieldState.error} + helperText={fieldState.error?.message ?? ""} /> )} /> {selectedMonitors.length > 0 && ( - { + if (!result.destination) return; + const reordered = Array.from(field.value); + const [removed] = reordered.splice(result.source.index, 1); + reordered.splice(result.destination.index, 0, removed); + field.onChange(reordered); + }} > - {selectedMonitors.map((monitor, index) => ( - - {monitor.name} - { - field.onChange( - field.value.filter((id: string) => id !== monitor.id) - ); - }} - aria-label="Remove monitor" + + {(provided) => ( + - - - {index < selectedMonitors.length - 1 && } - - ))} - + {selectedMonitors.map((monitor, index) => ( + + {(provided) => ( + + + {monitor.name} + { + field.onChange( + field.value.filter( + (id: string) => id !== monitor.id + ) + ); + }} + aria-label="Remove monitor" + > + + + + )} + + ))} + {provided.placeholder} + + )} + + )} ); diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts index cb148e633..fbe6a0690 100644 --- a/client/src/Validation/statusPage.ts +++ b/client/src/Validation/statusPage.ts @@ -15,7 +15,7 @@ export const statusPageSchema = z.object({ ), timezone: z.string().optional(), color: z.string().min(1, "Color is required"), - monitors: z.array(z.string()), + monitors: z.array(z.string()).min(1, "At least one monitor is required"), isPublished: z.boolean(), showCharts: z.boolean(), showUptimePercentage: z.boolean(), From 3566da75150b0c8a04f7ebd9fafac76a5da87089 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 18:02:24 +0000 Subject: [PATCH 04/24] uptime strings --- client/src/locales/en.json | 320 ++++++++----------------------------- 1 file changed, 64 insertions(+), 256 deletions(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c6ad859f0..f6aac1cbc 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -231,26 +231,6 @@ "details": "Details", "home": "Home" }, - "imageUpload": { - "clickToUpload": "Click to upload", - "orDragAndDrop": "or drag and drop", - "maxSize": "Max size", - "supportedFormats": "Supported formats" - }, - "errors": { - "invalidFileFormat": "Invalid file format", - "invalidFileSize": "File size exceeds maximum allowed" - }, - "imageUpload": { - "clickToUpload": "Click to upload", - "orDragAndDrop": "or drag and drop", - "maxSize": "Max size", - "supportedFormats": "Supported formats" - }, - "errors": { - "invalidFileFormat": "Invalid file format", - "invalidFileSize": "File size exceeds maximum allowed" - }, "buttons": { "cancel": "Cancel", "configure": "Configure", @@ -280,7 +260,17 @@ "title": "Are you sure you want to delete this?" } }, + "errors": { + "invalidFileFormat": "Invalid file format", + "invalidFileSize": "File size exceeds maximum allowed" + }, "forDocumentation": "for query language documentation.", + "imageUpload": { + "clickToUpload": "Click to upload", + "maxSize": "Max size", + "orDragAndDrop": "or drag and drop", + "supportedFormats": "Supported formats" + }, "labels": { "active": "Active", "na": "N/A", @@ -1278,38 +1268,22 @@ ], "title": "A status page is used to:" }, - "monitorsList": { - "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram" - }, - "serviceStauts": "Service stauts", - "statusBar": { - "allDown": "All systems down", - "allUp": "All systems operational", - "degraded": "Degraded performance", - "unknown": "Unknown status" - }, - "monitorsList": { - "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram" - }, "form": { "access": { - "title": "Access", "description": "Choose if you want your status page to be public", "option": { "published": { "name": "Published and publicly visible" } - } + }, + "title": "Access" }, "appearance": { - "title": "Appearance", "description": "Define the default look and feel of your public status page", - "option": {} + "option": {}, + "title": "Appearance" }, "basicInfo": { - "title": "Basic information", "description": "Name and address of your status page", "option": { "name": { @@ -1320,10 +1294,10 @@ "label": "URL", "placeholder": "my-status-page" } - } + }, + "title": "Basic information" }, "monitors": { - "title": "Monitors", "description": "Select which monitors to display on your status page", "noMonitors": "No monitors available. Create uptime monitors first.", "option": { @@ -1331,209 +1305,62 @@ "label": "Monitors", "placeholder": "Select monitors..." } - } + }, + "title": "Monitors" }, "timezone": { - "title": "Timezone", "description": "Select the timezone for your status page", "option": { "timezone": { "label": "Timezone", "placeholder": "Search timezones..." } - } + }, + "title": "Timezone" } }, + "monitorsList": { + "chartTypeHeatmap": "Heatmap", + "chartTypeHistogram": "Histogram" + }, + "serviceStauts": "Service stauts", "settings": { - "title": "Status page settings", "description": "Configure basic settings for your status page.", "option": { "name": { "label": "Status page name", "placeholder": "e.g. My Company Status Page" }, + "published": { + "label": "Published and visible to the public" + }, "subdomain": { "label": "Subdomain", "placeholder": "e.g. my-company" }, "timezone": { "label": "Timezone" - }, - "published": { - "label": "Published and visible to the public" } - } - } - } - }, - "used": "Used", - "total": "Total", - "status": "Status", - "uptime": "Uptime", - "actions": "Actions", - "integrations": "Integrations", - "integrationsPrism": "Connect Prism to your favorite service.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", - "commonSave": "Save", - "pause": "Pause", - "resume": "Resume", - "access": "Access", - "timezone": "Timezone", - "features": "Features", - "administrator": "Administrator?", - "loginHere": "Login here", - "portToMonitor": "Port to monitor", - "createMaintenanceWindow": "Create maintenance window", - "createMaintenance": "Create maintenance", - "editMaintenance": "Edit maintenance", - "maintenanceWindowName": "Maintenance Window Name", - "friendlyNameInput": "Friendly name", - "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", - "maintenanceRepeat": "Maintenance Repeat", - "maintenance": "maintenance", - "duration": "Duration", - "addMonitors": "Add monitors", - "window": "window", - "cancel": "Cancel", - "close": "Close", - "type": "Type", - "repeat": "Repeat", - "edit": "Edit", - "createA": "Create a", - "remove": "Remove", - "maintenanceWindowDescription": "During maintenance windows, all monitoring is suspended for selected monitors. No network checks will be performed, preventing any status updates or notifications from being triggered. Your monitors will appear frozen at their last known status, and status pages will display a maintenance indicator. Once the maintenance window ends, monitoring automatically resumes, and alerts will trigger if issues are detected. Maintenance periods do not count against uptime calculations.", - "startTime": "Start time", - "timeZoneInfo": "All dates and times are in GMT+0 time zone.", - "monitorsToApply": "Monitors to apply maintenance window to", - "nextWindow": "Next window", - "notFoundButton": "Go to the main dashboard", - "companyName": "Company name", - "statusPageCreateSettings": "If your status page is ready, you can mark it as published.", - "basicInformation": "Basic Information", - "statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.", - "statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.", - "statusPageCreateSettingsCheckboxLabel": "Published and visible to the public", - "statusPageCreateBasicInfoStatusPageAddress": "Your status page address", - "statusPageCreateTabsContent": "Status page servers", - "statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.", - "statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page", - "showCharts": "Show charts", - "showUptimePercentage": "Show uptime percentage", - "removeLogo": "Remove Logo", - "statusPageStatusServiceStatus": "Service status", - "deleteStatusPage": "Do you want to delete this status page?", - "deleteStatusPageConfirm": "Yes, delete status page", - "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", - "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", - "inviteNoTokenFound": "No invite token found", - "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", - "pageSpeedLearnMoreLink": "Click here", - "pageSpeedAddApiKey": "to add your API key.", - "update": "Update", - "invalidFileFormat": "Unsupported file format!", - "invalidFileSize": "File size is too large!", - "ClickUpload": "Click to upload", - "DragandDrop": "drag and drop", - "MaxSize": "Maximum Size", - "SupportedFormats": "Supported formats", - "FirstName": "First name", - "LastName": "Last name", - "EmailDescriptionText": "This is your current email address — it cannot be changed.", - "YourPhoto": "Profile photo", - "PhotoDescriptionText": "This photo will be displayed in your profile page.", - "save": "Save", - "DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.", - "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", - "DeleteWarningTitle": "Really remove this account?", - "bulkImport": { - "title": "Bulk Import", - "selectFileTips": "Select CSV file to upload", - "selectFileDescription": "You can download our or sample", - "selectFile": "Select File", - "uploadSuccess": "Monitors created successfully!", - "noFileSelected": "No file selected", - "fallbackPage": "Import a file to upload a list of servers in bulk", - "invalidFileType": "Invalid file type", - "uploadFailed": "Upload failed" - }, - "DeleteAccountTitle": "Remove account", - "DeleteAccountButton": "Remove account", - "publicLink": "Public link", - "reset": "Reset", - "createNew": "Create new", - "greeting": { - "prepend": "Hey there", - "append": "The afternoon is your playground—let's make it epic!", - "overview": "Here's an overview of your {{type}} monitors." - }, - "roles": { - "superAdmin": "Super admin", - "admin": "Admin", - "teamMember": "Team member", - "demoUser": "Demo user" - }, - "teamPanel": { - "teamMembers": "Team members", - "filter": { - "all": "All", - "member": "Member" - }, - "inviteTeamMember": "Invite a team member", - "inviteNewTeamMember": "Invite new team member", - "inviteDescription": "When you add a new team member, they will get access to all monitors.", - "email": "Email", - "selectRole": "Select role", - "inviteLink": "Invite link", - "cancel": "Cancel", - "noMembers": "There are no team members with this role", - "getToken": "Get token", - "emailToken": "E-mail token", - "table": { - "headers": { - "name": "Status page name", - "url": "Public URL" + }, + "title": "Status page settings" }, - "name": "Name", - "email": "Email", - "role": "Role", - "created": "Created" - }, - "addTeamMember": { - "addMemberMenu": "Add Team Member", - "title": "Register new team member", - "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.", - "addButton": "Add Member" - }, - "register": "Register a team member", - "registerToast": { - "success": "User created, share credentials with the member securely." + "statusBar": { + "allDown": "All systems down", + "allUp": "All systems operational", + "degraded": "Degraded performance", + "unknown": "Unknown status" + } }, "uptime": { - "fallback": { - "actionButton": "Create a monitor!", - "checks": [ - "Check if websites or servers are online & responsive", - "Alert teams about downtime or performance issues", - "Monitor HTTP endpoints, pings, containers & ports", - "Track historical uptime and reliability trends" - ], - "title": "An uptime monitor is used to:" + "table": { + "headers": { + "responseTime": "Response time" + } }, "filters": { "search": { "placeholder": "Search monitors..." } - }, - "table": { - "headers": { - "responseTime": "Response time" - } } } }, @@ -1789,12 +1616,6 @@ "title": "Register new team member" }, "cancel": "Cancel", - "changeTeamPassword": { - "changePasswordMenu": "Reset Password", - "description": "Create a new password for this team member. You will need to share the password with them securely.", - "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way.", - "title": "Reset team member password" - }, "email": "Email", "emailToken": "E-mail token", "filter": { @@ -1808,44 +1629,9 @@ "inviteTeamMember": "Invite a team member", "noMembers": "There are no team members with this role", "register": "Register a team member", - "registerTeamMember": { - "auth": { - "common": { - "inputs": { - "email": { - "errors": { - "empty": "To continue, please enter an email address", - "invalid": "Please recheck validity of entered email address" - } - }, - "firstName": { - "errors": { - "empty": "Please enter a name", - "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" - } - }, - "lastName": { - "errors": { - "empty": "Please enter a surname", - "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" - } - }, - "role": { - "errors": { - "empty": "Role is required" - } - } - } - } - }, - "title": "Register team member" - }, "registerToast": { - "dbUserExists": "User already exists.", - "success": "User created, share credentials with the member securely.", - "unknownError": "Unknown error occurred." + "success": "User created, share credentials with the member securely." }, - "role": "Role", "selectRole": "Select role", "table": { "created": "Created", @@ -1857,7 +1643,29 @@ "name": "Name", "role": "Role" }, - "teamMembers": "Team members" + "teamMembers": "Team members", + "uptime": { + "fallback": { + "actionButton": "Create a monitor!", + "checks": [ + "Check if websites or servers are online & responsive", + "Alert teams about downtime or performance issues", + "Monitor HTTP endpoints, pings, containers & ports", + "Track historical uptime and reliability trends" + ], + "title": "An uptime monitor is used to:" + }, + "filters": { + "search": { + "placeholder": "Search monitors..." + } + }, + "table": { + "headers": { + "responseTime": "Response time" + } + } + } }, "testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button", "time": { From 7aa05c7f8550ec9ede6991f83495b2c1ef87fea3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 18:34:15 +0000 Subject: [PATCH 05/24] strings --- client/src/locales/en.json | 804 +++++++------------------------------ 1 file changed, 134 insertions(+), 670 deletions(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index f6aac1cbc..52f0dff1f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -4,9 +4,7 @@ "actions": "Actions", "add": "Add", "addMonitors": "Add monitors", - "administrator": "Administrator?", "advancedMatching": "Advanced matching", - "ago": "ago", "area": "Area", "auth": { "common": { @@ -14,11 +12,6 @@ "validation": "Error validating data." }, "fields": { - "password": { - "errors": { - "incorrect": "The password you provided does not match our records" - } - }, "role": { "errors": { "min": "At least one role is required" @@ -55,7 +48,6 @@ "password": { "errors": { "empty": "Please enter your password", - "incorrect": "The password you provided does not match our records", "length": "Password must be at least 8 characters long", "lowercase": "Password must contain at least 1 lowercase letter", "number": "Password must contain at least 1 number", @@ -100,7 +92,6 @@ } }, "navigation": { - "back": "Back", "continue": "Continue" } }, @@ -110,12 +101,6 @@ "resetPassword": "Reset password" }, "heading": "Forgot password?", - "imageAlts": { - "email": "Email icon", - "lock": "Lock icon", - "passwordConfirm": "Password confirm icon", - "passwordKey": "Password key icon" - }, "links": { "login": "Go back to Log In", "resend": "Didn't receive the email? Click to resend" @@ -147,10 +132,6 @@ "register": "Do not have an account?", "registerLink": "Register here" }, - "subheadings": { - "stepOne": "Enter your email", - "stepTwo": "Enter your password" - }, "toasts": { "incorrectPassword": "Incorrect password", "success": "Welcome back! You're successfully logged in." @@ -162,23 +143,9 @@ "superAdmin": "Create your super admin account to get started", "user": "Sign up as a user and ask super admin for access to your monitors" }, - "gettingStartedButton": { - "superAdmin": "Create super admin account", - "user": "Sign up regular user" - }, "heading": { - "superAdmin": "Create super admin", "user": "Sign Up" }, - "links": { - "login": "Already have an account? Log In" - }, - "subheadings": { - "stepOne": "Enter your personal details", - "stepThree": "Create your password", - "stepTwo": "Enter your email" - }, - "termsAndPolicies": "By creating an account, you agree to our Terms of Service and Privacy Policy.", "toasts": { "success": "Welcome! Your account was created successfully." }, @@ -192,30 +159,18 @@ "fallbackPage": "Import a file to upload a list of servers in bulk", "invalidFileType": "Invalid file type", "noFileSelected": "No file selected", - "parsingFailed": "Parsing failed", "selectFile": "Select File", "selectFileDescription": "You can download our or sample", "selectFileTips": "Select CSV file to upload", "title": "Bulk Import", "uploadFailed": "Upload failed", - "uploadSuccess": "Monitors created successfully!", - "validationFailed": "Validation failed" + "uploadSuccess": "Monitors created successfully!" }, - "bytesPerSecond": "Bytes per second", - "bytesReceived": "Bytes Received", "bytesSent": "Bytes Sent", "cancel": "Cancel", - "checkFormError": "Please check the form for errors.", - "checkFrequency": "Check frequency", "checkHooks": { - "failureResolveAll": "Failed to resolve all incidents.", - "failureResolveMonitor": "Failed to resolve monitor incidents.", "failureResolveOne": "Failed to resolve incident." }, - "checkingEvery": "Checking every", - "checksPageActionResolveAllMonitor": "Resolve all checks", - "checksPageActionResolveMonitor": "Resolve monitor checks", - "checksPageTitle": "Checks", "chooseGame": "Choose game", "city": "CITY", "ClickUpload": "Click to upload", @@ -260,30 +215,16 @@ "title": "Are you sure you want to delete this?" } }, - "errors": { - "invalidFileFormat": "Invalid file format", - "invalidFileSize": "File size exceeds maximum allowed" - }, - "forDocumentation": "for query language documentation.", - "imageUpload": { - "clickToUpload": "Click to upload", - "maxSize": "Max size", - "orDragAndDrop": "or drag and drop", - "supportedFormats": "Supported formats" - }, "labels": { "active": "Active", "na": "N/A", "resolved": "Resolved", "responseTime": "Response time" }, - "monitoringAgentName": "Capture", - "see": "See", "table": { "empty": "Nothing here", "headers": { "actions": "Actions", - "active": "Active", "dateTime": "Date & time", "message": "Message", "monitor": "Monitor", @@ -296,6 +237,16 @@ "checkConnection": "Please check your connection", "networkError": "Network error", "unknownError": "Unknown error" + }, + "imageUpload": { + "clickToUpload": "Click to upload", + "dragAndDrop": "or drag and drop", + "supportedFormats": "Supported formats", + "maxSize": "Max size" + }, + "errors": { + "invalidFileType": "Invalid file type", + "fileTooLarge": "File too large" } }, "commonSave": "Save", @@ -303,24 +254,23 @@ "companyName": "Company name", "components": { "headerTimeRange": { - "description": { - "day": "Showing statistics for past 24 hours.", - "month": "Showing statistics for past 30 days.", - "recent": "Showing statistics for past 2 hours.", - "week": "Showing statistics for past 7 days." - }, "labels": { "day": "Day", "month": "Month", "recent": "Recent", "week": "Week" + }, + "description": { + "recent": "Showing statistics for past 2 hours.", + "day": "Showing statistics for past 24 hours.", + "week": "Showing statistics for past 7 days.", + "month": "Showing statistics for past 30 days." } } }, "configure": "Configure", "confirmPassword": "Confirm password", "cores": "Cores", - "country": "COUNTRY", "cpu": "CPU", "cpuFrequency": "CPU Frequency", "cpuLogical": "CPU (Logical)", @@ -331,76 +281,12 @@ "createMaintenance": "Create maintenance", "createMaintenanceWindow": "Create maintenance window", "createMonitor": "Create monitor", - "createMonitorPage": { - "incidentConfigDescription": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.", - "incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25", - "incidentConfigStatusCheckNumber": "How many checks before status change?", - "incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?", - "incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?", - "incidentConfigTitle": "Incidents", - "intervalDescription": "How often the monitor should be checked.", - "intervalTitle": "Check interval" - }, "createNew": "Create new", - "createNotifications": { - "dialogDeleteConfirm": "Delete", - "dialogDeleteTitle": "Are you sure you want to delete this notification?", - "discordSettings": { - "description": "Configure your Discord webhook here", - "title": "Discord", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "emailSettings": { - "description": "Destination email addresses.", - "emailLabel": "Email address", - "emailPlaceholder": "e.g. john@example.com", - "title": "Email" - }, - "nameSettings": { - "description": "A descriptive name for your integration.", - "nameLabel": "Name", - "namePlaceholder": "e.g. Slack notifications", - "title": "Name" - }, - "pagerdutySettings": { - "description": "Configure your PagerDuty integration here", - "integrationKeyLabel": "Integration key", - "integrationKeyPlaceholder": "1234567890", - "title": "PagerDuty" - }, - "slackSettings": { - "description": "Configure your Slack webhook here", - "title": "Slack", - "webhookLabel": "Slack webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "testNotification": "Test notification", - "title": "Create notification channel", - "typeSettings": { - "description": "Select the type of notification channel you want to create.", - "title": "Type", - "typeLabel": "Type" - }, - "webhookSettings": { - "description": "Configure your webhook here", - "title": "Webhook", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - } - }, - "createYour": "Create your", - "dataRate": "Data Rate", - "dataReceived": "Data Received", - "dataSent": "Data Sent", - "date&Time": "Date & Time", "delete": "Delete", "DeleteAccountButton": "Remove account", "DeleteAccountTitle": "Remove account", "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", "DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.", - "deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.", - "deleteDialogTitle": "Do you really want to delete this monitor?", "deleteStatusPage": "Do you want to delete this status page?", "deleteStatusPageConfirm": "Yes, delete status page", "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", @@ -425,23 +311,13 @@ "totalHeapSizeTitle": "Total heap size", "uptimeTitle": "Uptime", "usedHeapSizeTitle": "Used heap size" - }, - "statsDescription": "System statistics" + } }, "disk": "Disk", "diskUsage": "Disk Usage", "displayName": "Display name", - "distributedUptimeCreateAdvancedSettings": "Advanced settings", - "distributedUptimeCreateChecks": "Checks to perform", - "distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.", - "distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.", - "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", - "dnsLookup": "DNS lookup", - "dockerContainerMonitoring": "Docker container monitoring", - "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", "download": "Download", "DragandDrop": "drag and drop", - "drops": "Drops", "duration": "Duration", "edit": "Edit", "editing": "Editing...", @@ -456,10 +332,6 @@ }, "table": { "actionHeader": "Action", - "headers": { - "name": "Status page name", - "url": "Public URL" - }, "roleHeader": "Role" }, "title": "Edit user", @@ -469,9 +341,6 @@ } }, "EmailDescriptionText": "This is your current email address — it cannot be changed.", - "emailSent": "Email sent successfully", - "errorInvalidFieldId": "Invalid field ID provided", - "errorInvalidTypeId": "Invalid notification type provided", "errorPages": { "serverUnreachable": { "alertBox": "Server Connection Error", @@ -487,22 +356,13 @@ } }, "errors": "Errors", - "errorsIn": "Errors In", - "errorsOut": "Errors Out", "expectedValue": "Expected value", - "export": { - "failed": "Failed to export monitors", - "success": "Monitors exported successfully!", - "title": "Export Monitors" - }, "failedToSendEmail": "Failed to send email", "features": "Features", "FirstName": "First name", "frequency": "Frequency", "friendlyNameInput": "Friendly name", "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", - "gameServerMonitoring": "Game Server Monitoring", - "gameServerMonitoringDescription": "Check whether your game server is running or not", "gb": "GB", "general": { "noOptionsFound": "No {{unit}} found" @@ -516,109 +376,12 @@ "host": "Host", "http": "HTTP", "https": "HTTPS", - "ignoreTLSError": "Ignore TLS/SSL error", - "ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability", - "incidentsOptionsHeader": "Incidents for:", - "incidentsOptionsHeaderFilterAll": "All", - "incidentsOptionsHeaderFilterBy": "Filter by:", - "incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve", - "incidentsOptionsHeaderFilterDown": "Down", - "incidentsOptionsHeaderFilterResolved": "Resolved", - "incidentsOptionsHeaderLastDay": "Last day", - "incidentsOptionsHeaderLastHour": "Last hour", - "incidentsOptionsHeaderLastWeek": "Last week", - "incidentsOptionsHeaderShow": "Show:", - "incidentsOptionsHeaderTotalIncidents": "Total Incidents", - "incidentsOptionsPlaceholderAllServers": "All servers", "incidentsPage": { - "active": "Active", - "all": "All", - "allSystemsAreOperational": "All Systems Are Operational", - "automatic": "Automatic", - "avgResolutionTime": "Avg. Resolution Time", - "description": "Manage incidents for your monitors. You can resolve incidents here.", - "endTime": "End Time", - "errorFetchingIncident": "Error fetching incident", - "errorFetchingIncidents": "Error fetching incidents", - "errorFetchingIncidentSummary": "Error fetching incident summary", - "filterByDate": "Filter by date", - "filterByMonitor": "Filter by monitor", - "filterByPriority": "Filter by priority", - "filterByStatus": "Filter by status", - "filterByType": "Filter by type", - "incidentItemMonitor": "Monitor", - "incidentItemStatus": "Status", - "incidentResolvedFailed": "Error resolving incident", - "incidentResolvedSuccessfully": "Incident resolved successfully", - "incidentsActivePanelError": "Error loading active incidents", - "incidentsActivePanelTitle": "Total Active Incidents", - "incidentsLatestPanelEmpty": "No recent incidents", - "incidentsLatestPanelError": "Error loading latest incidents", - "incidentsLatestPanelTitle": "Latest Incidents", - "incidentsOptionsHeaderFilterActive": "Active", - "incidentsOptionsHeaderFilterAll": "All", - "incidentsOptionsHeaderFilterAutomatic": "Automatic", - "incidentsOptionsHeaderFilterManual": "Manual", - "incidentsOptionsHeaderFilterResolved": "Resolved", - "incidentsStatisticsPanelError": "Error loading statistics", - "incidentsStatisticsPanelTitle": "General Statistics", - "incidentsTableActionDetails": "Details", - "incidentsTableActionGoToMonitor": "Go to monitor", - "incidentsTableActionResolveManually": "Resolve Manually", - "incidentsTableResolved": "Closed", - "manual": "Manual", - "mostAffectedMonitor": "Most Affected Monitor", - "noIncidentIdProvided": "No incident ID provided", - "resolutions": "Resolutions", - "resolutionType": "Resolution Type", - "resolved": "Resolved", "resolveIncidentDialogCommentLabel": "Comment (optional)", "resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...", "resolveIncidentDialogConfirm": "Resolve", - "resolveIncidentDialogTitle": "Resolve Incident", - "startTime": "Start Time", - "title": "Incidents", - "totalIncidents": "Total Incidents", - "unknownMonitor": "Unknown Monitor", - "unresolved": "Unresolved" + "resolveIncidentDialogTitle": "Resolve Incident" }, - "incidentsPageActionResolveAll": "Resolve all incidents", - "incidentsPageActionResolveMonitor": "Resolve monitor incidents", - "incidentsPageTitle": "Incidents", - "incidentsTableActionResolve": "Resolve", - "incidentsTableDateTime": "Date & Time", - "incidentsTableMessage": "Message", - "incidentsTableMonitorName": "Monitor Name", - "incidentsTableNoIncidents": "No incidents recorded", - "incidentsTablePaginationLabel": "incidents", - "incidentsTableResolvedAt": "Resolved at", - "incidentsTableStatus": "Status", - "incidentsTableStatusCode": "Status Code", - "infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.", - "infrastructureAuthorizationSecretLabel": "Authorization secret", - "infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.", - "infrastructureCreateMonitor": "Create Infrastructure Monitor", - "infrastructureCreateYour": "Create your", - "infrastructureCustomizeAlerts": "Customize alerts", - "infrastructureDisplayNameLabel": "Display name", - "infrastructureEditMonitor": "Save Infrastructure Monitor", - "infrastructureEditYour": "Edit your", - "infrastructureMonitor": { - "fallback": { - "actionButton": "Let's create your first infrastructure monitor!", - "checks": [ - "Track the performance of your servers", - "Identify bottlenecks and optimize usage", - "Ensure reliability with real-time monitoring" - ], - "title": "An infrastructure monitor is used to:" - } - }, - "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", - "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", - "infrastructureProtocol": "Protocol", - "infrastructureServerRequirement": "The server you are monitoring must be running the", - "infrastructureServerUrlLabel": "Server URL", "integrations": "Integrations", "integrationsDiscord": "Discord", "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", @@ -631,9 +394,7 @@ "invalidFileSize": "File size is too large!", "inviteNoTokenFound": "No invite token found", "LastName": "Last name", - "loginHere": "Login here", "logsPage": { - "description": "System logs - last 1000 lines", "logLevelSelect": { "title": "Log level", "values": { @@ -646,10 +407,6 @@ }, "noLogs": "No logs found", "table": { - "headers": { - "name": "Status page name", - "url": "Public URL" - }, "level": "Level", "logs": "logs", "message": "Message", @@ -662,38 +419,19 @@ "logs": "Server logs", "queue": "Job queue" }, - "title": "Logs", - "toast": { - "fetchLogsSuccess": "Logs fetched successfully" - } + "title": "Logs" }, "low": "low", "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", - "maintenanceWindow": { - "fallback": { - "actionButton": "Let's create your first maintenance window!", - "checks": [ - "Mark your maintenance periods", - "Eliminate any misunderstandings", - "Stop sending alerts in maintenance windows" - ], - "title": "A maintenance window is used to:" - } - }, "maintenanceWindowDescription": "During maintenance windows, all monitoring is suspended for selected monitors. No network checks will be performed, preventing any status updates or notifications from being triggered. Your monitors will appear frozen at their last known status, and status pages will display a maintenance indicator. Once the maintenance window ends, monitoring automatically resumes, and alerts will trigger if issues are detected. Maintenance periods do not count against uptime calculations.", "maintenanceWindowName": "Maintenance Window Name", - "maskedPageSpeedKeyPlaceholder": "*************************************", "matchMethod": "Match Method", "matchMethodOptions": { "equal": "Equal", - "equalPlaceholder": "success", "include": "Include", - "includePlaceholder": "ok", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "Match Method" + "regex": "Regex" }, "MaxSize": "Maximum Size", "mb": "MB", @@ -707,7 +445,6 @@ "docs": "Docs", "incidents": "Incidents", "infrastructure": "Infrastructure", - "integrations": "Integrations", "logOut": "Log out", "logs": "Logs", "maintenance": "Maintenance", @@ -723,101 +460,18 @@ }, "message": "Message", "monitor": "monitor", - "monitorActions": { - "deleteFailed": "Failed to delete monitor", - "deleteSuccess": "Monitor deleted successfully", - "details": "Details", - "export": "Export Monitors", - "import": "Import Monitors", - "title": "Export/Import" - }, - "monitorDisplayName": "Monitor display name", "monitorHooks": { "failureAddDemoMonitors": "Failed to add demo monitors", "successAddDemoMonitors": "Successfully added demo monitors" }, "monitors": "monitors", - "monitorState": { - "active": "Active", - "paused": "Paused", - "resumed": "Resumed" - }, - "monitorStatus": { - "checkingEvery": "Checking every {{interval}}", - "down": "down", - "paused": "paused", - "up": "up", - "withCaptureAgent": "with Capture agent {{version}}" - }, - "monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding", - "monitorStatusUp": "Monitor {name} ({url}) is now UP and responding", "monitorsToApply": "Monitors to apply maintenance window to", - "monitorType": { - "docker": { - "label": "Container Name/ID", - "namePlaceholder": "My Container", - "placeholder": "my-app or abcd1234" - }, - "game": { - "label": "URL to monitor", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - }, - "http": { - "label": "URL to monitor", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "IP address to monitor", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "URL to monitor", - "namePlaceholder": "Localhost:5173", - "placeholder": "localhost" - } - }, - "mountedOn": "Mounted on", "ms": "ms", "navControls": "Controls", "network": "Network", - "networkDrops": "Network Drops", - "networkErrors": "Network Errors", - "networkInterface": "Network Interface", "nextWindow": "Next window", - "noNetworkStatsAvailable": "No network stats available.", "notFoundButton": "Go to the main dashboard", - "notificationConfig": { - "description": "Select the notifications channels you want to use", - "placeholder": "Search notifications...", - "title": "Notifications" - }, "notifications": { - "addOrEditNotifications": "Add or edit notifications", - "create": { - "failed": "Failed to create notification", - "success": "Notification created successfully" - }, - "createButton": "Create notification channel", - "createTitle": "Notification channel", - "delete": { - "failed": "Failed to delete notification", - "success": "Notification deleted successfully" - }, - "discord": { - "description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.", - "label": "Discord", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Discord webhook URL is required" - }, - "edit": { - "failed": "Failed to update notification", - "success": "Notification updated successfully" - }, - "enableNotifications": "Enable {{platform}} notifications", "fallback": { "actionButton": "Let's create your first notification channel!", "checks": [ @@ -828,52 +482,14 @@ "title": "A notification channel is used to:" }, "fetch": { - "failed": "Failed to fetch notifications", - "success": "Notifications fetched successfully" - }, - "integrationButton": "Notification Integration", - "networkError": "Network error occurred", - "slack": { - "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.", - "label": "Slack", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack webhook URL is required" - }, - "telegram": { - "chatIdLabel": "Your Chat ID", - "chatIdPlaceholder": "-1001234567890", - "description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.", - "fieldsRequired": "Telegram token and chat ID are required", - "label": "Telegram", - "tokenLabel": "Your bot token", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" + "failed": "Failed to fetch notifications" }, "test": { - "failed": "Failed to send test notification", "success": "Test notification sent successfully" - }, - "testFailed": "Failed to send test notification", - "testNotification": "Test notification", - "testNotificationDevelop": "Test notification 2", - "testSuccess": "Test notification sent successfully!", - "unsupportedType": "Unsupported notification type", - "webhook": { - "description": "You can set up a custom webhook to receive notifications when incidents occur.", - "label": "Webhooks", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Webhook URL is required" } }, - "notifyEmails": "Also notify via email to multiple addresses (coming soon)", - "notifySMS": "Notify via SMS (coming soon)", "now": "Now", "os": "OS", - "packetsPerSecond": "Packets per second", - "packetsReceived": "Packets Received", - "packetsReceivedRate": "Packets Received Rate", - "packetsSent": "Packets Sent", "pages": { "checks": { "selects": { @@ -929,7 +545,6 @@ "label": "Use advanced matching" }, "expectedValue": { - "description": "The expected value is used to match against response result, and the match determines the status.", "label": "Expected value" }, "jsonPath": { @@ -952,15 +567,6 @@ "title": "Check frequency" }, "general": { - "description": { - "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", - "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", - "hardware": "Enter the URL of your infrastructure agent, the authorization secret, and a clear display name that appears on the dashboard.", - "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", - "pagespeed": "Enter the URL of the page to analyze with Google PageSpeed and add a clear display name that appears on the dashboard.", - "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", - "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." - }, "option": { "container": { "label": "Container name/ID", @@ -983,7 +589,14 @@ "placeholder": "https://www.google.com" } }, - "title": "General settings" + "title": "General settings", + "description": { + "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", + "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", + "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type." + } }, "ignoreTls": { "description": "Configure TLS/SSL certificate validation for HTTPS connections.", @@ -1016,17 +629,12 @@ "optionDockerDescription": "Use Docker to monitor if a container is running.", "optionGame": "Game", "optionGameDescription": "Monitor if a specific game server is online.", - "optionHardware": "Hardware", - "optionHardwareDescription": "Monitor server hardware metrics like CPU, memory, and disk.", "optionHttp": "HTTP(S)", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", - "optionPagespeed": "PageSpeed", - "optionPagespeedDescription": "Analyze page performance using Google PageSpeed Insights.", "optionPing": "Ping", "optionPingDescription": "Use ICMP Ping to monitor if a server is online.", "optionPort": "Port", "optionPortDescription": "Monitor if a specific port on a server is open.", - "optionType": "Type", "title": "Type" } } @@ -1043,10 +651,6 @@ "overview": "Overview", "resolutionDetails": "Resolution details", "resolutionType": "Type:", - "resolutionTypes": { - "automatic": "Automatic", - "manual": "Manual" - }, "resolve": "Resolve Incident", "resolvedAt": "Resolved at:", "resolvedBy": "Resolved by:", @@ -1060,26 +664,26 @@ }, "filters": { "allMonitors": "All monitors", - "clearFilters": "Clear Filters", + "clearFilters": "Clear filters", "monitor": "Monitor", "resolutionType": "Resolution type", "resolutionTypes": { + "manual": "Manual", "automatic": "Automatic", - "manual": "Manual" + "all": "All" } }, "summaryCard": { "activeIncidents": { - "active_one": "{{count}} Active incident", - "active_other": "{{count}} Active incidents", "active_zero": "No active incidents", - "title": "Active Incidents" + "title": "Active Incidents", + "active": "active" }, "incidentStats": { - "avgResolutionTime": "Average resolution time", - "mostAffectedMonitor": "Most affected monitor", + "avgResolutionTime": "Avg Resolution Time", + "mostAffectedMonitor": "Most Affected Monitor", "title": "Incident Statistics", - "totalIncidents": "Total incidents" + "totalIncidents": "Total Incidents" }, "latestIncidents": { "title": "Latest Incidents" @@ -1101,7 +705,6 @@ "resolvedIncidents": "Resolved Incidents", "status": { "active": "Active", - "na": "N/A", "resolved": "Resolved" } } @@ -1117,15 +720,6 @@ "temp": "Temp" } }, - "fallback": { - "actionButton": "Let's create your first infrastructure monitor!", - "checks": [ - "Track the performance of your servers", - "Identify bottlenecks and optimize usage", - "Ensure reliability with real-time monitoring" - ], - "title": "An infrastructure monitor is used to:" - }, "gauges": { "cpu": { "lowerLabel": "Max frequency", @@ -1164,6 +758,15 @@ "network": "Network", "overview": "Overview" } + }, + "fallback": { + "actionButton": "Let's create your first infrastructure monitor!", + "checks": [ + "Track the performance of your servers", + "Identify bottlenecks and optimize usage", + "Ensure reliability with real-time monitoring" + ], + "title": "An infrastructure monitor is used to:" } }, "notifications": { @@ -1189,10 +792,8 @@ "title": "Address" }, "homeServer": { - "description": "Select the home server for this notification channel.", "optionHomeServer": "Home server", - "placeholder": "example.com", - "title": "Home server" + "placeholder": "example.com" }, "matrix": { "description": "Configure your Matrix homeserver connection for notifications.", @@ -1243,6 +844,11 @@ "title": "Performance report" } }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeed score" + } + }, "fallback": { "actionButton": "Let's create your first PageSpeed monitor!", "checks": [ @@ -1251,137 +857,59 @@ "Give suggestions on how the page can be improved" ], "title": "A PageSpeed monitor is used to:" - }, - "table": { - "headers": { - "pageSpeedScore": "PageSpeed score" - } } }, "statusPages": { "fallback": { - "actionButton": "Create a status page!", - "checks": [ - "Monitor and display the health of your services in real time", - "Track multiple services and share their status", - "Keep users informed about outages and performance" - ], - "title": "A status page is used to:" - }, - "form": { - "access": { - "description": "Choose if you want your status page to be public", - "option": { - "published": { - "name": "Published and publicly visible" - } - }, - "title": "Access" - }, - "appearance": { - "description": "Define the default look and feel of your public status page", - "option": {}, - "title": "Appearance" - }, - "basicInfo": { - "description": "Name and address of your status page", - "option": { - "name": { - "label": "Name", - "placeholder": "My status page" - }, - "url": { - "label": "URL", - "placeholder": "my-status-page" - } - }, - "title": "Basic information" - }, - "monitors": { - "description": "Select which monitors to display on your status page", - "noMonitors": "No monitors available. Create uptime monitors first.", - "option": { - "monitors": { - "label": "Monitors", - "placeholder": "Select monitors..." - } - }, - "title": "Monitors" - }, - "timezone": { - "description": "Select the timezone for your status page", - "option": { - "timezone": { - "label": "Timezone", - "placeholder": "Search timezones..." - } - }, - "title": "Timezone" - } + "actionButton": "Create status page", + "checks": "No status pages found", + "title": "Status pages" }, "monitorsList": { "chartTypeHeatmap": "Heatmap", "chartTypeHistogram": "Histogram" }, - "serviceStauts": "Service stauts", - "settings": { - "description": "Configure basic settings for your status page.", - "option": { - "name": { - "label": "Status page name", - "placeholder": "e.g. My Company Status Page" - }, - "published": { - "label": "Published and visible to the public" - }, - "subdomain": { - "label": "Subdomain", - "placeholder": "e.g. my-company" - }, - "timezone": { - "label": "Timezone" - } - }, - "title": "Status page settings" - }, "statusBar": { - "allDown": "All systems down", + "allDown": "All systems are down", "allUp": "All systems operational", - "degraded": "Degraded performance", - "unknown": "Unknown status" - } + "degraded": "Some systems are experiencing issues", + "unknown": "Unable to determine system status" + }, + "table": { + "headers": { + "name": "Status page name", + "url": "Public URL" + }, + "published": "Published", + "unpublished": "Unpublished" + }, + "title": "Status pages" }, "uptime": { + "filters": { + "search": { + "placeholder": "Search monitors..." + } + }, "table": { "headers": { "responseTime": "Response time" } }, - "filters": { - "search": { - "placeholder": "Search monitors..." - } + "fallback": { + "actionButton": "Create a monitor!", + "checks": [ + "Check if websites or servers are online & responsive", + "Alert teams about downtime or performance issues", + "Monitor HTTP endpoints, pings, containers & ports", + "Track historical uptime and reliability trends" + ], + "title": "An uptime monitor is used to:" } } }, - "pageSpeed": { - "fallback": { - "actionButton": "Let's create your first PageSpeed monitor!", - "checks": [ - "Report on the user experience of a page", - "Help analyze webpage speed", - "Give suggestions on how the page can be improved" - ], - "title": "A PageSpeed monitor is used to:" - } - }, "pageSpeedAddApiKey": "to add your API key.", - "pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.", - "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", - "pageSpeedDetailsPerformanceReportCalculator": "See calculator", "pageSpeedLearnMoreLink": "Click here", - "pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website", - "pageSpeedMonitor": "PageSpeed monitor", "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", "passwordPanel": { "confirmNewPassword": "Confirm new password", @@ -1391,15 +919,10 @@ "newPassword": "New password", "passwordChangedSuccess": "Your password was changed successfully.", "passwordInputIncorrect": "Your password input was incorrect.", - "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`).", - "saving": "Saving..." + "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)." }, "pause": "Pause", "PhotoDescriptionText": "This photo will be displayed in your profile page.", - "pingMonitoring": "Ping monitoring", - "pingMonitoringDescription": "Check whether your server is available or not.", - "portMonitoring": "Port monitoring", - "portMonitoringDescription": "Check whether your port is open or not.", "portToMonitor": "Port to monitor", "publicLink": "Public link", "queuePage": { @@ -1431,8 +954,7 @@ "title": "Queue metrics", "valueHeader": "Value" }, - "refreshButton": "Refresh", - "title": "Queue" + "refreshButton": "Refresh" }, "rate": "Rate", "remove": "Remove", @@ -1441,7 +963,6 @@ "reset": "Reset", "response": "RESPONSE", "responseTime": "Response time", - "responseTimeBreakdown": "Response time breakdown", "resume": "Resume", "roles": { "admin": "Admin", @@ -1451,13 +972,7 @@ }, "save": "Save", "selectAll": "Select all", - "selectInterface": "Select Interface", - "sendTestNotifications": "Send test notifications", - "seperateEmails": "You can separate multiple emails with a comma", - "settingsAppearance": "Appearance", - "settingsDisabled": "Disabled", "settingsDisplayTimezone": "Display timezone", - "settingsEmailUser": "Email user - Username for authentication, overrides email address if specified", "settingsFailedToClearStats": "Failed to clear stats", "settingsFailedToDeleteMonitors": "Failed to delete all monitors", "settingsFailedToSave": "Failed to save settings", @@ -1466,7 +981,6 @@ "settingsPage": { "aboutSettings": { "labelDevelopedBy": "Developed by Bluewave Labs", - "labelVersion": "Version", "title": "About" }, "demoMonitorsSettings": { @@ -1514,21 +1028,17 @@ "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.", "clearAllStatsDialogTitle": "Do you want to clear all stats?", "description": "Define how long you want to retain historical data. You can also clear all existing data.", - "labelTTL": "The days you want to keep monitoring history.", - "labelTTLOptional": "0 for infinite", "title": "Monitor history" }, "systemResetSettings": { "buttonRemoveAllMonitors": "Remove all monitors", "description": "Remove all monitors from your system.", "dialogConfirm": "Yes, remove all monitors", - "dialogDescription": "Once removed, the monitors cannot be retrieved.", "dialogTitle": "Do you want to remove all monitors?", "title": "System reset" }, "timezoneSettings": { "description": "Select the timezone used to display dates and times throughout the application.", - "label": "Display timezone", "title": "Display timezone" }, "title": "Settings", @@ -1549,7 +1059,6 @@ "title": "Monitor IP/URL on Status Page" } }, - "settingsSave": "Save", "settingsStatsCleared": "Stats cleared successfully", "settingsSuccessSaved": "Settings saved successfully", "settingsTestEmailFailed": "Failed to send test email", @@ -1568,28 +1077,20 @@ "statusBreadCrumbsDetails": "Details", "statusBreadCrumbsStatusPages": "Status Pages", "statusCode": "Status code", - "statusMsg": { - "down": "Your site is down.", - "paused": "Monitoring is paused.", - "pending": "Pending...", - "up": "Your site is up." - }, "statusPage": { "contents": "Contents", "createSuccess": "Status page created successfully", "deleteFailed": "Failed to delete status page", "deleteSuccess": "Status page deleted successfully", - "fallback": { - "actionButton": "Let's create your first status page!", - "checks": [ - "Monitor and display the health of your services in real time", - "Track multiple services and share their status", - "Keep users informed about outages and performance" - ], - "title": "A status page is used to:" - }, "generalSettings": "General settings", - "updateSuccess": "Status page updated successfully" + "updateSuccess": "Status page updated successfully", + "details": { + "statusHeader": { + "allUp": "All systems operational", + "allDown": "All systems are experiencing issues", + "anyDown": "Some systems are experiencing issues" + } + } }, "statusPageCreate": { "buttonSave": "Save" @@ -1607,7 +1108,6 @@ "statusPageStatusServiceStatus": "Service status", "submit": "Submit", "SupportedFormats": "Supported formats", - "tcpConnection": "TCP connection", "teamPanel": { "addTeamMember": { "addButton": "Add Member", @@ -1616,6 +1116,12 @@ "title": "Register new team member" }, "cancel": "Cancel", + "changeTeamPassword": { + "changePasswordMenu": "Reset Password", + "description": "Create a new password for this team member. You will need to share the password with them securely.", + "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way.", + "title": "Reset team member password" + }, "email": "Email", "emailToken": "E-mail token", "filter": { @@ -1629,114 +1135,72 @@ "inviteTeamMember": "Invite a team member", "noMembers": "There are no team members with this role", "register": "Register a team member", + "registerTeamMember": { + "auth": { + "common": { + "inputs": { + "role": { + "errors": { + "empty": "Role is required" + } + } + } + } + } + }, "registerToast": { "success": "User created, share credentials with the member securely." }, + "role": "Role", "selectRole": "Select role", "table": { "created": "Created", "email": "Email", - "headers": { - "name": "Status page name", - "url": "Public URL" - }, "name": "Name", "role": "Role" }, - "teamMembers": "Team members", - "uptime": { - "fallback": { - "actionButton": "Create a monitor!", - "checks": [ - "Check if websites or servers are online & responsive", - "Alert teams about downtime or performance issues", - "Monitor HTTP endpoints, pings, containers & ports", - "Track historical uptime and reliability trends" - ], - "title": "An uptime monitor is used to:" - }, - "filters": { - "search": { - "placeholder": "Search monitors..." - } - }, - "table": { - "headers": { - "responseTime": "Response time" - } - } - } + "teamMembers": "Team members" }, - "testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button", "time": { "fifteenMinutes": "15 minutes", "fifteenSeconds": "15 seconds", "fiveMinutes": "5 minutes", "fourMinutes": "4 minutes", - "oneDay": "1 day", - "oneHour": "1 hour", "oneMinute": "1 minute", - "oneWeek": "1 week", "tenMinutes": "10 minutes", "thirtyMinutes": "30 minutes", "thirtySeconds": "30 seconds", "threeMinutes": "3 minutes", - "twentyMinutes": "20 minutes", "twoMinutes": "2 minutes" }, - "timeToFirstByte": "Time to first byte", "timezone": "Timezone", "timeZoneInfo": "All dates and times are in GMT+0 time zone.", "title": "Title", - "tlsErrorIgnored": "TLS/SSL errors ignored", - "tlsHandshake": "TLS handshake", "total": "Total", "type": "Type", "update": "Update", "uptime": "Uptime", - "uptimeAdvancedMatching": { - "jsonPath": "JSON Path" - }, - "uptimeCreate": "The expected value is used to match against response result, and the match determines the status.", - "uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See", - "uptimeCreateJsonPathQuery": "for query language documentation.", - "uptimeGeneralInstructions": { - "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", - "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", - "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", - "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", - "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", - "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." - }, - "uptimeMonitor": { - "fallback": { - "actionButton": "Let's create your first uptime monitor!", - "checks": [ - "Check if websites or servers are online & responsive", - "Alert teams about downtime or performance issues", - "Monitor HTTP endpoints, pings, containers & ports", - "Track historical uptime and reliability trends" - ], - "title": "An uptime monitor is used to:" - } - }, "url": "URL", - "urlMonitor": "URL to monitor", "used": "Used", - "v1": { - "infrastructure": { - "disk_selection_description": "Select the specific disks you want to monitor.", - "disk_selection_info": "No disk detected for the moment.", - "disk_selection_title": "Disk Selection" - } - }, - "waitTime": "Wait time", - "webhookSendError": "Error sending webhook notification to {platform}", - "webhookSendSuccess": "Webhook notification sent successfully", - "webhookUnsupportedPlatform": "Unsupported platform: {platform}", - "websiteMonitoring": "Website monitoring", - "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", - "whenNewIncident": "When there is a new incident,", "window": "window", - "YourPhoto": "Profile photo" + "YourPhoto": "Profile photo", + "failedDeleteMonitor": "Failed to delete monitor", + "failedPauseMonitor": "Failed to pause monitor", + "hour": "hour", + "minute": "minute", + "monitorDeleted": "Monitor deleted successfully", + "monitorPaused": "Monitor paused successfully", + "monitorResumed": "Monitor resumed successfully", + "name": "Name", + "maintenanceWindow": { + "fallback": { + "actionButton": "Let's create your first maintenance window!", + "checks": [ + "Mark your maintenance periods", + "Eliminate any misunderstandings", + "Stop sending alerts in maintenance windows" + ], + "title": "A maintenance window is used to:" + } + } } From a1e6eaa9d042fe513af9a1490512a2ef72bd7d84 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 19:03:17 +0000 Subject: [PATCH 06/24] submit --- client/src/Pages/StatusPage/Create/index.tsx | 69 ++++++++++++++----- client/src/Pages/StatusPage/Status/index.tsx | 31 ++++++++- client/src/Routes/index.jsx | 8 ++- client/src/locales/en.json | 71 ++++++++++++++++++-- 4 files changed, 154 insertions(+), 25 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index ec5245e41..104ce6476 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -20,9 +20,11 @@ import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; import type { StatusPageFormData } from "@/Validation/statusPage"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, usePost } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; import timezones from "@/Utils/timezones.json"; +import { useNavigate } from "react-router-dom"; +import axios from "axios"; interface TimezoneOption { _id: string; @@ -32,8 +34,10 @@ interface TimezoneOption { const CreateStatusPage = () => { const theme = useTheme(); const { t } = useTranslation(); + const navigate = useNavigate(); const { schema, defaults } = useStatusPageForm(); + const { post, loading: isSubmitting } = usePost(); const { data: monitorsResponse } = useGet( "/monitors/team?type=http&type=ping&type=port&type=docker" @@ -55,8 +59,39 @@ const CreateStatusPage = () => { console.log(errors); }; - const onSubmit = (data: StatusPageFormData) => { - console.log("Form Data Submitted: ", data); + const onSubmit = async (data: StatusPageFormData) => { + const fd = new FormData(); + fd.append("type", "uptime"); + fd.append("isPublished", String(data.isPublished)); + if (data.companyName) fd.append("companyName", data.companyName); + if (data.url) fd.append("url", data.url); + if (data.timezone) fd.append("timezone", data.timezone); + + data.monitors.forEach((monitorId) => { + fd.append("monitors[]", monitorId); + }); + + if (data.logo?.data && data.logo.data !== "") { + try { + const imageResult = await axios.get(data.logo.data, { + responseType: "blob", + }); + fd.append("logo", imageResult.data); + if (data.logo.data.startsWith("blob:")) { + URL.revokeObjectURL(data.logo.data); + } + } catch (e) { + console.error("Error fetching logo blob:", e); + } + } + + const result = await post("/status-page", fd, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + if (result) { + navigate(`/status/uptime/${data.url}`); + } }; return ( @@ -137,10 +172,18 @@ const CreateStatusPage = () => { name="monitors" control={control} render={({ field, fieldState }) => { - // Preserve order from field.value (IDs) when building selectedMonitors const selectedMonitors = field.value .map((id: string) => monitors.find((m) => m.id === id)) - .filter((m): m is Monitor => m !== undefined); + .filter(Boolean) as Monitor[]; + + const handleDragEnd = (result: DropResult) => { + if (!result.destination) return; + const reordered = Array.from(field.value); + const [removed] = reordered.splice(result.source.index, 1); + reordered.splice(result.destination.index, 0, removed); + field.onChange(reordered); + }; + return ( { onChange={(_, newValue: Monitor[]) => { field.onChange(newValue.map((m) => m.id)); }} - isOptionEqualToValue={(option, value) => option.id === value.id} fieldLabel={t( "pages.statusPages.form.monitors.option.monitors.label" )} @@ -165,24 +207,15 @@ const CreateStatusPage = () => { helperText={fieldState.error?.message ?? ""} /> )} + renderTags={() => null} /> {selectedMonitors.length > 0 && ( - { - if (!result.destination) return; - const reordered = Array.from(field.value); - const [removed] = reordered.splice(result.source.index, 1); - reordered.splice(result.destination.index, 0, removed); - field.onChange(reordered); - }} - > + {(provided) => ( {selectedMonitors.map((monitor, index) => ( { justifyContent="flex-end" > + + ); +}; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 0175c925a..f8695ae80 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -29,6 +29,7 @@ import timezones from "@/Utils/timezones.json"; import { useNavigate, useParams } from "react-router-dom"; import axios from "axios"; import { MuiColorInput } from "mui-color-input"; +import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls"; interface TimezoneOption { _id: string; @@ -135,6 +136,13 @@ const CreateStatusPage = () => { component="form" onSubmit={handleSubmit(onSubmit, onError)} > + {!isCreate && ( + { + console.log("poo"); + }} + /> + )} Date: Tue, 3 Feb 2026 21:14:16 +0000 Subject: [PATCH 15/24] handle delete --- .../Components/HeaderConfigStatusControls.tsx | 4 +- client/src/Pages/StatusPage/Create/index.tsx | 41 +++++++++++++++---- .../Components/HeaderStatusPageControls.tsx | 4 +- client/src/Pages/StatusPage/Status/index.tsx | 4 +- .../Components/StatusPagesTable.tsx | 4 +- .../Pages/StatusPage/StatusPages/index.tsx | 4 +- client/src/Routes/index.jsx | 8 ++-- client/src/locales/en.json | 1 + server/src/routes/statusPageRoute.ts | 2 +- 9 files changed, 47 insertions(+), 25 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx b/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx index f628fd00d..99ae46cdd 100644 --- a/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx +++ b/client/src/Pages/StatusPage/Create/Components/HeaderConfigStatusControls.tsx @@ -1,12 +1,10 @@ import Stack from "@mui/material/Stack"; -import { Icon, MonitorStatus } from "@/Components/v2/design-elements"; +import { Icon } from "@/Components/v2/design-elements"; import { Button } from "@/Components/v2/inputs"; import { Trash } from "lucide-react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { usePost } from "@/Hooks/UseApi"; interface HeaderConfigStatusControlsProps { onDelete: () => void; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index f8695ae80..83286277b 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -13,16 +13,17 @@ import { TextField, Autocomplete, Checkbox, + Dialog, } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useStatusPageForm } from "@/Hooks/useStatusPageForm"; import type { StatusPageFormData } from "@/Validation/statusPage"; -import { useGet, usePost, usePut } from "@/Hooks/UseApi"; +import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi"; import type { Monitor } from "@/Types/Monitor"; import type { StatusPageResponse } from "@/Types/StatusPage"; import timezones from "@/Utils/timezones.json"; @@ -55,6 +56,10 @@ const CreateStatusPage = () => { const { post, loading: isSubmittingPost } = usePost(); const { put, loading: isSubmittingPut } = usePut(); + const { deleteFn, loading: isDeleting } = useDelete(); + + // Delete dialog state + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const isSubmitting = isSubmittingPost || isSubmittingPut; const { schema, defaults } = useStatusPageForm({ @@ -78,6 +83,22 @@ const CreateStatusPage = () => { console.log(errors); }; + const handleDeleteClick = () => { + setIsDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = async () => { + const result = await deleteFn(`/status-page/${statusPageData?.statusPage?.id}`); + if (result) { + navigate("/status"); + } + setIsDeleteDialogOpen(false); + }; + + const handleDeleteCancel = () => { + setIsDeleteDialogOpen(false); + }; + const onSubmit = async (data: StatusPageFormData) => { const fd = new FormData(); fd.append("type", "uptime"); @@ -136,13 +157,7 @@ const CreateStatusPage = () => { component="form" onSubmit={handleSubmit(onSubmit, onError)} > - {!isCreate && ( - { - console.log("poo"); - }} - /> - )} + {!isCreate && } { {t("common.buttons.save")} + ); }; diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx index 4fc2e41e1..d738671ac 100644 --- a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -50,7 +50,7 @@ export const HeaderStatusPageControls = ({ { window.open( - `/status/uptime/public/${statusPage.url}`, + `/status/public/${statusPage.url}`, "_blank", "noopener,noreferrer" ); @@ -77,7 +77,7 @@ export const HeaderStatusPageControls = ({ variant="contained" color="secondary" startIcon={} - onClick={() => navigate(`/status/uptime/configure/${statusPage.url}`)} + onClick={() => navigate(`/status/configure/${statusPage.url}`)} > {t("common.buttons.configure")} diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index fcb92f2f0..6cb9830b9 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -19,7 +19,7 @@ const StatusPageView = () => { const { url } = useParams(); const isAdmin = useIsAdmin(); const location = useLocation(); - const isPublic = location.pathname.startsWith("/status/uptime/public"); + const isPublic = location.pathname.startsWith("/status/public"); const apiUrl = url ? `/status-page/${url}?type=uptime` : null; @@ -47,7 +47,7 @@ const StatusPageView = () => { {t("pages.statusPages.details.empty.title")} {isAdmin && ( - + {t("pages.statusPages.details.empty.addMonitor")} )} diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx index 1741aeadf..d832751de 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx @@ -22,7 +22,7 @@ export const StatusPagesTable = ({ data }: StatusPagesTableProps) => { const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => { if (row.isPublished) { e.stopPropagation(); - const url = `/status/uptime/public/${row.url}`; + const url = `/status/public/${row.url}`; window.open(url, "_blank", "noopener,noreferrer"); } }; @@ -91,7 +91,7 @@ export const StatusPagesTable = ({ data }: StatusPagesTableProps) => { }; const handleRowClick = (statusPage: StatusPage) => { - navigate(`/status/uptime/${statusPage.url}`); + navigate(`/status/${statusPage.url}`); }; return ( diff --git a/client/src/Pages/StatusPage/StatusPages/index.tsx b/client/src/Pages/StatusPage/StatusPages/index.tsx index 6a34e587d..0c891591b 100644 --- a/client/src/Pages/StatusPage/StatusPages/index.tsx +++ b/client/src/Pages/StatusPage/StatusPages/index.tsx @@ -27,10 +27,10 @@ const StatusPages = () => { error={!!error} items={statusPages ?? []} actionButtonText={t("pages.statusPages.fallback.actionButton")} - actionLink="/status/uptime/create" + actionLink="/status/create" > diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 078d05d75..908d7efe1 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -237,7 +237,7 @@ const Routes = () => { /> @@ -248,7 +248,7 @@ const Routes = () => { /> @@ -259,7 +259,7 @@ const Routes = () => { /> @@ -377,7 +377,7 @@ const Routes = () => { element={} /> diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d98d4fbdd..04eca42b0 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -861,6 +861,7 @@ } }, "statusPages": { + "deleteSuccess": "Status page deleted successfully", "fallback": { "title": "A status page is used to:", "checks": [ diff --git a/server/src/routes/statusPageRoute.ts b/server/src/routes/statusPageRoute.ts index f70802f73..1468f9d16 100755 --- a/server/src/routes/statusPageRoute.ts +++ b/server/src/routes/statusPageRoute.ts @@ -19,7 +19,7 @@ class StatusPageRoutes { this.router.put("/:id", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage); this.router.get("/:url", this.statusPageController.getStatusPageByUrl); - this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage); + this.router.delete("/:id", verifyJWT, this.statusPageController.deleteStatusPage); } getRouter() { From f799d460257764e68c84f430b54afd785eabc33f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 21:29:02 +0000 Subject: [PATCH 16/24] cleanup, style picler --- .../src/Components/v2/inputs/ColorPicker.tsx | 48 +++ client/src/Components/v2/inputs/index.tsx | 1 + .../Create/Components/MonitorList/index.jsx | 110 ------- .../Create/Components/Progress/index.jsx | 43 --- .../Create/Components/Skeleton/index.jsx | 14 - .../Create/Components/Tabs/ConfigStack.jsx | 31 -- .../Create/Components/Tabs/Content.jsx | 107 ------ .../Create/Components/Tabs/Settings.jsx | 187 ----------- .../Create/Components/Tabs/index.jsx | 99 ------ .../Create/Hooks/useCreateStatusPage.jsx | 25 -- .../Create/Hooks/useMonitorsFetch.jsx | 33 -- client/src/Pages/StatusPage/Create/index.tsx | 6 +- client/src/Pages/StatusPage/Create/old.jsx | 311 ------------------ .../Status/Hooks/useStatusPageDelete.jsx | 43 --- .../Status/Hooks/useStatusPageFetch.jsx | 53 --- client/src/locales/en.json | 20 -- 16 files changed, 52 insertions(+), 1079 deletions(-) create mode 100644 client/src/Components/v2/inputs/ColorPicker.tsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/Progress/index.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx delete mode 100644 client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx delete mode 100644 client/src/Pages/StatusPage/Create/old.jsx delete mode 100644 client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx delete mode 100644 client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx diff --git a/client/src/Components/v2/inputs/ColorPicker.tsx b/client/src/Components/v2/inputs/ColorPicker.tsx new file mode 100644 index 000000000..f52e66317 --- /dev/null +++ b/client/src/Components/v2/inputs/ColorPicker.tsx @@ -0,0 +1,48 @@ +import { MuiColorInput } from "mui-color-input"; +import type { MuiColorInputProps } from "mui-color-input"; +import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import { useTheme } from "@mui/material"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "@/Components/v2/inputs/FieldLabel"; + +interface ColorPickerProps extends MuiColorInputProps { + fieldLabel?: string; + required?: boolean; +} + +export const ColorInput = ({ fieldLabel, required, ...props }: ColorPickerProps) => { + const theme = useTheme(); + const input = ( + + ); + if (fieldLabel) { + return ( + + {fieldLabel} + {input} + + ); + } + + return input; +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index ad7c984bd..fa04903c9 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -13,3 +13,4 @@ export * from "./Radio"; export * from "./Switch"; export * from "./Slider"; export * from "./ImageUpload"; +export * from "./ColorPicker"; diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx deleted file mode 100644 index 349571fe8..000000000 --- a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx +++ /dev/null @@ -1,110 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import Icon from "@/Components/v1/Icon"; - -// Utils -import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; -import { useTheme } from "@emotion/react"; -const MonitorListItem = ({ - monitor, - innerRef, - draggableProps, - dragHandleProps, - onDelete, -}) => { - const theme = useTheme(); - return ( - - - {monitor.name} - { - onDelete(monitor); - }} - /> - - ); -}; - -const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { - const onDelete = (monitorToDelete) => { - const newMonitors = selectedMonitors.filter( - (monitor) => monitor.id !== monitorToDelete.id - ); - setSelectedMonitors(newMonitors); - }; - const reorder = (list, startIndex, endIndex) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - - return result; - }; - - const onDragEnd = (result) => { - // dropped outside the list - if (!result.destination) { - return; - } - - const reorderedMonitors = reorder( - selectedMonitors, - result.source.index, - result.destination.index - ); - - setSelectedMonitors(reorderedMonitors); - }; - - return ( - - - {(provided, snapshot) => ( - - {selectedMonitors?.map((monitor, index) => ( - - {(provided, snapshot) => ( - - )} - - ))} - {provided.placeholder} - - )} - - - ); -}; - -export default MonitorList; diff --git a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx deleted file mode 100644 index 594fd9709..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Button, Box } from "@mui/material"; -import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import { useTranslation } from "react-i18next"; -import { formatBytes } from "../../../../../Utils/fileUtils.js"; -const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { - const { t } = useTranslation(); - if (isLoading) { - return ( - - } - label={logo?.name} - size={formatBytes(logo?.size)} - progress={progressValue} - onClick={removeLogo} - /> - ); - } - - if (logo && logoType) { - return ( - - - - ); - } -}; - -export default Progress; diff --git a/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx b/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx deleted file mode 100644 index 482c490fb..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx deleted file mode 100644 index f31c456ef..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; - -// This can be used to add any extra/additional section/stacks on top of existing sections on the tab -const ConfigStack = ({ title, description, children }) => { - const theme = useTheme(); - return ( - - - - {title} - - {description} - - {children} - - ); -}; - -ConfigStack.propTypes = { - title: PropTypes.string.isRequired, // Title must be a string and is required - description: PropTypes.string.isRequired, // Description must be a string and is required - children: PropTypes.node.isRequired, -}; - -export default ConfigStack; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx deleted file mode 100644 index e84de16dc..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ /dev/null @@ -1,107 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import { TabPanel } from "@mui/lab"; -import MonitorList from "../MonitorList/index.jsx"; -import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import ConfigStack from "./ConfigStack.jsx"; -const Content = ({ - tabValue, - form, - monitors, - handleFormChange, - errors, - selectedMonitors, - setSelectedMonitors, -}) => { - // Local state - const [search, setSearch] = useState(""); - - // Handlers - const handleMonitorsChange = (selectedMonitors) => { - handleFormChange({ - target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor.id) }, - }); - setSelectedMonitors(selectedMonitors); - }; - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - - - - - {errors["monitors"]} - - - - - - - - - - - - - - ); -}; - -export default Content; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx deleted file mode 100644 index d4c583d3a..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ /dev/null @@ -1,187 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import { TabPanel } from "@mui/lab"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import ColorPicker from "@/Components/v1/Inputs/ColorPicker/index.jsx"; -import Progress from "../Progress/index.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import timezones from "../../../../../Utils/timezones.json"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -import { useMemo, useState, useCallback } from "react"; - -const TabSettings = ({ - isCreate, - tabValue, - form, - handleFormChange, - handleImageChange, - progress, - removeLogo, - errors, -}) => { - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const [rawInput, setRawInput] = useState(""); - - const selectedTimezone = useMemo( - () => timezones.find((tz) => tz._id === form.timezone) ?? null, - [form.timezone] - ); - - const handleTimezoneChange = useCallback( - (newValue) => { - setRawInput(""); - handleFormChange({ - target: { - name: "timezone", - value: newValue?._id ?? "", - }, - }); - }, - [handleFormChange] - ); - - return ( - - - - - - {t("access")} - - {t("statusPageCreateSettings")} - - - - - - - - - {t("basicInformation")} - - - {t("statusPageCreateBasicInfoDescription")} - - - - - - - - - - - {t("timezone")} - - - {t("statusPageCreateSelectTimeZoneDescription")} - - - - setRawInput(newVal)} - handleChange={handleTimezoneChange} - isAdorned={true} - unit="timezone" - /> - - - - - - {t("statusPageCreateAppearanceTitle")} - - - {t("statusPageCreateAppearanceDescription")} - - - - - - - - - - - ); -}; - -TabSettings.propTypes = { - isCreate: PropTypes.bool, - tabValue: PropTypes.string, - form: PropTypes.object, - handleFormChange: PropTypes.func, - handleImageChange: PropTypes.func, - progress: PropTypes.object, - removeLogo: PropTypes.func, - errors: PropTypes.object, -}; - -export default TabSettings; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx deleted file mode 100644 index e8669c474..000000000 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -// Components -import { TabContext } from "@mui/lab"; -import { Tab } from "@mui/material"; -import Settings from "./Settings.jsx"; -import Content from "./Content.jsx"; - -// Utils -import PropTypes from "prop-types"; -import CustomTabList from "@/Components/v1/Tab/index.jsx"; - -const Tabs = ({ - isCreate, - form, - errors, - monitors, - selectedMonitors, - setSelectedMonitors, - handleFormChange, - handleImageChange, - progress, - removeLogo, - tab, - setTab, - TAB_LIST, - handleDelete, - isDeleteOpen, - setIsDeleteOpen, - isDeleting, - isLoading, -}) => { - return ( - - { - setTab(TAB_LIST.indexOf(selected)); - }} - aria-label="status page tabs" - > - {TAB_LIST.map((tabLabel) => ( - - ))} - - {tab === 0 ? ( - - ) : ( - - )} - - ); -}; - -Tabs.propTypes = { - isCreate: PropTypes.bool, - form: PropTypes.object, - errors: PropTypes.object, - monitors: PropTypes.array, - selectedMonitors: PropTypes.array, - setSelectedMonitors: PropTypes.func, - handleFormChange: PropTypes.func, - handleImageChange: PropTypes.func, - progress: PropTypes.object, - removeLogo: PropTypes.func, - tab: PropTypes.number, - setTab: PropTypes.func, - TAB_LIST: PropTypes.array, - handleDelete: PropTypes.func, - isDeleteOpen: PropTypes.bool, - setIsDeleteOpen: PropTypes.func, - isDeleting: PropTypes.bool, - isLoading: PropTypes.bool, -}; - -export default Tabs; diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx deleted file mode 100644 index c0bcf5ab4..000000000 --- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../main.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -const useCreateStatusPage = (isCreate) => { - const [isLoading, setIsLoading] = useState(false); - const [networkError, setNetworkError] = useState(false); - const createStatusPage = async ({ form, id }) => { - setIsLoading(true); - try { - await networkService.createStatusPage({ form, isCreate, id }); - return true; - } catch (error) { - setNetworkError(true); - createToast({ body: error?.response?.data?.msg ?? error.message }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [createStatusPage, isLoading, networkError]; -}; - -export { useCreateStatusPage }; diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx deleted file mode 100644 index 03ebb07bf..000000000 --- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -const useMonitorsFetch = () => { - const { user } = useSelector((state) => state.auth); - - const [monitors, setMonitors] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - useEffect(() => { - const fetchMonitors = async () => { - try { - const response = await networkService.getMonitorsByTeamId({ - limit: null, // donot return any checks for the monitors - types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection - }); - setMonitors(response.data.data); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [user]); - - return [monitors, isLoading, networkError]; -}; - -export { useMonitorsFetch }; diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 83286277b..f5eff80ca 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -14,6 +14,7 @@ import { Autocomplete, Checkbox, Dialog, + ColorInput, } from "@/Components/v2/inputs"; import { useTheme } from "@mui/material/styles"; @@ -29,7 +30,6 @@ import type { StatusPageResponse } from "@/Types/StatusPage"; import timezones from "@/Utils/timezones.json"; import { useNavigate, useParams } from "react-router-dom"; import axios from "axios"; -import { MuiColorInput } from "mui-color-input"; import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls"; interface TimezoneOption { @@ -393,11 +393,11 @@ const CreateStatusPage = () => { name="color" control={control} render={({ field }) => ( - )} /> diff --git a/client/src/Pages/StatusPage/Create/old.jsx b/client/src/Pages/StatusPage/Create/old.jsx deleted file mode 100644 index c5aee4b0a..000000000 --- a/client/src/Pages/StatusPage/Create/old.jsx +++ /dev/null @@ -1,311 +0,0 @@ -// Components -import { Stack, Button, Typography } from "@mui/material"; -import Tabs from "./Components/Tabs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -//Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect, useRef, useCallback } from "react"; -import { statusPageValidation } from "../../../Validation/validation.js"; -import { buildErrors } from "../../../Validation/error.js"; -import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx"; -import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useNavigate } from "react-router-dom"; -import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; -import { useParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -//Constants -const ERROR_TAB_MAPPING = [ - ["companyName", "url", "timezone", "color", "isPublished", "logo"], - ["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"], -]; - -const CreateStatusPage = () => { - const { url } = useParams(); - //Local state - const [tab, setTab] = useState(0); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const [form, setForm] = useState({ - isPublished: false, - companyName: "", - url: url ?? Math.floor(Math.random() * 1000000).toFixed(0), - logo: undefined, - timezone: "America/Toronto", - color: "#4169E1", - type: "uptime", - monitors: [], - showCharts: true, - showUptimePercentage: true, - showAdminLoginLink: false, - }); - const [errors, setErrors] = useState({}); - const [selectedMonitors, setSelectedMonitors] = useState([]); - // Refs - const intervalRef = useRef(null); - - // Setup - const isCreate = typeof url === "undefined"; - - //Utils - const theme = useTheme(); - const [monitors, isLoading, networkError] = useMonitorsFetch(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const [createStatusPage] = useCreateStatusPage(isCreate); - const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] = - useStatusPageFetch(isCreate, url); - // const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); - - // Handlers - const handleFormChange = (e) => { - let { type, name, value, checked } = e.target; - // Handle errors - const { error } = statusPageValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, name, error); - }); - - //Handle checkbox - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [name]: checked, - })); - return; - } - - // Handle other inputs - setForm((prev) => ({ - ...prev, - [name]: value, - })); - }; - - const handleImageChange = useCallback((fileObj) => { - if (!fileObj || !fileObj.file) return; - - setForm((prev) => ({ - ...prev, - logo: { - src: fileObj.src, - name: fileObj.name, - type: fileObj.file.type, - size: fileObj.file.size, - }, - })); - - intervalRef.current = setInterval(() => { - const buffer = 12; - setProgress((prev) => { - if (prev.value + buffer >= 100) { - clearInterval(intervalRef.current); - return { value: 100, isLoading: false }; - } - return { ...prev, value: prev.value + buffer }; - }); - }, 120); - }, []); - - const removeLogo = () => { - setForm((prev) => ({ - ...prev, - logo: undefined, - })); - // interrupt interval if image upload is canceled prior to completing the process - clearInterval(intervalRef.current); - setProgress({ value: 0, isLoading: false }); - }; - - /** - * Handle status page deletion with optimistic UI update - * Immediately navigates away without waiting for the deletion to complete - * to prevent unnecessary network requests for the deleted page - */ - const handleDelete = async () => { - setIsDeleteOpen(false); - // Start deletion process but don't wait for it - // deleteStatusPage(); - // Immediately navigate away to prevent additional fetches for the deleted page - navigate("/status"); - }; - - const handleSubmit = async () => { - let toSubmit = { - ...form, - logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null }, - }; - - const { error } = statusPageValidation.validate(toSubmit, { - abortEarly: false, - }); - - if (typeof error === "undefined") { - const success = await createStatusPage({ form, id: statusPage?.id }); - if (success) { - createToast({ - body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"), - }); - navigate(`/status/uptime/${form.url}`); - } - return; - } - - const newErrors = {}; - error?.details?.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors((prev) => ({ ...prev, ...newErrors })); - const errorTabs = Object.keys(newErrors).map((err) => { - return ERROR_TAB_MAPPING.findIndex((tab) => tab.includes(err)); - }); - - // If there's an error in the current tab, don't change the tab - if (errorTabs.some((errorTab) => errorTab === tab)) { - return; - } - - // If we get -1, there's an unknown error - if (errorTabs[0] === -1) { - createToast({ body: t("common.toasts.unknownError") }); - return; - } - - // Otherwise go to tab with error - setTab(errorTabs[0]); - }; - - // If we are configuring, populate fields - useEffect(() => { - if (isCreate) return; - if (typeof statusPage === "undefined") { - return; - } - - let newLogo = undefined; - if (statusPage.logo && Object.keys(statusPage.logo).length > 0) { - newLogo = { - src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`, - name: "logo", - type: statusPage.logo.contentType, - size: null, - }; - } - setForm((prev) => { - return { - ...prev, - companyName: statusPage?.companyName, - isPublished: statusPage?.isPublished, - timezone: statusPage?.timezone, - monitors: statusPageMonitors.map((monitor) => monitor.id), - color: statusPage?.color, - logo: newLogo, - showCharts: statusPage?.showCharts ?? true, - showUptimePercentage: statusPage?.showUptimePercentage ?? true, - showAdminLoginLink: statusPage?.showAdminLoginLink ?? false, - }; - }); - setSelectedMonitors(statusPageMonitors); - }, [isCreate, statusPage, statusPageMonitors]); - - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (isLoading) return ; - - // Load fields - return ( - - - {!isCreate && ( - - - setIsDeleteOpen(false)} - open={isDeleteOpen} - confirmationButtonLabel={t("deleteStatusPageConfirm")} - description={t("deleteStatusPageDescription")} - // isLoading={isDeleting || statusPageIsLoading} - /> - - )} - - - - - - ); -}; - -export default CreateStatusPage; diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx deleted file mode 100644 index 73b9a8a5f..000000000 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../Utils/NetworkService.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; - -/** - * Hook for deleting a status page with optimistic UI update - * @param {Function} fetchStatusPage - Function to fetch status page data - * @param {string} url - URL of the status page - * @returns {Array} - [deleteStatusPage function, isLoading state] - */ -const useStatusPageDelete = (fetchStatusPage, url) => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - /** - * Delete a status page with optimistic UI update - * @returns {Promise} - Success status - */ - const deleteStatusPage = async () => { - // We don't need to call fetchStatusPage after deletion - // This prevents the 404 error when trying to fetch a deleted status page - try { - setIsLoading(true); - await networkService.deleteStatusPage({ url }); - createToast({ - body: t("statusPage.deleteSuccess", "Status page deleted successfully"), - }); - return true; - } catch (error) { - createToast({ - body: t("statusPage.deleteFailed", "Failed to delete status page"), - }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [deleteStatusPage, isLoading]; -}; - -export { useStatusPageDelete }; diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx deleted file mode 100644 index 7db6b6c5e..000000000 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; - -const useStatusPageFetch = (isCreate = false, url) => { - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPage, setStatusPage] = useState(undefined); - const [monitors, setMonitors] = useState(undefined); - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - const fetchStatusPage = useCallback(async () => { - try { - const response = await networkService.getStatusPageByUrl({ - url, - type: "uptime", - }); - if (!response?.data?.data) return; - const { statusPage, monitors } = response.data.data; - setStatusPage(statusPage); - - const monitorsWithPercentage = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - - setMonitors(monitorsWithPercentage); - } catch (error) { - // If there is a 404, status page is not found - if (error?.response?.status === 404) { - setStatusPage(undefined); - return; - } - createToast({ body: error.message }); - setNetworkError(true); - } finally { - setIsLoading(false); - } - }, [theme, getMonitorWithPercentage, url]); - - useEffect(() => { - if (isCreate === true) { - return; - } - fetchStatusPage(); - }, [isCreate, fetchStatusPage]); - - return [statusPage, monitors, isLoading, networkError, fetchStatusPage]; -}; - -export { useStatusPageFetch }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 04eca42b0..a26f1808e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -359,7 +359,6 @@ "errors": "Errors", "expectedValue": "Expected value", "failedToSendEmail": "Failed to send email", - "features": "Features", "FirstName": "First name", "frequency": "Frequency", "friendlyNameInput": "Friendly name", @@ -1044,7 +1043,6 @@ }, "rate": "Rate", "remove": "Remove", - "removeLogo": "Remove Logo", "repeat": "Repeat", "reset": "Reset", "response": "RESPONSE", @@ -1058,7 +1056,6 @@ }, "save": "Save", "selectAll": "Select all", - "settingsDisplayTimezone": "Display timezone", "settingsFailedToClearStats": "Failed to clear stats", "settingsFailedToDeleteMonitors": "Failed to delete all monitors", "settingsFailedToSave": "Failed to save settings", @@ -1151,25 +1148,18 @@ "settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}", "settingsTestEmailSuccess": "Test email sent successfully", "settingsTestEmailUnknownError": "Unknown error", - "showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page", - "showCharts": "Show charts", "shown": "Shown", - "showUptimePercentage": "Show uptime percentage", "starPromptDescription": "See the latest releases and help grow the community on GitHub", "starPromptTitle": "Star Checkmate", "startTime": "Start time", "state": "State", "status": "Status", - "statusBreadCrumbsDetails": "Details", - "statusBreadCrumbsStatusPages": "Status Pages", "statusCode": "Status code", "statusPage": { "contents": "Contents", - "createSuccess": "Status page created successfully", "deleteFailed": "Failed to delete status page", "deleteSuccess": "Status page deleted successfully", "generalSettings": "General settings", - "updateSuccess": "Status page updated successfully", "details": { "statusHeader": { "allUp": "All systems operational", @@ -1181,16 +1171,6 @@ "statusPageCreate": { "buttonSave": "Save" }, - "statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.", - "statusPageCreateAppearanceTitle": "Appearance", - "statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "statusPageCreateBasicInfoStatusPageAddress": "Your status page address", - "statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.", - "statusPageCreateSettings": "If your status page is ready, you can mark it as published.", - "statusPageCreateSettingsCheckboxLabel": "Published and visible to the public", - "statusPageCreateTabsContent": "Status page servers", - "statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.", - "statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page", "statusPageStatusServiceStatus": "Service status", "submit": "Submit", "SupportedFormats": "Supported formats", From 0fcceeb7a61fca812d9fc90d7001009832930ef7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 21:34:16 +0000 Subject: [PATCH 17/24] strings --- client/src/Pages/Incidents/Components/CardDetails.tsx | 1 - client/src/locales/en.json | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/Incidents/Components/CardDetails.tsx b/client/src/Pages/Incidents/Components/CardDetails.tsx index 07ab49073..577c777de 100644 --- a/client/src/Pages/Incidents/Components/CardDetails.tsx +++ b/client/src/Pages/Incidents/Components/CardDetails.tsx @@ -27,7 +27,6 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => { if (!incident) { return null; } - console.log(incident); return ( Date: Tue, 3 Feb 2026 21:40:17 +0000 Subject: [PATCH 18/24] actin menu --- client/src/Pages/StatusPage/Create/index.tsx | 2 +- .../Components/StatusPagesTable.tsx | 73 ++++++++++++++++++- .../Pages/StatusPage/StatusPages/index.tsx | 6 +- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index f5eff80ca..cbf470abb 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -145,7 +145,7 @@ const CreateStatusPage = () => { } if (result) { - navigate(`/status/uptime/${data.url}`); + navigate(`/status/${data.url}`); } }; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx index d832751de..ecc1b14bb 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx @@ -2,22 +2,78 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { Table, type Header, ValueLabel } from "@/Components/v2/design-elements"; +import { ActionsMenu, type ActionMenuItem } from "@/Components/v2/actions-menu"; +import { Dialog } from "@/Components/v2/inputs"; import { ExternalLink } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; +import { useDelete } from "@/Hooks/UseApi"; +import { useState } from "react"; +import { createToast } from "@/Utils/toastUtils"; import type { StatusPage } from "@/Types/StatusPage"; interface StatusPagesTableProps { data: StatusPage[]; + refetch: () => void; } -export const StatusPagesTable = ({ data }: StatusPagesTableProps) => { +export const StatusPagesTable = ({ data, refetch }: StatusPagesTableProps) => { const { t } = useTranslation(); const theme = useTheme(); const navigate = useNavigate(); + const { deleteFn, loading: isDeleting } = useDelete(); + + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [selectedStatusPage, setSelectedStatusPage] = useState(null); + + const handleDeleteClick = (statusPage: StatusPage) => { + setSelectedStatusPage(statusPage); + setIsDeleteDialogOpen(true); + }; + + const handleDeleteConfirm = async () => { + if (!selectedStatusPage) return; + const result = await deleteFn(`/status-page/${selectedStatusPage.id}`); + if (result) { + createToast({ body: t("pages.statusPages.deleteSuccess") }); + refetch(); + } + setIsDeleteDialogOpen(false); + setSelectedStatusPage(null); + }; + + const handleDeleteCancel = () => { + setIsDeleteDialogOpen(false); + setSelectedStatusPage(null); + }; + + const getActions = (row: StatusPage): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("common.buttons.configure"), + action: () => { + navigate(`/status/configure/${row.url}`); + }, + closeMenu: true, + }, + { + id: 2, + label: ( + + {t("common.buttons.delete")} + + ), + action: () => { + handleDeleteClick(row); + }, + closeMenu: true, + }, + ]; + }; const handleUrlClick = (e: React.MouseEvent, row: StatusPage) => { if (row.isPublished) { @@ -87,6 +143,13 @@ export const StatusPagesTable = ({ data }: StatusPagesTableProps) => { ); }, }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, ]; }; @@ -102,6 +165,14 @@ export const StatusPagesTable = ({ data }: StatusPagesTableProps) => { onRowClick={handleRowClick} emptyViewText={t("common.table.empty")} /> + ); }; diff --git a/client/src/Pages/StatusPage/StatusPages/index.tsx b/client/src/Pages/StatusPage/StatusPages/index.tsx index 0c891591b..e1031e9e6 100644 --- a/client/src/Pages/StatusPage/StatusPages/index.tsx +++ b/client/src/Pages/StatusPage/StatusPages/index.tsx @@ -13,6 +13,7 @@ const StatusPages = () => { data: statusPages, isLoading, error, + refetch, } = useGet("/status-page/team"); const isAdmin = useIsAdmin(); @@ -33,7 +34,10 @@ const StatusPages = () => { path="/status/create" isAdmin={isAdmin} /> - + ); }; From e3921beac45454e1650742345e3c45d53b9724c3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 21:53:34 +0000 Subject: [PATCH 19/24] hide unused features --- client/src/Pages/StatusPage/Create/index.tsx | 5 +- .../Components/StatusPagesTable.tsx | 48 +++---------------- .../Pages/StatusPage/StatusPages/index.tsx | 29 ++++++++++- 3 files changed, 35 insertions(+), 47 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index cbf470abb..8b4394f14 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -251,7 +251,6 @@ const CreateStatusPage = () => { onChange={(_, newValue) => { field.onChange(newValue.map((m: Monitor) => m.id)); }} - renderTags={() => null} fieldLabel={t( "pages.statusPages.form.monitors.option.monitors.label" )} @@ -424,7 +423,7 @@ const CreateStatusPage = () => { /> )} /> - ( @@ -457,7 +456,7 @@ const CreateStatusPage = () => { )} /> )} - /> + /> */} } /> diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx index ecc1b14bb..192f9c101 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable.tsx @@ -3,52 +3,26 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { Table, type Header, ValueLabel } from "@/Components/v2/design-elements"; import { ActionsMenu, type ActionMenuItem } from "@/Components/v2/actions-menu"; -import { Dialog } from "@/Components/v2/inputs"; import { ExternalLink } from "lucide-react"; import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; -import { useDelete } from "@/Hooks/UseApi"; -import { useState } from "react"; -import { createToast } from "@/Utils/toastUtils"; import type { StatusPage } from "@/Types/StatusPage"; interface StatusPagesTableProps { data: StatusPage[]; - refetch: () => void; + setSelectedStatusPage: (statusPage: StatusPage | null) => void; } -export const StatusPagesTable = ({ data, refetch }: StatusPagesTableProps) => { +export const StatusPagesTable = ({ + data, + setSelectedStatusPage, +}: StatusPagesTableProps) => { const { t } = useTranslation(); const theme = useTheme(); const navigate = useNavigate(); - const { deleteFn, loading: isDeleting } = useDelete(); - - const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [selectedStatusPage, setSelectedStatusPage] = useState(null); - - const handleDeleteClick = (statusPage: StatusPage) => { - setSelectedStatusPage(statusPage); - setIsDeleteDialogOpen(true); - }; - - const handleDeleteConfirm = async () => { - if (!selectedStatusPage) return; - const result = await deleteFn(`/status-page/${selectedStatusPage.id}`); - if (result) { - createToast({ body: t("pages.statusPages.deleteSuccess") }); - refetch(); - } - setIsDeleteDialogOpen(false); - setSelectedStatusPage(null); - }; - - const handleDeleteCancel = () => { - setIsDeleteDialogOpen(false); - setSelectedStatusPage(null); - }; const getActions = (row: StatusPage): ActionMenuItem[] => { return [ @@ -67,9 +41,7 @@ export const StatusPagesTable = ({ data, refetch }: StatusPagesTableProps) => { {t("common.buttons.delete")} ), - action: () => { - handleDeleteClick(row); - }, + action: () => setSelectedStatusPage(row), closeMenu: true, }, ]; @@ -165,14 +137,6 @@ export const StatusPagesTable = ({ data, refetch }: StatusPagesTableProps) => { onRowClick={handleRowClick} emptyViewText={t("common.table.empty")} /> - ); }; diff --git a/client/src/Pages/StatusPage/StatusPages/index.tsx b/client/src/Pages/StatusPage/StatusPages/index.tsx index e1031e9e6..3ad555512 100644 --- a/client/src/Pages/StatusPage/StatusPages/index.tsx +++ b/client/src/Pages/StatusPage/StatusPages/index.tsx @@ -1,6 +1,8 @@ +import { useState } from "react"; import { BasePageWithStates } from "@/Components/v2/design-elements"; +import { Dialog } from "@/Components/v2/inputs"; import { StatusPagesTable } from "./Components/StatusPagesTable"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, useDelete } from "@/Hooks/UseApi"; import type { StatusPage } from "@/Types/StatusPage"; import { useTranslation } from "react-i18next"; import { HeaderCreate } from "@/Components/v2/common"; @@ -16,8 +18,23 @@ const StatusPages = () => { refetch, } = useGet("/status-page/team"); + const { deleteFn, loading: isDeleting } = useDelete(); + const [selectedStatusPage, setSelectedStatusPage] = useState(null); + const isDialogOpen = Boolean(selectedStatusPage); + const isAdmin = useIsAdmin(); + const handleConfirm = async () => { + if (!selectedStatusPage) return; + await deleteFn(`/status-page/${selectedStatusPage.id}`); + setSelectedStatusPage(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedStatusPage(null); + }; + return ( { /> + ); From 0a408f8cfd255acf2786e5c8b52d202ec11cfa14 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 22:07:59 +0000 Subject: [PATCH 20/24] sorting --- client/src/Hooks/useStatusPageForm.ts | 2 +- server/src/controllers/statusPageController.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts index 40fff6fda..a079095cc 100644 --- a/client/src/Hooks/useStatusPageForm.ts +++ b/client/src/Hooks/useStatusPageForm.ts @@ -28,7 +28,7 @@ export const useStatusPageForm = ({ url: data?.url || generateDefaultUrl(), timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, color: data?.color || "#4169E1", - monitors: monitors?.map((m) => m.id) || [], + monitors: data?.monitors || [], isPublished: data?.isPublished ?? false, showCharts: data?.showCharts ?? true, showUptimePercentage: data?.showUptimePercentage ?? true, diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index 3271e39a1..f9cca161e 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -82,7 +82,15 @@ class StatusPageController { const showURL = settings.showURL; const monitors = await this.monitorsRepository.findByIds(statusPage.monitors); - const normalizedMonitors = monitors.map((monitor) => { + // Sort monitors according to the order in statusPage.monitors + const monitorOrder = new Map(statusPage.monitors.map((id, index) => [id, index])); + const sortedMonitors = [...monitors].sort((a, b) => { + const orderA = monitorOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER; + const orderB = monitorOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER; + return orderA - orderB; + }); + + const normalizedMonitors = sortedMonitors.map((monitor) => { const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100); if (!showURL) { const { url, port, secret, notifications, ...rest } = monitor; From 4080ac8c2c2d91952f1ae2e05a93f0f6945c106c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 22:40:26 +0000 Subject: [PATCH 21/24] format --- .../status-pages/MongoStatusPagesRepository.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index a19e738fd..e19676285 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -32,9 +32,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { return undefined; } // Convert Buffer to base64 string for JSON serialization - const base64Data = Buffer.isBuffer(logo.data) - ? logo.data.toString("base64") - : logo.data; + const base64Data = Buffer.isBuffer(logo.data) ? logo.data.toString("base64") : logo.data; return { data: base64Data, contentType: logo.contentType, @@ -105,7 +103,12 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { return this.mapDocuments(statusPages); }; - updateById = async (id: string, teamId: string, image: Express.Multer.File | undefined, patch: Partial & { removeLogo?: string }): Promise => { + updateById = async ( + id: string, + teamId: string, + image: Express.Multer.File | undefined, + patch: Partial & { removeLogo?: string } + ): Promise => { const { logo: _logo, removeLogo, ...restPatch } = patch; const updateData: StatusPageUpdateData = { ...restPatch }; if (image) { From 4e3e5801793b10a9549d86afd5ccb0886ba56a6e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 22:52:20 +0000 Subject: [PATCH 22/24] align logo --- client/src/Pages/StatusPage/Status/index.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 6cb9830b9..78bee43c2 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -4,6 +4,7 @@ import { MonitorsList } from "@/Pages/StatusPage/Status/Components/MonitorsList" import Typography from "@mui/material/Typography"; import { Link } from "react-router-dom"; import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; @@ -64,6 +65,10 @@ const StatusPageView = () => { sx.paddingRight = "20vw"; } + const logoSrc = statusPage.logo?.data + ? `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}` + : null; + return ( { statusPage={statusPage} isPublic={isPublic} /> + {logoSrc && ( + + )} {t("statusPageStatusServiceStatus")} Date: Tue, 3 Feb 2026 22:58:09 +0000 Subject: [PATCH 23/24] responsive --- .../Pages/StatusPage/Status/Components/MonitorsList.tsx | 4 ++-- client/src/Pages/StatusPage/Status/index.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index fc12704a9..04060673a 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -96,8 +96,8 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { {chartType === "histogram" ? ( ) : ( diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 78bee43c2..1b509507e 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -6,7 +6,7 @@ import { Link } from "react-router-dom"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import { useTheme } from "@mui/material"; +import { useMediaQuery, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; import { useLocation, useParams } from "react-router-dom"; @@ -20,6 +20,7 @@ const StatusPageView = () => { const { url } = useParams(); const isAdmin = useIsAdmin(); const location = useLocation(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isPublic = location.pathname.startsWith("/status/public"); const apiUrl = url ? `/status-page/${url}?type=uptime` : null; @@ -61,8 +62,8 @@ const StatusPageView = () => { let sx: React.CSSProperties = {}; if (isPublic) { sx.paddingTop = theme.spacing(20); - sx.paddingLeft = "20vw"; - sx.paddingRight = "20vw"; + sx.paddingLeft = isSmall ? "5vw" : "20vw"; + sx.paddingRight = isSmall ? "5vw" : "20vw"; } const logoSrc = statusPage.logo?.data From 667dc55fc89437d3627228a68fe4072e9e00e076 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 23:30:23 +0000 Subject: [PATCH 24/24] types --- .../v2/common/charts/HeatmapResponseTimeTooltip.tsx | 10 ++++------ client/src/Pages/Settings/DummyChart.tsx | 4 +++- client/src/Types/Check.ts | 4 +++- client/src/Types/Monitor.ts | 3 +-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx index ec9ad4d26..c046e05b5 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx +++ b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx @@ -7,6 +7,7 @@ import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import { useTranslation } from "react-i18next"; + type HeatmapCheck = | CheckSnapshot | { status: "placeholder"; responseTime: 0; createdAt: "" }; @@ -55,12 +56,9 @@ export const HeatmapResponseTimeTooltip = ({ {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} - {check?.responseTime && ( - - {t("common.labels.responseTime")}: {check.responseTime.toFixed()} ms - - )} - + + {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms + Status:{" "} diff --git a/client/src/Pages/Settings/DummyChart.tsx b/client/src/Pages/Settings/DummyChart.tsx index 39323522e..df7cd5603 100644 --- a/client/src/Pages/Settings/DummyChart.tsx +++ b/client/src/Pages/Settings/DummyChart.tsx @@ -12,11 +12,13 @@ const generateDummyChecks = (): CheckSnapshot[] => { const checks: CheckSnapshot[] = []; for (let i = 0; i < 25; i++) { const isUp = Math.random() > 0.1; + const responseTime = Math.floor(Math.random() * 80) + 20; checks.push({ id: `dummy-${i}`, status: isUp, statusCode: isUp ? 200 : 500, - responseTime: Math.floor(Math.random() * 80) + 20, + responseTime: responseTime, + originalResponseTime: responseTime, message: "", createdAt: new Date(Date.now() - i * 60000).toISOString(), }); diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 9b903207c..f204ab2ca 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -237,7 +237,9 @@ export interface ChecksSummary { export type CheckSnapshot = Omit< Check, "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt" ->; +> & { + originalResponseTime: number; +}; export interface HasResponseTime { responseTime: number; diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 02d4ccf2d..2a0272fdc 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,5 +1,4 @@ -import type { GroupedCheck } from "@/Types/Check"; -import type { CheckSnapshot } from "@/Types/Check"; +import type { GroupedCheck, CheckSnapshot } from "@/Types/Check"; export type MonitorStatus = boolean | undefined; export const MonitorTypes = [