Merge branch 'dev' into cicd

This commit is contained in:
miloschwartz
2025-12-12 23:08:06 -05:00
8 changed files with 85 additions and 64 deletions

View File

@@ -120,11 +120,13 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
* Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses. * Parses an endpoint string (ip:port) handling both IPv4 and IPv6 addresses.
* IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080. * IPv6 addresses may be bracketed like [::1]:8080 or unbracketed like ::1:8080.
* For unbracketed IPv6, the last colon-separated segment is treated as the port. * For unbracketed IPv6, the last colon-separated segment is treated as the port.
* *
* @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080") * @param endpoint The endpoint string to parse (e.g., "192.168.1.1:8080" or "[::1]:8080" or "2607:fea8::1:8080")
* @returns An object with ip and port, or null if parsing fails * @returns An object with ip and port, or null if parsing fails
*/ */
export function parseEndpoint(endpoint: string): { ip: string; port: number } | null { export function parseEndpoint(
endpoint: string
): { ip: string; port: number } | null {
if (!endpoint) return null; if (!endpoint) return null;
// Check for bracketed IPv6 format: [ip]:port // Check for bracketed IPv6 format: [ip]:port
@@ -138,7 +140,7 @@ export function parseEndpoint(endpoint: string): { ip: string; port: number } |
// Check if this looks like IPv6 (contains multiple colons) // Check if this looks like IPv6 (contains multiple colons)
const colonCount = (endpoint.match(/:/g) || []).length; const colonCount = (endpoint.match(/:/g) || []).length;
if (colonCount > 1) { if (colonCount > 1) {
// This is IPv6 - the port is after the last colon // This is IPv6 - the port is after the last colon
const lastColonIndex = endpoint.lastIndexOf(":"); const lastColonIndex = endpoint.lastIndexOf(":");
@@ -163,7 +165,7 @@ export function parseEndpoint(endpoint: string): { ip: string; port: number } |
/** /**
* Formats an IP and port into a consistent endpoint string. * Formats an IP and port into a consistent endpoint string.
* IPv6 addresses are wrapped in brackets for proper parsing. * IPv6 addresses are wrapped in brackets for proper parsing.
* *
* @param ip The IP address (IPv4 or IPv6) * @param ip The IP address (IPv4 or IPv6)
* @param port The port number * @param port The port number
* @returns Formatted endpoint string * @returns Formatted endpoint string

View File

@@ -84,14 +84,11 @@ LQIDAQAB
-----END PUBLIC KEY-----`; -----END PUBLIC KEY-----`;
constructor(private hostMeta: HostMeta) { constructor(private hostMeta: HostMeta) {
setInterval( setInterval(async () => {
async () => { this.doRecheck = true;
this.doRecheck = true; await this.check();
await this.check(); this.doRecheck = false;
this.doRecheck = false; }, 1000 * this.phoneHomeInterval);
},
1000 * this.phoneHomeInterval
);
} }
public listKeys(): LicenseKeyCache[] { public listKeys(): LicenseKeyCache[] {
@@ -242,7 +239,9 @@ LQIDAQAB
// First failure: fail silently // First failure: fail silently
logger.error("Error communicating with license server:"); logger.error("Error communicating with license server:");
logger.error(e); logger.error(e);
logger.error(`Allowing failure. Will retry one more time at next run interval.`); logger.error(
`Allowing failure. Will retry one more time at next run interval.`
);
// return last known good status // return last known good status
return this.statusCache.get( return this.statusCache.get(
this.statusKey this.statusKey

View File

@@ -148,7 +148,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
} }
} }
export function logRequestAudit( export async function logRequestAudit(
data: { data: {
action: boolean; action: boolean;
reason: number; reason: number;
@@ -174,14 +174,13 @@ export function logRequestAudit(
} }
) { ) {
try { try {
// Quick synchronous check - if org has 0 retention, skip immediately // Check retention before buffering any logs
if (data.orgId) { if (data.orgId) {
const cached = cache.get<number>(`org_${data.orgId}_retentionDays`); const retentionDays = await getRetentionDays(data.orgId);
if (cached === 0) { if (retentionDays === 0) {
// do not log // do not log
return; return;
} }
// If not cached or > 0, we'll log it (async retention check happens in background)
} }
let actorType: string | undefined; let actorType: string | undefined;
@@ -261,16 +260,6 @@ export function logRequestAudit(
} else { } else {
scheduleFlush(); scheduleFlush();
} }
// Async retention check in background (don't await)
if (
data.orgId &&
cache.get<number>(`org_${data.orgId}_retentionDays`) === undefined
) {
getRetentionDays(data.orgId).catch((err) =>
logger.error("Error checking retention days:", err)
);
}
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
} }

View File

@@ -449,15 +449,16 @@ export default function ResourceRules(props: {
type="number" type="number"
onClick={(e) => e.currentTarget.focus()} onClick={(e) => e.currentTarget.focus()}
onBlur={(e) => { onBlur={(e) => {
const parsed = z const parsed = z.coerce
.number()
.int() .int()
.optional() .optional()
.safeParse(e.target.value); .safeParse(e.target.value);
if (!parsed.data) { if (!parsed.success) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("rulesErrorInvalidIpAddress"), // correct priority or IP? title: t("rulesErrorInvalidPriority"), // correct priority or IP?
description: t( description: t(
"rulesErrorInvalidPriorityDescription" "rulesErrorInvalidPriorityDescription"
) )

View File

@@ -177,7 +177,13 @@ const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter; const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
return ( return (
<CredenzaFooter className={cn("mt-8 md:mt-0 -mx-6 px-6 pt-6 border-t border-border", className)} {...props}> <CredenzaFooter
className={cn(
"mt-8 md:mt-0 -mx-6 px-6 pt-6 border-t border-border",
className
)}
{...props}
>
{children} {children}
</CredenzaFooter> </CredenzaFooter>
); );

View File

@@ -24,6 +24,8 @@ interface DataTablePaginationProps<TData> {
isServerPagination?: boolean; isServerPagination?: boolean;
isLoading?: boolean; isLoading?: boolean;
disabled?: boolean; disabled?: boolean;
pageSize?: number;
pageIndex?: number;
} }
export function DataTablePagination<TData>({ export function DataTablePagination<TData>({
@@ -33,10 +35,24 @@ export function DataTablePagination<TData>({
totalCount, totalCount,
isServerPagination = false, isServerPagination = false,
isLoading = false, isLoading = false,
disabled = false disabled = false,
pageSize: controlledPageSize,
pageIndex: controlledPageIndex
}: DataTablePaginationProps<TData>) { }: DataTablePaginationProps<TData>) {
const t = useTranslations(); const t = useTranslations();
// Use controlled values if provided, otherwise fall back to table state
const pageSize = controlledPageSize ?? table.getState().pagination.pageSize;
const pageIndex = controlledPageIndex ?? table.getState().pagination.pageIndex;
// Calculate page boundaries based on controlled state
// For server-side pagination, use totalCount if available for accurate page count
const pageCount = isServerPagination && totalCount !== undefined
? Math.ceil(totalCount / pageSize)
: table.getPageCount();
const canNextPage = pageIndex < pageCount - 1;
const canPreviousPage = pageIndex > 0;
const handlePageSizeChange = (value: string) => { const handlePageSizeChange = (value: string) => {
const newPageSize = Number(value); const newPageSize = Number(value);
table.setPageSize(newPageSize); table.setPageSize(newPageSize);
@@ -51,7 +67,7 @@ export function DataTablePagination<TData>({
action: "first" | "previous" | "next" | "last" action: "first" | "previous" | "next" | "last"
) => { ) => {
if (isServerPagination && onPageChange) { if (isServerPagination && onPageChange) {
const currentPage = table.getState().pagination.pageIndex; const currentPage = pageIndex;
const pageCount = table.getPageCount(); const pageCount = table.getPageCount();
let newPage: number; let newPage: number;
@@ -77,18 +93,24 @@ export function DataTablePagination<TData>({
} }
} else { } else {
// Use table's built-in navigation for client-side pagination // Use table's built-in navigation for client-side pagination
// But add bounds checking to prevent going beyond page boundaries
const pageCount = table.getPageCount();
switch (action) { switch (action) {
case "first": case "first":
table.setPageIndex(0); table.setPageIndex(0);
break; break;
case "previous": case "previous":
table.previousPage(); if (pageIndex > 0) {
table.previousPage();
}
break; break;
case "next": case "next":
table.nextPage(); if (pageIndex < pageCount - 1) {
table.nextPage();
}
break; break;
case "last": case "last":
table.setPageIndex(table.getPageCount() - 1); table.setPageIndex(Math.max(0, pageCount - 1));
break; break;
} }
} }
@@ -98,13 +120,13 @@ export function DataTablePagination<TData>({
<div className="flex items-center justify-between text-muted-foreground"> <div className="flex items-center justify-between text-muted-foreground">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Select <Select
value={`${table.getState().pagination.pageSize}`} value={`${pageSize}`}
onValueChange={handlePageSizeChange} onValueChange={handlePageSizeChange}
disabled={disabled} disabled={disabled}
> >
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}> <SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
<SelectValue <SelectValue
placeholder={table.getState().pagination.pageSize} placeholder={pageSize}
/> />
</SelectTrigger> </SelectTrigger>
<SelectContent side="bottom"> <SelectContent side="bottom">
@@ -121,16 +143,11 @@ export function DataTablePagination<TData>({
<div className="flex items-center justify-center text-sm font-medium"> <div className="flex items-center justify-center text-sm font-medium">
{isServerPagination && totalCount !== undefined {isServerPagination && totalCount !== undefined
? t("paginator", { ? t("paginator", {
current: current: pageIndex + 1,
table.getState().pagination.pageIndex + 1, last: Math.ceil(totalCount / pageSize)
last: Math.ceil(
totalCount /
table.getState().pagination.pageSize
)
}) })
: t("paginator", { : t("paginator", {
current: current: pageIndex + 1,
table.getState().pagination.pageIndex + 1,
last: table.getPageCount() last: table.getPageCount()
})} })}
</div> </div>
@@ -140,7 +157,7 @@ export function DataTablePagination<TData>({
className="hidden h-8 w-8 p-0 lg:flex" className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => handlePageNavigation("first")} onClick={() => handlePageNavigation("first")}
disabled={ disabled={
!table.getCanPreviousPage() || isLoading || disabled !canPreviousPage || isLoading || disabled
} }
> >
<span className="sr-only">{t("paginatorToFirst")}</span> <span className="sr-only">{t("paginatorToFirst")}</span>
@@ -151,7 +168,7 @@ export function DataTablePagination<TData>({
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
onClick={() => handlePageNavigation("previous")} onClick={() => handlePageNavigation("previous")}
disabled={ disabled={
!table.getCanPreviousPage() || isLoading || disabled !canPreviousPage || isLoading || disabled
} }
> >
<span className="sr-only"> <span className="sr-only">
@@ -164,7 +181,7 @@ export function DataTablePagination<TData>({
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
onClick={() => handlePageNavigation("next")} onClick={() => handlePageNavigation("next")}
disabled={ disabled={
!table.getCanNextPage() || isLoading || disabled !canNextPage || isLoading || disabled
} }
> >
<span className="sr-only">{t("paginatorToNext")}</span> <span className="sr-only">{t("paginatorToNext")}</span>
@@ -175,7 +192,7 @@ export function DataTablePagination<TData>({
className="hidden h-8 w-8 p-0 lg:flex" className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => handlePageNavigation("last")} onClick={() => handlePageNavigation("last")}
disabled={ disabled={
!table.getCanNextPage() || isLoading || disabled !canNextPage || isLoading || disabled
} }
> >
<span className="sr-only">{t("paginatorToLast")}</span> <span className="sr-only">{t("paginatorToLast")}</span>

View File

@@ -542,6 +542,8 @@ export function LogDataTable<TData, TValue>({
isServerPagination={isServerPagination} isServerPagination={isServerPagination}
isLoading={isLoading} isLoading={isLoading}
disabled={disabled} disabled={disabled}
pageSize={pageSize}
pageIndex={currentPage}
/> />
</div> </div>
</CardContent> </CardContent>

View File

@@ -10,7 +10,8 @@ import {
getSortedRowModel, getSortedRowModel,
ColumnFiltersState, ColumnFiltersState,
getFilteredRowModel, getFilteredRowModel,
VisibilityState VisibilityState,
PaginationState
} from "@tanstack/react-table"; } from "@tanstack/react-table";
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown // Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
@@ -227,6 +228,10 @@ export function DataTable<TData, TValue>({
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>( const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
initialColumnVisibility initialColumnVisibility
); );
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: pageSize
});
const [activeTab, setActiveTab] = useState<string>( const [activeTab, setActiveTab] = useState<string>(
defaultTab || tabs?.[0]?.id || "" defaultTab || tabs?.[0]?.id || ""
); );
@@ -256,6 +261,7 @@ export function DataTable<TData, TValue>({
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onGlobalFilterChange: setGlobalFilter, onGlobalFilterChange: setGlobalFilter,
onColumnVisibilityChange: setColumnVisibility, onColumnVisibilityChange: setColumnVisibility,
onPaginationChange: setPagination,
initialState: { initialState: {
pagination: { pagination: {
pageSize: pageSize, pageSize: pageSize,
@@ -267,21 +273,18 @@ export function DataTable<TData, TValue>({
sorting, sorting,
columnFilters, columnFilters,
globalFilter, globalFilter,
columnVisibility columnVisibility,
pagination
} }
}); });
// Persist pageSize to localStorage when it changes
useEffect(() => { useEffect(() => {
const currentPageSize = table.getState().pagination.pageSize; if (persistPageSize && pagination.pageSize !== pageSize) {
if (currentPageSize !== pageSize) { setStoredPageSize(pagination.pageSize, tableId);
table.setPageSize(pageSize); setPageSize(pagination.pageSize);
// Persist to localStorage if enabled
if (persistPageSize) {
setStoredPageSize(pageSize, tableId);
}
} }
}, [pageSize, table, persistPageSize, tableId]); }, [pagination.pageSize, persistPageSize, tableId, pageSize]);
useEffect(() => { useEffect(() => {
// Persist column visibility to localStorage when it changes // Persist column visibility to localStorage when it changes
@@ -293,13 +296,13 @@ export function DataTable<TData, TValue>({
const handleTabChange = (value: string) => { const handleTabChange = (value: string) => {
setActiveTab(value); setActiveTab(value);
// Reset to first page when changing tabs // Reset to first page when changing tabs
table.setPageIndex(0); setPagination(prev => ({ ...prev, pageIndex: 0 }));
}; };
// Enhanced pagination component that updates our local state // Enhanced pagination component that updates our local state
const handlePageSizeChange = (newPageSize: number) => { const handlePageSizeChange = (newPageSize: number) => {
setPagination(prev => ({ ...prev, pageSize: newPageSize, pageIndex: 0 }));
setPageSize(newPageSize); setPageSize(newPageSize);
table.setPageSize(newPageSize);
// Persist immediately when changed // Persist immediately when changed
if (persistPageSize) { if (persistPageSize) {
@@ -614,6 +617,8 @@ export function DataTable<TData, TValue>({
<DataTablePagination <DataTablePagination
table={table} table={table}
onPageSizeChange={handlePageSizeChange} onPageSizeChange={handlePageSizeChange}
pageSize={pagination.pageSize}
pageIndex={pagination.pageIndex}
/> />
</div> </div>
</CardContent> </CardContent>