mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 02:29:35 -06:00
Merge branch 'develop' into feat/dev-settings
This commit is contained in:
116
client/src/Components/MonitorActions/index.jsx
Normal file
116
client/src/Components/MonitorActions/index.jsx
Normal file
@@ -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 (
|
||||
<React.Fragment>
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="accent"
|
||||
ref={anchorRef}
|
||||
aria-label="Monitor actions"
|
||||
disabled={isLoading || isExporting}
|
||||
>
|
||||
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
|
||||
<Button
|
||||
size="small"
|
||||
aria-controls={open ? "split-button-menu" : undefined}
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
aria-label="select monitor action"
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Popper
|
||||
sx={{ zIndex: 1 }}
|
||||
open={open}
|
||||
anchorEl={anchorRef.current}
|
||||
role={undefined}
|
||||
transition
|
||||
disablePortal
|
||||
>
|
||||
{({ TransitionProps, placement }) => (
|
||||
<Grow
|
||||
{...TransitionProps}
|
||||
style={{
|
||||
transformOrigin: placement === "bottom" ? "center top" : "center bottom",
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList
|
||||
id="split-button-menu"
|
||||
autoFocusItem
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
selected={index === selectedIndex}
|
||||
onClick={(event) => handleMenuItemClick(event, index)}
|
||||
>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</ClickAwayListener>
|
||||
</Paper>
|
||||
</Grow>
|
||||
)}
|
||||
</Popper>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default MonitorActions;
|
||||
@@ -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")}
|
||||
</Button>
|
||||
{bulkPath && (
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
navigate(`${bulkPath}`);
|
||||
}}
|
||||
>
|
||||
{t("bulkImport.title")}
|
||||
</Button>
|
||||
)}
|
||||
{bulkPath && <MonitorActions isLoading={isLoading} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user