diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/MonitorActions/index.jsx
new file mode 100644
index 000000000..ac5d89a47
--- /dev/null
+++ b/client/src/Components/MonitorActions/index.jsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+import ClickAwayListener from "@mui/material/ClickAwayListener";
+import Grow from "@mui/material/Grow";
+import Paper from "@mui/material/Paper";
+import Popper from "@mui/material/Popper";
+import MenuItem from "@mui/material/MenuItem";
+import MenuList from "@mui/material/MenuList";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../Utils/toastUtils";
+import { useExportMonitors } from "../../Hooks/monitorHooks";
+
+const MonitorActions = ({ isLoading }) => {
+ const [open, setOpen] = React.useState(false);
+ const anchorRef = React.useRef(null);
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ const [exportMonitors, isExporting] = useExportMonitors();
+
+ const options = [t("monitorActions.import"), t("monitorActions.export")];
+
+ const handleClick = async () => {
+ if (selectedIndex === 0) {
+ // Import
+ navigate("/uptime/bulk-import");
+ } else {
+ // Export
+ const [success, error] = await exportMonitors();
+ if (!success) {
+ createToast({ body: error || t("export.failed") });
+ }
+ }
+ };
+
+ const handleMenuItemClick = (event, index) => {
+ setSelectedIndex(index);
+ setOpen(false);
+ };
+
+ const handleToggle = () => {
+ setOpen((prevOpen) => !prevOpen);
+ };
+
+ const handleClose = (event) => {
+ if (anchorRef.current && anchorRef.current.contains(event.target)) {
+ return;
+ }
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+ {({ TransitionProps, placement }) => (
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default MonitorActions;
diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx
index b630cb259..f2b6bbce4 100644
--- a/client/src/Components/MonitorCreateHeader/index.jsx
+++ b/client/src/Components/MonitorCreateHeader/index.jsx
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
+import MonitorActions from "../MonitorActions";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
@@ -28,18 +29,7 @@ const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath
>
{label || t("createNew")}
- {bulkPath && (
-
- )}
+ {bulkPath && }
);
};
diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js
index 7e46438e0..013e7fc25 100644
--- a/client/src/Hooks/monitorHooks.js
+++ b/client/src/Hooks/monitorHooks.js
@@ -453,6 +453,39 @@ const useCreateBulkMonitors = () => {
return [createBulkMonitors, isLoading];
};
+const useExportMonitors = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+
+ const exportMonitors = async () => {
+ setIsLoading(true);
+ try {
+ const response = await networkService.exportMonitors();
+
+ // Create a download link
+ const url = window.URL.createObjectURL(response.data);
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "monitors.csv");
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+
+ createToast({ body: t("export.success") });
+ return [true, null];
+ } catch (err) {
+ const errorMessage = err?.response?.data?.msg || err.message;
+ createToast({ body: errorMessage || t("export.failed") });
+ return [false, errorMessage];
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [exportMonitors, isLoading];
+};
+
export {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
@@ -469,4 +502,5 @@ export {
useDeleteAllMonitors,
useDeleteMonitorStats,
useCreateBulkMonitors,
+ useExportMonitors,
};
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 85feafbe6..d9a3853e1 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -1033,6 +1033,13 @@ class NetworkService {
async flushQueue() {
return this.axiosInstance.post(`/queue/flush`);
+
+
+ async exportMonitors() {
+ const response = await this.axiosInstance.get("/monitors/export", {
+ responseType: "blob",
+ });
+ return response;
}
}
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index e55acb530..d3a274a32 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -510,6 +510,13 @@
},
"auth": {
"common": {
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": "The password you provided does not match our records"
+ }
+ }
+ },
"navigation": {
"continue": "Continue",
"back": "Back"
@@ -779,5 +786,14 @@
"failedAtHeader": "Last failed at",
"failReasonHeader": "Fail reason"
}
+ "export": {
+ "title": "Export Monitors",
+ "success": "Monitors exported successfully!",
+ "failed": "Failed to export monitors"
+ },
+ "monitorActions": {
+ "title": "Export/Import",
+ "import": "Import Monitors",
+ "export": "Export Monitors"
}
}
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
index e1d087c74..d32ef463e 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -681,7 +681,7 @@ class MonitorController {
explain,
});
return res.success({
- msg: "OK", // TODO
+ msg: "OK",
data: result,
});
} catch (error) {
@@ -698,6 +698,42 @@ class MonitorController {
next(handleError(error, SERVICE_NAME, "seedDb"));
}
};
+
+ exportMonitorsToCSV = async (req, res, next) => {
+ try {
+ const { teamId } = req.user;
+
+ const monitors = await this.db.getMonitorsByTeamId({ teamId });
+ if (!monitors || monitors.length === 0) {
+ return res.success({
+ msg: this.stringService.noMonitorsFound,
+ data: null,
+ });
+ }
+ const csvData = monitors?.filteredMonitors?.map((monitor) => ({
+ name: monitor.name,
+ description: monitor.description,
+ type: monitor.type,
+ url: monitor.url,
+ interval: monitor.interval,
+ port: monitor.port,
+ ignoreTlsErrors: monitor.ignoreTlsErrors,
+ isActive: monitor.isActive,
+ }));
+
+ const csv = pkg.unparse(csvData);
+
+ return res.file({
+ data: csv,
+ headers: {
+ "Content-Type": "text/csv",
+ "Content-Disposition": "attachment; filename=monitors.csv",
+ },
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV"));
+ }
+ };
}
export default MonitorController;
diff --git a/server/middleware/responseHandler.js b/server/middleware/responseHandler.js
index a58e2e845..5169e7160 100755
--- a/server/middleware/responseHandler.js
+++ b/server/middleware/responseHandler.js
@@ -16,7 +16,12 @@ const responseHandler = (req, res, next) => {
* @param {*} [options.data=null] - Response data payload
* @returns {Object} Express response object
*/
- res.success = ({ status = 200, msg = "OK", data = null }) => {
+ res.success = ({ status = 200, msg = "OK", data = null, headers = {} }) => {
+ // Set custom headers if provided
+ Object.entries(headers).forEach(([key, value]) => {
+ res.set(key, value);
+ });
+
return res.status(status).json({
success: true,
msg: msg,
@@ -40,6 +45,21 @@ const responseHandler = (req, res, next) => {
data,
});
};
+
+ /**
+ * Sends a raw file response (for CSV, PDF, etc.)
+ * @param {Object} options
+ * @param {Buffer|string} options.data - The file content
+ * @param {Object} options.headers - Headers to set (e.g. Content-Type, Content-Disposition)
+ * @param {number} [options.status=200] - HTTP status code
+ */
+ res.file = ({ data, headers = {}, status = 200 }) => {
+ Object.entries(headers).forEach(([key, value]) => {
+ res.setHeader(key, value);
+ });
+ return res.status(status).send(data);
+ };
+
next();
};
diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js
index 0dabab407..2c6ae870e 100755
--- a/server/routes/monitorRoute.js
+++ b/server/routes/monitorRoute.js
@@ -17,6 +17,11 @@ class MonitorRoutes {
initRoutes() {
this.router.get("/", this.monitorController.getAllMonitors);
this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats);
+ this.router.get(
+ "/export",
+ isAllowed(["admin", "superadmin"]),
+ this.monitorController.exportMonitorsToCSV
+ );
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
this.router.get(
"/hardware/details/:monitorId",