refactor and add tiers

This commit is contained in:
miloschwartz
2026-02-10 10:27:10 -08:00
parent 69c2212ea0
commit d27482e812
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 { AxiosResponse } from "axios";
import { getTranslations } from "next-intl/server";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export interface ApprovalFeedPageProps {
params: Promise<{ orgId: string }>;
@@ -29,10 +30,9 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
// Fetch roles to check if approvals are enabled
let hasApprovalsEnabled = false;
const rolesRes = await internal
.get<AxiosResponse<ListRolesResponse>>(
`/org/${params.orgId}/roles`,
await authCookieHeader()
)
.get<
AxiosResponse<ListRolesResponse>
>(`/org/${params.orgId}/roles`, await authCookieHeader())
.catch((e) => {});
if (rolesRes && rolesRes.status === 200) {
@@ -52,7 +52,7 @@ export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
<ApprovalsBanner />
<PaidFeaturesAlert />
<PaidFeaturesAlert tiers={tierMatrix.deviceApprovals} />
<OrgProvider org={org}>
<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 { useEnvContext } from "@app/hooks/useEnvContext";
import { useState, useEffect } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon, ExternalLink } from "lucide-react";
import {
@@ -41,12 +40,13 @@ import {
InfoSectionTitle
} from "@app/components/InfoSection";
import CopyToClipboard from "@app/components/CopyToClipboard";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import IdpTypeBadge from "@app/components/IdpTypeBadge";
import { useTranslations } from "next-intl";
import { AxiosResponse } from "axios";
import { ListRolesResponse } from "@server/routers/role";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
export default function GeneralPage() {
const { env } = useEnvContext();
@@ -60,7 +60,6 @@ export default function GeneralPage() {
"role" | "expression"
>("role");
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
const { isUnlocked } = useLicenseStatusContext();
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
const [redirectUrl, setRedirectUrl] = useState(
@@ -499,6 +498,10 @@ export default function GeneralPage() {
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<PaidFeaturesAlert
tiers={tierMatrix.autoProvisioning}
/>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -1,6 +1,7 @@
"use client";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import {
SettingsContainer,
SettingsSection,
@@ -31,6 +32,7 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { ListRolesResponse } from "@server/routers/role";
import { AxiosResponse } from "axios";
import { InfoIcon } from "lucide-react";
@@ -50,7 +52,6 @@ export default function Page() {
const [roleMappingMode, setRoleMappingMode] = useState<
"role" | "expression"
>("role");
const { isUnlocked } = useLicenseStatusContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
@@ -363,6 +364,9 @@ export default function Page() {
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<PaidFeaturesAlert
tiers={tierMatrix.autoProvisioning}
/>
<Form {...form}>
<form
className="space-y-4"
@@ -808,7 +812,7 @@ export default function Page() {
</Button>
<Button
type="submit"
disabled={createLoading || !isPaidUser}
disabled={createLoading || !isPaidUser(tierMatrix.orgOidc)}
loading={createLoading}
onClick={() => {
// 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 { getTranslations } from "next-intl/server";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type OrgIdpPageProps = {
params: Promise<{ orgId: string }>;
@@ -35,7 +36,7 @@ export default async function OrgIdpPage(props: OrgIdpPageProps) {
description={t("idpManageDescription")}
/>
<PaidFeaturesAlert />
<PaidFeaturesAlert tiers={tierMatrix.orgOidc} />
<IdpTable idps={idps} orgId={params.orgId} />
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
"use client";
import { Card, CardContent } from "@app/components/ui/card";
import { build } from "@server/build";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
@@ -6,6 +7,7 @@ import { ExternalLink, KeyRound, Sparkles } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Tier } from "@server/types/Tiers";
const bannerClassName =
"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 =
"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 { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus();
const { env } = useEnvContext();
@@ -24,7 +30,7 @@ export function PaidFeaturesAlert() {
return (
<>
{build === "saas" && !hasSaasSubscription ? (
{build === "saas" && !hasSaasSubscription(tiers) ? (
<Card className={bannerClassName}>
<CardContent className={bannerContentClassName}>
<div className={bannerRowClassName}>

View File

@@ -5,16 +5,12 @@ import DeleteRoleForm from "@app/components/DeleteRoleForm";
import { RolesDataTable } from "@app/components/RolesDataTable";
import { Button } from "@app/components/ui/button";
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 { createApiClient } from "@app/lib/api";
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 { useRouter } from "next/navigation";
import { useState, useTransition } from "react";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -38,11 +34,6 @@ export default function UsersTable({ roles }: RolesTableProps) {
const [roleToRemove, setRoleToRemove] = useState<RoleRow | null>(null);
const api = createApiClient(useEnvContext());
const { org } = useOrgContext();
const { isPaidUser } = usePaidStatus();
const t = useTranslations();
const [isRefreshing, startTransition] = useTransition();