Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ecfe6ad8 | ||
|
|
f4664eec16 | ||
|
|
56f601e34b | ||
|
|
1244ff6d93 | ||
|
|
c0ea804c98 | ||
|
|
c21cf29db6 | ||
|
|
3d0da70ce6 | ||
|
|
feb061240b | ||
|
|
a924227805 | ||
|
|
2144d3f8c0 | ||
|
|
fcd6786149 | ||
|
|
ea968ebf3c | ||
|
|
8a7b852974 | ||
|
|
c8d3e3b70c | ||
|
|
7a90a6f9f4 |
No files matched your search
+1
-3
@@ -5,6 +5,4 @@
|
|||||||
mongo
|
mongo
|
||||||
node_modules/
|
node_modules/
|
||||||
docs/architecture
|
docs/architecture
|
||||||
docs/reviews
|
docs/reviews
|
||||||
docs/todo
|
|
||||||
docs/frontend
|
|
||||||
@@ -42,15 +42,15 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||||
|
|
||||||
## User's guide
|
## User's guide
|
||||||
|
|
||||||
Usage instructions can be found [here](https://checkmate.so/docs).
|
Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
|
||||||
|
|
||||||
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||||
|
|
||||||
@@ -157,11 +157,12 @@ Here's how you can contribute:
|
|||||||
|
|
||||||
0. Star this repo :)
|
0. Star this repo :)
|
||||||
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
|
1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag.
|
||||||
2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||||
3. Open an issue if you believe you've encountered a bug.
|
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||||
4. Check for good-first-issue's if you are a newcomer.
|
4. Open an issue if you believe you've encountered a bug.
|
||||||
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
5. Check for good-first-issue's if you are a newcomer.
|
||||||
6. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
|
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||||
|
7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram).
|
||||||
|
|
||||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||||
@@ -174,3 +175,9 @@ Here's how you can contribute:
|
|||||||
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
|
||||||
|
|
||||||
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
|
If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
|
||||||
|
|
||||||
|
Also check other developer and contributor-friendly projects of BlueWave:
|
||||||
|
|
||||||
|
- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform.
|
||||||
|
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||||
|
- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners.
|
||||||
Generated
+104
-690
File diff suppressed because it is too large.
Load diff
+3
-6
@@ -13,7 +13,7 @@
|
|||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "prettier --write"
|
"**/*": "npm run format"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.13.3",
|
"@emotion/react": "^11.13.3",
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
"@mui/x-charts": "7.29.1",
|
"@mui/x-charts": "7.29.1",
|
||||||
"@mui/x-date-pickers": "7.29.4",
|
"@mui/x-date-pickers": "7.29.4",
|
||||||
"@reduxjs/toolkit": "2.7.0",
|
"@reduxjs/toolkit": "2.7.0",
|
||||||
"@types/maplibre-gl": "^1.13.2",
|
|
||||||
"axios": "^1.7.4",
|
"axios": "^1.7.4",
|
||||||
"dayjs": "1.11.13",
|
"dayjs": "1.11.13",
|
||||||
"flag-icons": "7.3.2",
|
"flag-icons": "7.3.2",
|
||||||
@@ -34,7 +33,6 @@
|
|||||||
"i18next": "25.4.2",
|
"i18next": "25.4.2",
|
||||||
"joi": "17.13.3",
|
"joi": "17.13.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"maplibre-gl": "^5.19.0",
|
|
||||||
"mui-color-input": "^7.0.0",
|
"mui-color-input": "^7.0.0",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"pretty-ms": "^9.3.0",
|
"pretty-ms": "^9.3.0",
|
||||||
@@ -43,7 +41,6 @@
|
|||||||
"react-hook-form": "^7.63.0",
|
"react-hook-form": "^7.63.0",
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"react-map-gl": "^8.1.0",
|
|
||||||
"react-redux": "9.2.0",
|
"react-redux": "9.2.0",
|
||||||
"react-router": "^6.23.0",
|
"react-router": "^6.23.0",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
@@ -52,7 +49,7 @@
|
|||||||
"redux-persist": "6.0.0",
|
"redux-persist": "6.0.0",
|
||||||
"swr": "^2.3.6",
|
"swr": "^2.3.6",
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.5.2",
|
"@types/node": "24.5.2",
|
||||||
@@ -66,6 +63,6 @@
|
|||||||
"lint-staged": "^16.2.7",
|
"lint-staged": "^16.2.7",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"vite": "6.4.1"
|
"vite": "6.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+23
-36
@@ -4,52 +4,39 @@ import "react-toastify/dist/ReactToastify.css";
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { ThemeProvider } from "@emotion/react";
|
import { ThemeProvider } from "@emotion/react";
|
||||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||||
import { SWRConfig } from "swr";
|
|
||||||
import { Routes } from "./Routes";
|
import { Routes } from "./Routes";
|
||||||
import AppLayout from "@/Components/layout/AppLayout";
|
import AppLayout from "@/Components/layout/AppLayout";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { lightTheme, darkTheme } from "@/Utils/Theme/Theme";
|
import { lightTheme, darkTheme } from "@/Utils/Theme/Theme";
|
||||||
|
|
||||||
const swrConfig = {
|
|
||||||
dedupingInterval: 5000,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
shouldRetryOnError: false,
|
|
||||||
errorRetryCount: 2,
|
|
||||||
errorRetryInterval: 1000,
|
|
||||||
focusThrottleInterval: 5000,
|
|
||||||
};
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const mode = useSelector((state: RootState) => state.ui.mode);
|
const mode = useSelector((state: RootState) => state.ui.mode);
|
||||||
const theme = mode === "light" ? lightTheme : darkTheme;
|
const theme = mode === "light" ? lightTheme : darkTheme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SWRConfig value={swrConfig}>
|
<ThemeProvider theme={theme}>
|
||||||
<ThemeProvider theme={theme}>
|
<CssBaseline />
|
||||||
<CssBaseline />
|
<GlobalStyles
|
||||||
<GlobalStyles
|
styles={{
|
||||||
styles={{
|
body: {
|
||||||
body: {
|
backgroundColor: theme.palette.background.default,
|
||||||
backgroundColor: theme.palette.background.default,
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
<AppLayout>
|
||||||
<AppLayout>
|
<Routes />
|
||||||
<Routes />
|
</AppLayout>
|
||||||
</AppLayout>
|
<ToastContainer
|
||||||
<ToastContainer
|
newestOnTop={true}
|
||||||
newestOnTop={true}
|
theme={mode}
|
||||||
theme={mode}
|
style={
|
||||||
style={
|
{
|
||||||
{
|
"--toastify-color-progress-light": "#7C8BA1",
|
||||||
"--toastify-color-progress-light": "#7C8BA1",
|
"--toastify-color-progress-dark": "#7C8BA1",
|
||||||
"--toastify-color-progress-dark": "#7C8BA1",
|
} as CSSProperties
|
||||||
} as CSSProperties
|
}
|
||||||
}
|
/>
|
||||||
/>
|
</ThemeProvider>
|
||||||
</ThemeProvider>
|
|
||||||
</SWRConfig>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const HeatmapResponseTime = ({
|
|||||||
|
|
||||||
if (!checks || checks.length === 0) return null;
|
if (!checks || checks.length === 0) return null;
|
||||||
|
|
||||||
const latestChecks = checks.slice(-25);
|
const latestChecks = checks.slice(-25).reverse();
|
||||||
|
|
||||||
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
let data: Array<CheckSnapshot | PlaceholderCheck>;
|
||||||
if (latestChecks.length !== 25) {
|
if (latestChecks.length !== 25) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { BaseBox } from ".";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import type { ResponsiveStyleValue } from "@mui/system";
|
import type { ResponsiveStyleValue } from "@mui/system";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
@@ -27,7 +26,7 @@ export const BaseChart = ({
|
|||||||
return (
|
return (
|
||||||
<BaseBox
|
<BaseBox
|
||||||
sx={{
|
sx={{
|
||||||
padding: padding ?? theme.spacing(LAYOUT.MD),
|
padding: padding ?? theme.spacing(8),
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: width,
|
width: width,
|
||||||
@@ -35,13 +34,13 @@ export const BaseChart = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(8)}
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
gap={theme.spacing(LAYOUT.XS)}
|
gap={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
{icon && (
|
{icon && (
|
||||||
<BaseBox
|
<BaseBox
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Stack from "@mui/material/Stack";
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Alert from "@mui/material/Alert";
|
import Alert from "@mui/material/Alert";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
|
||||||
import {
|
import {
|
||||||
ErrorFallback,
|
ErrorFallback,
|
||||||
EmptyFallback,
|
EmptyFallback,
|
||||||
@@ -92,7 +91,7 @@ export const BasePage = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
spacing={theme.spacing(LAYOUT.LG)}
|
spacing={theme.spacing(10)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
<Breadcrumb breadcrumbOverride={breadcrumbOverride} />
|
||||||
@@ -221,7 +220,7 @@ export const BaseAuthPage = ({
|
|||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
breadcrumbOverride={[]}
|
breadcrumbOverride={[]}
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(8)}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
justifyContent={"center"}
|
justifyContent={"center"}
|
||||||
minHeight="100vh"
|
minHeight="100vh"
|
||||||
@@ -230,7 +229,7 @@ export const BaseAuthPage = ({
|
|||||||
>
|
>
|
||||||
<HeaderAuthControls
|
<HeaderAuthControls
|
||||||
hideLogo
|
hideLogo
|
||||||
py={theme.spacing(LAYOUT.XS)}
|
py={theme.spacing(4)}
|
||||||
position={"absolute"}
|
position={"absolute"}
|
||||||
top={0}
|
top={0}
|
||||||
left={0}
|
left={0}
|
||||||
@@ -244,14 +243,14 @@ export const BaseAuthPage = ({
|
|||||||
<Stack alignItems={"center"}>
|
<Stack alignItems={"center"}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h1"
|
variant="h1"
|
||||||
mb={theme.spacing(SPACING.LG)}
|
mb={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h2">{subtitle}</Typography>
|
<Typography variant="h2">{subtitle}</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(8)}
|
||||||
width={{
|
width={{
|
||||||
xs: "80%",
|
xs: "80%",
|
||||||
md: "25%",
|
md: "25%",
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Stack from "@mui/material/Stack";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
|
|
||||||
export const BulletPointCheck = ({
|
export const BulletPointCheck = ({
|
||||||
text,
|
text,
|
||||||
@@ -22,7 +21,7 @@ export const BulletPointCheck = ({
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
gap={theme.spacing(LAYOUT.SM)}
|
gap={theme.spacing(6)}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Stack from "@mui/material/Stack";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
|
import { BulletPointCheck, SkeletonCard } from "@/Components/design-elements";
|
||||||
import { Button } from "@/Components/inputs";
|
import { Button } from "@/Components/inputs";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
|
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
@@ -28,7 +27,7 @@ export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => {
|
|||||||
lg: "50%",
|
lg: "50%",
|
||||||
xl: "40%",
|
xl: "40%",
|
||||||
}}
|
}}
|
||||||
padding={{ xs: theme.spacing(LAYOUT.MD), md: theme.spacing(LAYOUT.XXL) }}
|
padding={{ xs: theme.spacing(8), md: theme.spacing(16) }}
|
||||||
bgcolor={theme.palette.background.paper}
|
bgcolor={theme.palette.background.paper}
|
||||||
border={1}
|
border={1}
|
||||||
borderColor={theme.palette.divider}
|
borderColor={theme.palette.divider}
|
||||||
@@ -56,7 +55,7 @@ export const ErrorFallback = ({
|
|||||||
<BaseFallback>
|
<BaseFallback>
|
||||||
<Typography
|
<Typography
|
||||||
variant="h1"
|
variant="h1"
|
||||||
marginY={theme.spacing(LAYOUT.XS)}
|
marginY={theme.spacing(4)}
|
||||||
color={theme.palette.text.secondary}
|
color={theme.palette.text.secondary}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -82,7 +81,7 @@ export const EmptyFallback = ({
|
|||||||
return (
|
return (
|
||||||
<BaseFallback>
|
<BaseFallback>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(10)}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
@@ -90,7 +89,7 @@ export const EmptyFallback = ({
|
|||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: theme.spacing(SPACING.MD),
|
gap: theme.spacing(2),
|
||||||
maxWidth: "1100px",
|
maxWidth: "1100px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +134,7 @@ export const EmptyMonitorFallback = ({
|
|||||||
return (
|
return (
|
||||||
<BaseFallback>
|
<BaseFallback>
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(10)}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
@@ -148,7 +147,7 @@ export const EmptyMonitorFallback = ({
|
|||||||
<Stack
|
<Stack
|
||||||
sx={{
|
sx={{
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: theme.spacing(SPACING.MD),
|
gap: theme.spacing(2),
|
||||||
maxWidth: "1100px",
|
maxWidth: "1100px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Box from "@mui/material/Box";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
export const SplitBox = ({
|
export const SplitBox = ({
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
@@ -19,10 +18,10 @@ export const SplitBox = ({
|
|||||||
bgcolor={theme.palette.background.paper}
|
bgcolor={theme.palette.background.paper}
|
||||||
border={1}
|
border={1}
|
||||||
borderColor={theme.palette.divider}
|
borderColor={theme.palette.divider}
|
||||||
borderRadius={theme.shape.borderRadius}
|
borderRadius={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
padding={theme.spacing(LAYOUT.XXL)}
|
padding={theme.spacing(15)}
|
||||||
borderRight={isSmall ? 0 : 1}
|
borderRight={isSmall ? 0 : 1}
|
||||||
borderBottom={isSmall ? 1 : 0}
|
borderBottom={isSmall ? 1 : 0}
|
||||||
borderColor={theme.palette.divider}
|
borderColor={theme.palette.divider}
|
||||||
@@ -38,7 +37,7 @@ export const SplitBox = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
flex={1}
|
flex={1}
|
||||||
padding={theme.spacing(LAYOUT.XXL)}
|
padding={theme.spacing(15)}
|
||||||
>
|
>
|
||||||
{right}
|
{right}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -61,7 +60,7 @@ export const ConfigBox = ({
|
|||||||
return (
|
return (
|
||||||
<SplitBox
|
<SplitBox
|
||||||
left={
|
left={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.XS)}>
|
<Stack spacing={theme.spacing(4)}>
|
||||||
<Typography
|
<Typography
|
||||||
textTransform={"capitalize"}
|
textTransform={"capitalize"}
|
||||||
component="h2"
|
component="h2"
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import type { TablePaginationOwnProps } from "@mui/material/TablePagination";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState, Fragment, useEffect } from "react";
|
import { useState, Fragment, useEffect } from "react";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import type { SxProps, Theme } from "@mui/material/styles";
|
import type { SxProps, Theme } from "@mui/material/styles";
|
||||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
|
||||||
@@ -91,20 +90,20 @@ export function DataTable<
|
|||||||
// Return stack of cards for small screens
|
// Return stack of cards for small screens
|
||||||
if (isSmall && cardsOnSmallScreens) {
|
if (isSmall && cardsOnSmallScreens) {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(LAYOUT.XS)}>
|
<Stack spacing={theme.spacing(4)}>
|
||||||
{data.map((row) => {
|
{data.map((row) => {
|
||||||
const key = row.id || row._id || Math.random();
|
const key = row.id || row._id || Math.random();
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
onClick={() => (onRowClick ? onRowClick(row) : null)}
|
||||||
spacing={theme.spacing(LAYOUT.XS)}
|
spacing={theme.spacing(4)}
|
||||||
sx={{
|
sx={{
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
padding: theme.spacing(LAYOUT.XS),
|
padding: theme.spacing(4),
|
||||||
cursor: onRowClick ? "pointer" : "default",
|
cursor: onRowClick ? "pointer" : "default",
|
||||||
}}
|
}}
|
||||||
key={key}
|
key={key}
|
||||||
@@ -156,7 +155,7 @@ export function DataTable<
|
|||||||
<Table
|
<Table
|
||||||
sx={{
|
sx={{
|
||||||
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
|
||||||
paddingLeft: theme.spacing(LAYOUT.MD),
|
paddingLeft: theme.spacing(8),
|
||||||
},
|
},
|
||||||
"& .MuiTableCell-root": {
|
"& .MuiTableCell-root": {
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
@@ -169,13 +168,13 @@ export function DataTable<
|
|||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
padding: `${theme.spacing(SPACING.LG)} ${theme.spacing(LAYOUT.MD)}`,
|
padding: `${theme.spacing(2)} ${theme.spacing(8)}`,
|
||||||
fontSize: theme.typography.fontSize,
|
fontSize: theme.typography.fontSize,
|
||||||
},
|
},
|
||||||
"& :is(td)": {
|
"& :is(td)": {
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
padding: `${theme.spacing(LAYOUT.MD)} ${theme.spacing(LAYOUT.MD)}`,
|
padding: `${theme.spacing(7)} ${theme.spacing(8)}`,
|
||||||
fontSize: theme.typography.fontSize,
|
fontSize: theme.typography.fontSize,
|
||||||
overflowX: "hidden",
|
overflowX: "hidden",
|
||||||
},
|
},
|
||||||
@@ -515,7 +514,7 @@ export const Pagination = ({ ...props }: PaginationProps) => {
|
|||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 1fr",
|
gridTemplateColumns: "1fr 1fr",
|
||||||
gridAutoRows: "auto",
|
gridAutoRows: "auto",
|
||||||
rowGap: theme.spacing(SPACING.SM),
|
rowGap: theme.spacing(1),
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyItems: "center",
|
justifyItems: "center",
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
@@ -557,8 +556,8 @@ const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) =>
|
|||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
justifyContent={"center"}
|
justifyContent={"center"}
|
||||||
sx={{
|
sx={{
|
||||||
py: theme.spacing(LAYOUT.XL),
|
py: theme.spacing(12),
|
||||||
px: theme.spacing(LAYOUT.XS),
|
px: theme.spacing(4),
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
@@ -575,7 +574,7 @@ const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) =>
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
mb: theme.spacing(SPACING.LG),
|
mb: theme.spacing(2),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
@@ -11,26 +11,20 @@ import type { ResponsiveStyleValue } from "@mui/system";
|
|||||||
interface SliderInputProps extends SliderProps {
|
interface SliderInputProps extends SliderProps {
|
||||||
sx?: SliderProps["sx"];
|
sx?: SliderProps["sx"];
|
||||||
showValue?: boolean;
|
showValue?: boolean;
|
||||||
formatDisplayValue?: (value: number) => string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
|
export const SliderInput = forwardRef<HTMLSpanElement, SliderInputProps>(
|
||||||
({ sx, showValue = false, formatDisplayValue, ...props }, ref) => {
|
({ sx, showValue = false, ...props }, ref) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
|
const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : [];
|
||||||
|
|
||||||
const displayValue =
|
|
||||||
showValue && formatDisplayValue && typeof props.value === "number"
|
|
||||||
? formatDisplayValue(props.value)
|
|
||||||
: props.value;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(8)}
|
gap={theme.spacing(8)}
|
||||||
direction={"row"}
|
direction={"row"}
|
||||||
alignItems={"center"}
|
alignItems={"center"}
|
||||||
>
|
>
|
||||||
{showValue && <Typography>{displayValue}</Typography>}
|
{showValue && <Typography>{props.value}</Typography>}
|
||||||
<Slider
|
<Slider
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -69,21 +63,12 @@ interface SliderWithLabelProps extends SliderProps {
|
|||||||
fieldLabel?: string;
|
fieldLabel?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
showValue?: boolean;
|
showValue?: boolean;
|
||||||
formatDisplayValue?: (value: number) => string;
|
|
||||||
sliderMaxWidth?: ResponsiveStyleValue<number | string>;
|
sliderMaxWidth?: ResponsiveStyleValue<number | string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
|
export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>(
|
||||||
(
|
(
|
||||||
{
|
{ fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props },
|
||||||
fieldLabel,
|
|
||||||
required,
|
|
||||||
showValue = true,
|
|
||||||
formatDisplayValue,
|
|
||||||
value,
|
|
||||||
sliderMaxWidth = "100%",
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -97,7 +82,6 @@ export const SliderWithLabel = forwardRef<HTMLSpanElement, SliderWithLabelProps>
|
|||||||
<SliderInput
|
<SliderInput
|
||||||
{...props}
|
{...props}
|
||||||
showValue={showValue}
|
showValue={showValue}
|
||||||
formatDisplayValue={formatDisplayValue}
|
|
||||||
value={value}
|
value={value}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import BackgroundSVG from "@/assets/Images/background.svg";
|
||||||
|
import type { RootState } from "@/Types/state";
|
||||||
import { OfflineBanner } from "@/Components/design-elements";
|
import { OfflineBanner } from "@/Components/design-elements";
|
||||||
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
|
import { setServerUnreachableCallback, get } from "@/Utils/ApiClient";
|
||||||
|
|
||||||
@@ -10,6 +13,7 @@ interface AppLayoutProps {
|
|||||||
|
|
||||||
const AppLayout = ({ children }: AppLayoutProps) => {
|
const AppLayout = ({ children }: AppLayoutProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const mode = useSelector((state: RootState) => state.ui.mode);
|
||||||
const [serverUnreachable, setServerUnreachable] = useState(false);
|
const [serverUnreachable, setServerUnreachable] = useState(false);
|
||||||
const retryIntervalRef = useRef<number | null>(null);
|
const retryIntervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
@@ -43,6 +47,10 @@ const AppLayout = ({ children }: AppLayoutProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
|
backgroundImage: mode === "dark" ? `url("${BackgroundSVG}")` : "none",
|
||||||
|
backgroundSize: "100% 100%",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<OfflineBanner visible={serverUnreachable} />
|
<OfflineBanner visible={serverUnreachable} />
|
||||||
|
|||||||
@@ -5,18 +5,8 @@ import useMediaQuery from "@mui/material/useMediaQuery";
|
|||||||
|
|
||||||
import type { MonitorType } from "@/Types/Monitor";
|
import type { MonitorType } from "@/Types/Monitor";
|
||||||
import { Typography, useTheme } from "@mui/material";
|
import { Typography, useTheme } from "@mui/material";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const types = ["http", "ping", "port", "docker", "game", "grpc", "websocket"];
|
const types = ["http", "ping", "port", "docker", "game"];
|
||||||
const typeDisplayNames: Record<string, string> = {
|
|
||||||
http: "HTTP",
|
|
||||||
ping: "Ping",
|
|
||||||
port: "Port",
|
|
||||||
docker: "Docker",
|
|
||||||
game: "Game",
|
|
||||||
grpc: "gRPC",
|
|
||||||
websocket: "WebSocket",
|
|
||||||
};
|
|
||||||
const statuses = ["up", "down"];
|
const statuses = ["up", "down"];
|
||||||
const states = ["active", "paused"];
|
const states = ["active", "paused"];
|
||||||
|
|
||||||
@@ -40,7 +30,6 @@ export const ControlsFilter = ({
|
|||||||
onClearFilters: () => void;
|
onClearFilters: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
|
||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
const isFilterActive =
|
const isFilterActive =
|
||||||
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
(selectedTypes?.length ?? 0) > 0 || selectedStatus !== "" || selectedState !== "";
|
||||||
@@ -61,7 +50,7 @@ export const ControlsFilter = ({
|
|||||||
key={type}
|
key={type}
|
||||||
value={type}
|
value={type}
|
||||||
>
|
>
|
||||||
<Typography>{typeDisplayNames[type] ?? type}</Typography>
|
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -99,7 +88,7 @@ export const ControlsFilter = ({
|
|||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={onClearFilters}
|
onClick={onClearFilters}
|
||||||
>
|
>
|
||||||
{t("common.buttons.clearFilters")}
|
Clear Filters
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
import { useRef, useEffect, useState } from "react";
|
|
||||||
import { Box, Typography, Stack, useTheme, GlobalStyles } from "@mui/material";
|
|
||||||
import Map, { Marker, Popup } from "react-map-gl/maplibre";
|
|
||||||
import type { MapRef } from "react-map-gl/maplibre";
|
|
||||||
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
|
||||||
|
|
||||||
interface GeoChecksMapProps {
|
|
||||||
geoChecks: FlatGeoCheck[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GeoChecksMap = ({ geoChecks }: GeoChecksMapProps) => {
|
|
||||||
const mapRef = useRef<MapRef>(null);
|
|
||||||
const [selectedCheck, setSelectedCheck] = useState<FlatGeoCheck | null>(null);
|
|
||||||
const theme = useTheme();
|
|
||||||
const isDarkMode = theme.palette.mode === "dark";
|
|
||||||
|
|
||||||
const mapPopupStyles = (
|
|
||||||
<GlobalStyles
|
|
||||||
styles={{
|
|
||||||
".maplibregl-popup-content": {
|
|
||||||
background: "transparent !important",
|
|
||||||
padding: "0 !important",
|
|
||||||
boxShadow: "none !important",
|
|
||||||
},
|
|
||||||
".maplibregl-popup-tip": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
".maplibregl-ctrl-attrib": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
".maplibregl-ctrl-logo": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
".maplibregl-popup-close-button": {
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
background: "transparent",
|
|
||||||
fontSize: "20px",
|
|
||||||
padding: "4px",
|
|
||||||
"&:hover": {
|
|
||||||
backgroundColor: theme.palette.action.hover,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (geoChecks.length === 0 || !mapRef.current) return;
|
|
||||||
|
|
||||||
const bounds = geoChecks.reduce(
|
|
||||||
(acc, check) => {
|
|
||||||
return {
|
|
||||||
minLng: Math.min(acc.minLng, check.location.longitude),
|
|
||||||
maxLng: Math.max(acc.maxLng, check.location.longitude),
|
|
||||||
minLat: Math.min(acc.minLat, check.location.latitude),
|
|
||||||
maxLat: Math.max(acc.maxLat, check.location.latitude),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
minLng: Infinity,
|
|
||||||
maxLng: -Infinity,
|
|
||||||
minLat: Infinity,
|
|
||||||
maxLat: -Infinity,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (bounds.minLng !== Infinity) {
|
|
||||||
mapRef.current.fitBounds(
|
|
||||||
[
|
|
||||||
[bounds.minLng, bounds.minLat],
|
|
||||||
[bounds.maxLng, bounds.maxLat],
|
|
||||||
],
|
|
||||||
{ padding: 50, duration: 1000 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [geoChecks]);
|
|
||||||
|
|
||||||
const getMarkerColor = (status: boolean): string => {
|
|
||||||
return status ? theme.palette.success.main : theme.palette.error.main;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatResponseTime = (timing: number): string => {
|
|
||||||
return `${timing.toFixed(0)}ms`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{mapPopupStyles}
|
|
||||||
<Box
|
|
||||||
height={500}
|
|
||||||
width={"100%"}
|
|
||||||
borderRadius={theme.shape.borderRadius}
|
|
||||||
overflow={"hidden"}
|
|
||||||
>
|
|
||||||
<Map
|
|
||||||
ref={mapRef}
|
|
||||||
initialViewState={{
|
|
||||||
longitude: 0,
|
|
||||||
latitude: 20,
|
|
||||||
zoom: 1.5,
|
|
||||||
}}
|
|
||||||
style={{ height: "100%", width: "100%" }}
|
|
||||||
mapStyle={
|
|
||||||
isDarkMode
|
|
||||||
? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
|
|
||||||
: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{geoChecks.map((check, index) => (
|
|
||||||
<Marker
|
|
||||||
key={`${check.id}-${index}`}
|
|
||||||
longitude={check.location.longitude}
|
|
||||||
latitude={check.location.latitude}
|
|
||||||
anchor="bottom"
|
|
||||||
onClick={(e: any) => {
|
|
||||||
e.originalEvent.stopPropagation();
|
|
||||||
setSelectedCheck(check);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "10px",
|
|
||||||
height: "10px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: getMarkerColor(check.status),
|
|
||||||
boxShadow: "0 2px 4px rgba(0,0,0,0.3)",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Marker>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{selectedCheck && (
|
|
||||||
<Popup
|
|
||||||
longitude={selectedCheck.location.longitude}
|
|
||||||
latitude={selectedCheck.location.latitude}
|
|
||||||
anchor="top"
|
|
||||||
onClose={() => setSelectedCheck(null)}
|
|
||||||
closeOnClick={false}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
minWidth: 200,
|
|
||||||
p: theme.spacing(4),
|
|
||||||
bgcolor: theme.palette.background.paper,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: "solid",
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
borderRadius: 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1"
|
|
||||||
fontWeight="bold"
|
|
||||||
gutterBottom
|
|
||||||
>
|
|
||||||
{selectedCheck.location.city}, {selectedCheck.location.country}
|
|
||||||
</Typography>
|
|
||||||
<Stack spacing={0.5}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
fontWeight="medium"
|
|
||||||
>
|
|
||||||
Status:
|
|
||||||
</Typography>{" "}
|
|
||||||
{selectedCheck.status ? "Up" : "Down"}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
fontWeight="medium"
|
|
||||||
>
|
|
||||||
Status Code:
|
|
||||||
</Typography>{" "}
|
|
||||||
{selectedCheck.statusCode}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
<Typography
|
|
||||||
component="span"
|
|
||||||
fontWeight="medium"
|
|
||||||
>
|
|
||||||
Response Time:
|
|
||||||
</Typography>{" "}
|
|
||||||
{formatResponseTime(selectedCheck.timings.total)}
|
|
||||||
</Typography>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
sx={{ mt: 0.5 }}
|
|
||||||
>
|
|
||||||
{new Date(selectedCheck.createdAt).toLocaleString()}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Popup>
|
|
||||||
)}
|
|
||||||
</Map>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
|
||||||
import { Tabs, Tab } from "@/Components/design-elements";
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import type { GeoContinent } from "@/Types/GeoCheck";
|
|
||||||
|
|
||||||
interface HeaderGeoTabsProps {
|
|
||||||
geoCheckEnabled: boolean;
|
|
||||||
locations: GeoContinent[] | undefined;
|
|
||||||
selectedLocation: GeoContinent;
|
|
||||||
onLocationChange: (location: GeoContinent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HeaderGeoTabs = ({
|
|
||||||
geoCheckEnabled,
|
|
||||||
locations,
|
|
||||||
selectedLocation,
|
|
||||||
onLocationChange,
|
|
||||||
}: HeaderGeoTabsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
if (!geoCheckEnabled || !locations || locations.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChange = (_event: React.SyntheticEvent, newValue: GeoContinent) => {
|
|
||||||
onLocationChange(newValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
spacing={{ xs: theme.spacing(8), md: 0 }}
|
|
||||||
direction={{ xs: "column", md: "row" }}
|
|
||||||
alignItems={"center"}
|
|
||||||
justifyContent={"flex-start"}
|
|
||||||
>
|
|
||||||
<Tabs
|
|
||||||
value={selectedLocation}
|
|
||||||
onChange={handleChange}
|
|
||||||
>
|
|
||||||
{locations.map((location) => (
|
|
||||||
<Tab
|
|
||||||
key={location}
|
|
||||||
label={t(
|
|
||||||
`pages.createMonitor.form.geoChecks.option.locations.options.${location}`
|
|
||||||
)}
|
|
||||||
value={location}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors";
|
import { HistogramPageSpeedScoresTooltip } from "@/Components/monitors";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import type { PageSpeedGroupedCheck } from "@/Types/Check";
|
import type { CheckSnapshot } from "@/Types/Check";
|
||||||
import type { Palette } from "@mui/material/styles";
|
import type { Palette } from "@mui/material/styles";
|
||||||
type PaletteColorKey = Extract<
|
type PaletteColorKey = Extract<
|
||||||
keyof Palette,
|
keyof Palette,
|
||||||
@@ -52,7 +52,7 @@ export const HistogramPageSpeedDetails = ({
|
|||||||
checks,
|
checks,
|
||||||
range,
|
range,
|
||||||
}: {
|
}: {
|
||||||
checks: PageSpeedGroupedCheck[];
|
checks: CheckSnapshot[];
|
||||||
range: string;
|
range: string;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -73,7 +73,7 @@ export const HistogramPageSpeedDetails = ({
|
|||||||
>
|
>
|
||||||
<AreaChart data={checks}>
|
<AreaChart data={checks}>
|
||||||
<XAxis
|
<XAxis
|
||||||
dataKey={"bucketDate"}
|
dataKey={"createdAt"}
|
||||||
tick={(props) => (
|
tick={(props) => (
|
||||||
<XTick
|
<XTick
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Box from "@mui/material/Box";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
import type { ConfigItem } from "@/Components/monitors";
|
import type { ConfigItem } from "@/Components/monitors";
|
||||||
import type { TooltipProps } from "recharts";
|
import type { TooltipProps } from "recharts";
|
||||||
@@ -31,8 +30,8 @@ export const HistogramPageSpeedScoresTooltip = ({
|
|||||||
border: 1,
|
border: 1,
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
py: theme.spacing(SPACING.LG),
|
py: theme.spacing(2),
|
||||||
px: theme.spacing(LAYOUT.XS),
|
px: theme.spacing(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -55,12 +54,12 @@ export const HistogramPageSpeedScoresTooltip = ({
|
|||||||
key={`${key}-tooltip`}
|
key={`${key}-tooltip`}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(SPACING.XXL)}
|
gap={theme.spacing(3)}
|
||||||
mt={theme.spacing(SPACING.SM)}
|
mt={theme.spacing(1)}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
width={theme.spacing(LAYOUT.XS)}
|
width={theme.spacing(4)}
|
||||||
height={theme.spacing(LAYOUT.XS)}
|
height={theme.spacing(4)}
|
||||||
sx={{ borderRadius: "50%", backgroundColor: dotColor }}
|
sx={{ borderRadius: "50%", backgroundColor: dotColor }}
|
||||||
/>
|
/>
|
||||||
<Typography
|
<Typography
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export * from "./ControlsFilter";
|
export * from "./ControlsFilter";
|
||||||
export * from "./MonitorStatBoxes";
|
export * from "./MonitorStatBoxes";
|
||||||
export * from "./HeaderMonitorControls";
|
export * from "./HeaderMonitorControls";
|
||||||
export * from "./HeaderGeoTabs";
|
|
||||||
export * from "./GeoChecksMap";
|
|
||||||
export * from "./charts/HistogramStatus";
|
export * from "./charts/HistogramStatus";
|
||||||
export * from "./charts/RadialAvgResponse";
|
export * from "./charts/RadialAvgResponse";
|
||||||
export * from "./charts/HistogramDetails";
|
export * from "./charts/HistogramDetails";
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import IconButton from "@mui/material/IconButton";
|
|||||||
import Menu from "@mui/material/Menu";
|
import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -65,9 +64,9 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const menuItemSx = {
|
const menuItemSx = {
|
||||||
gap: theme.spacing(LAYOUT.MD),
|
gap: theme.spacing(2),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
pl: theme.spacing(LAYOUT.XS),
|
pl: theme.spacing(4),
|
||||||
"& svg": { stroke: theme.palette.text.secondary },
|
"& svg": { stroke: theme.palette.text.secondary },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -75,9 +74,9 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
py={theme.spacing(LAYOUT.XS)}
|
py={theme.spacing(4)}
|
||||||
px={theme.spacing(LAYOUT.MD)}
|
px={theme.spacing(8)}
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
small
|
small
|
||||||
@@ -89,7 +88,7 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(SPACING.MD)}
|
gap={theme.spacing(2)}
|
||||||
minWidth={0}
|
minWidth={0}
|
||||||
flex={1}
|
flex={1}
|
||||||
>
|
>
|
||||||
@@ -133,15 +132,10 @@ export const AuthFooter = ({ collapsed, accountMenuItems }: AuthFooterProps) =>
|
|||||||
disableScrollLock
|
disableScrollLock
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
paper: {
|
paper: { sx: { mt: theme.spacing(-4), ml: collapsed ? theme.spacing(2) : 0 } },
|
||||||
sx: {
|
|
||||||
mt: theme.spacing(-LAYOUT.XS),
|
|
||||||
ml: collapsed ? theme.spacing(SPACING.LG) : 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
MenuListProps={{ sx: { p: 2, "& li": { m: 0 } } }}
|
MenuListProps={{ sx: { p: 2, "& li": { m: 0 } } }}
|
||||||
sx={{ ml: theme.spacing(LAYOUT.XS) }}
|
sx={{ ml: theme.spacing(4) }}
|
||||||
>
|
>
|
||||||
{collapsed && (
|
{collapsed && (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
import { Typography, IconButton, Stack, Box } from "@mui/material";
|
||||||
import { Icon } from "@/Components/design-elements";
|
import { Icon } from "@/Components/design-elements";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
|
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
@@ -35,22 +34,25 @@ export const StarPrompt = ({
|
|||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="column"
|
direction="column"
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
sx={{
|
||||||
padding={theme.spacing(LAYOUT.SM)}
|
width: "100%",
|
||||||
width={"100%"}
|
padding: `${theme.spacing(6)} ${theme.spacing(6)}`,
|
||||||
borderTop={`1px solid ${theme.palette.divider}`}
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
borderBottom={`1px solid ${theme.palette.divider}`}
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: 0,
|
||||||
|
gap: theme.spacing(1.5),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
width="100%"
|
width="100%"
|
||||||
pl={theme.spacing(LAYOUT.XS)}
|
pl={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="subtitle2"
|
variant="subtitle2"
|
||||||
mt={theme.spacing(SPACING.XXL)}
|
mt={theme.spacing(3)}
|
||||||
>
|
>
|
||||||
{t("components.sidebar.starPrompt.title")}
|
{t("components.sidebar.starPrompt.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -76,7 +78,7 @@ export const StarPrompt = ({
|
|||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
mb: 1,
|
mb: 1,
|
||||||
px: theme.spacing(LAYOUT.XS),
|
px: theme.spacing(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("components.sidebar.starPrompt.description")}
|
{t("components.sidebar.starPrompt.description")}
|
||||||
@@ -94,7 +96,7 @@ export const StarPrompt = ({
|
|||||||
"&:hover": {
|
"&:hover": {
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
pl: theme.spacing(LAYOUT.XS),
|
pl: theme.spacing(4),
|
||||||
filter: mode === "dark" ? "invert(1)" : "none",
|
filter: mode === "dark" ? "invert(1)" : "none",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Backdrop from "@mui/material/Backdrop";
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import { useSidebar } from "@/Hooks/useSidebar.js";
|
import { useSidebar } from "@/Hooks/useSidebar.js";
|
||||||
import { Logo } from "@/Components/sidebar/Logo";
|
import { Logo } from "@/Components/sidebar/Logo";
|
||||||
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
|
import { getMenu, getBottomMenu, getAccountMenu } from "@/Components/sidebar/Menu";
|
||||||
@@ -66,9 +65,9 @@ export const Sidebar = () => {
|
|||||||
left={0}
|
left={0}
|
||||||
minHeight={"100vh"}
|
minHeight={"100vh"}
|
||||||
maxHeight={"100vh"}
|
maxHeight={"100vh"}
|
||||||
paddingTop={theme.spacing(LAYOUT.SM)}
|
paddingTop={theme.spacing(6)}
|
||||||
paddingBottom={theme.spacing(LAYOUT.SM)}
|
paddingBottom={theme.spacing(6)}
|
||||||
gap={theme.spacing(LAYOUT.SM)}
|
gap={theme.spacing(6)}
|
||||||
borderRight={`1px solid ${theme.palette.divider}`}
|
borderRight={`1px solid ${theme.palette.divider}`}
|
||||||
width={width}
|
width={width}
|
||||||
sx={{
|
sx={{
|
||||||
@@ -82,13 +81,13 @@ export const Sidebar = () => {
|
|||||||
component="nav"
|
component="nav"
|
||||||
disablePadding
|
disablePadding
|
||||||
sx={{
|
sx={{
|
||||||
px: theme.spacing(LAYOUT.SM),
|
px: theme.spacing(6),
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Logo
|
<Logo
|
||||||
pt={theme.spacing(LAYOUT.MD)}
|
pt={theme.spacing(8)}
|
||||||
pb={theme.spacing(LAYOUT.MD)}
|
pb={theme.spacing(10)}
|
||||||
/>
|
/>
|
||||||
{menu.map((item) => {
|
{menu.map((item) => {
|
||||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||||
@@ -106,7 +105,7 @@ export const Sidebar = () => {
|
|||||||
<List
|
<List
|
||||||
component="nav"
|
component="nav"
|
||||||
disablePadding
|
disablePadding
|
||||||
sx={{ px: theme.spacing(LAYOUT.SM) }}
|
sx={{ px: theme.spacing(6) }}
|
||||||
>
|
>
|
||||||
{bottomMenu.map((item) => {
|
{bottomMenu.map((item) => {
|
||||||
const selected = location.pathname.startsWith(`/${item.path}`);
|
const selected = location.pathname.startsWith(`/${item.path}`);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import type { AxiosRequestConfig } from "axios";
|
|||||||
import { get, patch, post, put, deleteOp } from "@/Utils/ApiClient";
|
import { get, patch, post, put, deleteOp } from "@/Utils/ApiClient";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useToast } from "@/Hooks/UseToast";
|
import { useToast } from "@/Hooks/UseToast";
|
||||||
import { logger } from "@/Utils/logger";
|
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
interface ApiResponse<T> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -18,7 +17,7 @@ const fetcher = async <T>(url: string, config?: AxiosRequestConfig) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useGet = <T>(
|
export const useGet = <T>(
|
||||||
url: string | null | undefined,
|
url: string | null,
|
||||||
axiosConfig?: AxiosRequestConfig,
|
axiosConfig?: AxiosRequestConfig,
|
||||||
swrConfig?: SWRConfiguration
|
swrConfig?: SWRConfiguration
|
||||||
) => {
|
) => {
|
||||||
@@ -62,7 +61,7 @@ export const usePost = <B = any, R = any>() => {
|
|||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
logger.error("POST request failed", err, { endpoint, body });
|
console.error(err);
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
return null;
|
return null;
|
||||||
@@ -97,7 +96,7 @@ export const usePatch = <B = any, R = any>() => {
|
|||||||
toastSuccess(res.data?.msg || "Operation successful");
|
toastSuccess(res.data?.msg || "Operation successful");
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("PATCH request failed", err, { endpoint, body });
|
console.error(err);
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
@@ -133,7 +132,7 @@ export const usePut = <B = any, R = any>() => {
|
|||||||
toastSuccess(res.data?.msg || "Operation successful");
|
toastSuccess(res.data?.msg || "Operation successful");
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("PUT request failed", err, { endpoint, body });
|
console.error(err);
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
@@ -168,7 +167,7 @@ export const useDelete = <R = any>() => {
|
|||||||
toastSuccess(res.data?.msg || "Operation successful");
|
toastSuccess(res.data?.msg || "Operation successful");
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("DELETE request failed", err, { endpoint });
|
console.error(err);
|
||||||
const errMsg = err?.response?.data?.message || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.message || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
@@ -202,7 +201,7 @@ export const useLazyGet = <R = any>() => {
|
|||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("GET request failed", err, { endpoint });
|
console.error(err);
|
||||||
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
const errMsg = err?.response?.data?.msg || err.message || "An error occurred";
|
||||||
toastError(errMsg);
|
toastError(errMsg);
|
||||||
setError(errMsg);
|
setError(errMsg);
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const getBaseDefaults = (data?: Monitor | null) => ({
|
|||||||
notifications: data?.notifications || [],
|
notifications: data?.notifications || [],
|
||||||
statusWindowSize: data?.statusWindowSize || 5,
|
statusWindowSize: data?.statusWindowSize || 5,
|
||||||
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
||||||
geoCheckEnabled: data?.geoCheckEnabled ?? false,
|
|
||||||
geoCheckLocations: data?.geoCheckLocations || [],
|
|
||||||
geoCheckInterval: data?.geoCheckInterval || 300000,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMonitorForm = ({
|
export const useMonitorForm = ({
|
||||||
@@ -73,16 +70,6 @@ export const useMonitorForm = ({
|
|||||||
gameId: data?.gameId || "",
|
gameId: data?.gameId || "",
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "grpc":
|
|
||||||
defaults = {
|
|
||||||
...base,
|
|
||||||
type: "grpc",
|
|
||||||
url: data?.url || "",
|
|
||||||
port: data?.port || 50051,
|
|
||||||
grpcServiceName: data?.grpcServiceName || "",
|
|
||||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
case "pagespeed":
|
case "pagespeed":
|
||||||
defaults = {
|
defaults = {
|
||||||
...base,
|
...base,
|
||||||
@@ -103,14 +90,6 @@ export const useMonitorForm = ({
|
|||||||
selectedDisks: data?.selectedDisks || [],
|
selectedDisks: data?.selectedDisks || [],
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "websocket":
|
|
||||||
defaults = {
|
|
||||||
...base,
|
|
||||||
type: "websocket",
|
|
||||||
url: data?.url || "",
|
|
||||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
defaults = {
|
defaults = {
|
||||||
...base,
|
...base,
|
||||||
|
|||||||
@@ -22,22 +22,10 @@ export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) =>
|
|||||||
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
systemEmailTLSServername: data?.systemEmailTLSServername || "",
|
||||||
systemEmailPort: data?.systemEmailPort,
|
systemEmailPort: data?.systemEmailPort,
|
||||||
globalThresholds: {
|
globalThresholds: {
|
||||||
cpu:
|
cpu: data?.globalThresholds?.cpu ?? 0,
|
||||||
data?.globalThresholds?.cpu && data.globalThresholds.cpu >= 1
|
memory: data?.globalThresholds?.memory ?? 0,
|
||||||
? data.globalThresholds.cpu
|
disk: data?.globalThresholds?.disk ?? 0,
|
||||||
: 80,
|
temperature: data?.globalThresholds?.temperature ?? 0,
|
||||||
memory:
|
|
||||||
data?.globalThresholds?.memory && data.globalThresholds.memory >= 1
|
|
||||||
? data.globalThresholds.memory
|
|
||||||
: 80,
|
|
||||||
disk:
|
|
||||||
data?.globalThresholds?.disk && data.globalThresholds.disk >= 1
|
|
||||||
? data.globalThresholds.disk
|
|
||||||
: 80,
|
|
||||||
temperature:
|
|
||||||
data?.globalThresholds?.temperature && data.globalThresholds.temperature >= 1
|
|
||||||
? data.globalThresholds.temperature
|
|
||||||
: 80,
|
|
||||||
},
|
},
|
||||||
checkTTL: data?.checkTTL ?? 30,
|
checkTTL: data?.checkTTL ?? 30,
|
||||||
pagespeedApiKey: "",
|
pagespeedApiKey: "",
|
||||||
|
|||||||
@@ -11,17 +11,12 @@ import { ConfigBox, BasePage } from "@/Components/design-elements";
|
|||||||
import { UserRoles } from "@/Types/User";
|
import { UserRoles } from "@/Types/User";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { UserRole, User } from "@/Types/User";
|
import type { UserRole, User } from "@/Types/User";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { useEditUserForm } from "@/Hooks/useEditUserForm";
|
import { useEditUserForm } from "@/Hooks/useEditUserForm";
|
||||||
import { useGet, usePatch, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePatch } from "@/Hooks/UseApi";
|
||||||
import type { EditUserFormData } from "@/Validation/editUser";
|
import type { EditUserFormData } from "@/Validation/editUser";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { DialogInput } from "@/Components/inputs/Dialog";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import { useIsAdmin, useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
|
||||||
|
|
||||||
interface RoleOption {
|
interface RoleOption {
|
||||||
id: UserRole;
|
id: UserRole;
|
||||||
@@ -34,12 +29,6 @@ const EditUserPage = () => {
|
|||||||
const { userId } = useParams<{ userId: string }>();
|
const { userId } = useParams<{ userId: string }>();
|
||||||
const { resolver, defaults } = useEditUserForm();
|
const { resolver, defaults } = useEditUserForm();
|
||||||
const { patch, loading: isSaving } = usePatch();
|
const { patch, loading: isSaving } = usePatch();
|
||||||
const { deleteFn, loading: isDeleting } = useDelete();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
||||||
const currentUser = useSelector((state: RootState) => state.auth.user);
|
|
||||||
const isSuperAdmin = useIsSuperAdmin();
|
|
||||||
const isAdmin = useIsAdmin();
|
|
||||||
|
|
||||||
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
|
const { data: user, isLoading } = useGet<User>(`/auth/users/${userId}`);
|
||||||
|
|
||||||
@@ -71,20 +60,6 @@ const EditUserPage = () => {
|
|||||||
setValue("role", newRoles, { shouldValidate: true });
|
setValue("role", newRoles, { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const canDeleteUser =
|
|
||||||
isAdmin &&
|
|
||||||
userId !== currentUser?.id &&
|
|
||||||
!user?.role?.includes("demo") &&
|
|
||||||
(isSuperAdmin || user?.role?.every((r) => r !== "admin" && r !== "superadmin"));
|
|
||||||
|
|
||||||
const handleDeleteUser = async () => {
|
|
||||||
const result = await deleteFn(`/auth/users/${userId}`);
|
|
||||||
setShowDeleteDialog(false);
|
|
||||||
if (result) {
|
|
||||||
navigate("/account", { state: { tab: "team" } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: EditUserFormData) => {
|
const onSubmit = async (data: EditUserFormData) => {
|
||||||
await patch(`/auth/users/${userId}`, data);
|
await patch(`/auth/users/${userId}`, data);
|
||||||
};
|
};
|
||||||
@@ -190,45 +165,17 @@ const EditUserPage = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Stack
|
<Button
|
||||||
gap={LAYOUT.XS}
|
type="submit"
|
||||||
direction="row"
|
variant="contained"
|
||||||
justifyContent={"flex-end"}
|
color="primary"
|
||||||
width="100%"
|
loading={isSaving}
|
||||||
|
sx={{ alignSelf: "flex-end", minWidth: 100 }}
|
||||||
>
|
>
|
||||||
{canDeleteUser && (
|
{t("common.buttons.save")}
|
||||||
<Button
|
</Button>
|
||||||
variant="contained"
|
|
||||||
color="error"
|
|
||||||
onClick={() => setShowDeleteDialog(true)}
|
|
||||||
sx={{ minWidth: 100 }}
|
|
||||||
>
|
|
||||||
{t("common.buttons.removeUser")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
loading={isSaving}
|
|
||||||
sx={{ minWidth: 100 }}
|
|
||||||
>
|
|
||||||
{t("common.buttons.save")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
<DialogInput
|
|
||||||
open={showDeleteDialog}
|
|
||||||
title={t("pages.editUser.dialog.removeUser.title")}
|
|
||||||
content={t("pages.editUser.dialog.removeUser.content", {
|
|
||||||
name: `${user?.firstName} ${user?.lastName}`,
|
|
||||||
})}
|
|
||||||
onCancel={() => setShowDeleteDialog(false)}
|
|
||||||
onConfirm={handleDeleteUser}
|
|
||||||
confirmText={t("common.buttons.removeUser")}
|
|
||||||
loading={isDeleting}
|
|
||||||
/>
|
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Table } from "@/Components/design-elements";
|
import { Table } from "@/Components/design-elements";
|
||||||
import type { Header } from "@/Components/design-elements/Table";
|
import type { Header } from "@/Components/design-elements/Table";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
import { useIsSuperAdmin } from "@/Hooks/useIsAdmin";
|
||||||
import type { User } from "@/Types/User";
|
import type { User } from "@/Types/User";
|
||||||
|
|
||||||
interface TeamTableProps {
|
interface TeamTableProps {
|
||||||
@@ -13,7 +13,7 @@ interface TeamTableProps {
|
|||||||
export const TeamTable = ({ users }: TeamTableProps) => {
|
export const TeamTable = ({ users }: TeamTableProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const isAdmin = useIsAdmin();
|
const isSuperAdmin = useIsSuperAdmin();
|
||||||
|
|
||||||
const headers: Header<User>[] = [
|
const headers: Header<User>[] = [
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const handleRowClick = (row: User) => {
|
const handleRowClick = (row: User) => {
|
||||||
if (isAdmin) {
|
if (isSuperAdmin) {
|
||||||
navigate(`/account/team/${row.id}`);
|
navigate(`/account/team/${row.id}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -54,7 +54,7 @@ export const TeamTable = ({ users }: TeamTableProps) => {
|
|||||||
<Table
|
<Table
|
||||||
headers={headers}
|
headers={headers}
|
||||||
data={users}
|
data={users}
|
||||||
onRowClick={isAdmin ? handleRowClick : undefined}
|
onRowClick={isSuperAdmin ? handleRowClick : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -5,6 +5,7 @@ import type { Monitor } from "@/Types/Monitor";
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
import type { Check } from "@/Types/Check";
|
import type { Check } from "@/Types/Check";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@@ -28,6 +29,7 @@ export const ChecksTable = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
const getHeaders = (t: Function, uiTimezone: string) => {
|
||||||
const headers: Header<Check>[] = [
|
const headers: Header<Check>[] = [
|
||||||
@@ -107,6 +109,9 @@ export const ChecksTable = ({
|
|||||||
<Table
|
<Table
|
||||||
headers={headers}
|
headers={headers}
|
||||||
data={checks}
|
data={checks}
|
||||||
|
onRowClick={(row) => {
|
||||||
|
navigate(`/checks/${row.id}`);
|
||||||
|
}}
|
||||||
emptyViewText={t("pages.checks.table.empty")}
|
emptyViewText={t("pages.checks.table.empty")}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
<Pagination
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { logger } from "@/Utils/logger";
|
|
||||||
import { useParams, useLocation, useNavigate } from "react-router";
|
import { useParams, useLocation, useNavigate } from "react-router";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -16,7 +15,6 @@ import Divider from "@mui/material/Divider";
|
|||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import { Trash2 } from "lucide-react";
|
import { Trash2 } from "lucide-react";
|
||||||
import { HeaderDeleteControls } from "@/Components/monitors";
|
import { HeaderDeleteControls } from "@/Components/monitors";
|
||||||
import { GeoContinents } from "@/Types/GeoCheck";
|
|
||||||
|
|
||||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||||
import {
|
import {
|
||||||
@@ -29,15 +27,9 @@ import {
|
|||||||
SliderWithLabel,
|
SliderWithLabel,
|
||||||
Dialog,
|
Dialog,
|
||||||
} from "@/Components/inputs";
|
} from "@/Components/inputs";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||||
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
||||||
import {
|
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
||||||
type Monitor,
|
|
||||||
type MonitorType,
|
|
||||||
type GamesMap,
|
|
||||||
supportsGeoCheck,
|
|
||||||
} from "@/Types/Monitor";
|
|
||||||
import type { Notification } from "@/Types/Notification";
|
import type { Notification } from "@/Types/Notification";
|
||||||
import type { MonitorFormData } from "@/Validation/monitor";
|
import type { MonitorFormData } from "@/Validation/monitor";
|
||||||
|
|
||||||
@@ -49,8 +41,6 @@ interface GeneralSettingsConfig {
|
|||||||
showPort: boolean;
|
showPort: boolean;
|
||||||
showGameSelect: boolean;
|
showGameSelect: boolean;
|
||||||
showSecret: boolean;
|
showSecret: boolean;
|
||||||
showGrpcServiceName: boolean;
|
|
||||||
showIgnoreTls: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGeneralSettingsConfig = (
|
const getGeneralSettingsConfig = (
|
||||||
@@ -66,8 +56,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
},
|
||||||
ping: {
|
ping: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||||
@@ -77,8 +65,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
},
|
||||||
docker: {
|
docker: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
||||||
@@ -88,8 +74,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
},
|
||||||
port: {
|
port: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -99,8 +83,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: true,
|
showPort: true,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
},
|
||||||
game: {
|
game: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -110,19 +92,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: true,
|
showPort: true,
|
||||||
showGameSelect: true,
|
showGameSelect: true,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
|
||||||
grpc: {
|
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
|
||||||
urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"),
|
|
||||||
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
|
||||||
showUrl: true,
|
|
||||||
showPort: true,
|
|
||||||
showGameSelect: false,
|
|
||||||
showSecret: false,
|
|
||||||
showGrpcServiceName: true,
|
|
||||||
showIgnoreTls: true,
|
|
||||||
},
|
},
|
||||||
pagespeed: {
|
pagespeed: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -132,8 +101,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: false,
|
showSecret: false,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
},
|
||||||
hardware: {
|
hardware: {
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||||
@@ -143,19 +110,6 @@ const getGeneralSettingsConfig = (
|
|||||||
showPort: false,
|
showPort: false,
|
||||||
showGameSelect: false,
|
showGameSelect: false,
|
||||||
showSecret: true,
|
showSecret: true,
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: false,
|
|
||||||
},
|
|
||||||
websocket: {
|
|
||||||
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
|
|
||||||
urlPlaceholder: t("pages.createMonitor.form.general.option.wsUrl.placeholder"),
|
|
||||||
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
|
||||||
showUrl: true,
|
|
||||||
showPort: false,
|
|
||||||
showGameSelect: false,
|
|
||||||
showSecret: false,
|
|
||||||
showGrpcServiceName: false,
|
|
||||||
showIgnoreTls: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return configs[type] || configs.http;
|
return configs[type] || configs.http;
|
||||||
@@ -211,7 +165,6 @@ const CreateMonitorPage = () => {
|
|||||||
const watchedType = watch("type") as MonitorType;
|
const watchedType = watch("type") as MonitorType;
|
||||||
|
|
||||||
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
||||||
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearErrors();
|
clearErrors();
|
||||||
@@ -271,7 +224,7 @@ const CreateMonitorPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onError = (errors: unknown) => {
|
const onError = (errors: unknown) => {
|
||||||
logger.debug("Monitor creation validation errors", errors);
|
console.log(errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -298,7 +251,7 @@ const CreateMonitorPage = () => {
|
|||||||
<FormControl error={!!fieldState.error}>
|
<FormControl error={!!fieldState.error}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
{...field}
|
{...field}
|
||||||
sx={{ gap: theme.spacing(LAYOUT.MD) }}
|
sx={{ gap: theme.spacing(6) }}
|
||||||
>
|
>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="http"
|
value="http"
|
||||||
@@ -335,20 +288,6 @@ const CreateMonitorPage = () => {
|
|||||||
"pages.createMonitor.form.type.optionGameDescription"
|
"pages.createMonitor.form.type.optionGameDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
|
||||||
value="grpc"
|
|
||||||
label={t("pages.createMonitor.form.type.optionGrpc")}
|
|
||||||
description={t(
|
|
||||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<RadioWithDescription
|
|
||||||
value="websocket"
|
|
||||||
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
|
||||||
description={t(
|
|
||||||
"pages.createMonitor.form.type.optionWebSocketDescription"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -361,7 +300,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.general.title")}
|
title={t("pages.createMonitor.form.general.title")}
|
||||||
subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)}
|
subtitle={t(`pages.createMonitor.form.general.description.${watchedType}`)}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
{/* URL/Host/Container field - not shown for hardware */}
|
{/* URL/Host/Container field - not shown for hardware */}
|
||||||
{generalSettingsConfig.showUrl && (
|
{generalSettingsConfig.showUrl && (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -437,30 +376,6 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* gRPC Service Name field - only for grpc type */}
|
|
||||||
{generalSettingsConfig.showGrpcServiceName && (
|
|
||||||
<Controller
|
|
||||||
name="grpcServiceName"
|
|
||||||
control={control}
|
|
||||||
render={({ field, fieldState }) => (
|
|
||||||
<TextField
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
type="text"
|
|
||||||
fieldLabel={t(
|
|
||||||
"pages.createMonitor.form.general.option.grpcServiceName.label"
|
|
||||||
)}
|
|
||||||
placeholder={t(
|
|
||||||
"pages.createMonitor.form.general.option.grpcServiceName.placeholder"
|
|
||||||
)}
|
|
||||||
fullWidth
|
|
||||||
error={!!fieldState.error}
|
|
||||||
helperText={fieldState.error?.message ?? ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Secret field - only for hardware type */}
|
{/* Secret field - only for hardware type */}
|
||||||
{generalSettingsConfig.showSecret && (
|
{generalSettingsConfig.showSecret && (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -580,7 +495,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.thresholds.title")}
|
title={t("pages.createMonitor.form.thresholds.title")}
|
||||||
subtitle={t("pages.createMonitor.form.thresholds.description")}
|
subtitle={t("pages.createMonitor.form.thresholds.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="cpuAlertThreshold"
|
name="cpuAlertThreshold"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -662,7 +577,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.incidents.title")}
|
title={t("pages.createMonitor.form.incidents.title")}
|
||||||
subtitle={t("pages.createMonitor.form.incidents.description")}
|
subtitle={t("pages.createMonitor.form.incidents.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(12)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="statusWindowSize"
|
name="statusWindowSize"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -714,7 +629,7 @@ const CreateMonitorPage = () => {
|
|||||||
(field.value ?? []).includes(n.id)
|
(field.value ?? []).includes(n.id)
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(4)}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={notificationOptions}
|
options={notificationOptions}
|
||||||
@@ -765,9 +680,7 @@ const CreateMonitorPage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(watchedType === "http" ||
|
{watchedType === "http" && (
|
||||||
watchedType === "grpc" ||
|
|
||||||
watchedType === "websocket") && (
|
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.createMonitor.form.ignoreTls.title")}
|
title={t("pages.createMonitor.form.ignoreTls.title")}
|
||||||
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
||||||
@@ -779,7 +692,7 @@ const CreateMonitorPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(SPACING.LG)}
|
spacing={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
@@ -800,7 +713,7 @@ const CreateMonitorPage = () => {
|
|||||||
title={t("pages.createMonitor.form.advanced.title")}
|
title={t("pages.createMonitor.form.advanced.title")}
|
||||||
subtitle={t("pages.createMonitor.form.advanced.description")}
|
subtitle={t("pages.createMonitor.form.advanced.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="useAdvancedMatching"
|
name="useAdvancedMatching"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -808,7 +721,7 @@ const CreateMonitorPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(SPACING.LG)}
|
spacing={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
checked={field.value ?? false}
|
checked={field.value ?? false}
|
||||||
@@ -823,7 +736,7 @@ const CreateMonitorPage = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{watchedUseAdvancedMatching && (
|
{watchedUseAdvancedMatching && (
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="matchMethod"
|
name="matchMethod"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -910,140 +823,6 @@ const CreateMonitorPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{supportsGeoCheck(watchedType) && (
|
|
||||||
<ConfigBox
|
|
||||||
title={t("pages.createMonitor.form.geoChecks.title")}
|
|
||||||
subtitle={t("pages.createMonitor.form.geoChecks.description")}
|
|
||||||
rightContent={
|
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
|
||||||
<Controller
|
|
||||||
name="geoCheckEnabled"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
spacing={theme.spacing(SPACING.LG)}
|
|
||||||
>
|
|
||||||
<Switch
|
|
||||||
checked={field.value ?? false}
|
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<Typography>
|
|
||||||
{t("pages.createMonitor.form.geoChecks.option.enabled.label")}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{watchGeoCheckEnabled && (
|
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
|
||||||
<Controller
|
|
||||||
name="geoCheckLocations"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => {
|
|
||||||
// Map continents to have 'name' property for Autocomplete
|
|
||||||
const locationOptions = GeoContinents.map((continent) => ({
|
|
||||||
id: continent,
|
|
||||||
name: t(
|
|
||||||
`pages.createMonitor.form.geoChecks.option.locations.options.${continent}`
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
const selectedLocations = locationOptions.filter((loc) =>
|
|
||||||
(field.value ?? []).includes(loc.id)
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
|
||||||
<Autocomplete
|
|
||||||
multiple
|
|
||||||
options={locationOptions}
|
|
||||||
value={selectedLocations}
|
|
||||||
getOptionLabel={(option) => option.name}
|
|
||||||
onChange={(_: unknown, newValue: typeof locationOptions) => {
|
|
||||||
field.onChange(newValue.map((loc) => loc.id));
|
|
||||||
}}
|
|
||||||
isOptionEqualToValue={(option, value) =>
|
|
||||||
option.id === value.id
|
|
||||||
}
|
|
||||||
fieldLabel={t(
|
|
||||||
"pages.createMonitor.form.geoChecks.option.locations.label"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{selectedLocations.length > 0 && (
|
|
||||||
<Stack
|
|
||||||
flex={1}
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
{selectedLocations.map((location, index) => (
|
|
||||||
<Stack
|
|
||||||
direction="row"
|
|
||||||
alignItems="center"
|
|
||||||
key={location.id}
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<Typography flexGrow={1}>{location.name}</Typography>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
field.onChange(
|
|
||||||
(field.value ?? []).filter(
|
|
||||||
(id: string) => id !== location.id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
aria-label="Remove location"
|
|
||||||
>
|
|
||||||
<Trash2 size={16} />
|
|
||||||
</IconButton>
|
|
||||||
{index < selectedLocations.length - 1 && <Divider />}
|
|
||||||
</Stack>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="geoCheckInterval"
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? 300000}
|
|
||||||
fieldLabel={t(
|
|
||||||
"pages.createMonitor.form.geoChecks.option.interval.label"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MenuItem value={300000}>
|
|
||||||
{t(
|
|
||||||
"pages.createMonitor.form.geoChecks.option.interval.value.fiveMinutes"
|
|
||||||
)}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={600000}>
|
|
||||||
{t(
|
|
||||||
"pages.createMonitor.form.geoChecks.option.interval.value.tenMinutes"
|
|
||||||
)}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={900000}>
|
|
||||||
{t(
|
|
||||||
"pages.createMonitor.form.geoChecks.option.interval.value.fifteenMinutes"
|
|
||||||
)}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem value={1800000}>
|
|
||||||
{t(
|
|
||||||
"pages.createMonitor.form.geoChecks.option.interval.value.thirtyMinutes"
|
|
||||||
)}
|
|
||||||
</MenuItem>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Grid from "@mui/material/Grid";
|
|||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
import { BaseBox, ValueLabel } from "@/Components/design-elements";
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { Incident } from "@/Types/Incident";
|
import type { Incident } from "@/Types/Incident";
|
||||||
@@ -30,14 +29,14 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
gap={theme.spacing(4)}
|
||||||
sx={sx}
|
sx={sx}
|
||||||
>
|
>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.title")}
|
{t("pages.incidents.dialog.details.title")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<BaseBox padding={LAYOUT.MD}>
|
<BaseBox padding={8}>
|
||||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
<Stack gap={theme.spacing(4)}>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.overview")}
|
{t("pages.incidents.dialog.details.overview")}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -45,7 +44,7 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={theme.spacing(LAYOUT.MD)}
|
spacing={theme.spacing(4)}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
|
<Grid size={2}>{t("pages.incidents.dialog.details.status")}</Grid>
|
||||||
@@ -76,15 +75,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
<BaseBox padding={LAYOUT.MD}>
|
<BaseBox padding={8}>
|
||||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
<Stack gap={theme.spacing(4)}>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.analysis")}
|
{t("pages.incidents.dialog.details.analysis")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={theme.spacing(LAYOUT.MD)}
|
spacing={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Grid size={6}>
|
<Grid size={6}>
|
||||||
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
|
<Typography>{t("pages.incidents.dialog.details.timeline")}</Typography>
|
||||||
@@ -128,15 +127,15 @@ export const CardDetails = ({ incident, monitor, sx }: CardDetailsProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</BaseBox>
|
</BaseBox>
|
||||||
{!incident.status && (
|
{!incident.status && (
|
||||||
<BaseBox padding={LAYOUT.MD}>
|
<BaseBox padding={8}>
|
||||||
<Stack gap={theme.spacing(LAYOUT.XS)}>
|
<Stack gap={theme.spacing(4)}>
|
||||||
<Typography textTransform={"uppercase"}>
|
<Typography textTransform={"uppercase"}>
|
||||||
{t("pages.incidents.dialog.details.resolutionDetails")}
|
{t("pages.incidents.dialog.details.resolutionDetails")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={theme.spacing(LAYOUT.MD)}
|
spacing={theme.spacing(2)}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
>
|
>
|
||||||
<Grid size={2}>
|
<Grid size={2}>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
const LogsPage = () => {
|
const LogsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState<number>(1);
|
const [activeTab, setActiveTab] = useState<number>(2);
|
||||||
return (
|
return (
|
||||||
<BasePage>
|
<BasePage>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import { logger } from "@/Utils/logger";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||||
import {
|
import {
|
||||||
TextField,
|
TextField,
|
||||||
@@ -110,7 +108,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
const isLoading = isPosting || isPatching;
|
const isLoading = isPosting || isPatching;
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
logger.error("Maintenance form submission failed", undefined, { errors });
|
console.error("Form submission errors:", errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -122,7 +120,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
title={t("pages.maintenanceWindow.form.general.title")}
|
title={t("pages.maintenanceWindow.form.general.title")}
|
||||||
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
subtitle={t("pages.maintenanceWindow.form.general.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -172,7 +170,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
title={t("pages.maintenanceWindow.form.startDate.title")}
|
title={t("pages.maintenanceWindow.form.startDate.title")}
|
||||||
subtitle={t("pages.maintenanceWindow.form.startDate.description")}
|
subtitle={t("pages.maintenanceWindow.form.startDate.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="startDate"
|
name="startDate"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -199,7 +197,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
title={t("pages.maintenanceWindow.form.startTime.title")}
|
title={t("pages.maintenanceWindow.form.startTime.title")}
|
||||||
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
subtitle={t("pages.maintenanceWindow.form.startTime.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="startTime"
|
name="startTime"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -220,7 +218,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="flex-start"
|
alignItems="flex-start"
|
||||||
spacing={theme.spacing(LAYOUT.MD)}
|
spacing={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="duration"
|
name="duration"
|
||||||
@@ -285,7 +283,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
.filter((m): m is Monitor => m !== undefined);
|
.filter((m): m is Monitor => m !== undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(4)}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={monitorsList}
|
options={monitorsList}
|
||||||
@@ -319,8 +317,9 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
key={monitor.id}
|
key={monitor.id}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
padding={theme.spacing(LAYOUT.XS)}
|
spacing={theme.spacing(4)}
|
||||||
marginTop={theme.spacing(SPACING.LG)}
|
padding={theme.spacing(4)}
|
||||||
|
marginTop={theme.spacing(2)}
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
sx={{
|
sx={{
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
@@ -351,7 +350,7 @@ const CreateMaintenanceWindowPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
spacing={theme.spacing(SPACING.LG)}
|
spacing={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BasePage } from "@/Components/design-elements";
|
import { BasePage } from "@/Components/design-elements";
|
||||||
import { HeaderTimeRange } from "@/Components/common";
|
|
||||||
import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
|
import type { PageSpeedDetailsResponse } from "@/Types/Monitor";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import {
|
import {
|
||||||
@@ -14,36 +13,23 @@ import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
|||||||
import { useGet } from "@/Hooks/UseApi";
|
import { useGet } from "@/Hooks/UseApi";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
import { useState, useMemo } from "react";
|
|
||||||
|
|
||||||
const PageSpeedDetails = () => {
|
const PageSpeedDetails = () => {
|
||||||
const { monitorId } = useParams();
|
const { monitorId } = useParams();
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [dateRange, setDateRange] = useState<string>("day");
|
|
||||||
|
|
||||||
const monitorDetailsUrl = useMemo(() => {
|
|
||||||
if (!monitorId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("dateRange", dateRange);
|
|
||||||
return `/monitors/pagespeed/details/${monitorId}?${params.toString()}`;
|
|
||||||
}, [monitorId, dateRange]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: monitorData,
|
data: monitorData,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
} = useGet<PageSpeedDetailsResponse>(
|
} = useGet<PageSpeedDetailsResponse>(
|
||||||
monitorDetailsUrl,
|
monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null,
|
||||||
{},
|
{},
|
||||||
{ keepPreviousData: true, refreshInterval: 30000 }
|
{ keepPreviousData: true, refreshInterval: 30000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const monitor = monitorData?.monitorData?.monitor;
|
const monitor = monitorData?.monitor;
|
||||||
const groupedChecks = monitorData?.monitorData?.groupedChecks || [];
|
|
||||||
const monitorStats = monitorData?.monitorStats || null;
|
const monitorStats = monitorData?.monitorStats || null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -61,15 +47,9 @@ const PageSpeedDetails = () => {
|
|||||||
monitor={monitor}
|
monitor={monitor}
|
||||||
monitorStats={monitorStats}
|
monitorStats={monitorStats}
|
||||||
/>
|
/>
|
||||||
<HeaderTimeRange
|
|
||||||
isLoading={isLoading}
|
|
||||||
hasDateRange={true}
|
|
||||||
dateRange={dateRange}
|
|
||||||
setDateRange={setDateRange}
|
|
||||||
/>
|
|
||||||
<HistogramPageSpeedDetails
|
<HistogramPageSpeedDetails
|
||||||
checks={groupedChecks}
|
checks={monitor?.recentChecks || []}
|
||||||
range={dateRange}
|
range="day"
|
||||||
/>
|
/>
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: "column", md: "row" }}
|
direction={{ xs: "column", md: "row" }}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/Pa
|
|||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemo, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
|
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||||
@@ -20,6 +20,14 @@ const PageSpeedMonitorsPage = () => {
|
|||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
const { deleteFn, loading: isDeleting } = useDelete();
|
const { deleteFn, loading: isDeleting } = useDelete();
|
||||||
|
|
||||||
|
const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10";
|
||||||
|
const {
|
||||||
|
data: monitorsData,
|
||||||
|
isLoading: monitorsIsLoading,
|
||||||
|
error: monitorsError,
|
||||||
|
refetch,
|
||||||
|
} = useGet<MonitorsWithChecksResponse>(monitorsUrl, {}, { refreshInterval: 30000 });
|
||||||
|
|
||||||
const settingsUrl = "/settings";
|
const settingsUrl = "/settings";
|
||||||
const {
|
const {
|
||||||
data: settingsData,
|
data: settingsData,
|
||||||
@@ -34,28 +42,6 @@ const PageSpeedMonitorsPage = () => {
|
|||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||||
|
|
||||||
const monitorsWithChecksUrl = useMemo(() => {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("type", "pagespeed");
|
|
||||||
params.append("limit", "25");
|
|
||||||
if (page !== undefined) params.append("page", String(page));
|
|
||||||
if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage));
|
|
||||||
if (sortField) params.append("field", sortField);
|
|
||||||
if (sortOrder) params.append("order", sortOrder);
|
|
||||||
return `/monitors/team/with-checks?${params.toString()}`;
|
|
||||||
}, [page, rowsPerPage, sortField, sortOrder]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: monitorsData,
|
|
||||||
isLoading: monitorsIsLoading,
|
|
||||||
error: monitorsError,
|
|
||||||
refetch,
|
|
||||||
} = useGet<MonitorsWithChecksResponse>(
|
|
||||||
monitorsWithChecksUrl,
|
|
||||||
{},
|
|
||||||
{ refreshInterval: 30000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const monitors = monitorsData?.monitors;
|
const monitors = monitorsData?.monitors;
|
||||||
const monitorsCount = monitorsData?.count ?? 0;
|
const monitorsCount = monitorsData?.count ?? 0;
|
||||||
const summary = monitorsData?.summary ?? null;
|
const summary = monitorsData?.summary ?? null;
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements";
|
import { BasePage, ConfigBox, TextLink } from "@/Components/design-elements";
|
||||||
import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs";
|
import { Autocomplete, Select, Dialog, SwitchComponent } from "@/Components/inputs";
|
||||||
import { logger } from "@/Utils/logger";
|
import { Stack, useTheme, MenuItem, Link, type SelectChangeEvent } from "@mui/material";
|
||||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import {
|
|
||||||
Stack,
|
|
||||||
useTheme,
|
|
||||||
MenuItem,
|
|
||||||
Link,
|
|
||||||
Alert,
|
|
||||||
type SelectChangeEvent,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -17,13 +8,12 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import DummyChart from "@/Pages/Settings/DummyChart";
|
import DummyChart from "@/Pages/Settings/DummyChart";
|
||||||
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
||||||
import { useToast } from "@/Hooks/UseToast";
|
|
||||||
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||||
import type { SettingsFormData } from "@/Validation/settings";
|
import type { SettingsFormData } from "@/Validation/settings";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
import { TextField, Button, FieldLabel } from "@/Components/inputs";
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
import { useDelete } from "@/Hooks/UseApi";
|
import { useDelete } from "@/Hooks/UseApi";
|
||||||
|
|
||||||
@@ -37,7 +27,6 @@ import {
|
|||||||
} from "@/Features/UI/uiSlice.js";
|
} from "@/Features/UI/uiSlice.js";
|
||||||
import timezones from "@/Utils/timezones.json";
|
import timezones from "@/Utils/timezones.json";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
|
|
||||||
|
|
||||||
interface Timezone {
|
interface Timezone {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,13 +44,10 @@ export const SettingsPage = () => {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isAdmin = useIsAdmin();
|
const isAdmin = useIsAdmin();
|
||||||
const { toastError } = useToast();
|
|
||||||
// Local state for demo monitors dialog
|
// Local state for demo monitors dialog
|
||||||
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
|
const [isDemoMonitorsDialogOpen, setIsDemoMonitorsDialogOpen] = useState(false);
|
||||||
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
|
const { post: postDemoMonitors, loading: isPostingDemoMonitors } = usePost();
|
||||||
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
|
const { deleteFn: deleteAllMonitors, loading: isDeletingAllMonitors } = useDelete();
|
||||||
// Import monitors functionality
|
|
||||||
const { post: importMonitors, loading: isImportingMonitors } = usePost();
|
|
||||||
|
|
||||||
// Fetch settings data from API
|
// Fetch settings data from API
|
||||||
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
|
const { data: fetchedSettings } = useGet<SettingsResponse>("/settings");
|
||||||
@@ -216,36 +202,6 @@ export const SettingsPage = () => {
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = event.target.files?.[0];
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type !== "application/json") {
|
|
||||||
toastError("Please select a valid JSON file");
|
|
||||||
event.target.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const text = await file.text();
|
|
||||||
const monitors = JSON.parse(text);
|
|
||||||
|
|
||||||
if (!Array.isArray(monitors)) {
|
|
||||||
toastError("Invalid file format: expected an array of monitors");
|
|
||||||
event.target.value = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await importMonitors("/monitors/import/json", { monitors });
|
|
||||||
|
|
||||||
event.target.value = "";
|
|
||||||
} catch (error) {
|
|
||||||
toastError("Error parsing JSON file. Please check the file format.");
|
|
||||||
event.target.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (data: SettingsFormData) => {
|
const onSubmit = async (data: SettingsFormData) => {
|
||||||
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
|
// Don't send pagespeedApiKey if it's already set and user hasn't clicked reset
|
||||||
const dataToSend = { ...data };
|
const dataToSend = { ...data };
|
||||||
@@ -267,7 +223,8 @@ export const SettingsPage = () => {
|
|||||||
(acc as any)[typedKey] = Object.entries(value).reduce(
|
(acc as any)[typedKey] = Object.entries(value).reduce(
|
||||||
(nested, [nestedKey, nestedValue]) => ({
|
(nested, [nestedKey, nestedValue]) => ({
|
||||||
...nested,
|
...nested,
|
||||||
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
|
[nestedKey]:
|
||||||
|
nestedValue === undefined || nestedValue === 0 ? "" : nestedValue,
|
||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -291,7 +248,7 @@ export const SettingsPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onError = (errors: unknown) => {
|
const onError = (errors: unknown) => {
|
||||||
logger.debug("Form validation errors", errors);
|
console.log("Form validation errors:", errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
const languages = Object.keys(i18n.options.resources || {});
|
const languages = Object.keys(i18n.options.resources || {});
|
||||||
@@ -301,7 +258,7 @@ export const SettingsPage = () => {
|
|||||||
component="form"
|
component="form"
|
||||||
onSubmit={form.handleSubmit(onSubmit, onError)}
|
onSubmit={form.handleSubmit(onSubmit, onError)}
|
||||||
>
|
>
|
||||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
<Stack gap={theme.spacing(8)}>
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.settings.form.timezone.title")}
|
title={t("pages.settings.form.timezone.title")}
|
||||||
subtitle={t("pages.settings.form.timezone.description")}
|
subtitle={t("pages.settings.form.timezone.description")}
|
||||||
@@ -324,7 +281,7 @@ export const SettingsPage = () => {
|
|||||||
title={t("pages.settings.form.ui.title")}
|
title={t("pages.settings.form.ui.title")}
|
||||||
subtitle={t("pages.settings.form.ui.description")}
|
subtitle={t("pages.settings.form.ui.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
<Stack gap={theme.spacing(8)}>
|
||||||
<Select
|
<Select
|
||||||
value={mode}
|
value={mode}
|
||||||
onChange={handleModeChange}
|
onChange={handleModeChange}
|
||||||
@@ -463,43 +420,6 @@ export const SettingsPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Check Retention */}
|
|
||||||
{isAdmin && (
|
|
||||||
<ConfigBox
|
|
||||||
title={t("pages.settings.form.retention.title")}
|
|
||||||
subtitle={t("pages.settings.form.retention.description")}
|
|
||||||
rightContent={
|
|
||||||
<Controller
|
|
||||||
name="checkTTL"
|
|
||||||
control={form.control}
|
|
||||||
defaultValue={defaults.checkTTL}
|
|
||||||
render={({ field }) => (
|
|
||||||
<SliderWithLabel
|
|
||||||
{...field}
|
|
||||||
fieldLabel={t("pages.settings.form.retention.option.days.label")}
|
|
||||||
min={1}
|
|
||||||
max={CHECK_TTL_SENTINEL}
|
|
||||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
|
||||||
value={field.value || 30}
|
|
||||||
onChange={(_, value) => field.onChange(value)}
|
|
||||||
valueLabelDisplay="auto"
|
|
||||||
valueLabelFormat={(value: number) =>
|
|
||||||
value >= CHECK_TTL_SENTINEL
|
|
||||||
? t("pages.settings.form.retention.option.days.unlimited")
|
|
||||||
: `${value}`
|
|
||||||
}
|
|
||||||
formatDisplayValue={(value: number) =>
|
|
||||||
value >= CHECK_TTL_SENTINEL
|
|
||||||
? t("pages.settings.form.retention.option.days.unlimited")
|
|
||||||
: `${value}`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Global Thresholds */}
|
{/* Global Thresholds */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
@@ -511,16 +431,24 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.cpu"
|
name="globalThresholds.cpu"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.cpu}
|
defaultValue={defaults.globalThresholds?.cpu}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SliderWithLabel
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
|
value={
|
||||||
|
field.value === undefined || field.value === 0 ? "" : field.value
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
field.onChange(val === "" ? 0 : Number(val));
|
||||||
|
}}
|
||||||
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
|
fieldLabel={t("pages.settings.form.thresholds.option.cpu.label")}
|
||||||
min={1}
|
type="number"
|
||||||
max={100}
|
inputProps={{ min: 0 }}
|
||||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
placeholder={t(
|
||||||
value={field.value || 1}
|
"pages.settings.form.thresholds.option.cpu.placeholder"
|
||||||
onChange={(_, value) => field.onChange(value)}
|
)}
|
||||||
valueLabelDisplay="auto"
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -528,16 +456,24 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.memory"
|
name="globalThresholds.memory"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.memory}
|
defaultValue={defaults.globalThresholds?.memory}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SliderWithLabel
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
|
value={
|
||||||
|
field.value === undefined || field.value === 0 ? "" : field.value
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
field.onChange(val === "" ? 0 : Number(val));
|
||||||
|
}}
|
||||||
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
|
fieldLabel={t("pages.settings.form.thresholds.option.memory.label")}
|
||||||
min={1}
|
type="number"
|
||||||
max={100}
|
inputProps={{ min: 0 }}
|
||||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
placeholder={t(
|
||||||
value={field.value || 1}
|
"pages.settings.form.thresholds.option.memory.placeholder"
|
||||||
onChange={(_, value) => field.onChange(value)}
|
)}
|
||||||
valueLabelDisplay="auto"
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -545,16 +481,24 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.disk"
|
name="globalThresholds.disk"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.disk}
|
defaultValue={defaults.globalThresholds?.disk}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SliderWithLabel
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
|
value={
|
||||||
|
field.value === undefined || field.value === 0 ? "" : field.value
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
field.onChange(val === "" ? 0 : Number(val));
|
||||||
|
}}
|
||||||
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
|
fieldLabel={t("pages.settings.form.thresholds.option.disk.label")}
|
||||||
min={1}
|
type="number"
|
||||||
max={100}
|
inputProps={{ min: 0 }}
|
||||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
placeholder={t(
|
||||||
value={field.value || 1}
|
"pages.settings.form.thresholds.option.disk.placeholder"
|
||||||
onChange={(_, value) => field.onChange(value)}
|
)}
|
||||||
valueLabelDisplay="auto"
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -562,18 +506,26 @@ export const SettingsPage = () => {
|
|||||||
name="globalThresholds.temperature"
|
name="globalThresholds.temperature"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
defaultValue={defaults.globalThresholds?.temperature}
|
defaultValue={defaults.globalThresholds?.temperature}
|
||||||
render={({ field }) => (
|
render={({ field, fieldState }) => (
|
||||||
<SliderWithLabel
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
|
value={
|
||||||
|
field.value === undefined || field.value === 0 ? "" : field.value
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
field.onChange(val === "" ? 0 : Number(val));
|
||||||
|
}}
|
||||||
fieldLabel={t(
|
fieldLabel={t(
|
||||||
"pages.settings.form.thresholds.option.temperature.label"
|
"pages.settings.form.thresholds.option.temperature.label"
|
||||||
)}
|
)}
|
||||||
min={1}
|
type="number"
|
||||||
max={150}
|
inputProps={{ min: 0 }}
|
||||||
sliderMaxWidth={{ xs: "100%", md: "50%" }}
|
placeholder={t(
|
||||||
value={field.value || 1}
|
"pages.settings.form.thresholds.option.temperature.placeholder"
|
||||||
onChange={(_, value) => field.onChange(value)}
|
)}
|
||||||
valueLabelDisplay="auto"
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -589,7 +541,7 @@ export const SettingsPage = () => {
|
|||||||
title={t("pages.settings.form.email.title")}
|
title={t("pages.settings.form.email.title")}
|
||||||
subtitle={t("pages.settings.form.email.description")}
|
subtitle={t("pages.settings.form.email.description")}
|
||||||
leftContent={
|
leftContent={
|
||||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
<Stack gap={theme.spacing(8)}>
|
||||||
<TextLink
|
<TextLink
|
||||||
text={t("pages.settings.form.email.descriptionTransport")}
|
text={t("pages.settings.form.email.descriptionTransport")}
|
||||||
linkText={t("pages.settings.form.email.descriptionTransportLink")}
|
linkText={t("pages.settings.form.email.descriptionTransportLink")}
|
||||||
@@ -637,7 +589,7 @@ export const SettingsPage = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
<Stack gap={theme.spacing(10)}>
|
||||||
{/* Email Host */}
|
{/* Email Host */}
|
||||||
<Controller
|
<Controller
|
||||||
name="systemEmailHost"
|
name="systemEmailHost"
|
||||||
@@ -724,7 +676,7 @@ export const SettingsPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={theme.spacing(LAYOUT.XS)}
|
gap={theme.spacing(4)}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -799,7 +751,7 @@ export const SettingsPage = () => {
|
|||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: theme.spacing(LAYOUT.XS),
|
gap: theme.spacing(4),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
@@ -914,34 +866,17 @@ export const SettingsPage = () => {
|
|||||||
{/* Export Monitors - Admin Only */}
|
{/* Export Monitors - Admin Only */}
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.settings.form.importExportMonitors.title")}
|
title={t("pages.settings.form.exportMonitors.title")}
|
||||||
subtitle={t("pages.settings.form.importExportMonitors.description")}
|
subtitle={t("pages.settings.form.exportMonitors.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack
|
<Box>
|
||||||
gap={theme.spacing(LAYOUT.MD)}
|
|
||||||
direction={"row"}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="monitor-import-input"
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={handleFileSelect}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => document.getElementById("monitor-import-input")?.click()}
|
|
||||||
disabled={isImportingMonitors}
|
|
||||||
>
|
|
||||||
{t("common.buttons.importFromJSON")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={handleExportMonitors}
|
onClick={handleExportMonitors}
|
||||||
>
|
>
|
||||||
{t("common.buttons.exportToJSON")}
|
{t("common.buttons.exportToJSON")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -1004,46 +939,13 @@ export const SettingsPage = () => {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
padding: theme.spacing(LAYOUT.MD),
|
padding: theme.spacing(8),
|
||||||
marginLeft: theme.spacing(-LAYOUT.MD),
|
marginLeft: theme.spacing(-8),
|
||||||
marginRight: theme.spacing(-LAYOUT.MD),
|
marginRight: theme.spacing(-8),
|
||||||
marginBottom: theme.spacing(-LAYOUT.MD),
|
marginBottom: theme.spacing(-8),
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Validation Error Display */}
|
|
||||||
{Object.keys(form.formState.errors).length > 0 && (
|
|
||||||
<Alert
|
|
||||||
severity="error"
|
|
||||||
sx={{ mb: 2, flexGrow: 1, mr: 2 }}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
sx={{ fontWeight: 600, mb: 1 }}
|
|
||||||
>
|
|
||||||
{t("pages.settings.form.validation.errorMessage")}
|
|
||||||
</Typography>
|
|
||||||
<Box
|
|
||||||
component="ul"
|
|
||||||
sx={{ m: 0, pl: 2 }}
|
|
||||||
>
|
|
||||||
{Object.entries(form.formState.errors).map(([field, error]) => {
|
|
||||||
const message =
|
|
||||||
typeof error === "object" && error?.message
|
|
||||||
? error.message
|
|
||||||
: "Invalid value";
|
|
||||||
return (
|
|
||||||
<li key={field}>
|
|
||||||
<Typography variant="body2">
|
|
||||||
<strong>{field}:</strong> {message}
|
|
||||||
</Typography>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
loading={isSaving}
|
loading={isSaving}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
import { BasePage, ConfigBox } from "@/Components/design-elements";
|
||||||
import Stack from "@mui/material/Stack";
|
import Stack from "@mui/material/Stack";
|
||||||
import { logger } from "@/Utils/logger";
|
|
||||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
@@ -82,7 +80,7 @@ const CreateStatusPage = () => {
|
|||||||
}, [defaults, reset]);
|
}, [defaults, reset]);
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
logger.debug("Status page validation errors", errors);
|
console.log(errors);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteClick = () => {
|
const handleDeleteClick = () => {
|
||||||
@@ -130,7 +128,7 @@ const CreateStatusPage = () => {
|
|||||||
fd.append("logo", imageResult.data);
|
fd.append("logo", imageResult.data);
|
||||||
URL.revokeObjectURL(data.logo.data);
|
URL.revokeObjectURL(data.logo.data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to fetch logo blob", e instanceof Error ? e : undefined);
|
console.error("Error fetching logo blob:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +165,7 @@ const CreateStatusPage = () => {
|
|||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(SPACING.MD)}
|
spacing={theme.spacing(2)}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="isPublished"
|
name="isPublished"
|
||||||
@@ -189,7 +187,7 @@ const CreateStatusPage = () => {
|
|||||||
title={t("pages.statusPages.form.basicInfo.title")}
|
title={t("pages.statusPages.form.basicInfo.title")}
|
||||||
subtitle={t("pages.statusPages.form.basicInfo.description")}
|
subtitle={t("pages.statusPages.form.basicInfo.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(6)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="companyName"
|
name="companyName"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -244,7 +242,7 @@ const CreateStatusPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(4)}>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
multiple
|
multiple
|
||||||
options={monitors}
|
options={monitors}
|
||||||
@@ -292,9 +290,9 @@ const CreateStatusPage = () => {
|
|||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
spacing={theme.spacing(LAYOUT.XS)}
|
spacing={theme.spacing(4)}
|
||||||
padding={theme.spacing(LAYOUT.XS)}
|
padding={theme.spacing(4)}
|
||||||
marginTop={theme.spacing(SPACING.LG)}
|
marginTop={theme.spacing(2)}
|
||||||
borderRadius={1}
|
borderRadius={1}
|
||||||
sx={{
|
sx={{
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
@@ -368,7 +366,7 @@ const CreateStatusPage = () => {
|
|||||||
title={t("pages.statusPages.form.appearance.title")}
|
title={t("pages.statusPages.form.appearance.title")}
|
||||||
subtitle={t("pages.statusPages.form.appearance.description")}
|
subtitle={t("pages.statusPages.form.appearance.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(8)}>
|
||||||
<Stack alignItems={"center"}>
|
<Stack alignItems={"center"}>
|
||||||
<Controller
|
<Controller
|
||||||
name="logo"
|
name="logo"
|
||||||
@@ -409,7 +407,7 @@ const CreateStatusPage = () => {
|
|||||||
title={t("pages.statusPages.form.features.title")}
|
title={t("pages.statusPages.form.features.title")}
|
||||||
subtitle={t("pages.statusPages.form.features.description")}
|
subtitle={t("pages.statusPages.form.features.description")}
|
||||||
rightContent={
|
rightContent={
|
||||||
<Stack spacing={theme.spacing(LAYOUT.MD)}>
|
<Stack spacing={theme.spacing(2)}>
|
||||||
<Controller
|
<Controller
|
||||||
name="showCharts"
|
name="showCharts"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "date",
|
id: "date",
|
||||||
content: t("common.table.headers.dateTime"),
|
content: t("pages.checks.table.headers.dateTime"),
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import type { Header } from "@/Components/design-elements";
|
|
||||||
import type { FlatGeoCheck } from "@/Types/GeoCheck";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
|
||||||
import type { RootState } from "@/Types/state";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import prettyMilliseconds from "pretty-ms";
|
|
||||||
|
|
||||||
const getHeaders = (t: Function, uiTimezone: string) => {
|
|
||||||
const headers: Header<FlatGeoCheck>[] = [
|
|
||||||
{
|
|
||||||
id: "status",
|
|
||||||
content: t("common.table.headers.status"),
|
|
||||||
render: (row) => {
|
|
||||||
const status = row.status ? "up" : "down";
|
|
||||||
return <StatusLabel status={status} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "date",
|
|
||||||
content: t("common.table.headers.dateTime"),
|
|
||||||
render: (row) => {
|
|
||||||
return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "statusCode",
|
|
||||||
content: t("pages.checks.table.headers.statusCode"),
|
|
||||||
render: (row) => {
|
|
||||||
return row.statusCode || "N/A";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "location",
|
|
||||||
content: t("pages.checks.table.headers.location"),
|
|
||||||
render: (row) => {
|
|
||||||
const location = row.location;
|
|
||||||
if (!location) return "N/A";
|
|
||||||
const { continent, country, city } = location;
|
|
||||||
return `${continent} - ${country}, ${city}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "responseTime",
|
|
||||||
content: t("common.table.headers.responseTime"),
|
|
||||||
render: (row) => {
|
|
||||||
if (!row.timings?.total) return "N/A";
|
|
||||||
return prettyMilliseconds(row.timings.total, { compact: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return headers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GeoChecksTable = ({
|
|
||||||
geoChecks,
|
|
||||||
count,
|
|
||||||
page,
|
|
||||||
setPage,
|
|
||||||
rowsPerPage,
|
|
||||||
setRowsPerPage,
|
|
||||||
}: {
|
|
||||||
geoChecks: FlatGeoCheck[];
|
|
||||||
count: number;
|
|
||||||
page: number;
|
|
||||||
setPage: (page: number) => void;
|
|
||||||
rowsPerPage: number;
|
|
||||||
setRowsPerPage: (rowsPerPage: number) => void;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const uiTimezone = useSelector((state: RootState) => state.ui.timezone);
|
|
||||||
const headers = getHeaders(t, uiTimezone);
|
|
||||||
|
|
||||||
const handlePageChange = (
|
|
||||||
_e: React.MouseEvent<HTMLButtonElement> | null,
|
|
||||||
newPage: number
|
|
||||||
) => {
|
|
||||||
setPage(newPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowsPerPageChange = (
|
|
||||||
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
|
|
||||||
) => {
|
|
||||||
const value = Number(e.target.value);
|
|
||||||
setPage(0);
|
|
||||||
setRowsPerPage(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Table
|
|
||||||
headers={headers}
|
|
||||||
data={geoChecks}
|
|
||||||
/>
|
|
||||||
<Pagination
|
|
||||||
component="div"
|
|
||||||
count={count}
|
|
||||||
page={page}
|
|
||||||
rowsPerPage={rowsPerPage}
|
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onRowsPerPageChange={handleRowsPerPageChange}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -6,12 +6,9 @@ import {
|
|||||||
RadialAvgResponse,
|
RadialAvgResponse,
|
||||||
HistogramDetails,
|
HistogramDetails,
|
||||||
HeaderMonitorControls,
|
HeaderMonitorControls,
|
||||||
HeaderGeoTabs,
|
|
||||||
GeoChecksMap,
|
|
||||||
} from "@/Components/monitors";
|
} from "@/Components/monitors";
|
||||||
import { TrendingUp, AlertTriangle } from "lucide-react";
|
import { TrendingUp, AlertTriangle } from "lucide-react";
|
||||||
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
|
import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable";
|
||||||
import { GeoChecksTable } from "@/Pages/Uptime/Details/Components/GeoChecksTable";
|
|
||||||
import { MonitorStatBoxes } from "@/Components/monitors";
|
import { MonitorStatBoxes } from "@/Components/monitors";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
@@ -20,17 +17,11 @@ import { useState, useMemo } from "react";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useGet } from "@/Hooks/UseApi";
|
import { useGet } from "@/Hooks/UseApi";
|
||||||
import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor";
|
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
||||||
import type { ChecksResponse } from "@/Types/Check";
|
import type { ChecksResponse } from "@/Types/Check";
|
||||||
import type {
|
|
||||||
GeoChecksResult,
|
|
||||||
FlatGeoChecksResponse,
|
|
||||||
GeoContinent,
|
|
||||||
} from "@/Types/GeoCheck";
|
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
|
|
||||||
const certificateDateFormat = "MMM D, YYYY h A";
|
const certificateDateFormat = "MMM D, YYYY h A";
|
||||||
|
|
||||||
@@ -46,10 +37,7 @@ const UptimeDetailsPage = () => {
|
|||||||
|
|
||||||
const [page, setPage] = useState<number>(0);
|
const [page, setPage] = useState<number>(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
|
const [rowsPerPage, setRowsPerPage] = useState<number>(5);
|
||||||
const [geoPage, setGeoPage] = useState<number>(0);
|
|
||||||
const [geoRowsPerPage, setGeoRowsPerPage] = useState<number>(5);
|
|
||||||
const [dateRange, setDateRange] = useState<string>("recent");
|
const [dateRange, setDateRange] = useState<string>("recent");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<GeoContinent>("NA");
|
|
||||||
|
|
||||||
const monitorDetailsUrl = useMemo(() => {
|
const monitorDetailsUrl = useMemo(() => {
|
||||||
if (!monitorId) {
|
if (!monitorId) {
|
||||||
@@ -68,7 +56,7 @@ const UptimeDetailsPage = () => {
|
|||||||
} = useGet<MonitorDetailsResponse>(
|
} = useGet<MonitorDetailsResponse>(
|
||||||
monitorDetailsUrl,
|
monitorDetailsUrl,
|
||||||
{},
|
{},
|
||||||
{ refreshInterval: 10000, keepPreviousData: true, revalidateOnFocus: false }
|
{ refreshInterval: 10000, keepPreviousData: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const monitorData = monitorDetailsData?.monitorData;
|
const monitorData = monitorDetailsData?.monitorData;
|
||||||
@@ -83,11 +71,7 @@ const UptimeDetailsPage = () => {
|
|||||||
return `/monitors/certificate/${monitorId}`;
|
return `/monitors/certificate/${monitorId}`;
|
||||||
}, [monitorId, monitor?.type]);
|
}, [monitorId, monitor?.type]);
|
||||||
|
|
||||||
const { data: certificateData } = useGet<CertificateResponse>(
|
const { data: certificateData } = useGet<CertificateResponse>(certificateUrl);
|
||||||
certificateUrl,
|
|
||||||
{},
|
|
||||||
{ revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const certificateExpiry = useMemo(() => {
|
const certificateExpiry = useMemo(() => {
|
||||||
if (!certificateData?.certificateDate) {
|
if (!certificateData?.certificateDate) {
|
||||||
@@ -118,58 +102,9 @@ const UptimeDetailsPage = () => {
|
|||||||
const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>(
|
const { data: checksData, isLoading: checksIsLoading } = useGet<ChecksResponse>(
|
||||||
checksUrl,
|
checksUrl,
|
||||||
{},
|
{},
|
||||||
{ keepPreviousData: true, revalidateOnFocus: false }
|
{ keepPreviousData: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const geoChecksUrl = useMemo(() => {
|
|
||||||
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("dateRange", dateRange);
|
|
||||||
params.append("continent", selectedLocation);
|
|
||||||
return `/monitors/${monitorId}/geo-checks?${params.toString()}`;
|
|
||||||
}, [monitorId, monitor?.type, monitor?.geoCheckEnabled, dateRange, selectedLocation]);
|
|
||||||
|
|
||||||
const { data: geoGroupedData } = useGet<GeoChecksResult>(
|
|
||||||
geoChecksUrl,
|
|
||||||
{},
|
|
||||||
{ keepPreviousData: true, revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const geoGroupedChecks = geoGroupedData?.groupedGeoChecks ?? [];
|
|
||||||
|
|
||||||
// Fetch paginated geo checks for the table
|
|
||||||
const geoChecksTableUrl = useMemo(() => {
|
|
||||||
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
params.append("sortOrder", "desc");
|
|
||||||
params.append("dateRange", dateRange);
|
|
||||||
params.append("page", String(geoPage));
|
|
||||||
params.append("rowsPerPage", String(geoRowsPerPage));
|
|
||||||
return `/geo-checks/${monitorId}?${params.toString()}`;
|
|
||||||
}, [
|
|
||||||
monitorId,
|
|
||||||
monitor?.type,
|
|
||||||
monitor?.geoCheckEnabled,
|
|
||||||
dateRange,
|
|
||||||
geoPage,
|
|
||||||
geoRowsPerPage,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { data: geoChecksTableData } = useGet<FlatGeoChecksResponse>(
|
|
||||||
geoChecksTableUrl,
|
|
||||||
{},
|
|
||||||
{ keepPreviousData: true, revalidateOnFocus: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const geoChecksForTable = geoChecksTableData?.geoChecks ?? [];
|
|
||||||
const geoChecksCount = geoChecksTableData?.geoChecksCount ?? 0;
|
|
||||||
|
|
||||||
const geoLocations = monitor?.geoCheckLocations;
|
|
||||||
|
|
||||||
const checks = checksData?.checks ?? [];
|
const checks = checksData?.checks ?? [];
|
||||||
const checksCount = checksData?.checksCount ?? 0;
|
const checksCount = checksData?.checksCount ?? 0;
|
||||||
|
|
||||||
@@ -192,7 +127,6 @@ const UptimeDetailsPage = () => {
|
|||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
setDateRange={setDateRange}
|
setDateRange={setDateRange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
direction={{ xs: "column", md: "row" }}
|
direction={{ xs: "column", md: "row" }}
|
||||||
gap={theme.spacing(8)}
|
gap={theme.spacing(8)}
|
||||||
@@ -226,31 +160,6 @@ const UptimeDetailsPage = () => {
|
|||||||
rowsPerPage={rowsPerPage}
|
rowsPerPage={rowsPerPage}
|
||||||
setRowsPerPage={setRowsPerPage}
|
setRowsPerPage={setRowsPerPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{monitor?.geoCheckEnabled && (
|
|
||||||
<>
|
|
||||||
<Typography variant="h1">Location breakdown</Typography>
|
|
||||||
<HeaderGeoTabs
|
|
||||||
geoCheckEnabled={monitor?.geoCheckEnabled ?? false}
|
|
||||||
locations={geoLocations}
|
|
||||||
selectedLocation={selectedLocation}
|
|
||||||
onLocationChange={setSelectedLocation}
|
|
||||||
/>
|
|
||||||
<HistogramDetails
|
|
||||||
checks={geoGroupedChecks}
|
|
||||||
range={dateRange}
|
|
||||||
/>
|
|
||||||
<GeoChecksTable
|
|
||||||
geoChecks={geoChecksForTable}
|
|
||||||
count={geoChecksCount}
|
|
||||||
page={geoPage}
|
|
||||||
setPage={setGeoPage}
|
|
||||||
rowsPerPage={geoRowsPerPage}
|
|
||||||
setRowsPerPage={setGeoRowsPerPage}
|
|
||||||
/>
|
|
||||||
<GeoChecksMap geoChecks={geoChecksForTable} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</BasePage>
|
</BasePage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -181,7 +181,6 @@ export const MonitorTable = ({
|
|||||||
onClick={(e) => handleSort(e, "status")}
|
onClick={(e) => handleSort(e, "status")}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<Box width={theme.spacing(8)} />
|
|
||||||
{t("common.table.headers.status")}
|
{t("common.table.headers.status")}
|
||||||
{renderSortIcon(sortField === "status")}
|
{renderSortIcon(sortField === "status")}
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -212,7 +211,6 @@ export const MonitorTable = ({
|
|||||||
onClick={(e) => handleSort(e, "type")}
|
onClick={(e) => handleSort(e, "type")}
|
||||||
sx={{ cursor: "pointer" }}
|
sx={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<Box width={theme.spacing(8)} />
|
|
||||||
{t("common.table.headers.type")}
|
{t("common.table.headers.type")}
|
||||||
{renderSortIcon(sortField === "type")}
|
{renderSortIcon(sortField === "type")}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ const UptimeMonitorsPage = () => {
|
|||||||
const debouncedSearch = useDebounce<string>(search, 300);
|
const debouncedSearch = useDebounce<string>(search, 300);
|
||||||
|
|
||||||
// Convert filter selections to API filter values
|
// Convert filter selections to API filter values
|
||||||
// Status: pass "up"/"down" directly to the API
|
// Status: "up" -> true, "down" -> false
|
||||||
// State: "active" -> true, "paused" -> false
|
// State: "active" -> true, "paused" -> false
|
||||||
const toFilterStatus = useMemo(() => {
|
const toFilterStatus = useMemo(() => {
|
||||||
if (selectedStatus === "up") return "up";
|
if (selectedStatus === "up") return "true";
|
||||||
if (selectedStatus === "down") return "down";
|
if (selectedStatus === "down") return "false";
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [selectedStatus]);
|
}, [selectedStatus]);
|
||||||
|
|
||||||
@@ -68,9 +68,7 @@ const UptimeMonitorsPage = () => {
|
|||||||
|
|
||||||
// Default to all types when none selected
|
// Default to all types when none selected
|
||||||
const effectiveTypes =
|
const effectiveTypes =
|
||||||
selectedTypes.length > 0
|
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port", "game"];
|
||||||
? selectedTypes
|
|
||||||
: ["http", "ping", "docker", "port", "game", "grpc", "websocket"];
|
|
||||||
|
|
||||||
// Build URL for monitors with checks
|
// Build URL for monitors with checks
|
||||||
const monitorsWithChecksUrl = useMemo(() => {
|
const monitorsWithChecksUrl = useMemo(() => {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const CHECK_TTL_SENTINEL = 366;
|
|
||||||
|
|
||||||
export interface CheckMetadata {
|
export interface CheckMetadata {
|
||||||
monitorId: string;
|
monitorId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@@ -127,6 +125,9 @@ export interface Check {
|
|||||||
timings?: CheckTimings;
|
timings?: CheckTimings;
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
message: string;
|
message: string;
|
||||||
|
ack: boolean;
|
||||||
|
ackAt?: string | null;
|
||||||
|
expiry: string;
|
||||||
cpu?: CheckCpuInfo;
|
cpu?: CheckCpuInfo;
|
||||||
memory?: CheckMemoryInfo;
|
memory?: CheckMemoryInfo;
|
||||||
disk?: CheckDiskInfo[];
|
disk?: CheckDiskInfo[];
|
||||||
@@ -139,6 +140,7 @@ export interface Check {
|
|||||||
seo?: number;
|
seo?: number;
|
||||||
performance?: number;
|
performance?: number;
|
||||||
audits?: CheckAudits;
|
audits?: CheckAudits;
|
||||||
|
__v: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -149,15 +151,6 @@ export interface GroupedCheck {
|
|||||||
totalChecks: number;
|
totalChecks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageSpeedGroupedCheck {
|
|
||||||
bucketDate: string;
|
|
||||||
performance: number;
|
|
||||||
accessibility: number;
|
|
||||||
bestPractices: number;
|
|
||||||
seo: number;
|
|
||||||
totalChecks: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LatestCheck {
|
export interface LatestCheck {
|
||||||
status: boolean;
|
status: boolean;
|
||||||
responseTime: number;
|
responseTime: number;
|
||||||
@@ -241,7 +234,10 @@ export interface ChecksSummary {
|
|||||||
downChecks: number;
|
downChecks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt"> & {
|
export type CheckSnapshot = Omit<
|
||||||
|
Check,
|
||||||
|
"metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt"
|
||||||
|
> & {
|
||||||
originalResponseTime: number;
|
originalResponseTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
export const GeoContinents = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
|
|
||||||
export type GeoContinent = (typeof GeoContinents)[number];
|
|
||||||
|
|
||||||
export interface GeoCheckMetadata {
|
|
||||||
monitorId: string;
|
|
||||||
teamId: string;
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoCheckTimings {
|
|
||||||
total: number;
|
|
||||||
dns: number;
|
|
||||||
tcp: number;
|
|
||||||
tls: number;
|
|
||||||
firstByte: number;
|
|
||||||
download: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoCheckLocation {
|
|
||||||
continent: GeoContinent;
|
|
||||||
region: string;
|
|
||||||
country: string;
|
|
||||||
state: string;
|
|
||||||
city: string;
|
|
||||||
longitude: number;
|
|
||||||
latitude: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoCheckResult {
|
|
||||||
location: GeoCheckLocation;
|
|
||||||
status: boolean;
|
|
||||||
statusCode: number;
|
|
||||||
timings: GeoCheckTimings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoCheck {
|
|
||||||
id: string;
|
|
||||||
metadata: GeoCheckMetadata;
|
|
||||||
results: GeoCheckResult[];
|
|
||||||
expiry: string;
|
|
||||||
__v: number;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlatGeoCheck {
|
|
||||||
id: string;
|
|
||||||
monitorId: string;
|
|
||||||
teamId: string;
|
|
||||||
type: string;
|
|
||||||
location: GeoCheckLocation;
|
|
||||||
status: boolean;
|
|
||||||
statusCode: number;
|
|
||||||
timings: GeoCheckTimings;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GroupedGeoCheck {
|
|
||||||
bucketDate: string;
|
|
||||||
continent: GeoContinent;
|
|
||||||
avgResponseTime: number;
|
|
||||||
totalChecks: number;
|
|
||||||
uptimePercentage: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoChecksResult {
|
|
||||||
monitorType: string;
|
|
||||||
groupedGeoChecks: GroupedGeoCheck[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GeoChecksResponse {
|
|
||||||
geoChecks: GeoCheck[];
|
|
||||||
geoChecksCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlatGeoChecksResponse {
|
|
||||||
geoChecks: FlatGeoCheck[];
|
|
||||||
geoChecksCount: number;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
|
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
|
||||||
import type { PageSpeedGroupedCheck } from "@/Types/Check";
|
|
||||||
import type { GeoContinent } from "@/Types/GeoCheck";
|
|
||||||
export type { GeoContinent } from "@/Types/GeoCheck";
|
|
||||||
|
|
||||||
export const MonitorTypes = [
|
export const MonitorTypes = [
|
||||||
"http",
|
"http",
|
||||||
@@ -11,21 +8,10 @@ export const MonitorTypes = [
|
|||||||
"docker",
|
"docker",
|
||||||
"port",
|
"port",
|
||||||
"game",
|
"game",
|
||||||
"grpc",
|
|
||||||
"websocket",
|
|
||||||
"unknown",
|
"unknown",
|
||||||
] as const;
|
] as const;
|
||||||
export type MonitorType = (typeof MonitorTypes)[number];
|
export type MonitorType = (typeof MonitorTypes)[number];
|
||||||
|
|
||||||
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
|
||||||
|
|
||||||
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
|
|
||||||
if (!type) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return GeoCheckSupportedTypes.includes(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MonitorStatuses = [
|
export const MonitorStatuses = [
|
||||||
"up",
|
"up",
|
||||||
"down",
|
"down",
|
||||||
@@ -71,11 +57,7 @@ export interface Monitor {
|
|||||||
tempAlertCounter: number;
|
tempAlertCounter: number;
|
||||||
selectedDisks: string[];
|
selectedDisks: string[];
|
||||||
gameId?: string;
|
gameId?: string;
|
||||||
grpcServiceName?: string;
|
|
||||||
group: string | null;
|
group: string | null;
|
||||||
geoCheckEnabled?: boolean;
|
|
||||||
geoCheckLocations?: GeoContinent[];
|
|
||||||
geoCheckInterval?: number;
|
|
||||||
recentChecks: CheckSnapshot[];
|
recentChecks: CheckSnapshot[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -130,10 +112,7 @@ export interface MonitorDetailsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PageSpeedDetailsResponse {
|
export interface PageSpeedDetailsResponse {
|
||||||
monitorData: {
|
monitor: MonitorWithChecks;
|
||||||
monitor: Monitor;
|
|
||||||
groupedChecks: PageSpeedGroupedCheck[];
|
|
||||||
};
|
|
||||||
monitorStats: MonitorStats | null;
|
monitorStats: MonitorStats | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export const NotificationChannels = [
|
|||||||
"webhook",
|
"webhook",
|
||||||
"pager_duty",
|
"pager_duty",
|
||||||
"matrix",
|
"matrix",
|
||||||
"teams",
|
|
||||||
] as const;
|
] as const;
|
||||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||||
|
|
||||||
|
|||||||
Vendored
-6
@@ -4,7 +4,6 @@ declare const __APP_VERSION__: string;
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_API_V2_BASE_URL?: string;
|
readonly VITE_APP_API_V2_BASE_URL?: string;
|
||||||
readonly VITE_APP_LOG_LEVEL?: "debug" | "info" | "warn" | "error";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
@@ -18,8 +17,3 @@ declare module "*.svg?react" {
|
|||||||
>;
|
>;
|
||||||
export default ReactComponent;
|
export default ReactComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module "*.css" {
|
|
||||||
const content: string;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,6 @@ export const getMonitorPath = (type: MonitorType): string => {
|
|||||||
port: "uptime",
|
port: "uptime",
|
||||||
ping: "uptime",
|
ping: "uptime",
|
||||||
game: "uptime",
|
game: "uptime",
|
||||||
grpc: "uptime",
|
|
||||||
websocket: "uptime",
|
|
||||||
unknown: "uptime",
|
unknown: "uptime",
|
||||||
docker: "uptime",
|
docker: "uptime",
|
||||||
hardware: "infrastructure",
|
hardware: "infrastructure",
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ export const typographyLevels = {
|
|||||||
export const colors = {
|
export const colors = {
|
||||||
gray200: "#EFEFEF",
|
gray200: "#EFEFEF",
|
||||||
gray700: "#313131",
|
gray700: "#313131",
|
||||||
gray900: "#151518",
|
|
||||||
gray850: "#1c1c21",
|
|
||||||
blueBlueWave: "#1570EF",
|
blueBlueWave: "#1570EF",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,8 +31,4 @@ export const darkPalette = {
|
|||||||
secondary: {
|
secondary: {
|
||||||
main: colors.gray700,
|
main: colors.gray700,
|
||||||
},
|
},
|
||||||
background: {
|
|
||||||
default: colors.gray900,
|
|
||||||
paper: colors.gray850,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
export const SPACING = {
|
|
||||||
XS: 0.5,
|
|
||||||
SM: 1,
|
|
||||||
MD: 1.5,
|
|
||||||
LG: 2,
|
|
||||||
XL: 2.5,
|
|
||||||
XXL: 3,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const LAYOUT = {
|
|
||||||
XS: 4,
|
|
||||||
SM: 6,
|
|
||||||
MD: 8,
|
|
||||||
LG: 10,
|
|
||||||
XL: 12,
|
|
||||||
XXL: 16,
|
|
||||||
} as const;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import type { LogLevel } from "@/Types/Log";
|
|
||||||
|
|
||||||
interface LogContext {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
||||||
debug: 0,
|
|
||||||
info: 1,
|
|
||||||
warn: 2,
|
|
||||||
error: 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ErrorLogData {
|
|
||||||
timestamp: string;
|
|
||||||
level: LogLevel;
|
|
||||||
message: string;
|
|
||||||
error?: {
|
|
||||||
message: string;
|
|
||||||
name: string;
|
|
||||||
stack?: string;
|
|
||||||
};
|
|
||||||
context?: LogContext;
|
|
||||||
url: string;
|
|
||||||
userAgent: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuredLevel: LogLevel = import.meta.env.VITE_APP_LOG_LEVEL || "error";
|
|
||||||
const configuredPriority = LOG_LEVEL_PRIORITY[configuredLevel];
|
|
||||||
|
|
||||||
const shouldLog = (level: LogLevel): boolean => {
|
|
||||||
return LOG_LEVEL_PRIORITY[level] >= configuredPriority;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatError = (error?: Error) => {
|
|
||||||
if (!error) return undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: error.message,
|
|
||||||
name: error.name,
|
|
||||||
stack: error.stack,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createLogData = (
|
|
||||||
level: LogLevel,
|
|
||||||
message: string,
|
|
||||||
error?: Error,
|
|
||||||
context?: LogContext
|
|
||||||
): ErrorLogData => {
|
|
||||||
return {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level,
|
|
||||||
message,
|
|
||||||
error: formatError(error),
|
|
||||||
context,
|
|
||||||
url: window.location.href,
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = (message: string, error?: Error, context?: LogContext): void => {
|
|
||||||
if (!shouldLog("error")) return;
|
|
||||||
|
|
||||||
const logData = createLogData("error", message, error, context);
|
|
||||||
|
|
||||||
if (configuredLevel === "debug") {
|
|
||||||
console.group(`ERROR: ${message}`);
|
|
||||||
if (error) {
|
|
||||||
console.error("Error:", error);
|
|
||||||
}
|
|
||||||
if (context && Object.keys(context).length > 0) {
|
|
||||||
console.log("Context:", context);
|
|
||||||
}
|
|
||||||
console.log("URL:", logData.url);
|
|
||||||
console.log("Timestamp:", logData.timestamp);
|
|
||||||
console.groupEnd();
|
|
||||||
} else {
|
|
||||||
console.error(JSON.stringify(logData));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const warn = (message: string, context?: LogContext): void => {
|
|
||||||
if (!shouldLog("warn")) return;
|
|
||||||
|
|
||||||
const logData = createLogData("warn", message, undefined, context);
|
|
||||||
|
|
||||||
if (configuredLevel === "debug") {
|
|
||||||
console.group(`WARN: ${message}`);
|
|
||||||
if (context && Object.keys(context).length > 0) {
|
|
||||||
console.log("Context:", context);
|
|
||||||
}
|
|
||||||
console.groupEnd();
|
|
||||||
} else {
|
|
||||||
console.warn(JSON.stringify(logData));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debug = (message: string, data?: any): void => {
|
|
||||||
if (!shouldLog("debug")) return;
|
|
||||||
|
|
||||||
console.group(`DEBUG: ${message}`);
|
|
||||||
if (data !== undefined) {
|
|
||||||
console.log(data);
|
|
||||||
}
|
|
||||||
console.groupEnd();
|
|
||||||
};
|
|
||||||
|
|
||||||
const info = (message: string, data?: any): void => {
|
|
||||||
if (!shouldLog("info")) return;
|
|
||||||
|
|
||||||
if (configuredLevel === "debug") {
|
|
||||||
console.log(`INFO: ${message}`, data);
|
|
||||||
} else {
|
|
||||||
const logData = {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
level: "info" as LogLevel,
|
|
||||||
message,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
console.log(JSON.stringify(logData));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ILogger {
|
|
||||||
error(message: string, error?: Error, context?: LogContext): void;
|
|
||||||
warn(message: string, context?: LogContext): void;
|
|
||||||
debug(message: string, data?: any): void;
|
|
||||||
info(message: string, data?: any): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const logger: ILogger = {
|
|
||||||
error,
|
|
||||||
warn,
|
|
||||||
debug,
|
|
||||||
info,
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { GeoContinents } from "@/Types/GeoCheck";
|
|
||||||
|
|
||||||
// URL schema with custom error message
|
// URL schema with custom error message
|
||||||
const urlSchema = z.url({ message: "Please enter a valid URL" });
|
const urlSchema = z.url({ message: "Please enter a valid URL" });
|
||||||
@@ -21,12 +20,6 @@ const baseSchema = z.object({
|
|||||||
.number({ message: "Threshold percentage is required" })
|
.number({ message: "Threshold percentage is required" })
|
||||||
.min(1, "Incident percentage must be at least 1")
|
.min(1, "Incident percentage must be at least 1")
|
||||||
.max(100, "Incident percentage must be at most 100"),
|
.max(100, "Incident percentage must be at most 100"),
|
||||||
geoCheckEnabled: z.boolean().optional(),
|
|
||||||
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
|
|
||||||
geoCheckInterval: z
|
|
||||||
.number()
|
|
||||||
.min(300000, "Interval must be at least 5 minutes")
|
|
||||||
.optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// HTTP monitor schema
|
// HTTP monitor schema
|
||||||
@@ -73,18 +66,6 @@ const gameSchema = baseSchema.extend({
|
|||||||
gameId: z.string().min(1, "Game type is required"),
|
gameId: z.string().min(1, "Game type is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// gRPC monitor schema
|
|
||||||
const grpcSchema = baseSchema.extend({
|
|
||||||
type: z.literal("grpc"),
|
|
||||||
url: z.string().min(1, "Host is required"),
|
|
||||||
port: z
|
|
||||||
.number()
|
|
||||||
.min(1, "Port must be at least 1")
|
|
||||||
.max(65535, "Port must be at most 65535"),
|
|
||||||
grpcServiceName: z.string().optional(),
|
|
||||||
ignoreTlsErrors: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// PageSpeed monitor schema
|
// PageSpeed monitor schema
|
||||||
const pagespeedSchema = baseSchema.extend({
|
const pagespeedSchema = baseSchema.extend({
|
||||||
type: z.literal("pagespeed"),
|
type: z.literal("pagespeed"),
|
||||||
@@ -115,13 +96,6 @@ const hardwareSchema = baseSchema.extend({
|
|||||||
selectedDisks: z.array(z.string()),
|
selectedDisks: z.array(z.string()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// WebSocket monitor schema
|
|
||||||
const websocketSchema = baseSchema.extend({
|
|
||||||
type: z.literal("websocket"),
|
|
||||||
url: z.string().min(1, "WebSocket URL is required"),
|
|
||||||
ignoreTlsErrors: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Discriminated union of all monitor types
|
// Discriminated union of all monitor types
|
||||||
export const monitorSchema = z.discriminatedUnion("type", [
|
export const monitorSchema = z.discriminatedUnion("type", [
|
||||||
httpSchema,
|
httpSchema,
|
||||||
@@ -129,10 +103,8 @@ export const monitorSchema = z.discriminatedUnion("type", [
|
|||||||
portSchema,
|
portSchema,
|
||||||
dockerSchema,
|
dockerSchema,
|
||||||
gameSchema,
|
gameSchema,
|
||||||
grpcSchema,
|
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
websocketSchema,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type MonitorFormData = z.infer<typeof monitorSchema>;
|
export type MonitorFormData = z.infer<typeof monitorSchema>;
|
||||||
@@ -144,8 +116,6 @@ export {
|
|||||||
portSchema,
|
portSchema,
|
||||||
dockerSchema,
|
dockerSchema,
|
||||||
gameSchema,
|
gameSchema,
|
||||||
grpcSchema,
|
|
||||||
pagespeedSchema,
|
pagespeedSchema,
|
||||||
hardwareSchema,
|
hardwareSchema,
|
||||||
websocketSchema,
|
|
||||||
};
|
};
|
||||||
@@ -45,11 +45,6 @@ const matrixSchema = baseSchema.extend({
|
|||||||
accessToken: z.string().min(1, "Access token is required"),
|
accessToken: z.string().min(1, "Access token is required"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const teamsSchema = baseSchema.extend({
|
|
||||||
type: z.literal("teams"),
|
|
||||||
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const notificationSchema = z.discriminatedUnion("type", [
|
export const notificationSchema = z.discriminatedUnion("type", [
|
||||||
emailSchema,
|
emailSchema,
|
||||||
slackSchema,
|
slackSchema,
|
||||||
@@ -57,7 +52,6 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
webhookSchema,
|
webhookSchema,
|
||||||
pagerDutySchema,
|
pagerDutySchema,
|
||||||
matrixSchema,
|
matrixSchema,
|
||||||
teamsSchema,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { CHECK_TTL_SENTINEL } from "@/Types/Check";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const settingsSchema = z.object({
|
export const settingsSchema = z.object({
|
||||||
@@ -12,11 +11,7 @@ export const settingsSchema = z.object({
|
|||||||
systemEmailSecure: z.boolean().optional(),
|
systemEmailSecure: z.boolean().optional(),
|
||||||
systemEmailPool: z.boolean().optional(),
|
systemEmailPool: z.boolean().optional(),
|
||||||
showURL: z.boolean().optional(),
|
showURL: z.boolean().optional(),
|
||||||
checkTTL: z
|
checkTTL: z.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"),
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(1, "Please enter a value")
|
|
||||||
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
|
|
||||||
pagespeedApiKey: z
|
pagespeedApiKey: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||||
@@ -45,12 +40,34 @@ export const settingsSchema = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
globalThresholds: z.object({
|
globalThresholds: z
|
||||||
cpu: z.number().int().min(1).max(100),
|
.object({
|
||||||
memory: z.number().int().min(1).max(100),
|
cpu: z
|
||||||
disk: z.number().int().min(1).max(100),
|
.number()
|
||||||
temperature: z.number().int().min(1).max(150),
|
.int()
|
||||||
}),
|
.min(1, "Please enter a valid value")
|
||||||
|
.max(100, "Maximum value is 100")
|
||||||
|
.optional(),
|
||||||
|
memory: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, "Please enter a valid value")
|
||||||
|
.max(100, "Maximum value is 100")
|
||||||
|
.optional(),
|
||||||
|
disk: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, "Please enter a valid value")
|
||||||
|
.max(100, "Maximum value is 100")
|
||||||
|
.optional(),
|
||||||
|
temperature: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, "Please enter a valid value")
|
||||||
|
.max(150, "Maximum value is 150")
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||||
@@ -44,10 +44,7 @@
|
|||||||
"addDemo": "Add demo monitors",
|
"addDemo": "Add demo monitors",
|
||||||
"removeMonitors": "Remove monitors",
|
"removeMonitors": "Remove monitors",
|
||||||
"sendTestEmail": "Send test email",
|
"sendTestEmail": "Send test email",
|
||||||
"exportToJSON": "Export to JSON",
|
"exportToJSON": "Export to JSON"
|
||||||
"importFromJSON": "Import from JSON",
|
|
||||||
"clearFilters": "Clear filters",
|
|
||||||
"removeUser": "Remove user"
|
|
||||||
},
|
},
|
||||||
"charts": {
|
"charts": {
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -117,8 +114,7 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"url": "Url",
|
"url": "Url",
|
||||||
"interval": "Interval",
|
"interval": "Interval",
|
||||||
"active": "Active",
|
"active": "Active"
|
||||||
"responseTime": "Response time"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -375,8 +371,8 @@
|
|||||||
"table": {
|
"table": {
|
||||||
"empty": "No down checks in this time range",
|
"empty": "No down checks in this time range",
|
||||||
"headers": {
|
"headers": {
|
||||||
"statusCode": "Status code",
|
"dateTime": "Date & time",
|
||||||
"location": "Location"
|
"statusCode": "Status code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -484,14 +480,6 @@
|
|||||||
"port": {
|
"port": {
|
||||||
"label": "Port to monitor",
|
"label": "Port to monitor",
|
||||||
"placeholder": 80
|
"placeholder": 80
|
||||||
},
|
|
||||||
"grpcServiceName": {
|
|
||||||
"label": "Service name",
|
|
||||||
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
|
|
||||||
},
|
|
||||||
"wsUrl": {
|
|
||||||
"label": "WebSocket URL",
|
|
||||||
"placeholder": "wss://example.com/socket"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "General settings",
|
"title": "General settings",
|
||||||
@@ -502,8 +490,6 @@
|
|||||||
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
||||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
|
||||||
"websocket": "Enter the WebSocket URL to monitor (e.g., wss://example.com/socket) and add a display name.",
|
|
||||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -538,10 +524,6 @@
|
|||||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||||
"optionGame": "Game",
|
"optionGame": "Game",
|
||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
"optionGrpc": "gRPC",
|
|
||||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
|
||||||
"optionWebSocket": "WebSocket",
|
|
||||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
|
||||||
"optionHttp": "HTTP(S)",
|
"optionHttp": "HTTP(S)",
|
||||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||||
"optionPing": "Ping",
|
"optionPing": "Ping",
|
||||||
@@ -568,36 +550,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"geoChecks": {
|
|
||||||
"title": "Geo-Distributed Checks",
|
|
||||||
"description": "Run checks from multiple geographic locations to monitor global availability and performance.",
|
|
||||||
"option": {
|
|
||||||
"enabled": {
|
|
||||||
"label": "Enable geo-distributed checks"
|
|
||||||
},
|
|
||||||
"locations": {
|
|
||||||
"label": "Locations",
|
|
||||||
"placeholder": "Select locations",
|
|
||||||
"options": {
|
|
||||||
"EU": "Europe",
|
|
||||||
"NA": "North America",
|
|
||||||
"AS": "Asia",
|
|
||||||
"SA": "South America",
|
|
||||||
"AF": "Africa",
|
|
||||||
"OC": "Oceania"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"interval": {
|
|
||||||
"label": "Check interval",
|
|
||||||
"value": {
|
|
||||||
"fiveMinutes": "5 minutes",
|
|
||||||
"tenMinutes": "10 minutes",
|
|
||||||
"fifteenMinutes": "15 minutes",
|
|
||||||
"thirtyMinutes": "30 minutes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"url": {
|
"url": {
|
||||||
"title": "Monitor IP/URL on Status Page",
|
"title": "Monitor IP/URL on Status Page",
|
||||||
"description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
|
"description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
|
||||||
@@ -627,12 +579,6 @@
|
|||||||
"title": "Roles",
|
"title": "Roles",
|
||||||
"description": "Assign roles to the user. Multiple roles can be selected."
|
"description": "Assign roles to the user. Multiple roles can be selected."
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"dialog": {
|
|
||||||
"removeUser": {
|
|
||||||
"title": "Remove user",
|
|
||||||
"content": "Are you sure you want to remove {{name}} from your team? This action cannot be undone."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"incidents": {
|
"incidents": {
|
||||||
@@ -673,6 +619,7 @@
|
|||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"allMonitors": "All monitors",
|
"allMonitors": "All monitors",
|
||||||
|
"clearFilters": "Clear filters",
|
||||||
"monitor": "Monitor",
|
"monitor": "Monitor",
|
||||||
"resolutionType": "Resolution type",
|
"resolutionType": "Resolution type",
|
||||||
"resolutionTypes": {
|
"resolutionTypes": {
|
||||||
@@ -1041,7 +988,7 @@
|
|||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"title": "Monitor history",
|
"title": "Monitor history",
|
||||||
"description": "Clear all monitoring history and statistics for your team. This action is irreversible.",
|
"description": "Define how long you want to retain historical data. You can also clear all existing data.",
|
||||||
"option": {
|
"option": {
|
||||||
"clear": {
|
"clear": {
|
||||||
"label": "Clear all stats. This is irreversible."
|
"label": "Clear all stats. This is irreversible."
|
||||||
@@ -1052,16 +999,6 @@
|
|||||||
"description": "This cannot be undone"
|
"description": "This cannot be undone"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"retention": {
|
|
||||||
"title": "Check retention",
|
|
||||||
"description": "Set how long check data is retained before being automatically cleaned up.",
|
|
||||||
"option": {
|
|
||||||
"days": {
|
|
||||||
"label": "Retention period (days)",
|
|
||||||
"unlimited": "Unlimited"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"thresholds": {
|
"thresholds": {
|
||||||
"title": "Global Thresholds",
|
"title": "Global Thresholds",
|
||||||
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
|
"description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.",
|
||||||
@@ -1149,18 +1086,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exportMonitors": {
|
"exportMonitors": {
|
||||||
"title": "Export monitors"
|
"title": "Export monitors",
|
||||||
},
|
"description": "Export your monitors data as a JSON file for backup or transfer."
|
||||||
"importExportMonitors": {
|
|
||||||
"title": "Import / Export monitors",
|
|
||||||
"description": "Import or export your monitors data as a JSON file for backup or transfer."
|
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
"title": "About",
|
"title": "About",
|
||||||
"developedBy": "Developed by Bluewave Labs"
|
"developedBy": "Developed by Bluewave Labs"
|
||||||
},
|
|
||||||
"validation": {
|
|
||||||
"errorMessage": "Please fix the following validation errors:"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
export default defineConfig(({}) => {
|
export default defineConfig(({}) => {
|
||||||
let version = "3.5.1";
|
let version = "3.4.0";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: "/",
|
base: "/",
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# ==============================================
|
|
||||||
# Checkmate Server Configuration
|
|
||||||
# ==============================================
|
|
||||||
# Copy this file to .env and fill in your values
|
|
||||||
|
|
||||||
# Server Configuration
|
|
||||||
# --------------------
|
|
||||||
NODE_ENV=development
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# --------
|
|
||||||
DB_CONNECTION_STRING=mongodb://localhost:27017/uptime_db
|
|
||||||
|
|
||||||
# JWT Authentication
|
|
||||||
# ------------------
|
|
||||||
JWT_SECRET=my_secret
|
|
||||||
TOKEN_TTL=99d
|
|
||||||
|
|
||||||
# Client Configuration
|
|
||||||
# --------------------
|
|
||||||
CLIENT_HOST=http://localhost:5173
|
|
||||||
|
|
||||||
# Optional Configuration
|
|
||||||
# ----------------------
|
|
||||||
ORIGIN=localhost
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import globals from "globals";
|
import globals from "globals";
|
||||||
import pluginJs from "@eslint/js";
|
import pluginJs from "@eslint/js";
|
||||||
import mochaPlugin from "eslint-plugin-mocha";
|
import mochaPlugin from "eslint-plugin-mocha";
|
||||||
import tseslint from "typescript-eslint";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Please do not forget to look at the latest eslint configurations and rules.
|
Please do not forget to look at the latest eslint configurations and rules.
|
||||||
@@ -11,9 +10,6 @@ ESlint v9 configuration is different than v8.
|
|||||||
|
|
||||||
/** @type {import('eslint').Linter.Config[]} */
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
export default [
|
export default [
|
||||||
{
|
|
||||||
ignores: ["dist/**", "node_modules/**", "coverage/**", "test/**"],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
@@ -25,19 +21,9 @@ export default [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
pluginJs.configs.recommended, // Core JS rules
|
pluginJs.configs.recommended, // Core JS rules
|
||||||
...tseslint.configs.recommended, // TypeScript recommended rules
|
|
||||||
mochaPlugin.configs.flat.recommended, // Mocha rules
|
mochaPlugin.configs.flat.recommended, // Mocha rules
|
||||||
{
|
{
|
||||||
files: ["**/*.ts", "**/*.tsx"],
|
|
||||||
languageOptions: {
|
|
||||||
parser: tseslint.parser,
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
|
||||||
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,46 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/monitors": {
|
"/monitors": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["monitors"],
|
||||||
|
"summary": "Get all monitors",
|
||||||
|
"description": "Retrieve all monitors for the authenticated user",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Monitors retrieved successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/SuccessResponse"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/Monitor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"$ref": "#/components/responses/InternalServerError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"post": {
|
"post": {
|
||||||
"tags": ["monitors"],
|
"tags": ["monitors"],
|
||||||
"summary": "Create monitor",
|
"summary": "Create monitor",
|
||||||
|
|||||||
Generated
+848
-1138
File diff suppressed because it is too large.
Load diff
+6
-13
@@ -15,7 +15,7 @@
|
|||||||
"format-check": "prettier --check ."
|
"format-check": "prettier --check ."
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*": "prettier --write"
|
"**/*": "npm run format"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -24,8 +24,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "^1.14.3",
|
|
||||||
"@grpc/proto-loader": "^0.8.0",
|
|
||||||
"@pulsecron/pulse": "1.6.8",
|
"@pulsecron/pulse": "1.6.8",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"bcryptjs": "3.0.2",
|
"bcryptjs": "3.0.2",
|
||||||
@@ -38,28 +36,27 @@
|
|||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-rate-limit": "8.0.1",
|
"express-rate-limit": "8.0.1",
|
||||||
"gamedig": "5.2.0",
|
"gamedig": "^5.3.1",
|
||||||
"got": "14.4.7",
|
"got": "14.4.7",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"helmet": "^8.0.0",
|
"helmet": "^8.0.0",
|
||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
"isomorphic-dompurify": "^2.26.0",
|
"isomorphic-dompurify": "^2.26.0",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
|
"joi": "^17.13.1",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"jsonwebtoken": "9.0.2",
|
"jsonwebtoken": "9.0.2",
|
||||||
"mailersend": "^2.2.0",
|
"mailersend": "^2.2.0",
|
||||||
"mjml": "^5.0.0-alpha.4",
|
"mjml": "^5.0.0-alpha.4",
|
||||||
"mongoose": "^8.3.3",
|
"mongoose": "^8.3.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "8.0.1",
|
"nodemailer": "^6.9.14",
|
||||||
"ping": "0.4.4",
|
"ping": "0.4.4",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"ssl-checker": "2.0.10",
|
"ssl-checker": "2.0.10",
|
||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.4.5",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0"
|
||||||
"ws": "^8.19.0",
|
|
||||||
"zod": "^4.3.6"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
@@ -79,9 +76,6 @@
|
|||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
"@types/ws": "^8.18.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
|
||||||
"@typescript-eslint/parser": "^8.56.1",
|
|
||||||
"c8": "10.1.3",
|
"c8": "10.1.3",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-mocha": "^10.5.0",
|
"eslint-plugin-mocha": "^10.5.0",
|
||||||
@@ -95,7 +89,6 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.5",
|
"tsx": "4.20.5",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2"
|
||||||
"typescript-eslint": "^8.56.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,8 @@ import { MonitorModel } from "../dist/db/models/Monitor.js";
|
|||||||
import { CheckModel } from "../dist/db/models/Check.js";
|
import { CheckModel } from "../dist/db/models/Check.js";
|
||||||
|
|
||||||
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
||||||
|
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
|
||||||
|
const DEFAULT_USER_ID = "0000000000000000000000bb";
|
||||||
const DEFAULT_MONITOR_TYPE = "http";
|
const DEFAULT_MONITOR_TYPE = "http";
|
||||||
const DEFAULT_TOTAL = 1_000_000;
|
const DEFAULT_TOTAL = 1_000_000;
|
||||||
const DEFAULT_BATCH_SIZE = 5_000;
|
const DEFAULT_BATCH_SIZE = 5_000;
|
||||||
@@ -10,7 +12,7 @@ const DEFAULT_BATCH_SIZE = 5_000;
|
|||||||
const parseObjectId = (value, fallback) => {
|
const parseObjectId = (value, fallback) => {
|
||||||
try {
|
try {
|
||||||
return new mongoose.Types.ObjectId(value || fallback);
|
return new mongoose.Types.ObjectId(value || fallback);
|
||||||
} catch {
|
} catch (error) {
|
||||||
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
||||||
return new mongoose.Types.ObjectId(fallback);
|
return new mongoose.Types.ObjectId(fallback);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-16
@@ -74,22 +74,8 @@ export const createApp = ({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
// Swagger UI — dynamically set server URL from request
|
// Swagger UI
|
||||||
app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
|
||||||
const protocol = req.protocol;
|
|
||||||
const host = req.get("host");
|
|
||||||
const dynamicSpec = {
|
|
||||||
...openApiSpec,
|
|
||||||
servers: [
|
|
||||||
{
|
|
||||||
url: `${protocol}://${host}/api/v1`,
|
|
||||||
description: "Current Server",
|
|
||||||
},
|
|
||||||
...openApiSpec.servers,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
swaggerUi.setup(dynamicSpec)(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use("/api/v1/health", (req, res) => {
|
app.use("/api/v1/health", (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import MonitorController from "../controllers/monitorController.js";
|
|||||||
import AuthController from "../controllers/authController.js";
|
import AuthController from "../controllers/authController.js";
|
||||||
import SettingsController from "../controllers/settingsController.js";
|
import SettingsController from "../controllers/settingsController.js";
|
||||||
import CheckController from "../controllers/checkController.js";
|
import CheckController from "../controllers/checkController.js";
|
||||||
import GeoCheckController from "../controllers/geoCheckController.js";
|
|
||||||
import InviteController from "../controllers/inviteController.js";
|
import InviteController from "../controllers/inviteController.js";
|
||||||
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
|
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
|
||||||
import QueueController from "../controllers/queueController.js";
|
import QueueController from "../controllers/queueController.js";
|
||||||
@@ -18,7 +17,6 @@ export interface InitializedControllers {
|
|||||||
monitorController: MonitorController;
|
monitorController: MonitorController;
|
||||||
settingsController: SettingsController;
|
settingsController: SettingsController;
|
||||||
checkController: CheckController;
|
checkController: CheckController;
|
||||||
geoCheckController: GeoCheckController;
|
|
||||||
inviteController: InviteController;
|
inviteController: InviteController;
|
||||||
maintenanceWindowController: MaintenanceWindowController;
|
maintenanceWindowController: MaintenanceWindowController;
|
||||||
queueController: QueueController;
|
queueController: QueueController;
|
||||||
@@ -34,7 +32,6 @@ export const initializeControllers = (services: InitializedServices): Initialize
|
|||||||
monitorController: new MonitorController(services.monitorService),
|
monitorController: new MonitorController(services.monitorService),
|
||||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||||
checkController: new CheckController(services.checkService),
|
checkController: new CheckController(services.checkService),
|
||||||
geoCheckController: new GeoCheckController(services.geoChecksService),
|
|
||||||
inviteController: new InviteController(services.inviteService),
|
inviteController: new InviteController(services.inviteService),
|
||||||
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
||||||
queueController: new QueueController(services.jobQueue),
|
queueController: new QueueController(services.jobQueue),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import AuthRoutes from "../routes/authRoute.js";
|
|||||||
import InviteRoutes from "../routes/inviteRoute.js";
|
import InviteRoutes from "../routes/inviteRoute.js";
|
||||||
import MonitorRoutes from "../routes/monitorRoute.js";
|
import MonitorRoutes from "../routes/monitorRoute.js";
|
||||||
import CheckRoutes from "../routes/checkRoute.js";
|
import CheckRoutes from "../routes/checkRoute.js";
|
||||||
import GeoCheckRoutes from "../routes/geoCheckRoutes.js";
|
|
||||||
import SettingsRoutes from "../routes/settingsRoute.js";
|
import SettingsRoutes from "../routes/settingsRoute.js";
|
||||||
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
|
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
|
||||||
import StatusPageRoutes from "../routes/statusPageRoute.js";
|
import StatusPageRoutes from "../routes/statusPageRoute.js";
|
||||||
@@ -23,7 +22,6 @@ export const setupRoutes = (app: any, controllers: Record<string, any>, services
|
|||||||
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
||||||
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
|
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
|
||||||
const checkRoutes = new CheckRoutes(controllers.checkController);
|
const checkRoutes = new CheckRoutes(controllers.checkController);
|
||||||
const geoCheckRoutes = new GeoCheckRoutes(controllers.geoCheckController);
|
|
||||||
const inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT);
|
const inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT);
|
||||||
const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController);
|
const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController);
|
||||||
const queueRoutes = new QueueRoutes(controllers.queueController);
|
const queueRoutes = new QueueRoutes(controllers.queueController);
|
||||||
@@ -38,7 +36,6 @@ export const setupRoutes = (app: any, controllers: Record<string, any>, services
|
|||||||
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
|
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
|
||||||
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
|
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
|
||||||
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
|
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
|
||||||
app.use("/api/v1/geo-checks", verifyJWT, geoCheckRoutes.getRouter());
|
|
||||||
app.use("/api/v1/invite", inviteRoutes.getRouter());
|
app.use("/api/v1/invite", inviteRoutes.getRouter());
|
||||||
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
|
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
|
||||||
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
||||||
|
|||||||
+67
-122
@@ -1,70 +1,34 @@
|
|||||||
import MongoDB from "../db/MongoDB.js";
|
import MongoDB from "../db/MongoDB.js";
|
||||||
import { IDb } from "@/db/IDb.js";
|
import NetworkService from "../service/infrastructure/networkService.js";
|
||||||
|
import EmailService from "../service/infrastructure/emailService.js";
|
||||||
|
import BufferService from "../service/infrastructure/bufferService.js";
|
||||||
import {
|
import {
|
||||||
// Service classes
|
|
||||||
NetworkService,
|
|
||||||
EmailService,
|
|
||||||
BufferService,
|
|
||||||
GlobalPingService,
|
|
||||||
SuperSimpleQueue,
|
|
||||||
SuperSimpleQueueHelper,
|
|
||||||
NotificationsService,
|
NotificationsService,
|
||||||
StatusService,
|
StatusService,
|
||||||
NotificationMessageBuilder,
|
|
||||||
MonitorService,
|
|
||||||
StatusPageService,
|
|
||||||
UserService,
|
|
||||||
CheckService,
|
|
||||||
GeoChecksService,
|
|
||||||
DiagnosticService,
|
|
||||||
InviteService,
|
|
||||||
MaintenanceWindowService,
|
|
||||||
IncidentService,
|
|
||||||
// Notification providers
|
|
||||||
WebhookProvider,
|
WebhookProvider,
|
||||||
SlackProvider,
|
SlackProvider,
|
||||||
EmailProvider,
|
EmailProvider,
|
||||||
DiscordProvider,
|
DiscordProvider,
|
||||||
PagerDutyProvider,
|
PagerDutyProvider,
|
||||||
MatrixProvider,
|
MatrixProvider,
|
||||||
TeamsProvider,
|
|
||||||
// Interfaces
|
|
||||||
INetworkService,
|
|
||||||
IEmailService,
|
|
||||||
IBufferService,
|
|
||||||
ISuperSimpleQueue,
|
|
||||||
INotificationsService,
|
INotificationsService,
|
||||||
IStatusService,
|
|
||||||
IMonitorService,
|
|
||||||
IUserService,
|
|
||||||
ICheckService,
|
|
||||||
IGeoChecksService,
|
|
||||||
IDiagnosticService,
|
|
||||||
IInviteService,
|
|
||||||
IMaintenanceWindowService,
|
|
||||||
IStatusPageService,
|
|
||||||
IIncidentService,
|
|
||||||
INotificationMessageBuilder,
|
|
||||||
ISettingsService,
|
|
||||||
EnvConfig,
|
|
||||||
} from "@/service/index.js";
|
} from "@/service/index.js";
|
||||||
|
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||||
// Network providers
|
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||||
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
|
import UserService from "../service/business/userService.js";
|
||||||
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
|
import CheckService from "../service/business/checkService.js";
|
||||||
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
|
import DiagnosticService from "../service/business/diagnosticService.js";
|
||||||
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
|
import InviteService from "../service/business/inviteService.js";
|
||||||
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
|
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
||||||
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
|
import { MonitorService } from "@/service/index.js";
|
||||||
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
|
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
|
||||||
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
|
import IncidentService from "../service/business/incidentService.js";
|
||||||
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
|
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
|
||||||
import { WebSocketProvider } from "@/service/infrastructure/network/WebSocketProvider.js";
|
|
||||||
|
|
||||||
// Third-party
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import got from "got";
|
import got from "got";
|
||||||
import ping from "ping";
|
import ping from "ping";
|
||||||
|
import http from "http";
|
||||||
|
import https from "https";
|
||||||
import Docker from "dockerode";
|
import Docker from "dockerode";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@@ -77,15 +41,11 @@ import jwt from "jsonwebtoken";
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { games, GameDig } from "gamedig";
|
import { games, GameDig } from "gamedig";
|
||||||
import jmespath from "jmespath";
|
import jmespath from "jmespath";
|
||||||
import * as grpc from "@grpc/grpc-js";
|
|
||||||
import * as protoLoader from "@grpc/proto-loader";
|
|
||||||
import WebSocket from "ws";
|
|
||||||
|
|
||||||
// Repositories
|
// repositories
|
||||||
import {
|
import {
|
||||||
MongoMonitorsRepository,
|
MongoMonitorsRepository,
|
||||||
MongoChecksRepository,
|
MongoChecksRepository,
|
||||||
MongoGeoChecksRepository,
|
|
||||||
MongoMonitorStatsRepository,
|
MongoMonitorStatsRepository,
|
||||||
MongoStatusPagesRepository,
|
MongoStatusPagesRepository,
|
||||||
MongoUsersRepository,
|
MongoUsersRepository,
|
||||||
@@ -97,7 +57,6 @@ import {
|
|||||||
MongoMaintenanceWindowsRepository,
|
MongoMaintenanceWindowsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
IGeoChecksRepository,
|
|
||||||
IMonitorStatsRepository,
|
IMonitorStatsRepository,
|
||||||
IStatusPagesRepository,
|
IStatusPagesRepository,
|
||||||
IUsersRepository,
|
IUsersRepository,
|
||||||
@@ -110,24 +69,24 @@ import {
|
|||||||
IMaintenanceWindowsRepository,
|
IMaintenanceWindowsRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||||
|
|
||||||
export type InitializedServices = {
|
export type InitializedServices = {
|
||||||
settingsService: ISettingsService;
|
settingsService: any;
|
||||||
db: IDb;
|
db: any;
|
||||||
networkService: INetworkService;
|
networkService: any;
|
||||||
emailService: IEmailService;
|
emailService: any;
|
||||||
bufferService: IBufferService;
|
bufferService: any;
|
||||||
statusService: IStatusService;
|
statusService: any;
|
||||||
jobQueue: ISuperSimpleQueue;
|
jobQueue: any;
|
||||||
userService: IUserService;
|
userService: any;
|
||||||
checkService: ICheckService;
|
checkService: any;
|
||||||
geoChecksService: IGeoChecksService;
|
diagnosticService: any;
|
||||||
diagnosticService: IDiagnosticService;
|
inviteService: any;
|
||||||
inviteService: IInviteService;
|
maintenanceWindowService: any;
|
||||||
maintenanceWindowService: IMaintenanceWindowService;
|
monitorService: any;
|
||||||
monitorService: IMonitorService;
|
incidentService: any;
|
||||||
incidentService: IIncidentService;
|
logger: any;
|
||||||
logger: ILogger;
|
|
||||||
notificationsService: INotificationsService;
|
notificationsService: INotificationsService;
|
||||||
statusPageService: IStatusPageService;
|
statusPageService: IStatusPageService;
|
||||||
notificationMessageBuilder: INotificationMessageBuilder;
|
notificationMessageBuilder: INotificationMessageBuilder;
|
||||||
@@ -135,7 +94,6 @@ export type InitializedServices = {
|
|||||||
// Repositories
|
// Repositories
|
||||||
monitorsRepository: IMonitorsRepository;
|
monitorsRepository: IMonitorsRepository;
|
||||||
checksRepository: IChecksRepository;
|
checksRepository: IChecksRepository;
|
||||||
geoChecksRepository: IGeoChecksRepository;
|
|
||||||
monitorStatsRepository: IMonitorStatsRepository;
|
monitorStatsRepository: IMonitorStatsRepository;
|
||||||
statusPagesRepository: IStatusPagesRepository;
|
statusPagesRepository: IStatusPagesRepository;
|
||||||
usersRepository: IUsersRepository;
|
usersRepository: IUsersRepository;
|
||||||
@@ -156,7 +114,7 @@ export const initializeServices = async ({
|
|||||||
}: {
|
}: {
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
envSettings: EnvConfig;
|
envSettings: EnvConfig;
|
||||||
settingsService: ISettingsService;
|
settingsService: any;
|
||||||
settingsRepository: ISettingsRepository;
|
settingsRepository: ISettingsRepository;
|
||||||
}): Promise<InitializedServices> => {
|
}): Promise<InitializedServices> => {
|
||||||
// Create DB
|
// Create DB
|
||||||
@@ -168,7 +126,6 @@ export const initializeServices = async ({
|
|||||||
// Repositories
|
// Repositories
|
||||||
const monitorsRepository = new MongoMonitorsRepository();
|
const monitorsRepository = new MongoMonitorsRepository();
|
||||||
const checksRepository = new MongoChecksRepository(logger);
|
const checksRepository = new MongoChecksRepository(logger);
|
||||||
const geoChecksRepository = new MongoGeoChecksRepository(logger);
|
|
||||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
const statusPagesRepository = new MongoStatusPagesRepository();
|
||||||
const usersRepository = new MongoUsersRepository();
|
const usersRepository = new MongoUsersRepository();
|
||||||
@@ -179,57 +136,47 @@ export const initializeServices = async ({
|
|||||||
const teamsRepository = new MongoTeamsRepository();
|
const teamsRepository = new MongoTeamsRepository();
|
||||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||||
|
|
||||||
// Network providers
|
const networkService = new NetworkService({
|
||||||
const pingProvider = new PingProvider(ping);
|
axios,
|
||||||
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
|
got,
|
||||||
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
|
https,
|
||||||
const hardwareProvider = new HardwareProvider(httpProvider);
|
jmespath,
|
||||||
const dockerProvider = new DockerProvider(logger, Docker);
|
GameDig,
|
||||||
const portProvider = new PortProvider(net);
|
ping,
|
||||||
const gameProvider = new GameProvider(logger, GameDig);
|
logger,
|
||||||
const grpcProvider = new GrpcProvider(grpc, protoLoader);
|
http,
|
||||||
const webSocketProvider = new WebSocketProvider(WebSocket);
|
Docker,
|
||||||
|
net,
|
||||||
const networkService = new NetworkService(axios, logger, [
|
settingsService,
|
||||||
pingProvider,
|
});
|
||||||
httpProvider,
|
|
||||||
pageSpeedProvider,
|
|
||||||
hardwareProvider,
|
|
||||||
dockerProvider,
|
|
||||||
portProvider,
|
|
||||||
gameProvider,
|
|
||||||
grpcProvider,
|
|
||||||
webSocketProvider,
|
|
||||||
]);
|
|
||||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||||
|
|
||||||
const notificationMessageBuilder = new NotificationMessageBuilder();
|
const notificationMessageBuilder = new NotificationMessageBuilder();
|
||||||
|
|
||||||
const incidentService = new IncidentService(logger, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder);
|
const incidentService = new IncidentService({
|
||||||
|
|
||||||
const checkService = new CheckService(monitorsRepository, logger, checksRepository);
|
|
||||||
|
|
||||||
const globalPingService = new GlobalPingService(logger);
|
|
||||||
|
|
||||||
const geoChecksService = new GeoChecksService({
|
|
||||||
logger,
|
logger,
|
||||||
geoChecksRepository,
|
incidentsRepository,
|
||||||
globalPingService,
|
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
|
usersRepository,
|
||||||
|
notificationMessageBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService);
|
const checkService = new CheckService({
|
||||||
|
monitorsRepository,
|
||||||
|
logger,
|
||||||
|
checksRepository,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bufferService = new BufferService({ logger, checkService, settingsService });
|
||||||
|
|
||||||
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||||
|
|
||||||
// Notification providers
|
|
||||||
const webhookProvider = new WebhookProvider(logger);
|
const webhookProvider = new WebhookProvider(logger);
|
||||||
const slackProvider = new SlackProvider(logger);
|
const slackProvider = new SlackProvider(logger);
|
||||||
const emailProvider = new EmailProvider(emailService, logger);
|
const emailProvider = new EmailProvider(emailService, logger);
|
||||||
const discordProvider = new DiscordProvider(logger);
|
const discordProvider = new DiscordProvider(logger);
|
||||||
const pagerDutyProvider = new PagerDutyProvider(logger);
|
const pagerDutyProvider = new PagerDutyProvider(logger);
|
||||||
const matrixProvider = new MatrixProvider(logger);
|
const matrixProvider = new MatrixProvider(logger);
|
||||||
const teamsProvider = new TeamsProvider(logger);
|
|
||||||
|
|
||||||
const notificationsService = new NotificationsService(
|
const notificationsService = new NotificationsService(
|
||||||
notificationsRepository,
|
notificationsRepository,
|
||||||
@@ -240,20 +187,18 @@ export const initializeServices = async ({
|
|||||||
discordProvider,
|
discordProvider,
|
||||||
pagerDutyProvider,
|
pagerDutyProvider,
|
||||||
matrixProvider,
|
matrixProvider,
|
||||||
teamsProvider,
|
|
||||||
settingsService,
|
settingsService,
|
||||||
logger,
|
logger,
|
||||||
notificationMessageBuilder
|
notificationMessageBuilder
|
||||||
);
|
);
|
||||||
|
|
||||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper(
|
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
||||||
logger,
|
logger,
|
||||||
networkService,
|
networkService,
|
||||||
statusService,
|
statusService,
|
||||||
notificationsService,
|
notificationsService,
|
||||||
checkService,
|
checkService,
|
||||||
settingsService,
|
buffer: bufferService,
|
||||||
bufferService,
|
|
||||||
incidentService,
|
incidentService,
|
||||||
maintenanceWindowsRepository,
|
maintenanceWindowsRepository,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
@@ -261,11 +206,13 @@ export const initializeServices = async ({
|
|||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
incidentsRepository,
|
incidentsRepository,
|
||||||
geoChecksService,
|
});
|
||||||
geoChecksRepository
|
|
||||||
);
|
|
||||||
|
|
||||||
const superSimpleQueue = await SuperSimpleQueue.create(logger, superSimpleQueueHelper, monitorsRepository);
|
const superSimpleQueue = await SuperSimpleQueue.create({
|
||||||
|
logger,
|
||||||
|
helper: superSimpleQueueHelper,
|
||||||
|
monitorsRepository,
|
||||||
|
});
|
||||||
|
|
||||||
// Business services
|
// Business services
|
||||||
const userService = new UserService({
|
const userService = new UserService({
|
||||||
@@ -300,7 +247,6 @@ export const initializeServices = async ({
|
|||||||
games,
|
games,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
geoChecksRepository,
|
|
||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
statusPagesRepository,
|
statusPagesRepository,
|
||||||
incidentsRepository,
|
incidentsRepository,
|
||||||
@@ -309,6 +255,7 @@ export const initializeServices = async ({
|
|||||||
const statusPageService = new StatusPageService(statusPagesRepository);
|
const statusPageService = new StatusPageService(statusPagesRepository);
|
||||||
|
|
||||||
const services = {
|
const services = {
|
||||||
|
//v1
|
||||||
settingsService,
|
settingsService,
|
||||||
db,
|
db,
|
||||||
networkService,
|
networkService,
|
||||||
@@ -318,7 +265,6 @@ export const initializeServices = async ({
|
|||||||
jobQueue: superSimpleQueue,
|
jobQueue: superSimpleQueue,
|
||||||
userService,
|
userService,
|
||||||
checkService,
|
checkService,
|
||||||
geoChecksService,
|
|
||||||
diagnosticService,
|
diagnosticService,
|
||||||
inviteService,
|
inviteService,
|
||||||
maintenanceWindowService,
|
maintenanceWindowService,
|
||||||
@@ -332,7 +278,6 @@ export const initializeServices = async ({
|
|||||||
// Repositories
|
// Repositories
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
checksRepository,
|
checksRepository,
|
||||||
geoChecksRepository,
|
|
||||||
monitorStatsRepository,
|
monitorStatsRepository,
|
||||||
statusPagesRepository,
|
statusPagesRepository,
|
||||||
usersRepository,
|
usersRepository,
|
||||||
|
|||||||
@@ -1,34 +1,31 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
import { requireTeamId, requireUserRoles } from "./controllerUtils.js";
|
||||||
|
import type { UserRole } from "@/types/user.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
registrationBodyValidation,
|
registrationBodyValidation,
|
||||||
loginValidation,
|
loginValidation,
|
||||||
|
editUserBodyValidation,
|
||||||
|
createUserBodyValidation,
|
||||||
recoveryValidation,
|
recoveryValidation,
|
||||||
recoveryTokenBodyValidation,
|
recoveryTokenBodyValidation,
|
||||||
newPasswordValidation,
|
newPasswordValidation,
|
||||||
} from "@/validation/authValidation.js";
|
|
||||||
|
|
||||||
import {
|
|
||||||
editUserBodyValidation,
|
|
||||||
createUserBodyValidation,
|
|
||||||
getUserByIdParamValidation,
|
getUserByIdParamValidation,
|
||||||
editUserByIdParamValidation,
|
editUserByIdParamValidation,
|
||||||
editUserByIdBodyValidation,
|
editUserByIdBodyValidation,
|
||||||
editSuperadminUserByIdBodyValidation,
|
editSuperadminUserByIdBodyValidation,
|
||||||
editUserPasswordByIdBodyValidation,
|
editUserPasswordByIdBodyValidation,
|
||||||
} from "@/validation/userValidation.js";
|
} from "@/validation/joi.js";
|
||||||
import { IUserService } from "@/service/index.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "authController";
|
const SERVICE_NAME = "authController";
|
||||||
|
|
||||||
class AuthController {
|
class AuthController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private userService: IUserService;
|
private userService: any;
|
||||||
|
|
||||||
constructor(userService: IUserService) {
|
constructor(userService: any) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,12 +38,11 @@ class AuthController {
|
|||||||
const newUser = req.body.user;
|
const newUser = req.body.user;
|
||||||
const newUserToken = req.body.token;
|
const newUserToken = req.body.token;
|
||||||
if (newUser?.email) {
|
if (newUser?.email) {
|
||||||
const newUserEmail = requireUserEmail(newUser.email);
|
newUser.email = newUser.email.toLowerCase();
|
||||||
newUser.email = newUserEmail.toLowerCase();
|
|
||||||
}
|
}
|
||||||
const validatedBody = registrationBodyValidation.parse(newUser);
|
await registrationBodyValidation.validateAsync(newUser);
|
||||||
|
|
||||||
const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null);
|
const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file);
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "User registered successfully",
|
msg: "User registered successfully",
|
||||||
@@ -63,11 +59,11 @@ class AuthController {
|
|||||||
if (userData?.email) {
|
if (userData?.email) {
|
||||||
userData.email = userData.email.toLowerCase();
|
userData.email = userData.email.toLowerCase();
|
||||||
}
|
}
|
||||||
const validatedBody = createUserBodyValidation.parse(userData);
|
await createUserBodyValidation.validateAsync(userData);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const actorRoles = requireUserRoles(req.user?.role);
|
const actorRoles = requireUserRoles(req.user?.role) as UserRole[];
|
||||||
const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null);
|
const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file);
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "User created successfully",
|
msg: "User created successfully",
|
||||||
@@ -83,7 +79,7 @@ class AuthController {
|
|||||||
if (req.body?.email) {
|
if (req.body?.email) {
|
||||||
req.body.email = req.body.email?.toLowerCase();
|
req.body.email = req.body.email?.toLowerCase();
|
||||||
}
|
}
|
||||||
loginValidation.parse(req.body);
|
await loginValidation.validateAsync(req.body);
|
||||||
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
|
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -101,10 +97,8 @@ class AuthController {
|
|||||||
|
|
||||||
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
editUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedBody = editUserBodyValidation.parse(req.body);
|
await editUserBodyValidation.validateAsync(req.body);
|
||||||
const userId = requireUserId(req.user?.id);
|
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
|
||||||
const userEmail = requireUserEmail(req.user?.email);
|
|
||||||
const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail);
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -131,7 +125,7 @@ class AuthController {
|
|||||||
|
|
||||||
requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
requestRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
recoveryValidation.parse(req.body);
|
await recoveryValidation.validateAsync(req.body);
|
||||||
const email = req?.body?.email;
|
const email = req?.body?.email;
|
||||||
const msgId = await this.userService.requestRecovery(email);
|
const msgId = await this.userService.requestRecovery(email);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -146,7 +140,7 @@ class AuthController {
|
|||||||
|
|
||||||
validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
validateRecovery = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
recoveryTokenBodyValidation.parse(req.body);
|
await recoveryTokenBodyValidation.validateAsync(req.body);
|
||||||
await this.userService.validateRecovery(req.body.recoveryToken);
|
await this.userService.validateRecovery(req.body.recoveryToken);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -159,7 +153,7 @@ class AuthController {
|
|||||||
|
|
||||||
resetPassword = async (req: Request, res: Response, next: NextFunction) => {
|
resetPassword = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
newPasswordValidation.parse(req.body);
|
await newPasswordValidation.validateAsync(req.body);
|
||||||
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
|
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -173,15 +167,7 @@ class AuthController {
|
|||||||
|
|
||||||
deleteUser = async (req: Request, res: Response, next: NextFunction) => {
|
deleteUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const userId = requireUserId(req.user?.id);
|
await this.userService.deleteUser(req.user);
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
|
||||||
const roles = requireUserRoles(req.user?.role);
|
|
||||||
|
|
||||||
await this.userService.deleteUser({
|
|
||||||
userId,
|
|
||||||
teamId,
|
|
||||||
roles,
|
|
||||||
});
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "User deleted successfully",
|
msg: "User deleted successfully",
|
||||||
@@ -191,23 +177,6 @@ class AuthController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteUserById = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const validatedParams = getUserByIdParamValidation.parse(req.params);
|
|
||||||
const targetUserId = validatedParams.userId;
|
|
||||||
const actorId = requireUserId(req.user?.id);
|
|
||||||
const actorTeamId = requireTeamId(req.user?.teamId);
|
|
||||||
const actorRoles = requireUserRoles(req.user?.role);
|
|
||||||
await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId });
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "User removed successfully",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
|
getAllUsers = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const allUsers = await this.userService.getAllUsers();
|
const allUsers = await this.userService.getAllUsers();
|
||||||
@@ -223,9 +192,19 @@ class AuthController {
|
|||||||
|
|
||||||
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
getUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getUserByIdParamValidation.parse(req.params);
|
await getUserByIdParamValidation.validateAsync(req.params);
|
||||||
const actorRoles = requireUserRoles(req.user?.role);
|
const userId = req?.params?.userId;
|
||||||
const user = await this.userService.getUserById(actorRoles, validatedParams.userId);
|
const roles = req?.user?.role;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error("No user ID in request");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roles || roles.length === 0) {
|
||||||
|
throw new Error("No roles in request");
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await this.userService.getUserById(roles, userId);
|
||||||
|
|
||||||
return res.status(200).json({ success: true, msg: "ok", data: user });
|
return res.status(200).json({ success: true, msg: "ok", data: user });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -235,19 +214,24 @@ class AuthController {
|
|||||||
|
|
||||||
editUserById = async (req: Request, res: Response, next: NextFunction) => {
|
editUserById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const actorRoles = requireUserRoles(req.user?.role);
|
const roles = req?.user?.role;
|
||||||
const actorId = requireUserId(req.user?.id);
|
|
||||||
|
|
||||||
if (!actorRoles.includes("superadmin")) {
|
if (!roles || !roles.includes("superadmin")) {
|
||||||
throw new AppError({ message: "Unauthorized", status: 403 });
|
throw new AppError({ message: "Unauthorized", status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedParams = editUserByIdParamValidation.parse(req.params);
|
const userId = req.params.userId as string;
|
||||||
// If this is superadmin self edit, allow "superadmin" role
|
const user = { ...req.body };
|
||||||
const validatedBody =
|
|
||||||
validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body);
|
|
||||||
|
|
||||||
await this.userService.editUserById(validatedParams.userId, validatedBody);
|
await editUserByIdParamValidation.validateAsync(req.params);
|
||||||
|
// If this is superadmin self edit, allow "superadmin" role
|
||||||
|
if (userId === req.user?.id) {
|
||||||
|
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
|
||||||
|
} else {
|
||||||
|
await editUserByIdBodyValidation.validateAsync(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.userService.editUserById(userId, user);
|
||||||
return res.status(200).json({ success: true, msg: "ok" });
|
return res.status(200).json({ success: true, msg: "ok" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -256,14 +240,16 @@ class AuthController {
|
|||||||
|
|
||||||
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
|
editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const actorRoles = requireUserRoles(req.user?.role);
|
const roles = req?.user?.role;
|
||||||
if (!actorRoles.includes("superadmin")) {
|
if (!roles || !roles.includes("superadmin")) {
|
||||||
throw new AppError({ message: "Unauthorized", status: 403 });
|
throw new AppError({ message: "Unauthorized", status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedParams = editUserByIdParamValidation.parse(req.params);
|
const userId = req.params.userId as string;
|
||||||
const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body);
|
await editUserByIdParamValidation.validateAsync(req.params);
|
||||||
await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password);
|
await editUserPasswordByIdBodyValidation.validateAsync(req.body);
|
||||||
|
const updatedPassword = req.body.password;
|
||||||
|
await this.userService.setPasswordByUserId(userId, updatedPassword);
|
||||||
return res.status(200).json({ success: true, msg: "Password reset successfully" });
|
return res.status(200).json({ success: true, msg: "Password reset successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
|||||||
@@ -4,19 +4,21 @@ import {
|
|||||||
getChecksParamValidation,
|
getChecksParamValidation,
|
||||||
getChecksQueryValidation,
|
getChecksQueryValidation,
|
||||||
getTeamChecksQueryValidation,
|
getTeamChecksQueryValidation,
|
||||||
getChecksSummaryByTeamIdQueryValidation,
|
|
||||||
deleteChecksParamValidation,
|
deleteChecksParamValidation,
|
||||||
} from "@/validation/checkValidation.js";
|
deleteChecksByTeamIdParamValidation,
|
||||||
import { ICheckService } from "@/service/index.js";
|
updateChecksTTLBodyValidation,
|
||||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
ackCheckBodyValidation,
|
||||||
|
ackAllChecksParamValidation,
|
||||||
|
ackAllChecksBodyValidation,
|
||||||
|
} from "@/validation/joi.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "checkController";
|
const SERVICE_NAME = "checkController";
|
||||||
|
|
||||||
class CheckController {
|
class CheckController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private checkService: ICheckService;
|
private checkService: any;
|
||||||
constructor(checkService: ICheckService) {
|
constructor(checkService: any) {
|
||||||
this.checkService = checkService;
|
this.checkService = checkService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,20 +28,13 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getChecksParamValidation.parse(req.params);
|
await getChecksParamValidation.validateAsync(req.params);
|
||||||
const validatedQuery = getChecksQueryValidation.parse(req.query);
|
await getChecksQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
|
||||||
|
|
||||||
const result = await this.checkService.getChecksByMonitor({
|
const result = await this.checkService.getChecksByMonitor({
|
||||||
monitorId: validatedParams.monitorId,
|
monitorId: req?.params?.monitorId,
|
||||||
teamId,
|
query: req?.query,
|
||||||
sortOrder: validatedQuery.sortOrder,
|
teamId: req?.user?.teamId,
|
||||||
dateRange: validatedQuery.dateRange,
|
|
||||||
filter: validatedQuery.filter,
|
|
||||||
page: validatedQuery.page,
|
|
||||||
rowsPerPage: validatedQuery.rowsPerPage,
|
|
||||||
status: validatedQuery.status,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -54,16 +49,10 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedQuery = getTeamChecksQueryValidation.parse(req.query);
|
await getTeamChecksQueryValidation.validateAsync(req.query);
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
|
||||||
|
|
||||||
const checkData = await this.checkService.getChecksByTeam({
|
const checkData = await this.checkService.getChecksByTeam({
|
||||||
teamId,
|
teamId: req?.user?.teamId,
|
||||||
sortOrder: validatedQuery.sortOrder,
|
query: req?.query,
|
||||||
dateRange: validatedQuery.dateRange,
|
|
||||||
page: validatedQuery.page,
|
|
||||||
rowsPerPage: validatedQuery.rowsPerPage,
|
|
||||||
filter: validatedQuery.filter,
|
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -77,11 +66,8 @@ class CheckController {
|
|||||||
|
|
||||||
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query);
|
const dateRange = req.query?.dateRange || "hour";
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange });
|
||||||
const dateRange = validatedQuery.dateRange ?? "hour";
|
|
||||||
|
|
||||||
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange });
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Checks summary retrieved successfully",
|
msg: "Checks summary retrieved successfully",
|
||||||
@@ -94,12 +80,11 @@ class CheckController {
|
|||||||
|
|
||||||
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
deleteChecks = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = deleteChecksParamValidation.parse(req.params);
|
await deleteChecksParamValidation.validateAsync(req.params);
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
|
||||||
|
|
||||||
const deletedCount = await this.checkService.deleteChecks({
|
const deletedCount = await this.checkService.deleteChecks({
|
||||||
monitorId: validatedParams.monitorId,
|
monitorId: req.params.monitorId as string,
|
||||||
teamId,
|
teamId: req?.user?.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -114,9 +99,9 @@ class CheckController {
|
|||||||
|
|
||||||
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
|
||||||
|
|
||||||
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId });
|
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -127,6 +112,24 @@ class CheckController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
await updateChecksTTLBodyValidation.validateAsync(req.body);
|
||||||
|
|
||||||
|
await this.checkService.updateChecksTTL({
|
||||||
|
teamId: req?.user?.teamId,
|
||||||
|
ttl: req?.body?.ttl,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: "Checks TTL updated successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CheckController;
|
export default CheckController;
|
||||||
@@ -1,25 +1,24 @@
|
|||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js";
|
import { type MonitorType, MonitorTypes } from "@/types/index.js";
|
||||||
import sslChecker, { SSLDetails } from "ssl-checker";
|
|
||||||
type SSLCheckerType = typeof sslChecker;
|
|
||||||
|
|
||||||
export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): Promise<SSLDetails> => {
|
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
|
||||||
const monitorUrl = new URL(monitor.url);
|
const monitorUrl = new URL(monitor.url);
|
||||||
const hostname = monitorUrl.hostname;
|
const hostname = monitorUrl.hostname;
|
||||||
const cert = await checker(hostname);
|
const cert = await sslChecker(hostname);
|
||||||
|
// Throw an error if no cert or if cert.validTo is not present
|
||||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||||
throw new Error("Certificate not found");
|
throw new Error("Certificate not found");
|
||||||
}
|
}
|
||||||
return cert;
|
return cert;
|
||||||
};
|
};
|
||||||
export const requireString = (value: unknown, fieldName: string): string => {
|
const requireString = (value: unknown, fieldName: string): string => {
|
||||||
if (typeof value === "string" && value.trim().length > 0) {
|
if (typeof value === "string" && value.trim().length > 0) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
throw new AppError({ message: `${fieldName} is required`, status: 400 });
|
throw new AppError({ message: `${fieldName} is required`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -29,7 +28,7 @@ export const optionalString = (value: unknown, fieldName: string): string | unde
|
|||||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -45,7 +44,7 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde
|
|||||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -63,7 +62,7 @@ export const optionalBoolean = (value: unknown, fieldName: string): boolean | un
|
|||||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||||
const parseSingle = (input: unknown): MonitorType => {
|
const parseSingle = (input: unknown): MonitorType => {
|
||||||
if (typeof input !== "string") {
|
if (typeof input !== "string") {
|
||||||
throw new AppError({ message: "Monitor type must be a string", status: 400 });
|
throw new AppError({ message: "Monitor type must be a string", status: 400 });
|
||||||
@@ -83,7 +82,7 @@ export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorTyp
|
|||||||
return parseSingle(value);
|
return parseSingle(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -93,36 +92,42 @@ export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
|||||||
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
|
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireTeamId = (teamId?: string): string => {
|
const requireTeamId = (teamId?: string): string => {
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
}
|
}
|
||||||
return teamId;
|
return teamId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireUserId = (userId?: string): string => {
|
const requireUserId = (userId?: string): string => {
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new AppError({ message: "User ID is required", status: 400 });
|
throw new AppError({ message: "User ID is required", status: 400 });
|
||||||
}
|
}
|
||||||
return userId;
|
return userId;
|
||||||
};
|
};
|
||||||
export const requireUserEmail = (userEmail?: string): string => {
|
const requireUserEmail = (userEmail?: string): string => {
|
||||||
if (!userEmail) {
|
if (!userEmail) {
|
||||||
throw new AppError({ message: "User email is required", status: 400 });
|
throw new AppError({ message: "User email is required", status: 400 });
|
||||||
}
|
}
|
||||||
return userEmail;
|
return userEmail;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requireFirstName = (firstName?: string): string => {
|
export const requireUserRoles = (userRoles?: string[]): string[] => {
|
||||||
if (!firstName) {
|
|
||||||
throw new AppError({ message: "First name is required", status: 400 });
|
|
||||||
}
|
|
||||||
return firstName;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => {
|
|
||||||
if (!userRoles || userRoles.length === 0) {
|
if (!userRoles || userRoles.length === 0) {
|
||||||
throw new AppError({ message: "User roles are required", status: 400 });
|
throw new AppError({ message: "User roles are required", status: 400 });
|
||||||
}
|
}
|
||||||
return userRoles;
|
return userRoles;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchMonitorCertificate,
|
||||||
|
requireString,
|
||||||
|
optionalString,
|
||||||
|
optionalNumber,
|
||||||
|
optionalBoolean,
|
||||||
|
parseMonitorTypeFilter,
|
||||||
|
parseSortOrder,
|
||||||
|
requireTeamId,
|
||||||
|
requireUserId,
|
||||||
|
requireUserEmail,
|
||||||
|
};
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { IDiagnosticService } from "@/service/index.js";
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const SERVICE_NAME = "diagnosticController";
|
const SERVICE_NAME = "diagnosticController";
|
||||||
class DiagnosticController {
|
class DiagnosticController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private diagnosticService: IDiagnosticService;
|
private diagnosticService: any;
|
||||||
|
|
||||||
constructor(diagnosticService: IDiagnosticService) {
|
constructor(diagnosticService: any) {
|
||||||
this.diagnosticService = diagnosticService;
|
this.diagnosticService = diagnosticService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
|
||||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "geoCheckController";
|
|
||||||
|
|
||||||
class GeoCheckController {
|
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
|
||||||
|
|
||||||
private geoChecksService: IGeoChecksService;
|
|
||||||
constructor(geoChecksService: IGeoChecksService) {
|
|
||||||
this.geoChecksService = geoChecksService;
|
|
||||||
}
|
|
||||||
|
|
||||||
get serviceName() {
|
|
||||||
return GeoCheckController.SERVICE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
getChecksParamValidation.parse(req.params);
|
|
||||||
getChecksQueryValidation.parse(req.query);
|
|
||||||
|
|
||||||
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
|
||||||
monitorId: req?.params?.monitorId as string,
|
|
||||||
query: req?.query,
|
|
||||||
teamId: req?.user?.teamId as string,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "Geo checks retrieved successfully",
|
|
||||||
data: result,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GeoCheckController;
|
|
||||||
@@ -1,30 +1,35 @@
|
|||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
|
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
|
||||||
import { IIncidentService } from "@/service/index.js";
|
|
||||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "IncidentController";
|
const SERVICE_NAME = "incidentController";
|
||||||
|
|
||||||
class IncidentController {
|
class IncidentController {
|
||||||
private incidentService: IIncidentService;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
constructor(incidentService: IIncidentService) {
|
|
||||||
|
private incidentService: any;
|
||||||
|
constructor(incidentService: any) {
|
||||||
this.incidentService = incidentService;
|
this.incidentService = incidentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get serviceName() {
|
||||||
|
return IncidentController.SERVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query);
|
const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {};
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const result = await this.incidentService.getIncidentsByTeam(
|
const result = await this.incidentService.getIncidentsByTeam(
|
||||||
teamId,
|
teamId,
|
||||||
validatedQuery.sortOrder,
|
sortOrder,
|
||||||
validatedQuery.dateRange,
|
dateRange,
|
||||||
validatedQuery.page,
|
page,
|
||||||
validatedQuery.rowsPerPage,
|
rowsPerPage,
|
||||||
validatedQuery.status,
|
status,
|
||||||
validatedQuery.monitorId,
|
monitorId,
|
||||||
validatedQuery.resolutionType
|
resolutionType
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -40,9 +45,10 @@ class IncidentController {
|
|||||||
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
|
getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query);
|
|
||||||
|
|
||||||
const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit);
|
const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10;
|
||||||
|
|
||||||
|
const summary = await this.incidentService.getIncidentSummary(teamId, limit);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Incident summary retrieved successfully",
|
msg: "Incident summary retrieved successfully",
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js";
|
import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.js";
|
||||||
import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js";
|
||||||
import { IInviteService } from "@/service/index.js";
|
|
||||||
const SERVICE_NAME = "inviteController";
|
const SERVICE_NAME = "inviteController";
|
||||||
|
|
||||||
class InviteController {
|
class InviteController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private inviteService: IInviteService;
|
private inviteService: any;
|
||||||
constructor(inviteService: IInviteService) {
|
constructor(inviteService: any) {
|
||||||
this.inviteService = inviteService;
|
this.inviteService = inviteService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ class InviteController {
|
|||||||
const userRoles = requireUserRoles(req.user?.role);
|
const userRoles = requireUserRoles(req.user?.role);
|
||||||
const invite = req.body;
|
const invite = req.body;
|
||||||
invite.teamId = teamId;
|
invite.teamId = teamId;
|
||||||
inviteBodyValidation.parse(invite);
|
await inviteBodyValidation.validateAsync(invite);
|
||||||
const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles });
|
const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles });
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -35,17 +34,16 @@ class InviteController {
|
|||||||
|
|
||||||
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
|
sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const userRoles = requireUserRoles(req.user?.role);
|
const userRoles = requireUserRoles(req?.user?.role);
|
||||||
const firstName = requireFirstName(req.user?.firstName);
|
|
||||||
|
|
||||||
const inviteRequest = req.body;
|
const inviteRequest = req.body;
|
||||||
inviteRequest.teamId = teamId;
|
inviteRequest.teamId = teamId;
|
||||||
inviteBodyValidation.parse(inviteRequest);
|
await inviteBodyValidation.validateAsync(inviteRequest);
|
||||||
|
|
||||||
const inviteToken = await this.inviteService.sendInviteEmail({
|
const inviteToken = await this.inviteService.sendInviteEmail({
|
||||||
invite: inviteRequest,
|
invite: inviteRequest,
|
||||||
firstName,
|
firstName: req?.user?.firstName,
|
||||||
userRoles,
|
userRoles,
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -60,7 +58,7 @@ class InviteController {
|
|||||||
|
|
||||||
verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => {
|
verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
inviteVerificationBodyValidation.parse(req.body);
|
await inviteVerificationBodyValidation.validateAsync(req.body);
|
||||||
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
|
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { ILogger } from "@/utils/logger.js";
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const SERVICE_NAME = "LogController";
|
const SERVICE_NAME = "LogController";
|
||||||
|
|
||||||
class LogController {
|
class LogController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private logger: ILogger;
|
private logger: any;
|
||||||
constructor(logger: ILogger) {
|
constructor(logger: any) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
get serviceName() {
|
get serviceName() {
|
||||||
@@ -15,7 +14,7 @@ class LogController {
|
|||||||
|
|
||||||
getLogs = async (req: Request, res: Response, next: NextFunction) => {
|
getLogs = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const logs = this.logger.getLogs();
|
const logs = await this.logger.getLogs();
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Logs fetched successfully",
|
msg: "Logs fetched successfully",
|
||||||
|
|||||||
@@ -7,21 +7,28 @@ import {
|
|||||||
getMaintenanceWindowsByMonitorIdParamValidation,
|
getMaintenanceWindowsByMonitorIdParamValidation,
|
||||||
getMaintenanceWindowsByTeamIdQueryValidation,
|
getMaintenanceWindowsByTeamIdQueryValidation,
|
||||||
deleteMaintenanceWindowByIdParamValidation,
|
deleteMaintenanceWindowByIdParamValidation,
|
||||||
} from "@/validation/maintenanceWindowValidation.js";
|
} from "@/validation/joi.js";
|
||||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||||
import { IMaintenanceWindowService } from "@/service/index.js";
|
|
||||||
|
const SERVICE_NAME = "maintenanceWindowController";
|
||||||
|
|
||||||
class MaintenanceWindowController {
|
class MaintenanceWindowController {
|
||||||
private maintenanceWindowService: IMaintenanceWindowService;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
private maintenanceWindowService: any;
|
||||||
|
constructor(maintenanceWindowService: any) {
|
||||||
this.maintenanceWindowService = maintenanceWindowService;
|
this.maintenanceWindowService = maintenanceWindowService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get serviceName() {
|
||||||
|
return MaintenanceWindowController.SERVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
|
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body);
|
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody });
|
|
||||||
|
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -33,11 +40,11 @@ class MaintenanceWindowController {
|
|||||||
};
|
};
|
||||||
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params);
|
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId });
|
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -51,18 +58,11 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query);
|
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({
|
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
|
||||||
teamId,
|
|
||||||
active: validatedQuery.active,
|
|
||||||
page: validatedQuery.page,
|
|
||||||
rowsPerPage: validatedQuery.rowsPerPage,
|
|
||||||
field: validatedQuery.field,
|
|
||||||
order: validatedQuery.order,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -76,12 +76,12 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params);
|
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
|
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({
|
||||||
monitorId: validatedParams.monitorId,
|
monitorId: req.params.monitorId as string,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,11 +96,11 @@ class MaintenanceWindowController {
|
|||||||
};
|
};
|
||||||
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params);
|
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId });
|
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -113,14 +113,14 @@ class MaintenanceWindowController {
|
|||||||
|
|
||||||
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params);
|
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
|
||||||
const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body);
|
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
|
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({
|
||||||
id: validatedParams.id,
|
id: req.params.id as string,
|
||||||
body: validatedBody,
|
body: req.body,
|
||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import {
|
import {
|
||||||
getMonitorByIdParamValidation,
|
getMonitorByIdParamValidation,
|
||||||
getMonitorByIdQueryValidation,
|
getMonitorByIdQueryValidation,
|
||||||
|
getMonitorsByTeamIdParamValidation,
|
||||||
getMonitorsByTeamIdQueryValidation,
|
getMonitorsByTeamIdQueryValidation,
|
||||||
getMonitorsWithChecksQueryValidation,
|
getMonitorsWithChecksQueryValidation,
|
||||||
createMonitorBodyValidation,
|
createMonitorBodyValidation,
|
||||||
@@ -11,12 +12,18 @@ import {
|
|||||||
getCertificateParamValidation,
|
getCertificateParamValidation,
|
||||||
getHardwareDetailsByIdParamValidation,
|
getHardwareDetailsByIdParamValidation,
|
||||||
getHardwareDetailsByIdQueryValidation,
|
getHardwareDetailsByIdQueryValidation,
|
||||||
getUptimeDetailsByIdParamValidation,
|
} from "@/validation/joi.js";
|
||||||
getUptimeDetailsByIdQueryValidation,
|
|
||||||
importMonitorsBodyValidation,
|
|
||||||
} from "@/validation/monitorValidation.js";
|
|
||||||
import sslChecker from "ssl-checker";
|
import sslChecker from "ssl-checker";
|
||||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js";
|
import {
|
||||||
|
fetchMonitorCertificate,
|
||||||
|
requireString,
|
||||||
|
optionalString,
|
||||||
|
optionalNumber,
|
||||||
|
optionalBoolean,
|
||||||
|
parseMonitorTypeFilter,
|
||||||
|
parseSortOrder,
|
||||||
|
requireTeamId,
|
||||||
|
} from "./controllerUtils.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorService } from "@/service/index.js";
|
import { IMonitorService } from "@/service/index.js";
|
||||||
|
|
||||||
@@ -36,9 +43,10 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getCertificateParamValidation.parse(req.params);
|
await getCertificateParamValidation.validateAsync(req.params);
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId });
|
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
||||||
|
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||||
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -55,14 +63,10 @@ class MonitorController {
|
|||||||
|
|
||||||
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params);
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query);
|
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||||
|
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
|
||||||
const monitorId = validatedParams.monitorId;
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const dateRange = validatedQuery.dateRange;
|
|
||||||
const normalize = validatedQuery.normalize;
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
|
||||||
|
|
||||||
const data = await this.monitorService.getUptimeDetailsById({
|
const data = await this.monitorService.getUptimeDetailsById({
|
||||||
teamId,
|
teamId,
|
||||||
@@ -82,12 +86,12 @@ class MonitorController {
|
|||||||
|
|
||||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||||
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const dateRange = validatedQuery.dateRange || "recent";
|
const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent";
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const data = await this.monitorService.getHardwareDetailsById({
|
const data = await this.monitorService.getHardwareDetailsById({
|
||||||
teamId,
|
teamId,
|
||||||
@@ -106,12 +110,12 @@ class MonitorController {
|
|||||||
};
|
};
|
||||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params);
|
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||||
const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query);
|
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const dateRange = validatedQuery.dateRange || "recent";
|
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const data = await this.monitorService.getPageSpeedDetailsById({
|
const data = await this.monitorService.getPageSpeedDetailsById({
|
||||||
teamId,
|
teamId,
|
||||||
@@ -129,40 +133,13 @@ class MonitorController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
|
||||||
const validatedQuery = getMonitorByIdQueryValidation.parse(req.query);
|
|
||||||
|
|
||||||
const monitorId = validatedParams.monitorId;
|
|
||||||
const dateRange = validatedQuery.dateRange || "recent";
|
|
||||||
const continentParam = validatedQuery.continent;
|
|
||||||
const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined;
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
|
||||||
|
|
||||||
const data = await this.monitorService.getGeoChecksByMonitorId({
|
|
||||||
teamId,
|
|
||||||
monitorId,
|
|
||||||
dateRange,
|
|
||||||
continents,
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "Geo checks retrieved successfully",
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||||
|
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
|
|
||||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||||
|
|
||||||
@@ -178,12 +155,12 @@ class MonitorController {
|
|||||||
|
|
||||||
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
createMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedBody = createMonitorBodyValidation.parse(req.body);
|
await createMonitorBodyValidation.validateAsync(req.body);
|
||||||
|
|
||||||
const userId = requireUserId(req.user?.id);
|
const userId = requireString(req?.user?.id, "User ID");
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody);
|
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -195,19 +172,22 @@ class MonitorController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => {
|
createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const userId = req.user?.id;
|
||||||
const userId = requireUserId(req.user?.id);
|
if (!userId) {
|
||||||
const validatedBody = importMonitorsBodyValidation.parse(req.body);
|
throw new AppError({ message: "User ID is required", status: 400 });
|
||||||
const monitors = validatedBody.monitors;
|
}
|
||||||
|
|
||||||
const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors });
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
|
const data = req.body;
|
||||||
|
const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: `Successfully imported ${result.imported} monitor(s)`,
|
msg: "Bulk monitors created successfully",
|
||||||
data: result,
|
data: monitors,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -216,9 +196,9 @@ class MonitorController {
|
|||||||
|
|
||||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const monitorId = validatedParams.monitorId;
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
||||||
|
|
||||||
@@ -234,7 +214,8 @@ class MonitorController {
|
|||||||
|
|
||||||
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
|
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -248,12 +229,12 @@ class MonitorController {
|
|||||||
|
|
||||||
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
editMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = getMonitorByIdParamValidation.parse(req.params);
|
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||||
const validatedBody = editMonitorBodyValidation.parse(req.body);
|
await editMonitorBodyValidation.validateAsync(req.body);
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody });
|
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -267,10 +248,10 @@ class MonitorController {
|
|||||||
|
|
||||||
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
pauseMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedParams = pauseMonitorParamValidation.parse(req.params);
|
await pauseMonitorParamValidation.validateAsync(req.params);
|
||||||
|
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
|
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
|
||||||
|
|
||||||
@@ -286,8 +267,8 @@ class MonitorController {
|
|||||||
|
|
||||||
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const id = requireUserId(req.user?.id);
|
const id = requireString(req?.user?.id, "User ID");
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
|
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -300,13 +281,32 @@ class MonitorController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { to } = req.body;
|
||||||
|
if (!to || typeof to !== "string") {
|
||||||
|
throw new AppError({ message: "Invalid 'to' email address", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageId = await this.monitorService.sendTestEmail({ to });
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: "Test email sent successfully",
|
||||||
|
data: { messageId },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query);
|
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||||
|
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const type = validatedQuery.type;
|
const type = parseMonitorTypeFilter(req.query?.type);
|
||||||
const filter = validatedQuery.filter;
|
const filter = optionalString(req.query?.filter, "filter");
|
||||||
|
|
||||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
||||||
|
|
||||||
@@ -322,17 +322,17 @@ class MonitorController {
|
|||||||
|
|
||||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||||
|
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
|
||||||
const explain = validatedQuery.explain;
|
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||||
const limit = validatedQuery.limit;
|
const limit = optionalNumber(req?.query?.limit, "limit");
|
||||||
const page = validatedQuery.page;
|
const page = optionalNumber(req?.query?.page, "page");
|
||||||
const rowsPerPage = validatedQuery.rowsPerPage;
|
const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
|
||||||
const filter = validatedQuery.filter;
|
const filter = optionalString(req?.query?.filter, "filter");
|
||||||
const field = validatedQuery.field;
|
const field = optionalString(req?.query?.field, "field");
|
||||||
const order = validatedQuery.order;
|
const order = parseSortOrder(req?.query?.order);
|
||||||
const type = validatedQuery.type;
|
const type = parseMonitorTypeFilter(req?.query?.type);
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
|
|
||||||
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
||||||
teamId,
|
teamId,
|
||||||
@@ -357,7 +357,11 @@ class MonitorController {
|
|||||||
|
|
||||||
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
|
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = req?.user?.teamId;
|
||||||
|
if (!teamId) {
|
||||||
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const json = await this.monitorService.exportMonitorsToJSON({ teamId });
|
const json = await this.monitorService.exportMonitorsToJSON({ teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -385,7 +389,11 @@ class MonitorController {
|
|||||||
|
|
||||||
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = req?.user?.teamId;
|
||||||
|
if (!teamId) {
|
||||||
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const groups = await this.monitorService.getGroupsByTeamId({ teamId });
|
const groups = await this.monitorService.getGroupsByTeamId({ teamId });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
import {
|
import { Notification } from "@/types/index.js";
|
||||||
createNotificationBodyValidation,
|
import { createNotificationBodyValidation } from "@/validation/joi.js";
|
||||||
deleteNotificationParamValidation,
|
|
||||||
getNotificationByIdParamValidation,
|
|
||||||
testNotificationBodyValidation,
|
|
||||||
editNotificationParamValidation,
|
|
||||||
testAllNotificationsBodyValidation,
|
|
||||||
} from "@/validation/notificationValidation.js";
|
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import { INotificationsService } from "@/service/index.js";
|
import { INotificationsService } from "@/service/index.js";
|
||||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
import { requireTeamId } from "./controllerUtils.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "NotificationController";
|
const SERVICE_NAME = "NotificationController";
|
||||||
|
|
||||||
class NotificationController {
|
class NotificationController {
|
||||||
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private notificationsService: INotificationsService;
|
private notificationsService: INotificationsService;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
||||||
@@ -23,9 +18,13 @@ class NotificationController {
|
|||||||
this.monitorsRepository = monitorsRepository;
|
this.monitorsRepository = monitorsRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get serviceName() {
|
||||||
|
return NotificationController.SERVICE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
testNotification = async (req: Request, res: Response, next: NextFunction) => {
|
testNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const notification = testNotificationBodyValidation.parse(req.body);
|
const notification: Notification = req.body;
|
||||||
const success = await this.notificationsService.sendTestNotification(notification);
|
const success = await this.notificationsService.sendTestNotification(notification);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
@@ -35,7 +34,6 @@ class NotificationController {
|
|||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Notification sent successfully",
|
msg: "Notification sent successfully",
|
||||||
details: { service: SERVICE_NAME },
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@@ -44,12 +42,25 @@ class NotificationController {
|
|||||||
|
|
||||||
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
createNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
await createNotificationBodyValidation.validateAsync(req.body, {
|
||||||
|
abortEarly: false,
|
||||||
|
});
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const body = req.body;
|
||||||
const userId = requireUserId(req.user?.id);
|
|
||||||
|
|
||||||
const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId);
|
const teamId = req?.user?.teamId;
|
||||||
|
if (!teamId) {
|
||||||
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req?.user?.id;
|
||||||
|
if (!userId) {
|
||||||
|
throw new AppError({ message: "User ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
body.userId = userId;
|
||||||
|
body.teamId = teamId;
|
||||||
|
|
||||||
|
const notification = await this.notificationsService.createNotification(body);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Notification created successfully",
|
msg: "Notification created successfully",
|
||||||
@@ -62,7 +73,11 @@ class NotificationController {
|
|||||||
|
|
||||||
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = req?.user?.teamId;
|
||||||
|
if (!teamId) {
|
||||||
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
|
const notifications = await this.notificationsService.findNotificationsByTeamId(teamId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -77,10 +92,17 @@ class NotificationController {
|
|||||||
|
|
||||||
deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
|
deleteNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = req?.user?.teamId;
|
||||||
const validatedParams = deleteNotificationParamValidation.parse(req.params);
|
if (!teamId) {
|
||||||
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
await this.notificationsService.deleteById(validatedParams.id, teamId);
|
const notificationId = req.params.id as string;
|
||||||
|
if (!notificationId) {
|
||||||
|
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.notificationsService.deleteById(notificationId, teamId);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Notification deleted successfully",
|
msg: "Notification deleted successfully",
|
||||||
@@ -93,9 +115,12 @@ class NotificationController {
|
|||||||
getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
|
getNotificationById = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const validatedParams = getNotificationByIdParamValidation.parse(req.params);
|
const notificationId = req.params.id as string;
|
||||||
|
if (!notificationId) {
|
||||||
|
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const notification = await this.notificationsService.findById(validatedParams.id, teamId);
|
const notification = await this.notificationsService.findById(notificationId, teamId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -109,13 +134,16 @@ class NotificationController {
|
|||||||
|
|
||||||
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
editNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedBody = createNotificationBodyValidation.parse(req.body);
|
await createNotificationBodyValidation.validateAsync(req.body, {
|
||||||
const validatedParams = editNotificationParamValidation.parse(req.params);
|
abortEarly: false,
|
||||||
|
});
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
const notificationId = validatedParams.id;
|
const notificationId = req.params.id as string;
|
||||||
|
if (!notificationId) {
|
||||||
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody);
|
throw new AppError({ message: "Notification ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Notification updated successfully",
|
msg: "Notification updated successfully",
|
||||||
@@ -128,11 +156,13 @@ class NotificationController {
|
|||||||
|
|
||||||
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
testAllNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const validatedBody = testAllNotificationsBodyValidation.parse(req.body);
|
const monitorId = req.body.monitorId;
|
||||||
|
const teamId = req?.user?.teamId;
|
||||||
|
if (!teamId) {
|
||||||
|
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||||
|
|
||||||
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
|
||||||
|
|
||||||
const notifications = monitor.notifications;
|
const notifications = monitor.notifications;
|
||||||
if (notifications.length === 0) {
|
if (notifications.length === 0) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
import { sendTestEmailBodyValidation, updateAppSettingsBodyValidation } from "@/validation/joi.js";
|
||||||
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "SettingsController";
|
const SERVICE_NAME = "SettingsController";
|
||||||
@@ -56,24 +55,20 @@ class SettingsController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
await updateAppSettingsBodyValidation.validateAsync(req.body);
|
||||||
updateAppSettingsBodyValidation.parse(req.body);
|
|
||||||
|
|
||||||
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
||||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "App settings updated successfully",
|
msg: "App settings updated successfully",
|
||||||
data: returnSettings,
|
data: returnSettings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
sendTestEmail = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
sendTestEmailBodyValidation.parse(req.body);
|
await sendTestEmailBodyValidation.validateAsync(req.body);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
import {
|
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js";
|
||||||
createStatusPageBodyValidation,
|
|
||||||
getStatusPageParamValidation,
|
|
||||||
getStatusPageQueryValidation,
|
|
||||||
imageValidation,
|
|
||||||
} from "@/validation/statusPageValidation.js";
|
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||||
import { IStatusPageService } from "@/service/business/statusPageService.js";
|
import { IStatusPageService } from "@/service/business/statusPageService.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
import { ISettingsService } from "@/service/system/settingsService.js";
|
import { ISettingsService } from "@/service/system/settingsService.js";
|
||||||
|
import { ParseBoolean } from "@/utils/utils.js";
|
||||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "statusPageController";
|
const SERVICE_NAME = "statusPageController";
|
||||||
@@ -32,10 +28,8 @@ class StatusPageController {
|
|||||||
|
|
||||||
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
createStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
createStatusPageBodyValidation.parse(req.body);
|
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||||
if (req.file) {
|
await imageValidation.validateAsync(req.file);
|
||||||
imageValidation.parse(req.file);
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const userId = requireUserId(req?.user?.id);
|
const userId = requireUserId(req?.user?.id);
|
||||||
@@ -53,10 +47,8 @@ class StatusPageController {
|
|||||||
|
|
||||||
updateStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
updateStatusPage = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
createStatusPageBodyValidation.parse(req.body);
|
await createStatusPageBodyValidation.validateAsync(req.body);
|
||||||
if (req.file) {
|
await imageValidation.validateAsync(req.file);
|
||||||
imageValidation.parse(req.file);
|
|
||||||
}
|
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
const statusPageId = req.params.id as string;
|
const statusPageId = req.params.id as string;
|
||||||
if (!statusPageId) {
|
if (!statusPageId) {
|
||||||
@@ -78,8 +70,8 @@ class StatusPageController {
|
|||||||
|
|
||||||
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
|
getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getStatusPageParamValidation.parse(req.params);
|
await getStatusPageParamValidation.validateAsync(req.params);
|
||||||
getStatusPageQueryValidation.parse(req.query);
|
await getStatusPageQueryValidation.validateAsync(req.query);
|
||||||
|
|
||||||
if (!req.params.url) {
|
if (!req.params.url) {
|
||||||
throw new AppError({ message: "Status page URL is required", status: 400 });
|
throw new AppError({ message: "Status page URL is required", status: 400 });
|
||||||
@@ -109,7 +101,6 @@ class StatusPageController {
|
|||||||
const normalizedMonitors = sortedMonitors.map((monitor) => {
|
const normalizedMonitors = sortedMonitors.map((monitor) => {
|
||||||
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
|
||||||
if (!showURL) {
|
if (!showURL) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { url, port, secret, notifications, ...rest } = monitor;
|
const { url, port, secret, notifications, ...rest } = monitor;
|
||||||
return { ...rest, checks: normalizedChecks };
|
return { ...rest, checks: normalizedChecks };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export interface IDb {
|
|
||||||
connect(): Promise<void>;
|
|
||||||
disconnect(): Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import AppSettings from "@/db/models/AppSettings.js";
|
import AppSettings from "./models/AppSettings.js";
|
||||||
import { runMigrations } from "@/db/migration/index.js";
|
import { runMigrations } from "./migration/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||||
const SERVICE_NAME = "MongoDB";
|
const SERVICE_NAME = "MongoDB";
|
||||||
import { IDb } from "@/db/IDb.js";
|
class MongoDB {
|
||||||
|
|
||||||
class MongoDB implements IDb {
|
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
@@ -48,12 +46,12 @@ class MongoDB implements IDb {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await runMigrations();
|
await runMigrations();
|
||||||
} catch (error: unknown) {
|
} catch (error: any) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error instanceof Error ? error.message : "Unknown error",
|
message: error.message,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "connect",
|
method: "connect",
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -65,12 +63,12 @@ class MongoDB implements IDb {
|
|||||||
await mongoose.disconnect();
|
await mongoose.disconnect();
|
||||||
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
|
this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" });
|
||||||
return;
|
return;
|
||||||
} catch (error: unknown) {
|
} catch (error: any) {
|
||||||
this.logger.error({
|
this.logger.error({
|
||||||
message: error instanceof Error ? error.message : "Unknown error",
|
message: error.message,
|
||||||
service: SERVICE_NAME,
|
service: SERVICE_NAME,
|
||||||
method: "disconnect",
|
method: "disconnect",
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
stack: error.stack,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { MonitorModel } from "../models/Monitor.js";
|
|
||||||
import { logger } from "@/utils/logger.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fix infrastructure monitors that had thresholds incorrectly set to 0 or 5
|
|
||||||
* due to the old default value. Updates them to 100 (disabled).
|
|
||||||
*/
|
|
||||||
export async function fixInfrastructureThresholds(): Promise<void> {
|
|
||||||
const SERVICE_NAME = "Migration:FixInfrastructureThresholds";
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" });
|
|
||||||
|
|
||||||
const thresholdFields = ["cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", "tempAlertThreshold"];
|
|
||||||
|
|
||||||
// Find hardware monitors with any threshold set to 0 or 5
|
|
||||||
const filter = {
|
|
||||||
type: "hardware",
|
|
||||||
$or: thresholdFields.flatMap((field) => [{ [field]: 0 }, { [field]: 5 }]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitors = await MonitorModel.find(filter);
|
|
||||||
|
|
||||||
if (monitors.length === 0) {
|
|
||||||
logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 0 or 5 found" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
message: `Found ${monitors.length} monitors with threshold value of 0 or 5`,
|
|
||||||
});
|
|
||||||
|
|
||||||
let updatedCount = 0;
|
|
||||||
|
|
||||||
for (const monitor of monitors) {
|
|
||||||
const update: Record<string, number> = {};
|
|
||||||
|
|
||||||
for (const field of thresholdFields) {
|
|
||||||
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
|
|
||||||
update[field] = 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(update).length > 0) {
|
|
||||||
await MonitorModel.updateOne({ _id: monitor._id }, { $set: update });
|
|
||||||
updatedCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info({
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
message: `Fixed ${updatedCount} monitors — thresholds updated from 0/5 to 100`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
logger.error({ service: SERVICE_NAME, message: `Error fixing thresholds: ${errorMessage}` });
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
|
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
|
||||||
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
|
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
|
||||||
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
||||||
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
|
||||||
import MigrationModel from "../models/Migration.js";
|
import MigrationModel from "../models/Migration.js";
|
||||||
|
|
||||||
type MigrationEntry = {
|
type MigrationEntry = {
|
||||||
@@ -13,7 +12,6 @@ const migrations: MigrationEntry[] = [
|
|||||||
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
|
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
|
||||||
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
||||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||||
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
|
|||||||
type: MonitorType;
|
type: MonitorType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "createdAt" | "updatedAt"> & {
|
type CheckDocumentBase = Omit<Check, "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"> & {
|
||||||
metadata: CheckMetadataDocument;
|
metadata: CheckMetadataDocument;
|
||||||
|
expiry: Date;
|
||||||
|
ackAt?: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
__v: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CheckDocument extends CheckDocumentBase {
|
interface CheckDocument extends CheckDocumentBase {
|
||||||
@@ -222,7 +225,18 @@ const CheckSchema = new Schema<CheckDocument>(
|
|||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
expiry: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now,
|
||||||
|
},
|
||||||
|
ack: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
ackAt: {
|
||||||
|
type: Date,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
cpu: {
|
cpu: {
|
||||||
type: cpuSchema,
|
type: cpuSchema,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import { Schema, model, Types } from "mongoose";
|
|
||||||
import { MonitorTypes, type MonitorType } from "@/types/monitor.js";
|
|
||||||
import type { GeoCheck, GeoCheckLocation, GeoCheckMetadata, GeoCheckResult, GeoCheckTimings } from "@/types/geoCheck.js";
|
|
||||||
|
|
||||||
type GeoCheckMetadataDocument = Omit<GeoCheckMetadata, "monitorId" | "teamId"> & {
|
|
||||||
monitorId: Types.ObjectId;
|
|
||||||
teamId: Types.ObjectId;
|
|
||||||
type: MonitorType;
|
|
||||||
};
|
|
||||||
|
|
||||||
type GeoCheckDocumentBase = Omit<GeoCheck, "id" | "metadata" | "expiry" | "createdAt" | "updatedAt" | "results"> & {
|
|
||||||
metadata: GeoCheckMetadataDocument;
|
|
||||||
results: GeoCheckResult[];
|
|
||||||
expiry: Date;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
__v: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface GeoCheckDocument extends GeoCheckDocumentBase {
|
|
||||||
_id: Types.ObjectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const geoCheckMetadataSchema = new Schema<GeoCheckMetadataDocument>(
|
|
||||||
{
|
|
||||||
monitorId: { type: Schema.Types.ObjectId, required: true, index: true },
|
|
||||||
teamId: { type: Schema.Types.ObjectId, required: true, index: true },
|
|
||||||
type: { type: String, required: true, enum: MonitorTypes },
|
|
||||||
},
|
|
||||||
{ _id: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const geoCheckTimingsSchema = new Schema<GeoCheckTimings>(
|
|
||||||
{
|
|
||||||
total: { type: Number, default: 0 },
|
|
||||||
dns: { type: Number, default: 0 },
|
|
||||||
tcp: { type: Number, default: 0 },
|
|
||||||
tls: { type: Number, default: 0 },
|
|
||||||
firstByte: { type: Number, default: 0 },
|
|
||||||
download: { type: Number, default: 0 },
|
|
||||||
},
|
|
||||||
{ _id: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const geoCheckLocationSchema = new Schema<GeoCheckLocation>(
|
|
||||||
{
|
|
||||||
continent: { type: String, required: true },
|
|
||||||
region: { type: String, default: "" },
|
|
||||||
country: { type: String, default: "" },
|
|
||||||
state: { type: String, default: "" },
|
|
||||||
city: { type: String, default: "" },
|
|
||||||
longitude: { type: Number, default: 0 },
|
|
||||||
latitude: { type: Number, default: 0 },
|
|
||||||
},
|
|
||||||
{ _id: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const geoCheckResultSchema = new Schema<GeoCheckResult>(
|
|
||||||
{
|
|
||||||
location: {
|
|
||||||
type: geoCheckLocationSchema,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
statusCode: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
timings: {
|
|
||||||
type: geoCheckTimingsSchema,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ _id: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const GeoCheckSchema = new Schema<GeoCheckDocument>(
|
|
||||||
{
|
|
||||||
metadata: {
|
|
||||||
type: geoCheckMetadataSchema,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
results: {
|
|
||||||
type: [geoCheckResultSchema],
|
|
||||||
required: true,
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
expiry: {
|
|
||||||
type: Date,
|
|
||||||
default: Date.now,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: true,
|
|
||||||
strict: false,
|
|
||||||
timeseries: {
|
|
||||||
timeField: "createdAt",
|
|
||||||
metaField: "metadata",
|
|
||||||
granularity: "seconds",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
GeoCheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 });
|
|
||||||
GeoCheckSchema.index({ "metadata.monitorId": 1, createdAt: 1 });
|
|
||||||
GeoCheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
|
|
||||||
GeoCheckSchema.index({ createdAt: 1 });
|
|
||||||
|
|
||||||
const GeoCheckModel = model<GeoCheckDocument>("GeoCheck", GeoCheckSchema);
|
|
||||||
|
|
||||||
export type { GeoCheckMetadataDocument };
|
|
||||||
export { GeoCheckModel };
|
|
||||||
export default GeoCheckModel;
|
|
||||||
@@ -160,10 +160,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
|||||||
gameId: {
|
gameId: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
grpcServiceName: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
group: {
|
group: {
|
||||||
type: String,
|
type: String,
|
||||||
trim: true,
|
trim: true,
|
||||||
@@ -173,18 +169,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
|||||||
return value && value.trim() ? value.trim() : null;
|
return value && value.trim() ? value.trim() : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
geoCheckEnabled: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
geoCheckLocations: {
|
|
||||||
type: [String],
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
geoCheckInterval: {
|
|
||||||
type: Number,
|
|
||||||
default: 300000,
|
|
||||||
},
|
|
||||||
recentChecks: {
|
recentChecks: {
|
||||||
type: [checkSnapshotSchema],
|
type: [checkSnapshotSchema],
|
||||||
default: [],
|
default: [],
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
|||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[],
|
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
notificationName: {
|
notificationName: {
|
||||||
|
|||||||
@@ -33,6 +33,3 @@ export { default as TeamModel } from "@/db/models/Team.js";
|
|||||||
|
|
||||||
export * from "@/db/models/MaintenanceWindow.js";
|
export * from "@/db/models/MaintenanceWindow.js";
|
||||||
export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js";
|
export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js";
|
||||||
|
|
||||||
export * from "@/db/models/GeoCheck.js";
|
|
||||||
export { default as GeoCheckModel } from "@/db/models/GeoCheck.js";
|
|
||||||
+5
-8
@@ -2,23 +2,19 @@ import { initializeServices } from "./config/services.js";
|
|||||||
import { initializeControllers } from "./config/controllers.js";
|
import { initializeControllers } from "./config/controllers.js";
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { initShutdownListener } from "./shutdown.js";
|
import { initShutdownListener } from "./shutdown.js";
|
||||||
import { validateEnv } from "./validation/envValidation.js";
|
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { runMigrations } from "./db/migration/index.js";
|
import { runMigrations } from "./db/migration/index.js";
|
||||||
|
|
||||||
import Logger, { ILogger } from "@/utils/logger.js";
|
import Logger, { ILogger } from "@/utils/logger.js";
|
||||||
import { SettingsService } from "@/service/index.js";
|
import SettingsService from "@/service/system/settingsService.js";
|
||||||
import { MongoSettingsRepository } from "./repositories/index.js";
|
import { MongoSettingsRepository } from "./repositories/index.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "Server";
|
const SERVICE_NAME = "Server";
|
||||||
let logger: ILogger;
|
let logger: ILogger;
|
||||||
|
|
||||||
const startApp = async () => {
|
const startApp = async () => {
|
||||||
// Validate environment variables first
|
|
||||||
const env = validateEnv();
|
|
||||||
|
|
||||||
// FE path
|
// FE path
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -27,7 +23,7 @@ const startApp = async () => {
|
|||||||
|
|
||||||
// Create services
|
// Create services
|
||||||
const settingsRepository = new MongoSettingsRepository();
|
const settingsRepository = new MongoSettingsRepository();
|
||||||
const settingsService = new SettingsService(settingsRepository, env);
|
const settingsService = new SettingsService(settingsRepository);
|
||||||
|
|
||||||
const envSettings = settingsService.loadSettings();
|
const envSettings = settingsService.loadSettings();
|
||||||
|
|
||||||
@@ -50,8 +46,9 @@ const startApp = async () => {
|
|||||||
openApiSpec,
|
openApiSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = app.listen(env.PORT, () => {
|
const port = envSettings.port || 52345;
|
||||||
logger.info({ message: `Server started on port:${env.PORT}` });
|
const server = app.listen(port, () => {
|
||||||
|
logger.info({ message: `Server started on port:${port}` });
|
||||||
});
|
});
|
||||||
|
|
||||||
initShutdownListener(server, services);
|
initShutdownListener(server, services);
|
||||||
|
|||||||
@@ -20,19 +20,12 @@ export interface IChecksRepository {
|
|||||||
monitorId: string,
|
monitorId: string,
|
||||||
sortOrder: string,
|
sortOrder: string,
|
||||||
dateRange: string,
|
dateRange: string,
|
||||||
filter: string | undefined,
|
filter: string,
|
||||||
page: number,
|
page: number,
|
||||||
rowsPerPage: number,
|
rowsPerPage: number,
|
||||||
status: boolean | undefined
|
status: boolean | undefined
|
||||||
): Promise<ChecksQueryResult>;
|
): Promise<ChecksQueryResult>;
|
||||||
findByTeamId(
|
findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<ChecksQueryResult>;
|
||||||
sortOrder: string,
|
|
||||||
dateRange: string,
|
|
||||||
filter: string | undefined,
|
|
||||||
page: number,
|
|
||||||
rowsPerPage: number,
|
|
||||||
teamId: string
|
|
||||||
): Promise<ChecksQueryResult>;
|
|
||||||
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
|
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
|
||||||
findByDateRangeAndMonitorId(
|
findByDateRangeAndMonitorId(
|
||||||
monitorId: string,
|
monitorId: string,
|
||||||
@@ -47,5 +40,4 @@ export interface IChecksRepository {
|
|||||||
deleteByMonitorId(monitorId: string): Promise<number>;
|
deleteByMonitorId(monitorId: string): Promise<number>;
|
||||||
deleteByTeamId(teamId: string): Promise<number>;
|
deleteByTeamId(teamId: string): Promise<number>;
|
||||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||||
deleteOlderThan(date: Date): Promise<number>;
|
|
||||||
}
|
}
|
||||||
@@ -13,13 +13,20 @@ import type {
|
|||||||
GotTimings,
|
GotTimings,
|
||||||
MonitorType,
|
MonitorType,
|
||||||
} from "@/types/index.js";
|
} from "@/types/index.js";
|
||||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
|
||||||
import { ILogger } from "@/utils/logger.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "StatusService";
|
const SERVICE_NAME = "StatusService";
|
||||||
|
|
||||||
|
const dateRangeLookup: Record<string, Date | undefined> = {
|
||||||
|
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||||
|
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
||||||
|
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||||
|
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||||
|
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||||
|
all: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export type LatestChecksMap = Record<string, Check[]>;
|
export type LatestChecksMap = Record<string, Check[]>;
|
||||||
type DateRange = { start: Date; end: Date };
|
type DateRange = { start: Date; end: Date };
|
||||||
type HardwareUpChecks = { totalChecks: number };
|
type HardwareUpChecks = { totalChecks: number };
|
||||||
@@ -27,8 +34,8 @@ type HardwareUpChecks = { totalChecks: number };
|
|||||||
class MongoChecksRepository implements IChecksRepository {
|
class MongoChecksRepository implements IChecksRepository {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private logger: ILogger;
|
private logger: any;
|
||||||
constructor(logger: ILogger) {
|
constructor(logger: any) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +54,13 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toOptionalDateString = (value?: Date | string | null): string | undefined => {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return toDateString(value);
|
||||||
|
};
|
||||||
|
|
||||||
const mapTimings = (timings?: GotTimings): GotTimings => {
|
const mapTimings = (timings?: GotTimings): GotTimings => {
|
||||||
const phases = timings?.phases ?? {
|
const phases = timings?.phases ?? {
|
||||||
wait: 0,
|
wait: 0,
|
||||||
@@ -165,6 +179,9 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
timings: mapTimings(doc.timings),
|
timings: mapTimings(doc.timings),
|
||||||
statusCode: doc.statusCode ?? 0,
|
statusCode: doc.statusCode ?? 0,
|
||||||
message: doc.message ?? "",
|
message: doc.message ?? "",
|
||||||
|
ack: doc.ack ?? false,
|
||||||
|
ackAt: toOptionalDateString(doc.ackAt),
|
||||||
|
expiry: toDateString(doc.expiry),
|
||||||
cpu: mapCpu(doc.cpu),
|
cpu: mapCpu(doc.cpu),
|
||||||
memory: mapMemory(doc.memory),
|
memory: mapMemory(doc.memory),
|
||||||
disk: mapDisks(doc.disk),
|
disk: mapDisks(doc.disk),
|
||||||
@@ -177,6 +194,7 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
seo: doc.seo,
|
seo: doc.seo,
|
||||||
performance: doc.performance,
|
performance: doc.performance,
|
||||||
audits: mapAudits(doc.audits),
|
audits: mapAudits(doc.audits),
|
||||||
|
__v: doc.__v ?? 0,
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
};
|
};
|
||||||
@@ -221,18 +239,18 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
monitorId: string,
|
monitorId: string,
|
||||||
sortOrder: string,
|
sortOrder: string,
|
||||||
dateRange: string,
|
dateRange: string,
|
||||||
filter: string | undefined,
|
filter: string,
|
||||||
page: number,
|
page: number,
|
||||||
rowsPerPage: number,
|
rowsPerPage: number,
|
||||||
status: boolean | undefined
|
status: boolean | undefined
|
||||||
) => {
|
) => {
|
||||||
// Match
|
// Match
|
||||||
const matchStage: Record<string, unknown> = {
|
const matchStage: Record<string, any> = {
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||||
...(typeof status !== "undefined" && { status }),
|
...(typeof status !== "undefined" && { status }),
|
||||||
...(getDateForRange(dateRange) && {
|
...(dateRangeLookup[dateRange] && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: getDateForRange(dateRange),
|
$gte: dateRangeLookup[dateRange],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -279,11 +297,11 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
};
|
};
|
||||||
|
|
||||||
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
||||||
const matchStage: Record<string, unknown> = {
|
const matchStage: Record<string, any> = {
|
||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
...(getDateForRange(dateRange) && {
|
...(dateRangeLookup[dateRange] && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: getDateForRange(dateRange),
|
$gte: dateRangeLookup[dateRange],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -348,7 +366,7 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => {
|
const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => {
|
||||||
acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc));
|
acc[monitorId] = docs.map((doc: any) => this.toEntity(doc as CheckDocument));
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
return mapped;
|
return mapped;
|
||||||
@@ -360,7 +378,7 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||||
}
|
}
|
||||||
if (options?.type === "pagespeed") {
|
if (options?.type === "pagespeed") {
|
||||||
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
|
||||||
}
|
}
|
||||||
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
|
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
|
||||||
};
|
};
|
||||||
@@ -368,9 +386,9 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
|
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
|
||||||
const baseMatch = {
|
const baseMatch = {
|
||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||||
...(getDateForRange(dateRange) && {
|
...(dateRangeLookup[dateRange] && {
|
||||||
createdAt: {
|
createdAt: {
|
||||||
$gte: getDateForRange(dateRange),
|
$gte: dateRangeLookup[dateRange],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -402,31 +420,6 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
return result.deletedCount ?? 0;
|
return result.deletedCount ?? 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteOlderThan = async (cutoffDate: Date, batchDays: number = 30): Promise<number> => {
|
|
||||||
// Find the oldest check that is older than the cutoff
|
|
||||||
const oldest = await CheckModel.findOne({ createdAt: { $lt: cutoffDate } })
|
|
||||||
.sort({ createdAt: 1 })
|
|
||||||
.select({ createdAt: 1 });
|
|
||||||
if (!oldest?.createdAt) return 0;
|
|
||||||
|
|
||||||
let totalDeleted = 0;
|
|
||||||
let batchStart = new Date(oldest.createdAt);
|
|
||||||
const batchMs = batchDays * 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
// Delete in 30 day chunks until we reach the cutoff
|
|
||||||
while (batchStart < cutoffDate) {
|
|
||||||
// Advance startTime by batchMs to get to the end of last batch
|
|
||||||
const nextStart = batchStart.getTime() + batchMs;
|
|
||||||
const batchEnd = new Date(Math.min(nextStart, cutoffDate.getTime()));
|
|
||||||
const result = await CheckModel.deleteMany({
|
|
||||||
createdAt: { $gte: batchStart, $lt: batchEnd },
|
|
||||||
});
|
|
||||||
totalDeleted += result.deletedCount ?? 0;
|
|
||||||
batchStart = batchEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalDeleted;
|
|
||||||
};
|
|
||||||
private findUptimeDateRangeChecks = async (
|
private findUptimeDateRangeChecks = async (
|
||||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
||||||
monitorObjectId: mongoose.Types.ObjectId,
|
monitorObjectId: mongoose.Types.ObjectId,
|
||||||
@@ -579,50 +572,16 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
|
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
|
||||||
const matchStage = {
|
const matchStage = {
|
||||||
"metadata.monitorId": monitorObjectId,
|
"metadata.monitorId": monitorObjectId,
|
||||||
createdAt: { $gte: startDate, $lte: endDate },
|
createdAt: { $gte: startDate, $lte: endDate },
|
||||||
};
|
};
|
||||||
|
|
||||||
const [result] = await CheckModel.aggregate([
|
const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
|
||||||
{ $match: matchStage },
|
|
||||||
{ $sort: { createdAt: 1 } },
|
|
||||||
{
|
|
||||||
$facet: {
|
|
||||||
groupedChecks: [
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: {
|
|
||||||
$dateToString: { format: dateString, date: "$createdAt" },
|
|
||||||
},
|
|
||||||
avgPerformance: { $avg: "$performance" },
|
|
||||||
avgAccessibility: { $avg: "$accessibility" },
|
|
||||||
avgBestPractices: { $avg: "$bestPractices" },
|
|
||||||
avgSeo: { $avg: "$seo" },
|
|
||||||
totalChecks: { $sum: 1 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ $sort: { _id: 1 } },
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
bucketDate: "$_id",
|
|
||||||
performance: "$avgPerformance",
|
|
||||||
accessibility: "$avgAccessibility",
|
|
||||||
bestPractices: "$avgBestPractices",
|
|
||||||
seo: "$avgSeo",
|
|
||||||
totalChecks: 1,
|
|
||||||
_id: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
monitorType: "pagespeed" as const,
|
monitorType: "pagespeed" as const,
|
||||||
groupedChecks: result?.groupedChecks ?? [],
|
checks: this.mapDocuments(checks),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import type { GeoCheck, GroupedGeoCheck } from "@/types/geoCheck.js";
|
|
||||||
import type { GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
|
||||||
|
|
||||||
export interface GeoChecksQueryResult {
|
|
||||||
geoChecksCount: number;
|
|
||||||
geoChecks: GeoCheck[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FlatGeoChecksQueryResult {
|
|
||||||
geoChecksCount: number;
|
|
||||||
geoChecks: FlatGeoCheck[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGeoChecksRepository {
|
|
||||||
createGeoChecks(geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]>;
|
|
||||||
findByMonitorId(
|
|
||||||
monitorId: string,
|
|
||||||
sortOrder: string,
|
|
||||||
dateRange: string,
|
|
||||||
page: number,
|
|
||||||
rowsPerPage: number,
|
|
||||||
continents?: GeoContinent[]
|
|
||||||
): Promise<FlatGeoChecksQueryResult>;
|
|
||||||
findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]>;
|
|
||||||
findGroupedByMonitorIdAndDateRange(
|
|
||||||
monitorId: string,
|
|
||||||
startDate: Date,
|
|
||||||
endDate: Date,
|
|
||||||
dateFormat: string,
|
|
||||||
continents?: GeoContinent[]
|
|
||||||
): Promise<GroupedGeoCheck[]>;
|
|
||||||
deleteByMonitorId(monitorId: string): Promise<number>;
|
|
||||||
deleteByTeamId(teamId: string): Promise<number>;
|
|
||||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
|
||||||
}
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
|
||||||
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
|
||||||
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
|
||||||
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
|
||||||
import mongoose from "mongoose";
|
|
||||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
|
||||||
import { ILogger } from "@/utils/logger.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "GeoChecksRepository";
|
|
||||||
|
|
||||||
class MongoGeoChecksRepository implements IGeoChecksRepository {
|
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
|
||||||
|
|
||||||
private logger: ILogger;
|
|
||||||
constructor(logger: ILogger) {
|
|
||||||
this.logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
private toEntity = (doc: GeoCheckDocument): GeoCheck => {
|
|
||||||
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
|
|
||||||
if (!value) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toDateString = (value?: Date | string | null): string => {
|
|
||||||
if (!value) {
|
|
||||||
return new Date(0).toISOString();
|
|
||||||
}
|
|
||||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapMetadata = (metadata: any): GeoCheckMetadata => ({
|
|
||||||
monitorId: toStringId(metadata.monitorId),
|
|
||||||
teamId: toStringId(metadata.teamId),
|
|
||||||
type: metadata.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapResults = (results: any[]): GeoCheckResult[] => {
|
|
||||||
if (!results || !Array.isArray(results)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return results.map((result) => ({
|
|
||||||
location: {
|
|
||||||
continent: result.location?.continent ?? "",
|
|
||||||
region: result.location?.region ?? "",
|
|
||||||
country: result.location?.country ?? "",
|
|
||||||
state: result.location?.state ?? "",
|
|
||||||
city: result.location?.city ?? "",
|
|
||||||
longitude: result.location?.longitude ?? 0,
|
|
||||||
latitude: result.location?.latitude ?? 0,
|
|
||||||
},
|
|
||||||
status: result.status ?? false,
|
|
||||||
statusCode: result.statusCode ?? 0,
|
|
||||||
timings: {
|
|
||||||
total: result.timings?.total ?? 0,
|
|
||||||
dns: result.timings?.dns ?? 0,
|
|
||||||
tcp: result.timings?.tcp ?? 0,
|
|
||||||
tls: result.timings?.tls ?? 0,
|
|
||||||
firstByte: result.timings?.firstByte ?? 0,
|
|
||||||
download: result.timings?.download ?? 0,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: toStringId(doc._id),
|
|
||||||
metadata: mapMetadata(doc.metadata),
|
|
||||||
results: mapResults(doc.results),
|
|
||||||
expiry: toDateString(doc.expiry),
|
|
||||||
__v: doc.__v ?? 0,
|
|
||||||
createdAt: toDateString(doc.createdAt),
|
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
createGeoChecks = async (geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]> => {
|
|
||||||
try {
|
|
||||||
const docs = await GeoCheckModel.insertMany(
|
|
||||||
geoChecks.map((geoCheck) => ({
|
|
||||||
metadata: {
|
|
||||||
monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId),
|
|
||||||
teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId),
|
|
||||||
type: geoCheck.metadata.type,
|
|
||||||
},
|
|
||||||
results: geoCheck.results,
|
|
||||||
expiry: new Date(geoCheck.expiry),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
return docs.map((doc) => this.toEntity(doc));
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Failed to createGeoChecks: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "createGeoChecks",
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findByMonitorId = async (
|
|
||||||
monitorId: string,
|
|
||||||
sortOrder: string,
|
|
||||||
dateRange: string,
|
|
||||||
page: number,
|
|
||||||
rowsPerPage: number,
|
|
||||||
continents?: GeoContinent[]
|
|
||||||
): Promise<FlatGeoChecksQueryResult> => {
|
|
||||||
try {
|
|
||||||
const matchStage: Record<string, any> = {
|
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
|
||||||
...(getDateForRange(dateRange) && {
|
|
||||||
createdAt: {
|
|
||||||
$gte: getDateForRange(dateRange),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const convertedSortOrder = sortOrder === "asc" ? 1 : -1;
|
|
||||||
|
|
||||||
let skip = 0;
|
|
||||||
if (page && rowsPerPage) {
|
|
||||||
skip = page * rowsPerPage;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (continents && continents.length > 0) {
|
|
||||||
const pipeline: any[] = [
|
|
||||||
{ $match: matchStage },
|
|
||||||
{ $unwind: "$results" },
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
"results.location.continent": { $in: continents },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common pipeline stages for both paths
|
|
||||||
const pipeline: any[] = [
|
|
||||||
{ $match: matchStage },
|
|
||||||
{ $unwind: "$results" },
|
|
||||||
// Filter by continent if specified
|
|
||||||
...(continents && continents.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
"results.location.continent": { $in: continents },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
// Project to flat structure
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
_id: 0,
|
|
||||||
monitorId: "$metadata.monitorId",
|
|
||||||
teamId: "$metadata.teamId",
|
|
||||||
type: "$metadata.type",
|
|
||||||
location: "$results.location",
|
|
||||||
status: "$results.status",
|
|
||||||
statusCode: "$results.statusCode",
|
|
||||||
timings: "$results.timings",
|
|
||||||
createdAt: 1,
|
|
||||||
updatedAt: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ $sort: { createdAt: convertedSortOrder } },
|
|
||||||
{ $skip: skip },
|
|
||||||
{ $limit: rowsPerPage },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Count pipeline
|
|
||||||
const countPipeline: any[] = [
|
|
||||||
{ $match: matchStage },
|
|
||||||
{ $unwind: "$results" },
|
|
||||||
...(continents && continents.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
"results.location.continent": { $in: continents },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{ $count: "count" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]);
|
|
||||||
|
|
||||||
const geoChecksCount = countResult[0]?.count || 0;
|
|
||||||
const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({
|
|
||||||
id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`,
|
|
||||||
monitorId: doc.monitorId.toString(),
|
|
||||||
teamId: doc.teamId.toString(),
|
|
||||||
type: doc.type,
|
|
||||||
location: doc.location,
|
|
||||||
status: doc.status,
|
|
||||||
statusCode: doc.statusCode,
|
|
||||||
timings: doc.timings,
|
|
||||||
createdAt: new Date(doc.createdAt).toISOString(),
|
|
||||||
updatedAt: new Date(doc.updatedAt).toISOString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { geoChecksCount, geoChecks };
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Error finding geo checks by monitor ID: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "findByMonitorId",
|
|
||||||
stack: error.stack,
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findByMonitorIdAndDateRange = async (monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]> => {
|
|
||||||
try {
|
|
||||||
const docs = await GeoCheckModel.find({
|
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
|
||||||
createdAt: {
|
|
||||||
$gte: startDate,
|
|
||||||
$lte: endDate,
|
|
||||||
},
|
|
||||||
}).sort({ createdAt: -1 });
|
|
||||||
return docs.map(this.toEntity);
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Error finding geo checks by monitor ID and date range: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "findByMonitorIdAndDateRange",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
findGroupedByMonitorIdAndDateRange = async (
|
|
||||||
monitorId: string,
|
|
||||||
startDate: Date,
|
|
||||||
endDate: Date,
|
|
||||||
dateFormat: string,
|
|
||||||
continents?: GeoContinent[]
|
|
||||||
): Promise<GroupedGeoCheck[]> => {
|
|
||||||
try {
|
|
||||||
const pipeline: any[] = [
|
|
||||||
// Match geo checks for this monitor in date range
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
|
||||||
createdAt: {
|
|
||||||
$gte: startDate,
|
|
||||||
$lte: endDate,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Unwind the results array to process each location separately
|
|
||||||
{
|
|
||||||
$unwind: "$results",
|
|
||||||
},
|
|
||||||
// Filter by continent if specified
|
|
||||||
...(continents && continents.length > 0
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
"results.location.continent": { $in: continents },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
// Group by date bucket and continent
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: {
|
|
||||||
bucketDate: {
|
|
||||||
$dateToString: {
|
|
||||||
format: dateFormat,
|
|
||||||
date: "$createdAt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
continent: "$results.location.continent",
|
|
||||||
},
|
|
||||||
avgResponseTime: { $avg: "$results.timings.total" },
|
|
||||||
totalChecks: { $sum: 1 },
|
|
||||||
upChecks: {
|
|
||||||
$sum: {
|
|
||||||
$cond: ["$results.status", 1, 0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Calculate uptime percentage
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
_id: 0,
|
|
||||||
bucketDate: "$_id.bucketDate",
|
|
||||||
continent: "$_id.continent",
|
|
||||||
avgResponseTime: { $round: ["$avgResponseTime", 2] },
|
|
||||||
totalChecks: 1,
|
|
||||||
uptimePercentage: {
|
|
||||||
$round: [
|
|
||||||
{
|
|
||||||
$multiply: [
|
|
||||||
{
|
|
||||||
$divide: ["$upChecks", "$totalChecks"],
|
|
||||||
},
|
|
||||||
100,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Sort by date and continent
|
|
||||||
{
|
|
||||||
$sort: {
|
|
||||||
bucketDate: 1,
|
|
||||||
continent: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const results = await GeoCheckModel.aggregate(pipeline);
|
|
||||||
return results as GroupedGeoCheck[];
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Error finding grouped geo checks: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "findGroupedByMonitorIdAndDateRange",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteByMonitorId = async (monitorId: string): Promise<number> => {
|
|
||||||
try {
|
|
||||||
const result = await GeoCheckModel.deleteMany({
|
|
||||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
|
||||||
});
|
|
||||||
return result.deletedCount || 0;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Error deleting geo checks by monitor ID: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "deleteByMonitorId",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteByTeamId = async (teamId: string): Promise<number> => {
|
|
||||||
try {
|
|
||||||
const result = await GeoCheckModel.deleteMany({
|
|
||||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
|
||||||
});
|
|
||||||
return result.deletedCount || 0;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Error deleting geo checks by team ID: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "deleteByTeamId",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
|
||||||
try {
|
|
||||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
|
||||||
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
|
|
||||||
return result.deletedCount || 0;
|
|
||||||
} catch (error: any) {
|
|
||||||
this.logger.error({
|
|
||||||
message: `Error deleting orphaned geo checks: ${error.message}`,
|
|
||||||
service: SERVICE_NAME,
|
|
||||||
method: "deleteByMonitorIdsNotIn",
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export { MongoGeoChecksRepository };
|
|
||||||
export default MongoGeoChecksRepository;
|
|
||||||
@@ -33,6 +33,3 @@ export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeams
|
|||||||
|
|
||||||
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
|
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
|
||||||
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
|
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
|
||||||
|
|
||||||
export * from "@/repositories/geo-checks/IGeoChecksRepository.js";
|
|
||||||
export { default as MongoGeoChecksRepository } from "@/repositories/geo-checks/MongoGeoChecksRepository.js";
|
|
||||||
@@ -36,7 +36,7 @@ class MongoInvitesRepository implements IInvitesRepository {
|
|||||||
create = async (invite: Partial<Invite>) => {
|
create = async (invite: Partial<Invite>) => {
|
||||||
await InviteModel.deleteMany({ email: invite.email });
|
await InviteModel.deleteMany({ email: invite.email });
|
||||||
invite.token = crypto.randomBytes(32).toString("hex");
|
invite.token = crypto.randomBytes(32).toString("hex");
|
||||||
const inviteToken = await InviteModel.create(invite);
|
let inviteToken = await InviteModel.create(invite);
|
||||||
return this.toEntity(inviteToken);
|
return this.toEntity(inviteToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository {
|
|||||||
// fetch
|
// fetch
|
||||||
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
||||||
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
|
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
|
||||||
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>;
|
findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
|
||||||
|
|
||||||
// update
|
// update
|
||||||
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
|
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
|
||||||
// delete
|
// delete
|
||||||
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
|
||||||
// other
|
// other
|
||||||
countByTeamId(teamId: string, active?: boolean): Promise<number>;
|
countByTeamId(teamId: string, active: boolean): Promise<number>;
|
||||||
}
|
}
|
||||||
@@ -75,11 +75,11 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
|||||||
|
|
||||||
findByTeamId = async (
|
findByTeamId = async (
|
||||||
teamId: string,
|
teamId: string,
|
||||||
|
active: boolean,
|
||||||
page: number,
|
page: number,
|
||||||
rowsPerPage: number,
|
rowsPerPage: number,
|
||||||
field?: string,
|
field: string,
|
||||||
order?: string,
|
order: string
|
||||||
active?: boolean
|
|
||||||
): Promise<MaintenanceWindow[]> => {
|
): Promise<MaintenanceWindow[]> => {
|
||||||
const maintenanceQuery: Record<string, any> = { teamId };
|
const maintenanceQuery: Record<string, any> = { teamId };
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
const sort: Record<string, any> = {};
|
let sort: Record<string, any> = {};
|
||||||
if (field !== undefined && order !== undefined) {
|
if (field !== undefined && order !== undefined) {
|
||||||
sort[field] = order === "asc" ? 1 : -1;
|
sort[field] = order === "asc" ? 1 : -1;
|
||||||
}
|
}
|
||||||
@@ -126,7 +126,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
|
|||||||
}
|
}
|
||||||
return this.toEntity(deleted);
|
return this.toEntity(deleted);
|
||||||
};
|
};
|
||||||
countByTeamId = async (teamId: string, active?: boolean) => {
|
countByTeamId = async (teamId: string, active: boolean) => {
|
||||||
const maintenanceQuery: Record<string, any> = { teamId };
|
const maintenanceQuery: Record<string, any> = { teamId };
|
||||||
|
|
||||||
if (active !== undefined) maintenanceQuery.active = active;
|
if (active !== undefined) maintenanceQuery.active = active;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface SummaryConfig {
|
|||||||
export interface IMonitorsRepository {
|
export interface IMonitorsRepository {
|
||||||
// create
|
// create
|
||||||
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
|
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
|
||||||
createMonitors(monitors: Monitor[]): Promise<Monitor[]>;
|
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
|
||||||
// single fetch
|
// single fetch
|
||||||
findById(monitorId: string, teamId: string): Promise<Monitor>;
|
findById(monitorId: string, teamId: string): Promise<Monitor>;
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
return this.toEntity(saved);
|
return this.toEntity(saved);
|
||||||
};
|
};
|
||||||
|
|
||||||
createMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
|
createBulkMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
|
||||||
if (!monitors.length) {
|
if (!monitors.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
|
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
|
||||||
try {
|
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
||||||
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
return this.mapDocuments(inserted);
|
||||||
return this.mapDocuments(inserted);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") {
|
|
||||||
const insertedDocs = error.insertedDocs || [];
|
|
||||||
if (insertedDocs.length > 0) {
|
|
||||||
return this.mapDocuments(insertedDocs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
|
findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
|
||||||
@@ -352,12 +342,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
tempAlertCounter: doc.tempAlertCounter,
|
tempAlertCounter: doc.tempAlertCounter,
|
||||||
selectedDisks: doc.selectedDisks ?? [],
|
selectedDisks: doc.selectedDisks ?? [],
|
||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
|
||||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
|
||||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
};
|
};
|
||||||
@@ -411,12 +397,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
tempAlertCounter: doc.tempAlertCounter,
|
tempAlertCounter: doc.tempAlertCounter,
|
||||||
selectedDisks: doc.selectedDisks ?? [],
|
selectedDisks: doc.selectedDisks ?? [],
|
||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
|
||||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
|
||||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { NotificationModel, type NotificationDocument } from "@/db/models/index.
|
|||||||
import { INotificationsRepository } from "@/repositories/index.js";
|
import { INotificationsRepository } from "@/repositories/index.js";
|
||||||
import type { Notification } from "@/types/index.js";
|
import type { Notification } from "@/types/index.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { not } from "joi";
|
||||||
|
|
||||||
class MongoNotificationsRepository implements INotificationsRepository {
|
class MongoNotificationsRepository implements INotificationsRepository {
|
||||||
private mapDocuments = (documents: NotificationDocument[]): Notification[] => {
|
private mapDocuments = (documents: NotificationDocument[]): Notification[] => {
|
||||||
|
|||||||
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user