Restrict features

This commit is contained in:
Owen
2025-10-24 16:29:37 -07:00
parent 10a5af67aa
commit 4b40e7b8d6
16 changed files with 622 additions and 85 deletions

View File

@@ -42,15 +42,36 @@ import {
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import { ChevronDown } from "lucide-react";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
// Schema for general organization settings
const GeneralFormSchema = z.object({
name: z.string(),
subnet: z.string().optional()
subnet: z.string().optional(),
settingsLogRetentionDaysRequest: z.number(),
settingsLogRetentionDaysAccess: z.number(),
settingsLogRetentionDaysAction: z.number()
});
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const LOG_RETENTION_OPTIONS = [
{ label: "logRetentionDisabled", value: 0 },
{ label: "logRetention3Days", value: 3 },
{ label: "logRetention7Days", value: 7 },
{ label: "logRetention14Days", value: 14 },
{ label: "logRetention30Days", value: 30 },
{ label: "logRetentionForever", value: -1 }
];
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { orgUser } = userOrgUserContext();
@@ -60,6 +81,8 @@ export default function GeneralPage() {
const { user } = useUserContext();
const t = useTranslations();
const { env } = useEnvContext();
const { isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
const [loadingDelete, setLoadingDelete] = useState(false);
const [loadingSave, setLoadingSave] = useState(false);
@@ -69,7 +92,13 @@ export default function GeneralPage() {
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: org?.org.name,
subnet: org?.org.subnet || "" // Add default value for subnet
subnet: org?.org.subnet || "", // Add default value for subnet
settingsLogRetentionDaysRequest:
org.org.settingsLogRetentionDaysRequest ?? 15,
settingsLogRetentionDaysAccess:
org.org.settingsLogRetentionDaysAccess ?? 15,
settingsLogRetentionDaysAction:
org.org.settingsLogRetentionDaysAction ?? 15
},
mode: "onChange"
});
@@ -131,8 +160,14 @@ export default function GeneralPage() {
try {
// Update organization
await api.post(`/org/${org?.org.orgId}`, {
name: data.name
name: data.name,
// subnet: data.subnet // Include subnet in the API request
settingsLogRetentionDaysRequest:
data.settingsLogRetentionDaysRequest,
settingsLogRetentionDaysAccess:
data.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysAction:
data.settingsLogRetentionDaysAction
});
// Also save auth page settings if they have unsaved changes
@@ -159,6 +194,11 @@ export default function GeneralPage() {
}
}
const getLabelForValue = (value: number) => {
const option = LOG_RETENTION_OPTIONS.find((opt) => opt.value === value);
return option ? t(option.label) : `${value} days`;
};
return (
<SettingsContainer>
<ConfirmDeleteDialog
@@ -168,9 +208,7 @@ export default function GeneralPage() {
}}
dialog={
<div>
<p>
{t("orgQuestionRemove")}
</p>
<p>{t("orgQuestionRemove")}</p>
<p>{t("orgMessageRemove")}</p>
</div>
}
@@ -179,23 +217,24 @@ export default function GeneralPage() {
string={org?.org.name || ""}
title={t("orgDelete")}
/>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("orgGeneralSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("orgGeneralSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="org-settings-form"
>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="org-settings-form"
>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("orgGeneralSettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("orgGeneralSettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<FormField
control={form.control}
name="name"
@@ -235,15 +274,228 @@ export default function GeneralPage() {
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
{(build === "saas") && (
<AuthPageSettings ref={authPageSettingsRef} />
)}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("logRetention")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("logRetentionDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
{/* {build === "saas" && !subscription?.subscribed ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("orgAuthPageDisabled")}{" "}
{t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null} */}
<SettingsSectionForm>
<FormField
control={form.control}
name="settingsLogRetentionDaysRequest"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("logRetentionRequestLabel")}
</FormLabel>
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger
asChild
>
<Button
variant="outline"
className="w-full justify-between"
>
{getLabelForValue(
field.value
)}
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full">
{LOG_RETENTION_OPTIONS.map(
(option) => (
<DropdownMenuItem
key={
option.value
}
onClick={() =>
field.onChange(
option.value
)
}
>
{t(
option.label
)}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
<FormDescription>
{t(
"logRetentionRequestDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{build != "oss" && (
<>
<FormField
control={form.control}
name="settingsLogRetentionDaysAccess"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"logRetentionAccessLabel"
)}
</FormLabel>
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger
asChild
>
<Button
variant="outline"
className="w-full justify-between"
disabled={
(build ==
"saas" &&
!subscription?.subscribed) ||
(build ==
"enterprise" &&
!isUnlocked())
}
>
{getLabelForValue(
field.value
)}
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full">
{LOG_RETENTION_OPTIONS.map(
(
option
) => (
<DropdownMenuItem
key={
option.value
}
onClick={() =>
field.onChange(
option.value
)
}
>
{t(
option.label
)}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
<FormDescription>
{t(
"logRetentionAccessDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="settingsLogRetentionDaysAction"
render={({ field }) => (
<FormItem>
<FormLabel>
{t(
"logRetentionActionLabel"
)}
</FormLabel>
<FormControl>
<DropdownMenu>
<DropdownMenuTrigger
asChild
>
<Button
variant="outline"
className="w-full justify-between"
disabled={
(build ==
"saas" &&
!subscription?.subscribed) ||
(build ==
"enterprise" &&
!isUnlocked())
}
>
{getLabelForValue(
field.value
)}
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full">
{LOG_RETENTION_OPTIONS.map(
(
option
) => (
<DropdownMenuItem
key={
option.value
}
onClick={() =>
field.onChange(
option.value
)
}
>
{t(
option.label
)}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
<FormDescription>
{t(
"logRetentionActionDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
</form>
</Form>
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
{/* Save Button */}
<div className="flex justify-end">

View File

@@ -6,12 +6,21 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
import {
getStoredPageSize,
LogDataTable,
setStoredPageSize
} from "@app/components/LogDataTable";
import { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { ArrowUpRight, Key, User } from "lucide-react";
import Link from "next/link";
import { ColumnFilter } from "@app/components/ColumnFilter";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert";
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -21,6 +30,8 @@ export default function GeneralPage() {
const t = useTranslations();
const { env } = useEnvContext();
const { orgId } = useParams();
const subscription = useSubscriptionStatusContext();
const { isUnlocked } = useLicenseStatusContext();
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -202,6 +213,16 @@ export default function GeneralPage() {
}
) => {
console.log("Date range changed:", { startDate, endDate, page, size });
if (
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
) {
console.log(
"Access denied: subscription inactive or license locked"
);
return;
}
setIsLoading(true);
try {
@@ -583,6 +604,27 @@ export default function GeneralPage() {
return (
<>
<SettingsSectionTitle
title={t("accessLogs")}
description={t("accessLogsDescription")}
/>
{build == "saas" && !subscription?.subscribed ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
{build == "enterprise" && !isUnlocked() ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("licenseRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<LogDataTable
columns={columns}
data={rows}
@@ -610,6 +652,10 @@ export default function GeneralPage() {
// Row expansion props
expandable={true}
renderExpandedRow={renderExpandedRow}
disabled={
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
}
/>
</>
);

View File

@@ -6,11 +6,20 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
import {
getStoredPageSize,
LogDataTable,
setStoredPageSize
} from "@app/components/LogDataTable";
import { ColumnDef } from "@tanstack/react-table";
import { DateTimeValue } from "@app/components/DateTimePicker";
import { Key, User } from "lucide-react";
import { ColumnFilter } from "@app/components/ColumnFilter";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build";
import { Alert, AlertDescription } from "@app/components/ui/alert";
export default function GeneralPage() {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@@ -20,6 +29,8 @@ export default function GeneralPage() {
const { env } = useEnvContext();
const { orgId } = useParams();
const searchParams = useSearchParams();
const subscription = useSubscriptionStatusContext();
const { isUnlocked } = useLicenseStatusContext();
const [rows, setRows] = useState<any[]>([]);
const [isRefreshing, setIsRefreshing] = useState(false);
@@ -187,6 +198,15 @@ export default function GeneralPage() {
}
) => {
console.log("Date range changed:", { startDate, endDate, page, size });
if (
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
) {
console.log(
"Access denied: subscription inactive or license locked"
);
return;
}
setIsLoading(true);
try {
@@ -435,6 +455,27 @@ export default function GeneralPage() {
return (
<>
<SettingsSectionTitle
title={t("actionLogs")}
description={t("actionLogsDescription")}
/>
{build == "saas" && !subscription?.subscribed ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("subscriptionRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
{build == "enterprise" && !isUnlocked() ? (
<Alert variant="info" className="mb-6">
<AlertDescription>
{t("licenseRequiredToUse")}
</AlertDescription>
</Alert>
) : null}
<LogDataTable
columns={columns}
data={rows}
@@ -464,6 +505,10 @@ export default function GeneralPage() {
// Row expansion props
expandable={true}
renderExpandedRow={renderExpandedRow}
disabled={
(build == "saas" && !subscription?.subscribed) ||
(build == "enterprise" && !isUnlocked())
}
/>
</>
);

View File

@@ -123,16 +123,20 @@ export const orgNavSections = (
href: "/{orgId}/settings/logs/request",
icon: <SquareMousePointer className="h-4 w-4" />
},
{
title: "sidebarLogsAccess",
href: "/{orgId}/settings/logs/access",
icon: <ScanEye className="h-4 w-4" />
},
{
title: "sidebarLogsAction",
href: "/{orgId}/settings/logs/action",
icon: <Logs className="h-4 w-4" />
},
...(build != "oss"
? [
{
title: "sidebarLogsAccess",
href: "/{orgId}/settings/logs/access",
icon: <ScanEye className="h-4 w-4" />
},
{
title: "sidebarLogsAction",
href: "/{orgId}/settings/logs/action",
icon: <Logs className="h-4 w-4" />
}
]
: [])
]
},
{

View File

@@ -23,6 +23,7 @@ interface DataTablePaginationProps<TData> {
totalCount?: number;
isServerPagination?: boolean;
isLoading?: boolean;
disabled?: boolean;
}
export function DataTablePagination<TData>({
@@ -31,7 +32,8 @@ export function DataTablePagination<TData>({
onPageChange,
totalCount,
isServerPagination = false,
isLoading = false
isLoading = false,
disabled = false
}: DataTablePaginationProps<TData>) {
const t = useTranslations();
@@ -96,8 +98,9 @@ export function DataTablePagination<TData>({
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={handlePageSizeChange}
disabled={disabled}
>
<SelectTrigger className="h-8 w-[73px]">
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
<SelectValue
placeholder={table.getState().pagination.pageSize}
/>
@@ -128,7 +131,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => handlePageNavigation('first')}
disabled={!table.getCanPreviousPage() || isLoading}
disabled={!table.getCanPreviousPage() || isLoading || disabled}
>
<span className="sr-only">{t('paginatorToFirst')}</span>
<DoubleArrowLeftIcon className="h-4 w-4" />
@@ -137,7 +140,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={() => handlePageNavigation('previous')}
disabled={!table.getCanPreviousPage() || isLoading}
disabled={!table.getCanPreviousPage() || isLoading || disabled}
>
<span className="sr-only">{t('paginatorToPrevious')}</span>
<ChevronLeftIcon className="h-4 w-4" />
@@ -146,7 +149,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="h-8 w-8 p-0"
onClick={() => handlePageNavigation('next')}
disabled={!table.getCanNextPage() || isLoading}
disabled={!table.getCanNextPage() || isLoading || disabled}
>
<span className="sr-only">{t('paginatorToNext')}</span>
<ChevronRightIcon className="h-4 w-4" />
@@ -155,7 +158,7 @@ export function DataTablePagination<TData>({
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => handlePageNavigation('last')}
disabled={!table.getCanNextPage() || isLoading}
disabled={!table.getCanNextPage() || isLoading || disabled}
>
<span className="sr-only">{t('paginatorToLast')}</span>
<DoubleArrowRightIcon className="h-4 w-4" />

View File

@@ -102,6 +102,7 @@ type DataTableProps<TData, TValue> = {
};
tabs?: TabFilter[];
defaultTab?: string;
disabled?: boolean;
onDateRangeChange?: (
startDate: DateTimeValue,
endDate: DateTimeValue
@@ -144,6 +145,7 @@ export function LogDataTable<TData, TValue>({
onPageSizeChange: onPageSizeChangeProp,
isLoading = false,
expandable = false,
disabled=false,
renderExpandedRow
}: DataTableProps<TData, TValue>) {
const t = useTranslations();
@@ -175,6 +177,11 @@ export function LogDataTable<TData, TValue>({
// Apply tab filter to data
const filteredData = useMemo(() => {
// If disabled, return empty array to prevent data loading
if (disabled) {
return [];
}
if (!tabs || activeTab === "") {
return data;
}
@@ -185,7 +192,7 @@ export function LogDataTable<TData, TValue>({
}
return data.filter(activeTabFilter.filterFn);
}, [data, tabs, activeTab]);
}, [data, tabs, activeTab, disabled]);
// Toggle row expansion
const toggleRowExpansion = (rowId: string) => {
@@ -219,9 +226,12 @@ export function LogDataTable<TData, TValue>({
variant="ghost"
size="sm"
className="h-6 w-6 p-0"
disabled={disabled}
onClick={(e) => {
toggleRowExpansion(row.id);
e.stopPropagation();
if (!disabled) {
toggleRowExpansion(row.id);
e.stopPropagation();
}
}}
>
{isExpanded ? (
@@ -236,7 +246,7 @@ export function LogDataTable<TData, TValue>({
};
return [expansionColumn, ...columns];
}, [columns, expandable, expandedRows, toggleRowExpansion]);
}, [columns, expandable, expandedRows, toggleRowExpansion, disabled]);
const table = useReactTable({
data: filteredData,
@@ -298,6 +308,8 @@ export function LogDataTable<TData, TValue>({
}, [currentPage, table, isServerPagination]);
const handleTabChange = (value: string) => {
if (disabled) return;
setActiveTab(value);
// Reset to first page when changing tabs
table.setPageIndex(0);
@@ -305,6 +317,8 @@ export function LogDataTable<TData, TValue>({
// Enhanced pagination component that updates our local state
const handlePageSizeChange = (newPageSize: number) => {
if (disabled) return;
// setPageSize(newPageSize);
table.setPageSize(newPageSize);
@@ -321,6 +335,8 @@ export function LogDataTable<TData, TValue>({
// Handle page changes for server pagination
const handlePageChange = (newPageIndex: number) => {
if (disabled) return;
if (isServerPagination && onPageChange) {
onPageChange(newPageIndex);
}
@@ -330,6 +346,8 @@ export function LogDataTable<TData, TValue>({
start: DateTimeValue,
end: DateTimeValue
) => {
if (disabled) return;
setStartDate(start);
setEndDate(end);
onDateRangeChange?.(start, end);
@@ -358,14 +376,15 @@ export function LogDataTable<TData, TValue>({
endValue={endDate}
onRangeChange={handleDateRangeChange}
className="flex-wrap gap-2"
disabled={disabled}
/>
</div>
<div className="flex items-start gap-2 sm:justify-end">
{onRefresh && (
<Button
variant="outline"
onClick={onRefresh}
disabled={isRefreshing}
onClick={() => !disabled && onRefresh()}
disabled={isRefreshing || disabled}
>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
@@ -374,7 +393,7 @@ export function LogDataTable<TData, TValue>({
</Button>
)}
{onExport && (
<Button onClick={onExport} disabled={isExporting}>
<Button onClick={() => !disabled && onExport()} disabled={isExporting || disabled}>
<Download
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
/>
@@ -415,7 +434,7 @@ export function LogDataTable<TData, TValue>({
"selected"
}
onClick={() =>
expandable
expandable && !disabled
? toggleRowExpansion(
row.id
)
@@ -500,6 +519,7 @@ export function LogDataTable<TData, TValue>({
totalCount={totalCount}
isServerPagination={isServerPagination}
isLoading={isLoading}
disabled={disabled}
/>
</div>
</CardContent>