diff --git a/client/src/Components/PageStateWrapper/index.jsx b/client/src/Components/PageStateWrapper/index.jsx
new file mode 100644
index 000000000..f971b2c9c
--- /dev/null
+++ b/client/src/Components/PageStateWrapper/index.jsx
@@ -0,0 +1,66 @@
+import GenericFallback from "../GenericFallback";
+import Fallback from "../Fallback";
+import { Typography } from "@mui/material";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { useIsAdmin } from "../../Hooks/useIsAdmin";
+import PropTypes from "prop-types";
+const PageStateWrapper = ({
+ networkError,
+ isLoading,
+ items,
+ type,
+ fallbackLink,
+ fallbackChildren,
+ children,
+}) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const isAdmin = useIsAdmin();
+ const isEmpty = items === null || (Array.isArray(items) && items.length === 0);
+ const hasInitialized = items !== undefined && !(isLoading && isEmpty);
+ const showEmpty = !isLoading && isEmpty;
+ if (networkError) {
+ return (
+
+
+ {t("common.toasts.networkError")}
+
+ {t("common.toasts.checkConnection")}
+
+ );
+ }
+ if (!hasInitialized) {
+ return null;
+ }
+ if (showEmpty) {
+ return (
+
+ {fallbackChildren}
+
+ );
+ }
+ return <>{children}>;
+};
+
+PageStateWrapper.propTypes = {
+ networkError: PropTypes.any,
+ isLoading: PropTypes.bool,
+ items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]),
+ type: PropTypes.string,
+ fallbackLink: PropTypes.string,
+ fallbackChildren: PropTypes.node,
+ children: PropTypes.node,
+};
+
+export default PageStateWrapper;
diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
index 7feebd44c..539725958 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/useNotifications.js
@@ -34,7 +34,7 @@ const useCreateNotification = () => {
const useGetNotificationsByTeamId = (updateTrigger) => {
const [notifications, setNotifications] = useState([]);
- const [isLoading, setIsLoading] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const { t } = useTranslation();
diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx
index f7ca892c2..bfd2270fe 100644
--- a/client/src/Pages/Infrastructure/Monitors/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/index.jsx
@@ -1,12 +1,11 @@
// Components
-import { Stack, Typography } from "@mui/material";
+import { Stack } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
import MonitorsTable from "./Components/MonitorsTable";
import Pagination from "../../..//Components/Table/TablePagination";
-import GenericFallback from "../../../Components/GenericFallback";
-import Fallback from "../../../Components/Fallback";
+import PageStateWrapper from "../../../Components/PageStateWrapper";
import Filter from "./Components/Filters";
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent";
// Utils
@@ -81,75 +80,58 @@ const InfrastructureMonitors = () => {
updateTrigger,
});
- if (networkError === true) {
- return (
-
-
- {t("common.toasts.networkError")}
-
- {t("common.toasts.checkConnection")}
-
- );
- }
-
- if (!isLoading && typeof summary?.totalMonitors === "undefined") {
- return (
-
- );
- }
-
return (
-
-
-
+
-
-
-
-
-
+ items={monitors}
+ type="infrastructureMonitor"
+ fallbackLink="/infrastructure/create"
+ >
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+ >
);
};
diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/Maintenance/index.jsx
index 9f9528a67..ee142f524 100644
--- a/client/src/Pages/Maintenance/index.jsx
+++ b/client/src/Pages/Maintenance/index.jsx
@@ -1,6 +1,5 @@
import { Stack, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
-import Fallback from "../../Components/Fallback";
import { useState, useEffect } from "react";
import "./index.css";
import MaintenanceTable from "./MaintenanceTable";
@@ -8,24 +7,21 @@ import { useSelector } from "react-redux";
import { networkService } from "../../main";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { useNavigate } from "react-router-dom";
-import { useIsAdmin } from "../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
-import { Typography } from "@mui/material";
-import GenericFallback from "../../Components/GenericFallback";
+import PageStateWrapper from "../../Components/PageStateWrapper";
const Maintenance = () => {
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5);
- const isAdmin = useIsAdmin();
- const [maintenanceWindows, setMaintenanceWindows] = useState([]);
+ const [maintenanceWindows, setMaintenanceWindows] = useState(undefined);
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
const [page, setPage] = useState(0);
const [sort, setSort] = useState({});
const [updateTrigger, setUpdateTrigger] = useState(false);
const [networkError, setNetworkError] = useState(false);
- const [isDataFetched, setIsDataFetched] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
const handleActionMenuDelete = () => {
setUpdateTrigger((prev) => !prev);
@@ -34,80 +30,65 @@ const Maintenance = () => {
useEffect(() => {
const fetchMaintenanceWindows = async () => {
try {
+ setNetworkError(false);
+ setIsLoading(true);
const response = await networkService.getMaintenanceWindowsByTeamId({
page: page,
rowsPerPage: rowsPerPage,
});
const { maintenanceWindows, maintenanceWindowCount } = response.data.data;
- setMaintenanceWindows(maintenanceWindows);
- setMaintenanceWindowCount(maintenanceWindowCount);
+ setMaintenanceWindows(maintenanceWindows ?? []);
+ setMaintenanceWindowCount(maintenanceWindowCount ?? 0);
} catch (error) {
setNetworkError(true);
} finally {
- setIsDataFetched(true);
+ setIsLoading(false);
}
};
fetchMaintenanceWindows();
}, [page, rowsPerPage, updateTrigger]);
- if (networkError) {
- return (
-
-
- {t("common.toasts.networkError")}
-
- {t("common.toasts.checkConnection")}
-
- );
- }
- // Only show the fallback if we've fetched data and there are no maintenance windows
- if (isDataFetched && maintenanceWindows.length === 0) {
- return (
-
- );
- }
-
return (
-
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx
index f9fc2e674..910fbf1fc 100644
--- a/client/src/Pages/Notifications/index.jsx
+++ b/client/src/Pages/Notifications/index.jsx
@@ -4,11 +4,10 @@ import Typography from "@mui/material/Typography";
import Breadcrumbs from "../../Components/Breadcrumbs";
import Button from "@mui/material/Button";
import DataTable from "../../Components/Table";
-import Fallback from "../../Components/Fallback";
import ActionMenu from "./components/ActionMenu";
+import PageStateWrapper from "../../Components/PageStateWrapper";
// Utils
-import { useIsAdmin } from "../../Hooks/useIsAdmin";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
@@ -23,7 +22,6 @@ const Notifications = () => {
const theme = useTheme();
const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }];
const [updateTrigger, setUpdateTrigger] = useState(false);
- const isAdmin = useIsAdmin();
const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger);
const [deleteNotification, isDeleting, deleteError] = useDeleteNotification();
const { t } = useTranslation();
@@ -76,49 +74,47 @@ const Notifications = () => {
},
];
- if (notifications?.length === 0) {
- return (
-
- );
- }
-
return (
-
-
-
+
-
-
- {t("notifications.createTitle")}
- navigate(`/notifications/${row._id}`),
- rowSX: {
- cursor: "pointer",
- "&:hover td": {
- backgroundColor: theme.palette.tertiary.main,
- transition: "background-color .3s ease",
- },
- },
- }}
- headers={headers}
- data={notifications}
- />
-
+
+
+
+
+
+ {t("notifications.createTitle")}
+ navigate(`/notifications/${row._id}`),
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ headers={headers}
+ data={notifications}
+ />
+
+
+ >
);
};
diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/index.jsx
index dff28e9cb..b5b13ba6e 100644
--- a/client/src/Pages/PageSpeed/Monitors/index.jsx
+++ b/client/src/Pages/PageSpeed/Monitors/index.jsx
@@ -1,18 +1,16 @@
// Components
import { useState } from "react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import { Stack, Typography } from "@mui/material";
+import { Stack } from "@mui/material";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid";
-import Fallback from "../../../Components/Fallback";
-import GenericFallback from "../../../Components/GenericFallback";
+import PageStateWrapper from "../../../Components/PageStateWrapper";
import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useTranslation } from "react-i18next";
import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
import { useFetchSettings } from "../../../Hooks/settingsHooks";
// Constants
@@ -20,7 +18,6 @@ const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const TYPES = ["pagespeed"];
const PageSpeed = () => {
const theme = useTheme();
- const { t } = useTranslation();
const isAdmin = useIsAdmin();
const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({
@@ -40,56 +37,42 @@ const PageSpeed = () => {
setIsEmailPasswordSet: () => {},
});
- if (networkError === true) {
- return (
-
-
- {t("common.toasts.networkError")}
-
- {t("common.toasts.checkConnection")}
-
- );
- }
-
- if (!isLoading && monitors?.length === 0) {
- return (
-
- {isAdmin && settingsData && !settingsData.pagespeedApiKey && (
-
- )}
-
- );
- }
-
return (
-
-
-
+
-
-
-
+ items={monitors}
+ type="pageSpeed"
+ fallbackLink="/pagespeed/create"
+ fallbackChildren={
+ isAdmin &&
+ settingsData &&
+ !settingsData.pagespeedApiKey && (
+
+ )
+ }
+ >
+
+
+
+
+
+
+
+ >
);
};
diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx
index 7057eec0c..fa26c2b81 100644
--- a/client/src/Pages/StatusPage/StatusPages/index.jsx
+++ b/client/src/Pages/StatusPage/StatusPages/index.jsx
@@ -1,14 +1,11 @@
// Components
-import { Stack, Typography } from "@mui/material";
+import { Stack } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import Fallback from "../../../Components/Fallback";
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
-import GenericFallback from "../../../Components/GenericFallback";
import StatusPagesTable from "./Components/StatusPagesTable";
-import SkeletonLayout from "../../../Components/Skeletons/FullPage";
+import PageStateWrapper from "../../../Components/PageStateWrapper";
// Utils
import { useTheme } from "@emotion/react";
-import { useTranslation } from "react-i18next";
import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
const BREADCRUMBS = [{ name: `Status Pages`, path: "" }];
@@ -16,52 +13,30 @@ const BREADCRUMBS = [{ name: `Status Pages`, path: "" }];
const StatusPages = () => {
// Utils
const theme = useTheme();
- const { t } = useTranslation();
const isAdmin = useIsAdmin();
const [isLoading, networkError, statusPages] = useStatusPagesFetch();
- if (isLoading) {
- return ;
- }
-
- if (networkError === true) {
- return (
-
-
- {t("common.toasts.networkError")}
-
- {t("common.toasts.checkConnection")}
-
- );
- }
-
- if (!isLoading && typeof statusPages !== "undefined" && statusPages.length === 0) {
- return (
-
- );
- }
-
return (
-
-
-
+
-
-
+ items={statusPages}
+ type="statusPage"
+ fallbackLink="/status/uptime/create"
+ >
+
+
+
+
+
+
+ >
);
};
diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx
index 607092785..7f28c1cd7 100644
--- a/client/src/Pages/Uptime/Monitors/index.jsx
+++ b/client/src/Pages/Uptime/Monitors/index.jsx
@@ -10,16 +10,14 @@ import StatusBoxes from "./Components/StatusBoxes";
import UptimeDataTable from "./Components/UptimeDataTable";
import Pagination from "../../../Components/Table/TablePagination";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
-import Fallback from "../../../Components/Fallback";
-import GenericFallback from "../../../Components/GenericFallback";
import SearchComponent from "./Components/SearchComponent";
import Filter from "./Components/Filter";
+import PageStateWrapper from "../../../Components/PageStateWrapper";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
// MUI Components
-import { Stack, Box, Button, Typography } from "@mui/material";
-
+import { Stack, Box, Button } from "@mui/material";
// Utils
import { useState, useCallback, useEffect } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
@@ -153,93 +151,75 @@ const UptimeMonitors = () => {
}, [isSearching]);
const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading;
- if (networkError) {
- return (
-
-
- {t("common.toasts.networkError")}
-
- {t("common.toasts.checkConnection")}
-
- );
- }
- if (
- !isLoading &&
- (monitorsSummary?.totalMonitors === 0 ||
- typeof monitorsSummary?.totalMonitors === "undefined")
- ) {
- return (
-
- );
- }
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
);
};
diff --git a/client/vite.config.js b/client/vite.config.js
index a90b67462..a6b7152f1 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -5,12 +5,12 @@ import { execSync } from "child_process";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
- let version = "3.1.7";
+ let version = "3.1.8";
return {
base: "/",
plugins: [svgr(), react()],
- optimizeDeps: {
+ optimizeDeps: { Maybe this will be the straw th
include: ["@mui/material/Tooltip", "@emotion/styled"],
},
define: {
diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile
index 3a4aa755d..e47db9a74 100644
--- a/docker/dist-arm/server.Dockerfile
+++ b/docker/dist-arm/server.Dockerfile
@@ -3,10 +3,7 @@
# ---------------------
FROM node:24-slim AS frontend-build
-# Install ping
-RUN apt-get update \
- && apt-get install -y iputils-ping \
- && rm -rf /var/lib/apt/lists/*
+
WORKDIR /app/client
@@ -25,6 +22,11 @@ RUN npm run build
# ---------------------
FROM node:24-slim AS backend
+# Install ping
+RUN apt-get update \
+ && apt-get install -y iputils-ping \
+ && rm -rf /var/lib/apt/lists/*
+
WORKDIR /app/server
COPY server/package.json ./
diff --git a/docker/dist-mono/docker-compose.yaml b/docker/dist-mono/docker-compose.yaml
index 0e0b49445..9553757b9 100755
--- a/docker/dist-mono/docker-compose.yaml
+++ b/docker/dist-mono/docker-compose.yaml
@@ -1,6 +1,7 @@
services:
server:
image: ghcr.io/bluewave-labs/checkmate-backend-mono:latest
+ pull_policy: always
restart: always
ports:
- "52345:52345"
diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile
index 7304d9995..405f9e6ac 100644
--- a/docker/dist-mono/server.Dockerfile
+++ b/docker/dist-mono/server.Dockerfile
@@ -1,10 +1,5 @@
FROM node:20-slim AS frontend-build
-# Install ping
-RUN apt-get update \
- && apt-get install -y iputils-ping \
- && rm -rf /var/lib/apt/lists/*
-
WORKDIR /app/client
COPY client/package*.json ./
@@ -15,6 +10,11 @@ RUN npm run build
FROM node:20-slim AS app
+# Install ping
+RUN apt-get update \
+ && apt-get install -y iputils-ping \
+ && rm -rf /var/lib/apt/lists/*
+
WORKDIR /app/server
COPY server ./