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 }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option} + + ))} + + + + + )} + + + ); +}; + +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,