Split settings panel registry context owner

This commit is contained in:
rcourtman 2026-03-21 14:17:56 +00:00
parent 5c9149122b
commit 88bdac466f
7 changed files with 148 additions and 124 deletions

View file

@ -84,8 +84,9 @@ work extends shared components instead of creating new local variants.
62. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
63. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
64. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`
65. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
66. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
65. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx`
66. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
67. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
## Shared Boundaries
@ -272,7 +273,7 @@ through `frontend-modern/src/utils/alertIncidentPresentation.ts` instead of
maintaining page-local incident panel styling inside
`frontend-modern/src/pages/Alerts.tsx`.
The settings shell now also has an explicit four-way ownership split.
The settings shell now also has an explicit five-way ownership split.
`frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` owns the
shared discovery draft and subnet-validation state,
`frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`
@ -280,11 +281,13 @@ owns infrastructure workspace prop assembly and resource-derived infrastructure
read-model shaping for the shell,
`frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` owns
system panel prop assembly for general, network, updates, and recovery, and
`frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` owns
registry composition only. `frontend-modern/src/components/Settings/Settings.tsx`
`frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` owns
registry context assembly for dispatchable settings tabs while
`frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` owns the
final memoized registry composition only. `frontend-modern/src/components/Settings/Settings.tsx`
must stay a shell that wires those owners together instead of re-accumulating
infrastructure workspace props, system panel prop maps, or discovery draft
state inline.
infrastructure workspace props, registry context maps, system panel prop maps,
or discovery draft state inline.
The resource incident panel's collapsed activity summary is now part of that
same shared primitive boundary. Event-type count chips, visible-event copy,

View file

@ -1866,6 +1866,7 @@
"frontend-modern/src/components/Settings/settingsHeaderMeta.ts",
"frontend-modern/src/components/Settings/SettingsPageShell.tsx",
"frontend-modern/src/components/Settings/settingsPanelRegistry.ts",
"frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx",
"frontend-modern/src/components/Settings/ssoProvidersModel.ts",
"frontend-modern/src/components/Settings/SSOProvidersPanel.tsx",
"frontend-modern/src/components/Settings/SystemLogsPanel.tsx",
@ -1951,6 +1952,7 @@
"frontend-modern/src/components/Settings/settingsHeaderMeta.ts",
"frontend-modern/src/components/Settings/SettingsPageShell.tsx",
"frontend-modern/src/components/Settings/settingsPanelRegistry.ts",
"frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx",
"frontend-modern/src/components/Settings/ssoProvidersModel.ts",
"frontend-modern/src/components/Settings/SSOProvidersPanel.tsx",
"frontend-modern/src/components/Settings/UpdateInstallGuide.tsx",

View file

@ -29,6 +29,7 @@ import infrastructureConfiguredNodesStateSource from '../useInfrastructureConfig
import infrastructureDiscoveryRuntimeStateSource from '../useInfrastructureDiscoveryRuntimeState.ts?raw';
import settingsInfrastructurePanelPropsSource from '../useSettingsInfrastructurePanelProps.ts?raw';
import nodeModalStateSource from '../useNodeModalState.ts?raw';
import settingsPanelRegistryContextSource from '../settingsPanelRegistryContext.tsx?raw';
import settingsPanelRegistryHookSource from '../useSettingsPanelRegistry.tsx?raw';
import settingsSystemPanelsSource from '../useSettingsSystemPanels.tsx?raw';
import apiAccessPanelSource from '../APIAccessPanel.tsx?raw';
@ -138,6 +139,7 @@ const extractedModules = [
'../useInfrastructureConfiguredNodesState.ts',
'../useInfrastructureDiscoveryRuntimeState.ts',
'../useSettingsInfrastructurePanelProps.ts',
'../settingsPanelRegistryContext.tsx',
'../apiTokenManagerModel.ts',
'../useAPITokenManagerState.ts',
'../useAuditLogPanelState.ts',
@ -380,6 +382,7 @@ describe('Settings architecture guardrails', () => {
expect(accessHookSource).toContain('shouldHideSettingsNavItem');
expect(accessHookSource).toContain('tabFeatureRequirements');
expect(panelRegistryHookSource).toContain('createSettingsPanelRegistry');
expect(panelRegistryHookSource).toContain('buildSettingsPanelRegistryContext');
expect(shellHookSource).toContain('SETTINGS_HEADER_META');
expect(settingsSource).toContain('useSettingsPanelRegistry');
expect(settingsSource).toContain('useSettingsAccess');
@ -411,19 +414,22 @@ describe('Settings architecture guardrails', () => {
expect(settingsSystemPanelsSource).toContain('allowedOrigins:');
expect(settingsSystemPanelsSource).toContain('backupPollingEnabled:');
expect(settingsSystemPanelsSource).toContain('handleDiscoveryEnabledChange:');
expect(settingsPanelRegistryHookSource).toContain('systemPanels: SettingsSystemPanels');
expect(settingsPanelRegistryHookSource).toContain(
expect(settingsPanelRegistryHookSource).toContain('buildSettingsPanelRegistryContext');
expect(settingsPanelRegistryContextSource).toContain('systemPanels: SettingsSystemPanels');
expect(settingsPanelRegistryContextSource).toContain(
'systemGeneralPanel: params.systemPanels.systemGeneralPanel',
);
expect(settingsPanelRegistryHookSource).toContain(
expect(settingsPanelRegistryContextSource).toContain(
'getNetworkPanelProps: params.systemPanels.getNetworkPanelProps',
);
expect(settingsPanelRegistryHookSource).toContain(
expect(settingsPanelRegistryContextSource).toContain(
'getUpdatesPanelProps: params.systemPanels.getUpdatesPanelProps',
);
expect(settingsPanelRegistryHookSource).toContain(
expect(settingsPanelRegistryContextSource).toContain(
'getRecoveryPanelProps: params.systemPanels.getRecoveryPanelProps',
);
expect(settingsPanelRegistryContextSource).toContain('const systemAiPanel: Component');
expect(settingsPanelRegistryContextSource).toContain('const securitySsoPanel: Component');
expect(settingsPanelRegistryHookSource).not.toContain('pvePollingInterval: params.');
expect(settingsPanelRegistryHookSource).not.toContain('allowedOrigins: params.');
expect(settingsPanelRegistryHookSource).not.toContain('backupPollingEnabled: params.');

View file

@ -0,0 +1,112 @@
import type { Accessor, Component, Setter } from 'solid-js';
import type { VersionInfo } from '@/api/updates';
import type { SecurityStatus as SecurityStatusInfo } from '@/types/config';
import { AICostDashboard } from '@/components/AI/AICostDashboard';
import { AISettings } from './AISettings';
import { ProLicensePanel } from './ProLicensePanel';
import { SSOProvidersPanel } from './SSOProvidersPanel';
import type { ProxmoxSettingsPanelProps } from './proxmoxSettingsModel';
import type { SettingsPanelRegistryContext } from './settingsPanelRegistry';
import type { SettingsSystemPanels } from './useSettingsSystemPanels';
export interface UseSettingsPanelRegistryParams {
securityStatus: Accessor<SecurityStatusInfo | null>;
securityStatusLoading: Accessor<boolean>;
organizationMonitoredSystemUsage: Accessor<number>;
organizationGuestUsage: Accessor<number>;
loadSecurityStatus: () => Promise<void>;
showQuickSecuritySetup: Accessor<boolean>;
setShowQuickSecuritySetup: Setter<boolean>;
showQuickSecurityWizard: Accessor<boolean>;
setShowQuickSecurityWizard: Setter<boolean>;
showPasswordModal: Accessor<boolean>;
setShowPasswordModal: Setter<boolean>;
hideLocalLogin: Accessor<boolean>;
hideLocalLoginLocked: Accessor<boolean>;
savingHideLocalLogin: Accessor<boolean>;
handleHideLocalLoginChange: (enabled: boolean) => Promise<void>;
versionInfo: Accessor<VersionInfo | null>;
getInfrastructurePanelProps: () => ProxmoxSettingsPanelProps;
systemPanels: SettingsSystemPanels;
}
export function buildSettingsPanelRegistryContext(
params: UseSettingsPanelRegistryParams,
): SettingsPanelRegistryContext {
const settingsCapabilities = () => params.securityStatus()?.settingsCapabilities ?? null;
const systemAiPanel: Component = () => (
<div class="space-y-6">
<AISettings />
<AICostDashboard />
</div>
);
const systemBillingPanel: Component = () => (
<div class="space-y-6">
<ProLicensePanel />
</div>
);
const securitySsoPanel: Component = () => (
<div class="space-y-6">
<SSOProvidersPanel
onConfigUpdated={params.loadSecurityStatus}
canManage={settingsCapabilities()?.singleSignOnWrite === true}
/>
</div>
);
return {
getInfrastructurePanelProps: params.getInfrastructurePanelProps,
systemGeneralPanel: params.systemPanels.systemGeneralPanel,
systemAiPanel,
systemBillingPanel,
securitySsoPanel,
getNetworkPanelProps: params.systemPanels.getNetworkPanelProps,
getUpdatesPanelProps: params.systemPanels.getUpdatesPanelProps,
getRecoveryPanelProps: params.systemPanels.getRecoveryPanelProps,
getOrganizationOverviewPanelProps: () => ({}),
getOrganizationAccessPanelProps: () => ({}),
getOrganizationSharingPanelProps: () => ({}),
getOrganizationBillingPanelProps: () => ({
nodeUsage: params.organizationMonitoredSystemUsage(),
guestUsage: params.organizationGuestUsage(),
}),
getApiAccessPanelProps: () => ({
currentTokenHint: params.securityStatus()?.apiTokenHint,
onTokensChanged: () => {
void params.loadSecurityStatus();
},
refreshing: params.securityStatusLoading(),
canManage: settingsCapabilities()?.apiAccessWrite === true,
}),
getSecurityOverviewPanelProps: () => ({
securityStatus: params.securityStatus,
securityStatusLoading: params.securityStatusLoading,
}),
getSecurityAuthPanelProps: () => ({
securityStatus: params.securityStatus,
securityStatusLoading: params.securityStatusLoading,
versionInfo: params.versionInfo,
showQuickSecuritySetup: params.showQuickSecuritySetup,
setShowQuickSecuritySetup: params.setShowQuickSecuritySetup,
showQuickSecurityWizard: params.showQuickSecurityWizard,
setShowQuickSecurityWizard: params.setShowQuickSecurityWizard,
showPasswordModal: params.showPasswordModal,
setShowPasswordModal: params.setShowPasswordModal,
hideLocalLogin: params.hideLocalLogin,
hideLocalLoginLocked: params.hideLocalLoginLocked,
savingHideLocalLogin: params.savingHideLocalLogin,
handleHideLocalLoginChange: params.handleHideLocalLoginChange,
loadSecurityStatus: params.loadSecurityStatus,
canManage: settingsCapabilities()?.authenticationWrite === true,
}),
getRelayPanelProps: () => ({
canManage: settingsCapabilities()?.relayWrite === true,
}),
getAuditWebhookPanelProps: () => ({
canManage: settingsCapabilities()?.auditWebhooksWrite === true,
}),
};
}

View file

@ -1,114 +1,10 @@
import { Accessor, Component, Setter, createMemo } from 'solid-js';
import type { VersionInfo } from '@/api/updates';
import type { SecurityStatus as SecurityStatusInfo } from '@/types/config';
import { AISettings } from './AISettings';
import { AICostDashboard } from '@/components/AI/AICostDashboard';
import { ProLicensePanel } from './ProLicensePanel';
import { SSOProvidersPanel } from './SSOProvidersPanel';
import { createMemo } from 'solid-js';
import { createSettingsPanelRegistry } from './settingsPanelRegistry';
import type { ProxmoxSettingsPanelProps } from './proxmoxSettingsModel';
import type { SettingsSystemPanels } from './useSettingsSystemPanels';
interface UseSettingsPanelRegistryParams {
securityStatus: Accessor<SecurityStatusInfo | null>;
securityStatusLoading: Accessor<boolean>;
organizationMonitoredSystemUsage: Accessor<number>;
organizationGuestUsage: Accessor<number>;
loadSecurityStatus: () => Promise<void>;
showQuickSecuritySetup: Accessor<boolean>;
setShowQuickSecuritySetup: Setter<boolean>;
showQuickSecurityWizard: Accessor<boolean>;
setShowQuickSecurityWizard: Setter<boolean>;
showPasswordModal: Accessor<boolean>;
setShowPasswordModal: Setter<boolean>;
hideLocalLogin: Accessor<boolean>;
hideLocalLoginLocked: Accessor<boolean>;
savingHideLocalLogin: Accessor<boolean>;
handleHideLocalLoginChange: (enabled: boolean) => Promise<void>;
versionInfo: Accessor<VersionInfo | null>;
getInfrastructurePanelProps: () => ProxmoxSettingsPanelProps;
systemPanels: SettingsSystemPanels;
}
import {
buildSettingsPanelRegistryContext,
type UseSettingsPanelRegistryParams,
} from './settingsPanelRegistryContext';
export function useSettingsPanelRegistry(params: UseSettingsPanelRegistryParams) {
const settingsCapabilities = createMemo(
() => params.securityStatus()?.settingsCapabilities ?? null,
);
const systemAiPanel: Component = () => (
<div class="space-y-6">
<AISettings />
<AICostDashboard />
</div>
);
const systemBillingPanel: Component = () => (
<div class="space-y-6">
<ProLicensePanel />
</div>
);
const securitySsoPanel: Component = () => (
<div class="space-y-6">
<SSOProvidersPanel
onConfigUpdated={params.loadSecurityStatus}
canManage={settingsCapabilities()?.singleSignOnWrite === true}
/>
</div>
);
return createMemo(() =>
createSettingsPanelRegistry({
getInfrastructurePanelProps: params.getInfrastructurePanelProps,
systemGeneralPanel: params.systemPanels.systemGeneralPanel,
systemAiPanel,
systemBillingPanel,
securitySsoPanel,
getNetworkPanelProps: params.systemPanels.getNetworkPanelProps,
getUpdatesPanelProps: params.systemPanels.getUpdatesPanelProps,
getRecoveryPanelProps: params.systemPanels.getRecoveryPanelProps,
getOrganizationOverviewPanelProps: () => ({}),
getOrganizationAccessPanelProps: () => ({}),
getOrganizationSharingPanelProps: () => ({}),
getOrganizationBillingPanelProps: () => ({
nodeUsage: params.organizationMonitoredSystemUsage(),
guestUsage: params.organizationGuestUsage(),
}),
getApiAccessPanelProps: () => ({
currentTokenHint: params.securityStatus()?.apiTokenHint,
onTokensChanged: () => {
void params.loadSecurityStatus();
},
refreshing: params.securityStatusLoading(),
canManage: settingsCapabilities()?.apiAccessWrite === true,
}),
getSecurityOverviewPanelProps: () => ({
securityStatus: params.securityStatus,
securityStatusLoading: params.securityStatusLoading,
}),
getSecurityAuthPanelProps: () => ({
securityStatus: params.securityStatus,
securityStatusLoading: params.securityStatusLoading,
versionInfo: params.versionInfo,
showQuickSecuritySetup: params.showQuickSecuritySetup,
setShowQuickSecuritySetup: params.setShowQuickSecuritySetup,
showQuickSecurityWizard: params.showQuickSecurityWizard,
setShowQuickSecurityWizard: params.setShowQuickSecurityWizard,
showPasswordModal: params.showPasswordModal,
setShowPasswordModal: params.setShowPasswordModal,
hideLocalLogin: params.hideLocalLogin,
hideLocalLoginLocked: params.hideLocalLoginLocked,
savingHideLocalLogin: params.savingHideLocalLogin,
handleHideLocalLoginChange: params.handleHideLocalLoginChange,
loadSecurityStatus: params.loadSecurityStatus,
canManage: settingsCapabilities()?.authenticationWrite === true,
}),
getRelayPanelProps: () => ({
canManage: settingsCapabilities()?.relayWrite === true,
}),
getAuditWebhookPanelProps: () => ({
canManage: settingsCapabilities()?.auditWebhooksWrite === true,
}),
}),
);
return createMemo(() => createSettingsPanelRegistry(buildSettingsPanelRegistryContext(params)));
}

View file

@ -29,6 +29,7 @@ import diagnosticsResultsPanelSource from '@/components/Settings/DiagnosticsResu
import diagnosticsStateSource from '@/components/Settings/useDiagnosticsPanelState.ts?raw';
import settingsShellSource from '@/components/Settings/Settings.tsx?raw';
import settingsPanelRegistrySource from '@/components/Settings/useSettingsPanelRegistry.tsx?raw';
import settingsPanelRegistryContextSource from '@/components/Settings/settingsPanelRegistryContext.tsx?raw';
import settingsSystemPanelsSource from '@/components/Settings/useSettingsSystemPanels.tsx?raw';
import settingsInfrastructurePanelPropsSource from '@/components/Settings/useSettingsInfrastructurePanelProps.ts?raw';
import discoverySettingsStateSource from '@/components/Settings/useDiscoverySettingsState.ts?raw';
@ -858,11 +859,14 @@ describe('frontend resource type boundaries', () => {
expect(settingsShellSource).toContain(
'const settingsPanelRegistry = useSettingsPanelRegistry({',
);
expect(settingsPanelRegistrySource).toContain('buildSettingsPanelRegistryContext');
expect(settingsShellSource).not.toContain('getInfrastructurePanelProps: () => ({');
expect(settingsPanelRegistrySource).toContain('systemPanels: SettingsSystemPanels');
expect(settingsPanelRegistrySource).toContain(
expect(settingsPanelRegistryContextSource).toContain('systemPanels: SettingsSystemPanels');
expect(settingsPanelRegistryContextSource).toContain(
'getNetworkPanelProps: params.systemPanels.getNetworkPanelProps',
);
expect(settingsPanelRegistryContextSource).toContain('const systemBillingPanel: Component');
expect(settingsPanelRegistryContextSource).toContain('getSecurityAuthPanelProps');
expect(settingsPanelRegistrySource).not.toContain('allowedOrigins: params.');
expect(settingsPanelRegistrySource).not.toContain('backupPollingEnabled: params.');
expect(settingsSystemPanelsSource).toContain('GeneralSettingsPanel');

View file

@ -1910,6 +1910,7 @@ class SubsystemLookupTest(unittest.TestCase):
"frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx",
"frontend-modern/src/components/Settings/SSOProvidersPanel.tsx",
"frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx",
"frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx",
"frontend-modern/src/components/Settings/useDiscoverySettingsState.ts",
"frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts",
"frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx",