fix: address PR feedback

This commit is contained in:
nikhil2004 committed 2026-09-22 03:33:27 +05:30
1 parent d5e887903e
commit de4c528d55
9 files changed
+95 -35

No files matched your search

@@ -348,6 +348,7 @@ export const BaseStatusPage = ({
url={statusPage.url}
monitorId={selectedMonitorId}
date={selectedDate}
timezone={bucketTimezone}
onClose={() => {
setSelectedMonitorId(null);
setSelectedDate(null);
@@ -7,6 +7,9 @@ import IconButton from "@mui/material/IconButton";
import { X as XIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useGet } from "@/Hooks/UseApi";
import { useTheme } from "@mui/material";
import { SPACING } from "@/Utils/Theme/constants";
import { formatDateWithTz } from "@/Utils/TimeUtils";
interface Incident {
id: string;
@@ -20,10 +23,12 @@ interface IncidentModalProps {
url: string;
monitorId: string | null;
date: string | null;
timezone: string;
onClose: () => void;
}
export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalProps) => {
export const IncidentModal = ({ url, monitorId, date, timezone, onClose }: IncidentModalProps) => {
const theme = useTheme();
const { t } = useTranslation();
const open = Boolean(monitorId && date);
@@ -43,7 +48,7 @@ export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalPr
<DialogTitle
sx={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}
>
{t("pages.statusPages.monitorsList.incidents.modalTitle", { date })}
{t("pages.statusPages.monitorsList.incidents.modalTitle", { date })} ({timezone})
<IconButton
onClick={onClose}
size="small"
@@ -54,21 +59,21 @@ export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalPr
</DialogTitle>
<DialogContent>
{isLoading ? (
<Typography mt={1}>
<Typography mt={SPACING.SM}>
{t("pages.statusPages.monitorsList.incidents.loading")}
</Typography>
) : data?.incidents && data.incidents.length > 0 ? (
<Stack
gap={2}
mt={1}
gap={SPACING.LG}
mt={SPACING.SM}
>
{data.incidents.map((incident) => (
<Stack
key={incident.id}
p={2}
p={SPACING.LG}
border={1}
borderColor="divider"
borderRadius={1}
borderRadius={theme.shape.borderRadius}
>
<Typography
variant="subtitle2"
@@ -79,18 +84,18 @@ export const IncidentModal = ({ url, monitorId, date, onClose }: IncidentModalPr
</Typography>
<Typography
variant="body2"
color="text.secondary"
color={theme.palette.text.secondary}
>
{new Date(incident.startTime).toLocaleTimeString()} -{" "}
{formatDateWithTz(incident.startTime, "h:mm A", timezone)} -{" "}
{incident.endTime
? new Date(incident.endTime).toLocaleTimeString()
? formatDateWithTz(incident.endTime, "h:mm A", timezone)
: t("pages.statusPages.monitorsList.incidents.ongoing")}
</Typography>
</Stack>
))}
</Stack>
) : (
<Typography mt={1}>
<Typography mt={SPACING.SM}>
{t("pages.statusPages.monitorsList.incidents.noIncidents")}
</Typography>
)}
@@ -2,7 +2,7 @@ import Tooltip from "@mui/material/Tooltip";
import Box from "@mui/material/Box";
import type { SxProps, Theme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { mergeSx } from "@/Utils/mergeSx";
import type {
ChartCell,
HeatCellKind,
@@ -20,7 +20,7 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props
return (
<Box
sx={mergeSx(containerSx, { gridTemplateColumns: `repeat(${cells.length}, 1fr)` })}
sx={[containerSx, { gridTemplateColumns: `repeat(${cells.length}, 1fr)` }]}
role="img"
aria-label={t("pages.statusPages.monitorsList.chart.heatmapAria")}
>
@@ -29,10 +29,10 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props
return (
<Box
key={cell.key}
sx={mergeSx(
sx={[
cellSx("empty"),
!!cell.date && !!onCellClick ? { cursor: "pointer" } : undefined
)}
]}
onClick={() => {
if (cell.date && onCellClick) {
onCellClick(cell.date);
@@ -49,10 +49,10 @@ export const ThemedHeatmap = ({ cells, containerSx, cellSx, onCellClick }: Props
placement="top"
>
<Box
sx={mergeSx(
sx={[
cellSx(cell.heatKind, cell.severity),
!!cell.date && !!onCellClick ? { cursor: "pointer" } : undefined
)}
]}
aria-label={cell.ariaLabel}
onClick={() => {
if (cell.date && onCellClick) {
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack";
import type { SxProps, Theme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { formatMs } from "@/Utils/TimeUtils";
import { mergeSx } from "@/Utils/mergeSx";
import type {
BarKind,
ChartCell,
@@ -42,17 +42,17 @@ export const ThemedHistogram = ({
return (
<Stack gap={statsGap}>
<Box
sx={mergeSx(containerSx, { gridTemplateColumns: `repeat(${cells.length}, 1fr)` })}
sx={[containerSx, { gridTemplateColumns: `repeat(${cells.length}, 1fr)` }]}
>
{cells.map((cell) => {
if (cell.barKind === "empty") {
return (
<Box
key={cell.key}
sx={mergeSx(
sx={[
barSx("empty", cell.heightPct),
!!cell.date && !!onCellClick ? { cursor: "pointer" } : undefined
)}
]}
onClick={() => {
if (cell.date && onCellClick) {
onCellClick(cell.date);
@@ -69,10 +69,10 @@ export const ThemedHistogram = ({
placement="top"
>
<Box
sx={mergeSx(
sx={[
barSx(cell.barKind, cell.heightPct, cell.severity),
!!cell.date && !!onCellClick ? { cursor: "pointer" } : undefined
)}
]}
aria-label={cell.ariaLabel}
onClick={() => {
if (cell.date && onCellClick) {
-8
View File
@@ -1,8 +0,0 @@
import type { SxProps, Theme } from "@mui/material/styles";
export const mergeSx = (
...styles: Array<SxProps<Theme> | false | null | undefined>
): SxProps<Theme> =>
styles
.flatMap((style) => (Array.isArray(style) ? style : [style]))
.filter(Boolean) as SxProps<Theme>;
@@ -82,6 +82,7 @@ IncidentSchema.index({ teamId: 1, startTime: -1 });
IncidentSchema.index({ status: 1, startTime: -1 });
IncidentSchema.index({ resolutionType: 1, status: 1 });
IncidentSchema.index({ resolvedBy: 1, status: 1 });
IncidentSchema.index({ monitorId: 1, startTime: -1 });
IncidentSchema.index({ createdAt: -1 });
const IncidentModel = model<IncidentDocument>("Incident", IncidentSchema);
@@ -20,6 +20,8 @@ export interface Incident {
updatedAt: string;
}
export type PublicIncident = Pick<Incident, "id" | "monitorId" | "status" | "startTime" | "endTime" | "resolutionType" | "message" | "statusCode" | "createdAt">;
export interface IncidentSummaryTopMonitor {
monitorId: string;
monitorName: string | null;
@@ -1,5 +1,5 @@
import { type IIncidentsRepository } from "@/domain/incidents/incident.repository.interface.js";
import type { Incident } from "@/domain/incidents/incident.type.js";
import type { Incident, PublicIncident } from "@/domain/incidents/incident.type.js";
import { type IStatusPagesRepository } from "@/domain/status-pages/status-page-repository.interface.js";
import { ISettingsService } from "@/domain/app-settings/app-settings.service.js";
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
@@ -29,7 +29,7 @@ export interface IStatusPageService {
getStatusPageByCustomDomain(customDomain: string): Promise<StatusPage>;
getStatusPagesByTeamId(teamId: string): Promise<StatusPage[]>;
getPublicStatusPagePayload(statusPage: StatusPage, requesterTeamId: string | undefined, range: StatusPageRange): Promise<PublicStatusPagePayload>;
getPublicMonitorIncidents(url: string, monitorId: string, date: string): Promise<Incident[]>;
getPublicMonitorIncidents(url: string, monitorId: string, date: string): Promise<PublicIncident[]>;
updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage>;
deleteStatusPage(statusPageId: string, teamId: string): Promise<StatusPage>;
@@ -105,6 +105,19 @@ export class StatusPageService implements IStatusPageService {
return base;
};
private toPublicIncident = (incident: Incident): PublicIncident => ({
id: incident.id,
monitorId: incident.monitorId,
status: incident.status,
startTime: incident.startTime,
endTime: incident.endTime,
resolutionType: incident.resolutionType,
message: incident.message ?? null,
statusCode: incident.statusCode ?? null,
createdAt: incident.createdAt,
});
createStatusPage = async (
userId: string,
teamId: string,
@@ -177,7 +190,7 @@ export class StatusPageService implements IStatusPageService {
};
};
getPublicMonitorIncidents = async (url: string, monitorId: string, date: string): Promise<Incident[]> => {
getPublicMonitorIncidents = async (url: string, monitorId: string, date: string): Promise<PublicIncident[]> => {
const statusPage = await this.getStatusPageByUrl(url);
// Ensure the status page is public
@@ -194,7 +207,8 @@ export class StatusPageService implements IStatusPageService {
const dateStart = dayjs.tz(date, tz).startOf("day").toDate();
const dateEnd = dayjs.tz(date, tz).endOf("day").toDate();
return this.incidentsRepository.findByMonitorIdAndDate(monitorId, statusPage.teamId, dateStart, dateEnd);
const incidents = await this.incidentsRepository.findByMonitorIdAndDate(monitorId, statusPage.teamId, dateStart, dateEnd);
return incidents.map(this.toPublicIncident);
};
updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial<StatusPage>): Promise<StatusPage> => {
@@ -431,6 +431,51 @@ describe("StatusPageService", () => {
});
});
describe("getPublicMonitorIncidents", () => {
const publishedPage = () => makeStatusPage({ isPublished: true, url: "my-status-page", monitors: ["mon-1"], teamId: "team-1" });
it("rejects an unpublished page (403)", async () => {
const { service, repo } = createService();
(repo.findByUrl as jest.Mock).mockResolvedValue(makeStatusPage({ isPublished: false }));
await expect(service.getPublicMonitorIncidents("my-status-page", "mon-1", "2026-08-01")).rejects.toMatchObject({ status: 403 });
});
it("rejects if monitor is not on the page (404)", async () => {
const { service, repo } = createService();
(repo.findByUrl as jest.Mock).mockResolvedValue(publishedPage());
await expect(service.getPublicMonitorIncidents("my-status-page", "mon-99", "2026-08-01")).rejects.toMatchObject({ status: 404 });
});
it("fetches and sanitizes incidents successfully", async () => {
const { service, repo, incidentsRepo } = createService();
(repo.findByUrl as jest.Mock).mockResolvedValue(publishedPage());
const mockIncident = {
id: "inc-1",
monitorId: "mon-1",
teamId: "team-1",
resolvedBy: "user-99",
status: false,
startTime: "2026-08-01T10:00:00Z",
endTime: "2026-08-01T11:00:00Z",
resolutionType: "manual" as const,
message: "Test incident",
createdAt: "2026-08-01T10:00:00Z",
updatedAt: "2026-08-01T10:00:00Z",
};
(incidentsRepo.findByMonitorIdAndDate as jest.Mock).mockResolvedValue([mockIncident]);
const results = await service.getPublicMonitorIncidents("my-status-page", "mon-1", "2026-08-01");
expect(incidentsRepo.findByMonitorIdAndDate).toHaveBeenCalled();
expect(results).toHaveLength(1);
expect(results[0]).toHaveProperty("id", "inc-1");
expect(results[0]).not.toHaveProperty("teamId");
expect(results[0]).not.toHaveProperty("resolvedBy");
expect(results[0]).not.toHaveProperty("updatedAt");
});
});
describe("deleteStatusPage", () => {
it("delegates to repository and returns deleted page", async () => {
const { service, repo } = createService(true);