mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-03 17:26:38 +00:00
Merge branch 'dev' into cicd
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user