fix: address PR feedback
This commit is contained in:
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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user