entitlements FE

This commit is contained in:
Alex Holliday committed 2025-11-28 22:48:32 +00:00
1 parent c43241eb52
commit e600f293b0
23 files changed
+124 -88

No files matched your search

@@ -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 (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap={theme.spacing(6)}
>
<Tooltip
title={limitReached ? "Upgrade your plan to create more teams" : ""}
disableHoverListener={!limitReached}
>
<span>
<Button
loading={isLoading}
variant="contained"
color="accent"
disabled={limitReached}
onClick={() => navigate(path)}
>
{limitReached
? "Your plan's limit has been reached"
: label || t("createNew")}
</Button>
</span>
</Tooltip>
</Stack>
);
};
+1
View File
@@ -0,0 +1 @@
export { HeaderCreate } from "./HeaderCreate";
@@ -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 (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
gap={theme.spacing(6)}
>
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
{label || t("createNew")}
</Button>
</Stack>
);
};
-1
View File
@@ -1,4 +1,3 @@
export { HeaderCreate } from "./HeaderCreate";
export { HistogramStatus } from "./HistogramStatus";
export { ChartAvgResponse } from "./ChartAvgResponse";
export { HeaderRange } from "../common/HeaderRange";
@@ -1 +0,0 @@
export { HeaderCreate } from "@/components/monitors/HeaderCreate";
@@ -1 +0,0 @@
export { HeaderCreate } from "@/components/monitors/HeaderCreate";
@@ -25,6 +25,7 @@ export const useInitForm = ({
notificationChannels: initialData?.notificationChannels || [],
name: initialData?.name || "",
interval: humanInterval,
rejectUnauthorized: true,
};
return { defaults };
}, [initialData]);
@@ -24,6 +24,7 @@ export const useInitForm = ({
notificationChannels: initialData?.notificationChannels || [],
name: initialData?.name || "",
interval: humanInterval,
rejectUnauthorized: true,
};
return { defaults };
}, [initialData]);
-1
View File
@@ -58,7 +58,6 @@ const Login = () => {
navigate("/login");
return;
}
dispatch(setAuthenticated(true));
dispatch(setUser(user));
dispatch(setSelectedTeamId(user.teams[0]?.id || null));
+1 -1
View File
@@ -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 {
@@ -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."
/>
<HeaderCreate isLoading={loading} path="/infrastructure/create" />
<HeaderCreate
isLoading={loading}
path="/infrastructure/create"
entitlement="monitorsMax"
entitlementCount={count}
/>
<Stack direction={isSmall ? "column" : "row"} gap={theme.spacing(8)}>
<UpStatusBox n={upCount} />
<DownStatusBox n={downCount} />
+3 -1
View File
@@ -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}
/>
<Table
headers={headers}
@@ -39,7 +39,6 @@ export const MaintenanceForm = ({
handleSubmit,
control,
reset,
getValues,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(maintenanceSchema),
@@ -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}
/>
<Table
headers={headers}
@@ -5,8 +5,8 @@ import {
PausedStatusBox,
InfoBox,
} from "@/components/design-elements";
import { HeaderCreate } from "@/components/monitors";
import Stack from "@mui/material/Stack";
import { HeaderCreate } from "@/components/common";
import { Dialog } from "@/components/inputs";
import { PageSpeedMonitorTable } from "./PageSpeedMonitorTable";
@@ -86,7 +86,12 @@ const PageSpeedMonitorsPage = () => {
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."
/>
<HeaderCreate isLoading={loading} path="/pagespeed/create" />
<HeaderCreate
isLoading={loading}
path="/pagespeed/create"
entitlement="monitorsMax"
entitlementCount={count}
/>
<Stack direction={isSmall ? "column" : "row"} gap={theme.spacing(8)}>
<UpStatusBox n={upCount} />
<DownStatusBox n={downCount} />
+6 -36
View File
@@ -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<ITimeZone | null>(
null
);
useEffect(() => {
const tz = timezones.find((tz) => tz._id === timezone);
setSelectedTimezone(tz || null);
}, [timezone]);
const { post, loading: posting } = usePost<Partial<FormValues>, any>();
const { patch, loading: patching } = usePatch<Partial<FormValues>, any>();
+3 -1
View File
@@ -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}
/>
<Table
headers={headers}
+3 -1
View File
@@ -2,7 +2,7 @@ import { BasePage } from "@/components/design-elements";
import Typography from "@mui/material/Typography";
import Stack from "@mui/material/Stack";
import { Table } from "@/components/design-elements";
import { HeaderCreate } from "@/components/teams/HeaderCreate";
import { HeaderCreate } from "@/components/common";
import type { Header } from "@/components/design-elements/Table";
import { useNavigate } from "react-router";
@@ -77,6 +77,8 @@ const TeamDetailsPage = () => {
label="Add new team member"
path={`/teams/${id}/member/create`}
isLoading={false}
entitlement={null}
entitlementCount={teamMembers?.length || 0}
/>
<Typography variant="h2" marginTop={theme.spacing(4)}>
Team Members
+3 -1
View File
@@ -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}
/>
<Table
headers={headers}
+8 -2
View File
@@ -5,7 +5,8 @@ import {
PausedStatusBox,
InfoBox,
} from "@/components/design-elements";
import { HeaderCreate, HeaderFilter } from "@/components/monitors";
import { HeaderFilter } from "@/components/monitors";
import { HeaderCreate } from "@/components/common";
import Stack from "@mui/material/Stack";
import { Dialog } from "@/components/inputs";
import { MonitorTable } from "@/pages/uptime/MonitorTable";
@@ -124,7 +125,12 @@ const UptimeMonitors = () => {
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."
/>
<HeaderCreate isLoading={loading} path="/uptime/create" />
<HeaderCreate
isLoading={loading}
path="/uptime/create"
entitlement="monitorsMax"
entitlementCount={count}
/>
<Stack direction={isSmall ? "column" : "row"} gap={theme.spacing(8)}>
<UpStatusBox n={upCount} />
<DownStatusBox n={downCount} />
+12
View File
@@ -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;
}
+3
View File
@@ -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;
+2
View File
@@ -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({