Merge branch 'develop' into feat/dev-settings

This commit is contained in:
Alexander Holliday
2025-06-21 18:00:23 +08:00
committed by GitHub
8 changed files with 238 additions and 14 deletions
@@ -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>
);
};
+34
View File
@@ -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,
};
+7
View File
@@ -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;
}
}
+16
View File
@@ -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"
}
}