This commit is contained in:
Alex Holliday committed 2026-02-03 19:03:17 +00:00
1 parent c1d2041482
commit a1e6eaa9d0
4 files changed
+154 -25

No files matched your search

+51 -18
View File
@@ -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<Monitor[]>(
"/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 (
<Stack spacing={theme.spacing(4)}>
<Autocomplete
@@ -151,7 +194,6 @@ const CreateStatusPage = () => {
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 && (
<DragDropContext
onDragEnd={(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);
}}
>
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="monitors-list">
{(provided) => (
<Stack
{...provided.droppableProps}
ref={provided.innerRef}
flex={1}
width="100%"
>
{selectedMonitors.map((monitor, index) => (
<Draggable
@@ -301,7 +334,7 @@ const CreateStatusPage = () => {
justifyContent="flex-end"
>
<Button
// loading={isSubmitting}
loading={isSubmitting}
type="submit"
variant="contained"
color="primary"
+30 -1
View File
@@ -1,7 +1,9 @@
import { BasePage } from "@/Components/v2/design-elements";
import { BasePage, BaseFallback } from "@/Components/v2/design-elements";
import { StatusBar } from "@/Pages/StatusPage/Status/Components/StatusBar";
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 { useTheme } from "@mui/material";
import { useTranslation } from "react-i18next";
@@ -28,6 +30,33 @@ const StatusPageView = () => {
if (!statusPage) return null;
if (monitors?.length === 0) {
return (
<BasePage
loading={isLoading}
error={error}
breadcrumbOverride={isPublic ? [] : undefined}
>
<Stack alignItems={"center"}>
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.text.secondary}
>
{t("pages.statusPages.details.empty.title")}
</Typography>
{isAdmin && (
<Link to={`/status/uptime/configure/${url}`}>
{t("pages.statusPages.details.empty.addMonitor")}
</Link>
)}
</BaseFallback>
</Stack>
</BasePage>
);
}
let sx: React.CSSProperties = {};
if (isPublic) {
sx.paddingTop = theme.spacing(20);
+7 -1
View File
@@ -260,7 +260,13 @@ const Routes = () => {
<Route
path="status/uptime/configure/:url"
element={<CreateStatus />}
element={
<>
<ThemeProvider theme={v2theme}>
<CreateStatus />
</ThemeProvider>
</>
}
/>
<Route
+66 -5
View File
@@ -242,7 +242,8 @@
"clickToUpload": "Click to upload",
"dragAndDrop": "or drag and drop",
"supportedFormats": "Supported formats",
"maxSize": "Max size"
"maxSize": "Max size",
"orDragAndDrop": "or drag and drop"
},
"errors": {
"invalidFileType": "Invalid file type",
@@ -861,9 +862,14 @@
},
"statusPages": {
"fallback": {
"actionButton": "Create status page",
"checks": "No status pages found",
"title": "Status pages"
"title": "A status page is used to:",
"checks": [
"Communicate system status to users and stakeholders",
"Display real-time uptime information publicly",
"Build trust with transparent service monitoring",
"Reduce support requests during incidents"
],
"actionButton": "Let's create your first status page!"
},
"monitorsList": {
"chartTypeHeatmap": "Heatmap",
@@ -883,7 +889,62 @@
"published": "Published",
"unpublished": "Unpublished"
},
"title": "Status pages"
"title": "Status pages",
"details": {
"empty": {
"title": "There's nothing here yet",
"addMonitor": "Add a monitor to get started"
}
},
"form": {
"access": {
"title": "Access",
"description": "If your status page is ready, you can mark it as published.",
"option": {
"published": {
"name": "Published and visible to the public"
}
}
},
"basicInfo": {
"title": "Basic information",
"description": "Define company name and the subdomain that your status page points to.",
"option": {
"name": {
"label": "Company name",
"placeholder": "Acme Inc."
},
"url": {
"label": "Your status page address",
"placeholder": "my-status-page"
}
}
},
"monitors": {
"title": "Monitors",
"description": "Select the monitors to display on your status page.",
"noMonitors": "No monitors selected",
"option": {
"monitors": {
"label": "Select monitors",
"placeholder": "Search and select monitors..."
}
}
},
"timezone": {
"title": "Timezone",
"description": "Select the timezone that your status page will be displayed in.",
"option": {
"timezone": {
"label": "Timezone"
}
}
},
"appearance": {
"title": "Appearance",
"description": "Define the default look and feel of your public status page."
}
}
},
"uptime": {
"filters": {