From 47eb1316b303f4fa72dde77b881a379525285414 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 20 Mar 2026 21:05:48 +0000 Subject: [PATCH] Normalize storage route shell --- .../v6/internal/subsystems/cloud-paid.md | 6 ++++ .../v6/internal/subsystems/registry.json | 5 ++- .../internal/subsystems/storage-recovery.md | 35 ++++++++++--------- frontend-modern/src/App.tsx | 4 +-- .../src/__tests__/App.architecture.test.ts | 4 +++ frontend-modern/src/pages/Storage.tsx | 7 ++++ .../pages/__tests__/Storage.helpers.test.ts | 19 ++++++++++ .../canonical_completion_guard_test.py | 19 ++++++++++ .../release_control/subsystem_lookup_test.py | 22 ++++++++++++ 9 files changed, 102 insertions(+), 19 deletions(-) create mode 100644 frontend-modern/src/pages/Storage.tsx create mode 100644 frontend-modern/src/pages/__tests__/Storage.helpers.test.ts diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index e0bb430e0c..c3c283260f 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -224,6 +224,12 @@ theme synchronization, and authenticated runtime startup, and as org switching and kiosk-safe navigation. Future hosted browser bootstrap work must extend that split rather than pulling org bootstrap and app chrome back into one monolithic route component. +That same route/provider shell must stay page-oriented as well: `App.tsx` +should lazy-load route shells like `frontend-modern/src/pages/Storage.tsx` +instead of wiring product-surface components such as +`frontend-modern/src/components/Storage/Storage.tsx` directly into the router, +so hosted bootstrap ownership stays at the app boundary rather than leaking +route concerns back into feature components. The Pro license settings surface now follows the same rule as well. Changes to `frontend-modern/src/components/Settings/ProLicensePanel.tsx` must carry this contract and the dedicated Pro-license proof file instead of remaining an diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 263cf55190..b0faf4d72d 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2420,6 +2420,7 @@ "frontend-modern/src/pages/DashboardPanels/RecoveryStatusPanel.tsx", "frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx", "frontend-modern/src/pages/RecoveryRoute.tsx", + "frontend-modern/src/pages/Storage.tsx", "frontend-modern/src/types/recovery.ts", "frontend-modern/src/utils/dashboardRecoveryPresentation.ts", "frontend-modern/src/utils/recoveryActionPresentation.ts", @@ -2530,7 +2531,8 @@ "frontend-modern/src/features/storageBackups/" ], "match_files": [ - "frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx" + "frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx", + "frontend-modern/src/pages/Storage.tsx" ], "allow_same_subsystem_tests": false, "test_prefixes": [], @@ -2548,6 +2550,7 @@ "frontend-modern/src/features/storageBackups/__tests__/storageModelCore.test.ts", "frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts", "frontend-modern/src/features/storageBackups/__tests__/storagePoolsTablePresentation.test.ts", + "frontend-modern/src/pages/__tests__/Storage.helpers.test.ts", "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts" ] } diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index ae7c036edb..fa320bef2d 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -33,18 +33,19 @@ querying, and the operator-facing storage health presentation layer. 8. `frontend-modern/src/components/Recovery/RecoveryHistorySection.tsx` 9. `frontend-modern/src/components/Recovery/RecoveryHistoryTable.tsx` 10. `frontend-modern/src/components/Recovery/useRecoveryHistorySectionState.ts` -11. `frontend-modern/src/components/Storage/Storage.tsx` -12. `frontend-modern/src/features/storageBackups/storageModelCore.ts` -13. `frontend-modern/src/hooks/useRecoveryPoints.ts` -14. `frontend-modern/src/hooks/useRecoveryRollups.ts` -15. `frontend-modern/src/pages/RecoveryRoute.tsx` -16. `frontend-modern/src/pages/Dashboard.tsx` -17. `frontend-modern/src/pages/DashboardPanels/dashboardWidgets.ts` -18. `frontend-modern/src/pages/DashboardPanels/RecoveryStatusPanel.tsx` -19. `frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx` -20. `frontend-modern/src/types/recovery.ts` -21. `frontend-modern/src/utils/recoveryTablePresentation.ts` -22. `frontend-modern/src/utils/textPresentation.ts` +11. `frontend-modern/src/pages/Storage.tsx` +12. `frontend-modern/src/components/Storage/Storage.tsx` +13. `frontend-modern/src/features/storageBackups/storageModelCore.ts` +14. `frontend-modern/src/hooks/useRecoveryPoints.ts` +15. `frontend-modern/src/hooks/useRecoveryRollups.ts` +16. `frontend-modern/src/pages/RecoveryRoute.tsx` +17. `frontend-modern/src/pages/Dashboard.tsx` +18. `frontend-modern/src/pages/DashboardPanels/dashboardWidgets.ts` +19. `frontend-modern/src/pages/DashboardPanels/RecoveryStatusPanel.tsx` +20. `frontend-modern/src/pages/DashboardPanels/StoragePanel.tsx` +21. `frontend-modern/src/types/recovery.ts` +22. `frontend-modern/src/utils/recoveryTablePresentation.ts` +23. `frontend-modern/src/utils/textPresentation.ts` ## Shared Boundaries @@ -54,7 +55,7 @@ querying, and the operator-facing storage health presentation layer. 1. Add or change recovery-point persistence, rollups, or series derivation through `internal/recovery/` 2. Add or change recovery page UX through `frontend-modern/src/components/Recovery/` and keep canonical route/query/filter state ownership in `frontend-modern/src/features/recovery/useRecoverySurfaceState.ts` -3. Add or change storage page UX through `frontend-modern/src/components/Storage/` and `frontend-modern/src/features/storageBackups/` +3. Add or change storage page UX through `frontend-modern/src/pages/Storage.tsx`, `frontend-modern/src/components/Storage/`, and `frontend-modern/src/features/storageBackups/` 4. Route transport changes for storage and recovery endpoints through `internal/api/` and the owning `api-contracts` proof routes 5. Route canonical storage/recovery resource selection through `frontend-modern/src/hooks/useUnifiedResources.ts` and the owning `unified-resources` contract That shared hook now also projects resource `clusterId` through the shared cluster-name helper, so storage and recovery links keep the same cluster-context label as other unified-resource consumers instead of rebuilding a local fallback chain. @@ -198,9 +199,11 @@ adjacent consumers do not each keep a local `getOrgID() || 'default'` fallback. The frontend storage and recovery surfaces are also first-class runtime entry -points. `frontend-modern/src/components/Storage/` plus -`frontend-modern/src/features/storageBackups/` define the operator-facing -storage health model and presentation, while +points. `frontend-modern/src/pages/Storage.tsx` is the storage route shell, +`frontend-modern/src/components/Storage/Storage.tsx` is the operator-facing +storage surface owner, and `frontend-modern/src/components/Storage/` plus +`frontend-modern/src/features/storageBackups/` define the storage health model +and presentation, while `frontend-modern/src/components/Recovery/` and the recovery hooks define the timeline, protected-item, and recovery-summary UX. The recovery table presentation helper now owns the canonical subject-type diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx index 393f914dd0..f17f466705 100644 --- a/frontend-modern/src/App.tsx +++ b/frontend-modern/src/App.tsx @@ -57,7 +57,7 @@ function isPublicRoutePath(pathname: string): boolean { const Dashboard = lazy(() => import('./components/Dashboard/Dashboard').then((module) => ({ default: module.Dashboard })), ); -const StorageComponent = lazy(() => import('./components/Storage/Storage')); +const StoragePage = lazy(() => import('./pages/Storage')); const RecoveryRoute = lazy(() => import('./pages/RecoveryRoute')); const CephPage = lazy(() => import('./pages/Ceph')); const AlertsPage = lazy(() => @@ -384,7 +384,7 @@ function App() { } /> - + diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index fdd3cebf3c..1203442145 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -8,6 +8,10 @@ describe('App architecture', () => { expect(appSource).toContain("import { AppLayout } from '@/AppLayout';"); expect(appSource).toContain("import { useAppRuntimeState } from '@/useAppRuntimeState';"); expect(appSource).toContain('const runtime = useAppRuntimeState();'); + expect(appSource).toContain("const StoragePage = lazy(() => import('./pages/Storage'));"); + expect(appSource).not.toContain( + "const StorageComponent = lazy(() => import('./components/Storage/Storage'));", + ); expect(appSource).not.toContain('function ConnectionStatusBadge('); expect(appSource).not.toContain('function AppLayout('); expect(appSource).not.toContain('const [organizations, setOrganizations] = createSignal('); diff --git a/frontend-modern/src/pages/Storage.tsx b/frontend-modern/src/pages/Storage.tsx new file mode 100644 index 0000000000..0ff3bdbb0a --- /dev/null +++ b/frontend-modern/src/pages/Storage.tsx @@ -0,0 +1,7 @@ +import StorageSurface from '@/components/Storage/Storage'; + +export function Storage() { + return ; +} + +export default Storage; diff --git a/frontend-modern/src/pages/__tests__/Storage.helpers.test.ts b/frontend-modern/src/pages/__tests__/Storage.helpers.test.ts new file mode 100644 index 0000000000..ee1acf55b0 --- /dev/null +++ b/frontend-modern/src/pages/__tests__/Storage.helpers.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import appSource from '@/App.tsx?raw'; +import storagePageRouteSource from '@/pages/Storage.tsx?raw'; +import storageSurfaceSource from '@/components/Storage/Storage.tsx?raw'; + +describe('storage page route shell', () => { + it('keeps App routing on a page shell instead of the storage surface component', () => { + expect(appSource).toContain("const StoragePage = lazy(() => import('./pages/Storage'));"); + expect(appSource).not.toContain( + "const StorageComponent = lazy(() => import('./components/Storage/Storage'));", + ); + expect(storagePageRouteSource).toContain( + "import StorageSurface from '@/components/Storage/Storage';", + ); + expect(storagePageRouteSource).toContain(''); + expect(storagePageRouteSource).not.toContain('useStoragePageModel'); + expect(storageSurfaceSource).toContain('useStoragePageModel'); + }); +}); diff --git a/scripts/release_control/canonical_completion_guard_test.py b/scripts/release_control/canonical_completion_guard_test.py index d330c94125..baccc95082 100644 --- a/scripts/release_control/canonical_completion_guard_test.py +++ b/scripts/release_control/canonical_completion_guard_test.py @@ -978,12 +978,31 @@ class CanonicalCompletionGuardTest(unittest.TestCase): "frontend-modern/src/features/storageBackups/__tests__/storageModelCore.test.ts", "frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts", "frontend-modern/src/features/storageBackups/__tests__/storagePoolsTablePresentation.test.ts", + "frontend-modern/src/pages/__tests__/Storage.helpers.test.ts", "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", ], } ], ) + def test_storage_page_route_change_requires_storage_recovery_contract(self): + required = infer_impacted_subsystems(["frontend-modern/src/pages/Storage.tsx"]) + self.assertEqual(set(required), {"storage-recovery"}) + + recovery = required["storage-recovery"] + self.assertEqual( + recovery["contract"], + "docs/release-control/v6/internal/subsystems/storage-recovery.md", + ) + self.assertEqual( + recovery["touched_runtime_files"], + ["frontend-modern/src/pages/Storage.tsx"], + ) + self.assertEqual( + recovery["verification_requirements"][0]["id"], + "storage-product-surface", + ) + def test_recovery_types_change_requires_storage_recovery_contract(self): required = infer_impacted_subsystems(["frontend-modern/src/types/recovery.ts"]) self.assertEqual(set(required), {"storage-recovery"}) diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index f6aabb5e7c..ee16d2bd82 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -333,10 +333,32 @@ class SubsystemLookupTest(unittest.TestCase): "frontend-modern/src/features/storageBackups/__tests__/storageModelCore.test.ts", "frontend-modern/src/features/storageBackups/__tests__/storagePagePresentation.test.ts", "frontend-modern/src/features/storageBackups/__tests__/storagePoolsTablePresentation.test.ts", + "frontend-modern/src/pages/__tests__/Storage.helpers.test.ts", "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", ], ) + def test_lookup_paths_assigns_storage_page_to_storage_recovery(self) -> None: + result = lookup_paths(["frontend-modern/src/pages/Storage.tsx"]) + self.assertEqual(result["unowned_runtime_files"], []) + self.assertEqual( + {item["subsystem"] for item in result["impacted_subsystems"]}, + {"storage-recovery"}, + ) + file_entry = result["files"][0] + self.assertEqual(file_entry["classification"], "runtime") + self.assertEqual( + {match["subsystem"] for match in file_entry["matches"]}, + {"storage-recovery"}, + ) + match = file_entry["matches"][0] + self.assertEqual( + match["contract"], + "docs/release-control/v6/internal/subsystems/storage-recovery.md", + ) + self.assertEqual(match["lane_context"]["lane_id"], "L15") + self.assertEqual(match["verification_requirement"]["id"], "storage-product-surface") + def test_lookup_paths_assigns_recovery_types_to_storage_recovery(self) -> None: result = lookup_paths(["frontend-modern/src/types/recovery.ts"]) self.assertEqual(result["unowned_runtime_files"], [])