two fixed panels could show two of NPM's seven sections and had nowhere to put the other five. now the left column is the section list — proxy hosts, redirections, streams, 404 hosts, access lists, certificates, connection — and the right renders whichever is chosen. NPM's own vocabulary, in NPM's own order. somebody who has used it should find the same words, and somebody who has not will end up in NPM's UI eventually anyway; better names would just mean learning both. "404 hosts" reads oddly and is theirs, so the hint under it says what it means. the two panels never speak. the nav writes ?section= and the detail pane reads it — navigation-audit's rule, and it is what makes a section linkable and survive a reload. changing section clears ?host=, which belonged to the list that was open. three things folded up while doing it. the unconfigured check moved to the detail pane and happens once, so all seven sections say "no connection yet" the same way instead of six saying it badly. the settings gear is gone: connection is a nav row now. and the four list-only sections are one component, differing only in what a row says — proxy hosts and certificates stay separate because they have real behaviour, and folding them in would be a props bag that is a worse version of both. the counts in the nav are one request each. affordable against a local NPM, and they answer "is this worth opening" without a click. a count that fails is absent rather than zero — zero is a fact about NPM, a failure is a fact about the request, and the detail pane reports that properly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
76 lines
2.9 KiB
TypeScript
76 lines
2.9 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { useSearchParams } from 'react-router';
|
|
import { useClient } from 'hooks/useClient';
|
|
import { DEFAULT_SECTION, 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 { useConnectionChanged } 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] = useConnectionChanged();
|
|
|
|
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>;
|
|
}
|
|
|
|
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} />;
|
|
}
|
|
};
|