the order is a claim about what people come here for: proxy hosts, certificates, streams, then the rest. this file argued the opposite yesterday — "NPM's words in NPM's order" — and half of that was wrong. the VOCABULARY is worth keeping, because somebody will end up in NPM's UI eventually and better names just mean learning both. the ORDER is an accident of how NPM grew, and reproducing it gives up the one thing a plugin is for. and two new entries, listed and not built: Domains, and DNS Providers. they sit below a line, with Connection, because they are OFFICER's own rather than things NPM owns — what it knows about your domains, and the registrar keys it holds. shown rather than hidden on purpose. a nav that shows them says what this is FOR, including to whoever opens it in six months wondering what was decided. the pane says plainly that it does not exist and then what it will be — "not built" alone reads as an error, a description alone reads as something broken, and together they read as a plan. the planned check runs BEFORE the connection check: "you have not connected NPM" is not why Domains is empty, and would be a misleading thing to be told. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { useSearchParams } from 'react-router';
|
|
import { useClient } from 'hooks/useClient';
|
|
import { DEFAULT_SECTION, SECTIONS, isSectionKey, type SectionKey } from './sections';
|
|
import { HostsPanel } from './HostsPanel';
|
|
import { CertificatesPanel } from './CertificatesPanel';
|
|
import { ConnectionPanel } from './ConnectionPanel';
|
|
import { ListSection, REDIRECTIONS, STREAMS, DEAD_HOSTS, ACCESS_LISTS } from './ListSection';
|
|
import { PlannedSection } from './PlannedSection';
|
|
import { useNpmChanged } from './channel';
|
|
|
|
/**
|
|
* The right column: whichever section the nav has selected.
|
|
*
|
|
* It reads `?section=` from the URL rather than being handed it — the two panels are siblings in a
|
|
* workspace and neither owns the other, and `docs/navigation-audit.md` puts addressable state in the URL
|
|
* for exactly this reason. A bogus or missing value falls back rather than rendering nothing.
|
|
*
|
|
* ── The unconfigured check lives HERE, once ──
|
|
*
|
|
* Every section needs a connection, so every section would otherwise have to handle its absence, and each
|
|
* one would say something slightly different. Asking `/status` once and rendering the connection form
|
|
* instead of any section is the same answer for all seven, and it means "no connection yet" reads as a
|
|
* setup step rather than as six broken lists.
|
|
*/
|
|
|
|
type Status = { configured: boolean };
|
|
|
|
export const DetailPanel = () => {
|
|
const [params] = useSearchParams();
|
|
const raw = params.get('section');
|
|
const section: SectionKey = isSectionKey(raw) ? raw : DEFAULT_SECTION;
|
|
|
|
const client = useClient();
|
|
const clientRef = useRef(client);
|
|
clientRef.current = client;
|
|
|
|
const [configured, setConfigured] = useState<boolean | null>(null);
|
|
const [connectionSignal] = useNpmChanged();
|
|
|
|
const check = useCallback(async () => {
|
|
try {
|
|
const status = await clientRef.current.get<Status>('/proxy-manager/status');
|
|
setConfigured(status.configured);
|
|
} catch {
|
|
// Cannot tell. Render the section rather than the setup form — a transient failure must not look
|
|
// like "you have not set this up", which is a much more alarming thing to be told.
|
|
setConfigured(true);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void check();
|
|
}, [check, connectionSignal]);
|
|
|
|
if (configured === null) {
|
|
return <div className="flex size-full items-center justify-center p-6 text-sm text-muted-foreground">Loading…</div>;
|
|
}
|
|
|
|
// A planned section says so before anything else — including before the connection check, because "you
|
|
// have not connected NPM" is not why Domains is empty and would be a misleading thing to be told.
|
|
const planned = SECTIONS.find((s) => s.key === section && s.planned);
|
|
if (planned) return <PlannedSection section={planned} />;
|
|
|
|
if (!configured || section === 'connection') return <ConnectionPanel />;
|
|
|
|
switch (section) {
|
|
case 'proxy-hosts':
|
|
return <HostsPanel />;
|
|
case 'certificates':
|
|
return <CertificatesPanel />;
|
|
case 'redirections':
|
|
return <ListSection {...REDIRECTIONS} />;
|
|
case 'streams':
|
|
return <ListSection {...STREAMS} />;
|
|
case 'dead-hosts':
|
|
return <ListSection {...DEAD_HOSTS} />;
|
|
case 'access-lists':
|
|
return <ListSection {...ACCESS_LISTS} />;
|
|
default:
|
|
// `domains` and `dns-providers` are handled above; this keeps the switch total without a cast.
|
|
return null;
|
|
}
|
|
};
|