headscale: give the server picker its own panel
This commit is contained in:
@@ -5,16 +5,17 @@ import { WorkspaceView, DEFAULT_HEADSCALE_SECTION, headscaleSectionPath, isHeads
|
||||
import { useDashboardState } from 'state/useDashboardState';
|
||||
import { defaultLayout } from './defaultLayout';
|
||||
|
||||
// /headscale uses the Workspace/Panel system (like /soulseek and /music): a section nav (headscale-nav) on
|
||||
// the left and a section view (headscale-view) on the right. Both talk to the officer-headscale sidecar
|
||||
// through the /api/headscale auth proxy, which holds no Headscale credentials of its own — the registered
|
||||
// servers and their keys live in the sidecar.
|
||||
// /headscale uses the Workspace/Panel system (like /soulseek and /music): the server picker
|
||||
// (headscale-servers) above the section nav (headscale-nav) on the left, and the section view
|
||||
// (headscale-view) on the right. All three talk to the officer-headscale sidecar through the /api/headscale
|
||||
// auth proxy, which holds no Headscale credentials of its own — the registered servers and their keys live
|
||||
// in the sidecar.
|
||||
//
|
||||
// The open section is :section in the URL, so both panels read it with useParams instead of passing it
|
||||
// The open section is :section in the URL, so every panel reads it with useParams instead of passing it
|
||||
// between themselves over a channel. This screen backs both /headscale and /headscale/:section and is the
|
||||
// single place that decides what an absent or bogus section means.
|
||||
|
||||
const ALLOWED_APP_TYPES = new Set<string | null>(['headscale-nav', 'headscale-view', null]);
|
||||
const ALLOWED_APP_TYPES = new Set<string | null>(['headscale-servers', 'headscale-nav', 'headscale-view', null]);
|
||||
|
||||
function normalizeLayout(node: LayoutNode): LayoutNode {
|
||||
if (node.type === 'panel') {
|
||||
@@ -28,12 +29,22 @@ function normalizeLayout(node: LayoutNode): LayoutNode {
|
||||
return changed ? { ...node, children } : node;
|
||||
}
|
||||
|
||||
function hasAppType(node: LayoutNode, appType: string): boolean {
|
||||
if (node.type === 'panel') return node.appType === appType;
|
||||
return node.children.some((c) => hasAppType(c.node, appType));
|
||||
}
|
||||
|
||||
export const HeadscaleScreen = () => {
|
||||
const { section } = useParams();
|
||||
const rawWorkspace = useDashboardState<LayoutNode>('screens/headscale', defaultLayout);
|
||||
|
||||
const workspace = useMemo(() => {
|
||||
const fixed = normalizeLayout(rawWorkspace.value);
|
||||
// A layout saved before the server picker existed has no panel for it, and nothing else would ever add
|
||||
// one — so it is rebuilt from the default. That costs a one-time reset of any manual sizing, which is
|
||||
// cheaper than a screen permanently missing a panel.
|
||||
const fixed = hasAppType(rawWorkspace.value, 'headscale-servers')
|
||||
? normalizeLayout(rawWorkspace.value)
|
||||
: defaultLayout;
|
||||
if (fixed === rawWorkspace.value) return rawWorkspace;
|
||||
return { ...rawWorkspace, value: fixed };
|
||||
}, [rawWorkspace]);
|
||||
|
||||
@@ -5,7 +5,18 @@ export const defaultLayout: LayoutNode = {
|
||||
id: 'headscale-root',
|
||||
direction: 'horizontal',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'headscale-nav', appType: 'headscale-nav' }, size: 22 },
|
||||
{
|
||||
node: {
|
||||
type: 'group',
|
||||
id: 'headscale-sidebar',
|
||||
direction: 'vertical',
|
||||
children: [
|
||||
{ node: { type: 'panel', id: 'headscale-servers', appType: 'headscale-servers' }, size: 30 },
|
||||
{ node: { type: 'panel', id: 'headscale-nav', appType: 'headscale-nav' }, size: 70 },
|
||||
],
|
||||
},
|
||||
size: 22,
|
||||
},
|
||||
{ node: { type: 'panel', id: 'headscale-view', appType: 'headscale-view' }, size: 78 },
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user