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({