diff --git a/client/src/components/common/HeaderCreate.tsx b/client/src/components/common/HeaderCreate.tsx
new file mode 100644
index 000000000..112e2ae4e
--- /dev/null
+++ b/client/src/components/common/HeaderCreate.tsx
@@ -0,0 +1,60 @@
+import Stack from "@mui/material/Stack";
+import { Button } from "@/components/inputs";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router";
+import Tooltip from "@mui/material/Tooltip";
+import { useSelector } from "react-redux";
+import type { RootState } from "@/store";
+import { useMemo } from "react";
+import type { EntitlemenstKey } from "@/types/entitlements";
+export const HeaderCreate = ({
+ label,
+ isLoading,
+ path,
+ entitlement,
+ entitlementCount,
+}: {
+ label?: string;
+ isLoading: boolean;
+ path: string;
+ entitlement: EntitlemenstKey | null;
+ entitlementCount: number;
+}) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const ent = useSelector((s: RootState) => s.auth.user?.entitlements);
+ const limitReached = useMemo(() => {
+ if (!entitlement) return false;
+ if (!ent || typeof ent[entitlement] !== "number") return false;
+ return entitlementCount < ent[entitlement] ? false : true;
+ }, [ent, entitlementCount]);
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/client/src/components/common/index.tsx b/client/src/components/common/index.tsx
new file mode 100644
index 000000000..3b0acbd20
--- /dev/null
+++ b/client/src/components/common/index.tsx
@@ -0,0 +1 @@
+export { HeaderCreate } from "./HeaderCreate";
diff --git a/client/src/components/monitors/HeaderCreate.tsx b/client/src/components/monitors/HeaderCreate.tsx
deleted file mode 100644
index aa90ea73f..000000000
--- a/client/src/components/monitors/HeaderCreate.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import Stack from "@mui/material/Stack";
-import { Button } from "@/components/inputs";
-import { useTheme } from "@mui/material/styles";
-import { useTranslation } from "react-i18next";
-import { useNavigate } from "react-router";
-export const HeaderCreate = ({
- label,
- isLoading,
- path,
-}: {
- label?: string;
- isLoading: boolean;
- path: string;
-}) => {
- const theme = useTheme();
- const { t } = useTranslation();
- const navigate = useNavigate();
- return (
-
-
-
- );
-};
diff --git a/client/src/components/monitors/index.tsx b/client/src/components/monitors/index.tsx
index 69627fd3a..6003f60e8 100644
--- a/client/src/components/monitors/index.tsx
+++ b/client/src/components/monitors/index.tsx
@@ -1,4 +1,3 @@
-export { HeaderCreate } from "./HeaderCreate";
export { HistogramStatus } from "./HistogramStatus";
export { ChartAvgResponse } from "./ChartAvgResponse";
export { HeaderRange } from "../common/HeaderRange";
diff --git a/client/src/components/notification-channels/HeaderCreate.tsx b/client/src/components/notification-channels/HeaderCreate.tsx
deleted file mode 100644
index 543ada4d3..000000000
--- a/client/src/components/notification-channels/HeaderCreate.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { HeaderCreate } from "@/components/monitors/HeaderCreate";
diff --git a/client/src/components/teams/HeaderCreate.tsx b/client/src/components/teams/HeaderCreate.tsx
deleted file mode 100644
index 543ada4d3..000000000
--- a/client/src/components/teams/HeaderCreate.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { HeaderCreate } from "@/components/monitors/HeaderCreate";
diff --git a/client/src/hooks/forms/UseInitInfraForm.tsx b/client/src/hooks/forms/UseInitInfraForm.tsx
index d5f740f5b..033468447 100644
--- a/client/src/hooks/forms/UseInitInfraForm.tsx
+++ b/client/src/hooks/forms/UseInitInfraForm.tsx
@@ -25,6 +25,7 @@ export const useInitForm = ({
notificationChannels: initialData?.notificationChannels || [],
name: initialData?.name || "",
interval: humanInterval,
+ rejectUnauthorized: true,
};
return { defaults };
}, [initialData]);
diff --git a/client/src/hooks/forms/UseInitPageSpeedForm.tsx b/client/src/hooks/forms/UseInitPageSpeedForm.tsx
index c5438860e..7c080c608 100644
--- a/client/src/hooks/forms/UseInitPageSpeedForm.tsx
+++ b/client/src/hooks/forms/UseInitPageSpeedForm.tsx
@@ -24,6 +24,7 @@ export const useInitForm = ({
notificationChannels: initialData?.notificationChannels || [],
name: initialData?.name || "",
interval: humanInterval,
+ rejectUnauthorized: true,
};
return { defaults };
}, [initialData]);
diff --git a/client/src/pages/auth/Login.tsx b/client/src/pages/auth/Login.tsx
index 763d95b6e..297555291 100644
--- a/client/src/pages/auth/Login.tsx
+++ b/client/src/pages/auth/Login.tsx
@@ -58,7 +58,6 @@ const Login = () => {
navigate("/login");
return;
}
-
dispatch(setAuthenticated(true));
dispatch(setUser(user));
dispatch(setSelectedTeamId(user.teams[0]?.id || null));
diff --git a/client/src/pages/export/Export.tsx b/client/src/pages/export/Export.tsx
index e382472d4..e9464438f 100644
--- a/client/src/pages/export/Export.tsx
+++ b/client/src/pages/export/Export.tsx
@@ -50,7 +50,7 @@ const ExportPage = () => {
try {
const text = await file.text();
const json = JSON.parse(text);
- const res = await post("/monitors/import", { monitors: json });
+ await post("/monitors/import", { monitors: json });
} catch (error) {
console.error("Failed to parse JSON", error);
} finally {
diff --git a/client/src/pages/infrastructure/InfraMonitors.tsx b/client/src/pages/infrastructure/InfraMonitors.tsx
index 885b04bbd..2161c052d 100644
--- a/client/src/pages/infrastructure/InfraMonitors.tsx
+++ b/client/src/pages/infrastructure/InfraMonitors.tsx
@@ -5,7 +5,7 @@ import {
PausedStatusBox,
InfoBox,
} from "@/components/design-elements";
-import { HeaderCreate } from "@/components/monitors";
+import { HeaderCreate } from "@/components/common";
import Stack from "@mui/material/Stack";
import { Dialog } from "@/components/inputs";
import { InfraMonitorsTable } from "./InfraMonitorsTable";
@@ -86,7 +86,12 @@ const PageSpeedMonitorsPage = () => {
title="Server Health Monitoring"
description="Keep track of your servers' vital statistics including CPU usage, memory, disk space, and network performance. Receive alerts when resources run low."
/>
-
+
diff --git a/client/src/pages/maintenance/Maintenance.tsx b/client/src/pages/maintenance/Maintenance.tsx
index 40571a463..03bbff631 100644
--- a/client/src/pages/maintenance/Maintenance.tsx
+++ b/client/src/pages/maintenance/Maintenance.tsx
@@ -1,4 +1,4 @@
-import { HeaderCreate } from "@/components/notification-channels/HeaderCreate";
+import { HeaderCreate } from "@/components/common";
import { BasePageWithStates, InfoBox } from "@/components/design-elements";
import { ActionsMenu } from "@/components/actions-menu";
import { Table } from "@/components/design-elements";
@@ -134,6 +134,8 @@ const MaintenancePage = () => {
label={"Create a new maintenance window"}
isLoading={isValidating || isPausing || isDeleting}
path="/maintenance/create"
+ entitlement={null}
+ entitlementCount={maintenance.length}
/>
({
resolver: zodResolver(maintenanceSchema),
diff --git a/client/src/pages/notification-channels/NotificationChannels.tsx b/client/src/pages/notification-channels/NotificationChannels.tsx
index 5544323d3..516c475c8 100644
--- a/client/src/pages/notification-channels/NotificationChannels.tsx
+++ b/client/src/pages/notification-channels/NotificationChannels.tsx
@@ -1,4 +1,4 @@
-import { HeaderCreate } from "@/components/notification-channels/HeaderCreate";
+import { HeaderCreate } from "@/components/common";
import Typography from "@mui/material/Typography";
import { BasePageWithStates, InfoBox } from "@/components/design-elements";
import { Table } from "@/components/design-elements";
@@ -140,6 +140,8 @@ const NotificationChannelsPage = () => {
label={"Create a new notification channel"}
isLoading={isValidating || pausing || deleting}
path="/notification-channels/create"
+ entitlement={"notificationChannelsMax"}
+ entitlementCount={notificationChannels.length}
/>
{
title="Website Performance Tracking"
description="Measure how fast your websites load and identify performance bottlenecks. Track Core Web Vitals and get actionable insights to improve user experience."
/>
-
+
diff --git a/client/src/pages/settings/AdminSettings.tsx b/client/src/pages/settings/AdminSettings.tsx
index 977ebb96d..a201c5437 100644
--- a/client/src/pages/settings/AdminSettings.tsx
+++ b/client/src/pages/settings/AdminSettings.tsx
@@ -1,26 +1,10 @@
-import Stack from "@mui/material/Stack";
-import Box from "@mui/material/Box";
-import { BasePage, ConfigBox } from "@/components/design-elements";
-import {
- AutoComplete,
- Select,
- LanguageSelector,
- Button,
- RadioWithDescription,
- Dialog,
-} from "@/components/inputs";
-import MenuItem from "@mui/material/MenuItem";
-import Typography from "@mui/material/Typography";
-import { SettingsForm } from "@/pages/settings/SettingsForm";
-import DummyChart from "@/pages/settings/DummyChart";
+import { BasePage } from "@/components/design-elements";
-import { useTheme } from "@mui/material/styles";
-import { useEffect, useMemo, useState } from "react";
-import { useTranslation } from "react-i18next";
-import { setTimezone, setMode, setChartType } from "@/features/uiSlice";
-import { useAppSelector, useAppDispatch } from "@/hooks/AppHooks";
-import timezones from "@/utils/timezones.json";
-import type { ITimeZone } from "@/types/timezone";
+import { SettingsForm } from "@/pages/settings/SettingsForm";
+
+import { useMemo } from "react";
+import {} from "react-i18next";
+import { useAppSelector } from "@/hooks/AppHooks";
import { useGet, usePatch, usePost } from "@/hooks/UseApi";
import type { ApiResponse } from "@/hooks/UseApi";
import { z } from "zod";
@@ -34,20 +18,6 @@ const AdminSettingsPage = () => {
const orgPermissions = user?.org?.permissions || [];
const hasMaster = orgPermissions.includes("master");
- const theme = useTheme();
- const { t } = useTranslation();
- const dispatch = useAppDispatch();
- const timezone = useAppSelector((state) => state.ui.timezone);
- const mode = useAppSelector((state) => state.ui.mode);
- const chartType = useAppSelector((state) => state.ui.chartType);
- const [selectedTimezone, setSelectedTimezone] = useState(
- null
- );
- useEffect(() => {
- const tz = timezones.find((tz) => tz._id === timezone);
- setSelectedTimezone(tz || null);
- }, [timezone]);
-
const { post, loading: posting } = usePost, any>();
const { patch, loading: patching } = usePatch, any>();
diff --git a/client/src/pages/status-page/StatusPages.tsx b/client/src/pages/status-page/StatusPages.tsx
index 03fc5ed5b..737f3f854 100644
--- a/client/src/pages/status-page/StatusPages.tsx
+++ b/client/src/pages/status-page/StatusPages.tsx
@@ -1,4 +1,4 @@
-import { HeaderCreate } from "@/components/notification-channels/HeaderCreate";
+import { HeaderCreate } from "@/components/common";
import Typography from "@mui/material/Typography";
import { BasePageWithStates, InfoBox } from "@/components/design-elements";
import { Table } from "@/components/design-elements";
@@ -124,6 +124,8 @@ const StatusPages = () => {
label={"Create a new Status Page"}
isLoading={isValidating || deleting}
path="/status-pages/create"
+ entitlement="statusPagesMax"
+ entitlementCount={statusPages.length}
/>
{
label="Add new team member"
path={`/teams/${id}/member/create`}
isLoading={false}
+ entitlement={null}
+ entitlementCount={teamMembers?.length || 0}
/>
Team Members
diff --git a/client/src/pages/teams/Teams.tsx b/client/src/pages/teams/Teams.tsx
index 80c75afcd..945766b44 100644
--- a/client/src/pages/teams/Teams.tsx
+++ b/client/src/pages/teams/Teams.tsx
@@ -1,6 +1,6 @@
import { BasePage, InfoBox } from "@/components/design-elements";
import { Table } from "@/components/design-elements";
-import { HeaderCreate } from "@/components/teams/HeaderCreate";
+import { HeaderCreate } from "@/components/common";
import { ActionsMenu } from "@/components/actions-menu";
import { Dialog } from "@/components/inputs";
@@ -93,6 +93,8 @@ const TeamsPage = () => {
label="Create new team"
isLoading={loading}
path="/teams/create"
+ entitlement="teamsMax"
+ entitlementCount={teams.length}
/>
{
title="Website & API Uptime Monitoring"
description="Monitor your websites and APIs to ensure they're always accessible. Get instant alerts when your services go down and track uptime history over time."
/>
-
+
diff --git a/client/src/types/entitlements.ts b/client/src/types/entitlements.ts
new file mode 100644
index 000000000..5241e47f2
--- /dev/null
+++ b/client/src/types/entitlements.ts
@@ -0,0 +1,12 @@
+export type EntitlemenstKey = keyof Entitlements;
+
+export interface Entitlements {
+ plan: string;
+ monitorsMax: number;
+ notificationChannelsMax: number;
+ statusPagesMax: number;
+ checksIntervalMsMin: number;
+ teamsMax: number;
+ notificationsEnabled: boolean;
+ retentionDays: number;
+}
diff --git a/client/src/types/user.ts b/client/src/types/user.ts
index 81e362786..3205f2c51 100644
--- a/client/src/types/user.ts
+++ b/client/src/types/user.ts
@@ -1,3 +1,5 @@
+import type { Entitlements } from "./entitlements";
+
export interface IUser {
id: string;
email: string;
@@ -12,6 +14,7 @@ export interface IUser {
name: string;
permissions: string;
}[];
+ entitlements: Entitlements;
}
export interface IMongoUser {
_id: string;
diff --git a/client/src/validation/zod.ts b/client/src/validation/zod.ts
index 769d109d7..9b1056fd9 100644
--- a/client/src/validation/zod.ts
+++ b/client/src/validation/zod.ts
@@ -144,6 +144,7 @@ export const monitorSchemaPageSpeed = z.object({
notificationChannels: z.array(z.string()).optional().default([]),
name: z.string().min(1, "Display name is required"),
interval: durationSchemaPageSpeed,
+ rejectUnauthorized: z.boolean().default(true),
});
export const monitorSchemaInfra = z.object({
type: z.string().min(1, "You must select an option"),
@@ -156,6 +157,7 @@ export const monitorSchemaInfra = z.object({
notificationChannels: z.array(z.string()).optional().default([]),
name: z.string().min(1, "Display name is required"),
interval: durationSchemaInfra,
+ rejectUnauthorized: z.boolean().default(true),
});
export const teamSchema = z.object({