refactor and add tiers

This commit is contained in:
miloschwartz
2026-02-10 10:27:10 -08:00
committed by Owen
parent 911b5e6814
commit da8b620c75
20 changed files with 201 additions and 78 deletions

View File

@@ -11,6 +11,7 @@ import type { GetOrgResponse } from "@server/routers/org";
import type { ListRolesResponse } from "@server/routers/role"; import type { ListRolesResponse } from "@server/routers/role";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export interface ApprovalFeedPageProps { export interface ApprovalFeedPageProps {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -29,10 +30,9 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
// Fetch roles to check if approvals are enabled // Fetch roles to check if approvals are enabled
let hasApprovalsEnabled = false; let hasApprovalsEnabled = false;
const rolesRes = await internal const rolesRes = await internal
.get<AxiosResponse<ListRolesResponse>>( .get<
`/org/${params.orgId}/roles`, AxiosResponse<ListRolesResponse>
await authCookieHeader() >(`/org/${params.orgId}/roles`, await authCookieHeader())
)
.catch((e) => {}); .catch((e) => {});
if (rolesRes && rolesRes.status === 200) { if (rolesRes && rolesRes.status === 200) {
@@ -52,7 +52,7 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
<ApprovalsBanner /> <ApprovalsBanner />
<PaidFeaturesAlert /> <PaidFeaturesAlert tiers={tierMatrix.deviceApprovals} />
<OrgProvider org={org}> <OrgProvider org={org}>
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">

View File

@@ -31,7 +31,6 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon, ExternalLink } from "lucide-react"; import { InfoIcon, ExternalLink } from "lucide-react";
import { import {
@@ -41,12 +40,13 @@ import {
InfoSectionTitle InfoSectionTitle
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import IdpTypeBadge from "@app/components/IdpTypeBadge"; import IdpTypeBadge from "@app/components/IdpTypeBadge";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget"; import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export default function GeneralPage() { export default function GeneralPage() {
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -60,7 +60,6 @@ export default function GeneralPage() {
"role" | "expression" "role" | "expression"
>("role"); >("role");
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc"); const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
const { isUnlocked } = useLicenseStatusContext();
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`; const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
const [redirectUrl, setRedirectUrl] = useState( const [redirectUrl, setRedirectUrl] = useState(
@@ -499,6 +498,10 @@ export default function GeneralPage() {
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<PaidFeaturesAlert
tiers={tierMatrix.autoProvisioning}
/>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget"; import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { import {
SettingsContainer, SettingsContainer,
SettingsSection, SettingsSection,
@@ -31,6 +32,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { InfoIcon } from "lucide-react"; import { InfoIcon } from "lucide-react";
@@ -50,7 +52,6 @@ export default function Page() {
const [roleMappingMode, setRoleMappingMode] = useState< const [roleMappingMode, setRoleMappingMode] = useState<
"role" | "expression" "role" | "expression"
>("role"); >("role");
const { isUnlocked } = useLicenseStatusContext();
const t = useTranslations(); const t = useTranslations();
const { isPaidUser } = usePaidStatus(); const { isPaidUser } = usePaidStatus();
@@ -363,6 +364,9 @@ export default function Page() {
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<PaidFeaturesAlert
tiers={tierMatrix.autoProvisioning}
/>
<Form {...form}> <Form {...form}>
<form <form
className="space-y-4" className="space-y-4"
@@ -808,7 +812,7 @@ export default function Page() {
</Button> </Button>
<Button <Button
type="submit" type="submit"
disabled={createLoading || !isPaidUser} disabled={createLoading || !isPaidUser(tierMatrix.orgOidc)}
loading={createLoading} loading={createLoading}
onClick={() => { onClick={() => {
// log any issues with the form // log any issues with the form

View File

@@ -5,6 +5,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import IdpTable, { IdpRow } from "@app/components/OrgIdpTable"; import IdpTable, { IdpRow } from "@app/components/OrgIdpTable";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type OrgIdpPageProps = { type OrgIdpPageProps = {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -35,7 +36,7 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
description={t("idpManageDescription")} description={t("idpManageDescription")}
/> />
<PaidFeaturesAlert /> <PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
<IdpTable idps={idps} orgId={params.orgId} /> <IdpTable idps={idps} orgId={params.orgId} />
</> </>

View File

@@ -129,7 +129,9 @@ export default function CredentialsPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<InfoSections cols={3}> <InfoSections cols={3}>
<InfoSection> <InfoSection>
@@ -194,7 +196,9 @@ export default function CredentialsPage() {
setShouldDisconnect(false); setShouldDisconnect(false);
setModalOpen(true); setModalOpen(true);
}} }}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
> >
{t("regenerateCredentialsButton")} {t("regenerateCredentialsButton")}
</Button> </Button>
@@ -203,7 +207,9 @@ export default function CredentialsPage() {
setShouldDisconnect(true); setShouldDisconnect(true);
setModalOpen(true); setModalOpen(true);
}} }}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
> >
{t("remoteExitNodeRegenerateAndDisconnect")} {t("remoteExitNodeRegenerateAndDisconnect")}
</Button> </Button>

View File

@@ -119,7 +119,9 @@ export default function CredentialsPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<InfoSections cols={3}> <InfoSections cols={3}>
<InfoSection> <InfoSection>
@@ -180,7 +182,9 @@ export default function CredentialsPage() {
setShouldDisconnect(false); setShouldDisconnect(false);
setModalOpen(true); setModalOpen(true);
}} }}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
> >
{t("regenerateCredentialsButton")} {t("regenerateCredentialsButton")}
</Button> </Button>
@@ -189,7 +193,9 @@ export default function CredentialsPage() {
setShouldDisconnect(true); setShouldDisconnect(true);
setModalOpen(true); setModalOpen(true);
}} }}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
> >
{t("clientRegenerateAndDisconnect")} {t("clientRegenerateAndDisconnect")}
</Button> </Button>

View File

@@ -155,13 +155,11 @@ export default function GeneralPage() {
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const { env } = useEnvContext(); const { env } = useEnvContext();
const showApprovalFeatures = build !== "oss" && isPaidUser; const showApprovalFeatures =
build !== "oss" && isPaidUser(tierMatrix.deviceApprovals);
const formatPostureValue = ( const formatPostureValue = (value: boolean | null | undefined | "-") => {
value: boolean | null | undefined | "-" if (value === null || value === undefined || value === "-") return "-";
) => {
if (value === null || value === undefined || value === "-")
return "-";
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{value ? ( {value ? (
@@ -584,7 +582,8 @@ export default function GeneralPage() {
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<PaidFeaturesAlert /> <PaidFeaturesAlert tiers={tierMatrix.devicePosture} />
{client.posture && {client.posture &&
Object.keys(client.posture).length > 0 ? ( Object.keys(client.posture).length > 0 ? (
<> <>
@@ -598,7 +597,9 @@ export default function GeneralPage() {
{t("biometricsEnabled")} {t("biometricsEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.biometricsEnabled .biometricsEnabled
@@ -616,7 +617,9 @@ export default function GeneralPage() {
{t("diskEncrypted")} {t("diskEncrypted")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.diskEncrypted .diskEncrypted
@@ -634,7 +637,9 @@ export default function GeneralPage() {
{t("firewallEnabled")} {t("firewallEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.firewallEnabled .firewallEnabled
@@ -653,7 +658,9 @@ export default function GeneralPage() {
{t("autoUpdatesEnabled")} {t("autoUpdatesEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.autoUpdatesEnabled .autoUpdatesEnabled
@@ -671,7 +678,9 @@ export default function GeneralPage() {
{t("tpmAvailable")} {t("tpmAvailable")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.tpmAvailable .tpmAvailable
@@ -693,7 +702,9 @@ export default function GeneralPage() {
)} )}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.windowsAntivirusEnabled .windowsAntivirusEnabled
@@ -711,7 +722,9 @@ export default function GeneralPage() {
{t("macosSipEnabled")} {t("macosSipEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.macosSipEnabled .macosSipEnabled
@@ -733,7 +746,9 @@ export default function GeneralPage() {
)} )}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.macosGatekeeperEnabled .macosGatekeeperEnabled
@@ -755,7 +770,9 @@ export default function GeneralPage() {
)} )}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.macosFirewallStealthMode .macosFirewallStealthMode
@@ -774,7 +791,9 @@ export default function GeneralPage() {
{t("linuxAppArmorEnabled")} {t("linuxAppArmorEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.linuxAppArmorEnabled .linuxAppArmorEnabled
@@ -793,7 +812,9 @@ export default function GeneralPage() {
{t("linuxSELinuxEnabled")} {t("linuxSELinuxEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser(tierMatrix.devicePosture) {isPaidUser(
tierMatrix.devicePosture
)
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.linuxSELinuxEnabled .linuxSELinuxEnabled

View File

@@ -43,6 +43,8 @@ import { SwitchInput } from "@app/components/SwitchInput";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
import type { OrgContextType } from "@app/contexts/orgContext"; import type { OrgContextType } from "@app/contexts/orgContext";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { isAppPageRouteDefinition } from "next/dist/server/route-definitions/app-page-route-definition";
// Session length options in hours // Session length options in hours
const SESSION_LENGTH_OPTIONS = [ const SESSION_LENGTH_OPTIONS = [
@@ -244,13 +246,17 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
{!env.flags.disableEnterpriseFeatures && ( {!env.flags.disableEnterpriseFeatures && (
<> <>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.accessLogs}
/>
<FormField <FormField
control={form.control} control={form.control}
name="settingsLogRetentionDaysAccess" name="settingsLogRetentionDaysAccess"
render={({ field }) => { render={({ field }) => {
const isDisabled = !isPaidUser; const isDisabled = !isPaidUser(
tierMatrix.accessLogs
);
return ( return (
<FormItem> <FormItem>
@@ -316,7 +322,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
control={form.control} control={form.control}
name="settingsLogRetentionDaysAction" name="settingsLogRetentionDaysAction"
render={({ field }) => { render={({ field }) => {
const isDisabled = !isPaidUser; const isDisabled = !isPaidUser(
tierMatrix.actionLogs
);
return ( return (
<FormItem> <FormItem>
@@ -521,12 +529,17 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
id="security-settings-section-form" id="security-settings-section-form"
className="space-y-4" className="space-y-4"
> >
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.twoFactorEnforcement}
/>
<FormField <FormField
control={form.control} control={form.control}
name="requireTwoFactor" name="requireTwoFactor"
render={({ field }) => { render={({ field }) => {
const isDisabled = !isPaidUser; const isDisabled = !isPaidUser(
tierMatrix.twoFactorEnforcement
);
return ( return (
<FormItem className="col-span-2"> <FormItem className="col-span-2">
@@ -573,7 +586,9 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
control={form.control} control={form.control}
name="maxSessionLengthHours" name="maxSessionLengthHours"
render={({ field }) => { render={({ field }) => {
const isDisabled = !isPaidUser; const isDisabled = !isPaidUser(
tierMatrix.sessionDurationPolicies
);
return ( return (
<FormItem className="col-span-2"> <FormItem className="col-span-2">
@@ -653,7 +668,9 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
control={form.control} control={form.control}
name="passwordExpiryDays" name="passwordExpiryDays"
render={({ field }) => { render={({ field }) => {
const isDisabled = !isPaidUser; const isDisabled = !isPaidUser(
tierMatrix.passwordExpirationPolicies
);
return ( return (
<FormItem className="col-span-2"> <FormItem className="col-span-2">
@@ -739,7 +756,12 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) {
type="submit" type="submit"
form="security-settings-section-form" form="security-settings-section-form"
loading={loadingSave} loading={loadingSave}
disabled={loadingSave || !isPaidUser} disabled={
loadingSave ||
!isPaidUser(tierMatrix.twoFactorEnforcement) ||
!isPaidUser(tierMatrix.sessionDurationPolicies) ||
!isPaidUser(tierMatrix.passwordExpirationPolicies)
}
> >
{t("saveSettings")} {t("saveSettings")}
</Button> </Button>

View File

@@ -608,7 +608,7 @@ export default function GeneralPage() {
description={t("accessLogsDescription")} description={t("accessLogsDescription")}
/> />
<PaidFeaturesAlert /> <PaidFeaturesAlert tiers={tierMatrix.accessLogs} />
<LogDataTable <LogDataTable
columns={columns} columns={columns}
@@ -618,6 +618,9 @@ export default function GeneralPage() {
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
onExport={() => startTransition(exportData)} onExport={() => startTransition(exportData)}
isExporting={isExporting} isExporting={isExporting}
isExportDisabled={
!isPaidUser(tierMatrix.accessLogs) || build === "oss"
}
onDateRangeChange={handleDateRangeChange} onDateRangeChange={handleDateRangeChange}
dateRange={{ dateRange={{
start: dateRange.startDate, start: dateRange.startDate,

View File

@@ -461,7 +461,7 @@ export default function GeneralPage() {
description={t("actionLogsDescription")} description={t("actionLogsDescription")}
/> />
<PaidFeaturesAlert /> <PaidFeaturesAlert tiers={tierMatrix.actionLogs} />
<LogDataTable <LogDataTable
columns={columns} columns={columns}
@@ -472,6 +472,9 @@ export default function GeneralPage() {
onRefresh={refreshData} onRefresh={refreshData}
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
onExport={() => startTransition(exportData)} onExport={() => startTransition(exportData)}
isExportDisabled={
!isPaidUser(tierMatrix.logExport) || build === "oss"
}
isExporting={isExporting} isExporting={isExporting}
onDateRangeChange={handleDateRangeChange} onDateRangeChange={handleDateRangeChange}
dateRange={{ dateRange={{

View File

@@ -178,13 +178,15 @@ function MaintenanceSectionForm({
className="space-y-4" className="space-y-4"
id="maintenance-settings-form" id="maintenance-settings-form"
> >
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.maintencePage}
/>
<FormField <FormField
control={maintenanceForm.control} control={maintenanceForm.control}
name="maintenanceModeEnabled" name="maintenanceModeEnabled"
render={({ field }) => { render={({ field }) => {
const isDisabled = const isDisabled =
isPaidUser(tierMatrix.maintencePage) || !isPaidUser(tierMatrix.maintencePage) ||
resource.http === false; resource.http === false;
return ( return (
@@ -251,7 +253,11 @@ function MaintenanceSectionForm({
defaultValue={ defaultValue={
field.value field.value
} }
disabled={isPaidUser(tierMatrix.maintencePage)} disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
className="flex flex-col space-y-1" className="flex flex-col space-y-1"
> >
<FormItem className="flex items-start space-x-3 space-y-0"> <FormItem className="flex items-start space-x-3 space-y-0">
@@ -324,7 +330,11 @@ function MaintenanceSectionForm({
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
disabled={isPaidUser(tierMatrix.maintencePage)} disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder="We'll be back soon!" placeholder="We'll be back soon!"
/> />
</FormControl> </FormControl>
@@ -350,7 +360,11 @@ function MaintenanceSectionForm({
<Textarea <Textarea
{...field} {...field}
rows={4} rows={4}
disabled={isPaidUser(tierMatrix.maintencePage)} disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder={t( placeholder={t(
"maintenancePageMessagePlaceholder" "maintenancePageMessagePlaceholder"
)} )}
@@ -379,7 +393,11 @@ function MaintenanceSectionForm({
<FormControl> <FormControl>
<Input <Input
{...field} {...field}
disabled={isPaidUser(tierMatrix.maintencePage)} disabled={
!isPaidUser(
tierMatrix.maintencePage
)
}
placeholder={t( placeholder={t(
"maintenanceTime" "maintenanceTime"
)} )}
@@ -405,7 +423,10 @@ function MaintenanceSectionForm({
<Button <Button
type="submit" type="submit"
loading={maintenanceSaveLoading} loading={maintenanceSaveLoading}
disabled={maintenanceSaveLoading || !isPaidUser} disabled={
maintenanceSaveLoading ||
!isPaidUser(tierMatrix.maintencePage)
}
form="maintenance-settings-form" form="maintenance-settings-form"
> >
{t("saveSettings")} {t("saveSettings")}

View File

@@ -196,7 +196,9 @@ export default function CredentialsPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<SettingsSectionBody> <SettingsSectionBody>
<InfoSections cols={3}> <InfoSections cols={3}>
@@ -268,7 +270,11 @@ export default function CredentialsPage() {
setShouldDisconnect(false); setShouldDisconnect(false);
setModalOpen(true); setModalOpen(true);
}} }}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(
tierMatrix.rotateCredentials
)
}
> >
{t("regenerateCredentialsButton")} {t("regenerateCredentialsButton")}
</Button> </Button>
@@ -277,7 +283,11 @@ export default function CredentialsPage() {
setShouldDisconnect(true); setShouldDisconnect(true);
setModalOpen(true); setModalOpen(true);
}} }}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(
tierMatrix.rotateCredentials
)
}
> >
{t("siteRegenerateAndDisconnect")} {t("siteRegenerateAndDisconnect")}
</Button> </Button>
@@ -304,7 +314,9 @@ export default function CredentialsPage() {
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<SettingsSectionBody> <SettingsSectionBody>
{!loadingDefaults && ( {!loadingDefaults && (
@@ -378,7 +390,11 @@ export default function CredentialsPage() {
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button
onClick={() => setModalOpen(true)} onClick={() => setModalOpen(true)}
disabled={isPaidUser(tierMatrix.rotateCredentials)} disabled={
!isPaidUser(
tierMatrix.rotateCredentials
)
}
> >
{t("siteRegenerateAndDisconnect")} {t("siteRegenerateAndDisconnect")}
</Button> </Button>

View File

@@ -35,6 +35,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { build } from "@server/build"; import { build } from "@server/build";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export type AuthPageCustomizationProps = { export type AuthPageCustomizationProps = {
orgId: string; orgId: string;
@@ -139,7 +140,7 @@ export default function AuthPageBrandingForm({
`Choose your preferred authentication method for {{resourceName}}`, `Choose your preferred authentication method for {{resourceName}}`,
primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color
}, },
disabled: !isPaidUser disabled: !isPaidUser(tierMatrix.loginPageBranding)
}); });
async function updateBranding() { async function updateBranding() {
@@ -221,7 +222,9 @@ export default function AuthPageBrandingForm({
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.loginPageBranding}
/>
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -285,7 +285,7 @@ function AuthPageSettings({
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<PaidFeaturesAlert /> <PaidFeaturesAlert tiers={tierMatrix.loginPageDomain} />
<Form {...form}> <Form {...form}>
<form <form

View File

@@ -20,6 +20,8 @@ import {
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Control, FieldValues, Path } from "react-hook-form"; import { Control, FieldValues, Path } from "react-hook-form";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type Role = { type Role = {
roleId: number; roleId: number;
@@ -49,6 +51,8 @@ export default function AutoProvisionConfigWidget<T extends FieldValues>({
}: AutoProvisionConfigWidgetProps<T>) { }: AutoProvisionConfigWidgetProps<T>) {
const t = useTranslations(); const t = useTranslations();
const { isPaidUser } = usePaidStatus();
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="mb-4"> <div className="mb-4">
@@ -57,6 +61,7 @@ export default function AutoProvisionConfigWidget<T extends FieldValues>({
label={t("idpAutoProvisionUsers")} label={t("idpAutoProvisionUsers")}
defaultChecked={autoProvision} defaultChecked={autoProvision}
onCheckedChange={onAutoProvisionChange} onCheckedChange={onAutoProvisionChange}
disabled={!isPaidUser(tierMatrix.autoProvisioning)}
/> />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{t("idpAutoProvisionUsersDescription")} {t("idpAutoProvisionUsersDescription")}

View File

@@ -36,6 +36,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox"; import { CheckboxWithLabel } from "./ui/checkbox";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type CreateRoleFormProps = { type CreateRoleFormProps = {
open: boolean; open: boolean;
@@ -164,7 +165,9 @@ export default function CreateRoleForm({
{!env.flags.disableEnterpriseFeatures && ( {!env.flags.disableEnterpriseFeatures && (
<> <>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.deviceApprovals}
/>
<FormField <FormField
control={form.control} control={form.control}
@@ -175,7 +178,9 @@ export default function CreateRoleForm({
<CheckboxWithLabel <CheckboxWithLabel
{...field} {...field}
disabled={ disabled={
!isPaidUser !isPaidUser(
tierMatrix.deviceApprovals
)
} }
value="on" value="on"
checked={form.watch( checked={form.watch(

View File

@@ -42,6 +42,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox"; import { CheckboxWithLabel } from "./ui/checkbox";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type CreateRoleFormProps = { type CreateRoleFormProps = {
role: Role; role: Role;
@@ -172,7 +173,9 @@ export default function EditRoleForm({
{!env.flags.disableEnterpriseFeatures && ( {!env.flags.disableEnterpriseFeatures && (
<> <>
<PaidFeaturesAlert /> <PaidFeaturesAlert
tiers={tierMatrix.deviceApprovals}
/>
<FormField <FormField
control={form.control} control={form.control}
@@ -183,7 +186,9 @@ export default function EditRoleForm({
<CheckboxWithLabel <CheckboxWithLabel
{...field} {...field}
disabled={ disabled={
!isPaidUser !isPaidUser(
tierMatrix.deviceApprovals
)
} }
value="on" value="on"
checked={form.watch( checked={form.watch(

View File

@@ -120,6 +120,7 @@ type DataTableProps<TData, TValue> = {
// Row expansion props // Row expansion props
expandable?: boolean; expandable?: boolean;
renderExpandedRow?: (row: TData) => React.ReactNode; renderExpandedRow?: (row: TData) => React.ReactNode;
isExportDisabled?: boolean;
}; };
export function LogDataTable<TData, TValue>({ export function LogDataTable<TData, TValue>({
@@ -145,7 +146,8 @@ export function LogDataTable<TData, TValue>({
isLoading = false, isLoading = false,
expandable = false, expandable = false,
disabled = false, disabled = false,
renderExpandedRow renderExpandedRow,
isExportDisabled
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const t = useTranslations(); const t = useTranslations();
@@ -403,7 +405,7 @@ export function LogDataTable<TData, TValue>({
onClick={() => onClick={() =>
!disabled && onExport() !disabled && onExport()
} }
disabled={isExporting || disabled} disabled={isExporting || disabled || isExportDisabled}
> >
{isExporting ? ( {isExporting ? (
<Loader className="mr-2 size-4 animate-spin" /> <Loader className="mr-2 size-4 animate-spin" />

View File

@@ -1,4 +1,5 @@
"use client"; "use client";
import { Card, CardContent } from "@app/components/ui/card"; import { Card, CardContent } from "@app/components/ui/card";
import { build } from "@server/build"; import { build } from "@server/build";
import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { usePaidStatus } from "@app/hooks/usePaidStatus";
@@ -6,6 +7,7 @@ import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Tier } from "@server/types/Tiers";
const bannerClassName = const bannerClassName =
"mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden"; "mb-6 border-primary/30 bg-linear-to-br from-primary/10 via-background to-background overflow-hidden";
@@ -13,7 +15,11 @@ const bannerContentClassName = "py-3 px-4";
const bannerRowClassName = const bannerRowClassName =
"flex items-center gap-2.5 text-sm text-muted-foreground"; "flex items-center gap-2.5 text-sm text-muted-foreground";
export function PaidFeaturesAlert() { type Props = {
tiers: Tier[];
};
export function PaidFeaturesAlert({ tiers }: Props) {
const t = useTranslations(); const t = useTranslations();
const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus(); const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus();
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -24,7 +30,7 @@ export function PaidFeaturesAlert() {
return ( return (
<> <>
{build === "saas" && !hasSaasSubscription ? ( {build === "saas" && !hasSaasSubscription(tiers) ? (
<Card className={bannerClassName}> <Card className={bannerClassName}>
<CardContent className={bannerContentClassName}> <CardContent className={bannerContentClassName}>
<div className={bannerRowClassName}> <div className={bannerRowClassName}>

View File

@@ -5,16 +5,12 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm";
import { RolesDataTable } from "@app/components/RolesDataTable"; import { RolesDataTable } from "@app/components/RolesDataTable";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ExtendedColumnDef } from "@app/components/ui/data-table"; import { ExtendedColumnDef } from "@app/components/ui/data-table";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { Role } from "@server/db"; import { Role } from "@server/db";
import { ArrowRight, ArrowUpDown, Link, MoreHorizontal } from "lucide-react"; import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuTrigger, DropdownMenuTrigger,
@@ -38,11 +34,6 @@ export default function UsersTable({ roles }: RolesTableProps) {
const [roleToRemove, setRoleToRemove] = useState<RoleRow | null>(null); const [roleToRemove, setRoleToRemove] = useState<RoleRow | null>(null);
const api = createApiClient(useEnvContext());
const { org } = useOrgContext();
const { isPaidUser } = usePaidStatus();
const t = useTranslations(); const t = useTranslations();
const [isRefreshing, startTransition] = useTransition(); const [isRefreshing, startTransition] = useTransition();