This commit is contained in:
Alex Holliday committed 2026-02-03 19:24:50 +00:00
1 parent a1e6eaa9d0
commit 6bc8a521c9
6 files changed
+109 -53

No files matched your search

+79 -29
View File
@@ -20,10 +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, usePost } from "@/Hooks/UseApi";
import { useGet, usePost, usePut } from "@/Hooks/UseApi";
import type { Monitor } from "@/Types/Monitor";
import type { StatusPageResponse } from "@/Types/StatusPage";
import timezones from "@/Utils/timezones.json";
import { useNavigate } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import axios from "axios";
interface TimezoneOption {
@@ -35,9 +36,18 @@ const CreateStatusPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const { url } = useParams<{ url: string }>();
const isCreate = typeof url === "undefined";
const { schema, defaults } = useStatusPageForm();
const { post, loading: isSubmitting } = usePost();
const { post, loading: isSubmittingPost } = usePost();
const { put, loading: isSubmittingPut } = usePut();
const isSubmitting = isSubmittingPost || isSubmittingPut;
// Fetch existing status page data when configuring
const { data: statusPageData, isLoading: isLoadingStatusPage } =
useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
const { data: monitorsResponse } = useGet<Monitor[]>(
"/monitors/team?type=http&type=ping&type=port&type=docker"
@@ -51,9 +61,34 @@ const CreateStatusPage = () => {
const { control, reset, handleSubmit } = form;
// Populate form with existing data when configuring
useEffect(() => {
reset(defaults);
}, [defaults, reset]);
if (isCreate) {
reset(defaults);
return;
}
if (!statusPageData) return;
const { statusPage, monitors: statusPageMonitors } = statusPageData;
let logoData: { data: string; contentType: string } | null = null;
if (statusPage.logo && statusPage.logo.data) {
logoData = {
data: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
contentType: statusPage.logo.contentType,
};
}
reset({
isPublished: statusPage.isPublished ?? false,
companyName: statusPage.companyName ?? "",
url: statusPage.url ?? "",
timezone: statusPage.timezone ?? "America/Toronto",
monitors: statusPageMonitors?.map((m) => m.id) ?? [],
logo: logoData,
});
}, [isCreate, statusPageData, reset, defaults]);
const onError = (errors: any) => {
console.log(errors);
@@ -71,29 +106,42 @@ const CreateStatusPage = () => {
fd.append("monitors[]", monitorId);
});
// Handle logo upload
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:")) {
// Only upload if it's a blob URL (new upload), not base64 (existing)
if (data.logo.data.startsWith("blob:")) {
try {
const imageResult = await axios.get(data.logo.data, {
responseType: "blob",
});
fd.append("logo", imageResult.data);
URL.revokeObjectURL(data.logo.data);
} catch (e) {
console.error("Error fetching logo blob:", e);
}
} catch (e) {
console.error("Error fetching logo blob:", e);
}
}
const result = await post("/status-page", fd, {
headers: { "Content-Type": "multipart/form-data" },
});
let result;
if (isCreate) {
result = await post("/status-page", fd, {
headers: { "Content-Type": "multipart/form-data" },
});
} else {
result = await put(`/status-page/${url}`, fd, {
headers: { "Content-Type": "multipart/form-data" },
});
}
if (result) {
navigate(`/status/uptime/${data.url}`);
}
};
if (!isCreate && isLoadingStatusPage) {
return <BasePage>Loading...</BasePage>;
}
return (
<BasePage
component="form"
@@ -128,7 +176,7 @@ const CreateStatusPage = () => {
title={t("pages.statusPages.form.basicInfo.title")}
subtitle={t("pages.statusPages.form.basicInfo.description")}
rightContent={
<Stack spacing={theme.spacing(8)}>
<Stack spacing={theme.spacing(6)}>
<Controller
name="companyName"
control={control}
@@ -139,9 +187,8 @@ const CreateStatusPage = () => {
placeholder={t(
"pages.statusPages.form.basicInfo.option.name.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
helperText={fieldState.error?.message}
/>
)}
/>
@@ -155,9 +202,8 @@ const CreateStatusPage = () => {
placeholder={t(
"pages.statusPages.form.basicInfo.option.url.placeholder"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
helperText={fieldState.error?.message}
/>
)}
/>
@@ -174,7 +220,7 @@ const CreateStatusPage = () => {
render={({ field, fieldState }) => {
const selectedMonitors = field.value
.map((id: string) => monitors.find((m) => m.id === id))
.filter(Boolean) as Monitor[];
.filter((m): m is Monitor => m !== undefined);
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
@@ -191,23 +237,27 @@ const CreateStatusPage = () => {
options={monitors}
getOptionLabel={(option: Monitor) => option.name}
value={selectedMonitors}
onChange={(_, newValue: Monitor[]) => {
field.onChange(newValue.map((m) => m.id));
onChange={(_, newValue) => {
field.onChange(newValue.map((m: Monitor) => m.id));
}}
renderTags={() => null}
fieldLabel={t(
"pages.statusPages.form.monitors.option.monitors.label"
)}
renderInput={(params) => (
<TextField
{...params}
placeholder={t(
"pages.statusPages.form.monitors.option.monitors.placeholder"
)}
placeholder={
selectedMonitors.length === 0
? t(
"pages.statusPages.form.monitors.option.monitors.placeholder"
)
: ""
}
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
helperText={fieldState.error?.message}
/>
)}
renderTags={() => null}
/>
{selectedMonitors.length > 0 && (
<DragDropContext onDragEnd={handleDragEnd}>
@@ -96,6 +96,8 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
{chartType === "histogram" ? (
<HistogramResponseTime
height={100}
gap={theme.spacing(5)}
checks={monitor?.checks?.slice().reverse() ?? []}
/>
) : (
@@ -16,24 +16,20 @@ const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
monitorsStatus.color = theme.palette.success.main;
monitorsStatus.icon = <CircleCheck size={24} />;
}
if (monitors.every((monitor) => monitor.status === false)) {
return monitorsStatus;
} else if (monitors.every((monitor) => monitor.status === false)) {
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
monitorsStatus.color = theme.palette.error.main;
}
if (monitors.some((monitor) => monitor.status === false)) {
return monitorsStatus;
} else if (monitors.some((monitor) => monitor.status === false)) {
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
monitorsStatus.color = theme.palette.warning.main;
}
// Paused or unknown
if (monitors.some((monitor) => typeof monitor.status === "undefined")) {
return monitorsStatus;
} else {
monitorsStatus.msg = t("pages.statusPages.statusBar.unknown");
monitorsStatus.color = theme.palette.warning.main;
return monitorsStatus;
}
return monitorsStatus;
};
interface StatusBarProps {
+4 -4
View File
@@ -1,15 +1,15 @@
import { Schema, model, type Types } from "mongoose";
import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js";
import type { StatusPage, StatusPageLogoDocument } from "@/types/statusPage.js";
import { StatusPageTypes } from "@/types/statusPage.js";
type StatusPageDocumentBase = Omit<
StatusPage,
"id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "createdAt" | "updatedAt"
"id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "logo" | "createdAt" | "updatedAt"
> & {
monitors: Types.ObjectId[];
subMonitors: Types.ObjectId[];
originalMonitors?: Types.ObjectId[];
logo?: StatusPageLogo | null;
logo?: StatusPageLogoDocument | null;
};
interface StatusPageDocument extends StatusPageDocumentBase {
@@ -20,7 +20,7 @@ interface StatusPageDocument extends StatusPageDocumentBase {
updatedAt: Date;
}
const logoSchema = new Schema<StatusPageLogo & { data: Buffer }>(
const logoSchema = new Schema<StatusPageLogoDocument>(
{
data: { type: Buffer },
contentType: { type: String },
@@ -1,6 +1,6 @@
import { IStatusPagesRepository } from "@/repositories/index.js";
import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js";
import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js";
import type { StatusPage, StatusPageLogo, StatusPageLogoDocument } from "@/types/statusPage.js";
import mongoose from "mongoose";
import { AppError } from "@/utils/AppError.js";
@@ -23,12 +23,16 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
return values?.map((value) => this.toStringId(value)) ?? [];
};
private mapLogo = (logo?: StatusPageLogo | null): StatusPageLogo | undefined => {
private mapLogo = (logo?: StatusPageLogoDocument | null): StatusPageLogo | undefined => {
if (!logo) {
return undefined;
}
// Convert Buffer to base64 string for JSON serialization
const base64Data = Buffer.isBuffer(logo.data)
? logo.data.toString("base64")
: logo.data;
return {
data: logo.data,
data: base64Data,
contentType: logo.contentType,
};
};
@@ -72,7 +76,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
});
if (image) {
statusPage.logo = {
data: image.buffer,
data: image.buffer as Buffer,
contentType: image.mimetype,
};
}
@@ -97,16 +101,15 @@ class MongoStatusPagesRepository implements IStatusPagesRepository {
};
updateById = async (id: string, teamId: string, image: Express.Multer.File | undefined, patch: Partial<StatusPage>): Promise<StatusPage> => {
const updateData: any = { ...patch };
if (image) {
patch.logo = {
data: image.buffer,
updateData.logo = {
data: image.buffer as Buffer,
contentType: image.mimetype,
};
} else {
patch.logo = null;
}
const statusPage = await StatusPageModel.findOneAndUpdate({ teamId, _id: id }, patch, {
const statusPage = await StatusPageModel.findOneAndUpdate({ teamId, _id: id }, updateData, {
new: true,
});
+5
View File
@@ -2,6 +2,11 @@ export const StatusPageTypes = ["uptime"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
export interface StatusPageLogo {
data: string;
contentType: string;
}
export interface StatusPageLogoDocument {
data: Buffer;
contentType: string;
}