From 6bc8a521c9d906093177d0c156ef7364b22cc802 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 3 Feb 2026 19:24:50 +0000 Subject: [PATCH] logo --- client/src/Pages/StatusPage/Create/index.tsx | 108 +++++++++++++----- .../Status/Components/MonitorsList.tsx | 2 + .../Status/Components/StatusBar.tsx | 18 ++- server/src/db/models/StatusPage.ts | 8 +- .../MongoStatusPagesRepository.ts | 21 ++-- server/src/types/statusPage.ts | 5 + 6 files changed, 109 insertions(+), 53 deletions(-) diff --git a/client/src/Pages/StatusPage/Create/index.tsx b/client/src/Pages/StatusPage/Create/index.tsx index 104ce6476..9190ad7f6 100644 --- a/client/src/Pages/StatusPage/Create/index.tsx +++ b/client/src/Pages/StatusPage/Create/index.tsx @@ -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(isCreate ? null : `/status-page/${url}?type=uptime`); const { data: monitorsResponse } = useGet( "/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 Loading...; + } + return ( { title={t("pages.statusPages.form.basicInfo.title")} subtitle={t("pages.statusPages.form.basicInfo.description")} rightContent={ - + { 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) => ( )} - renderTags={() => null} /> {selectedMonitors.length > 0 && ( diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index d559e7c82..fc12704a9 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -96,6 +96,8 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { {chartType === "histogram" ? ( ) : ( diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx index c5aca4754..8b3343249 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx @@ -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 = ; - } - - 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 { diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index 42d11bc46..56c5ee299 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -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( +const logoSchema = new Schema( { data: { type: Buffer }, contentType: { type: String }, diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index d5461bef3..a8241bcb6 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -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): Promise => { + 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, }); diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts index 64a5ac50b..b11cf53d6 100644 --- a/server/src/types/statusPage.ts +++ b/server/src/types/statusPage.ts @@ -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; }