monitor import/export
This commit is contained in:
1 parent
af6fede1b3
commit
6881be7555
12 files changed
+381
-154
No files matched your search
@@ -42,6 +42,11 @@ export const SettingsSwitch = () => {
|
||||
orgPermissions.includes("invite.write") ||
|
||||
orgPermissions.includes("*");
|
||||
|
||||
const hasMonitorRead =
|
||||
orgPermissions.includes("monitors.*") ||
|
||||
orgPermissions.includes("monitors.read") ||
|
||||
orgPermissions.includes("*");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Settings" placement="top">
|
||||
@@ -69,6 +74,11 @@ export const SettingsSwitch = () => {
|
||||
horizontal: "right",
|
||||
}}
|
||||
>
|
||||
{hasMonitorRead && (
|
||||
<MenuItem onClick={() => handleClick("export")}>
|
||||
<Typography>Import | Export</Typography>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => handleClick("profile")}>
|
||||
<Typography>Profile</Typography>
|
||||
</MenuItem>
|
||||
|
||||
@@ -182,3 +182,41 @@ export const useDelete = <R = any,>() => {
|
||||
|
||||
return { deleteFn, loading, error };
|
||||
};
|
||||
|
||||
export const useGetOnDemand = <R = any>() => {
|
||||
const currentTeamId = useAppSelector((state) => state.auth.selectedTeamId);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { toastError } = useToast();
|
||||
|
||||
const getFn = async (
|
||||
endpoint: string,
|
||||
config?: AxiosRequestConfig
|
||||
): Promise<ApiResponse<R> | null> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await get<ApiResponse<R>>(endpoint, {
|
||||
...config,
|
||||
headers: {
|
||||
...config?.headers,
|
||||
"x-team-id": currentTeamId,
|
||||
},
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
const errMsg =
|
||||
err?.response?.data?.message || err.message || "An error occurred";
|
||||
toastError(errMsg);
|
||||
setError(errMsg);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return { get: getFn, loading, error };
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { BasePage, ConfigBox } from "@/components/design-elements/index.js";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Button } from "@/components/inputs";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useGetOnDemand, usePost } from "@/hooks/UseApi";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useRef } from "react";
|
||||
|
||||
const ExportPage = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const { get, loading } = useGetOnDemand();
|
||||
const { post, loading: isPosting } = usePost();
|
||||
|
||||
const handleExport = async () => {
|
||||
const res = await get("/monitors/export");
|
||||
if (!res?.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(res.data, null, 2)], {
|
||||
type: "application/json",
|
||||
});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "monitors.json";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!fileInputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const input = fileInputRef.current;
|
||||
|
||||
const handleFileChange = async () => {
|
||||
if (!input.files || input.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = input.files[0];
|
||||
try {
|
||||
const text = await file.text();
|
||||
const json = JSON.parse(text);
|
||||
const res = await post("/monitors/import", { monitors: json });
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse JSON", error);
|
||||
} finally {
|
||||
input.value = "";
|
||||
input.removeEventListener("change", handleFileChange);
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener("change", handleFileChange);
|
||||
input.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage>
|
||||
<ConfigBox
|
||||
title="Export current team's monitors"
|
||||
subtitle="This will only export the basic monitor settings, not the check history, incidents history, or notifications"
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Typography>Click here to export your monitors to JSON</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleExport}
|
||||
>
|
||||
Export to JSON
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ConfigBox
|
||||
title="Import monitors"
|
||||
subtitle="Import monitors from a JSON file. Sample:"
|
||||
leftContent={
|
||||
<Box
|
||||
component={"pre"}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
p: 2,
|
||||
borderRadius: 1,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<code>
|
||||
{JSON.stringify(
|
||||
[
|
||||
{
|
||||
name: "Metrics",
|
||||
url: "http://localhost:59232/api/v1/metrics",
|
||||
type: "infrastructure",
|
||||
interval: 60000,
|
||||
n: 3,
|
||||
},
|
||||
{
|
||||
name: "Yahoo",
|
||||
url: "https://www.yahoo.com",
|
||||
type: "pagespeed",
|
||||
interval: 180000,
|
||||
n: 3,
|
||||
},
|
||||
],
|
||||
null,
|
||||
2
|
||||
)}
|
||||
</code>
|
||||
</Box>
|
||||
}
|
||||
rightContent={
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Typography>
|
||||
Click here to import your monitors from JSON
|
||||
</Typography>
|
||||
<Box>
|
||||
<Button
|
||||
loading={isPosting}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleImport}
|
||||
>
|
||||
Import from JSON
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="application/json"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportPage;
|
||||
@@ -291,29 +291,33 @@ export const SettingsForm = ({
|
||||
</Stack>
|
||||
)}
|
||||
/>
|
||||
<Stack
|
||||
gap={theme.spacing(4)}
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
onTest(formValues);
|
||||
}}
|
||||
>
|
||||
Test transport config
|
||||
</Button>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
>
|
||||
Save e-mail settings
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Stack gap={theme.spacing(4)} direction="row" justifyContent="flex-end">
|
||||
<Button
|
||||
loading={loading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
onTest(formValues);
|
||||
}}
|
||||
>
|
||||
Test transport config
|
||||
</Button>
|
||||
<Button
|
||||
loading={loading}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -45,6 +45,8 @@ import ResetPage from "@/pages/recovery/Reset";
|
||||
|
||||
import ProfilePage from "@/pages/profile/Profile";
|
||||
|
||||
import ExportPage from "@/pages/export/Export";
|
||||
|
||||
import RootLayout from "@/components/layouts/RootLayout";
|
||||
import { ProtectedRoute } from "@/components/protected-route";
|
||||
|
||||
@@ -132,6 +134,7 @@ const Router = () => {
|
||||
<Route path="diagnostics" element={<DiagnosticPage />} />
|
||||
|
||||
<Route path="profile" element={<ProfilePage />} />
|
||||
<Route path="export" element={<ExportPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
Generated
-130
@@ -4046,14 +4046,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
@@ -4182,27 +4174,6 @@
|
||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/babel-plugin-istanbul": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
|
||||
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"test/babel-8"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@istanbuljs/load-nyc-config": "^1.0.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"istanbul-lib-instrument": "^6.0.2",
|
||||
"test-exclude": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-current-node-syntax": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz",
|
||||
@@ -7104,32 +7075,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-haste-map": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
|
||||
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
"anymatch": "^3.1.3",
|
||||
"fb-watchman": "^2.0.2",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"jest-regex-util": "30.0.1",
|
||||
"jest-util": "30.2.0",
|
||||
"jest-worker": "30.2.0",
|
||||
"micromatch": "^4.0.8",
|
||||
"walker": "^1.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-leak-detector": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz",
|
||||
@@ -8549,52 +8494,6 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "30.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz",
|
||||
"integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@ungap/structured-clone": "^1.3.0",
|
||||
"jest-util": "30.2.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jest/node_modules/@jest/schemas": {
|
||||
"version": "29.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
|
||||
@@ -10135,20 +10034,6 @@
|
||||
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
@@ -10886,21 +10771,6 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/write-file-atomic": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
|
||||
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"imurmurhash": "^0.1.4",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -19,6 +19,8 @@ export interface IMonitorController {
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<void>;
|
||||
export(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||
import(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||
}
|
||||
class MonitorController {
|
||||
private monitorService: MonitorService;
|
||||
@@ -315,6 +317,62 @@ class MonitorController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
export = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userContext = req.user;
|
||||
if (!userContext) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const teamId = userContext.currentTeamId;
|
||||
if (!teamId) {
|
||||
throw new ApiError("No team ID", 400);
|
||||
}
|
||||
|
||||
const monitors = await this.monitorService.export(teamId);
|
||||
res.status(200).json({
|
||||
message: "Monitors exported successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
import = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const userContext = req.user;
|
||||
if (!userContext) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const teamId = userContext.currentTeamId;
|
||||
if (!teamId) {
|
||||
throw new ApiError("No team ID", 400);
|
||||
}
|
||||
|
||||
const orgId = userContext.orgId;
|
||||
if (!orgId) {
|
||||
throw new ApiError("No organization ID", 400);
|
||||
}
|
||||
|
||||
const result = await this.monitorService.import(
|
||||
orgId,
|
||||
teamId,
|
||||
userContext.sub,
|
||||
req.body
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: `Import request received. ${result.imported} successfully inserted, ${result.errors} failed`,
|
||||
data: result,
|
||||
});
|
||||
|
||||
try {
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorController;
|
||||
@@ -6,7 +6,7 @@ export type {
|
||||
ILighthouseResult,
|
||||
} from "./checks/Check.js";
|
||||
|
||||
export { Monitor, MonitorStatuses } from "./monitors/Monitor.js";
|
||||
export { Monitor, MonitorStatuses, MonitorTypes } from "./monitors/Monitor.js";
|
||||
export type { IMonitor } from "./monitors/Monitor.js";
|
||||
|
||||
export { MonitorStats } from "./monitors/MonitorStats.js";
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
monitorIdChecksQuerySchema,
|
||||
monitorPatchSchema,
|
||||
monitorAllEmbedChecksQuerySchema,
|
||||
monitorImportSchema,
|
||||
} from "@/validation/index.js";
|
||||
import { verify } from "node:crypto";
|
||||
|
||||
class MonitorRoutes {
|
||||
private router;
|
||||
@@ -41,6 +41,22 @@ class MonitorRoutes {
|
||||
this.controller.getAll
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
"/export",
|
||||
verifyToken,
|
||||
addUserContext,
|
||||
verifyTeamPermission([PERMISSIONS.monitors.read]),
|
||||
this.controller.export
|
||||
);
|
||||
this.router.post(
|
||||
"/import",
|
||||
verifyToken,
|
||||
addUserContext,
|
||||
validateBody(monitorImportSchema),
|
||||
verifyTeamPermission([PERMISSIONS.monitors.write]),
|
||||
this.controller.import
|
||||
);
|
||||
|
||||
this.router.get(
|
||||
"/:id/checks",
|
||||
verifyToken,
|
||||
|
||||
@@ -6,6 +6,14 @@ import { IJobQueue } from "@/services/infrastructure/JobQueue.js";
|
||||
import { MonitorWithChecksResponse } from "@/types/index.js";
|
||||
import { MonitorStatus, MonitorType } from "@/db/models/monitors/Monitor.js";
|
||||
|
||||
export interface IImportedMonitor {
|
||||
name: string;
|
||||
type: MonitorType;
|
||||
interval: number;
|
||||
url: string;
|
||||
n: number;
|
||||
}
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
|
||||
export interface IMonitorService {
|
||||
@@ -50,6 +58,13 @@ export interface IMonitorService {
|
||||
updateData: Partial<IMonitor>
|
||||
) => Promise<IMonitor>;
|
||||
delete: (teamId: string, monitorId: string) => Promise<boolean>;
|
||||
export: (teamId: string) => Promise<Array<Record<string, unknown>>>;
|
||||
import: (
|
||||
orgId: string,
|
||||
teamId: string,
|
||||
userId: string,
|
||||
data: { monitors: Array<IImportedMonitor> }
|
||||
) => Promise<{ imported: number; errors: number }>;
|
||||
}
|
||||
|
||||
class MonitorService implements IMonitorService {
|
||||
@@ -639,6 +654,43 @@ class MonitorService implements IMonitorService {
|
||||
await monitor.deleteOne();
|
||||
return true;
|
||||
}
|
||||
|
||||
export = async (teamId: string) => {
|
||||
return Monitor.find({ teamId })
|
||||
.select("name url type interval n secret -_id")
|
||||
.lean();
|
||||
};
|
||||
|
||||
import = async (
|
||||
orgId: string,
|
||||
teamId: string,
|
||||
userId: string,
|
||||
data: { monitors: Array<IImportedMonitor> }
|
||||
) => {
|
||||
let result = [];
|
||||
try {
|
||||
result = await Monitor.insertMany(
|
||||
data?.monitors.map((monitor) => ({
|
||||
...monitor,
|
||||
orgId: new mongoose.Types.ObjectId(orgId),
|
||||
teamId: new mongoose.Types.ObjectId(teamId),
|
||||
createdBy: new mongoose.Types.ObjectId(userId),
|
||||
updatedBy: new mongoose.Types.ObjectId(userId),
|
||||
}))
|
||||
);
|
||||
|
||||
for (const monitor of result) {
|
||||
await this.jobQueue.addJob(monitor);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Import error:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
imported: result.length,
|
||||
errors: data?.monitors?.length - result.length,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
@@ -96,9 +96,12 @@ class StatusService implements IStatusService {
|
||||
statusResponse: StatusResponse,
|
||||
statusChanged: boolean
|
||||
) => {
|
||||
const stats = await MonitorStats.findOne({ monitorId: monitor._id });
|
||||
let stats = await MonitorStats.findOne({ monitorId: monitor._id });
|
||||
if (!stats) {
|
||||
throw new ApiError("MonitorStats not found", 500);
|
||||
stats = await MonitorStats.create({
|
||||
monitorId: monitor._id,
|
||||
currentStreakStartedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Update check counts
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ChannelTypes,
|
||||
MonitorStatuses,
|
||||
MonitorTypes,
|
||||
ResolutionTypes,
|
||||
} from "@/db/models/index.js";
|
||||
import { z } from "zod";
|
||||
@@ -82,6 +83,25 @@ export const monitorSchema = z
|
||||
}
|
||||
});
|
||||
|
||||
export const monitorImportSchema = z.object({
|
||||
monitors: z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string().trim().min(1, "Monitor name is required"),
|
||||
url: z.string().trim().regex(urlRegex, "Invalid URL"),
|
||||
secret: z.string().trim().optional(),
|
||||
type: z.enum(MonitorTypes),
|
||||
interval: z.number().min(1, "Interval must be greater than 0"),
|
||||
n: z.number().min(1, "n must be at least 1"),
|
||||
})
|
||||
.strict()
|
||||
)
|
||||
.min(1, { message: "Provide at least one monitor" }),
|
||||
});
|
||||
|
||||
export type MonitorImportPayload = z.infer<typeof monitorImportSchema>;
|
||||
|
||||
export const monitorPatchSchema = monitorSchema
|
||||
.omit({
|
||||
url: true,
|
||||
|
||||
Reference in new issue
Block a user