'use client'; import { useEffect, useMemo, useState } from 'react'; import { Eye } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { EmptyStateWithBorder } from '@/components/layout/empty'; import { ErrorInline } from '@/components/layout/error'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import type { AccessLogIPSummaryItem, AccessLogIPSummaryList, } from '@/lib/services/openflare'; import { formatDateTime } from '@/lib/utils'; import { formatBytes, formatCompactNumber } from '@/lib/utils/metrics'; import { formatOverviewRangeHint, IP_SORT_OPTIONS, OVERVIEW_RANGE_OPTIONS, PAGE_SIZE_OPTIONS, type OverviewRangeHours, } from './access-log-utils'; import { IpDetailDialog } from './ip-detail-dialog'; export function toLocalInputValue(date: Date) { const pad = (n: number) => `${n}`.padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`; } /** Default local range ending now, spanning `hours`. */ export function defaultLocalRangeForHours(hours: number) { const until = new Date(); const since = new Date(until.getTime() - hours * 3_600_000); return { since: toLocalInputValue(since), until: toLocalInputValue(until), }; } function localInputToRFC3339(value: string) { const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; return date.toISOString(); } const MAX_CUSTOM_RANGE_MS = 30 * 24 * 3_600_000; /** Validate custom local datetime-local range; returns error message or null. */ export function validateCustomTimeRange( sinceLocal: string, untilLocal: string, t?: (key: 'ip.needRange' | 'ip.invalidTime' | 'ip.endAfterStart' | 'ip.maxRange') => string, ): string | null { if (!sinceLocal.trim() || !untilLocal.trim()) { return t ? t('ip.needRange') : 'needRange'; } const sinceMs = new Date(sinceLocal).getTime(); const untilMs = new Date(untilLocal).getTime(); if (Number.isNaN(sinceMs) || Number.isNaN(untilMs)) { return t ? t('ip.invalidTime') : 'invalidTime'; } if (untilMs <= sinceMs) { return t ? t('ip.endAfterStart') : 'endAfterStart'; } if (untilMs - sinceMs > MAX_CUSTOM_RANGE_MS) { return t ? t('ip.maxRange') : 'maxRange'; } return null; } function formatRatio(ratio: number) { if (!Number.isFinite(ratio) || ratio < 0) return '0%'; return `${(ratio * 100).toFixed(1)}%`; } /** Exact hours for analysis API (1–720), matching list window duration. */ export function resolveAnalysisHours(input: { timeMode: IpTabTimeMode; hours: OverviewRangeHours; customSince: string; customUntil: string; }): number { if (input.timeMode === 'custom' && input.customSince && input.customUntil) { const ms = new Date(input.customUntil).getTime() - new Date(input.customSince).getTime(); if (Number.isFinite(ms) && ms > 0) { return Math.min(720, Math.max(1, Math.ceil(ms / 3_600_000))); } } return input.hours; } function PaginationBar({ page, hasMore, loading, totalIp, onPrev, onNext, }: { page: number; hasMore: boolean; loading: boolean; totalIp?: number; onPrev: () => void; onNext: () => void; }) { const t = useTranslations('accessLogs.ip'); return (
{t('page', { page: page + 1 })} {typeof totalIp === 'number' ? t('total', { count: totalIp }) : ''}
{t('ip.timeRange')}
{t('ip.current', { range: rangeHint })} {data?.total_ip != null ? t('ip.total', { count: data.total_ip }) : ''}
{t('ip.start')}
{ setDraftSince(e.target.value); setCustomError(null); }} />{t('ip.end')}
{ setDraftUntil(e.target.value); setCustomError(null); }} />{customError}
) : ({t('ip.applyHint')}
)}{t('ip.sort')}
{t('ip.pageSize')}
{t('ip.title')}