import { useCallback, useEffect, useRef, useState } from 'react'; import { RefreshCw } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { readableError } from './errors'; import { useNpmChanged } from './channel'; /** * The four sections that are a list and nothing more — redirections, streams, 404 hosts, access lists. * * One component rather than four near-identical ones. Each differs only in what a row SAYS, so that is the * one thing passed in; everything around it — loading, failure, empty, refresh — is the same problem four * times over and was not worth writing four times. * * Proxy hosts and certificates are deliberately NOT here. They have real behaviour of their own: a * selection with an enable/disable action, and expiry arithmetic that decides the colour of a row. Folding * them in would mean a props bag that is a worse version of both. */ type Row = { id: number } & Record; export type ListSectionProps = { /** Plugin route, e.g. `/proxy-manager/streams`. */ path: string; /** What this is, shown when there are none. */ emptyText: string; /** The row's heading — usually a domain or a port. */ primary: (row: Row) => string; /** The line under it — where it goes, or what it holds. */ secondary: (row: Row) => string; /** Dimmed when false. NPM's `enabled` is 0/1, and some of these have no such field at all. */ isEnabled?: (row: Row) => boolean; }; const Centred = ({ children }: { children: React.ReactNode }) => (
{children}
); export const ListSection = ({ path, emptyText, primary, secondary, isEnabled }: ListSectionProps) => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [rows, setRows] = useState(null); const [error, setError] = useState(null); const [connectionSignal] = useNpmChanged(); const load = useCallback(async () => { try { const next = await clientRef.current.get(path); setRows(Array.isArray(next) ? next : []); setError(null); } catch (caught) { setError(readableError(caught, 'Could not load this.')); setRows(null); } }, [path]); useEffect(() => { void load(); }, [load, connectionSignal]); if (error) return {error}; if (!rows) return Loading…; return (
{rows.length} {rows.length === 1 ? 'entry' : 'entries'}
{rows.length === 0 ? ( {emptyText} ) : (
    {rows.map((row) => (
  • {primary(row)} {secondary(row)}
  • ))}
)}
); }; /** NPM's `enabled` is an integer. Absent on the kinds that cannot be switched off. */ const enabled = (row: Row) => row.enabled !== 0; const domains = (row: Row) => (Array.isArray(row.domain_names) ? (row.domain_names as string[]).join(', ') : '—'); /** * The four, with only the parts that differ. * * Field names are NPM's, straight off the wire — `docs/npm-api.md` has the shapes. Nothing is renamed on * the way through, so a value that looks wrong here is a value that looks the same in NPM's own UI. */ export const REDIRECTIONS: ListSectionProps = { path: '/proxy-manager/redirections', emptyText: 'No redirections. These send a domain somewhere else rather than serving it.', primary: domains, secondary: (r) => `→ ${String(r.forward_scheme ?? 'auto')}://${String(r.forward_domain_name ?? '?')}`, isEnabled: enabled, }; export const STREAMS: ListSectionProps = { path: '/proxy-manager/streams', emptyText: 'No streams. These forward a raw TCP or UDP port rather than HTTP.', primary: (r) => `port ${String(r.incoming_port ?? '?')}`, secondary: (r) => `→ ${String(r.forwarding_host ?? '?')}:${String(r.forwarding_port ?? '?')}` + `${r.tcp_forwarding ? ' · TCP' : ''}${r.udp_forwarding ? ' · UDP' : ''}`, isEnabled: enabled, }; export const DEAD_HOSTS: ListSectionProps = { path: '/proxy-manager/dead-hosts', emptyText: 'No 404 hosts. These serve a domain deliberately as nothing.', primary: domains, secondary: (r) => (r.certificate_id ? 'HTTPS' : 'HTTP only'), isEnabled: enabled, }; export const ACCESS_LISTS: ListSectionProps = { path: '/proxy-manager/access-lists', emptyText: 'No access lists. These decide who may reach a host.', primary: (r) => String(r.name ?? '(unnamed)'), secondary: (r) => { const users = Array.isArray(r.items) ? r.items.length : 0; const clients = Array.isArray(r.clients) ? r.clients.length : 0; return `${users} user${users === 1 ? '' : 's'} · ${clients} client rule${clients === 1 ? '' : 's'}`; }, };