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]>
This commit is contained in:
1 parent
5f29a438c9
commit
48b5d9fb15
12 files changed
+360
-23
No files matched your search
+36
-15
@@ -31,11 +31,12 @@ import { publicUrl } from './base'
|
||||
import { Menu } from '../ui/Menu'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { ThisServerName } from '../ui/ClusterNodeSelect'
|
||||
import { HubMode, ThisServerName } from '../ui/ClusterNodeSelect'
|
||||
import { FooterLinks } from '../ui/FooterLinks'
|
||||
import { Versions } from './Versions'
|
||||
import { BlockingMark } from './BlockingMark'
|
||||
import { useBlockingMark } from '../screens/blocking/blocking-state'
|
||||
import { useHubWarnings } from '../api/hub-warnings'
|
||||
import { Confirm } from '../ui/Confirm'
|
||||
import { Notifier } from '../ui/Notifier'
|
||||
import type { Notice } from '../lib/notice'
|
||||
@@ -102,6 +103,8 @@ export interface ShellSession {
|
||||
dnsServerDomain: string
|
||||
permissions?: Record<string, Permission>
|
||||
clusterInitialized?: boolean
|
||||
/** Only on the hub (`hub/`): a read-only console over several servers. */
|
||||
hub?: boolean
|
||||
/** Only on an initialised cluster (WebServiceAuthApi.cs:104-106). */
|
||||
clusterDomain?: string
|
||||
clusterNodes?: { name: string; type: string }[]
|
||||
@@ -125,6 +128,8 @@ export function Shell({
|
||||
forcePasswordChange?: boolean
|
||||
}) {
|
||||
const permissions = session.info?.permissions
|
||||
const hub = session.info?.hub === true
|
||||
const hubWarnings = useHubWarnings()
|
||||
// Memoised: if it is recreated on every render, the `hashchange` resubscribes on each.
|
||||
const sections = useMemo(() => visibleSections(permissions), [permissions])
|
||||
/* The starting section comes from the address bar if it carries one, and only
|
||||
@@ -166,11 +171,11 @@ export function Shell({
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [])
|
||||
const [modal, setModal] = useState<ModalId | null>(forcePasswordChange ? 'password' : null)
|
||||
const [modal, setModal] = useState<ModalId | null>(forcePasswordChange && !hub ? 'password' : null)
|
||||
/* The factory password travels only with the opening the login caused; from the
|
||||
user menu `showChangePasswordModal()` is called with nothing (main.js). */
|
||||
const [forcedCurrentPassword, setForcedCurrentPassword] = useState<string | undefined>(
|
||||
forcePasswordChange ? 'admin' : undefined,
|
||||
forcePasswordChange && !hub ? 'admin' : undefined,
|
||||
)
|
||||
const [sub, setSub] = useState<string | null>(initialRoute?.sub ?? null)
|
||||
const [displayName, setDisplayName] = useState(session.displayName)
|
||||
@@ -398,6 +403,7 @@ export function Shell({
|
||||
return (
|
||||
<HandoffContext.Provider value={handoff}>
|
||||
<ThisServerName.Provider value={info?.dnsServerDomain ?? session.info?.dnsServerDomain ?? ''}>
|
||||
<HubMode.Provider value={hub}>
|
||||
<SlotProvider>
|
||||
{(slot) => (
|
||||
<div className={styles.shell} data-rail={rail}>
|
||||
@@ -499,26 +505,31 @@ export function Shell({
|
||||
>
|
||||
{(close) => (
|
||||
<>
|
||||
<button type="button" onClick={() => { close(); open('profile') }}>
|
||||
My Profile
|
||||
</button>
|
||||
{/* The hub serves reads only: no profile, password, 2FA or tokens. */}
|
||||
{!hub && (
|
||||
<button type="button" onClick={() => { close(); open('profile') }}>
|
||||
My Profile
|
||||
</button>
|
||||
)}
|
||||
{/* main.js:71-87 (v15.5.1) switches on `sessionData.type`:
|
||||
`RemoteSSO` hides both, `RemoteLDAP` hides only the password
|
||||
(the directory owns it, the 2FA is still this server's) and
|
||||
`Local` or anything else shows both. */}
|
||||
{session.type !== 'RemoteSSO' && session.type !== 'RemoteLDAP' && (
|
||||
{!hub && session.type !== 'RemoteSSO' && session.type !== 'RemoteLDAP' && (
|
||||
<button type="button" onClick={() => { close(); open('password') }}>
|
||||
Change Password
|
||||
</button>
|
||||
)}
|
||||
{session.type !== 'RemoteSSO' && (
|
||||
{!hub && session.type !== 'RemoteSSO' && (
|
||||
<button type="button" onClick={() => { close(); open('twofa') }}>
|
||||
Configure 2FA
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={() => { close(); open('token') }}>
|
||||
Create API Token
|
||||
</button>
|
||||
{!hub && (
|
||||
<button type="button" onClick={() => { close(); open('token') }}>
|
||||
Create API Token
|
||||
</button>
|
||||
)}
|
||||
{/* index.html:71: after the account's own entries, before the
|
||||
update notification. */}
|
||||
<button type="button" onClick={() => { close(); open('theme') }}>
|
||||
@@ -574,9 +585,13 @@ export function Shell({
|
||||
))}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger className={styles.status} data-attention={blockingNow != null} aria-label="Server status">
|
||||
<i className={styles.pdot} data-tone={blockingNow?.tone ?? 'ok'} />
|
||||
<span className={styles.lbl}>{blockingNow != null ? `Blocking ${blockingNow.text.toLowerCase()}` : (info?.dnsServerDomain ?? 'Server')}</span>
|
||||
<PopoverTrigger className={styles.status} data-attention={blockingNow != null || hubWarnings.length > 0} aria-label="Server status">
|
||||
<i className={styles.pdot} data-tone={hubWarnings.length > 0 ? 'warn' : (blockingNow?.tone ?? 'ok')} />
|
||||
<span className={styles.lbl}>
|
||||
{hubWarnings.length > 0
|
||||
? `${hubWarnings.length} ${hubWarnings.length === 1 ? 'server' : 'servers'} not answering`
|
||||
: blockingNow != null ? `Blocking ${blockingNow.text.toLowerCase()}` : (info?.dnsServerDomain ?? 'Server')}
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className={styles.pop}>
|
||||
<dl>
|
||||
@@ -585,6 +600,11 @@ export function Shell({
|
||||
<dt>Up since</dt><dd>{info?.uptimestamp ? new Date(info.uptimestamp).toLocaleString() : '-'}</dd>
|
||||
<dt>Blocking</dt><dd>{blockingNow ? blockingNow.label.replace(/^Blocking,?\s*/, '') : 'Enabled'}</dd>
|
||||
</dl>
|
||||
{hubWarnings.length > 0 && (
|
||||
<ul className={styles.warnings} aria-label="Servers not answering">
|
||||
{hubWarnings.map((w) => (<li key={w}>{w}</li>))}
|
||||
</ul>
|
||||
)}
|
||||
<Versions
|
||||
token={session.token}
|
||||
serverVersion={info?.version}
|
||||
@@ -737,7 +757,7 @@ export function Shell({
|
||||
])}
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Account">
|
||||
<CommandItem value="My Profile" onSelect={() => { setPalette(false); open('profile') }}>My Profile</CommandItem>
|
||||
{!hub && <CommandItem value="My Profile" onSelect={() => { setPalette(false); open('profile') }}>My Profile</CommandItem>}
|
||||
<CommandItem value="Change Theme" onSelect={() => { setPalette(false); open('theme') }}>Change Theme</CommandItem>
|
||||
<CommandItem value="Logout" onSelect={() => { setPalette(false); onLogout() }}>Logout</CommandItem>
|
||||
</CommandGroup>
|
||||
@@ -798,6 +818,7 @@ export function Shell({
|
||||
</div>
|
||||
)}
|
||||
</SlotProvider>
|
||||
</HubMode.Provider>
|
||||
</ThisServerName.Provider>
|
||||
</HandoffContext.Provider>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user