99 files changed
+2293
-14644
No files matched your search
@@ -0,0 +1,77 @@
|
||||
name: Distribution deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-client:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Client Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:frontend-dist \
|
||||
-f ./docker/dist/client.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Client Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:frontend-dist
|
||||
|
||||
docker-build-and-push-server:
|
||||
needs: docker-build-and-push-client
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Server Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:backend-dist \
|
||||
-f ./docker/dist/server.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Server Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist
|
||||
|
||||
- name: Build Mongo Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:mongo-dist \
|
||||
-f ./docker/dist/mongoDB.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push MongoDB Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:mongo-dist
|
||||
|
||||
- name: Build Redis Docker image
|
||||
run: |
|
||||
docker build \
|
||||
-t ghcr.io/bluewave-labs/checkmate:redis-dist \
|
||||
-f ./docker/dist/redis.Dockerfile \
|
||||
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
|
||||
.
|
||||
|
||||
- name: Push Redis Docker image
|
||||
run: docker push ghcr.io/bluewave-labs/checkmate:redis-dist
|
||||
@@ -13,9 +13,6 @@ on:
|
||||
required: false
|
||||
default: "key_value_json"
|
||||
|
||||
# For automatic execution at a specific time (every day at midnight)
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -87,7 +87,9 @@ jobs:
|
||||
username: ${{ secrets.DEMO_SERVER_USER }}
|
||||
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd checkmate/docker/prod
|
||||
cd checkmate
|
||||
git pull
|
||||
cd docker/prod
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
@@ -87,7 +87,9 @@ jobs:
|
||||
username: ${{ secrets.STAGING_SERVER_USER }}
|
||||
key: ${{ secrets.STAGING_SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd checkmate/docker/staging
|
||||
cd checkmate
|
||||
git pull
|
||||
cd docker/staging
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
+7
-8
@@ -48,24 +48,23 @@ Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) —
|
||||
|
||||
### Set up Checkmate locally?
|
||||
|
||||
Frontend:
|
||||
Frontend & backend:
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
Backend:
|
||||
Clone and follow instructions in [checkmate-backend](https://github.com/bluewave-labs/checkmate-backend).
|
||||
|
||||
By default, the frontend expects the backend on `http://localhost:3001`. Update configs if needed.
|
||||
|
||||
### Start contributing code?
|
||||
|
||||
1. Pick or open an issue (check `good-first-issue`s first)
|
||||
2. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well.
|
||||
3. Create a branch from `develop`.
|
||||
4. Write your code.
|
||||
5. Run and test locally.
|
||||
6. Open a PR to `develop`.
|
||||
2. (optional but highly suggested) Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||
3. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well.
|
||||
4. Create a branch from `develop`.
|
||||
5. Write your code.
|
||||
6. Run and test locally.
|
||||
7. Open a PR to `develop`.
|
||||
|
||||
Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ We **love** what we are building here, and we continuously learn a few things ab
|
||||
|
||||
## 📦 Demo
|
||||
|
||||
See [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).
|
||||
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
|
||||
|
||||
@@ -69,11 +69,10 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
**Short term roadmap:**
|
||||
|
||||
- Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593
|
||||
- Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131
|
||||
- Translations (i18n) (**in progress**)
|
||||
- Better notification options (Webhooks, Discord, Telegram, Slack) (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1545
|
||||
- JSON query monitoring https://github.com/bluewave-labs/Checkmate/issues/1573
|
||||
- JSON query monitoring (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1573
|
||||
- Tagging/grouping monitors https://github.com/bluewave-labs/Checkmate/issues/1546
|
||||
- More configuration options
|
||||
- DNS monitoring
|
||||
@@ -118,10 +117,11 @@ Here's how you can contribute:
|
||||
|
||||
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.
|
||||
2. Optionally, read [project structure](https://docs.checkmate.so/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.
|
||||
4. Check for good-first-issue's if you are a newcomer.
|
||||
5. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
2. Check [project structure](https://docs.checkmate.so/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
|
||||
3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture.
|
||||
4. Open an issue if you believe you've encountered a bug.
|
||||
5. Check for good-first-issue's if you are a newcomer.
|
||||
6. Make a pull request to add new features/make quality-of-life improvements/fix bugs.
|
||||
|
||||
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||
@@ -135,8 +135,6 @@ Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their doc
|
||||
|
||||
Also check other developer and contributor-friendly projects of BlueWave:
|
||||
|
||||
- [LangRoute](https://github.com/bluewave-labs/langroute), an LLM proxy and gateway
|
||||
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom.
|
||||
- [Headcount](https://github.com/bluewave-labs/bluewave-hrm), a complete Human Resource Management platform.
|
||||
- [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.
|
||||
- [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.
|
||||
@@ -1,2 +1,3 @@
|
||||
VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL
|
||||
VITE_STATUS_PAGE_SUBDOMAIN_PREFIX=UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX
|
||||
VITE_APP_CLIENT_HOST=UPTIME_APP_CLIENT_HOST
|
||||
VITE_APP_LOG_LEVEL=UPTIME_APP_LOG_LEVEL
|
||||
Generated
+71
-13627
File diff suppressed because it is too large.
Load diff
+8
-6
@@ -22,12 +22,6 @@
|
||||
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
|
||||
"@mui/material": "6.4.11",
|
||||
"@reduxjs/toolkit": "2.7.0",
|
||||
"@solana/wallet-adapter-base": "0.9.25",
|
||||
"@solana/wallet-adapter-material-ui": "0.16.35",
|
||||
"@solana/wallet-adapter-react": "0.15.37",
|
||||
"@solana/wallet-adapter-react-ui": "0.9.37",
|
||||
"@solana/wallet-adapter-wallets": "0.19.34",
|
||||
"@solana/web3.js": "1.98.0",
|
||||
"axios": "^1.7.4",
|
||||
"dayjs": "1.11.13",
|
||||
"flag-icons": "7.3.2",
|
||||
@@ -52,6 +46,14 @@
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
"unusedDepencies": {
|
||||
"@solana/wallet-adapter-base": "0.9.25",
|
||||
"@solana/wallet-adapter-material-ui": "0.16.35",
|
||||
"@solana/wallet-adapter-react": "0.15.37",
|
||||
"@solana/wallet-adapter-react-ui": "0.9.37",
|
||||
"@solana/wallet-adapter-wallets": "0.19.34",
|
||||
"@solana/web3.js": "1.98.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
|
||||
http,HTTP Monitor,https://www.google.com,60000,,,,
|
||||
http,API Monitor,http://reqres.in/api/users/2,120000,,Janet,data.first_name,equal
|
||||
ping,Ping monitor,127.0.0.1,60000,,,,
|
||||
docker,Docker monitor,bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01,60000,,,,
|
||||
port,Port monitor,127.0.0.1,60000,5000,,,
|
||||
|
@@ -0,0 +1 @@
|
||||
type,name,url,interval,port,expectedValue,jsonPath,matchMethod
|
||||
|
@@ -1,43 +1,18 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { ThemeProvider } from "@emotion/react";
|
||||
import lightTheme from "./Utils/Theme/lightTheme";
|
||||
import darkTheme from "./Utils/Theme/darkTheme";
|
||||
import { CssBaseline, GlobalStyles } from "@mui/material";
|
||||
import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
import { Routes } from "./Routes";
|
||||
import WalletProvider from "./Components/WalletProvider";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { setLanguage } from "./Features/UI/uiSlice";
|
||||
|
||||
function App() {
|
||||
const mode = useSelector((state) => state.ui.mode);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
dispatch(getAppSettings({ authToken })).then((action) => {
|
||||
if (action.payload && action.payload.success) {
|
||||
const { language } = action.payload.data;
|
||||
const availableLanguages = Object.keys(i18n.options.resources || {});
|
||||
if (language && availableLanguages.includes(language)) {
|
||||
dispatch(setLanguage(language));
|
||||
i18n.changeLanguage(language);
|
||||
} else {
|
||||
dispatch(setLanguage(availableLanguages[0]));
|
||||
i18n.changeLanguage(availableLanguages[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [dispatch, authToken, i18n]);
|
||||
|
||||
// Cleanup
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { Box, Button, Stack, Typography, Link } from "@mui/material";
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
|
||||
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
@@ -35,9 +36,8 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, show
|
||||
<>
|
||||
{t("pageSpeedWarning")} {" "}
|
||||
<Link
|
||||
href="https://docs.checkmate.so/users-guide/quickstart#env-vars-server"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
component={RouterLink}
|
||||
to="/settings"
|
||||
sx={{
|
||||
textDecoration: "underline",
|
||||
color: "inherit",
|
||||
@@ -118,6 +118,17 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false, show
|
||||
>
|
||||
Let's create your first {title}
|
||||
</Button>
|
||||
{/* Bulk create of uptime monitors */}
|
||||
{title === "uptime monitor" && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ alignSelf: "center" }}
|
||||
onClick={() => navigate("/uptime/bulk-import")}
|
||||
>
|
||||
{t("bulkImport.fallbackPage")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Warning box for PageSpeed monitor */}
|
||||
{(title === "pagespeed monitor" && showPageSpeedWarning) && (
|
||||
|
||||
@@ -6,18 +6,6 @@ import PropTypes from "prop-types";
|
||||
const getSx = (theme, type, maxWidth) => {
|
||||
const sx = {
|
||||
maxWidth: maxWidth,
|
||||
"& .MuiOutlinedInput-root ": {
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.contrastText, // Adjust hover border color
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.accent.main, // Adjust focus border color
|
||||
},
|
||||
"&.Mui-disabled .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: theme.palette.primary.contrastText, // CAIO_REVIEW
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
|
||||
"& .MuiFormHelperText-root": {
|
||||
position: "absolute",
|
||||
@@ -116,6 +104,7 @@ const TextInput = forwardRef(
|
||||
fontSize={"var(--env-var-font-size-medium)"}
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
fontWeight={500}
|
||||
mb={theme.spacing(2)}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <Required />}
|
||||
|
||||
@@ -1,30 +1,49 @@
|
||||
import { Stack, Button } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
const CreateMonitorHeader = ({
|
||||
isAdmin,
|
||||
label = "Create new",
|
||||
shouldRender = true,
|
||||
isLoading = true,
|
||||
path,
|
||||
bulkPath,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
if (!isAdmin) return null;
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(6)}
|
||||
>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
{bulkPath && (
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={() => {
|
||||
navigate(`${bulkPath}`);
|
||||
}}
|
||||
>
|
||||
{t("bulkImport.title")}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -33,7 +52,8 @@ export default CreateMonitorHeader;
|
||||
|
||||
CreateMonitorHeader.propTypes = {
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
shouldRender: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
path: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
bulkPath: PropTypes.string,
|
||||
};
|
||||
@@ -25,10 +25,12 @@ const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(2)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
<Typography variant="h2">
|
||||
<Typography variant="h2"
|
||||
style={{fontFamily : "monospace" , fontWeight : 'bolder'}}
|
||||
>
|
||||
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Dot />
|
||||
|
||||
@@ -55,10 +55,9 @@ const MonitorTimeFrameHeader = ({
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
mb={theme.spacing(8)}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
Showing statistics for past{" "}
|
||||
|
||||
+385
-319
@@ -1,16 +1,17 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useEffect, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import TabPanel from "./TabPanel";
|
||||
@@ -19,335 +20,400 @@ import useNotifications from "../Hooks/useNotification";
|
||||
|
||||
// Define constants for notification types to avoid magic values
|
||||
const NOTIFICATION_TYPES = {
|
||||
SLACK: 'slack',
|
||||
DISCORD: 'discord',
|
||||
TELEGRAM: 'telegram',
|
||||
WEBHOOK: 'webhook'
|
||||
SLACK: "slack",
|
||||
DISCORD: "discord",
|
||||
TELEGRAM: "telegram",
|
||||
WEBHOOK: "webhook",
|
||||
};
|
||||
|
||||
// Define constants for field IDs
|
||||
const FIELD_IDS = {
|
||||
WEBHOOK: 'webhook',
|
||||
TOKEN: 'token',
|
||||
CHAT_ID: 'chatId',
|
||||
URL: 'url'
|
||||
WEBHOOK: "webhook",
|
||||
TOKEN: "token",
|
||||
CHAT_ID: "chatId",
|
||||
URL: "url",
|
||||
};
|
||||
|
||||
const NotificationIntegrationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
monitor,
|
||||
setMonitor,
|
||||
// Optional prop to configure available notification types
|
||||
notificationTypes = null
|
||||
const NotificationIntegrationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
monitor,
|
||||
setMonitor,
|
||||
// Optional prop to configure available notification types
|
||||
notificationTypes = null,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
const [loading, _, sendTestNotification] = useNotifications();
|
||||
|
||||
// Helper to get the field state key with error handling
|
||||
const getFieldKey = (typeId, fieldId) => {
|
||||
if (typeof typeId !== 'string' || typeId === '') {
|
||||
throw new Error(t('errorInvalidTypeId'));
|
||||
}
|
||||
|
||||
if (typeof fieldId !== 'string' || fieldId === '') {
|
||||
throw new Error(t('errorInvalidFieldId'));
|
||||
}
|
||||
|
||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
||||
};
|
||||
|
||||
// Define notification types
|
||||
const DEFAULT_NOTIFICATION_TYPES = [
|
||||
{
|
||||
id: NOTIFICATION_TYPES.SLACK,
|
||||
label: t('notifications.slack.label'),
|
||||
description: t('notifications.slack.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t('notifications.slack.webhookLabel'),
|
||||
placeholder: t('notifications.slack.webhookPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.DISCORD,
|
||||
label: t('notifications.discord.label'),
|
||||
description: t('notifications.discord.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t('notifications.discord.webhookLabel'),
|
||||
placeholder: t('notifications.discord.webhookPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.TELEGRAM,
|
||||
label: t('notifications.telegram.label'),
|
||||
description: t('notifications.telegram.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.TOKEN,
|
||||
label: t('notifications.telegram.tokenLabel'),
|
||||
placeholder: t('notifications.telegram.tokenPlaceholder'),
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: FIELD_IDS.CHAT_ID,
|
||||
label: t('notifications.telegram.chatIdLabel'),
|
||||
placeholder: t('notifications.telegram.chatIdPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.WEBHOOK,
|
||||
label: t('notifications.webhook.label'),
|
||||
description: t('notifications.webhook.description'),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.URL,
|
||||
label: t('notifications.webhook.urlLabel'),
|
||||
placeholder: t('notifications.webhook.urlPlaceholder'),
|
||||
type: 'text'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
// Use provided notification types or default to our translated ones
|
||||
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
|
||||
|
||||
// Memoized function to initialize integrations state
|
||||
const initialIntegrationsState = useMemo(() => {
|
||||
const state = {};
|
||||
|
||||
activeNotificationTypes.forEach(type => {
|
||||
// Add enabled flag for each notification type
|
||||
state[type.id] = monitor?.notifications?.some(n => n.type === type.id) || false;
|
||||
|
||||
// Add state for each field in the notification type
|
||||
type.fields.forEach(field => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
state[fieldKey] = monitor?.notifications?.find(n => n.type === type.id)?.[field.id] || "";
|
||||
});
|
||||
});
|
||||
|
||||
return state;
|
||||
}, [monitor, activeNotificationTypes]); // Only recompute when these dependencies change
|
||||
|
||||
const [integrations, setIntegrations] = useState(initialIntegrationsState);
|
||||
const [loading, _, sendTestNotification] = useNotifications();
|
||||
|
||||
const handleChangeTab = (event, newValue) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
// Helper to get the field state key with error handling
|
||||
const getFieldKey = useCallback(
|
||||
(typeId, fieldId) => {
|
||||
if (typeof typeId !== "string" || typeId === "") {
|
||||
throw new Error(t("errorInvalidTypeId"));
|
||||
}
|
||||
|
||||
const handleIntegrationChange = (type, checked) => {
|
||||
setIntegrations(prev => ({
|
||||
...prev,
|
||||
[type]: checked
|
||||
}));
|
||||
};
|
||||
if (typeof fieldId !== "string" || fieldId === "") {
|
||||
throw new Error(t("errorInvalidFieldId"));
|
||||
}
|
||||
|
||||
const handleInputChange = (type, value) => {
|
||||
setIntegrations(prev => ({
|
||||
...prev,
|
||||
[type]: value
|
||||
}));
|
||||
};
|
||||
return `${typeId}${fieldId.charAt(0).toUpperCase() + fieldId.slice(1)}`;
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleTestNotification = async (type) => {
|
||||
// Get the notification type details
|
||||
const notificationType = activeNotificationTypes.find(t => t.id === type);
|
||||
|
||||
if (typeof notificationType === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare config object based on notification type
|
||||
const config = {};
|
||||
|
||||
// Add each field value to the config object
|
||||
notificationType.fields.forEach(field => {
|
||||
const fieldKey = getFieldKey(type, field.id);
|
||||
config[field.id] = integrations[fieldKey];
|
||||
});
|
||||
|
||||
await sendTestNotification(type, config);
|
||||
};
|
||||
|
||||
// In NotificationIntegrationModal.jsx, update the handleSave function:
|
||||
// Define notification types
|
||||
const DEFAULT_NOTIFICATION_TYPES = [
|
||||
{
|
||||
id: NOTIFICATION_TYPES.SLACK,
|
||||
label: t("notifications.slack.label"),
|
||||
description: t("notifications.slack.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t("notifications.slack.webhookLabel"),
|
||||
placeholder: t("notifications.slack.webhookPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.DISCORD,
|
||||
label: t("notifications.discord.label"),
|
||||
description: t("notifications.discord.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.WEBHOOK,
|
||||
label: t("notifications.discord.webhookLabel"),
|
||||
placeholder: t("notifications.discord.webhookPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.TELEGRAM,
|
||||
label: t("notifications.telegram.label"),
|
||||
description: t("notifications.telegram.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.TOKEN,
|
||||
label: t("notifications.telegram.tokenLabel"),
|
||||
placeholder: t("notifications.telegram.tokenPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
id: FIELD_IDS.CHAT_ID,
|
||||
label: t("notifications.telegram.chatIdLabel"),
|
||||
placeholder: t("notifications.telegram.chatIdPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: NOTIFICATION_TYPES.WEBHOOK,
|
||||
label: t("notifications.webhook.label"),
|
||||
description: t("notifications.webhook.description"),
|
||||
fields: [
|
||||
{
|
||||
id: FIELD_IDS.URL,
|
||||
label: t("notifications.webhook.urlLabel"),
|
||||
placeholder: t("notifications.webhook.urlPlaceholder"),
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const handleSave = () => {
|
||||
// Get existing notifications
|
||||
const notifications = [...(monitor?.notifications || [])];
|
||||
|
||||
// Get all notification types IDs
|
||||
const existingTypes = activeNotificationTypes.map(type => type.id);
|
||||
|
||||
// Filter out notifications that are configurable in this modal
|
||||
const filteredNotifications = notifications.filter(
|
||||
notification => {
|
||||
|
||||
if (notification.platform) {
|
||||
return !existingTypes.includes(notification.platform);
|
||||
}
|
||||
|
||||
return !existingTypes.includes(notification.type);
|
||||
}
|
||||
);
|
||||
// Use provided notification types or default to our translated ones
|
||||
const activeNotificationTypes = notificationTypes || DEFAULT_NOTIFICATION_TYPES;
|
||||
|
||||
// Add each enabled notification with its configured fields
|
||||
activeNotificationTypes.forEach(type => {
|
||||
if (integrations[type.id]) {
|
||||
// Memoized function to initialize integrations state
|
||||
const initialIntegrationsState = useMemo(() => {
|
||||
const state = {};
|
||||
|
||||
let notificationObject = {
|
||||
type: "webhook",
|
||||
platform: type.id, // Set platform to identify the specific service
|
||||
config: {}
|
||||
};
|
||||
|
||||
// Configure based on notification type
|
||||
switch(type.id) {
|
||||
case "slack":
|
||||
case "discord":
|
||||
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'webhook')];
|
||||
break;
|
||||
case "telegram":
|
||||
notificationObject.config.botToken = integrations[getFieldKey(type.id, 'token')];
|
||||
notificationObject.config.chatId = integrations[getFieldKey(type.id, 'chatId')];
|
||||
break;
|
||||
case "webhook":
|
||||
notificationObject.config.webhookUrl = integrations[getFieldKey(type.id, 'url')];
|
||||
break;
|
||||
}
|
||||
|
||||
filteredNotifications.push(notificationObject);
|
||||
}
|
||||
});
|
||||
activeNotificationTypes.forEach((type) => {
|
||||
// Add enabled flag for each notification type
|
||||
state[type.id] = false;
|
||||
|
||||
// Update monitor with new notifications
|
||||
setMonitor(prev => ({
|
||||
...prev,
|
||||
notifications: filteredNotifications
|
||||
}));
|
||||
|
||||
onClose();
|
||||
};
|
||||
// Add state for each field in the notification type
|
||||
type.fields.forEach((field) => {
|
||||
const fieldKey = getFieldKey(type.id, field.id);
|
||||
state[fieldKey] = "";
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
sx={{
|
||||
'& .MuiDialog-paper': {
|
||||
width: `calc(80% - ${theme.spacing(40)})`,
|
||||
maxWidth: `${theme.breakpoints.values.md - 70}px`
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
height: `calc(26vh - ${theme.spacing(20)})`
|
||||
}}>
|
||||
{/* Left sidebar with tabs */}
|
||||
<Box sx={{
|
||||
borderRight: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
width: '30%',
|
||||
maxWidth: theme.spacing(120),
|
||||
pr: theme.spacing(10)
|
||||
}}>
|
||||
<Typography variant="subtitle1" sx={{
|
||||
my: theme.spacing(1),
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.typography.fontSize * 0.9,
|
||||
color: theme.palette.primary.contrastTextSecondary,
|
||||
pl: theme.spacing(4)
|
||||
}}>
|
||||
{t('notifications.addOrEditNotifications')}
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
variant="scrollable"
|
||||
value={tabValue}
|
||||
onChange={handleChangeTab}
|
||||
aria-label="Notification tabs"
|
||||
>
|
||||
{activeNotificationTypes.map((type) => (
|
||||
<Tab
|
||||
key={type.id}
|
||||
label={type.label}
|
||||
orientation="vertical"
|
||||
disableRipple
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
return state;
|
||||
}, [activeNotificationTypes, getFieldKey]); // Only recompute when these dependencies change
|
||||
|
||||
{/* Right side content */}
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
pl: theme.spacing(7.5),
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{activeNotificationTypes.map((type, index) => (
|
||||
<TabPanel key={type.id} value={tabValue} index={index}>
|
||||
<TabComponent
|
||||
type={type}
|
||||
integrations={integrations}
|
||||
handleIntegrationChange={handleIntegrationChange}
|
||||
handleInputChange={handleInputChange}
|
||||
handleTestNotification={handleTestNotification}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{
|
||||
p: theme.spacing(4),
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
mb: theme.spacing(5),
|
||||
mr: theme.spacing(5)
|
||||
}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
sx={{
|
||||
width: 'auto',
|
||||
minWidth: theme.spacing(60),
|
||||
px: theme.spacing(8)
|
||||
}}
|
||||
>
|
||||
{t('commonSave')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
const [integrations, setIntegrations] = useState(initialIntegrationsState);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const extractNotificationValues = () => {
|
||||
const values = {};
|
||||
|
||||
if (!monitor?.notifications || !Array.isArray(monitor.notifications)) {
|
||||
return values;
|
||||
}
|
||||
|
||||
monitor.notifications.forEach((notification) => {
|
||||
// Handle notification based on its structure
|
||||
if (notification.type === "webhook" && notification.platform) {
|
||||
if (typeof notification.config === "undefined") return;
|
||||
const platform = notification.platform;
|
||||
values[platform] = true; // Set platform as enabled
|
||||
|
||||
// Extract configuration based on platform
|
||||
switch (platform) {
|
||||
case NOTIFICATION_TYPES.SLACK:
|
||||
case NOTIFICATION_TYPES.DISCORD:
|
||||
if (notification.config.webhookUrl) {
|
||||
values[getFieldKey(platform, FIELD_IDS.WEBHOOK)] =
|
||||
notification.config.webhookUrl;
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPES.TELEGRAM:
|
||||
if (notification.config.botToken) {
|
||||
values[getFieldKey(platform, FIELD_IDS.TOKEN)] =
|
||||
notification.config.botToken;
|
||||
}
|
||||
if (notification.config.chatId) {
|
||||
values[getFieldKey(platform, FIELD_IDS.CHAT_ID)] =
|
||||
notification.config.chatId;
|
||||
}
|
||||
break;
|
||||
case NOTIFICATION_TYPES.WEBHOOK:
|
||||
if (notification.config.webhookUrl) {
|
||||
values[getFieldKey(platform, FIELD_IDS.URL)] =
|
||||
notification.config.webhookUrl;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
const extractedValues = extractNotificationValues();
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
...extractedValues,
|
||||
}));
|
||||
}
|
||||
}, [open, monitor, getFieldKey]);
|
||||
|
||||
const handleChangeTab = (event, newValue) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
const handleIntegrationChange = (type, checked) => {
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
[type]: checked,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleInputChange = (type, value) => {
|
||||
setIntegrations((prev) => ({
|
||||
...prev,
|
||||
[type]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleTestNotification = async (type) => {
|
||||
// Get the notification type details
|
||||
const notificationType = activeNotificationTypes.find((t) => t.id === type);
|
||||
|
||||
if (typeof notificationType === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare config object based on notification type
|
||||
const config = {};
|
||||
|
||||
// Add each field value to the config object
|
||||
notificationType.fields.forEach((field) => {
|
||||
const fieldKey = getFieldKey(type, field.id);
|
||||
config[field.id] = integrations[fieldKey];
|
||||
});
|
||||
|
||||
await sendTestNotification(type, config);
|
||||
};
|
||||
|
||||
// In NotificationIntegrationModal.jsx, update the handleSave function:
|
||||
|
||||
const handleSave = () => {
|
||||
// Get existing notifications
|
||||
const notifications = [...(monitor?.notifications || [])];
|
||||
|
||||
// Get all notification types IDs
|
||||
const existingTypes = activeNotificationTypes.map((type) => type.id);
|
||||
|
||||
// Filter out notifications that are configurable in this modal
|
||||
const filteredNotifications = notifications.filter((notification) => {
|
||||
if (notification.platform) {
|
||||
return !existingTypes.includes(notification.platform);
|
||||
}
|
||||
|
||||
return !existingTypes.includes(notification.type);
|
||||
});
|
||||
|
||||
// Add each enabled notification with its configured fields
|
||||
activeNotificationTypes.forEach((type) => {
|
||||
if (integrations[type.id]) {
|
||||
let notificationObject = {
|
||||
type: "webhook",
|
||||
platform: type.id, // Set platform to identify the specific service
|
||||
config: {},
|
||||
};
|
||||
|
||||
// Configure based on notification type
|
||||
switch (type.id) {
|
||||
case "slack":
|
||||
case "discord":
|
||||
notificationObject.config.webhookUrl =
|
||||
integrations[getFieldKey(type.id, "webhook")];
|
||||
break;
|
||||
case "telegram":
|
||||
notificationObject.config.botToken =
|
||||
integrations[getFieldKey(type.id, "token")];
|
||||
notificationObject.config.chatId =
|
||||
integrations[getFieldKey(type.id, "chatId")];
|
||||
break;
|
||||
case "webhook":
|
||||
notificationObject.config.webhookUrl =
|
||||
integrations[getFieldKey(type.id, "url")];
|
||||
break;
|
||||
}
|
||||
|
||||
filteredNotifications.push(notificationObject);
|
||||
}
|
||||
});
|
||||
|
||||
// Update monitor with new notifications
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
notifications: filteredNotifications,
|
||||
}));
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
sx={{
|
||||
"& .MuiDialog-paper": {
|
||||
width: `calc(80% - ${theme.spacing(40)})`,
|
||||
maxWidth: `${theme.breakpoints.values.md - 70}px`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
height: `calc(30vh - ${theme.spacing(20)})`,
|
||||
}}
|
||||
>
|
||||
{/* Left sidebar with tabs */}
|
||||
<Box
|
||||
sx={{
|
||||
borderRight: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
width: "30%",
|
||||
maxWidth: theme.spacing(120),
|
||||
pr: theme.spacing(10),
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2">
|
||||
{t("notifications.addOrEditNotifications")}
|
||||
</Typography>
|
||||
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
variant="scrollable"
|
||||
value={tabValue}
|
||||
onChange={handleChangeTab}
|
||||
aria-label="Notification tabs"
|
||||
>
|
||||
{activeNotificationTypes.map((type) => (
|
||||
<Tab
|
||||
key={type.id}
|
||||
label={type.label}
|
||||
orientation="vertical"
|
||||
disableRipple
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Right side content */}
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
pl: theme.spacing(7.5),
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{activeNotificationTypes.map((type, index) => (
|
||||
<TabPanel
|
||||
key={type.id}
|
||||
value={tabValue}
|
||||
index={index}
|
||||
>
|
||||
<TabComponent
|
||||
type={type}
|
||||
integrations={integrations}
|
||||
handleIntegrationChange={handleIntegrationChange}
|
||||
handleInputChange={handleInputChange}
|
||||
handleTestNotification={handleTestNotification}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</TabPanel>
|
||||
))}
|
||||
</Box>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: theme.spacing(4),
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
mb: theme.spacing(5),
|
||||
mr: theme.spacing(5),
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSave}
|
||||
loading={loading}
|
||||
sx={{
|
||||
width: "auto",
|
||||
minWidth: theme.spacing(60),
|
||||
px: theme.spacing(8),
|
||||
}}
|
||||
>
|
||||
{t("commonSave")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationIntegrationModal.propTypes = {
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
setMonitor: PropTypes.func.isRequired,
|
||||
notificationTypes: PropTypes.array
|
||||
open: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
setMonitor: PropTypes.func.isRequired,
|
||||
notificationTypes: PropTypes.array,
|
||||
};
|
||||
|
||||
export default NotificationIntegrationModal;
|
||||
export default NotificationIntegrationModal;
|
||||
@@ -20,8 +20,8 @@ const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
|
||||
direction="row"
|
||||
flexWrap={flexWrap}
|
||||
gap={theme.spacing(8)}
|
||||
justifyContent="space-between"
|
||||
display="flex"
|
||||
justifyContent="flex-start"
|
||||
display="flex"
|
||||
>
|
||||
{children}
|
||||
</Stack>
|
||||
@@ -30,6 +30,7 @@ const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
|
||||
|
||||
StatusBoxes.propTypes = {
|
||||
shouldRender: PropTypes.bool,
|
||||
flexWrap: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { update } from "../../../Features/Auth/authSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { getTouchedFieldErrors } from "../../../Validation/error";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const defaultPasswordsState = {
|
||||
password: "",
|
||||
@@ -26,6 +27,7 @@ const defaultPasswordsState = {
|
||||
const PasswordPanel = () => {
|
||||
const theme = useTheme();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const SPACING_GAP = theme.spacing(12);
|
||||
|
||||
@@ -203,7 +205,7 @@ const PasswordPanel = () => {
|
||||
<TextInput
|
||||
type="password"
|
||||
id="edit-confirm-password"
|
||||
placeholder="Reenter your new password"
|
||||
placeholder={t("confirmPassword")}
|
||||
autoComplete="new-password"
|
||||
value={localData.confirm}
|
||||
onChange={handleChange}
|
||||
|
||||
@@ -344,7 +344,7 @@ const ProfilePanel = () => {
|
||||
spellCheck="false"
|
||||
>
|
||||
<Box mb={theme.spacing(6)}>
|
||||
<Typography component="h1">{t('DeleteAccount')}</Typography>
|
||||
<Typography component="h1">{t('DeleteAccountTitle')}</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
sx={{ opacity: 0.6 }}
|
||||
@@ -357,7 +357,7 @@ const ProfilePanel = () => {
|
||||
color="error"
|
||||
onClick={() => setIsOpen("delete")}
|
||||
>
|
||||
{t('DeleteAccount')}
|
||||
{t('DeleteAccountButton')}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
@@ -367,7 +367,7 @@ const ProfilePanel = () => {
|
||||
title={t('DeleteWarningTitle')}
|
||||
description={t('DeleteAccountWarning')}
|
||||
onCancel={() => setIsOpen("")}
|
||||
confirmationButtonLabel={t('DeleteAccount')}
|
||||
confirmationButtonLabel={t('DeleteAccountButton')}
|
||||
onConfirm={handleDeleteAccount}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
@@ -25,7 +25,7 @@ const TeamPanel = () => {
|
||||
|
||||
const [toInvite, setToInvite] = useState({
|
||||
email: "",
|
||||
role: ["0"],
|
||||
role: ["user"],
|
||||
});
|
||||
const [data, setData] = useState([]);
|
||||
const [members, setMembers] = useState([]);
|
||||
@@ -107,7 +107,7 @@ const TeamPanel = () => {
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value } = event.target;
|
||||
const newEmail = value?.toLowerCase() || value
|
||||
const newEmail = value?.toLowerCase() || value;
|
||||
setToInvite((prev) => ({
|
||||
...prev,
|
||||
email: newEmail,
|
||||
|
||||
@@ -1,46 +1,50 @@
|
||||
import { useMemo } from "react";
|
||||
import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
|
||||
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
|
||||
import {
|
||||
UnsafeBurnerWalletAdapter,
|
||||
PhantomWalletAdapter,
|
||||
} from "@solana/wallet-adapter-wallets";
|
||||
// import { useMemo } from "react";
|
||||
// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react";
|
||||
// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
|
||||
// import {
|
||||
// UnsafeBurnerWalletAdapter,
|
||||
// PhantomWalletAdapter,
|
||||
// } from "@solana/wallet-adapter-wallets";
|
||||
|
||||
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
|
||||
import { clusterApiUrl } from "@solana/web3.js";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
|
||||
// import { clusterApiUrl } from "@solana/web3.js";
|
||||
// import PropTypes from "prop-types";
|
||||
// import "./index.css";
|
||||
|
||||
// Default styles that can be overridden by your app
|
||||
import "@solana/wallet-adapter-react-ui/styles.css";
|
||||
// // Default styles that can be overridden by your app
|
||||
// import "@solana/wallet-adapter-react-ui/styles.css";
|
||||
|
||||
export const Wallet = ({ children }) => {
|
||||
// The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
|
||||
const network = WalletAdapterNetwork.Mainnet;
|
||||
// export const Wallet = ({ children }) => {
|
||||
// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'.
|
||||
// const network = WalletAdapterNetwork.Mainnet;
|
||||
|
||||
// You can also provide a custom RPC endpoint.
|
||||
const endpoint = useMemo(() => clusterApiUrl(network), [network]);
|
||||
// // You can also provide a custom RPC endpoint.
|
||||
// const endpoint = useMemo(() => clusterApiUrl(network), [network]);
|
||||
|
||||
const wallets = useMemo(
|
||||
() => [new PhantomWalletAdapter()],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[network]
|
||||
);
|
||||
// const wallets = useMemo(
|
||||
// () => [new PhantomWalletAdapter()],
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// [network]
|
||||
// );
|
||||
|
||||
return (
|
||||
<ConnectionProvider endpoint={endpoint}>
|
||||
<WalletProvider
|
||||
wallets={wallets}
|
||||
autoConnect
|
||||
>
|
||||
<WalletModalProvider>{children}</WalletModalProvider>
|
||||
</WalletProvider>
|
||||
</ConnectionProvider>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <ConnectionProvider endpoint={endpoint}>
|
||||
// <WalletProvider
|
||||
// wallets={wallets}
|
||||
// autoConnect
|
||||
// >
|
||||
// <WalletModalProvider>{children}</WalletModalProvider>
|
||||
// </WalletProvider>
|
||||
// </ConnectionProvider>
|
||||
// );
|
||||
// };
|
||||
|
||||
Wallet.propTypes = {
|
||||
children: PropTypes.node,
|
||||
// Wallet.propTypes = {
|
||||
// children: PropTypes.node,
|
||||
// };
|
||||
|
||||
const Wallet = ({ children }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
export default Wallet;
|
||||
@@ -101,7 +101,7 @@ export const updatePageSpeed = createAsyncThunk(
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
// notifications: monitor.notifications,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
|
||||
@@ -5,7 +5,8 @@ const initialState = {
|
||||
isLoading: false,
|
||||
apiBaseUrl: "",
|
||||
logLevel: "debug",
|
||||
language: "",
|
||||
language: "gb",
|
||||
pagespeedApiKey: "",
|
||||
};
|
||||
|
||||
export const getAppSettings = createAsyncThunk(
|
||||
@@ -33,6 +34,7 @@ export const updateAppSettings = createAsyncThunk(
|
||||
try {
|
||||
const parsedSettings = {
|
||||
language: settings.language,
|
||||
pagespeedApiKey: settings.pagespeedApiKey,
|
||||
};
|
||||
const res = await networkService.updateAppSettings({ settings: parsedSettings });
|
||||
return res.data;
|
||||
@@ -56,6 +58,7 @@ const handleGetSettingsFulfilled = (state, action) => {
|
||||
state.apiBaseUrl = action.payload.data.apiBaseUrl;
|
||||
state.logLevel = action.payload.data.logLevel;
|
||||
state.language = action.payload.data.language;
|
||||
state.pagespeedApiKey = action.payload.data.pagespeedApiKey;
|
||||
};
|
||||
const handleGetSettingsRejected = (state, action) => {
|
||||
state.isLoading = false;
|
||||
|
||||
@@ -81,11 +81,13 @@ export const updateUptimeMonitor = createAsyncThunk(
|
||||
notifications: monitor.notifications,
|
||||
matchMethod: monitor.matchMethod,
|
||||
expectedValue: monitor.expectedValue,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
jsonPath: monitor.jsonPath,
|
||||
...(monitor.type === "port" && { port: monitor.port }),
|
||||
};
|
||||
const res = await networkService.updateMonitor({
|
||||
monitorId: monitor._id,
|
||||
monitor,
|
||||
updatedFields,
|
||||
});
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { networkService } from "../main";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const CLIENT_HOST = import.meta.env.VITE_CLIENT_HOST;
|
||||
const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST;
|
||||
|
||||
const useGetInviteToken = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useState } from "react";
|
||||
import { networkService } from "../main";
|
||||
|
||||
export const useBulkMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const createBulkMonitors = async (file, user) => {
|
||||
setIsLoading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("csvFile", file);
|
||||
formData.append("userId", user._id);
|
||||
formData.append("teamId", user.teamId);
|
||||
|
||||
try {
|
||||
const response = await networkService.createBulkMonitors(formData);
|
||||
return [true, response.data, null]; // [success, data, error]
|
||||
} catch (err) {
|
||||
const errorMessage = err?.response?.data?.msg || err.message;
|
||||
return [false, null, errorMessage];
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return [createBulkMonitors, isLoading];
|
||||
};
|
||||
@@ -58,7 +58,7 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
helperText={errors.email ? t(errors.email) : ""}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
@@ -70,8 +70,17 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => {
|
||||
color="accent"
|
||||
type="submit"
|
||||
disabled={errors.email && true}
|
||||
className="dashboard-style-button"
|
||||
sx={{
|
||||
width: "30%",
|
||||
px: theme.spacing(6),
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
'&.MuiButtonBase-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
'&.MuiButton-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
|
||||
@@ -75,8 +75,16 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
variant="outlined"
|
||||
color="info"
|
||||
onClick={onBack}
|
||||
className="dashboard-style-button"
|
||||
sx={{
|
||||
px: theme.spacing(5),
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
'&.MuiButtonBase-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
'&.MuiButton-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
"& svg.MuiSvgIcon-root": {
|
||||
mr: theme.spacing(3),
|
||||
},
|
||||
@@ -95,13 +103,22 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
|
||||
type="submit"
|
||||
loading={authState.isLoading}
|
||||
disabled={errors.password && true}
|
||||
className="dashboard-style-button"
|
||||
sx={{
|
||||
width: "30%",
|
||||
px: theme.spacing(4),
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
'&.MuiButtonBase-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
'&.MuiButton-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
"&.Mui-focusVisible": {
|
||||
outline: `2px solid ${theme.palette.primary.main}`,
|
||||
outlineOffset: `2px`,
|
||||
boxShadow: `none`,
|
||||
},
|
||||
boxShadow: `none`,
|
||||
}}
|
||||
>
|
||||
{t("continue")}
|
||||
|
||||
@@ -9,13 +9,13 @@ import { createToast } from "../../../Utils/toastUtils";
|
||||
import { networkService } from "../../../main";
|
||||
import Background from "../../../assets/Images/background-grid.svg?react";
|
||||
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import "../index.css";
|
||||
import EmailStep from "./Components/EmailStep";
|
||||
import PasswordStep from "./Components/PasswordStep";
|
||||
import ThemeSwitch from "../../../Components/ThemeSwitch";
|
||||
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
|
||||
import LanguageSelector from "../../../Components/LanguageSelector";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DEMO = import.meta.env.VITE_APP_DEMO;
|
||||
|
||||
@@ -24,10 +24,10 @@ const DEMO = import.meta.env.VITE_APP_DEMO;
|
||||
*/
|
||||
|
||||
const Login = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const authState = useSelector((state) => state.auth);
|
||||
const { authToken } = authState;
|
||||
|
||||
@@ -43,33 +43,28 @@ const Login = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (authToken) {
|
||||
navigate("/uptime");
|
||||
return;
|
||||
}
|
||||
networkService
|
||||
.doesSuperAdminExist()
|
||||
.then((response) => {
|
||||
if (response.data.data === false) {
|
||||
navigate("/register");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(error);
|
||||
});
|
||||
}, [authToken, navigate]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
const lowerCasedValue = name === idMap["login-email-input"]? value?.toLowerCase()||value : value
|
||||
const lowerCasedValue =
|
||||
name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: lowerCasedValue,
|
||||
}));
|
||||
|
||||
const { error } = credentials.validate({ [name]: lowerCasedValue }, { abortEarly: false });
|
||||
const { error } = credentials.validate(
|
||||
{ [name]: lowerCasedValue },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const prevErrors = { ...prev };
|
||||
@@ -88,8 +83,10 @@ const Login = () => {
|
||||
{ abortEarly: false }
|
||||
);
|
||||
if (error) {
|
||||
setErrors((prev) => ({ ...prev, email: error.details[0].message }));
|
||||
createToast({ body: error.details[0].message });
|
||||
const errorMessage = error.details[0].message;
|
||||
const translatedMessage = errorMessage.startsWith('auth') ? t(errorMessage) : errorMessage;
|
||||
setErrors((prev) => ({ ...prev, email: translatedMessage }));
|
||||
createToast({ body: translatedMessage });
|
||||
} else {
|
||||
setStep(1);
|
||||
}
|
||||
@@ -106,15 +103,15 @@ const Login = () => {
|
||||
createToast({
|
||||
body:
|
||||
error.details && error.details.length > 0
|
||||
? error.details[0].message
|
||||
: "Error validating data.",
|
||||
? (error.details[0].message.startsWith('auth') ? t(error.details[0].message) : error.details[0].message)
|
||||
: t("Error validating data."),
|
||||
});
|
||||
} else {
|
||||
const action = await dispatch(login(form));
|
||||
if (action.payload.success) {
|
||||
navigate("/uptime");
|
||||
createToast({
|
||||
body: "Welcome back! You're successfully logged in.",
|
||||
body: t("welcomeBack"),
|
||||
});
|
||||
} else {
|
||||
if (action.payload) {
|
||||
@@ -230,6 +227,31 @@ const Login = () => {
|
||||
email={form.email}
|
||||
errorEmail={errors.email}
|
||||
/>
|
||||
|
||||
{/* Registration link */}
|
||||
<Box textAlign="center" >
|
||||
<Typography
|
||||
className="forgot-p"
|
||||
display="inline-block"
|
||||
color={theme.palette.primary.main}
|
||||
>
|
||||
{t("doNotHaveAccount")}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
color={theme.palette.accent.main}
|
||||
ml={theme.spacing(2)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: theme.palette.accent.darker
|
||||
}
|
||||
}}
|
||||
onClick={() => navigate("/register")}
|
||||
>
|
||||
{t("registerHere")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ import Background from "../../../assets/Images/background-grid.svg?react";
|
||||
import Logo from "../../../assets/icons/checkmate-icon.svg?react";
|
||||
import Mail from "../../../assets/icons/mail.svg?react";
|
||||
import "../index.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
/**
|
||||
* Displays the initial landing page.
|
||||
*
|
||||
@@ -67,42 +67,47 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
|
||||
</Box>
|
||||
<Box maxWidth={400}>
|
||||
<Typography className="tos-p">
|
||||
{t("authRegisterBySigningUp")}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
<Trans
|
||||
i18nKey="authRegisterBySigningUp"
|
||||
components={{
|
||||
a1: (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/terms-of-service-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
a2: (
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("authRegisterTerms")}
|
||||
</Typography>{" "}
|
||||
{t("and")}{" "}
|
||||
<Typography
|
||||
component="span"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
"https://bluewavelabs.ca/privacy-policy-open-source",
|
||||
"_blank",
|
||||
"noreferrer"
|
||||
);
|
||||
}}
|
||||
sx={{
|
||||
"&:hover": {
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("authRegisterPrivacy")}
|
||||
</Typography>
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -148,8 +153,8 @@ const Register = ({ isSuperAdmin }) => {
|
||||
try {
|
||||
const res = await networkService.verifyInvitationToken(token);
|
||||
const invite = res.data.data;
|
||||
const { role, email, teamId } = invite;
|
||||
setForm({ ...form, email, role, teamId });
|
||||
const { email } = invite;
|
||||
setForm({ ...form, email });
|
||||
} catch (error) {
|
||||
navigate("/register", { replace: true });
|
||||
}
|
||||
@@ -259,7 +264,8 @@ const Register = ({ isSuperAdmin }) => {
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const name = idMap[id];
|
||||
const lowerCasedValue = name === idMap["register-email-input"]? value?.toLowerCase() || value : value
|
||||
const lowerCasedValue =
|
||||
name === idMap["register-email-input"] ? value?.toLowerCase() || value : value;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[name]: lowerCasedValue,
|
||||
@@ -381,7 +387,7 @@ const Register = ({ isSuperAdmin }) => {
|
||||
}}
|
||||
sx={{ userSelect: "none", color: theme.palette.accent.main }}
|
||||
>
|
||||
{t("authLoginTitle")}
|
||||
{t("authRegisterLoginLink")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -64,7 +64,7 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
|
||||
>
|
||||
<TextInput
|
||||
id="register-firstname-input"
|
||||
label="Name"
|
||||
label={t("authRegisterFirstName")}
|
||||
isRequired={true}
|
||||
placeholder="Jordan"
|
||||
autoComplete="given-name"
|
||||
@@ -76,7 +76,7 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
|
||||
/>
|
||||
<TextInput
|
||||
id="register-lastname-input"
|
||||
label="Surname"
|
||||
label={t("authRegisterLastName")}
|
||||
isRequired={true}
|
||||
placeholder="Ellis"
|
||||
autoComplete="family-name"
|
||||
|
||||
@@ -64,7 +64,7 @@ function StepThree({ onSubmit, onBack }) {
|
||||
type="password"
|
||||
id="register-password-input"
|
||||
name="password"
|
||||
label={t("commonPassword")}
|
||||
label={t("authLoginEnterPassword")}
|
||||
isRequired={true}
|
||||
placeholder={t("createAPassword")}
|
||||
autoComplete="current-password"
|
||||
|
||||
@@ -60,7 +60,7 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
|
||||
<TextInput
|
||||
type="email"
|
||||
id="register-email-input"
|
||||
label={t("commonEmail")}
|
||||
label={t("authRegisterEmail")}
|
||||
isRequired={true}
|
||||
placeholder="jordan.ellis@domain.com"
|
||||
autoComplete="email"
|
||||
@@ -68,7 +68,11 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
|
||||
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
|
||||
onChange={onChange}
|
||||
error={errors.email ? true : false}
|
||||
helperText={errors.email}
|
||||
helperText={errors.email && (
|
||||
errors.email.includes("required") ? t("authRegisterEmailRequired") :
|
||||
errors.email.includes("valid email") ? t("authRegisterEmailInvalid") :
|
||||
errors.email
|
||||
)}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<Stack
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
// Utils
|
||||
const { formatBytes } = useHardwareUtils();
|
||||
const { statusStyles, determineState } = useUtils();
|
||||
const { determineState } = useUtils();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { stats, uptimePercentage } = monitor ?? {};
|
||||
@@ -41,7 +41,8 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatBox
|
||||
sx={statusStyles[determineState(monitor)]}
|
||||
gradient={true}
|
||||
status={determineState(monitor)}
|
||||
heading={t("status")}
|
||||
subHeading={determineState(monitor)}
|
||||
/>
|
||||
@@ -50,7 +51,9 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
subHeading={
|
||||
<>
|
||||
{physicalCores}
|
||||
<Typography component="span">cores</Typography>
|
||||
<Typography component="span">
|
||||
{physicalCores === 1 ? "core" : "cores"}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -60,7 +63,9 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
subHeading={
|
||||
<>
|
||||
{logicalCores}
|
||||
<Typography component="span">cores</Typography>
|
||||
<Typography component="span">
|
||||
{logicalCores === 1 ? "core" : "cores"}
|
||||
</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -78,7 +83,7 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
|
||||
subHeading={
|
||||
<>
|
||||
{cpuTemperature.toFixed(2)}
|
||||
<Typography component="span">C</Typography>
|
||||
<Typography component="span">°C</Typography>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -52,7 +52,12 @@ const Filter = ({
|
||||
}, [selectedStatus]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
m: theme.spacing(2),
|
||||
ml: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<FilterHeader
|
||||
header={t("status")}
|
||||
options={statusOptions}
|
||||
|
||||
@@ -75,6 +75,7 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
|
||||
<IconButton
|
||||
aria-label="monitor actions"
|
||||
onClick={openMenu}
|
||||
disabled={!isAdmin}
|
||||
sx={{
|
||||
"&:focus": {
|
||||
outline: "none",
|
||||
|
||||
@@ -94,19 +94,21 @@ const InfrastructureMonitors = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
isLoading={isLoading}
|
||||
path="/infrastructure/create"
|
||||
/>
|
||||
<MonitorCountHeader
|
||||
shouldRender={!isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
<Stack direction={"row"}>
|
||||
<MonitorCountHeader
|
||||
shouldRender={!isLoading}
|
||||
monitorCount={summary?.totalMonitors ?? 0}
|
||||
/>
|
||||
<Filter
|
||||
selectedStatus={selectedStatus}
|
||||
setSelectedStatus={setSelectedStatus}
|
||||
setToFilterStatus={setToFilterStatus}
|
||||
handleReset={handleReset}
|
||||
/>
|
||||
</Stack>
|
||||
<MonitorsTable
|
||||
shouldRender={!isLoading}
|
||||
monitors={monitors}
|
||||
|
||||
@@ -21,6 +21,7 @@ const PageSpeed = () => {
|
||||
const { t } = useTranslation();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { pagespeedApiKey } = useSelector((state) => state.settings);
|
||||
|
||||
const { isLoading, monitors, summary, networkError } = useMonitorsFetch({
|
||||
teamId: user.teamId,
|
||||
@@ -52,7 +53,7 @@ const PageSpeed = () => {
|
||||
]}
|
||||
link="/pagespeed/create"
|
||||
isAdmin={isAdmin}
|
||||
showPageSpeedWarning={true}
|
||||
showPageSpeedWarning={isAdmin && !pagespeedApiKey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -62,7 +63,7 @@ const PageSpeed = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
isLoading={isLoading}
|
||||
path="/pagespeed/create"
|
||||
/>
|
||||
<MonitorCountHeader
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, { useState } from "react";
|
||||
import { Box, Typography, Button, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { networkService } from "../Utils/NetworkService";
|
||||
import Alert from "../Components/Alert";
|
||||
import { createToast } from "../Utils/toastUtils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Background from "../assets/Images/background-grid.svg?react";
|
||||
import Logo from "../assets/icons/checkmate-icon.svg?react";
|
||||
import ThemeSwitch from "../Components/ThemeSwitch";
|
||||
import LanguageSelector from "../Components/LanguageSelector";
|
||||
|
||||
const ServerUnreachable = () => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// State for tracking connection check status
|
||||
const [isCheckingConnection, setIsCheckingConnection] = useState(false);
|
||||
|
||||
const handleRetry = React.useCallback(async () => {
|
||||
setIsCheckingConnection(true);
|
||||
try {
|
||||
// Try to connect to the backend with a simple API call
|
||||
// We'll use any lightweight endpoint that doesn't require authentication
|
||||
await networkService.axiosInstance.get('/health', { timeout: 5000 });
|
||||
|
||||
// If successful, show toast and navigate to login page
|
||||
createToast({
|
||||
body: t("backendReconnected", "Connection to server restored"),
|
||||
});
|
||||
navigate("/login");
|
||||
} catch (error) {
|
||||
// If still unreachable, stay on this page and show toast
|
||||
createToast({
|
||||
body: t("backendStillUnreachable", "Server is still unreachable"),
|
||||
});
|
||||
} finally {
|
||||
setIsCheckingConnection(false);
|
||||
}
|
||||
}, [navigate, t]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="login-page auth"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box
|
||||
className="background-pattern-svg"
|
||||
sx={{
|
||||
"& svg g g:last-of-type path": {
|
||||
stroke: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background style={{ width: "100%" }} />
|
||||
</Box>
|
||||
|
||||
{/* Header with logo */}
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
px={theme.spacing(12)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
|
||||
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
>
|
||||
<LanguageSelector />
|
||||
<ThemeSwitch />
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack
|
||||
width="100%"
|
||||
maxWidth={600}
|
||||
flex={1}
|
||||
justifyContent="center"
|
||||
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
|
||||
pb={theme.spacing(20)}
|
||||
mx="auto"
|
||||
rowGap={theme.spacing(8)}
|
||||
sx={{
|
||||
"& > .MuiStack-root": {
|
||||
border: 1,
|
||||
borderRadius: theme.spacing(5),
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
padding: {
|
||||
xs: theme.spacing(12),
|
||||
sm: theme.spacing(20),
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack spacing={theme.spacing(6)} alignItems="center">
|
||||
<Box sx={{
|
||||
width: theme.spacing(220),
|
||||
mx: 'auto',
|
||||
'& .alert.row-stack': {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing(3)
|
||||
}
|
||||
}}>
|
||||
<Alert
|
||||
variant="error"
|
||||
body={t("backendUnreachable", "Server Unreachable")}
|
||||
hasIcon={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
align="center"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("backendUnreachableMessage", "The Checkmate server is not responding. Please check your deployment configuration or try again later.")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ mt: theme.spacing(4) }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleRetry}
|
||||
disabled={isCheckingConnection}
|
||||
className="dashboard-style-button"
|
||||
sx={{
|
||||
px: theme.spacing(6),
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`,
|
||||
'&.MuiButtonBase-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
},
|
||||
'&.MuiButton-root': {
|
||||
borderRadius: `${theme.shape.borderRadius}px !important`
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isCheckingConnection ?
|
||||
t("retryingConnection", "Retrying Connection...") :
|
||||
t("retryConnection", "Retry Connection")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerUnreachable;
|
||||
@@ -1,15 +1,17 @@
|
||||
// Components
|
||||
import { Box, Stack, Typography, Button, Switch } from "@mui/material";
|
||||
import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import TextInput from "../../Components/Inputs/TextInput";
|
||||
import Link from "../../Components/Link";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { useIsAdmin } from "../../Hooks/useIsAdmin";
|
||||
import Dialog from "../../Components/Dialog";
|
||||
import ConfigBox from "../../Components/ConfigBox";
|
||||
import {
|
||||
WalletMultiButton,
|
||||
WalletDisconnectButton,
|
||||
} from "@solana/wallet-adapter-react-ui";
|
||||
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
|
||||
import { getAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
// import {
|
||||
// WalletMultiButton,
|
||||
// WalletDisconnectButton,
|
||||
// } from "@solana/wallet-adapter-react-ui";
|
||||
|
||||
//Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -23,12 +25,7 @@ import {
|
||||
} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { update } from "../../Features/Auth/authSlice";
|
||||
import PropTypes from "prop-types";
|
||||
import {
|
||||
setTimezone,
|
||||
setMode,
|
||||
setDistributedUptimeEnabled,
|
||||
setLanguage,
|
||||
} from "../../Features/UI/uiSlice";
|
||||
import { setTimezone, setMode, setLanguage } from "../../Features/UI/uiSlice";
|
||||
import timezones from "../../Utils/timezones.json";
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../main";
|
||||
@@ -50,12 +47,18 @@ const Settings = () => {
|
||||
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
|
||||
const { timezone, distributedUptimeEnabled } = useSelector((state) => state.ui);
|
||||
const { mode } = useSelector((state) => state.ui);
|
||||
const { pagespeedApiKey } = useSelector((state) => state.settings);
|
||||
const [checksIsLoading, setChecksIsLoading] = useState(false);
|
||||
const [form, setForm] = useState({
|
||||
enableDistributedUptime: distributedUptimeEnabled,
|
||||
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
|
||||
pagespeedApiKey: pagespeedApiKey,
|
||||
});
|
||||
|
||||
const [version, setVersion] = useState("unknown");
|
||||
const [apiKeyFieldType, setApiKeyFieldType] = useState("password");
|
||||
const [isApiKeySet, setIsApiKeySet] = useState(pagespeedApiKey ? true : false);
|
||||
const [tempPSKey, setTempPSKey] = useState("");
|
||||
const [errors, setErrors] = useState({});
|
||||
const deleteStatsMonitorsInitState = { deleteMonitors: false, deleteStats: false };
|
||||
const [isOpen, setIsOpen] = useState(deleteStatsMonitorsInitState);
|
||||
@@ -80,6 +83,10 @@ const Settings = () => {
|
||||
fetchLatestVersion();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getAppSettings());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { type, checked, value, id } = event.target;
|
||||
|
||||
@@ -91,12 +98,19 @@ const Settings = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = settingsValidation.validate(
|
||||
{ [id]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
if (id === "pagespeedApiKey") {
|
||||
setTempPSKey(value);
|
||||
return;
|
||||
}
|
||||
|
||||
let inputValue = value;
|
||||
if (id === "ttl") {
|
||||
inputValue = value.replace(/[^0-9]/g, "");
|
||||
}
|
||||
|
||||
const updatedForm = { ...form, [id]: inputValue };
|
||||
const { error } = settingsValidation.validate(updatedForm, { abortEarly: false });
|
||||
|
||||
if (!error || error.details.length === 0) {
|
||||
setErrors({});
|
||||
} else {
|
||||
@@ -107,12 +121,8 @@ const Settings = () => {
|
||||
setErrors(newErrors);
|
||||
logger.error("Validation errors:", error.details);
|
||||
}
|
||||
let inputValue = value;
|
||||
id === "ttl" && (inputValue = value.replace(/[^0-9]/g, ""));
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
[id]: inputValue,
|
||||
}));
|
||||
|
||||
setForm(updatedForm);
|
||||
};
|
||||
|
||||
// TODO Handle saving
|
||||
@@ -125,7 +135,14 @@ const Settings = () => {
|
||||
const updatedUser = { ...user, checkTTL: form.ttl };
|
||||
const [userAction, settingsAction] = await Promise.all([
|
||||
dispatch(update({ localData: updatedUser })),
|
||||
dispatch(updateAppSettings({ settings: { language: language } })),
|
||||
dispatch(
|
||||
updateAppSettings({
|
||||
settings: {
|
||||
language: language,
|
||||
pagespeedApiKey: tempPSKey ? tempPSKey : form.pagespeedApiKey,
|
||||
},
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
if (userAction.payload.success && settingsAction.payload.success) {
|
||||
@@ -187,6 +204,14 @@ const Settings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetApiKey = () => {
|
||||
setIsApiKeySet(false);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
pagespeedApiKey: "",
|
||||
}));
|
||||
};
|
||||
|
||||
const languages = Object.keys(i18n.options.resources || {});
|
||||
|
||||
return (
|
||||
@@ -254,7 +279,7 @@ const Settings = () => {
|
||||
></Select>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{isAdmin && (
|
||||
{/* {isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDistributedUptime")}</Typography>
|
||||
@@ -276,8 +301,50 @@ const Settings = () => {
|
||||
: t("settingsDisabled")}
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
)} */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("pageSpeedApiKeyFieldTitle")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
|
||||
{t("pageSpeedApiKeyFieldDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
id="pagespeedApiKey"
|
||||
label={t("pageSpeedApiKeyFieldLabel")}
|
||||
value={form.pagespeedApiKey ? form.pagespeedApiKey : tempPSKey}
|
||||
type={apiKeyFieldType}
|
||||
onChange={handleChange}
|
||||
disabled={isApiKeySet}
|
||||
optionalLabel="(Optional)"
|
||||
error={!!errors.pagespeedApiKey}
|
||||
helperText={errors.pagespeedApiKey}
|
||||
endAdornment={
|
||||
!isApiKeySet && (
|
||||
<PasswordEndAdornment
|
||||
fieldType={apiKeyFieldType}
|
||||
setFieldType={setApiKeyFieldType}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{isApiKeySet && (
|
||||
<Box>
|
||||
<Typography>{t("pageSpeedApiKeyFieldResetLabel")}</Typography>
|
||||
<Button
|
||||
onClick={handleResetApiKey}
|
||||
variant="contained"
|
||||
color="error"
|
||||
sx={{ mt: theme.spacing(4) }}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
{/* {isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsWallet")}</Typography>
|
||||
@@ -302,7 +369,7 @@ const Settings = () => {
|
||||
</Stack>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
)}
|
||||
)} */}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
@@ -348,14 +415,15 @@ const Settings = () => {
|
||||
</ConfigBox>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsDemoMonitorsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<>
|
||||
{/* Demo Monitors Section */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsDemoMonitors")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsDemoMonitorsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsAddDemoMonitors")}</Typography>
|
||||
<Button
|
||||
@@ -368,6 +436,16 @@ const Settings = () => {
|
||||
{t("settingsAddDemoMonitorsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</ConfigBox>
|
||||
|
||||
{/* System Reset Section */}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h1">{t("settingsSystemReset")}</Typography>
|
||||
<Typography sx={{ mt: theme.spacing(2) }}>
|
||||
{t("settingsSystemResetDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>{t("settingsRemoveAllMonitors")}</Typography>
|
||||
<Button
|
||||
@@ -382,17 +460,17 @@ const Settings = () => {
|
||||
{t("settingsRemoveAllMonitorsButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Dialog
|
||||
open={isOpen.deleteMonitors}
|
||||
theme={theme}
|
||||
title={t("settingsRemoveAllMonitorsDialogTitle")}
|
||||
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
|
||||
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
|
||||
onConfirm={handleDeleteAllMonitors}
|
||||
isLoading={isLoading || authIsLoading || checksIsLoading}
|
||||
/>
|
||||
</ConfigBox>
|
||||
<Dialog
|
||||
open={isOpen.deleteMonitors}
|
||||
theme={theme}
|
||||
title={t("settingsRemoveAllMonitorsDialogTitle")}
|
||||
onCancel={() => setIsOpen(deleteStatsMonitorsInitState)}
|
||||
confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
|
||||
onConfirm={handleDeleteAllMonitors}
|
||||
isLoading={isLoading || authIsLoading || checksIsLoading}
|
||||
/>
|
||||
</ConfigBox>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ConfigBox>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Box, Stack, Typography, Button } from "@mui/material";
|
||||
import Image from "../../../../../Components/Image";
|
||||
import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
|
||||
import ThemeSwitch from "../../../../../Components/ThemeSwitch";
|
||||
import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward";
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -86,6 +87,8 @@ const ControlsHeader = ({
|
||||
type = "uptime",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const publicUrl = `/status/uptime/public/${url}`;
|
||||
|
||||
return (
|
||||
<Stack
|
||||
@@ -118,6 +121,26 @@ const ControlsHeader = ({
|
||||
>
|
||||
{statusPage?.companyName}
|
||||
</Typography>
|
||||
{statusPage?.isPublished && !isPublic && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
onClick={() => {
|
||||
window.open(publicUrl, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
":hover": {
|
||||
cursor: "pointer",
|
||||
borderBottom: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography>{t("publicLink")}</Typography>
|
||||
<ArrowOutwardIcon />
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Controls
|
||||
isDeleting={isDeleting}
|
||||
|
||||
@@ -139,6 +139,7 @@ const PublicStatus = () => {
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
url={url}
|
||||
isPublic={isPublic}
|
||||
/>
|
||||
<Typography variant="h2">{t("statusPageStatusServiceStatus")}</Typography>
|
||||
<StatusBar monitors={monitors} />
|
||||
|
||||
@@ -61,6 +61,7 @@ const StatusPages = () => {
|
||||
label="Create status page"
|
||||
isAdmin={isAdmin}
|
||||
path="/status/uptime/create"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatusPagesTable data={statusPages} />
|
||||
</Stack>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState, useRef } from "react";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const UploadFile = ({ onFileSelect }) => { // Changed prop to onFileSelect
|
||||
const theme = useTheme();
|
||||
const [file, setFile] = useState();
|
||||
const [error, setError] = useState("");
|
||||
const inputRef = useRef();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSelectFile = () => {
|
||||
inputRef.current.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
setError("");
|
||||
const selectedFile = e.target.files[0];
|
||||
|
||||
// Basic file validation
|
||||
if (!selectedFile) return;
|
||||
|
||||
if (!selectedFile.name.endsWith('.csv')) {
|
||||
setError(t("bulkImport.invalidFileType"));
|
||||
return;
|
||||
}
|
||||
|
||||
setFile(selectedFile);
|
||||
onFileSelect(selectedFile); // Pass the file directly to parent
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".csv"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<Typography
|
||||
component="h2"
|
||||
mb={theme.spacing(1.5)}
|
||||
sx={{ wordBreak: "break-all" }}
|
||||
>
|
||||
{file?.name || t("bulkImport.noFileSelected")}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="div"
|
||||
mb={theme.spacing(1.5)}
|
||||
color={theme.palette.error.main}
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSelectFile}
|
||||
>
|
||||
{t("bulkImport.selectFile")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
UploadFile.prototype = {
|
||||
onFileSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UploadFile;
|
||||
@@ -0,0 +1,112 @@
|
||||
// React, Redux, Router
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
// MUI
|
||||
import { Box, Stack, Typography, Button, Link } from "@mui/material";
|
||||
|
||||
//Components
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import UploadFile from "./Upload";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { useBulkMonitors } from "../../../Hooks/useBulkMonitors";
|
||||
|
||||
const BulkImport = () => {
|
||||
const theme = useTheme();
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
|
||||
const crumbs = [
|
||||
{ name: t("uptime"), path: "/uptime" },
|
||||
{ name: t("bulkImport.title"), path: `/uptime/bulk-import` },
|
||||
];
|
||||
|
||||
const [createBulkMonitors, hookLoading] = useBulkMonitors();
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedFile) {
|
||||
createToast({ body: t("bulkImport.noFileSelected") });
|
||||
return;
|
||||
}
|
||||
|
||||
const [success, data, error] = await createBulkMonitors(selectedFile, user);
|
||||
|
||||
if (success) {
|
||||
// You can use `data` here if needed
|
||||
createToast({ body: t("bulkImport.uploadSuccess") });
|
||||
navigate("/uptime");
|
||||
} else {
|
||||
createToast({ body: error || t("bulkImport.uploadFailed") });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="bulk-import-monitor">
|
||||
<Breadcrumbs list={crumbs} />
|
||||
<Stack
|
||||
component="form"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
{t("bulkImport.title")}
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("bulkImport.selectFileTips")}</Typography>
|
||||
<Typography component="p">
|
||||
<Trans
|
||||
i18nKey="bulkImport.selectFileDescription"
|
||||
components={{
|
||||
template: (
|
||||
<Link
|
||||
color="info"
|
||||
download
|
||||
href="/bulk_import_monitors_template.csv"
|
||||
/>
|
||||
),
|
||||
sample: (
|
||||
<Link
|
||||
color="info"
|
||||
download
|
||||
href="/bulk_import_monitors_sample.csv"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<UploadFile onFileSelect={(file) => setSelectedFile(file)} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSubmit}
|
||||
disabled={hookLoading}
|
||||
loading={hookLoading}
|
||||
>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkImport;
|
||||
@@ -2,7 +2,15 @@ import { useNavigate, useParams } from "react-router";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Box, Stack, Tooltip, Typography, Button } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Button,
|
||||
FormControlLabel,
|
||||
Switch,
|
||||
} from "@mui/material";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
@@ -25,6 +33,7 @@ import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import "./index.css";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
@@ -94,7 +103,7 @@ const Configure = () => {
|
||||
}, [monitorId, navigate]);
|
||||
|
||||
const handleChange = (event, name) => {
|
||||
let { value, id } = event.target;
|
||||
let { checked, value, id } = event.target;
|
||||
if (!name) name = idMap[id];
|
||||
|
||||
if (name.includes("notification-")) {
|
||||
@@ -126,6 +135,9 @@ const Configure = () => {
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
if (name === "ignoreTlsErrors") {
|
||||
value = checked;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
@@ -196,6 +208,17 @@ const Configure = () => {
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
|
||||
// Notification modal state
|
||||
const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
|
||||
|
||||
const handleOpenNotificationModal = () => {
|
||||
setIsNotificationModalOpen(true);
|
||||
};
|
||||
|
||||
const handleClosenNotificationModal = () => {
|
||||
setIsNotificationModalOpen(false);
|
||||
};
|
||||
|
||||
const statusColor = {
|
||||
true: theme.palette.success.main,
|
||||
false: theme.palette.error.main,
|
||||
@@ -371,6 +394,17 @@ const Configure = () => {
|
||||
value={parsedUrl?.host || monitor?.url || ""}
|
||||
disabled={true}
|
||||
/>
|
||||
<TextInput
|
||||
type="number"
|
||||
id="monitor-port"
|
||||
label={t("portToMonitor")}
|
||||
placeholder="5173"
|
||||
value={monitor.port || ""}
|
||||
onChange={(event) => handleChange(event, "port")}
|
||||
error={errors["port"] ? true : false}
|
||||
helperText={errors["port"]}
|
||||
hidden={monitor.type !== "port"}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
@@ -413,6 +447,15 @@ const Configure = () => {
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Box mt={theme.spacing(2)}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleOpenNotificationModal}
|
||||
>
|
||||
{t("notifications.integrationButton")}
|
||||
</Button>
|
||||
</Box>
|
||||
{/* <Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
@@ -443,7 +486,28 @@ const Configure = () => {
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("distributedUptimeCreateAdvancedSettings")}</Typography>
|
||||
<Typography component="h2">{t("ignoreTLSError")}</Typography>
|
||||
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="ignore-error"
|
||||
checked={monitor.ignoreTlsErrors}
|
||||
onChange={(event) => handleChange(event, "ignoreTlsErrors")}
|
||||
sx={{ mr: theme.spacing(2) }}
|
||||
/>
|
||||
}
|
||||
label={t("tlsErrorIgnored")}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
{t("distributedUptimeCreateAdvancedSettings")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
@@ -546,6 +610,13 @@ const Configure = () => {
|
||||
onConfirm={handleRemove}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<NotificationIntegrationModal
|
||||
open={isNotificationModalOpen}
|
||||
onClose={handleClosenNotificationModal}
|
||||
monitor={monitor}
|
||||
setMonitor={setMonitor}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,8 @@ import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMon
|
||||
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
// MUI
|
||||
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
|
||||
//Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
@@ -92,6 +94,7 @@ const CreateMonitor = () => {
|
||||
url: "",
|
||||
name: "",
|
||||
type: "http",
|
||||
ignoreTlsErrors: false,
|
||||
notifications: [],
|
||||
interval: 1,
|
||||
});
|
||||
@@ -107,6 +110,7 @@ const CreateMonitor = () => {
|
||||
port: monitor.type === "port" ? monitor.port : undefined,
|
||||
name: monitor.name || monitor.url.substring(0, 50),
|
||||
type: monitor.type,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
|
||||
@@ -130,19 +134,6 @@ const CreateMonitor = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (monitor.type === "http") {
|
||||
// const checkEndpointAction = await dispatch(
|
||||
// checkEndpointResolution({ monitorURL: form.url })
|
||||
// );
|
||||
// if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
// createToast({
|
||||
// body: "The endpoint you entered doesn't resolve. Check the URL again.",
|
||||
// });
|
||||
// setErrors({ url: "The entered URL is not reachable." });
|
||||
// return;
|
||||
// }
|
||||
}
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: monitor.name || monitor.url,
|
||||
@@ -160,11 +151,13 @@ const CreateMonitor = () => {
|
||||
};
|
||||
|
||||
const handleChange = (event, formName) => {
|
||||
const { value } = event.target;
|
||||
const { type, checked, value } = event.target;
|
||||
|
||||
const newVal = type === "checkbox" ? checked : value;
|
||||
|
||||
const newMonitor = {
|
||||
...monitor,
|
||||
[formName]: value,
|
||||
[formName]: newVal,
|
||||
};
|
||||
if (formName === "type") {
|
||||
newMonitor.url = "";
|
||||
@@ -172,7 +165,7 @@ const CreateMonitor = () => {
|
||||
setMonitor(newMonitor);
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ type: monitor.type, [formName]: value },
|
||||
{ type: monitor.type, [formName]: newVal },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
@@ -422,6 +415,25 @@ const CreateMonitor = () => {
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("ignoreTLSError")}</Typography>
|
||||
<Typography component="p">{t("ignoreTLSErrorDescription")}</Typography>
|
||||
</Box>
|
||||
<Stack>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
name="ignore-error"
|
||||
checked={monitor.ignoreTlsErrors}
|
||||
onChange={(event) => handleChange(event, "ignoreTlsErrors")}
|
||||
sx={{ mr: theme.spacing(2) }}
|
||||
/>
|
||||
}
|
||||
label={t("tlsErrorIgnored")}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">
|
||||
|
||||
@@ -61,18 +61,20 @@ const UptimeStatusBoxes = ({
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatBox
|
||||
heading="certificate expiry"
|
||||
subHeading={
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={13}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{certificateExpiry}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{monitor?.type === "http" && (
|
||||
<StatBox
|
||||
heading="certificate expiry"
|
||||
subHeading={
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize={13}
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{certificateExpiry}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -189,8 +189,9 @@ const UptimeMonitors = () => {
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<CreateMonitorHeader
|
||||
isAdmin={isAdmin}
|
||||
shouldRender={!isLoading}
|
||||
isLoading={isLoading}
|
||||
path="/uptime/create"
|
||||
bulkPath="/uptime/bulk-import"
|
||||
/>
|
||||
<Greeting type="uptime" />
|
||||
<StatusBoxes
|
||||
|
||||
+30
-17
@@ -36,6 +36,9 @@ import DistributedUptimeDetails from "../Pages/DistributedUptime/Details";
|
||||
import CreateDistributedUptimeStatus from "../Pages/DistributedUptimeStatus/Create";
|
||||
import DistributedUptimeStatus from "../Pages/DistributedUptimeStatus/Status";
|
||||
|
||||
// Server Status
|
||||
import ServerUnreachable from "../Pages/ServerUnreachable";
|
||||
|
||||
// Incidents
|
||||
import Incidents from "../Pages/Incidents";
|
||||
|
||||
@@ -56,6 +59,7 @@ import ProtectedRoute from "../Components/ProtectedRoute";
|
||||
import ProtectedDistributedUptimeRoute from "../Components/ProtectedDistributedUptimeRoute";
|
||||
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
|
||||
import withAdminCheck from "../Components/HOC/withAdminCheck";
|
||||
import BulkImport from "../Pages/Uptime/BulkImport";
|
||||
|
||||
const Routes = () => {
|
||||
const AdminCheckedRegister = withAdminCheck(AuthRegister);
|
||||
@@ -78,6 +82,11 @@ const Routes = () => {
|
||||
element={<Uptime />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/uptime/bulk-import"
|
||||
element={<BulkImport />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/uptime/create/:monitorId?"
|
||||
element={<UptimeCreate />}
|
||||
@@ -90,16 +99,16 @@ const Routes = () => {
|
||||
path="/uptime/configure/:monitorId/"
|
||||
element={<UptimeConfigure />}
|
||||
/>
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/distributed-uptime"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeMonitors />{" "}
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/distributed-uptime/create"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
@@ -114,15 +123,15 @@ const Routes = () => {
|
||||
<CreateDistributedUptime />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
/> */}
|
||||
{/* <Route
|
||||
path="/distributed-uptime/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeDetails />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="pagespeed"
|
||||
@@ -148,9 +157,9 @@ const Routes = () => {
|
||||
path="infrastructure/create"
|
||||
element={<InfrastructureCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="/infrastructure/configure/:monitorId"
|
||||
element={<InfrastructureCreate />}
|
||||
<Route
|
||||
path="/infrastructure/configure/:monitorId"
|
||||
element={<InfrastructureCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure/:monitorId"
|
||||
@@ -171,42 +180,42 @@ const Routes = () => {
|
||||
element={<Status />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/:url"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="status/uptime/create"
|
||||
element={<CreateStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/create/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="status/uptime/configure/:url"
|
||||
element={<CreateStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/configure/:url"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="integrations"
|
||||
@@ -274,11 +283,15 @@ const Routes = () => {
|
||||
path="/status/uptime/public/:url"
|
||||
element={<Status />}
|
||||
/>
|
||||
<Route
|
||||
{/* <Route
|
||||
path="/status/distributed/public/:url"
|
||||
element={<DistributedUptimeStatus />}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<Route
|
||||
path="/server-unreachable"
|
||||
element={<ServerUnreachable />}
|
||||
/>
|
||||
<Route
|
||||
path="*"
|
||||
element={<NotFound />}
|
||||
|
||||
@@ -32,7 +32,7 @@ class NetworkService {
|
||||
|
||||
config.headers = {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Accept-Language": currentLanguage,
|
||||
"Accept-Language": currentLanguage === "gb" ? "en" : currentLanguage,
|
||||
...config.headers,
|
||||
};
|
||||
|
||||
@@ -45,6 +45,16 @@ class NetworkService {
|
||||
this.axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// Handle network errors (server unreachable)
|
||||
if (error.code === "ERR_NETWORK") {
|
||||
// Navigate to server unreachable page
|
||||
navigate("/server-unreachable");
|
||||
// Return an empty resolved promise to stop the error propagation
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle authentication errors
|
||||
|
||||
if (error.response && error.response.status === 401) {
|
||||
dispatch(clearAuthState());
|
||||
dispatch(clearUptimeMonitorState());
|
||||
@@ -257,16 +267,8 @@ class NetworkService {
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios PUT request.
|
||||
*/
|
||||
async updateMonitor(config) {
|
||||
const { monitorId, monitor, updatedFields } = config;
|
||||
const payload = updatedFields ?? {
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
matchMethod: monitor.matchMethod,
|
||||
expectedValue: monitor.expectedValue,
|
||||
jsonPath: monitor.jsonPath,
|
||||
};
|
||||
const { monitorId, updatedFields } = config;
|
||||
const payload = updatedFields;
|
||||
return this.axiosInstance.put(`/monitors/${monitorId}`, payload, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -1086,6 +1088,19 @@ class NetworkService {
|
||||
return this.axiosInstance.delete(`/status-page/${encodedUrl}`, {});
|
||||
}
|
||||
|
||||
// ************************************
|
||||
// Create bulk monitors
|
||||
// ************************************
|
||||
|
||||
async createBulkMonitors(formData) {
|
||||
const response = await this.axiosInstance.post(`/monitors/bulk`, formData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// ************************************
|
||||
// Fetch monitors with summary by TeamID
|
||||
// ************************************
|
||||
|
||||
@@ -63,7 +63,6 @@ const paletteColors = {
|
||||
orange500: "#E88C30",
|
||||
orange600: "#DC6803",
|
||||
orange800: "#624711",
|
||||
|
||||
};
|
||||
|
||||
const semanticColors = {
|
||||
@@ -155,7 +154,6 @@ const newSemanticColors = {
|
||||
light: newColors.blueGray600,
|
||||
dark: newColors.gray200,
|
||||
},
|
||||
// CAIO_REVIEW, need a brighter color for dark bg
|
||||
contrastTextSecondaryDarkBg: {
|
||||
light: newColors.gray200,
|
||||
dark: newColors.gray200,
|
||||
@@ -164,6 +162,14 @@ const newSemanticColors = {
|
||||
light: newColors.blueGray500,
|
||||
dark: newColors.gray500,
|
||||
},
|
||||
contrastBorder: {
|
||||
light: newColors.gray500,
|
||||
dark: newColors.blueGray600,
|
||||
},
|
||||
contrastBorderDisabled: {
|
||||
light: newColors.gray100,
|
||||
dark: newColors.blueGray800,
|
||||
},
|
||||
lowContrast: {
|
||||
light: newColors.gray200,
|
||||
dark: newColors.blueGray600,
|
||||
@@ -264,7 +270,7 @@ const newSemanticColors = {
|
||||
dark: newColors.red600,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
/* These are temporary, just for everything not to break */
|
||||
gradient: {
|
||||
color1: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { typographyLevels } from "./constants";
|
||||
|
||||
const fontFamilyPrimary = '"Inter" , sans-serif';
|
||||
// const fontFamilySecondary = '"Avenir", sans-serif';
|
||||
|
||||
@@ -300,13 +299,15 @@ const baseTheme = (palette) => ({
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
MuiTextField: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
"& fieldset": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.contrastBorder,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
|
||||
"& .MuiInputBase-input": {
|
||||
padding: ".75em",
|
||||
minHeight: "var(--env-var-height-2)",
|
||||
@@ -356,10 +357,19 @@ const baseTheme = (palette) => ({
|
||||
MuiOutlinedInput: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: palette.primary.contrastTextTertiary,
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.primary.contrastTextTertiary,
|
||||
"&.Mui-disabled .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.primary.contrastBorderDisabled,
|
||||
},
|
||||
"&.Mui-disabled:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.primary.contrastBorderDisabled,
|
||||
},
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.primary.contrastText, // Adjust hover border color
|
||||
},
|
||||
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: palette.accent.main, // Adjust focus border color
|
||||
},
|
||||
color: palette.primary.contrastTextTertiary,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -453,8 +463,32 @@ const baseTheme = (palette) => ({
|
||||
MuiSwitch: {
|
||||
styleOverrides: {
|
||||
root: ({ theme }) => ({
|
||||
width: 42,
|
||||
height: 26,
|
||||
padding: 0,
|
||||
"& .MuiSwitch-switchBase": {
|
||||
padding: 0,
|
||||
margin: 2,
|
||||
transitionDuration: "300ms",
|
||||
"&.Mui-checked": {
|
||||
transform: "translateX(16px)",
|
||||
color: "#fff",
|
||||
"& + .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.accent.main,
|
||||
opacity: 1,
|
||||
border: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"& .MuiSwitch-thumb": {
|
||||
boxSizing: "border-box",
|
||||
width: 22,
|
||||
height: 22,
|
||||
},
|
||||
"& .MuiSwitch-track": {
|
||||
backgroundColor: theme.palette.primary.contrastText,
|
||||
borderRadius: 13,
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
opacity: 1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -486,7 +520,7 @@ const baseTheme = (palette) => ({
|
||||
root: ({ theme }) => ({
|
||||
ml: "auto",
|
||||
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderColor: theme.palette.primary.contrastBorder,
|
||||
width: "auto",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ Object.keys(translations).forEach((path) => {
|
||||
});
|
||||
|
||||
const savedLanguage = store.getState()?.ui?.language;
|
||||
const initialLanguage = savedLanguage || primaryLanguage;
|
||||
const initialLanguage = savedLanguage;
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
|
||||
@@ -7,18 +7,11 @@ const nameSchema = joi
|
||||
.string()
|
||||
.max(50)
|
||||
.trim()
|
||||
.pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s']+$/u, {
|
||||
name: "name.containsLetterOrSymbol",
|
||||
})
|
||||
.pattern(/^[\p{L}\p{Sc}\s']+$/u, {
|
||||
name: "name.validCharacters",
|
||||
})
|
||||
.pattern(/^[\p{L}\p{M}''\- ]+$/u)
|
||||
.messages({
|
||||
"string.empty": "Name is required",
|
||||
"string.max": "Name must be less than 50 characters",
|
||||
"string.pattern.name": "Name must contain at least 1 letter or currency symbol",
|
||||
"string.pattern.base.validCharacters":
|
||||
"Can only contain letters, spaces, apostrophes, and currency symbols",
|
||||
"string.pattern.base": "Name must contain only letters, spaces, apostrophes, or hyphens"
|
||||
});
|
||||
|
||||
const passwordSchema = joi
|
||||
@@ -73,8 +66,8 @@ const credentials = joi.object({
|
||||
return lowercasedValue;
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "Email is required",
|
||||
"string.email": "Must be a valid email address",
|
||||
"string.empty": "authRegisterEmailRequired",
|
||||
"string.email": "authRegisterEmailInvalid",
|
||||
}),
|
||||
password: passwordSchema,
|
||||
newPassword: passwordSchema,
|
||||
@@ -159,11 +152,25 @@ const monitorValidation = joi.object({
|
||||
"string.invalidUrl": "Please enter a valid URL with optional port",
|
||||
"string.pattern.base": "Please enter a valid container ID.",
|
||||
}),
|
||||
port: joi.number(),
|
||||
port: joi.number()
|
||||
.integer()
|
||||
.min(1)
|
||||
.max(65535)
|
||||
.when("type", {
|
||||
is: "port",
|
||||
then: joi.required().messages({
|
||||
"number.base": "Port must be a number.",
|
||||
"number.min": "Port must be at least 1.",
|
||||
"number.max": "Port must be at most 65535.",
|
||||
"any.required": "Port is required for port monitors.",
|
||||
}),
|
||||
otherwise: joi.optional(),
|
||||
}),
|
||||
name: joi.string().trim().max(50).allow("").messages({
|
||||
"string.max": "This field should not exceed the 50 characters limit.",
|
||||
}),
|
||||
type: joi.string().trim().messages({ "string.empty": "This field is required." }),
|
||||
ignoreTlsErrors: joi.boolean(),
|
||||
interval: joi.number().messages({
|
||||
"number.base": "Frequency must be a number.",
|
||||
"any.required": "Frequency is required.",
|
||||
@@ -247,9 +254,13 @@ const statusPageValidation = joi.object({
|
||||
});
|
||||
const settingsValidation = joi.object({
|
||||
ttl: joi.number().required().messages({
|
||||
"string.empty": "TTL is required",
|
||||
"string.empty": "Please enter a value",
|
||||
"number.base": "Please enter a valid number",
|
||||
"any.required": "Please enter a value"
|
||||
}),
|
||||
});
|
||||
pagespeedApiKey: joi.string().allow("").optional(),
|
||||
})
|
||||
.unknown(true);
|
||||
|
||||
const dayjsValidator = (value, helpers) => {
|
||||
if (!dayjs(value).isValid()) {
|
||||
@@ -366,5 +377,5 @@ export {
|
||||
advancedSettingsValidation,
|
||||
infrastructureMonitorValidation,
|
||||
statusPageValidation,
|
||||
logoImageValidation,
|
||||
logoImageValidation
|
||||
};
|
||||
+71
-26
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"dontHaveAccount": "Don't have account",
|
||||
"doNotHaveAccount": "Do not have an account?",
|
||||
"registerHere": "Register here",
|
||||
"email": "E-mail",
|
||||
"forgotPassword": "Forgot Password",
|
||||
"password": "password",
|
||||
"signUp": "Sign up",
|
||||
"password": "Password",
|
||||
"signUp": "Sign Up",
|
||||
"submit": "Submit",
|
||||
"title": "Title",
|
||||
"continue": "Continue",
|
||||
@@ -15,9 +17,11 @@
|
||||
"authForgotPasswordTitle": "Forgot password?",
|
||||
"authForgotPasswordResetPassword": "Reset password",
|
||||
"createPassword": "Create your password",
|
||||
"createAPassword": "Create a password",
|
||||
"createAPassword": "Password",
|
||||
"authRegisterAlreadyHaveAccount": "Already have an account?",
|
||||
"authRegisterLoginLink": "Log In",
|
||||
"commonAppName": "Checkmate",
|
||||
"welcomeBack": "Welcome back! You're successfully logged in.",
|
||||
"authLoginEnterEmail": "Enter your email",
|
||||
"authRegisterTitle": "Create an account",
|
||||
"authRegisterStepOneTitle": "Create your account",
|
||||
@@ -40,7 +44,7 @@
|
||||
"authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
|
||||
"authSetNewPasswordNewPassword": "New password",
|
||||
"authSetNewPasswordConfirmPassword": "Confirm password",
|
||||
"confirmPassword": "Confirm your password",
|
||||
"confirmPassword": "Re-enter password to confirm",
|
||||
"authSetNewPasswordResetPassword": "Reset password",
|
||||
"authSetNewPasswordBackTo": "Back to",
|
||||
"authPasswordMustBeAtLeast": "Must be at least",
|
||||
@@ -51,16 +55,19 @@
|
||||
"authPasswordUpperCharacter": "one upper character",
|
||||
"authPasswordLowerCharacter": "one lower character",
|
||||
"authPasswordConfirmAndPassword": "Confirm password and password",
|
||||
"authPasswordMustMatch": "must match",
|
||||
"authPasswordMustMatch": "Passwords must match",
|
||||
"validationNameRequired": "Please enter your name",
|
||||
"validationNameTooLong": "Name should be less than 50 characters",
|
||||
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
|
||||
"authRegisterCreateAccount": "Create your account to get started",
|
||||
"authRegisterCreateSuperAdminAccount": "Create your Super admin account to get started",
|
||||
"authRegisterSignUpWithEmail": "Sign up with Email",
|
||||
"authRegisterBySigningUp": "By signing up, you agree to our",
|
||||
"authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
|
||||
"authRegisterSignUpWithEmail": "Create super admin account",
|
||||
"authRegisterBySigningUp": "By creating an account, you agree to our <a1>Terms of Service</a1> and <a2>Privacy Policy</a2>.",
|
||||
"distributedStatusHeaderText": "Real-time, real-device coverage",
|
||||
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
|
||||
"settingsGeneralSettings": "General settings",
|
||||
"settingsDisplayTimezone": "Display timezone",
|
||||
"settingsDisplayTimezoneDescription": "The timezone of the dashboard you publicly display.",
|
||||
"settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
|
||||
"settingsThemeMode": "Theme Mode",
|
||||
@@ -69,23 +76,25 @@
|
||||
"settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
|
||||
"settingsEnabled": "Enabled",
|
||||
"settingsDisabled": "Disabled",
|
||||
"settingsHistoryAndMonitoring": "History and monitoring",
|
||||
"settingsHistoryAndMonitoringDescription": "Define here for how long you want to keep the data. You can also remove all past data.",
|
||||
"settingsHistoryAndMonitoring": "History of monitoring",
|
||||
"settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
|
||||
"settingsTTLLabel": "The days you want to keep monitoring history.",
|
||||
"settingsTTLOptionalLabel": "0 for infinite",
|
||||
"settingsClearAllStats": "Clear all stats. This is irreversible.",
|
||||
"settingsClearAllStatsButton": "Clear all stats",
|
||||
"settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
|
||||
"settingsClearAllStatsDialogDescription": "Once deleted, your monitors cannot be retrieved.",
|
||||
"settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
|
||||
"settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
|
||||
"settingsDemoMonitors": "Demo monitors",
|
||||
"settingsDemoMonitorsDescription": "Here you can add and remove demo monitors.",
|
||||
"settingsAddDemoMonitors": "Add demo monitors",
|
||||
"settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
|
||||
"settingsAddDemoMonitors": "Adding demo monitors",
|
||||
"settingsAddDemoMonitorsButton": "Add demo monitors",
|
||||
"settingsRemoveAllMonitors": "Remove all monitors",
|
||||
"settingsSystemReset": "System reset",
|
||||
"settingsSystemResetDescription": "Remove all monitors from your system.",
|
||||
"settingsRemoveAllMonitors": "Removing all monitors",
|
||||
"settingsRemoveAllMonitorsButton": "Remove all monitors",
|
||||
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Yes, clear all monitors",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
|
||||
"settingsWallet": "Wallet",
|
||||
"settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
|
||||
"settingsAbout": "About",
|
||||
@@ -99,18 +108,26 @@
|
||||
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
|
||||
"settingsMonitorsDeleted": "Successfully deleted all monitors",
|
||||
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
|
||||
"backendUnreachable": "Server Connection Error",
|
||||
"backendUnreachableMessage": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
|
||||
"backendUnreachableError": "Cannot connect to the server. Please try again later.",
|
||||
"retryConnection": "Retry connection",
|
||||
"retryingConnection": "Connecting...",
|
||||
"backendReconnected": "Successfully reconnected to the server.",
|
||||
"backendStillUnreachable": "Server is still unreachable. Please try again later.",
|
||||
"backendConnectionError": "Error connecting to the server. Please check your network connection.",
|
||||
"starPromptTitle": "Star Checkmate",
|
||||
"starPromptDescription": "See the latest releases and help grow the community on GitHub",
|
||||
"https": "HTTPS",
|
||||
"http": "HTTP",
|
||||
"monitor": "monitor",
|
||||
"aboutus": "About Us",
|
||||
"signUP": "Sign Up",
|
||||
|
||||
"now": "Now",
|
||||
"delete": "Delete",
|
||||
"configure": "Configure",
|
||||
"networkError": "Network error",
|
||||
"responseTime": "Response time:",
|
||||
"responseTime": "Response time",
|
||||
"ms": "ms",
|
||||
"bar": "Bar",
|
||||
"area": "Area",
|
||||
@@ -376,9 +393,9 @@
|
||||
"errorInvalidTypeId": "Invalid notification type provided",
|
||||
"errorInvalidFieldId": "Invalid field ID provided",
|
||||
"inviteNoTokenFound": "No invite token found",
|
||||
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key. Without it, the PageSpeed monitor won't function.",
|
||||
"pageSpeedLearnMoreLink": "Click here to learn",
|
||||
"pageSpeedAddApiKey": "how to add your API key.",
|
||||
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.",
|
||||
"pageSpeedLearnMoreLink": "Click here",
|
||||
"pageSpeedAddApiKey": "to add your API key.",
|
||||
"update": "Update",
|
||||
"invalidFileFormat": "Unsupported file format!",
|
||||
"invalidFileSize": "File size is too large!",
|
||||
@@ -389,11 +406,39 @@
|
||||
"FirstName": "First name",
|
||||
"LastName": "Last name",
|
||||
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
|
||||
"YourPhoto": "Your photo",
|
||||
"ignoreTLSError": "Ignore TLS/SSL error",
|
||||
"tlsErrorIgnored": "TLS/SSL errors ignored",
|
||||
"ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability",
|
||||
"YourPhoto": "Profile photo",
|
||||
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
|
||||
"save": "Save",
|
||||
"DeleteAccount": "Delete account",
|
||||
"DeleteDescriptionText": "Note that deleting your account will remove all data from the server. This is permanent and non-recoverable.",
|
||||
"DeleteAccountWarning": "If you delete your account, you will no longer be able to sign in, and all of your data will be deleted. Deleting your account is permanent and non-recoverable action.",
|
||||
"DeleteWarningTitle": "Really delete this account?"
|
||||
"DeleteAccountTitle": "Remove account",
|
||||
"DeleteAccountButton": "Remove account",
|
||||
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
|
||||
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
|
||||
"DeleteWarningTitle": "Really remove this account?",
|
||||
"authRegisterFirstName": "Name",
|
||||
"authRegisterLastName": "Surname",
|
||||
"authRegisterEmail": "Email",
|
||||
"authRegisterEmailRequired": "To continue, please enter your email address",
|
||||
"authRegisterEmailInvalid": "Please enter a valid email address",
|
||||
"bulkImport": {
|
||||
"title": "Bulk Import",
|
||||
"selectFileTips": "Select CSV file to upload",
|
||||
"selectFileDescription": "You can download our <template>template</template> or <sample>sample</sample>",
|
||||
"selectFile": "Select File",
|
||||
"parsingFailed": "Parsing failed",
|
||||
"uploadSuccess": "Monitors created successfully!",
|
||||
"validationFailed": "Validation failed",
|
||||
"noFileSelected": "No file selected",
|
||||
"fallbackPage": "Import a file to upload a list of servers in bulk"
|
||||
},
|
||||
"publicLink": "Public link",
|
||||
"maskedPageSpeedKeyPlaceholder": "*************************************",
|
||||
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
|
||||
"pageSpeedApiKeyFieldLabel": "PageSpeed API key",
|
||||
"pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.",
|
||||
"pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.",
|
||||
"reset": "Reset"
|
||||
}
|
||||
|
||||
+48
-18
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"dontHaveAccount": "Нет аккаунта",
|
||||
"doNotHaveAccount": "У вас нет учетной записи?",
|
||||
"registerHere": "Зарегистрироваться здесь",
|
||||
"email": "Почта",
|
||||
"forgotPassword": "Забыли пароль",
|
||||
"password": "пароль",
|
||||
"password": "Пароль",
|
||||
"signUp": "Зарегистрироваться",
|
||||
"submit": "Подтвердить",
|
||||
"title": "Название",
|
||||
@@ -15,9 +17,11 @@
|
||||
"authForgotPasswordTitle": "Забыли пароль?",
|
||||
"authForgotPasswordResetPassword": "Сбросить пароль",
|
||||
"createPassword": "Создайте свой пароль",
|
||||
"createAPassword": "Создайте пароль",
|
||||
"createAPassword": "Пароль",
|
||||
"authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
|
||||
"authRegisterLoginLink": "Войти",
|
||||
"commonAppName": "Checkmate",
|
||||
"welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
|
||||
"authLoginEnterEmail": "Введите свой email",
|
||||
"authRegisterTitle": "Создать аккаунт",
|
||||
"authRegisterStepOneTitle": "Создайте свой аккаут",
|
||||
@@ -40,7 +44,8 @@
|
||||
"authSetNewPasswordDescription": "Ваш новый пароль должен отличаться от ранее использованных паролей.",
|
||||
"authSetNewPasswordNewPassword": "Новый пароль",
|
||||
"authSetNewPasswordConfirmPassword": "Подтвердите пароль",
|
||||
"confirmPassword": "Подтвердите ваш пароль",
|
||||
"confirmPassword": "Введите пароль еще раз для подтверждения",
|
||||
"confirmNewPasswordPlaceholder": "Подтвердите ваш новый пароль",
|
||||
"authSetNewPasswordResetPassword": "Сбросить пароль",
|
||||
"authSetNewPasswordBackTo": "Назад к",
|
||||
"authPasswordMustBeAtLeast": "Должно быть как минимум",
|
||||
@@ -51,16 +56,21 @@
|
||||
"authPasswordUpperCharacter": "один верхний символ",
|
||||
"authPasswordLowerCharacter": "один нижний символ",
|
||||
"authPasswordConfirmAndPassword": "Подтвердите пароль и пароль",
|
||||
"authPasswordMustMatch": "должен совпадать",
|
||||
"authPasswordMustMatch": "Пароли должны совпадать",
|
||||
"authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
|
||||
"authRegisterCreateSuperAdminAccount": "Создайте учетную запись суперадминистратора, чтобы начать работу",
|
||||
"authRegisterSignUpWithEmail": "Зарегистрироваться по электронной почте",
|
||||
"authRegisterBySigningUp": "Регистрируясь, вы соглашаетесь с нашими",
|
||||
"authRegisterSignUpWithEmail": "Создать учетную запись суперадминистратора",
|
||||
"authRegisterBySigningUp": "Создавая учетную запись, вы соглашаетесь с нашими <a1>Условиями использования</a1> и <a2>Политикой конфиденциальности</a2>.",
|
||||
"authRegisterFirstName": "Имя",
|
||||
"authRegisterLastName": "Фамилия",
|
||||
"authRegisterEmail": "Эл. почта",
|
||||
"authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите ваш адрес электронной почты",
|
||||
"authRegisterEmailInvalid": "Пожалуйста, введите корректный адрес электронной почты",
|
||||
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
|
||||
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
|
||||
"settingsGeneralSettings": "Общие настройки",
|
||||
"settingsDisplayTimezone": "Отображать часовой пояс",
|
||||
"settingsDisplayTimezoneDescription": "Часовой пояс панели мониторинга, которую вы публично отображаете.",
|
||||
"settingsDisplayTimezoneDescription": "Выберите часовой пояс, используемый для отображения дат и времени в приложении.",
|
||||
"settingsAppearance": "Внешний вид",
|
||||
"settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
|
||||
"settingsThemeMode": "Тема",
|
||||
@@ -69,23 +79,25 @@
|
||||
"settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
|
||||
"settingsEnabled": "Включено",
|
||||
"settingsDisabled": "Выключено",
|
||||
"settingsHistoryAndMonitoring": "История и мониторинг",
|
||||
"settingsHistoryAndMonitoringDescription": "Определите здесь, как долго вы хотите хранить данные. Вы также можете удалить все прошлые данные.",
|
||||
"settingsHistoryAndMonitoring": "История мониторинга",
|
||||
"settingsHistoryAndMonitoringDescription": "Определите, как долго вы хотите хранить исторические данные. Вы также можете очистить все существующие данные.",
|
||||
"settingsTTLLabel": "Дни, за которыми вы хотите следить.",
|
||||
"settingsTTLOptionalLabel": "0 для бесконечности",
|
||||
"settingsClearAllStats": "Очистить всю статистику. Это необратимо.",
|
||||
"settingsClearAllStatsButton": "Очистить всю статистику",
|
||||
"settingsClearAllStatsDialogTitle": "Хотите очистить всю статистику?",
|
||||
"settingsClearAllStatsDialogDescription": "После удаления ваши мониторы не могут быть восстановлены.",
|
||||
"settingsClearAllStatsDialogDescription": "После удаления история мониторинга и статистика не могут быть восстановлены.",
|
||||
"settingsClearAllStatsDialogConfirm": "Да, очистить всю статистику",
|
||||
"settingsDemoMonitors": "Демо мониторы",
|
||||
"settingsDemoMonitorsDescription": "Здесь вы можете добавлять и удалять демонстрационные мониторы.",
|
||||
"settingsAddDemoMonitors": "Добавьте демонстрационные мониторы",
|
||||
"settingsDemoMonitorsDescription": "Добавьте примеры мониторов для демонстрации.",
|
||||
"settingsAddDemoMonitors": "Добавление демонстрационных мониторов",
|
||||
"settingsAddDemoMonitorsButton": "Добавьте демонстрационные мониторы",
|
||||
"settingsRemoveAllMonitors": "Удалить все демонстрационные мониторы",
|
||||
"settingsRemoveAllMonitorsButton": "Удалить все демонстрационные мониторы",
|
||||
"settingsSystemReset": "Сброс системы",
|
||||
"settingsSystemResetDescription": "Удалить все мониторы из вашей системы.",
|
||||
"settingsRemoveAllMonitors": "Удаление всех мониторов",
|
||||
"settingsRemoveAllMonitorsButton": "Удалить все мониторы",
|
||||
"settingsRemoveAllMonitorsDialogTitle": "Хотите удалить все мониторы?",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Да, очистить все мониторы",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Да, удалить все мониторы",
|
||||
"settingsWallet": "Кошелёк",
|
||||
"settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
|
||||
"settingsAbout": "О",
|
||||
@@ -95,22 +107,40 @@
|
||||
"settingsFailedToSave": "Не удалось сохранить настройки",
|
||||
"settingsStatsCleared": "Статистика успешно очищена",
|
||||
"settingsFailedToClearStats": "Не удалось очистить статистику",
|
||||
"FirstName": "Имя",
|
||||
"LastName": "Фамилия",
|
||||
"YourPhoto": "Фото профиля",
|
||||
"PhotoDescriptionText": "Это фото будет отображаться на странице вашего профиля.",
|
||||
"EmailDescriptionText": "Ваш текущий email—его нельзя изменить.",
|
||||
"DeleteAccountTitle": "Удалить аккаунт",
|
||||
"DeleteAccountButton": "Удалить аккаунт",
|
||||
"DeleteDescriptionText": "Это удалит аккаунт и все связанные данные с сервера. Это необратимо.",
|
||||
"DeleteAccountWarning": "Удаление аккаунта означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
|
||||
"DeleteWarningTitle": "Действительно удалить этот аккаунт?",
|
||||
"settingsDemoMonitorsAdded": "Успешно добавлены демонстрационные мониторы",
|
||||
"settingsFailedToAddDemoMonitors": "Не удалось добавить демонстрационные мониторы",
|
||||
"settingsMonitorsDeleted": "Успешно удалены все мониторы",
|
||||
"settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы",
|
||||
"backendUnreachable": "Ошибка подключения к серверу",
|
||||
"backendUnreachableMessage": "Мы не можем подключиться к серверу. Пожалуйста, проверьте ваше интернет-соединение или проверьте конфигурацию развертывания, если проблема не устраняется.",
|
||||
"backendUnreachableError": "Невозможно подключиться к серверу. Пожалуйста, повторите попытку позже.",
|
||||
"retryConnection": "Повторить подключение",
|
||||
"retryingConnection": "Подключение...",
|
||||
"backendReconnected": "Успешно восстановлено подключение к серверу.",
|
||||
"backendStillUnreachable": "Сервер по-прежнему недоступен. Пожалуйста, повторите попытку позже.",
|
||||
"backendConnectionError": "Ошибка подключения к серверу. Пожалуйста, проверьте ваше сетевое подключение.",
|
||||
"starPromptTitle": "Star Checkmate",
|
||||
"starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub",
|
||||
"https": "HTTPS",
|
||||
"http": "HTTP",
|
||||
"monitor": "монитор",
|
||||
"aboutus": "О Нас",
|
||||
"signUP": "Зарегистрироваться",
|
||||
|
||||
"now": "Сейчас",
|
||||
"delete": "Удалить",
|
||||
"configure": "Настроить",
|
||||
"networkError": "Ошибка сети",
|
||||
"responseTime": "Время ответа:",
|
||||
"responseTime": "Время ответа",
|
||||
"ms": "мс",
|
||||
"bar": "Bar",
|
||||
"area": "Area",
|
||||
@@ -364,4 +394,4 @@
|
||||
"pageSpeedWarning": "Предупреждение: Вы не добавили ключ API Google PageSpeed. Без него монитор PageSpeed не будет работать.",
|
||||
"pageSpeedLearnMoreLink": "Нажмите здесь, чтобы узнать",
|
||||
"pageSpeedAddApiKey": "как добавить ваш ключ API."
|
||||
}
|
||||
}
|
||||
+54
-22
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"dontHaveAccount": "Hesabınız yok mu",
|
||||
"doNotHaveAccount": "Hesabınız yok mu?",
|
||||
"registerHere": "Buradan kayıt olun",
|
||||
"email": "E-posta",
|
||||
"forgotPassword": "Parolamı unuttum",
|
||||
"password": "Parola",
|
||||
@@ -15,9 +17,11 @@
|
||||
"authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
|
||||
"authForgotPasswordResetPassword": "Parola sıfırla",
|
||||
"createPassword": "Parolanızı oluşturun",
|
||||
"createAPassword": "Bir parola oluşturun",
|
||||
"createAPassword": "Parola",
|
||||
"authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
|
||||
"authRegisterLoginLink": "Giriş Yap",
|
||||
"commonAppName": "Checkmate",
|
||||
"welcomeBack": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
|
||||
"authLoginEnterEmail": "E-posta adresinizi girin",
|
||||
"authRegisterTitle": "Hesap oluştur",
|
||||
"authRegisterStepOneTitle": "Hesabınızı oluşturun",
|
||||
@@ -40,7 +44,8 @@
|
||||
"authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
|
||||
"authSetNewPasswordNewPassword": "Yeni şifre",
|
||||
"authSetNewPasswordConfirmPassword": "Parolayı onayla",
|
||||
"confirmPassword": "Parolanızı onaylayın",
|
||||
"confirmPassword": "Onaylamak için parolayı tekrar girin",
|
||||
"confirmNewPasswordPlaceholder": "Yeni parolanızı onaylayın",
|
||||
"authSetNewPasswordResetPassword": "Parola sıfırla",
|
||||
"authSetNewPasswordBackTo": "Geri dön",
|
||||
"authPasswordMustBeAtLeast": "En az",
|
||||
@@ -51,7 +56,7 @@
|
||||
"authPasswordUpperCharacter": "bir büyük harf",
|
||||
"authPasswordLowerCharacter": "bir küçük harf",
|
||||
"authPasswordConfirmAndPassword": "Onay şifresi ve şifre",
|
||||
"authPasswordMustMatch": "eşleşmelidir",
|
||||
"authPasswordMustMatch": "Parolalar eşleşmelidir",
|
||||
"authRegisterCreateAccount": "Hesap oluşturmak için devam et",
|
||||
"authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin",
|
||||
"authRegisterSignUpWithEmail": "E-posta ile kayıt ol",
|
||||
@@ -60,7 +65,7 @@
|
||||
"distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin",
|
||||
"settingsGeneralSettings": "Genel ayarlar",
|
||||
"settingsDisplayTimezone": "Görüntüleme saat dilimi",
|
||||
"settingsDisplayTimezoneDescription": "Herkese açık olarak görüntülediğiniz kontrol panelinin saat dilimi.",
|
||||
"settingsDisplayTimezoneDescription": "Uygulama genelinde tarih ve saatlerin görüntülenmesi için kullanılacak saat dilimini seçin.",
|
||||
"settingsAppearance": "Görünüm",
|
||||
"settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin",
|
||||
"settingsThemeMode": "Tema",
|
||||
@@ -69,23 +74,25 @@
|
||||
"settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
|
||||
"settingsEnabled": "Etkin",
|
||||
"settingsDisabled": "Devre dışı",
|
||||
"settingsHistoryAndMonitoring": "Geçmiş ve izleme",
|
||||
"settingsHistoryAndMonitoringDescription": "Verileri ne kadar süreyle saklamak istediğinizi burada tanımlayın. Ayrıca tüm geçmiş verileri kaldırabilirsiniz.",
|
||||
"settingsHistoryAndMonitoring": "İzleme geçmişi",
|
||||
"settingsHistoryAndMonitoringDescription": "Geçmiş verileri ne kadar süre saklamak istediğinizi tanımlayın. Ayrıca mevcut tüm verileri temizleyebilirsiniz.",
|
||||
"settingsTTLLabel": "İzleme geçmişini saklamak istediğiniz gün sayısı.",
|
||||
"settingsTTLOptionalLabel": "Sınırsız için 0",
|
||||
"settingsClearAllStats": "Tüm istatistikleri temizle. Bu geri alınamaz.",
|
||||
"settingsClearAllStatsButton": "Tüm istatistikleri temizle",
|
||||
"settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?",
|
||||
"settingsClearAllStatsDialogDescription": "Silindikten sonra, monitörleriniz geri alınamaz.",
|
||||
"settingsClearAllStatsDialogDescription": "Kaldırıldıktan sonra, izleme geçmişi ve istatistikleri geri alınamaz.",
|
||||
"settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle",
|
||||
"settingsDemoMonitors": "Demo monitörler",
|
||||
"settingsDemoMonitorsDescription": "Burada demo monitörler ekleyebilir ve kaldırabilirsiniz.",
|
||||
"settingsAddDemoMonitors": "Demo monitörler ekle",
|
||||
"settingsDemoMonitorsDescription": "Gösterim amaçlı örnek monitörler ekleyin.",
|
||||
"settingsAddDemoMonitors": "Demo monitörler ekleniyor",
|
||||
"settingsAddDemoMonitorsButton": "Demo monitörler ekle",
|
||||
"settingsRemoveAllMonitors": "Tüm monitörleri kaldır",
|
||||
"settingsSystemReset": "Sistem sıfırlama",
|
||||
"settingsSystemResetDescription": "Sisteminizden tüm monitörleri kaldırın.",
|
||||
"settingsRemoveAllMonitors": "Tüm monitörler kaldırılıyor",
|
||||
"settingsRemoveAllMonitorsButton": "Tüm monitörleri kaldır",
|
||||
"settingsRemoveAllMonitorsDialogTitle": "Tüm monitörleri kaldırmak istiyor musunuz?",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri temizle",
|
||||
"settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri kaldır",
|
||||
"settingsWallet": "Cüzdan",
|
||||
"settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.",
|
||||
"settingsAbout": "Hakkında",
|
||||
@@ -99,18 +106,26 @@
|
||||
"settingsFailedToAddDemoMonitors": "Demo monitörler eklenemedi",
|
||||
"settingsMonitorsDeleted": "Tüm monitörler başarıyla silindi",
|
||||
"settingsFailedToDeleteMonitors": "Monitörler silinemedi",
|
||||
"backendUnreachable": "Sunucu Bağlantı Hatası",
|
||||
"backendUnreachableMessage": "Sunucuya bağlanamıyoruz. Lütfen internet bağlantınızı kontrol edin veya sorun devam ederse dağıtım yapılandırmanızı doğrulayın.",
|
||||
"backendUnreachableError": "Sunucuya bağlanılamıyor. Lütfen daha sonra tekrar deneyin.",
|
||||
"retryConnection": "Bağlantıyı Yeniden Dene",
|
||||
"retryingConnection": "Bağlanıyor...",
|
||||
"backendReconnected": "Sunucuya başarıyla yeniden bağlandı.",
|
||||
"backendStillUnreachable": "Sunucu hala erişilemez durumda. Lütfen daha sonra tekrar deneyin.",
|
||||
"backendConnectionError": "Sunucuya bağlanırken hata oluştu. Lütfen ağ bağlantınızı kontrol edin.",
|
||||
"starPromptTitle": "Checkmate yıldızla değerlendirin",
|
||||
"starPromptDescription": "En son sürümleri görün ve GitHub'daki topluluğun büyümesine yardımcı olun",
|
||||
"https": "HTTPS",
|
||||
"http": "HTTP",
|
||||
"monitor": "monitör",
|
||||
"aboutus": "Hakkımızda",
|
||||
"signUP": "Hesap Oluştur",
|
||||
|
||||
"now": "Şimdi",
|
||||
"delete": "Sil",
|
||||
"configure": "Yapılandır",
|
||||
"networkError": "Ağ hatası",
|
||||
"responseTime": "Yanıt süresi:",
|
||||
"responseTime": "Yanıt süresi",
|
||||
"ms": "ms",
|
||||
"bar": "Çubuk",
|
||||
"area": "Alan",
|
||||
@@ -386,14 +401,31 @@
|
||||
"DragandDrop": "",
|
||||
"MaxSize": "",
|
||||
"SupportedFormats": "",
|
||||
"FirstName": "",
|
||||
"LastName": "",
|
||||
"EmailDescriptionText": "",
|
||||
"YourPhoto": "",
|
||||
"PhotoDescriptionText": "",
|
||||
"FirstName": "Ad",
|
||||
"LastName": "Soyad",
|
||||
"EmailDescriptionText": "Mevcut e-postanız—değiştirilemez.",
|
||||
"YourPhoto": "Profil fotoğrafı",
|
||||
"PhotoDescriptionText": "Bu fotoğraf profil sayfanızda görüntülenecektir.",
|
||||
"save": "",
|
||||
"DeleteAccount": "",
|
||||
"DeleteDescriptionText": "",
|
||||
"DeleteAccountWarning": "",
|
||||
"DeleteWarningTitle": ""
|
||||
"DeleteAccountTitle": "Hesabı kaldır",
|
||||
"DeleteAccountButton": "Hesabı kaldır",
|
||||
"DeleteDescriptionText": "Bu, hesabı ve tüm ilişkili verileri sunucudan kaldıracaktır. Bu geri alınamaz.",
|
||||
"DeleteAccountWarning": "Hesabınızı kaldırmak, tekrar oturum açamayacağınız ve tüm verilerinizin kaldırılacağı anlamına gelir. Bu geri alınamaz.",
|
||||
"DeleteWarningTitle": "Bu hesabı gerçekten kaldırmak istiyor musunuz?",
|
||||
"authRegisterFirstName": "Ad",
|
||||
"authRegisterLastName": "Soyad",
|
||||
"authRegisterEmail": "E-posta",
|
||||
"authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
|
||||
"authRegisterEmailInvalid": "Lütfen geçerli bir e-posta adresi girin",
|
||||
"bulkImport": {
|
||||
"title": "",
|
||||
"selectFileTips": "",
|
||||
"selectFileDescription": "",
|
||||
"selectFile": "",
|
||||
"parsingFailed": "",
|
||||
"uploadSuccess": "",
|
||||
"validationFailed": "",
|
||||
"noFileSelected": "",
|
||||
"fallbackPage": ""
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,6 @@ dist/mongo/data/*
|
||||
dist/redis/data/*
|
||||
prod/mongo/data/*
|
||||
prod/redis/data/*
|
||||
dist/docker-compose-test.yaml
|
||||
*.env
|
||||
prod/certbot/*
|
||||
+14
-10
@@ -4,23 +4,27 @@
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["uptime_client"]="./docker/dev/client.Dockerfile"
|
||||
["uptime_database_mongo"]="./docker/dev/mongoDB.Dockerfile"
|
||||
["uptime_redis"]="./docker/dev/redis.Dockerfile"
|
||||
["uptime_server"]="./docker/dev/server.Dockerfile"
|
||||
# Define service names and their corresponding Dockerfiles in parallel arrays
|
||||
services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server")
|
||||
dockerfiles=(
|
||||
"./docker/dev/client.Dockerfile"
|
||||
"./docker/dev/mongoDB.Dockerfile"
|
||||
"./docker/dev/redis.Dockerfile"
|
||||
"./docker/dev/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
for i in "${!services[@]}"; do
|
||||
service="${services[$i]}"
|
||||
dockerfile="${dockerfiles[$i]}"
|
||||
|
||||
## Check if the build succeeded
|
||||
docker build -f "$dockerfile" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All images built successfully"
|
||||
echo "All images built successfully"
|
||||
@@ -18,10 +18,13 @@ COPY ./client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ./client .
|
||||
COPY ./client ./
|
||||
|
||||
RUN npm run build-dev
|
||||
RUN npm run build
|
||||
|
||||
RUN npm install -g serve
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
CMD ["serve","-s", "dist", "-l", "5173"]
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -3,15 +3,19 @@ services:
|
||||
image: uptime_client:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "5173:5173"
|
||||
|
||||
- "80:80"
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "http://localhost"
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d/
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
image: uptime_server:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
@@ -31,10 +35,17 @@ services:
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: uptime_database_mongo:latest
|
||||
image: uptime_mongo:latest
|
||||
restart: always
|
||||
command: ["mongod", "--quiet"]
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
server_name localhost; # Set server name to localhost
|
||||
server_tokens off;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# location /api/ {
|
||||
# proxy_pass http://server:52345/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_set_header Connection '';
|
||||
# chunked_transfer_encoding off;
|
||||
# proxy_buffering off;
|
||||
# proxy_cache off;
|
||||
# }
|
||||
|
||||
# location /api-docs/ {
|
||||
# proxy_pass http://server:52345/api-docs/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
}
|
||||
@@ -8,6 +8,6 @@ RUN npm install
|
||||
|
||||
COPY ./server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
Vendored
+14
-6
@@ -1,16 +1,17 @@
|
||||
services:
|
||||
client:
|
||||
image: bluewaveuptime/uptime_client:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:frontend-dist
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "http://localhost"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
- server
|
||||
server:
|
||||
image: bluewaveuptime/uptime_server:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-dist
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
@@ -18,13 +19,13 @@ services:
|
||||
- redis
|
||||
- mongodb
|
||||
environment:
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db
|
||||
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- CLIENT_HOST=http://localhost
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: bluewaveuptime/uptime_redis:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:redis-dist
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -37,10 +38,17 @@ services:
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
mongodb:
|
||||
image: bluewaveuptime/uptime_database_mongo:latest
|
||||
image: ghcr.io/bluewave-labs/checkmate:mongo-dist
|
||||
restart: always
|
||||
volumes:
|
||||
- ./mongo/data:/data/db
|
||||
command: ["mongod", "--quiet"]
|
||||
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
|
||||
ports:
|
||||
- "27017:27017"
|
||||
healthcheck:
|
||||
test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'mongodb:27017'}]}) }" | mongosh --port 27017 --quiet
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
var username = process.env.USERNAME_ENV_VAR;
|
||||
var password = process.env.PASSWORD_ENV_VAR;
|
||||
|
||||
db = db.getSiblingDB("uptime_db");
|
||||
|
||||
db.createUser({
|
||||
user: username,
|
||||
pwd: password,
|
||||
roles: [
|
||||
{
|
||||
role: "readWrite",
|
||||
db: "uptime_db",
|
||||
},
|
||||
],
|
||||
});
|
||||
print("User uptime_user created successfully");
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
try {
|
||||
const status = rs.status();
|
||||
printjson(status);
|
||||
} catch (e) {
|
||||
if (e.codeName === "NotYetInitialized") {
|
||||
print("Replica set not initialized. Initiating...");
|
||||
rs.initiate({
|
||||
_id: "rs0",
|
||||
members: [{ _id: 0, host: "mongodb:27017" }],
|
||||
});
|
||||
} else {
|
||||
print("Unexpected error during rs.status():");
|
||||
printjson(e);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -15,14 +15,14 @@ server {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
# location /api/ {
|
||||
# proxy_pass http://server:5000/api/;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
|
||||
Vendored
+1
-1
@@ -8,6 +8,6 @@ RUN npm install
|
||||
|
||||
COPY ./server/ ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -25,12 +25,14 @@ services:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-demo
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
- redis
|
||||
- mongodb
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: ghcr.io/bluewave-labs/checkmate:redis-demo
|
||||
restart: always
|
||||
|
||||
@@ -16,7 +16,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -25,7 +25,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -50,7 +50,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -59,7 +59,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -10,6 +10,6 @@ RUN npm install
|
||||
|
||||
COPY ./server ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1"
|
||||
UPTIME_STATUS_PAGE_SUBDOMAIN_PREFIX: "http://uptimegenie.com/"
|
||||
UPTIME_APP_CLIENT_HOST: "https://checkmate-test.bluewavelabs.ca"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -27,7 +27,7 @@ services:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-staging
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "52345:52345"
|
||||
env_file:
|
||||
- server.env
|
||||
depends_on:
|
||||
|
||||
@@ -16,7 +16,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -29,7 +29,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -54,7 +54,7 @@ server {
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:5000/api/;
|
||||
proxy_pass http://server:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
@@ -67,7 +67,7 @@ server {
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server:5000/api-docs/;
|
||||
proxy_pass http://server:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@@ -10,6 +10,6 @@ RUN npm install
|
||||
|
||||
COPY ./server ./
|
||||
|
||||
EXPOSE 5000
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
@@ -57,9 +57,9 @@ class AuthController {
|
||||
*/
|
||||
registerUser = async (req, res, next) => {
|
||||
try {
|
||||
if(req.body?.email){
|
||||
if (req.body?.email) {
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
}
|
||||
}
|
||||
await registrationBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
@@ -68,11 +68,13 @@ class AuthController {
|
||||
}
|
||||
// Create a new user
|
||||
try {
|
||||
const { inviteToken } = req.body;
|
||||
const user = req.body;
|
||||
// If superAdmin exists, a token should be attached to all further register requests
|
||||
const superAdminExists = await this.db.checkSuperadmin(req, res);
|
||||
if (superAdminExists) {
|
||||
await this.db.getInviteTokenAndDelete(inviteToken);
|
||||
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
|
||||
user.role = invitedUser.role;
|
||||
user.teamId = invitedUser.teamId;
|
||||
} else {
|
||||
// This is the first account, create JWT secret to use if one is not supplied by env
|
||||
const jwtSecret = crypto.randomBytes(64).toString("hex");
|
||||
@@ -133,8 +135,8 @@ class AuthController {
|
||||
*/
|
||||
loginUser = async (req, res, next) => {
|
||||
try {
|
||||
if(req.body?.email){
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
if (req.body?.email) {
|
||||
req.body.email = req.body.email?.toLowerCase();
|
||||
}
|
||||
await loginValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
|
||||
@@ -24,13 +24,15 @@ import axios from "axios";
|
||||
import seedDb from "../db/mongo/utils/seedDb.js";
|
||||
import { seedDistributedTest } from "../db/mongo/utils/seedDb.js";
|
||||
const SERVICE_NAME = "monitorController";
|
||||
import pkg from "papaparse";
|
||||
|
||||
class MonitorController {
|
||||
constructor(db, settingsService, jobQueue, stringService) {
|
||||
constructor(db, settingsService, jobQueue, stringService, emailService) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,56 +245,119 @@ class MonitorController {
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates bulk monitors and adds them to the job queue.
|
||||
* Creates bulk monitors and adds them to the job queue after parsing CSV.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {Object} req.file - The uploaded CSV file.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
|
||||
* @returns {Object} The response object with a success status and message.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
createBulkMonitors = async (req, res, next) => {
|
||||
try {
|
||||
await createMonitorsBodyValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
const { parse } = pkg;
|
||||
|
||||
try {
|
||||
// create monitors
|
||||
const monitors = await this.db.createBulkMonitors(req);
|
||||
// validate the file
|
||||
if (!req.file) {
|
||||
throw new Error("No file uploaded");
|
||||
}
|
||||
|
||||
// create notifications for each monitor
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor, index) => {
|
||||
const notifications = req.body[index].notifications;
|
||||
// Check if the file is a CSV
|
||||
if (!req.file.mimetype.includes("csv")) {
|
||||
throw new Error("File is not a CSV");
|
||||
}
|
||||
|
||||
if (notifications?.length) {
|
||||
monitor.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
return await this.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
await monitor.save();
|
||||
// Validate if the file is empty
|
||||
if (req.file.size === 0) {
|
||||
throw new Error("File is empty");
|
||||
}
|
||||
|
||||
const { userId, teamId } = req.body;
|
||||
|
||||
if (!userId || !teamId) {
|
||||
throw new Error("Missing userId or teamId in form data");
|
||||
}
|
||||
|
||||
// Get file buffer from memory and convert to string
|
||||
const fileData = req.file.buffer.toString("utf-8");
|
||||
|
||||
// Parse the CSV data
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value, header) => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw new Error(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
// Add monitor to job queue
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }) => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw new Error("Error parsing CSV");
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.bulkMonitorsCreate,
|
||||
data: monitors,
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
try {
|
||||
const monitors = await this.db.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor, index) => {
|
||||
const notifications = enrichedData[index].notifications;
|
||||
|
||||
if (notifications?.length) {
|
||||
monitor.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
return await this.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
await monitor.save();
|
||||
}
|
||||
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.bulkMonitorsCreate,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
return next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the endpoint can be resolved
|
||||
* @async
|
||||
@@ -473,18 +538,16 @@ class MonitorController {
|
||||
const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
|
||||
|
||||
// Get notifications from the request body
|
||||
const notifications = req.body.notifications;
|
||||
|
||||
const notifications = req.body.notifications ?? [];
|
||||
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
|
||||
|
||||
await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
|
||||
|
||||
await Promise.all(
|
||||
notifications &&
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = editedMonitor._id;
|
||||
await this.db.createNotification(notification);
|
||||
})
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = editedMonitor._id;
|
||||
await this.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
|
||||
// Delete the old job(editedMonitor has the same ID as the old monitor)
|
||||
@@ -572,6 +635,50 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a test email to verify email delivery functionality.
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.body - The body of the request.
|
||||
* @property {string} req.body.to - The email address to send the test email to.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status and the email delivery message ID.
|
||||
* @throws {Error} If there is an error while sending the test email.
|
||||
*/
|
||||
sendTestEmail = async (req, res, next) => {
|
||||
try {
|
||||
const { to } = req.body;
|
||||
|
||||
if (!to || typeof to !== "string") {
|
||||
return res.error({ msg: this.stringService.errorForValidEmailAddress });
|
||||
}
|
||||
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const messageId = await this.emailService.buildAndSendEmail(
|
||||
"testEmailTemplate",
|
||||
context,
|
||||
to,
|
||||
subject
|
||||
);
|
||||
|
||||
if (!messageId) {
|
||||
return res.error({
|
||||
msg: "Failed to send test email.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.sendTestEmail,
|
||||
data: { messageId },
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "sendTestEmail"));
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
|
||||
@@ -11,16 +11,15 @@ class SettingsController {
|
||||
}
|
||||
|
||||
getAppSettings = async (req, res, next) => {
|
||||
try {
|
||||
const settings = { ...(await this.settingsService.getSettings()) };
|
||||
delete settings.jwtSecret;
|
||||
return res.success({
|
||||
msg: this.stringService.getAppSettings,
|
||||
data: settings,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAppSettings"));
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
const sanitizedSettings = { ...dbSettings };
|
||||
if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") {
|
||||
sanitizedSettings.pagespeedApiKey = "********";
|
||||
}
|
||||
return res.success({
|
||||
msg: this.stringService.getAppSettings,
|
||||
data: sanitizedSettings,
|
||||
});
|
||||
};
|
||||
|
||||
updateAppSettings = async (req, res, next) => {
|
||||
|
||||
@@ -2,48 +2,9 @@ import mongoose from "mongoose";
|
||||
|
||||
const AppSettingsSchema = mongoose.Schema(
|
||||
{
|
||||
apiBaseUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "http://localhost:5000/api/v1",
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
logLevel: {
|
||||
type: String,
|
||||
default: "debug",
|
||||
enum: ["debug", "none", "error", "warn"],
|
||||
},
|
||||
clientHost: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "http://localhost:5173",
|
||||
},
|
||||
jwtSecret: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "my_secret",
|
||||
},
|
||||
dbType: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "MongoDB",
|
||||
},
|
||||
dbConnectionString: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "mongodb://localhost:27017/uptime_db",
|
||||
},
|
||||
redisUrl: {
|
||||
type: String,
|
||||
default: "redis://127.0.0.1:6379",
|
||||
},
|
||||
jwtTTL: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "2h",
|
||||
default: "gb",
|
||||
},
|
||||
pagespeedApiKey: {
|
||||
type: String,
|
||||
@@ -51,19 +12,15 @@ const AppSettingsSchema = mongoose.Schema(
|
||||
},
|
||||
systemEmailHost: {
|
||||
type: String,
|
||||
default: "smtp.gmail.com",
|
||||
},
|
||||
systemEmailPort: {
|
||||
type: Number,
|
||||
default: 465,
|
||||
},
|
||||
systemEmailAddress: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
systemEmailPassword: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
singleton: {
|
||||
type: Boolean,
|
||||
@@ -77,4 +34,4 @@ const AppSettingsSchema = mongoose.Schema(
|
||||
}
|
||||
);
|
||||
|
||||
export default mongoose.model("AppSettings", AppSettingsSchema);
|
||||
export default mongoose.model("AppSettings", AppSettingsSchema);
|
||||
@@ -15,6 +15,7 @@ const InviteTokenSchema = mongoose.Schema(
|
||||
role: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: ["user"],
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
|
||||
@@ -39,6 +39,10 @@ const MonitorSchema = mongoose.Schema(
|
||||
"distributed_test",
|
||||
],
|
||||
},
|
||||
ignoreTlsErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
jsonPath: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
@@ -76,7 +76,8 @@ import * as diagnosticModule from "./modules/diagnosticModule.js";
|
||||
class MongoDB {
|
||||
static SERVICE_NAME = "MongoDB";
|
||||
|
||||
constructor() {
|
||||
constructor({ appSettings }) {
|
||||
this.appSettings = appSettings;
|
||||
Object.assign(this, userModule);
|
||||
Object.assign(this, inviteModule);
|
||||
Object.assign(this, recoveryModule);
|
||||
@@ -95,8 +96,7 @@ class MongoDB {
|
||||
connect = async () => {
|
||||
try {
|
||||
const connectionString =
|
||||
process.env.DB_CONNECTION_STRING || "mongodb://localhost:27017/uptime_db";
|
||||
console.log("Connecting to MongoDB with connection string:", connectionString);
|
||||
this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
|
||||
await mongoose.connect(connectionString);
|
||||
// If there are no AppSettings, create one
|
||||
await AppSettings.findOneAndUpdate(
|
||||
|
||||
@@ -751,7 +751,7 @@ const createMonitor = async (req, res) => {
|
||||
*/
|
||||
const createBulkMonitors = async (req) => {
|
||||
try {
|
||||
const monitors = req.body.map(
|
||||
const monitors = req.map(
|
||||
(item) => new Monitor({ ...item, notifications: undefined })
|
||||
);
|
||||
await Monitor.bulkSave(monitors);
|
||||
|
||||
@@ -14,10 +14,11 @@ const getAppSettings = async () => {
|
||||
|
||||
const updateAppSettings = async (newSettings) => {
|
||||
try {
|
||||
console.log(newSettings);
|
||||
const settings = await AppSettings.findOneAndUpdate(
|
||||
{},
|
||||
{ $set: newSettings },
|
||||
{ new: true }
|
||||
{ new: true, upsert: true }
|
||||
);
|
||||
return settings;
|
||||
} catch (error) {
|
||||
|
||||
+30
-23
@@ -81,7 +81,9 @@ import ServiceRegistry from "./service/serviceRegistry.js";
|
||||
|
||||
import MongoDB from "./db/mongo/MongoDB.js";
|
||||
|
||||
// Redis Service and dependencies
|
||||
import IORedis from "ioredis";
|
||||
import RedisService from "./service/redisService.js";
|
||||
|
||||
import TranslationService from "./service/translationService.js";
|
||||
import languageMiddleware from "./middleware/languageMiddleware.js";
|
||||
@@ -98,8 +100,6 @@ const openApiSpec = JSON.parse(
|
||||
|
||||
let server;
|
||||
|
||||
const PORT = 5000;
|
||||
|
||||
const shutdown = async () => {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
@@ -113,21 +113,16 @@ const shutdown = async () => {
|
||||
method: "shutdown",
|
||||
});
|
||||
// flush Redis
|
||||
const settings =
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings() || {};
|
||||
const redisService = ServiceRegistry.get(RedisService.SERVICE_NAME);
|
||||
await redisService.flushall();
|
||||
|
||||
const { redisUrl } = settings;
|
||||
const redis = new IORedis(redisUrl, { maxRetriesPerRequest: null });
|
||||
|
||||
logger.info({ message: "Flushing Redis" });
|
||||
await redis.flushall();
|
||||
logger.info({ message: "Redis flushed" });
|
||||
process.exit(1);
|
||||
}, SHUTDOWN_TIMEOUT);
|
||||
try {
|
||||
server.close();
|
||||
await ServiceRegistry.get(JobQueue.SERVICE_NAME).obliterate();
|
||||
await ServiceRegistry.get(MongoDB.SERVICE_NAME).disconnect();
|
||||
await ServiceRegistry.get(RedisService.SERVICE_NAME).flushall();
|
||||
logger.info({ message: "Graceful shutdown complete" });
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
@@ -142,19 +137,21 @@ const shutdown = async () => {
|
||||
// Need to wrap server setup in a function to handle async nature of JobQueue
|
||||
const startApp = async () => {
|
||||
const app = express();
|
||||
const allowedOrigin = process.env.CLIENT_HOST;
|
||||
// Create and Register Primary services
|
||||
const translationService = new TranslationService(logger);
|
||||
const stringService = new StringService(translationService);
|
||||
ServiceRegistry.register(StringService.SERVICE_NAME, stringService);
|
||||
|
||||
// Create DB
|
||||
const db = new MongoDB();
|
||||
await db.connect();
|
||||
|
||||
// Create services
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
await settingsService.loadSettings();
|
||||
const appSettings = settingsService.loadSettings();
|
||||
|
||||
// Create DB
|
||||
const db = new MongoDB({ appSettings });
|
||||
await db.connect();
|
||||
|
||||
// Set allowed origin
|
||||
const allowedOrigin = appSettings.clientHost;
|
||||
|
||||
const networkService = new NetworkService(
|
||||
axios,
|
||||
@@ -185,7 +182,13 @@ const startApp = async () => {
|
||||
stringService
|
||||
);
|
||||
|
||||
const jobQueue = new JobQueue(
|
||||
const redisService = await RedisService.createInstance({
|
||||
logger,
|
||||
IORedis,
|
||||
SettingsService: settingsService,
|
||||
});
|
||||
|
||||
const jobQueue = new JobQueue({
|
||||
db,
|
||||
statusService,
|
||||
networkService,
|
||||
@@ -194,8 +197,9 @@ const startApp = async () => {
|
||||
stringService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
);
|
||||
Worker,
|
||||
redisService,
|
||||
});
|
||||
|
||||
// Register services
|
||||
ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
|
||||
@@ -207,11 +211,13 @@ const startApp = async () => {
|
||||
ServiceRegistry.register(StatusService.SERVICE_NAME, statusService);
|
||||
ServiceRegistry.register(NotificationService.SERVICE_NAME, notificationService);
|
||||
ServiceRegistry.register(TranslationService.SERVICE_NAME, translationService);
|
||||
ServiceRegistry.register(RedisService.SERVICE_NAME, redisService);
|
||||
|
||||
await translationService.initialize();
|
||||
|
||||
server = app.listen(PORT, () => {
|
||||
logger.info({ message: `server started on port:${PORT}` });
|
||||
const port = appSettings.port || 52345;
|
||||
server = app.listen(port, () => {
|
||||
logger.info({ message: `Server started on port:${port}` });
|
||||
});
|
||||
|
||||
process.on("SIGUSR2", shutdown);
|
||||
@@ -231,7 +237,8 @@ const startApp = async () => {
|
||||
ServiceRegistry.get(MongoDB.SERVICE_NAME),
|
||||
ServiceRegistry.get(SettingsService.SERVICE_NAME),
|
||||
ServiceRegistry.get(JobQueue.SERVICE_NAME),
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME)
|
||||
ServiceRegistry.get(StringService.SERVICE_NAME),
|
||||
ServiceRegistry.get(EmailService.SERVICE_NAME)
|
||||
);
|
||||
|
||||
const settingsController = new SettingsController(
|
||||
@@ -343,7 +350,7 @@ const startApp = async () => {
|
||||
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
||||
app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
|
||||
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
|
||||
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
|
||||
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
|
||||
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
|
||||
app.use("/api/v1/health", (req, res) => {
|
||||
res.json({
|
||||
|
||||
@@ -158,5 +158,8 @@
|
||||
"platformRequired": "Platform is required",
|
||||
"testNotificationFailed": "Failed to send test notification",
|
||||
"monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\n📟 Status Code: {code}\n\u200B\n",
|
||||
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n"
|
||||
"monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n",
|
||||
"sendTestEmail": "Test email sent successfully",
|
||||
"errorForValidEmailAddress": "A valid recipient email address is required.",
|
||||
"testEmailSubject": "Test Email from Monitoring System"
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
import logger from "../utils/logger.js";
|
||||
|
||||
const languageMiddleware =
|
||||
(stringService, translationService, settingsService) => async (req, res, next) => {
|
||||
(stringService, translationService) => async (req, res, next) => {
|
||||
try {
|
||||
const settings = await settingsService.getSettings();
|
||||
|
||||
let language = settings && settings.language ? settings.language : null;
|
||||
|
||||
if (!language) {
|
||||
const acceptLanguage = req.headers["accept-language"] || "en";
|
||||
language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
|
||||
}
|
||||
const acceptLanguage = req.headers["accept-language"] || "en";
|
||||
const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase();
|
||||
|
||||
translationService.setLanguage(language);
|
||||
stringService.setLanguage(language);
|
||||
|
||||
+2
-1
@@ -33,8 +33,9 @@
|
||||
"mailersend": "^2.2.0",
|
||||
"mjml": "^5.0.0-alpha.4",
|
||||
"mongoose": "^8.3.3",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
"papaparse": "^5.5.2",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import multer from "multer";
|
||||
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage() // Store file in memory as Buffer
|
||||
});
|
||||
|
||||
class MonitorRoutes {
|
||||
constructor(monitorController) {
|
||||
this.router = Router();
|
||||
@@ -89,10 +94,17 @@ class MonitorRoutes {
|
||||
this.router.post(
|
||||
"/bulk",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
upload.single("csvFile"),
|
||||
this.monitorController.createBulkMonitors
|
||||
);
|
||||
|
||||
this.router.post("/seed", isAllowed(["superadmin"]), this.monitorController.seedDb);
|
||||
|
||||
this.router.post(
|
||||
"/test-email",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
this.monitorController.sendTestEmail
|
||||
);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -67,6 +67,7 @@ class EmailService {
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
|
||||
testEmailTemplate: this.loadTemplate("testEmailTemplate")
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import IORedis from "ioredis";
|
||||
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const JOBS_PER_WORKER = 5;
|
||||
@@ -18,7 +16,7 @@ const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
|
||||
class NewJobQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor(
|
||||
constructor({
|
||||
db,
|
||||
statusService,
|
||||
networkService,
|
||||
@@ -27,16 +25,14 @@ class NewJobQueue {
|
||||
stringService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
) {
|
||||
const settings = settingsService.getSettings() || {};
|
||||
const { redisUrl } = settings;
|
||||
const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
|
||||
Worker,
|
||||
redisService,
|
||||
}) {
|
||||
this.connection = redisService.getConnection();
|
||||
this.queues = {};
|
||||
this.workers = {};
|
||||
this.lastJobProcessedTime = {};
|
||||
|
||||
this.connection = connection;
|
||||
this.db = db;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
@@ -47,7 +43,7 @@ class NewJobQueue {
|
||||
this.stringService = stringService;
|
||||
|
||||
QUEUE_NAMES.forEach((name) => {
|
||||
const q = new Queue(name, { connection });
|
||||
const q = new Queue(name, { connection: this.connection });
|
||||
this.lastJobProcessedTime[q.name] = Date.now();
|
||||
q.on("error", (error) => {
|
||||
this.logger.error({
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import jmespath from "jmespath";
|
||||
import https from "https";
|
||||
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
|
||||
|
||||
@@ -133,6 +135,7 @@ class NetworkService {
|
||||
name,
|
||||
teamId,
|
||||
type,
|
||||
ignoreTlsErrors,
|
||||
jsonPath,
|
||||
matchMethod,
|
||||
expectedValue,
|
||||
@@ -141,6 +144,12 @@ class NetworkService {
|
||||
|
||||
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
|
||||
|
||||
if (ignoreTlsErrors === true) {
|
||||
config.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
}
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.axios.get(url, config)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
class RedisService {
|
||||
static SERVICE_NAME = "RedisService";
|
||||
|
||||
constructor({ logger, IORedis, SettingsService }) {
|
||||
this.logger = logger;
|
||||
this.IORedis = IORedis;
|
||||
this.SettingsService = SettingsService;
|
||||
this.connection = null;
|
||||
}
|
||||
|
||||
static async createInstance({ logger, IORedis, SettingsService }) {
|
||||
const instance = new RedisService({ logger, IORedis, SettingsService });
|
||||
await instance.connect();
|
||||
return instance;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const settings = this.SettingsService.getSettings();
|
||||
const { redisUrl } = settings;
|
||||
this.connection = new this.IORedis(redisUrl, {
|
||||
maxRetriesPerRequest: null,
|
||||
retryStrategy: (times) => {
|
||||
if (times >= 5) {
|
||||
throw new Error("Failed to connect to Redis");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: "Retrying Redis connection",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
details: { times },
|
||||
});
|
||||
return Math.min(times * 100, 2000);
|
||||
},
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let errorOccurred = false;
|
||||
|
||||
this.connection.on("ready", () => {
|
||||
if (!errorOccurred) {
|
||||
this.logger.info({
|
||||
message: "Redis connection established",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
});
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
this.connection.on("error", (err) => {
|
||||
errorOccurred = true;
|
||||
this.logger.error({
|
||||
message: "Redis connection error",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
error: err,
|
||||
});
|
||||
setTimeout(() => reject(err), 5000);
|
||||
});
|
||||
});
|
||||
}
|
||||
async flushall() {
|
||||
this.logger.debug({
|
||||
message: "Flushing all Redis data",
|
||||
service: RedisService.SERVICE_NAME,
|
||||
});
|
||||
await this.connection.flushall();
|
||||
}
|
||||
|
||||
getConnection() {
|
||||
return this.connection;
|
||||
}
|
||||
}
|
||||
|
||||
export default RedisService;
|
||||
@@ -1,28 +1,26 @@
|
||||
const SERVICE_NAME = "SettingsService";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
const envConfig = {
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
logLevel: process.env.LOG_LEVEL,
|
||||
language: process.env.LANGUAGE,
|
||||
clientHost: process.env.CLIENT_HOST,
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
dbType: process.env.DB_TYPE,
|
||||
dbConnectionString: process.env.DB_CONNECTION_STRING,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
jwtTTL: process.env.TOKEN_TTL,
|
||||
pagespeedApiKey: process.env.PAGESPEED_API_KEY,
|
||||
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
|
||||
systemEmailPort: process.env.SYSTEM_EMAIL_PORT,
|
||||
systemEmailUser: process.env.SYSTEM_EMAIL_USER,
|
||||
systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS,
|
||||
systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD,
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
jwtTTL: process.env.TOKEN_TTL,
|
||||
clientHost: process.env.CLIENT_HOST,
|
||||
dbConnectionString: process.env.DB_CONNECTION_STRING,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
callbackUrl: process.env.CALLBACK_URL,
|
||||
port: process.env.PORT,
|
||||
pagespeedApiKey: process.env.PAGESPEED_API_KEY,
|
||||
uprockApiKey: process.env.UPROCK_API_KEY,
|
||||
};
|
||||
/**
|
||||
* SettingsService
|
||||
*
|
||||
* This service is responsible for loading and managing the application settings.
|
||||
* It gives priority to environment variables and will only load settings
|
||||
* from the database if they are not set in the environment.
|
||||
*/
|
||||
class SettingsService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -35,38 +33,17 @@ class SettingsService {
|
||||
this.settings = { ...envConfig };
|
||||
}
|
||||
/**
|
||||
* Load settings from the database and merge with environment settings.
|
||||
* If there are any settings that weren't set by environment variables, use user settings from the database.
|
||||
* @returns {Promise<Object>} The merged settings.
|
||||
* @throws Will throw an error if settings are not found in the database or if settings have not been loaded.
|
||||
*/ async loadSettings() {
|
||||
try {
|
||||
const dbSettings = await this.appSettings.findOne();
|
||||
if (!this.settings) {
|
||||
throw new Error("Settings not found");
|
||||
}
|
||||
|
||||
// If there are any settings that weren't set by environment variables, use user settings from DB
|
||||
for (const key in envConfig) {
|
||||
if (
|
||||
typeof envConfig?.[key] === "undefined" &&
|
||||
typeof dbSettings?.[key] !== "undefined"
|
||||
) {
|
||||
this.settings[key] = dbSettings[key];
|
||||
}
|
||||
}
|
||||
return this.settings;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "loadSettings") : null;
|
||||
throw error;
|
||||
}
|
||||
* Load settings from env settings
|
||||
* @returns {Object>} The settings.
|
||||
*/
|
||||
loadSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
/**
|
||||
* Reload settings by calling loadSettings.
|
||||
* @returns {Promise<Object>} The reloaded settings.
|
||||
*/
|
||||
async reloadSettings() {
|
||||
reloadSettings() {
|
||||
return this.loadSettings();
|
||||
}
|
||||
/**
|
||||
@@ -80,6 +57,11 @@ class SettingsService {
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
async getDBSettings() {
|
||||
const settings = await this.appSettings.findOne({ singleton: true }).lean();
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsService;
|
||||
@@ -0,0 +1,17 @@
|
||||
<mjml>
|
||||
<mj-body>
|
||||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text font-size="20px" font-family="Helvetica Neue">
|
||||
Hello!
|
||||
</mj-text>
|
||||
<mj-text font-size="16px" font-family="Helvetica Neue">
|
||||
This is a test email from the Monitoring System.
|
||||
</mj-text>
|
||||
<mj-text font-size="14px" font-family="Helvetica Neue">
|
||||
If you're receiving this, your email configuration is working correctly.
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
+21
-1
@@ -1,4 +1,6 @@
|
||||
import { createLogger, format, transports } from "winston";
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
@@ -40,8 +42,10 @@ class Logger {
|
||||
}
|
||||
);
|
||||
|
||||
const logLevel = process.env.LOG_LEVEL || "info";
|
||||
|
||||
this.logger = createLogger({
|
||||
level: "info",
|
||||
level: logLevel,
|
||||
format: format.combine(format.timestamp()),
|
||||
transports: [
|
||||
new transports.Console({
|
||||
@@ -107,6 +111,22 @@ class Logger {
|
||||
stack: config.stack,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Logs a debug message.
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.message - The message to log.
|
||||
* @param {string} config.service - The service name.
|
||||
* @param {string} config.method - The method name.
|
||||
* @param {Object} config.details - Additional details.
|
||||
*/
|
||||
debug(config) {
|
||||
this.logger.debug(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
stack: config.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
|
||||
+22
-14
@@ -63,11 +63,7 @@ const registrationBodyValidation = joi.object({
|
||||
}),
|
||||
password: joi.string().min(8).required().pattern(passwordPattern),
|
||||
profileImage: joi.any(),
|
||||
role: joi
|
||||
.array()
|
||||
.items(joi.string().valid("superadmin", "admin", "user", "demo"))
|
||||
.min(1)
|
||||
.required(),
|
||||
role: joi.array().items(joi.string().valid("superadmin", "admin", "user", "demo")),
|
||||
teamId: joi.string().allow("").required(),
|
||||
inviteToken: joi.string().allow("").required(),
|
||||
});
|
||||
@@ -83,7 +79,6 @@ const editUserBodyValidation = joi.object({
|
||||
newPassword: joi.string().min(8).pattern(passwordPattern),
|
||||
password: joi.string().min(8).pattern(passwordPattern),
|
||||
deleteProfileImage: joi.boolean(),
|
||||
role: joi.array(),
|
||||
});
|
||||
|
||||
const recoveryValidation = joi.object({
|
||||
@@ -188,6 +183,7 @@ const createMonitorBodyValidation = joi.object({
|
||||
description: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
url: joi.string().required(),
|
||||
ignoreTlsErrors: joi.boolean().default(false),
|
||||
port: joi.number(),
|
||||
isActive: joi.boolean(),
|
||||
interval: joi.number(),
|
||||
@@ -212,9 +208,11 @@ const editMonitorBodyValidation = joi.object({
|
||||
interval: joi.number(),
|
||||
notifications: joi.array().items(joi.object()),
|
||||
secret: joi.string(),
|
||||
ignoreTlsErrors: joi.boolean(),
|
||||
jsonPath: joi.string().allow(""),
|
||||
expectedValue: joi.string().allow(""),
|
||||
matchMethod: joi.string(),
|
||||
port: joi.number().min(1).max(65535),
|
||||
thresholds: joi.object().keys({
|
||||
usage_cpu: joi.number(),
|
||||
usage_memory: joi.number(),
|
||||
@@ -305,7 +303,18 @@ const getChecksParamValidation = joi.object({
|
||||
});
|
||||
|
||||
const getChecksQueryValidation = joi.object({
|
||||
type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "distributed_http", "distributed_test"),
|
||||
type: joi
|
||||
.string()
|
||||
.valid(
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed",
|
||||
"hardware",
|
||||
"docker",
|
||||
"port",
|
||||
"distributed_http",
|
||||
"distributed_test"
|
||||
),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
|
||||
@@ -578,16 +587,15 @@ const triggerNotificationBodyValidation = joi.object({
|
||||
|
||||
const createAnnouncementValidation = joi.object({
|
||||
title: joi.string().required().messages({
|
||||
'string.empty': 'Title cannot be empty',
|
||||
'any.required': 'Title is required',
|
||||
"string.empty": "Title cannot be empty",
|
||||
"any.required": "Title is required",
|
||||
}),
|
||||
message: joi.string().required().messages({
|
||||
'string.empty': 'Message cannot be empty',
|
||||
'any.required': 'Message is required',
|
||||
"string.empty": "Message cannot be empty",
|
||||
"any.required": "Message is required",
|
||||
}),
|
||||
userId: joi.string().required(),
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
export {
|
||||
roleValidatior,
|
||||
@@ -651,5 +659,5 @@ export {
|
||||
imageValidation,
|
||||
triggerNotificationBodyValidation,
|
||||
webhookConfigValidation,
|
||||
createAnnouncementValidation
|
||||
createAnnouncementValidation,
|
||||
};
|
||||
Reference in new issue
Block a user