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({