Files
technitium-console/src/ui/ClusterNodeSelect.tsx
T
DenozordecandClaude Opus 5.5 48b5d9fb15 Hub stage 3: the console in hub mode
With info.hub the node selector reads Server / All servers, the default-credentials
password prompt and the account actions the hub cannot serve are gone, the header
status reports the servers an aggregate could not reach (hubWarnings, kept until the
next aggregate), and merged Query Logs show a Server column. Without the flag
nothing changes. The hub sends hubWarnings on every aggregate answer, empty when all
servers answered, and never on a single-node one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:18:58 +07:00

110 lines
3.8 KiB
TypeScript

import { createContext, useContext } from 'react'
import { Select } from './Select'
import { Field } from './Field'
/*
This server's name (`dnsServerDomain`), which the shell provides. With no node
chosen, upstream's dropdown selects it (`updateClusterNodeDropDown`,
cluster.js:1041-1051). The value a screen holds stays empty, which the server
takes as this node, so only what the dropdown shows changes: this node's name
instead of the empty-value dash.
*/
export const ThisServerName = createContext('')
/*
True when the console talks to a hub (`info.hub` in the session) and not to a
Technitium server. The "nodes" are then independent servers, so the words change:
"All servers" for the aggregate, "Server" for the selector, and no `(primary)` tag.
*/
export const HubMode = createContext(false)
/*
Which cluster node a screen is looking at.
Upstream puts one of these on ten surfaces (`cluster.js`,
`updateAllClusterNodeDropDowns`) and this console had it on none of them. It
is not decoration: the server proxies the whole request to the chosen
node (`DnsWebService.cs:2378`), so without it a console can only ever show the
node it happens to be served from.
Three rules, all upstream's:
· It exists only when the server says `clusterInitialized`. On a single-server
install there is nothing to choose and nothing is drawn.
· Two of the ten (Dashboard and Settings) also offer **Cluster**, the aggregate,
and it is their default. The other eight start on this server.
· A node is listed as `name (type)`, with the type lowercased.
*/
export interface ClusterNode {
name: string
type: string
}
/** Upstream's word for the aggregate. The API client does not send it. */
export const AGGREGATE = 'cluster'
export function nodeOptions(
nodes: ClusterNode[],
{ aggregate = false, hub = false }: { aggregate?: boolean; hub?: boolean } = {},
): { value: string; label: string }[] {
const out = aggregate ? [{ value: AGGREGATE, label: hub ? 'All servers' : 'Cluster' }] : []
for (const n of nodes) out.push({ value: n.name, label: hub ? n.name : `${n.name} (${n.type.toLowerCase()})` })
return out
}
/** What a screen shows before anyone chooses: the aggregate where there is one,
* and this server everywhere else. */
export function defaultNode(aggregate: boolean, thisServer: string): string {
return aggregate ? AGGREGATE : thisServer
}
export function ClusterNodeSelect({
nodes,
initialised,
aggregate = false,
value,
onChange,
label = 'Node',
disabled = false,
}: {
nodes: ClusterNode[]
initialised: boolean
aggregate?: boolean
value: string
onChange: (node: string) => void
label?: string
/** Upstream disables it where the node is already fixed (a zone open on one). */
disabled?: boolean
}) {
const thisServer = useContext(ThisServerName)
const hub = useContext(HubMode)
if (!initialised) return null
/* Only this server's own name stands in for the empty value: it is the node the
empty value means. Upstream's fallback to the first node would also SEND that
node, so it is not drawn here without being sent. */
const shown = value === '' && !aggregate && nodes.some((n) => n.name === thisServer) ? thisServer : value
return (
<Field label={hub ? 'Server' : label}>
{(id) => (
<Select id={id} value={shown} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
{nodeOptions(nodes, { aggregate, hub }).map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</Select>
)}
</Field>
)
}
/* `getPrimaryClusterNodeName` (cluster.js:997): the node whose type is `Primary`,
or empty when the cluster is not initialised. */
export function primaryNodeName(nodes: readonly ClusterNode[], initialised: boolean): string {
if (!initialised) return ''
return nodes.find((n) => n.type === 'Primary')?.name ?? ''
}