From ee04ac2b9857e459fb5b023c2e4fc15427e35003 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 26 Nov 2025 23:57:40 +0000 Subject: [PATCH] delete all --- .../common/HistogramResponseTime.tsx | 6 +- client/src/pages/settings/Settings.tsx | 64 +++++++++++++++++-- client/src/validation/zod.ts | 2 +- server/src/controllers/MonitorController.ts | 27 ++++++++ server/src/routes/monitors.ts | 12 +++- .../src/services/business/MonitorService.ts | 10 +++ server/src/validation/index.ts | 2 +- 7 files changed, 115 insertions(+), 8 deletions(-) diff --git a/client/src/components/common/HistogramResponseTime.tsx b/client/src/components/common/HistogramResponseTime.tsx index 83526fe5c..553735132 100644 --- a/client/src/components/common/HistogramResponseTime.tsx +++ b/client/src/components/common/HistogramResponseTime.tsx @@ -37,6 +37,8 @@ export const HistogramResponseTime = ({ data = normalized; } + console.log(data); + const chartHeight = typeof height === "number" ? `${height}px` : height; const gridGap = gap ?? theme.spacing(0.5); @@ -85,7 +87,9 @@ export const HistogramResponseTime = ({ ); return isPlaceholder ? ( - {bar} + + {bar} + ) : ( {bar} diff --git a/client/src/pages/settings/Settings.tsx b/client/src/pages/settings/Settings.tsx index 18073f4dc..1e71f3692 100644 --- a/client/src/pages/settings/Settings.tsx +++ b/client/src/pages/settings/Settings.tsx @@ -1,23 +1,31 @@ import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; import { BasePage, ConfigBox } from "@/components/design-elements"; -import { AutoComplete, Select, LanguageSelector } from "@/components/inputs"; -import { RadioWithDescription } from "@/components/inputs/RadioInput"; +import { + AutoComplete, + Select, + LanguageSelector, + Button, + RadioWithDescription, + Dialog, +} from "@/components/inputs"; import MenuItem from "@mui/material/MenuItem"; import Typography from "@mui/material/Typography"; import { SettingsForm } from "@/pages/settings/SettingsForm"; +import DummyChart from "@/pages/settings/DummyChart"; import { useTheme } from "@mui/material/styles"; import { useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; import { setTimezone, setMode, setChartType } from "@/features/uiSlice"; import { useAppSelector, useAppDispatch } from "@/hooks/AppHooks"; import timezones from "@/utils/timezones.json"; import type { ITimeZone } from "@/types/timezone"; import type { SelectChangeEvent } from "@mui/material/Select"; -import { useGet, usePatch, usePost } from "@/hooks/UseApi"; +import { useGet, usePatch, usePost, useDelete } from "@/hooks/UseApi"; import type { ApiResponse } from "@/hooks/UseApi"; import { z } from "zod"; import { systemSettingsSchema } from "@/validation/zod"; -import DummyChart from "./DummyChart"; const SettingsPage = () => { type FormValues = z.infer; @@ -26,8 +34,13 @@ const SettingsPage = () => { const orgPermissions = user?.org?.permissions || []; const hasMaster = orgPermissions.includes("master"); + const hasDelete = + orgPermissions.includes("monitors.*") || + orgPermissions.includes("monitors.delete") || + hasMaster; const theme = useTheme(); + const { t } = useTranslation(); const dispatch = useAppDispatch(); const timezone = useAppSelector((state) => state.ui.timezone); const mode = useAppSelector((state) => state.ui.mode); @@ -42,6 +55,7 @@ const SettingsPage = () => { const { post, loading: posting } = usePost, any>(); const { patch, loading: patching } = usePatch, any>(); + const { deleteFn, loading: isDeleting } = useDelete(); const { response, loading } = useGet>( hasMaster ? "/settings" : null @@ -56,6 +70,15 @@ const SettingsPage = () => { const onTest = async (data: Partial) => { await post("/settings/test-transport", data); }; + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const handleOpen = () => setIsDialogOpen(true); + const handleCancel = () => setIsDialogOpen(false); + const handleConfirm = async () => { + await deleteFn("/monitors"); + setIsDialogOpen(false); + }; + return ( { } /> + {hasDelete && ( + + + + + + + + } + /> + )} + {hasMaster && ( { loading={posting || loading || patching} /> )} + ); }; diff --git a/client/src/validation/zod.ts b/client/src/validation/zod.ts index 16dcee953..25f1cd97b 100644 --- a/client/src/validation/zod.ts +++ b/client/src/validation/zod.ts @@ -21,7 +21,7 @@ export const registerSchema = z }); const urlRegex = - /^(https?:\/\/)?(localhost|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/; + /^(https?:\/\/)?(localhost|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/; const durationSchema = z .string() diff --git a/server/src/controllers/MonitorController.ts b/server/src/controllers/MonitorController.ts index 0bba80b48..0fb203a50 100644 --- a/server/src/controllers/MonitorController.ts +++ b/server/src/controllers/MonitorController.ts @@ -15,6 +15,11 @@ export interface IMonitorController { togglePause(req: Request, res: Response, next: NextFunction): Promise; update(req: Request, res: Response, next: NextFunction): Promise; delete(req: Request, res: Response, next: NextFunction): Promise; + deleteAllInOrg( + req: Request, + res: Response, + next: NextFunction + ): Promise; testNotifications( req: Request, res: Response, @@ -287,6 +292,28 @@ class MonitorController { } }; + deleteAllInOrg = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const orgId = tokenizedUser.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + await this.monitorService.deleteAllInOrg(orgId); + + res.status(200).json({ + message: "All monitors in organization deleted successfully", + }); + } catch (error) { + next(error); + } + }; + testNotifications = async ( req: Request, res: Response, diff --git a/server/src/routes/monitors.ts b/server/src/routes/monitors.ts index 077da7567..c5eb0500e 100644 --- a/server/src/routes/monitors.ts +++ b/server/src/routes/monitors.ts @@ -1,7 +1,10 @@ import { Router } from "express"; import { MonitorController } from "@/controllers/index.js"; import { verifyToken } from "@/middleware/VerifyToken.js"; -import { verifyTeamPermission } from "@/middleware/VerifyPermission.js"; +import { + verifyOrgPermission, + verifyTeamPermission, +} from "@/middleware/VerifyPermission.js"; import { addUserContext } from "@/middleware/AddUserContext.js"; import { PERMISSIONS } from "@/services/business/AuthService.js"; import { validateBody, validateQuery } from "@/middleware/validation.js"; @@ -109,6 +112,13 @@ class MonitorRoutes { verifyTeamPermission([PERMISSIONS.monitors.delete]), this.controller.delete ); + this.router.delete( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.monitors.delete]), + this.controller.deleteAllInOrg + ); }; getRouter() { diff --git a/server/src/services/business/MonitorService.ts b/server/src/services/business/MonitorService.ts index 6d9603c5b..ff9193a57 100644 --- a/server/src/services/business/MonitorService.ts +++ b/server/src/services/business/MonitorService.ts @@ -59,6 +59,7 @@ export interface IMonitorService { updateData: Partial ) => Promise; delete: (teamId: string, monitorId: string) => Promise; + deleteAllInOrg: (orgId: string) => Promise; export: (teamId: string) => Promise>>; import: ( orgId: string, @@ -665,6 +666,15 @@ class MonitorService implements IMonitorService { return true; } + async deleteAllInOrg(orgId: string) { + const montiors = await Monitor.find({ orgId }); + for (const monitor of montiors) { + await this.jobQueue.deleteJob(monitor); + } + const deleted = await Monitor.deleteMany({ orgId }); + return deleted.acknowledged; + } + export = async (teamId: string) => { return Monitor.find({ teamId }) .select("name url port type interval n secret -_id") diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts index a4bd753a9..e966aedc1 100644 --- a/server/src/validation/index.ts +++ b/server/src/validation/index.ts @@ -45,7 +45,7 @@ export const inviteSchema = z.object({ }); const urlRegex = - /^(https?:\/\/)?(localhost|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/; + /^(https?:\/\/)?(localhost|([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|[a-zA-Z0-9-]+|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/; export const monitorSchema = z .object({