diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index b95eed8bb..7b7622206 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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, diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index e41b17362..66a17abcf 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -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", diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 149b2694e..9e1209134 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -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.'); diff --git a/frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx b/frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx new file mode 100644 index 000000000..ec0284e35 --- /dev/null +++ b/frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx @@ -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; + securityStatusLoading: Accessor; + organizationMonitoredSystemUsage: Accessor; + organizationGuestUsage: Accessor; + loadSecurityStatus: () => Promise; + showQuickSecuritySetup: Accessor; + setShowQuickSecuritySetup: Setter; + showQuickSecurityWizard: Accessor; + setShowQuickSecurityWizard: Setter; + showPasswordModal: Accessor; + setShowPasswordModal: Setter; + hideLocalLogin: Accessor; + hideLocalLoginLocked: Accessor; + savingHideLocalLogin: Accessor; + handleHideLocalLoginChange: (enabled: boolean) => Promise; + versionInfo: Accessor; + getInfrastructurePanelProps: () => ProxmoxSettingsPanelProps; + systemPanels: SettingsSystemPanels; +} + +export function buildSettingsPanelRegistryContext( + params: UseSettingsPanelRegistryParams, +): SettingsPanelRegistryContext { + const settingsCapabilities = () => params.securityStatus()?.settingsCapabilities ?? null; + + const systemAiPanel: Component = () => ( +
+ + +
+ ); + + const systemBillingPanel: Component = () => ( +
+ +
+ ); + + const securitySsoPanel: Component = () => ( +
+ +
+ ); + + 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, + }), + }; +} diff --git a/frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx b/frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx index f6af19d95..daa3d3ea6 100644 --- a/frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx +++ b/frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx @@ -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; - securityStatusLoading: Accessor; - organizationMonitoredSystemUsage: Accessor; - organizationGuestUsage: Accessor; - loadSecurityStatus: () => Promise; - showQuickSecuritySetup: Accessor; - setShowQuickSecuritySetup: Setter; - showQuickSecurityWizard: Accessor; - setShowQuickSecurityWizard: Setter; - showPasswordModal: Accessor; - setShowPasswordModal: Setter; - hideLocalLogin: Accessor; - hideLocalLoginLocked: Accessor; - savingHideLocalLogin: Accessor; - handleHideLocalLoginChange: (enabled: boolean) => Promise; - versionInfo: Accessor; - 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 = () => ( -
- - -
- ); - - const systemBillingPanel: Component = () => ( -
- -
- ); - - const securitySsoPanel: Component = () => ( -
- -
- ); - - 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))); } diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index a451dc7f3..9b429a04b 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -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'); diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index 8441158e3..9e5f7ffdf 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -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",