diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/MonitorActions/index.jsx
new file mode 100644
index 000000000..7086cb307
--- /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 options = ["Import Monitors", "Export Monitors"];
+
+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 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 a62123531..f2b6bbce4 100644
--- a/client/src/Components/MonitorCreateHeader/index.jsx
+++ b/client/src/Components/MonitorCreateHeader/index.jsx
@@ -3,13 +3,12 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
-import { useExportMonitors } from "../../Hooks/monitorHooks";
+import MonitorActions from "../MonitorActions";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
const { t } = useTranslation();
const theme = useTheme();
- const [exportMonitors, isExporting] = useExportMonitors();
// Use the provided label or fall back to the translated default
@@ -30,29 +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 d7c8e4287..5f303d26a 100644
--- a/client/src/Hooks/monitorHooks.js
+++ b/client/src/Hooks/monitorHooks.js
@@ -460,10 +460,10 @@ const useExportMonitors = () => {
const exportMonitors = async () => {
setIsLoading(true);
try {
- const blob = await networkService.exportMonitors();
+ const response = await networkService.exportMonitors();
// Create a download link
- const url = window.URL.createObjectURL(blob);
+ const url = window.URL.createObjectURL(response);
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "monitors.csv");
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 6451f93e8..1a6b2a516 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -1027,7 +1027,7 @@ class NetworkService {
const response = await this.axiosInstance.get("/monitors/export", {
responseType: "blob",
});
- return response.data;
+ return response;
}
}
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index d13d44f7f..9fde3e9dd 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -744,5 +744,10 @@
"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 5d71f9cb6..612fc328f 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -685,7 +685,7 @@ class MonitorController {
explain,
});
return res.success({
- msg: "OK", // TODO
+ msg: "OK",
data: result,
});
} catch (error) {
@@ -708,6 +708,12 @@ class MonitorController {
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,
@@ -721,10 +727,14 @@ class MonitorController {
const csv = pkg.unparse(csvData);
- res.setHeader("Content-Type", "text/csv");
- res.setHeader("Content-Disposition", "attachment; filename=monitors.csv");
-
- res.send(csv);
+ return res.success({
+ msg: this.stringService.monitorsExported,
+ data: csv,
+ headers: {
+ "Content-Type": "text/csv",
+ "Content-Disposition": "attachment; filename=monitors.csv",
+ },
+ });
} catch (error) {
next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV"));
}
diff --git a/server/middleware/responseHandler.js b/server/middleware/responseHandler.js
index a58e2e845..e051e3fa5 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,