'use client'; import { useEffect, useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { ChevronDown, Filter, Loader2, X } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { Input } from '@/components/ui/input'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; import { ZoneService, zoneQueryKey } from '@/lib/services/openflare'; import { cn } from '@/lib/utils'; import { OVERVIEW_RANGE_OPTIONS, RATE_LIMIT_RANGE_OPTIONS, type OverviewRangeHours, type RateLimitRangeHours, } from './access-log-utils'; type ManagedZoneDomains = { zoneId: number; zoneName: string; domains: string[]; }; function useManagedZoneDomains(enabled: boolean) { return useQuery({ queryKey: [...zoneQueryKey, 'zone-domain-tree'], enabled, staleTime: 60_000, queryFn: async (): Promise => { const zones = await ZoneService.list(); const overviews = await Promise.all( zones.map((zone) => ZoneService.getOverview(zone.id)), ); return overviews .map((overview) => { const domainSet = new Set(); for (const item of overview.domains ?? []) { const domain = item.domain?.trim(); if (domain) domainSet.add(domain); } return { zoneId: overview.zone.id, zoneName: overview.zone.domain || `Zone #${overview.zone.id}`, domains: Array.from(domainSet).sort((a, b) => a.localeCompare(b)), }; }) .filter((zone) => zone.domains.length > 0) .sort((a, b) => a.zoneName.localeCompare(b.zoneName)); }, }); } export function OverviewHostFilter({ hosts, onHostsChange, }: { hosts: string[]; onHostsChange: (hosts: string[]) => void; }) { const t = useTranslations('accessLogs.toolbar'); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); const [expandedZones, setExpandedZones] = useState>( {}, ); const zonesQuery = useManagedZoneDomains(open); const zones = useMemo(() => zonesQuery.data ?? [], [zonesQuery.data]); const selectedSet = useMemo(() => new Set(hosts), [hosts]); const filteredZones = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return zones; return zones .map((zone) => { const zoneMatched = zone.zoneName.toLowerCase().includes(q); const domains = zoneMatched ? zone.domains : zone.domains.filter((domain) => domain.toLowerCase().includes(q)); return { ...zone, domains }; }) .filter((zone) => zone.domains.length > 0); }, [query, zones]); useEffect(() => { if (!open || zones.length === 0) return; setExpandedZones((prev) => { const next = { ...prev }; let changed = false; for (const zone of zones) { if (next[zone.zoneId] === undefined) { next[zone.zoneId] = true; changed = true; } } return changed ? next : prev; }); }, [open, zones]); useEffect(() => { const q = query.trim(); if (!q || filteredZones.length === 0) return; setExpandedZones((prev) => { const next = { ...prev }; for (const zone of filteredZones) { next[zone.zoneId] = true; } return next; }); }, [filteredZones, query]); const toggleHost = (domain: string, checked: boolean | 'indeterminate') => { if (checked === true) { if (selectedSet.has(domain)) return; onHostsChange([...hosts, domain]); return; } onHostsChange(hosts.filter((item) => item !== domain)); }; const toggleZone = ( zoneDomains: string[], checked: boolean | 'indeterminate', ) => { if (checked === true) { const next = new Set(hosts); for (const domain of zoneDomains) next.add(domain); onHostsChange(Array.from(next)); return; } onHostsChange(hosts.filter((item) => !zoneDomains.includes(item))); }; return ( { setOpen(next); if (!next) setQuery(''); }} >

{t('filterHosts')}

{hosts.length > 0 ? ( ) : null}
setQuery(event.target.value)} placeholder={t('searchPlaceholder')} className='h-8 text-xs' />
{zonesQuery.isLoading ? (
{t('loadingHosts')}
) : zonesQuery.isError ? (

{t('loadHostsFailed')}

) : filteredZones.length === 0 ? (

{zones.length === 0 ? t('noRegistered') : t('noMatch')}

) : ( filteredZones.map((zone) => { const selectedCount = zone.domains.filter((domain) => selectedSet.has(domain), ).length; const allSelected = selectedCount === zone.domains.length; const partialSelected = selectedCount > 0 && selectedCount < zone.domains.length; const expanded = expandedZones[zone.zoneId] ?? true; return ( setExpandedZones((prev) => ({ ...prev, [zone.zoneId]: next, })) } className='rounded-md border border-dashed' >
toggleZone(zone.domains, checked) } aria-label={t('selectZone', { name: zone.zoneName })} />
{zone.domains.map((domain) => { const selected = selectedSet.has(domain); return ( ); })}
); }) )}
); } export function OverviewToolbar({ hours, hosts, onHoursChange, onHostsChange, rangeOptions, }: { hours: OverviewRangeHours | RateLimitRangeHours; hosts: string[]; onHoursChange: (hours: OverviewRangeHours) => void; onHostsChange: (hosts: string[]) => void; rangeOptions?: ReadonlyArray<{ value: number; label: string }>; }) { const t = useTranslations('accessLogs'); const options = rangeOptions ?? OVERVIEW_RANGE_OPTIONS.map((option) => ({ value: option.value, label: t(option.labelKey), })); return (
{hosts.length > 0 ? ( {hosts.length === 1 ? hosts[0] : t('toolbar.selectedCount', { count: hosts.length })} ) : null} { if (!value) return; onHoursChange(Number.parseInt(value, 10) as OverviewRangeHours); }} variant='outline' size='sm' className='justify-end' > {options.map((option) => ( {option.label} ))}
); } export { RATE_LIMIT_RANGE_OPTIONS };