logo
This commit is contained in:
1 parent
a1e6eaa9d0
commit
6bc8a521c9
6 files changed
+109
-53
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user