mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Unify settings infrastructure ledger
This commit is contained in:
parent
a6145510d0
commit
847d09d17b
34 changed files with 1286 additions and 882 deletions
|
|
@ -35,65 +35,62 @@ management, and fleet control surfaces.
|
|||
11. `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`
|
||||
12. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`
|
||||
13. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`
|
||||
14. `frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx`
|
||||
15. `frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx`
|
||||
16. `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`
|
||||
17. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
|
||||
18. `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
|
||||
19. `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`
|
||||
20. `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`
|
||||
21. `frontend-modern/src/components/Settings/ProxmoxDirectWorkspace.tsx`
|
||||
22. `frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.tsx`
|
||||
23. `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`
|
||||
24. `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`
|
||||
25. `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`
|
||||
26. `frontend-modern/src/components/Settings/ProxmoxNodeModalStack.tsx`
|
||||
27. `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`
|
||||
28. `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`
|
||||
29. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
|
||||
30. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`
|
||||
31. `frontend-modern/src/components/Settings/useProxmoxDirectWorkspaceState.ts`
|
||||
32. `frontend-modern/src/components/Settings/NodeModal.tsx`
|
||||
33. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx`
|
||||
34. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx`
|
||||
35. `frontend-modern/src/components/Settings/nodeModalModel.ts`
|
||||
36. `frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx`
|
||||
37. `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx`
|
||||
38. `frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx`
|
||||
39. `frontend-modern/src/components/Settings/useNodeModalState.ts`
|
||||
40. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
|
||||
41. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`
|
||||
42. `frontend-modern/src/utils/agentProfilesPresentation.ts`
|
||||
43. `frontend-modern/src/utils/agentInstallCommand.ts`
|
||||
44. `frontend-modern/src/api/nodes.ts`
|
||||
45. `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
|
||||
46. `frontend-modern/src/components/Settings/InfrastructureInventorySection.tsx`
|
||||
47. `frontend-modern/src/components/Settings/InfrastructureActiveRowDetails.tsx`
|
||||
48. `frontend-modern/src/components/Settings/InfrastructureIgnoredRowDetails.tsx`
|
||||
49. `frontend-modern/src/components/Settings/InfrastructureStopMonitoringDialog.tsx`
|
||||
50. `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`
|
||||
51. `frontend-modern/src/components/Settings/useInfrastructureReportingState.tsx`
|
||||
52. `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`
|
||||
53. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`
|
||||
54. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`
|
||||
55. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts`
|
||||
56. `frontend-modern/src/utils/agentCapabilityPresentation.ts`
|
||||
57. `frontend-modern/src/utils/agentProfileSuggestionPresentation.ts`
|
||||
58. `frontend-modern/src/utils/configuredNodeCapabilityPresentation.ts`
|
||||
59. `frontend-modern/src/utils/configuredNodeStatusPresentation.ts`
|
||||
60. `frontend-modern/src/utils/unifiedAgentInventoryPresentation.ts`
|
||||
61. `frontend-modern/src/utils/unifiedAgentStatusPresentation.ts`
|
||||
62. `frontend-modern/src/utils/clusterEndpointPresentation.ts`
|
||||
63. `frontend-modern/src/utils/nodeModalPresentation.ts`
|
||||
64. `frontend-modern/src/utils/proxmoxSettingsPresentation.ts`
|
||||
65. `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`
|
||||
66. `frontend-modern/src/components/Settings/platformConnectionsModel.ts`
|
||||
67. `frontend-modern/src/components/Settings/TrueNASSettingsPanel.tsx`
|
||||
68. `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`
|
||||
69. `frontend-modern/src/components/Settings/VMwareSettingsPanel.tsx`
|
||||
70. `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`
|
||||
71. `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`
|
||||
72. `internal/hostagent/proxmox_setup.go`
|
||||
14. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
|
||||
15. `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
|
||||
16. `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`
|
||||
17. `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`
|
||||
18. `frontend-modern/src/components/Settings/ProxmoxDirectWorkspace.tsx`
|
||||
19. `frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.tsx`
|
||||
20. `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`
|
||||
21. `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`
|
||||
22. `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`
|
||||
23. `frontend-modern/src/components/Settings/ProxmoxNodeModalStack.tsx`
|
||||
24. `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`
|
||||
25. `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`
|
||||
26. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
|
||||
27. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`
|
||||
28. `frontend-modern/src/components/Settings/useProxmoxDirectWorkspaceState.ts`
|
||||
29. `frontend-modern/src/components/Settings/NodeModal.tsx`
|
||||
30. `frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx`
|
||||
31. `frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsx`
|
||||
32. `frontend-modern/src/components/Settings/nodeModalModel.ts`
|
||||
33. `frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsx`
|
||||
34. `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx`
|
||||
35. `frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx`
|
||||
36. `frontend-modern/src/components/Settings/useNodeModalState.ts`
|
||||
37. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
|
||||
38. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`
|
||||
39. `frontend-modern/src/utils/agentProfilesPresentation.ts`
|
||||
40. `frontend-modern/src/utils/agentInstallCommand.ts`
|
||||
41. `frontend-modern/src/api/nodes.ts`
|
||||
42. `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
|
||||
43. `frontend-modern/src/components/Settings/InfrastructureInventorySection.tsx`
|
||||
44. `frontend-modern/src/components/Settings/InfrastructureActiveRowDetails.tsx`
|
||||
45. `frontend-modern/src/components/Settings/InfrastructureIgnoredRowDetails.tsx`
|
||||
46. `frontend-modern/src/components/Settings/InfrastructureStopMonitoringDialog.tsx`
|
||||
47. `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`
|
||||
48. `frontend-modern/src/components/Settings/useInfrastructureReportingState.tsx`
|
||||
49. `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`
|
||||
50. `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`
|
||||
51. `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`
|
||||
52. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts`
|
||||
53. `frontend-modern/src/utils/agentCapabilityPresentation.ts`
|
||||
54. `frontend-modern/src/utils/agentProfileSuggestionPresentation.ts`
|
||||
55. `frontend-modern/src/utils/configuredNodeCapabilityPresentation.ts`
|
||||
56. `frontend-modern/src/utils/configuredNodeStatusPresentation.ts`
|
||||
57. `frontend-modern/src/utils/unifiedAgentInventoryPresentation.ts`
|
||||
58. `frontend-modern/src/utils/unifiedAgentStatusPresentation.ts`
|
||||
59. `frontend-modern/src/utils/clusterEndpointPresentation.ts`
|
||||
60. `frontend-modern/src/utils/nodeModalPresentation.ts`
|
||||
61. `frontend-modern/src/utils/proxmoxSettingsPresentation.ts`
|
||||
62. `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`
|
||||
63. `frontend-modern/src/components/Settings/platformConnectionsModel.ts`
|
||||
64. `frontend-modern/src/components/Settings/TrueNASSettingsPanel.tsx`
|
||||
65. `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`
|
||||
66. `frontend-modern/src/components/Settings/VMwareSettingsPanel.tsx`
|
||||
67. `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`
|
||||
68. `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`
|
||||
69. `internal/hostagent/proxmox_setup.go`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
|
|
@ -205,7 +202,7 @@ an add-only capacity posture.
|
|||
4. Keep legacy Unified Agent compatibility names explicitly secondary when touching shared `internal/api/` runtime helpers: the legacy host-route family and `host-agent:*` scope names may remain as ingress or migration aliases, but they must not retake primary ownership in router state, live runtime scope checks, handler commentary, or operator-facing guidance.
|
||||
5. Add or change the unified agent CLI entrypoint, version/help exit semantics, or startup argument/error routing through `cmd/pulse-agent/main.go`.
|
||||
6. Add or change installer flags, persisted service arguments, or upgrade-safe re-entry behavior through `scripts/install.sh` and `scripts/install.ps1`.
|
||||
7. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, the API-backed platform connections workspace shell, route model, reporting summary owner, shared install/inventory/dialog section owners, the split infrastructure install/reporting state owners, the split direct-node/discovery infrastructure settings owners plus their shared model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system admission preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInventorySection.tsx`, `frontend-modern/src/components/Settings/InfrastructureActiveRowDetails.tsx`, `frontend-modern/src/components/Settings/InfrastructureIgnoredRowDetails.tsx`, `frontend-modern/src/components/Settings/InfrastructureStopMonitoringDialog.tsx`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`, `frontend-modern/src/components/Settings/platformConnectionsModel.ts`, `frontend-modern/src/components/Settings/TrueNASSettingsPanel.tsx`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/VMwareSettingsPanel.tsx`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ProxmoxDirectWorkspace.tsx`, `frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.tsx`, `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`, `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`, `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`, `frontend-modern/src/components/Settings/ProxmoxNodeModalStack.tsx`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureReportingState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/useProxmoxDirectWorkspaceState.ts`, `frontend-modern/src/components/Settings/NodeModal.tsx`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`.
|
||||
7. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, the connections-ledger workspace shell, route model, shared install/inventory/dialog section owners, the shared direct-node/discovery infrastructure settings owners plus their model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system admission preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureInventorySection.tsx`, `frontend-modern/src/components/Settings/InfrastructureActiveRowDetails.tsx`, `frontend-modern/src/components/Settings/InfrastructureIgnoredRowDetails.tsx`, `frontend-modern/src/components/Settings/InfrastructureStopMonitoringDialog.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`, `frontend-modern/src/components/Settings/platformConnectionsModel.ts`, `frontend-modern/src/components/Settings/TrueNASSettingsPanel.tsx`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/VMwareSettingsPanel.tsx`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ProxmoxDirectWorkspace.tsx`, `frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.tsx`, `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`, `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`, `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`, `frontend-modern/src/components/Settings/ProxmoxNodeModalStack.tsx`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureReportingState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/useProxmoxDirectWorkspaceState.ts`, `frontend-modern/src/components/Settings/NodeModal.tsx`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`.
|
||||
Those lifecycle-owned settings hooks may consume websocket state only through `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx` or recreate root-shell providers.
|
||||
Public demo and other read-only settings posture must stay reporting-first
|
||||
on that same lifecycle-owned workspace boundary: infrastructure workspace
|
||||
|
|
@ -357,29 +354,27 @@ an add-only capacity posture.
|
|||
7. Keep `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
|
||||
and `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
|
||||
aligned with that same lifecycle path. The bare
|
||||
`/settings/infrastructure` route must render a unified Connections table
|
||||
that lists every monitored system — Proxmox VE, PBS, PMG, TrueNAS, VMware,
|
||||
and agent hosts — as sibling rows sharing a single name/kind/method/status/
|
||||
last-reported shape, so operators read what is currently being monitored
|
||||
in one scan instead of tab-hopping between per-kind surfaces. Adding a new
|
||||
system must be a single entry point on that table: an `Add a system`
|
||||
picker whose tiles route the operator into the right flow per kind
|
||||
(`/settings/infrastructure/install` for the agent choice,
|
||||
`/settings/infrastructure/platforms/<kind>` for Proxmox/TrueNAS/VMware
|
||||
tiles). `/settings/infrastructure/install`,
|
||||
`/settings/infrastructure` route must render one Connections and inventory
|
||||
ledger that lists top-level infrastructure only — active or ignored
|
||||
infrastructure roots plus saved Proxmox VE, PBS, PMG, TrueNAS, VMware, and
|
||||
agent-managed entries — as sibling rows sharing one
|
||||
system/coverage/collection/status/last-activity workspace model, so
|
||||
operators can read infrastructure state in one scan instead of hopping
|
||||
between install, reporting, and provider shells. Guest-linked agent rows
|
||||
still belong to the reporting inventory and inline lifecycle detail, but
|
||||
they must not appear as peer connection rows on that top ledger. Adding a
|
||||
new system must stay a single entry point on that ledger:
|
||||
one `Add a system` picker that keeps `Install on a host` explicit for the
|
||||
agent path while opening the saved-connection create flow for API-backed
|
||||
platforms on the same page. `/settings/infrastructure/install`,
|
||||
`/settings/infrastructure/platforms`, and
|
||||
`/settings/infrastructure/operations` remain reachable as detail routes
|
||||
for install, platform connections, and legacy reporting/control surfaces
|
||||
respectively, but the workspace shell must not gate inventory visibility
|
||||
behind tab navigation. Every non-inventory detail route under
|
||||
`/settings/infrastructure/*` must render a persistent breadcrumb header
|
||||
that includes a one-click `Connections and Inventory` control returning
|
||||
to the bare `/settings/infrastructure` route, so operators are never
|
||||
stranded on a deeper install or platform-connection surface without a
|
||||
visible path back to the unified table. Read-only sessions must continue
|
||||
to redirect the install detail route back to the unified inventory view
|
||||
and suppress the add-system entry point on the base table so
|
||||
presentation-policy restrictions still hold.
|
||||
`/settings/infrastructure/operations` remain valid deep links, but they
|
||||
must resolve to section focus on that same single-page workspace rather
|
||||
than rendering separate page shells or hiding the top ledger. Read-only
|
||||
sessions must redirect any non-inventory infrastructure deep link back to
|
||||
`/settings/infrastructure`, suppress the add-system entry point, and hide
|
||||
configuration-only sections so presentation-policy restrictions still
|
||||
hold.
|
||||
8. Keep post-install lifecycle completion explicit inside
|
||||
`frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`
|
||||
and `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`.
|
||||
|
|
@ -658,7 +653,12 @@ agent extensions. `frontend-modern/src/components/Settings/infrastructureOperati
|
|||
must keep Proxmox, PBS, PMG, and TrueNAS on the shared Platform connections
|
||||
path, while only machine-installed agent, Docker, and Kubernetes surfaces
|
||||
participate in host stop-monitoring scope, uninstall commands, and upgrade
|
||||
actions.
|
||||
actions. That same lifecycle-owned reporting contract now also owns guest-link
|
||||
truth for agent rows: when a host agent is actually attached to a VM or system
|
||||
container, the shared connected-infrastructure payload must preserve that
|
||||
linked guest identity so the top Connections and inventory ledger can stay
|
||||
scoped to top-level infrastructure instead of rendering guest-backed agents as
|
||||
peer infrastructure roots.
|
||||
Those unified audit list endpoints also clamp oversized `limit` requests to
|
||||
the governed maximum, so audit history stays bounded even when callers ask
|
||||
for arbitrarily large pages.
|
||||
|
|
@ -823,12 +823,10 @@ the backend contract owns the reporting validation classification.
|
|||
The API-backed platform connections workspace now also lives explicitly inside
|
||||
this lifecycle boundary: `InfrastructureWorkspace.tsx`,
|
||||
`infrastructureWorkspaceModel.ts`,
|
||||
`InfrastructureInstallPanel.tsx`, `InfrastructureInstallerSection.tsx`,
|
||||
`InfrastructureReportingPanel.tsx`, `InfrastructureInventorySection.tsx`,
|
||||
`InfrastructureInstallerSection.tsx`, `InfrastructureInventorySection.tsx`,
|
||||
`InfrastructureActiveRowDetails.tsx`,
|
||||
`InfrastructureIgnoredRowDetails.tsx`,
|
||||
`InfrastructureStopMonitoringDialog.tsx`,
|
||||
`InfrastructurePlatformConnectionsSummaryCard.tsx`,
|
||||
`PlatformConnectionsWorkspace.tsx`, `platformConnectionsModel.ts`,
|
||||
`TrueNASSettingsPanel.tsx`, `useTrueNASSettingsPanelState.ts`,
|
||||
`ProxmoxSettingsPanel.tsx`, `proxmoxSettingsModel.ts`,
|
||||
|
|
@ -927,10 +925,10 @@ That lifecycle-owned VMware slice now also includes the first live runtime
|
|||
handoff rule. `frontend-modern/src/components/Settings/VMwareSettingsPanel.tsx`,
|
||||
`frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`,
|
||||
`frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`,
|
||||
`frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`,
|
||||
and `internal/api/router.go` must keep VMware on the shared platform-
|
||||
connections workflow with one `vCenter` connection family and one summary card
|
||||
count. Lifecycle-adjacent install or discovery surfaces must not fork that
|
||||
connections workflow with one `vCenter` connection family under the same
|
||||
saved-connection ledger. Lifecycle-adjacent install or discovery surfaces must
|
||||
not fork that
|
||||
into a VMware-only install wizard, direct-ESXi setup branch, or agent-first
|
||||
bootstrap story just because the runtime now has a live VMware connection
|
||||
panel and poller.
|
||||
|
|
@ -943,19 +941,19 @@ through unified metrics targets, but emitted workload IDs must stay on the
|
|||
canonical `/workloads` row contract so lifecycle settings, reporting, and
|
||||
handoff surfaces never depend on provider-native metric keys.
|
||||
That same lifecycle-owned settings slice now also owns the shared VMware
|
||||
summary and handoff framing. `InfrastructurePlatformConnectionsSummaryCard.tsx`,
|
||||
`InfrastructureReportingPanel.tsx`, `useInfrastructureSettingsState.ts`, and
|
||||
handoff framing. `InfrastructureWorkspace.tsx`,
|
||||
`PlatformConnectionsWorkspace.tsx`, `useInfrastructureSettingsState.ts`, and
|
||||
`useSettingsInfrastructurePanelProps.ts` must surface VMware availability and
|
||||
connection counts from the same shared infrastructure settings state that owns
|
||||
the VMware panel itself, rather than letting reporting cards or adjacent setup
|
||||
surfaces grow a second VMware availability fetch or a VMware-only handoff path.
|
||||
the VMware panel itself, rather than letting adjacent setup surfaces grow a
|
||||
second VMware availability fetch or a VMware-only handoff path.
|
||||
That same infrastructure workspace boundary now also owns the first-run
|
||||
handoff copy for new operators. `InfrastructureWorkspace.tsx` must tell a new
|
||||
Pulse user to start with `Install on a host` to add the first monitored
|
||||
system, while still presenting `Platform connections` as the explicit
|
||||
API-backed alternative path instead of leaving first-session install guidance
|
||||
implicit in generic settings-shell prose or retreating to one provider's name
|
||||
as the primary alternative.
|
||||
handoff copy for new operators. `InfrastructureWorkspace.tsx` must keep
|
||||
`Install on a host` visible as the first monitored-system path while still
|
||||
presenting `Platform connections` as the explicit API-backed alternative
|
||||
instead of leaving first-session install guidance implicit in generic
|
||||
settings-shell prose or retreating to one provider's name as the primary
|
||||
alternative.
|
||||
When that infrastructure workspace needs to redirect operators to the Pulse Pro
|
||||
surface for billing, monitored-system limits, or license status, it must
|
||||
consume the settings-owned referral copy from
|
||||
|
|
|
|||
|
|
@ -443,7 +443,12 @@ the canonical monitored-system blocked payload.
|
|||
must preserve the transport distinction between machine-managed surfaces
|
||||
(`agent`, `docker`, `kubernetes`) and platform-connections-managed
|
||||
surfaces (`proxmox`, `pbs`, `pmg`, `truenas`) instead of collapsing them
|
||||
into one uninstall/stop-monitoring model.
|
||||
into one uninstall/stop-monitoring model. That same shared payload
|
||||
contract must also preserve guest-linked host identity on connected
|
||||
infrastructure and removed-host records through `linkedVmId` and
|
||||
`linkedContainerId`, so settings consumers can keep the top connections
|
||||
ledger scoped to top-level infrastructure without re-deriving guest status
|
||||
from names or local heuristics.
|
||||
24. Keep AI settings payload continuity explicit on the shared `/api/settings/ai`
|
||||
surface: `internal/api/ai_handlers.go` and `internal/api/contract_test.go`
|
||||
must expose masked provider-auth state such as `ollama_username` and
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ work extends shared components instead of creating new local variants.
|
|||
helper plus runtime `monitored_system_capacity` reads rather than
|
||||
reconstructing raw `current / limit` slash math or `0 remaining` copy in
|
||||
the banner shell, state owner, or shared model.
|
||||
5. Keep shared platform-connections shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`, `frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, and `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source instead of creating provider-local summary fetches or VMware-only shell vocabulary.
|
||||
5. Keep shared platform-connections shell state on the reusable settings boundary: `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, and `frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx` must continue to derive provider counts, availability, and shared subtab copy from one infrastructure-settings source instead of creating provider-local summary fetches or VMware-only shell vocabulary.
|
||||
6. Keep Proxmox deep-link route selection on the shared settings-navigation boundary. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` and `frontend-modern/src/components/Settings/useSettingsNavigation.ts` must treat the canonical PBS and PMG Proxmox deep links as agent-selection authority even though those URLs resolve to the shared `infrastructure-operations` tab. Reloading or remounting on a PBS or PMG deep link must not silently fall back to the PVE selector state.
|
||||
7. Keep shared storage feature presenters on canonical platform truth. When reusable storage presenters under `frontend-modern/src/features/storageBackups/` classify canonical resources for the shared storage route, API-backed virtualization datastores such as VMware must stay inventory-only datastores instead of inheriting PBS-specific backup-repository or protected-target copy from older fallback branches.
|
||||
8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest.
|
||||
|
|
@ -518,10 +518,10 @@ connections` visible as the API-backed alternative for Proxmox and
|
|||
30. Keep the infrastructure settings platform-connections summary and provider
|
||||
workspaces on one shared state source. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`,
|
||||
`frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`,
|
||||
`frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`,
|
||||
`frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`,
|
||||
`frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`, and `frontend-modern/src/components/Settings/TrueNASSettingsPanel.tsx` must
|
||||
derive TrueNAS connection counts and availability from the shared
|
||||
infrastructure settings state instead of letting the reporting summary and
|
||||
infrastructure settings state instead of letting the top-level ledger and
|
||||
the provider-specific panel issue separate connection fetches.
|
||||
31. Keep alert-history feature composition on the current owned state contract.
|
||||
`frontend-modern/src/features/alerts/tabs/HistoryTab.tsx` must react to the
|
||||
|
|
@ -2129,7 +2129,7 @@ locks that direct-root contract so single-surface pages do not quietly regain
|
|||
redundant outer spacing chrome.
|
||||
The same shared settings-shell boundary now also owns the API-backed
|
||||
alternative path inside Connections & Inventory. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`,
|
||||
`frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`,
|
||||
`frontend-modern/src/components/Settings/settingsHeaderMeta.ts`,
|
||||
`frontend-modern/src/components/Settings/settingsNavigationModel.ts`, `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`,
|
||||
`frontend-modern/src/utils/infrastructureEmptyStatePresentation.ts`, and adjacent setup guidance must
|
||||
treat `Platform connections` as the canonical API-backed alternative for
|
||||
|
|
@ -2138,11 +2138,11 @@ top-level `Direct Proxmox` wording or shell-local provider routes.
|
|||
That same settings-shell contract also owns the shared platform-connections
|
||||
summary state. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`,
|
||||
`frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`,
|
||||
`frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx`,
|
||||
`frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`,
|
||||
`frontend-modern/src/components/Settings/PlatformConnectionsWorkspace.tsx`, `frontend-modern/src/components/Settings/TrueNASSettingsPanel.tsx`, and
|
||||
`frontend-modern/src/components/Settings/VMwareSettingsPanel.tsx` must derive Proxmox/PBS/PMG/TrueNAS/VMware counts
|
||||
and availability from one shared infrastructure settings state source instead
|
||||
of letting the reporting summary and the provider-specific panels fetch the
|
||||
of letting the top-level ledger and the provider-specific panels fetch the
|
||||
same connection state separately.
|
||||
That same shared settings-shell boundary also owns provider parity inside the
|
||||
platform workspace. Adding VMware to the shared `Platform connections`
|
||||
|
|
|
|||
|
|
@ -698,7 +698,12 @@ carry TrueNAS hostname/version through the shared top-level system grouping,
|
|||
and preserve platform-managed surfaces such as `proxmox`, `pbs`, `pmg`, and
|
||||
`truenas` when host telemetry is ignored. Ignore/remove semantics on that
|
||||
surface remain machine-scoped and may only strip the local `agent`, `docker`,
|
||||
and `kubernetes` reporting surfaces from the grouped row.
|
||||
and `kubernetes` reporting surfaces from the grouped row. That same
|
||||
connected-infrastructure payload now also owns guest-link continuity for host
|
||||
agents: when an agent is running inside a VM or system container, monitoring
|
||||
must preserve the canonical linked guest identity on both active and ignored
|
||||
connected-infrastructure rows instead of forcing settings consumers to infer
|
||||
guest-backed hosts from labels or hostnames.
|
||||
path or treat API-backed app workloads as second-class compared with native
|
||||
Docker reports.
|
||||
That same boundary now also owns native host-history fallback for API-backed
|
||||
|
|
|
|||
|
|
@ -605,12 +605,9 @@
|
|||
"frontend-modern/src/components/Settings/InfrastructureActiveRowDetails.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureIgnoredRowDetails.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInventorySection.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx",
|
||||
"frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/infrastructureSettingsModel.ts",
|
||||
"frontend-modern/src/components/Settings/InfrastructureStopMonitoringDialog.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
|
||||
|
|
@ -901,9 +898,6 @@
|
|||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx",
|
||||
"frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx",
|
||||
"frontend-modern/src/components/Settings/infrastructureSettingsModel.ts",
|
||||
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
|
||||
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ export interface RemovedHostAgent {
|
|||
id: string;
|
||||
hostname?: string;
|
||||
displayName?: string;
|
||||
linkedVmId?: string;
|
||||
linkedContainerId?: string;
|
||||
removedAt: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
import { Component, For } from 'solid-js';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import type { ConnectionKind, ConnectionMethod } from './connectionsTableModel';
|
||||
|
||||
export type AddSystemChoiceKind = 'pve' | 'pbs' | 'pmg' | 'truenas' | 'vmware' | 'agent';
|
||||
export type AddSystemChoiceMethod = 'api' | 'agent';
|
||||
|
||||
export interface AddSystemChoice {
|
||||
kind: ConnectionKind;
|
||||
kind: AddSystemChoiceKind;
|
||||
title: string;
|
||||
description: string;
|
||||
method: ConnectionMethod;
|
||||
method: AddSystemChoiceMethod;
|
||||
methodLabel: string;
|
||||
}
|
||||
|
||||
export const ADD_SYSTEM_CHOICES: readonly AddSystemChoice[] = [
|
||||
{
|
||||
kind: 'agent',
|
||||
title: 'Install on a host',
|
||||
description:
|
||||
'Generate an install token and run the Pulse agent on the host itself. Best for the first monitored system or anything without a supported API.',
|
||||
method: 'agent',
|
||||
methodLabel: 'Agent install',
|
||||
},
|
||||
{
|
||||
kind: 'pve',
|
||||
title: 'Proxmox VE',
|
||||
|
|
@ -46,14 +56,6 @@ export const ADD_SYSTEM_CHOICES: readonly AddSystemChoice[] = [
|
|||
method: 'api',
|
||||
methodLabel: 'API connection',
|
||||
},
|
||||
{
|
||||
kind: 'agent',
|
||||
title: 'Linux or Docker host (agent)',
|
||||
description:
|
||||
'Generate an install token and run the Pulse agent on the host itself. Best for anything without a supported API.',
|
||||
method: 'agent',
|
||||
methodLabel: 'Agent install',
|
||||
},
|
||||
];
|
||||
|
||||
interface AddSystemPickerProps {
|
||||
|
|
|
|||
|
|
@ -8,30 +8,23 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/shared/Table';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import type { ConnectionRow, ConnectionStatus } from './connectionsTableModel';
|
||||
import type { ConnectionRow } from './connectionsTableModel';
|
||||
|
||||
interface ConnectionsTableProps {
|
||||
rows: Accessor<readonly ConnectionRow[]>;
|
||||
onAddSystem?: () => void;
|
||||
onManageRow?: (row: ConnectionRow) => void;
|
||||
}
|
||||
|
||||
const STATUS_DOT_CLASS: Record<ConnectionStatus, string> = {
|
||||
reporting: 'bg-emerald-500',
|
||||
pending: 'bg-amber-400',
|
||||
offline: 'bg-slate-400',
|
||||
error: 'bg-rose-500',
|
||||
unknown: 'bg-slate-300',
|
||||
};
|
||||
|
||||
export const ConnectionsTable: Component<ConnectionsTableProps> = (props) => {
|
||||
return (
|
||||
<Card padding="none" tone="card" class="rounded-md">
|
||||
<div class="flex items-center justify-between gap-3 border-b border-border px-4 py-3">
|
||||
<div class="flex flex-col gap-3 border-b border-border px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-base-content">Connections</h3>
|
||||
<h3 class="text-base font-semibold text-base-content">Connections and inventory</h3>
|
||||
<p class="text-xs text-muted">
|
||||
Every system Pulse is configured to monitor, regardless of how it reports.
|
||||
Configured platform connections, active reporting items, and ignored systems in one
|
||||
ledger.
|
||||
</p>
|
||||
</div>
|
||||
<Show when={props.onAddSystem}>
|
||||
|
|
@ -44,61 +37,96 @@ export const ConnectionsTable: Component<ConnectionsTableProps> = (props) => {
|
|||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={props.rows().length > 0}
|
||||
fallback={
|
||||
<div class="px-4 py-10 text-center text-sm text-muted">
|
||||
No systems connected yet. Use
|
||||
Nothing is configured or reporting yet. Use
|
||||
<span class="mx-1 font-medium text-base-content">+ Add a system</span>
|
||||
to connect your first one.
|
||||
to connect the first one.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="overflow-auto">
|
||||
<Table class="min-w-[max-content] w-full divide-y divide-border text-sm">
|
||||
<Table class="min-w-[1040px] w-full divide-y divide-border text-sm">
|
||||
<TableHeader class="bg-surface-alt">
|
||||
<TableRow>
|
||||
<TableHead class="py-2 pl-4 pr-3 text-left text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Name
|
||||
System
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Kind
|
||||
Coverage
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Method
|
||||
Collection
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead class="px-3 py-2 text-left text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Last reported
|
||||
Last activity
|
||||
</TableHead>
|
||||
<Show when={props.onManageRow}>
|
||||
<TableHead class="px-4 py-2 text-right text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Manage
|
||||
</TableHead>
|
||||
</Show>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class="divide-y divide-border bg-surface">
|
||||
<For each={props.rows()}>
|
||||
{(r) => (
|
||||
{(row) => (
|
||||
<TableRow class="even:bg-surface-alt">
|
||||
<TableCell class="py-2.5 pl-4 pr-3">
|
||||
<div class="font-medium text-base-content">{r.name}</div>
|
||||
<Show when={r.host && r.host !== r.name}>
|
||||
<div class="text-xs text-muted">{r.host}</div>
|
||||
</Show>
|
||||
<TableCell class="py-3 pl-4 pr-3 align-top">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="font-medium text-base-content">{row.name}</div>
|
||||
<Show when={row.host}>
|
||||
<div class="truncate text-xs text-muted">{row.host}</div>
|
||||
</Show>
|
||||
<div class="text-xs text-muted">{row.subtitle}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2.5 text-base-content">{r.kindLabel}</TableCell>
|
||||
<TableCell class="px-3 py-2.5 text-base-content">{r.methodLabel}</TableCell>
|
||||
<TableCell class="px-3 py-2.5">
|
||||
<span class="inline-flex items-center gap-2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class={`inline-block h-2 w-2 rounded-full ${STATUS_DOT_CLASS[r.status]}`}
|
||||
/>
|
||||
<span class="text-base-content">{r.statusLabel}</span>
|
||||
|
||||
<TableCell class="px-3 py-3 align-top">
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<For each={row.coverageLabels}>
|
||||
{(label) => (
|
||||
<span class="inline-flex items-center rounded-full border border-border bg-surface px-2 py-0.5 text-xs font-medium text-base-content">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="px-3 py-3 align-top text-base-content">
|
||||
{row.collectionLabel}
|
||||
</TableCell>
|
||||
|
||||
<TableCell class="px-3 py-3 align-top">
|
||||
<span
|
||||
class={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${row.statusClassName}`}
|
||||
>
|
||||
{row.statusLabel}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class="px-3 py-2.5 text-muted">
|
||||
{formatRelativeTime(r.lastReportedMs, { emptyText: '—' })}
|
||||
|
||||
<TableCell class="px-3 py-3 align-top text-muted">
|
||||
{row.lastActivityText}
|
||||
</TableCell>
|
||||
|
||||
<Show when={props.onManageRow}>
|
||||
<TableCell class="px-4 py-3 align-top text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onManageRow?.(row)}
|
||||
class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
{row.manageLabel}
|
||||
</button>
|
||||
</TableCell>
|
||||
</Show>
|
||||
</TableRow>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
|
||||
import { InfrastructureOperationsStateProvider } from './useInfrastructureOperationsState';
|
||||
|
||||
export const InfrastructureInstallPanel: Component = () => {
|
||||
return (
|
||||
<InfrastructureOperationsStateProvider embedded>
|
||||
<InfrastructureInstallerSection />
|
||||
</InfrastructureOperationsStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureInstallPanel;
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { Card } from '@/components/shared/Card';
|
||||
|
||||
interface InfrastructurePlatformConnectionsSummaryCardProps {
|
||||
pveCount: number;
|
||||
pbsCount: number;
|
||||
pmgCount: number;
|
||||
truenasCount: number;
|
||||
truenasAvailable: boolean;
|
||||
vmwareCount: number;
|
||||
vmwareAvailable: boolean;
|
||||
onManagePlatformConnections: () => void;
|
||||
}
|
||||
|
||||
export const InfrastructurePlatformConnectionsSummaryCard: Component<
|
||||
InfrastructurePlatformConnectionsSummaryCardProps
|
||||
> = (props) => {
|
||||
return (
|
||||
<Card padding="lg" class="rounded-xl border border-border shadow-sm">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-base-content">Platform connections</h3>
|
||||
<p class="text-sm text-muted">
|
||||
Manage the API-backed platforms Pulse polls directly. Proxmox VE, PBS, PMG, TrueNAS,
|
||||
and VMware all live in the same shared platform-connections workspace.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={props.onManagePlatformConnections}
|
||||
class="inline-flex min-h-10 sm:min-h-9 items-center justify-center rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Open platform connections
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
||||
<div
|
||||
class="rounded-lg border border-border bg-surface-alt px-4 py-3"
|
||||
data-testid="platform-connections-pve"
|
||||
>
|
||||
<div class="text-sm font-medium text-base-content">PVE</div>
|
||||
<div class="mt-1 text-xl font-semibold text-base-content">{props.pveCount}</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-border bg-surface-alt px-4 py-3"
|
||||
data-testid="platform-connections-pbs"
|
||||
>
|
||||
<div class="text-sm font-medium text-base-content">PBS</div>
|
||||
<div class="mt-1 text-xl font-semibold text-base-content">{props.pbsCount}</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-border bg-surface-alt px-4 py-3"
|
||||
data-testid="platform-connections-pmg"
|
||||
>
|
||||
<div class="text-sm font-medium text-base-content">PMG</div>
|
||||
<div class="mt-1 text-xl font-semibold text-base-content">{props.pmgCount}</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-border bg-surface-alt px-4 py-3"
|
||||
data-testid="platform-connections-truenas"
|
||||
>
|
||||
<div class="text-sm font-medium text-base-content">TrueNAS</div>
|
||||
<div
|
||||
class={`mt-1 ${props.truenasAvailable ? 'text-xl font-semibold text-base-content' : 'text-sm font-medium text-muted'}`}
|
||||
>
|
||||
{props.truenasAvailable ? props.truenasCount : 'Disabled'}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
{props.truenasAvailable
|
||||
? 'API-backed NAS connections'
|
||||
: 'Explicitly disabled on this Pulse server.'}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-lg border border-border bg-surface-alt px-4 py-3"
|
||||
data-testid="platform-connections-vmware"
|
||||
>
|
||||
<div class="text-sm font-medium text-base-content">VMware</div>
|
||||
<div
|
||||
class={`mt-1 ${props.vmwareAvailable ? 'text-xl font-semibold text-base-content' : 'text-sm font-medium text-muted'}`}
|
||||
>
|
||||
{props.vmwareAvailable ? props.vmwareCount : 'Disabled'}
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
{props.vmwareAvailable
|
||||
? 'vCenter platform connections'
|
||||
: 'Explicitly disabled on this Pulse server.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructurePlatformConnectionsSummaryCard;
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import type { Component } from 'solid-js';
|
||||
import { AgentProfilesPanel } from './AgentProfilesPanel';
|
||||
import { InfrastructurePlatformConnectionsSummaryCard } from './InfrastructurePlatformConnectionsSummaryCard';
|
||||
import { InfrastructureInventorySection } from './InfrastructureInventorySection';
|
||||
import { InfrastructureStopMonitoringDialog } from './InfrastructureStopMonitoringDialog';
|
||||
import { InfrastructureOperationsStateProvider } from './useInfrastructureOperationsState';
|
||||
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
|
||||
|
||||
interface InfrastructureReportingPanelProps extends InfrastructurePlatformSettingsProps {
|
||||
onManagePlatformConnections: () => void;
|
||||
}
|
||||
|
||||
export const InfrastructureReportingPanel: Component<InfrastructureReportingPanelProps> = (
|
||||
props,
|
||||
) => {
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<InfrastructureOperationsStateProvider>
|
||||
<InfrastructureStopMonitoringDialog />
|
||||
<InfrastructureInventorySection />
|
||||
</InfrastructureOperationsStateProvider>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
|
||||
<InfrastructurePlatformConnectionsSummaryCard
|
||||
pveCount={props.platformConnectionsSummary().pveCount}
|
||||
pbsCount={props.platformConnectionsSummary().pbsCount}
|
||||
pmgCount={props.platformConnectionsSummary().pmgCount}
|
||||
truenasCount={props.platformConnectionsSummary().truenasCount}
|
||||
truenasAvailable={props.platformConnectionsSummary().truenasAvailable}
|
||||
vmwareCount={props.platformConnectionsSummary().vmwareCount}
|
||||
vmwareAvailable={props.platformConnectionsSummary().vmwareAvailable}
|
||||
onManagePlatformConnections={props.onManagePlatformConnections}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AgentProfilesPanel />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InfrastructureReportingPanel;
|
||||
|
|
@ -1,122 +1,226 @@
|
|||
import { Component, Match, Show, Switch, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { Component, Show, createEffect, createMemo, createSignal } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
|
||||
import { InfrastructureInstallPanel } from './InfrastructureInstallPanel';
|
||||
import { InfrastructureReportingPanel } from './InfrastructureReportingPanel';
|
||||
import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace';
|
||||
import { ConnectionsTable } from './ConnectionsTable';
|
||||
import { AgentProfilesPanel } from './AgentProfilesPanel';
|
||||
import { AddSystemPicker, type AddSystemChoice } from './AddSystemPicker';
|
||||
import { buildConnectionRows, type ConnectionRow } from './connectionsTableModel';
|
||||
import { ConnectionsTable } from './ConnectionsTable';
|
||||
import {
|
||||
buildConnectionRows,
|
||||
type ConnectionRow,
|
||||
type ConnectionManageAction,
|
||||
} from './connectionsTableModel';
|
||||
import { InfrastructureInstallerSection } from './InfrastructureInstallerSection';
|
||||
import { InfrastructureInventorySection } from './InfrastructureInventorySection';
|
||||
import { InfrastructureStopMonitoringDialog } from './InfrastructureStopMonitoringDialog';
|
||||
import { PlatformConnectionsWorkspace } from './PlatformConnectionsWorkspace';
|
||||
import {
|
||||
buildInfrastructureWorkspacePath,
|
||||
getInfrastructureWorkspaceViewFromPath,
|
||||
} from './infrastructureWorkspaceModel';
|
||||
import type { InfrastructurePlatformSettingsProps } from './proxmoxSettingsModel';
|
||||
import {
|
||||
InfrastructureOperationsStateProvider,
|
||||
useInfrastructureOperationsContext,
|
||||
} from './useInfrastructureOperationsState';
|
||||
|
||||
export type InfrastructureWorkspaceProps = InfrastructurePlatformSettingsProps;
|
||||
|
||||
export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> = (props) => {
|
||||
const scrollSectionIntoView = (section?: HTMLDivElement) => {
|
||||
if (
|
||||
typeof window === 'undefined' ||
|
||||
!section ||
|
||||
typeof section.scrollIntoView !== 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
section.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
||||
});
|
||||
};
|
||||
|
||||
const proxmoxRouteForKind = (kind: 'pve' | 'pbs' | 'pmg') =>
|
||||
`/settings/infrastructure/platforms/proxmox/${kind}`;
|
||||
|
||||
const InfrastructureWorkspaceContent: Component<InfrastructureWorkspaceProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const state = useInfrastructureOperationsContext();
|
||||
|
||||
const activeView = createMemo(() => getInfrastructureWorkspaceViewFromPath(location.pathname));
|
||||
const readOnlyWorkspace = createMemo(() => presentationPolicyIsReadOnly());
|
||||
const [pickerOpen, setPickerOpen] = createSignal(false);
|
||||
|
||||
let inventorySectionRef: HTMLDivElement | undefined;
|
||||
let platformSectionRef: HTMLDivElement | undefined;
|
||||
let installSectionRef: HTMLDivElement | undefined;
|
||||
|
||||
const rows = createMemo<ConnectionRow[]>(() =>
|
||||
buildConnectionRows({
|
||||
activeRows: state.activeRows(),
|
||||
monitoringStoppedRows: state.monitoringStoppedRows(),
|
||||
pveNodes: props.pveNodes(),
|
||||
pbsNodes: props.pbsNodes(),
|
||||
pmgNodes: props.pmgNodes(),
|
||||
truenasConnections: props.trueNASSettings.connections(),
|
||||
vmwareConnections: props.vmwareSettings.connections(),
|
||||
agentResources: props.agentStateResources?.() ?? [],
|
||||
includeConfigurationRows: !readOnlyWorkspace(),
|
||||
}),
|
||||
);
|
||||
|
||||
const openProxmoxNode = (nodeKind: 'pve' | 'pbs' | 'pmg', nodeId: string) => {
|
||||
const nodes =
|
||||
nodeKind === 'pve'
|
||||
? props.pveNodes()
|
||||
: nodeKind === 'pbs'
|
||||
? props.pbsNodes()
|
||||
: props.pmgNodes();
|
||||
const node = nodes.find((candidate) => candidate.id === nodeId) ?? null;
|
||||
|
||||
props.onSelectAgent(nodeKind);
|
||||
props.setCurrentNodeType(nodeKind);
|
||||
props.setEditingNode(node);
|
||||
props.setModalResetKey((value) => value + 1);
|
||||
props.setShowNodeModal(true);
|
||||
navigate(proxmoxRouteForKind(nodeKind));
|
||||
scrollSectionIntoView(platformSectionRef);
|
||||
};
|
||||
|
||||
const handleAddSystem = (choice: AddSystemChoice) => {
|
||||
setPickerOpen(false);
|
||||
|
||||
if (choice.kind === 'agent') {
|
||||
navigate('/settings/infrastructure/install');
|
||||
navigate(buildInfrastructureWorkspacePath('install'));
|
||||
scrollSectionIntoView(installSectionRef);
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.kind === 'truenas') {
|
||||
props.trueNASSettings.openCreateDialog();
|
||||
navigate('/settings/infrastructure/platforms/truenas');
|
||||
scrollSectionIntoView(platformSectionRef);
|
||||
return;
|
||||
}
|
||||
|
||||
if (choice.kind === 'vmware') {
|
||||
props.vmwareSettings.openCreateDialog();
|
||||
navigate('/settings/infrastructure/platforms/vmware');
|
||||
scrollSectionIntoView(platformSectionRef);
|
||||
return;
|
||||
}
|
||||
props.onSelectAgent(choice.kind);
|
||||
navigate(`/settings/infrastructure/platforms/proxmox/${choice.kind}`);
|
||||
|
||||
openProxmoxNode(choice.kind, '');
|
||||
};
|
||||
|
||||
const handleManageAction = (action: ConnectionManageAction) => {
|
||||
switch (action.kind) {
|
||||
case 'inventory-active':
|
||||
state.setExpandedRowKey(action.rowKey);
|
||||
navigate(buildInfrastructureWorkspacePath('operations'));
|
||||
scrollSectionIntoView(inventorySectionRef);
|
||||
return;
|
||||
case 'inventory-ignored':
|
||||
state.setSelectedIgnoredRowKey(action.rowKey);
|
||||
navigate(buildInfrastructureWorkspacePath('operations'));
|
||||
scrollSectionIntoView(inventorySectionRef);
|
||||
return;
|
||||
case 'proxmox-node':
|
||||
openProxmoxNode(action.nodeKind, action.nodeId);
|
||||
return;
|
||||
case 'truenas-connection': {
|
||||
const connection = props
|
||||
.trueNASSettings
|
||||
.connections()
|
||||
.find((candidate) => candidate.id === action.connectionId);
|
||||
if (connection) {
|
||||
props.trueNASSettings.openEditDialog(connection);
|
||||
}
|
||||
navigate('/settings/infrastructure/platforms/truenas');
|
||||
scrollSectionIntoView(platformSectionRef);
|
||||
return;
|
||||
}
|
||||
case 'vmware-connection': {
|
||||
const connection = props
|
||||
.vmwareSettings
|
||||
.connections()
|
||||
.find((candidate) => candidate.id === action.connectionId);
|
||||
if (connection) {
|
||||
props.vmwareSettings.openEditDialog(connection);
|
||||
}
|
||||
navigate('/settings/infrastructure/platforms/vmware');
|
||||
scrollSectionIntoView(platformSectionRef);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (readOnlyWorkspace() && activeView() === 'install') {
|
||||
if (readOnlyWorkspace() && activeView() !== 'inventory') {
|
||||
navigate(buildInfrastructureWorkspacePath('inventory'), { replace: true });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const subviewHeading = createMemo(() => {
|
||||
switch (activeView()) {
|
||||
case 'install':
|
||||
return 'Install on a host';
|
||||
case 'platforms':
|
||||
return 'Platform connections';
|
||||
case 'operations':
|
||||
return 'Reporting';
|
||||
default:
|
||||
return '';
|
||||
const view = activeView();
|
||||
if (view === 'inventory') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'operations') {
|
||||
scrollSectionIntoView(inventorySectionRef);
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'platforms') {
|
||||
scrollSectionIntoView(platformSectionRef);
|
||||
return;
|
||||
}
|
||||
|
||||
if (view === 'install') {
|
||||
scrollSectionIntoView(installSectionRef);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<Show when={activeView() !== 'inventory'}>
|
||||
<div class="flex items-center gap-2 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(buildInfrastructureWorkspacePath('inventory'))}
|
||||
class="inline-flex items-center gap-1 rounded-md border border-border bg-surface px-2.5 py-1 text-muted hover:bg-surface-hover hover:text-base-content"
|
||||
>
|
||||
<span aria-hidden="true">←</span>
|
||||
<span>Connections and Inventory</span>
|
||||
</button>
|
||||
<span class="text-muted" aria-hidden="true">/</span>
|
||||
<span class="font-medium text-base-content">{subviewHeading()}</span>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="space-y-8">
|
||||
<ConnectionsTable
|
||||
rows={rows}
|
||||
onAddSystem={readOnlyWorkspace() ? undefined : () => setPickerOpen(true)}
|
||||
onManageRow={(row) => handleManageAction(row.manage)}
|
||||
/>
|
||||
|
||||
<Switch>
|
||||
<Match when={activeView() === 'inventory'}>
|
||||
<ConnectionsTable
|
||||
rows={rows}
|
||||
onAddSystem={readOnlyWorkspace() ? undefined : () => setPickerOpen(true)}
|
||||
/>
|
||||
<AddSystemPicker
|
||||
isOpen={pickerOpen()}
|
||||
onClose={() => setPickerOpen(false)}
|
||||
onSelect={handleAddSystem}
|
||||
/>
|
||||
</Match>
|
||||
<AddSystemPicker
|
||||
isOpen={pickerOpen()}
|
||||
onClose={() => setPickerOpen(false)}
|
||||
onSelect={handleAddSystem}
|
||||
/>
|
||||
|
||||
<Match when={activeView() === 'install'}>
|
||||
<InfrastructureInstallPanel />
|
||||
</Match>
|
||||
<InfrastructureStopMonitoringDialog />
|
||||
|
||||
<Match when={activeView() === 'platforms'}>
|
||||
<div ref={inventorySectionRef}>
|
||||
<InfrastructureInventorySection />
|
||||
</div>
|
||||
|
||||
<Show when={!readOnlyWorkspace()}>
|
||||
<div ref={platformSectionRef} class="space-y-6">
|
||||
<PlatformConnectionsWorkspace {...props} />
|
||||
</Match>
|
||||
</div>
|
||||
|
||||
<Match when={activeView() === 'operations'}>
|
||||
<InfrastructureReportingPanel
|
||||
{...props}
|
||||
onManagePlatformConnections={() =>
|
||||
navigate('/settings/infrastructure/platforms')
|
||||
}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div ref={installSectionRef}>
|
||||
<InfrastructureInstallerSection />
|
||||
</div>
|
||||
|
||||
<AgentProfilesPanel />
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfrastructureWorkspace: Component<InfrastructureWorkspaceProps> = (props) => {
|
||||
return (
|
||||
<InfrastructureOperationsStateProvider embedded>
|
||||
<InfrastructureWorkspaceContent {...props} />
|
||||
</InfrastructureOperationsStateProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,16 +4,17 @@ import { ConnectionsTable } from '../ConnectionsTable';
|
|||
import type { ConnectionRow } from '../connectionsTableModel';
|
||||
|
||||
const row = (overrides: Partial<ConnectionRow> = {}): ConnectionRow => ({
|
||||
id: 'pve:pve-1',
|
||||
kind: 'pve',
|
||||
kindLabel: 'Proxmox VE',
|
||||
method: 'api',
|
||||
methodLabel: 'API',
|
||||
id: 'row-1',
|
||||
name: 'production-pve',
|
||||
subtitle: 'Configured platform connection',
|
||||
host: '10.0.0.1',
|
||||
status: 'reporting',
|
||||
statusLabel: 'Reporting',
|
||||
lastReportedMs: Date.now() - 5_000,
|
||||
coverageLabels: ['Proxmox VE data'],
|
||||
collectionLabel: 'API',
|
||||
statusLabel: 'Connected',
|
||||
statusClassName: 'bg-green-100 text-green-800',
|
||||
lastActivityText: '5s ago',
|
||||
manageLabel: 'Edit connection',
|
||||
manage: { kind: 'proxmox-node', nodeKind: 'pve', nodeId: 'pve-1' },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
@ -24,23 +25,26 @@ describe('ConnectionsTable', () => {
|
|||
render(() => (
|
||||
<ConnectionsTable rows={() => []} />
|
||||
) as any);
|
||||
expect(screen.getByText(/No systems connected yet/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(/Nothing is configured or reporting yet/i)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('table')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders one row per connection with kind, method, and status labels', () => {
|
||||
it('renders one row per connection with coverage, collection, and status labels', () => {
|
||||
render(() => (
|
||||
<ConnectionsTable
|
||||
rows={() => [
|
||||
row(),
|
||||
row({
|
||||
id: 'agent:tower',
|
||||
kind: 'agent',
|
||||
kindLabel: 'Agent host',
|
||||
method: 'agent',
|
||||
methodLabel: 'Agent',
|
||||
id: 'row-2',
|
||||
name: 'tower',
|
||||
subtitle: 'Live reporting item',
|
||||
host: undefined,
|
||||
coverageLabels: ['Host telemetry', 'Docker runtime data'],
|
||||
collectionLabel: 'Agent',
|
||||
statusLabel: 'Reporting',
|
||||
manageLabel: 'View details',
|
||||
manage: { kind: 'inventory-active', rowKey: 'agent-tower' },
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
|
|
@ -49,9 +53,13 @@ describe('ConnectionsTable', () => {
|
|||
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||
expect(screen.getByText('production-pve')).toBeInTheDocument();
|
||||
expect(screen.getByText('tower')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Reporting')).toHaveLength(2);
|
||||
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
|
||||
expect(screen.getByText('Agent host')).toBeInTheDocument();
|
||||
expect(screen.getByText('Proxmox VE data')).toBeInTheDocument();
|
||||
expect(screen.getByText('Host telemetry')).toBeInTheDocument();
|
||||
expect(screen.getByText('Docker runtime data')).toBeInTheDocument();
|
||||
expect(screen.getByText('API')).toBeInTheDocument();
|
||||
expect(screen.getByText('Agent')).toBeInTheDocument();
|
||||
expect(screen.getByText('Connected')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reporting')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces the add-system action only when an onAddSystem handler is provided', () => {
|
||||
|
|
@ -64,4 +72,19 @@ describe('ConnectionsTable', () => {
|
|||
fireEvent.click(button);
|
||||
expect(onAddSystem).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('routes per-row manage actions through the provided callback', () => {
|
||||
const onManageRow = vi.fn();
|
||||
render(() => (
|
||||
<ConnectionsTable rows={() => [row()]} onManageRow={onManageRow} />
|
||||
) as any);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Edit connection' }));
|
||||
expect(onManageRow).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'row-1',
|
||||
manage: { kind: 'proxmox-node', nodeKind: 'pve', nodeId: 'pve-1' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,14 +3,10 @@ import { render, fireEvent, screen, waitFor, cleanup, within } from '@solidjs/te
|
|||
import { createSignal } from 'solid-js';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { Router, Route } from '@solidjs/router';
|
||||
import { InfrastructurePlatformConnectionsSummaryCard } from '../InfrastructurePlatformConnectionsSummaryCard';
|
||||
import { InfrastructureOperationsController } from '../InfrastructureOperationsController';
|
||||
import infrastructureInstallPanelSource from '../InfrastructureInstallPanel.tsx?raw';
|
||||
import infrastructureInstallerSectionSource from '../InfrastructureInstallerSection.tsx?raw';
|
||||
import infrastructureOperationsControllerSource from '../InfrastructureOperationsController.tsx?raw';
|
||||
import infrastructureOperationsModelSource from '../infrastructureOperationsModel.tsx?raw';
|
||||
import infrastructureReportingPanelSource from '../InfrastructureReportingPanel.tsx?raw';
|
||||
import infrastructurePlatformConnectionsSummaryCardSource from '../InfrastructurePlatformConnectionsSummaryCard.tsx?raw';
|
||||
import infrastructureInventorySectionSource from '../InfrastructureInventorySection.tsx?raw';
|
||||
import infrastructureInstallStateSource from '../useInfrastructureInstallState.tsx?raw';
|
||||
import infrastructureOperationsStateSource from '../useInfrastructureOperationsState.tsx?raw';
|
||||
|
|
@ -97,18 +93,6 @@ describe('InfrastructureOperationsController ownership guardrails', () => {
|
|||
expect(infrastructureOperationsControllerSource).toContain(
|
||||
'InfrastructureStopMonitoringDialog',
|
||||
);
|
||||
expect(infrastructureInstallPanelSource).toContain('InfrastructureOperationsStateProvider');
|
||||
expect(infrastructureInstallPanelSource).toContain('InfrastructureInstallerSection');
|
||||
expect(infrastructureReportingPanelSource).toContain('InfrastructureOperationsStateProvider');
|
||||
expect(infrastructureReportingPanelSource).toContain('InfrastructureInventorySection');
|
||||
expect(infrastructureReportingPanelSource).toContain('InfrastructureStopMonitoringDialog');
|
||||
expect(infrastructureReportingPanelSource).toContain(
|
||||
'./InfrastructurePlatformConnectionsSummaryCard',
|
||||
);
|
||||
expect(infrastructureReportingPanelSource).not.toContain('Platform connections');
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain('Platform connections');
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain('TrueNAS');
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain('VMware');
|
||||
expect(infrastructureOperationsStateSource).toContain('./infrastructureOperationsModel');
|
||||
expect(infrastructureOperationsStateSource).toContain('./useInfrastructureInstallState');
|
||||
expect(infrastructureOperationsStateSource).toContain('./useInfrastructureReportingState');
|
||||
|
|
@ -164,61 +148,6 @@ describe('InfrastructureOperationsController ownership guardrails', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('InfrastructurePlatformConnectionsSummaryCard', () => {
|
||||
it('renders TrueNAS as a counted first-class platform connection', () => {
|
||||
const onManagePlatformConnections = vi.fn();
|
||||
|
||||
render(() => (
|
||||
<InfrastructurePlatformConnectionsSummaryCard
|
||||
pveCount={1}
|
||||
pbsCount={2}
|
||||
pmgCount={3}
|
||||
truenasCount={4}
|
||||
truenasAvailable={true}
|
||||
vmwareCount={5}
|
||||
vmwareAvailable={true}
|
||||
onManagePlatformConnections={onManagePlatformConnections}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('PVE')).toBeInTheDocument();
|
||||
expect(screen.getByText('PBS')).toBeInTheDocument();
|
||||
expect(screen.getByText('PMG')).toBeInTheDocument();
|
||||
expect(screen.getByText('TrueNAS')).toBeInTheDocument();
|
||||
expect(screen.getByText('VMware')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('platform-connections-truenas')).toHaveTextContent('4');
|
||||
expect(screen.getByTestId('platform-connections-vmware')).toHaveTextContent('5');
|
||||
expect(screen.getByText('API-backed NAS connections')).toBeInTheDocument();
|
||||
expect(screen.getByText('vCenter platform connections')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open platform connections' }));
|
||||
expect(onManagePlatformConnections).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows when the TrueNAS integration is disabled instead of implying zero configured systems', () => {
|
||||
render(() => (
|
||||
<InfrastructurePlatformConnectionsSummaryCard
|
||||
pveCount={0}
|
||||
pbsCount={0}
|
||||
pmgCount={0}
|
||||
truenasCount={0}
|
||||
truenasAvailable={false}
|
||||
vmwareCount={0}
|
||||
vmwareAvailable={false}
|
||||
onManagePlatformConnections={() => {}}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(
|
||||
within(screen.getByTestId('platform-connections-truenas')).getByText('Disabled'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(screen.getByTestId('platform-connections-vmware')).getByText('Disabled'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Explicitly disabled on this Pulse server.')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
vi.mock('@/contexts/appRuntime', () => ({
|
||||
useWebSocket: () => mockWsStore,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ describe('infrastructure operations model', () => {
|
|||
name: 'node-a',
|
||||
hostname: 'node-a.internal',
|
||||
status: 'active',
|
||||
linkedVmId: '101',
|
||||
scopeAgentId: 'agent-1',
|
||||
surfaces: [
|
||||
{
|
||||
|
|
@ -51,6 +52,7 @@ describe('infrastructure operations model', () => {
|
|||
expect(row.rowKey).toBe('agent-agent-1');
|
||||
expect(row.capabilities).toEqual(['agent', 'pbs']);
|
||||
expect(row.installFlags).toEqual(['--enable-proxmox', '--proxmox-type pbs']);
|
||||
expect(row.linkedVmId).toBe('101');
|
||||
expect(row.searchText).toContain('node-a.internal');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { UnifiedAgentRow } from '../infrastructureOperationsModel';
|
||||
import { InfrastructureWorkspace } from '../InfrastructureWorkspace';
|
||||
|
||||
let mockPathname = '/settings/infrastructure';
|
||||
const navigateSpy = vi.hoisted(() => vi.fn());
|
||||
const presentationPolicyIsReadOnlyMock = vi.hoisted(() => vi.fn(() => false));
|
||||
const setExpandedRowKeySpy = vi.hoisted(() => vi.fn());
|
||||
const setSelectedIgnoredRowKeySpy = vi.hoisted(() => vi.fn());
|
||||
|
||||
let mockActiveRows: UnifiedAgentRow[] = [];
|
||||
let mockIgnoredRows: UnifiedAgentRow[] = [];
|
||||
|
||||
vi.mock('@solidjs/router', async () => {
|
||||
const actual = await vi.importActual<typeof import('@solidjs/router')>('@solidjs/router');
|
||||
|
|
@ -19,39 +25,96 @@ vi.mock('@/stores/sessionPresentationPolicy', () => ({
|
|||
presentationPolicyIsReadOnly: () => presentationPolicyIsReadOnlyMock(),
|
||||
}));
|
||||
|
||||
vi.mock('../InfrastructureInstallPanel', () => ({
|
||||
InfrastructureInstallPanel: () => <div data-testid="install-panel">install</div>,
|
||||
vi.mock('../useInfrastructureOperationsState', () => ({
|
||||
InfrastructureOperationsStateProvider: (props: { children: unknown }) => <>{props.children}</>,
|
||||
useInfrastructureOperationsContext: () => ({
|
||||
activeRows: () => mockActiveRows,
|
||||
monitoringStoppedRows: () => mockIgnoredRows,
|
||||
setExpandedRowKey: setExpandedRowKeySpy,
|
||||
setSelectedIgnoredRowKey: setSelectedIgnoredRowKeySpy,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../InfrastructureReportingPanel', () => ({
|
||||
InfrastructureReportingPanel: () => <div data-testid="reporting-panel">operations</div>,
|
||||
vi.mock('../InfrastructureInventorySection', () => ({
|
||||
InfrastructureInventorySection: () => <div data-testid="inventory-section">inventory</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../InfrastructureInstallerSection', () => ({
|
||||
InfrastructureInstallerSection: () => <div data-testid="install-section">install</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../PlatformConnectionsWorkspace', () => ({
|
||||
PlatformConnectionsWorkspace: () => <div data-testid="platform-connections">platforms</div>,
|
||||
PlatformConnectionsWorkspace: () => <div data-testid="platform-section">platforms</div>,
|
||||
}));
|
||||
|
||||
vi.mock('../InfrastructureStopMonitoringDialog', () => ({
|
||||
InfrastructureStopMonitoringDialog: () => <div data-testid="stop-monitoring-dialog" />,
|
||||
}));
|
||||
|
||||
vi.mock('../AgentProfilesPanel', () => ({
|
||||
AgentProfilesPanel: () => <div data-testid="agent-profiles">profiles</div>,
|
||||
}));
|
||||
|
||||
const reportingRow = (overrides: Partial<UnifiedAgentRow> = {}): UnifiedAgentRow =>
|
||||
({
|
||||
rowKey: 'agent:tower',
|
||||
id: 'tower',
|
||||
name: 'tower',
|
||||
hostname: 'tower.local',
|
||||
capabilities: ['agent'],
|
||||
status: 'active',
|
||||
healthStatus: 'online',
|
||||
lastSeen: Date.now(),
|
||||
upgradePlatform: 'linux',
|
||||
scope: { label: 'Default', category: 'default' },
|
||||
installFlags: [],
|
||||
searchText: 'tower',
|
||||
surfaces: [
|
||||
{
|
||||
key: 'agent',
|
||||
kind: 'agent',
|
||||
label: 'Host telemetry',
|
||||
detail: 'Host telemetry',
|
||||
action: 'stop-monitoring',
|
||||
controlId: 'tower',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
}) as UnifiedAgentRow;
|
||||
|
||||
const trueNASOpenCreateDialogSpy = vi.fn();
|
||||
const trueNASOpenEditDialogSpy = vi.fn();
|
||||
const vmwareOpenCreateDialogSpy = vi.fn();
|
||||
const vmwareOpenEditDialogSpy = vi.fn();
|
||||
const setShowNodeModalSpy = vi.fn();
|
||||
const setEditingNodeSpy = vi.fn();
|
||||
const setCurrentNodeTypeSpy = vi.fn();
|
||||
const setModalResetKeySpy = vi.fn();
|
||||
|
||||
const onSelectAgentSpy = vi.fn();
|
||||
|
||||
const baseProps = () =>
|
||||
({
|
||||
pveNodes: () => [],
|
||||
pveNodes: () => [{ id: 'pve-1', name: 'zeus', host: '10.0.0.1', type: 'pve', status: 'connected' }],
|
||||
pbsNodes: () => [],
|
||||
pmgNodes: () => [],
|
||||
agentStateResources: () => [],
|
||||
trueNASSettings: { connections: () => [] },
|
||||
vmwareSettings: { connections: () => [] },
|
||||
platformConnectionsSummary: () => ({
|
||||
pveCount: 0,
|
||||
pbsCount: 0,
|
||||
pmgCount: 0,
|
||||
truenasCount: 0,
|
||||
truenasAvailable: true,
|
||||
vmwareCount: 0,
|
||||
vmwareAvailable: true,
|
||||
}),
|
||||
trueNASSettings: {
|
||||
connections: () => [{ id: 'tn-1', name: 'Tower NAS', host: '10.0.0.20', enabled: true }],
|
||||
openCreateDialog: trueNASOpenCreateDialogSpy,
|
||||
openEditDialog: trueNASOpenEditDialogSpy,
|
||||
},
|
||||
vmwareSettings: {
|
||||
connections: () => [{ id: 'vm-1', name: 'lab-vcenter', host: '10.0.0.30', enabled: true }],
|
||||
openCreateDialog: vmwareOpenCreateDialogSpy,
|
||||
openEditDialog: vmwareOpenEditDialogSpy,
|
||||
},
|
||||
selectedAgent: () => 'pve',
|
||||
onSelectAgent: onSelectAgentSpy,
|
||||
setShowNodeModal: setShowNodeModalSpy,
|
||||
setEditingNode: setEditingNodeSpy,
|
||||
setCurrentNodeType: setCurrentNodeTypeSpy,
|
||||
setModalResetKey: setModalResetKeySpy,
|
||||
}) as any;
|
||||
|
||||
describe('InfrastructureWorkspace', () => {
|
||||
|
|
@ -59,8 +122,20 @@ describe('InfrastructureWorkspace', () => {
|
|||
navigateSpy.mockReset();
|
||||
presentationPolicyIsReadOnlyMock.mockReset();
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(false);
|
||||
setExpandedRowKeySpy.mockReset();
|
||||
setSelectedIgnoredRowKeySpy.mockReset();
|
||||
trueNASOpenCreateDialogSpy.mockReset();
|
||||
trueNASOpenEditDialogSpy.mockReset();
|
||||
vmwareOpenCreateDialogSpy.mockReset();
|
||||
vmwareOpenEditDialogSpy.mockReset();
|
||||
setShowNodeModalSpy.mockReset();
|
||||
setEditingNodeSpy.mockReset();
|
||||
setCurrentNodeTypeSpy.mockReset();
|
||||
setModalResetKeySpy.mockReset();
|
||||
onSelectAgentSpy.mockReset();
|
||||
mockPathname = '/settings/infrastructure';
|
||||
mockActiveRows = [reportingRow()];
|
||||
mockIgnoredRows = [];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -70,34 +145,23 @@ describe('InfrastructureWorkspace', () => {
|
|||
const renderWorkspace = (propOverrides: Record<string, unknown> = {}) =>
|
||||
render(() => (<InfrastructureWorkspace {...{ ...baseProps(), ...propOverrides }} />) as any);
|
||||
|
||||
it('renders the unified connections table at the base infrastructure route', () => {
|
||||
it('renders the single-page workspace sections at the base infrastructure route', () => {
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.getByText('Connections')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /Add a system/i })).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('install-panel')).toBeNull();
|
||||
expect(screen.queryByTestId('platform-connections')).toBeNull();
|
||||
expect(screen.getByText('Connections and inventory')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('inventory-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('platform-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('agent-profiles')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('merges every connection source into a single alpha-sorted table', () => {
|
||||
renderWorkspace({
|
||||
pveNodes: () => [
|
||||
{ id: 'n1', name: 'zeus', host: '10.0.0.1', type: 'pve', status: 'connected' },
|
||||
],
|
||||
agentStateResources: () => [
|
||||
{ id: 'a1', name: 'tower', displayName: 'tower', status: 'online', lastSeen: Date.now() },
|
||||
],
|
||||
trueNASSettings: {
|
||||
connections: () => [
|
||||
{ id: 't1', name: 'nas.home', host: '10.0.0.2', enabled: true, insecureSkipVerify: false, useHttps: true },
|
||||
],
|
||||
},
|
||||
});
|
||||
it('merges reporting and configured connection rows into the top ledger', () => {
|
||||
renderWorkspace();
|
||||
|
||||
const rowNames = screen.getAllByText(/zeus|tower|nas\.home/).map((el) => el.textContent);
|
||||
expect(rowNames).toContain('nas.home');
|
||||
expect(rowNames).toContain('tower');
|
||||
expect(rowNames).toContain('zeus');
|
||||
expect(screen.getByText('tower')).toBeInTheDocument();
|
||||
expect(screen.getByText('zeus')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tower NAS')).toBeInTheDocument();
|
||||
expect(screen.getByText('lab-vcenter')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the add-system picker when the add button is clicked', () => {
|
||||
|
|
@ -105,113 +169,86 @@ describe('InfrastructureWorkspace', () => {
|
|||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
|
||||
expect(screen.getByText('Linux or Docker host (agent)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Install on a host')).toBeInTheDocument();
|
||||
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
|
||||
expect(screen.getByText('TrueNAS SCALE')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('routes the agent-host choice to the dedicated install workspace', () => {
|
||||
it('routes the agent-host choice to the install section deep link', () => {
|
||||
renderWorkspace();
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
|
||||
fireEvent.click(screen.getByText('Linux or Docker host (agent)'));
|
||||
fireEvent.click(screen.getByText('Install on a host'));
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/install');
|
||||
});
|
||||
|
||||
it('routes TrueNAS and VMware choices to their platform panels', () => {
|
||||
it('opens provider creation flows directly from the add-system picker', () => {
|
||||
renderWorkspace();
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
fireEvent.click(screen.getByText('TrueNAS SCALE'));
|
||||
|
||||
expect(trueNASOpenCreateDialogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/truenas');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
fireEvent.click(screen.getByText('VMware vSphere or ESXi'));
|
||||
|
||||
expect(vmwareOpenCreateDialogSpy).toHaveBeenCalledTimes(1);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware');
|
||||
});
|
||||
|
||||
it('preselects the Proxmox kind and lands on the kind-scoped platforms route for PVE, PBS, and PMG', () => {
|
||||
it('opens the proxmox node modal directly from the add-system picker', () => {
|
||||
renderWorkspace();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
fireEvent.click(screen.getByText('Proxmox VE'));
|
||||
|
||||
expect(onSelectAgentSpy).toHaveBeenCalledWith('pve');
|
||||
expect(setCurrentNodeTypeSpy).toHaveBeenCalledWith('pve');
|
||||
expect(setEditingNodeSpy).toHaveBeenCalledWith(null);
|
||||
expect(setShowNodeModalSpy).toHaveBeenCalledWith(true);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pve');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
fireEvent.click(screen.getByText('Proxmox Backup Server'));
|
||||
expect(onSelectAgentSpy).toHaveBeenCalledWith('pbs');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pbs');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add a system/i }));
|
||||
fireEvent.click(screen.getByText('Proxmox Mail Gateway'));
|
||||
expect(onSelectAgentSpy).toHaveBeenCalledWith('pmg');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/proxmox/pmg');
|
||||
});
|
||||
|
||||
it('renders the install workspace when the URL is /install', () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
expect(screen.getByTestId('install-panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the platforms workspace when the URL is under /platforms', () => {
|
||||
mockPathname = '/settings/infrastructure/platforms/truenas';
|
||||
renderWorkspace();
|
||||
expect(screen.getByTestId('platform-connections')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps /operations reachable as a legacy detail route', () => {
|
||||
mockPathname = '/settings/infrastructure/operations';
|
||||
renderWorkspace();
|
||||
expect(screen.getByTestId('reporting-panel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a back-to-inventory header on the install subview', () => {
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
it('opens reporting details from the top ledger for live items', () => {
|
||||
renderWorkspace();
|
||||
|
||||
const backButton = screen.getByRole('button', { name: /Connections and Inventory/i });
|
||||
expect(backButton).toBeInTheDocument();
|
||||
expect(screen.getByText('Install on a host')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View details' }));
|
||||
|
||||
fireEvent.click(backButton);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
expect(setExpandedRowKeySpy).toHaveBeenCalledWith('agent:tower');
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/operations');
|
||||
});
|
||||
|
||||
it('renders a back-to-inventory header on the platforms subview', () => {
|
||||
it('opens saved VMware connections from the top ledger', () => {
|
||||
renderWorkspace({ pveNodes: () => [], trueNASSettings: { ...baseProps().trueNASSettings, connections: () => [] } });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Edit connection' }));
|
||||
|
||||
expect(vmwareOpenEditDialogSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'vm-1', name: 'lab-vcenter' }),
|
||||
);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure/platforms/vmware');
|
||||
});
|
||||
|
||||
it('keeps the same single-page surface on platform deep links', () => {
|
||||
mockPathname = '/settings/infrastructure/platforms/truenas';
|
||||
renderWorkspace();
|
||||
|
||||
const backButton = screen.getByRole('button', { name: /Connections and Inventory/i });
|
||||
expect(backButton).toBeInTheDocument();
|
||||
expect(screen.getByText('Platform connections')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(backButton);
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure');
|
||||
expect(screen.getByText('Connections and inventory')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('inventory-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('platform-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('install-section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the back-to-inventory header on the inventory landing', () => {
|
||||
mockPathname = '/settings/infrastructure';
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.queryByRole('button', { name: /^Connections and Inventory$/ })).toBeNull();
|
||||
});
|
||||
|
||||
it('hides the add-system action and redirects install routes in read-only mode', () => {
|
||||
it('collapses read-only sessions back to inventory and hides setup sections', () => {
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
mockPathname = '/settings/infrastructure/install';
|
||||
renderWorkspace();
|
||||
|
||||
expect(navigateSpy).toHaveBeenCalledWith('/settings/infrastructure', { replace: true });
|
||||
});
|
||||
|
||||
it('still renders the connections table without an add button in read-only mode', () => {
|
||||
presentationPolicyIsReadOnlyMock.mockReturnValue(true);
|
||||
mockPathname = '/settings/infrastructure';
|
||||
renderWorkspace();
|
||||
|
||||
expect(screen.getByText('Connections')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Add a system/i })).toBeNull();
|
||||
expect(screen.queryByTestId('platform-section')).toBeNull();
|
||||
expect(screen.queryByTestId('install-section')).toBeNull();
|
||||
expect(screen.queryByTestId('agent-profiles')).toBeNull();
|
||||
expect(screen.getByTestId('inventory-section')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,55 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { NodeConfigWithStatus } from '@/types/nodes';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import type { TrueNASConnection } from '@/api/truenas';
|
||||
import type { VMwareConnection } from '@/api/vmware';
|
||||
import {
|
||||
agentConnectionRow,
|
||||
buildConnectionRows,
|
||||
pbsConnectionRow,
|
||||
pmgConnectionRow,
|
||||
pveConnectionRow,
|
||||
truenasConnectionRow,
|
||||
vmwareConnectionRow,
|
||||
} from '../connectionsTableModel';
|
||||
import type { UnifiedAgentRow } from '../infrastructureOperationsModel';
|
||||
import { buildConnectionRows } from '../connectionsTableModel';
|
||||
|
||||
const activeRow = (overrides: Partial<UnifiedAgentRow> = {}): UnifiedAgentRow =>
|
||||
({
|
||||
rowKey: 'agent:tower',
|
||||
id: 'tower',
|
||||
name: 'tower',
|
||||
hostname: 'tower.local',
|
||||
capabilities: ['agent'],
|
||||
status: 'active',
|
||||
healthStatus: 'online',
|
||||
lastSeen: Date.now(),
|
||||
upgradePlatform: 'linux',
|
||||
scope: { label: 'Default', category: 'default' },
|
||||
installFlags: [],
|
||||
searchText: 'tower tower.local',
|
||||
surfaces: [
|
||||
{
|
||||
key: 'agent',
|
||||
kind: 'agent',
|
||||
label: 'Host telemetry',
|
||||
detail: 'Host telemetry',
|
||||
action: 'stop-monitoring',
|
||||
controlId: 'tower',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
}) as UnifiedAgentRow;
|
||||
|
||||
const ignoredRow = (overrides: Partial<UnifiedAgentRow> = {}): UnifiedAgentRow =>
|
||||
({
|
||||
...activeRow(),
|
||||
rowKey: 'removed:tower',
|
||||
status: 'removed',
|
||||
removedAt: Date.now(),
|
||||
surfaces: [
|
||||
{
|
||||
key: 'agent',
|
||||
kind: 'agent',
|
||||
label: 'Host telemetry',
|
||||
detail: 'Host telemetry',
|
||||
action: 'allow-reconnect',
|
||||
controlId: 'tower',
|
||||
},
|
||||
],
|
||||
...overrides,
|
||||
}) as UnifiedAgentRow;
|
||||
|
||||
const pveNode = (overrides: Partial<NodeConfigWithStatus> = {}): NodeConfigWithStatus =>
|
||||
({
|
||||
|
|
@ -51,87 +89,124 @@ const vmware = (overrides: Partial<VMwareConnection> = {}): VMwareConnection =>
|
|||
...overrides,
|
||||
}) as VMwareConnection;
|
||||
|
||||
const agentResource = (overrides: Partial<Resource> = {}): Resource =>
|
||||
({
|
||||
id: 'agent-1',
|
||||
type: 'agent',
|
||||
name: 'tower',
|
||||
displayName: 'tower',
|
||||
platformId: 'agent-tower',
|
||||
platformType: 'agent',
|
||||
sourceType: 'agent',
|
||||
status: 'online',
|
||||
lastSeen: 1700000000000,
|
||||
...overrides,
|
||||
}) as Resource;
|
||||
|
||||
describe('connectionsTableModel', () => {
|
||||
it('maps PVE / PBS / PMG node status into unified reporting states', () => {
|
||||
expect(pveConnectionRow(pveNode()).status).toBe('reporting');
|
||||
expect(pveConnectionRow(pveNode({ status: 'pending' })).status).toBe('pending');
|
||||
expect(pveConnectionRow(pveNode({ status: 'disconnected' })).status).toBe('offline');
|
||||
expect(pveConnectionRow(pveNode({ status: 'error' })).status).toBe('error');
|
||||
expect(pbsConnectionRow(pveNode({ type: 'pbs', status: 'offline' })).kind).toBe('pbs');
|
||||
expect(pmgConnectionRow(pveNode({ type: 'pmg', status: 'connected' })).kind).toBe('pmg');
|
||||
});
|
||||
|
||||
it('prefers the display name over the internal name', () => {
|
||||
const row = pveConnectionRow(pveNode({ displayName: 'Production cluster', name: 'pve-1' }));
|
||||
expect(row.name).toBe('Production cluster');
|
||||
});
|
||||
|
||||
it('treats a disabled TrueNAS or VMware connection as offline regardless of poll state', () => {
|
||||
const tnRow = truenasConnectionRow(truenas({ enabled: false, poll: { lastSuccessAt: '2026-01-01T00:00:00Z' } }));
|
||||
const vmRow = vmwareConnectionRow(vmware({ enabled: false, poll: { lastSuccessAt: '2026-01-01T00:00:00Z' } }));
|
||||
expect(tnRow.status).toBe('offline');
|
||||
expect(vmRow.status).toBe('offline');
|
||||
});
|
||||
|
||||
it('reports pending when a connection has no poll success and no failures', () => {
|
||||
expect(truenasConnectionRow(truenas()).status).toBe('pending');
|
||||
expect(vmwareConnectionRow(vmware()).status).toBe('pending');
|
||||
});
|
||||
|
||||
it('reports reporting with a lastReportedMs once a poll has succeeded', () => {
|
||||
const row = truenasConnectionRow(
|
||||
truenas({ poll: { lastSuccessAt: '2026-01-01T00:00:00Z', consecutiveFailures: 0 } }),
|
||||
);
|
||||
expect(row.status).toBe('reporting');
|
||||
expect(row.lastReportedMs).toBe(Date.parse('2026-01-01T00:00:00Z'));
|
||||
});
|
||||
|
||||
it('reports error when consecutive failures exceed the tolerance', () => {
|
||||
expect(
|
||||
truenasConnectionRow(
|
||||
truenas({ poll: { lastSuccessAt: '2026-01-01T00:00:00Z', consecutiveFailures: 5 } }),
|
||||
).status,
|
||||
).toBe('error');
|
||||
expect(
|
||||
vmwareConnectionRow(vmware({ poll: { consecutiveFailures: 1 } })).status,
|
||||
).toBe('error');
|
||||
});
|
||||
|
||||
it('maps agent resource status into unified reporting states and carries lastSeen', () => {
|
||||
expect(agentConnectionRow(agentResource()).status).toBe('reporting');
|
||||
expect(agentConnectionRow(agentResource()).lastReportedMs).toBe(1700000000000);
|
||||
expect(agentConnectionRow(agentResource({ status: 'offline' })).status).toBe('offline');
|
||||
expect(agentConnectionRow(agentResource({ status: 'degraded' })).status).toBe('error');
|
||||
expect(agentConnectionRow(agentResource({ status: 'unknown' })).status).toBe('unknown');
|
||||
});
|
||||
|
||||
it('merges every source into a single alpha-sorted row set with stable composite ids', () => {
|
||||
it('merges active reporting, ignored rows, and configured connections into one alpha-sorted ledger', () => {
|
||||
const rows = buildConnectionRows({
|
||||
activeRows: [activeRow({ name: 'tower' })],
|
||||
monitoringStoppedRows: [ignoredRow({ name: 'archive' })],
|
||||
pveNodes: [pveNode({ id: 'a', name: 'zeus' })],
|
||||
pbsNodes: [pveNode({ id: 'b', type: 'pbs', name: 'backup' })],
|
||||
pmgNodes: [],
|
||||
truenasConnections: [truenas({ id: 'c', name: 'mira' })],
|
||||
vmwareConnections: [vmware({ id: 'd', name: 'apex' })],
|
||||
agentResources: [agentResource({ id: 'e', name: 'tower', displayName: 'tower' })],
|
||||
});
|
||||
|
||||
expect(rows.map((r) => r.name)).toEqual(['apex', 'backup', 'mira', 'tower', 'zeus']);
|
||||
expect(new Set(rows.map((r) => r.id)).size).toBe(rows.length);
|
||||
expect(rows.find((r) => r.name === 'tower')?.method).toBe('agent');
|
||||
expect(rows.find((r) => r.name === 'apex')?.method).toBe('api');
|
||||
expect(rows.map((row) => row.name)).toEqual(['apex', 'archive', 'backup', 'mira', 'tower', 'zeus']);
|
||||
expect(rows.find((row) => row.name === 'archive')).toMatchObject({
|
||||
subtitle: 'Ignored by Pulse',
|
||||
manageLabel: 'Review ignored',
|
||||
});
|
||||
expect(rows.find((row) => row.name === 'tower')).toMatchObject({
|
||||
subtitle: 'Live reporting item',
|
||||
manageLabel: 'View details',
|
||||
});
|
||||
expect(rows.find((row) => row.name === 'zeus')).toMatchObject({
|
||||
subtitle: 'Configured platform connection',
|
||||
collectionLabel: 'API',
|
||||
});
|
||||
});
|
||||
|
||||
it('drops configured rows that are already represented by the reporting projection', () => {
|
||||
const rows = buildConnectionRows({
|
||||
activeRows: [
|
||||
activeRow({
|
||||
name: 'tower',
|
||||
capabilities: ['truenas'],
|
||||
surfaces: [
|
||||
{
|
||||
key: 'truenas',
|
||||
kind: 'truenas',
|
||||
label: 'TrueNAS data',
|
||||
detail: 'TrueNAS data',
|
||||
idValue: '10.0.0.20',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
monitoringStoppedRows: [],
|
||||
pveNodes: [],
|
||||
pbsNodes: [],
|
||||
pmgNodes: [],
|
||||
truenasConnections: [truenas()],
|
||||
vmwareConnections: [],
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].name).toBe('tower');
|
||||
});
|
||||
|
||||
it('can collapse to reporting-only rows for read-only sessions', () => {
|
||||
const rows = buildConnectionRows({
|
||||
activeRows: [activeRow()],
|
||||
monitoringStoppedRows: [ignoredRow()],
|
||||
pveNodes: [pveNode()],
|
||||
pbsNodes: [],
|
||||
pmgNodes: [],
|
||||
truenasConnections: [truenas()],
|
||||
vmwareConnections: [vmware()],
|
||||
includeConfigurationRows: false,
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(2);
|
||||
expect(rows.every((row) => row.subtitle !== 'Configured platform connection')).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps guest-linked agents out of the top-level infrastructure ledger', () => {
|
||||
const rows = buildConnectionRows({
|
||||
activeRows: [
|
||||
activeRow({ name: 'tower' }),
|
||||
activeRow({
|
||||
rowKey: 'agent:guest-101',
|
||||
id: 'guest-101',
|
||||
name: 'debian-go',
|
||||
linkedVmId: '101',
|
||||
}),
|
||||
],
|
||||
monitoringStoppedRows: [
|
||||
ignoredRow({
|
||||
rowKey: 'removed:guest-102',
|
||||
id: 'guest-102',
|
||||
name: 'archive-guest',
|
||||
linkedContainerId: '102',
|
||||
}),
|
||||
],
|
||||
pveNodes: [],
|
||||
pbsNodes: [],
|
||||
pmgNodes: [],
|
||||
truenasConnections: [],
|
||||
vmwareConnections: [],
|
||||
});
|
||||
|
||||
expect(rows.map((row) => row.name)).toEqual(['tower']);
|
||||
});
|
||||
|
||||
it('keeps saved VMware connections visible even though the reporting projection does not own them yet', () => {
|
||||
const rows = buildConnectionRows({
|
||||
activeRows: [],
|
||||
monitoringStoppedRows: [],
|
||||
pveNodes: [],
|
||||
pbsNodes: [],
|
||||
pmgNodes: [],
|
||||
truenasConnections: [],
|
||||
vmwareConnections: [vmware({ name: 'lab-vcenter' })],
|
||||
});
|
||||
|
||||
expect(rows).toEqual([
|
||||
expect.objectContaining({
|
||||
name: 'lab-vcenter',
|
||||
coverageLabels: ['VMware data'],
|
||||
manage: { kind: 'vmware-connection', connectionId: 'vm-1' },
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,12 +9,9 @@ import infrastructureWorkspaceSource from '../InfrastructureWorkspace.tsx?raw';
|
|||
import infrastructureWorkspaceModelSource from '../infrastructureWorkspaceModel.ts?raw';
|
||||
import platformConnectionsWorkspaceSource from '../PlatformConnectionsWorkspace.tsx?raw';
|
||||
import platformConnectionsModelSource from '../platformConnectionsModel.ts?raw';
|
||||
import infrastructureInstallPanelSource from '../InfrastructureInstallPanel.tsx?raw';
|
||||
import infrastructureInstallerSectionSource from '../InfrastructureInstallerSection.tsx?raw';
|
||||
import infrastructureOperationsControllerSource from '../InfrastructureOperationsController.tsx?raw';
|
||||
import infrastructureOperationsModelSource from '../infrastructureOperationsModel.tsx?raw';
|
||||
import infrastructureReportingPanelSource from '../InfrastructureReportingPanel.tsx?raw';
|
||||
import infrastructurePlatformConnectionsSummaryCardSource from '../InfrastructurePlatformConnectionsSummaryCard.tsx?raw';
|
||||
import trueNASSettingsPanelSource from '../TrueNASSettingsPanel.tsx?raw';
|
||||
import vmwareSettingsPanelSource from '../VMwareSettingsPanel.tsx?raw';
|
||||
import infrastructureInventorySectionSource from '../InfrastructureInventorySection.tsx?raw';
|
||||
|
|
@ -202,10 +199,7 @@ const extractedModules = [
|
|||
'../useNodeModalState.ts',
|
||||
'../InfrastructureWorkspace.tsx',
|
||||
'../infrastructureWorkspaceModel.ts',
|
||||
'../InfrastructureInstallPanel.tsx',
|
||||
'../InfrastructureInstallerSection.tsx',
|
||||
'../InfrastructureReportingPanel.tsx',
|
||||
'../InfrastructurePlatformConnectionsSummaryCard.tsx',
|
||||
'../InfrastructureInventorySection.tsx',
|
||||
'../InfrastructureActiveRowDetails.tsx',
|
||||
'../InfrastructureIgnoredRowDetails.tsx',
|
||||
|
|
@ -755,7 +749,6 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(proxmoxSettingsModelSource).toContain(
|
||||
'export interface InfrastructurePlatformSettingsProps',
|
||||
);
|
||||
expect(proxmoxSettingsModelSource).toContain('platformConnectionsSummary');
|
||||
expect(proxmoxSettingsModelSource).toContain('trueNASSettings');
|
||||
expect(proxmoxSettingsModelSource).toContain('./infrastructureSettingsModel');
|
||||
expect(proxmoxDirectWorkspaceStateSource).toContain(
|
||||
|
|
@ -889,11 +882,14 @@ describe('Settings architecture guardrails', () => {
|
|||
);
|
||||
expect(infrastructureWorkspaceSource).toContain('createEffect(() =>');
|
||||
expect(infrastructureWorkspaceSource).toContain(
|
||||
"if (readOnlyWorkspace() && activeView() === 'install')",
|
||||
"if (readOnlyWorkspace() && activeView() !== 'inventory')",
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallPanel');
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureOperationsStateProvider');
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureInventorySection');
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallerSection');
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureStopMonitoringDialog');
|
||||
expect(infrastructureWorkspaceSource).toContain('AgentProfilesPanel');
|
||||
expect(infrastructureWorkspaceSource).toContain('PlatformConnectionsWorkspace');
|
||||
expect(infrastructureWorkspaceSource).toContain('InfrastructureReportingPanel');
|
||||
expect(platformConnectionsWorkspaceSource).toContain('./platformConnectionsModel');
|
||||
expect(platformConnectionsWorkspaceSource).toContain('./ProxmoxSettingsPanel');
|
||||
expect(platformConnectionsWorkspaceSource).toContain('./TrueNASSettingsPanel');
|
||||
|
|
@ -902,16 +898,6 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(platformConnectionsModelSource).toContain(
|
||||
'export function getPlatformConnectionsViewFromPath',
|
||||
);
|
||||
expect(infrastructureInstallPanelSource).toContain('InfrastructureOperationsStateProvider');
|
||||
expect(infrastructureInstallPanelSource).toContain('InfrastructureInstallerSection');
|
||||
expect(infrastructureReportingPanelSource).toContain('InfrastructureOperationsStateProvider');
|
||||
expect(infrastructureReportingPanelSource).toContain('InfrastructureInventorySection');
|
||||
expect(infrastructureReportingPanelSource).toContain('InfrastructureStopMonitoringDialog');
|
||||
expect(infrastructureReportingPanelSource).toContain(
|
||||
'./InfrastructurePlatformConnectionsSummaryCard',
|
||||
);
|
||||
expect(infrastructureReportingPanelSource).not.toContain('Platform connections');
|
||||
expect(infrastructureReportingPanelSource).not.toContain('Manage direct connections');
|
||||
expect(infrastructureOperationsControllerSource).toContain(
|
||||
'InfrastructureOperationsStateProvider',
|
||||
);
|
||||
|
|
@ -964,14 +950,6 @@ describe('Settings architecture guardrails', () => {
|
|||
expect(infrastructureWorkspaceModelSource).toContain(
|
||||
'export function buildInfrastructureWorkspacePath',
|
||||
);
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain('Platform connections');
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain('TrueNAS');
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain('VMware');
|
||||
expect(infrastructurePlatformConnectionsSummaryCardSource).toContain(
|
||||
'Open platform connections',
|
||||
);
|
||||
expect(infrastructureInstallPanelSource).not.toContain('<PageHeader');
|
||||
expect(infrastructureReportingPanelSource).not.toContain('<PageHeader');
|
||||
});
|
||||
|
||||
it('keeps the infrastructure operations model extracted from the reactive hook owner', () => {
|
||||
|
|
@ -1414,9 +1392,10 @@ describe('Settings architecture guardrails', () => {
|
|||
source.includes('SettingsPanel') ||
|
||||
source.includes('CommercialBillingShell') ||
|
||||
(panelName === 'InfrastructureWorkspace' &&
|
||||
source.includes('./InfrastructureInstallPanel') &&
|
||||
source.includes('./InfrastructureInstallerSection') &&
|
||||
source.includes('./InfrastructureInventorySection') &&
|
||||
source.includes('./PlatformConnectionsWorkspace') &&
|
||||
source.includes('./InfrastructureReportingPanel'));
|
||||
source.includes('./ConnectionsTable'));
|
||||
expect(usesCanonicalShell, `${panelName} should use a canonical settings panel shell`).toBe(
|
||||
true,
|
||||
);
|
||||
|
|
@ -1498,7 +1477,7 @@ describe('Settings architecture guardrails', () => {
|
|||
'Setup changes stay unavailable in this read-only session.',
|
||||
);
|
||||
expect(infrastructureWorkspaceSource).toContain('presentationPolicyIsReadOnly');
|
||||
expect(infrastructureWorkspaceSource).toContain("activeView() === 'inventory'");
|
||||
expect(infrastructureWorkspaceSource).toContain("activeView() !== 'inventory'");
|
||||
});
|
||||
|
||||
it('keeps relay shell copy on the shared relay presentation owner', () => {
|
||||
|
|
|
|||
|
|
@ -137,39 +137,6 @@ describe('useSettingsInfrastructurePanelProps', () => {
|
|||
|
||||
expect(panelProps.pbsInstances()).toEqual([expect.objectContaining({ name: 'PBS Main' })]);
|
||||
expect(panelProps.pmgInstances()).toEqual([expect.objectContaining({ name: 'PMG Main' })]);
|
||||
expect(panelProps.platformConnectionsSummary()).toMatchObject({
|
||||
pveCount: 0,
|
||||
pbsCount: 0,
|
||||
pmgCount: 0,
|
||||
truenasCount: 0,
|
||||
truenasAvailable: true,
|
||||
vmwareCount: 0,
|
||||
vmwareAvailable: true,
|
||||
});
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('derives platform connection counts from the shared infrastructure settings state', () => {
|
||||
const { hookState, dispose } = mountHook([], {
|
||||
pveCount: 1,
|
||||
pbsCount: 2,
|
||||
pmgCount: 3,
|
||||
truenasConnections: [{ id: 'truenas-1' }, { id: 'truenas-2' }],
|
||||
vmwareConnections: [{ id: 'vmware-1' }],
|
||||
});
|
||||
|
||||
const panelProps = hookState.getInfrastructurePanelProps();
|
||||
|
||||
expect(panelProps.platformConnectionsSummary()).toEqual({
|
||||
pveCount: 1,
|
||||
pbsCount: 2,
|
||||
pmgCount: 3,
|
||||
truenasCount: 2,
|
||||
truenasAvailable: true,
|
||||
vmwareCount: 1,
|
||||
vmwareAvailable: true,
|
||||
});
|
||||
|
||||
dispose();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,173 +1,337 @@
|
|||
import type { Resource } from '@/types/resource';
|
||||
import type { NodeConfigWithStatus } from '@/types/nodes';
|
||||
import type { TrueNASConnection } from '@/api/truenas';
|
||||
import type { VMwareConnection } from '@/api/vmware';
|
||||
import type { NodeConfigWithStatus } from '@/types/nodes';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import { getUnifiedAgentLastSeenLabel } from '@/utils/unifiedAgentInventoryPresentation';
|
||||
import {
|
||||
getUnifiedAgentStatusPresentation,
|
||||
MONITORING_STOPPED_STATUS_LABEL,
|
||||
} from '@/utils/unifiedAgentStatusPresentation';
|
||||
import type { UnifiedAgentRow } from './infrastructureOperationsModel';
|
||||
|
||||
export type ConnectionKind = 'pve' | 'pbs' | 'pmg' | 'truenas' | 'vmware' | 'agent';
|
||||
export type ConnectionMethod = 'api' | 'agent';
|
||||
export type ConnectionStatus = 'reporting' | 'pending' | 'offline' | 'error' | 'unknown';
|
||||
type ManagedNodeKind = 'pve' | 'pbs' | 'pmg';
|
||||
|
||||
export type ConnectionManageAction =
|
||||
| { kind: 'inventory-active'; rowKey: string }
|
||||
| { kind: 'inventory-ignored'; rowKey: string }
|
||||
| { kind: 'proxmox-node'; nodeKind: ManagedNodeKind; nodeId: string }
|
||||
| { kind: 'truenas-connection'; connectionId: string }
|
||||
| { kind: 'vmware-connection'; connectionId: string };
|
||||
|
||||
export interface ConnectionRow {
|
||||
id: string;
|
||||
kind: ConnectionKind;
|
||||
kindLabel: string;
|
||||
method: ConnectionMethod;
|
||||
methodLabel: string;
|
||||
name: string;
|
||||
subtitle: string;
|
||||
host?: string;
|
||||
coverageLabels: string[];
|
||||
collectionLabel: string;
|
||||
statusLabel: string;
|
||||
statusClassName: string;
|
||||
lastActivityText: string;
|
||||
manageLabel: string;
|
||||
manage: ConnectionManageAction;
|
||||
}
|
||||
|
||||
const SUCCESS_BADGE_CLASS =
|
||||
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300';
|
||||
const WARNING_BADGE_CLASS =
|
||||
'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200';
|
||||
const DANGER_BADGE_CLASS = 'bg-red-100 text-red-700 dark:bg-red-950/40 dark:text-red-300';
|
||||
const MUTED_BADGE_CLASS = 'bg-surface text-muted';
|
||||
const DEFAULT_BADGE_CLASS = 'bg-surface-alt text-base-content';
|
||||
|
||||
const PROXMOX_CAPABILITY_KEYS = ['proxmox', 'pbs', 'pmg'] as const;
|
||||
const AGENT_CAPABILITY_KEYS = ['agent', 'docker', 'kubernetes'] as const;
|
||||
|
||||
const formatActivity = (value?: number | string | null) =>
|
||||
value ? formatRelativeTime(value, { emptyText: '—' }) : '—';
|
||||
|
||||
const normalizeConfigKey = (value?: string | null) => value?.trim().toLowerCase() ?? '';
|
||||
|
||||
const buildConfiguredRow = (params: {
|
||||
id: string;
|
||||
name: string;
|
||||
host?: string;
|
||||
status: ConnectionStatus;
|
||||
subtitle: string;
|
||||
coverageLabels: string[];
|
||||
collectionLabel: string;
|
||||
statusLabel: string;
|
||||
lastReportedMs?: number;
|
||||
}
|
||||
statusClassName: string;
|
||||
lastActivityText: string;
|
||||
manageLabel: string;
|
||||
manage: ConnectionManageAction;
|
||||
}) => ({
|
||||
id: params.id,
|
||||
name: params.name,
|
||||
subtitle: params.subtitle,
|
||||
host: params.host && params.host !== params.name ? params.host : undefined,
|
||||
coverageLabels: params.coverageLabels,
|
||||
collectionLabel: params.collectionLabel,
|
||||
statusLabel: params.statusLabel,
|
||||
statusClassName: params.statusClassName,
|
||||
lastActivityText: params.lastActivityText,
|
||||
manageLabel: params.manageLabel,
|
||||
manage: params.manage,
|
||||
});
|
||||
|
||||
export const CONNECTION_KIND_LABELS: Record<ConnectionKind, string> = {
|
||||
pve: 'Proxmox VE',
|
||||
pbs: 'PBS',
|
||||
pmg: 'PMG',
|
||||
truenas: 'TrueNAS',
|
||||
vmware: 'VMware',
|
||||
agent: 'Agent host',
|
||||
const collectionLabelFromCapabilities = (capabilities: UnifiedAgentRow['capabilities']) => {
|
||||
const hasAgent = capabilities.some((capability) =>
|
||||
AGENT_CAPABILITY_KEYS.includes(capability as (typeof AGENT_CAPABILITY_KEYS)[number]),
|
||||
);
|
||||
const hasApi = capabilities.some((capability) =>
|
||||
PROXMOX_CAPABILITY_KEYS.includes(capability as (typeof PROXMOX_CAPABILITY_KEYS)[number]) ||
|
||||
capability === 'truenas',
|
||||
);
|
||||
|
||||
if (hasAgent && hasApi) {
|
||||
return 'Agent + API';
|
||||
}
|
||||
if (hasApi) {
|
||||
return 'API';
|
||||
}
|
||||
if (hasAgent) {
|
||||
return 'Agent';
|
||||
}
|
||||
return 'Runtime';
|
||||
};
|
||||
|
||||
export const CONNECTION_METHOD_LABELS: Record<ConnectionMethod, string> = {
|
||||
api: 'API',
|
||||
agent: 'Agent',
|
||||
};
|
||||
const reportingRow = (row: UnifiedAgentRow): ConnectionRow => {
|
||||
const statusPresentation = getUnifiedAgentStatusPresentation(row.status, row.healthStatus);
|
||||
|
||||
export const CONNECTION_STATUS_LABELS: Record<ConnectionStatus, string> = {
|
||||
reporting: 'Reporting',
|
||||
pending: 'Pending',
|
||||
offline: 'Offline',
|
||||
error: 'Error',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
|
||||
function row(
|
||||
kind: ConnectionKind,
|
||||
id: string,
|
||||
name: string,
|
||||
method: ConnectionMethod,
|
||||
status: ConnectionStatus,
|
||||
host?: string,
|
||||
lastReportedMs?: number,
|
||||
): ConnectionRow {
|
||||
return {
|
||||
id: `${kind}:${id}`,
|
||||
kind,
|
||||
kindLabel: CONNECTION_KIND_LABELS[kind],
|
||||
method,
|
||||
methodLabel: CONNECTION_METHOD_LABELS[method],
|
||||
name,
|
||||
host,
|
||||
status,
|
||||
statusLabel: CONNECTION_STATUS_LABELS[status],
|
||||
lastReportedMs,
|
||||
id: row.rowKey,
|
||||
name: row.name,
|
||||
subtitle: row.status === 'removed' ? 'Ignored by Pulse' : 'Live reporting item',
|
||||
host:
|
||||
row.hostname && row.hostname !== row.name && row.hostname !== row.displayName
|
||||
? row.hostname
|
||||
: undefined,
|
||||
coverageLabels: row.surfaces.map((surface) => surface.label),
|
||||
collectionLabel: collectionLabelFromCapabilities(row.capabilities),
|
||||
statusLabel: statusPresentation.label,
|
||||
statusClassName: statusPresentation.badgeClass,
|
||||
lastActivityText: getUnifiedAgentLastSeenLabel(row, MONITORING_STOPPED_STATUS_LABEL),
|
||||
manageLabel: row.status === 'removed' ? 'Review ignored' : 'View details',
|
||||
manage:
|
||||
row.status === 'removed'
|
||||
? { kind: 'inventory-ignored', rowKey: row.rowKey }
|
||||
: { kind: 'inventory-active', rowKey: row.rowKey },
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function mapNodeStatus(status: NodeConfigWithStatus['status']): ConnectionStatus {
|
||||
const nodeStatusPresentation = (status: NodeConfigWithStatus['status']) => {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return 'reporting';
|
||||
return { label: 'Connected', className: SUCCESS_BADGE_CLASS };
|
||||
case 'pending':
|
||||
return 'pending';
|
||||
return { label: 'Pending', className: WARNING_BADGE_CLASS };
|
||||
case 'disconnected':
|
||||
case 'offline':
|
||||
return 'offline';
|
||||
return { label: 'Offline', className: MUTED_BADGE_CLASS };
|
||||
case 'error':
|
||||
return 'error';
|
||||
return { label: 'Error', className: DANGER_BADGE_CLASS };
|
||||
default:
|
||||
return 'unknown';
|
||||
return { label: 'Unknown', className: DEFAULT_BADGE_CLASS };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function pveConnectionRow(node: NodeConfigWithStatus): ConnectionRow {
|
||||
return row('pve', node.id, node.displayName || node.name, 'api', mapNodeStatus(node.status), node.host);
|
||||
}
|
||||
const proxmoxNodeRow = (
|
||||
node: NodeConfigWithStatus,
|
||||
kindLabel: string,
|
||||
nodeKind: ManagedNodeKind,
|
||||
): ConnectionRow => {
|
||||
const status = nodeStatusPresentation(node.status);
|
||||
|
||||
export function pbsConnectionRow(node: NodeConfigWithStatus): ConnectionRow {
|
||||
return row('pbs', node.id, node.displayName || node.name, 'api', mapNodeStatus(node.status), node.host);
|
||||
}
|
||||
return buildConfiguredRow({
|
||||
id: `${nodeKind}:${node.id}`,
|
||||
name: node.displayName || node.name,
|
||||
host: node.host,
|
||||
subtitle: 'Configured platform connection',
|
||||
coverageLabels: [`${kindLabel} data`],
|
||||
collectionLabel: 'API',
|
||||
statusLabel: status.label,
|
||||
statusClassName: status.className,
|
||||
lastActivityText: '—',
|
||||
manageLabel: 'Edit connection',
|
||||
manage: { kind: 'proxmox-node', nodeKind, nodeId: node.id },
|
||||
});
|
||||
};
|
||||
|
||||
export function pmgConnectionRow(node: NodeConfigWithStatus): ConnectionRow {
|
||||
return row('pmg', node.id, node.displayName || node.name, 'api', mapNodeStatus(node.status), node.host);
|
||||
}
|
||||
|
||||
function pollBackedStatus(
|
||||
const pollBackedPresentation = (
|
||||
enabled: boolean,
|
||||
lastSuccessAt: string | undefined,
|
||||
consecutiveFailures: number | undefined,
|
||||
): { status: ConnectionStatus; lastReportedMs?: number } {
|
||||
if (!enabled) return { status: 'offline' };
|
||||
const lastReportedMs = lastSuccessAt ? Date.parse(lastSuccessAt) || undefined : undefined;
|
||||
const fails = consecutiveFailures ?? 0;
|
||||
if (fails > 2) return { status: 'error', lastReportedMs };
|
||||
if (fails > 0 && !lastReportedMs) return { status: 'error' };
|
||||
if (lastReportedMs) return { status: 'reporting', lastReportedMs };
|
||||
return { status: 'pending' };
|
||||
}
|
||||
) => {
|
||||
const lastActivityText = formatActivity(lastSuccessAt);
|
||||
const failures = consecutiveFailures ?? 0;
|
||||
|
||||
export function truenasConnectionRow(conn: TrueNASConnection): ConnectionRow {
|
||||
const { status, lastReportedMs } = pollBackedStatus(
|
||||
conn.enabled,
|
||||
conn.poll?.lastSuccessAt,
|
||||
conn.poll?.consecutiveFailures,
|
||||
);
|
||||
return row('truenas', conn.id, conn.name || conn.host, 'api', status, conn.host, lastReportedMs);
|
||||
}
|
||||
|
||||
export function vmwareConnectionRow(conn: VMwareConnection): ConnectionRow {
|
||||
const { status, lastReportedMs } = pollBackedStatus(
|
||||
conn.enabled,
|
||||
conn.poll?.lastSuccessAt,
|
||||
conn.poll?.consecutiveFailures,
|
||||
);
|
||||
return row('vmware', conn.id, conn.name || conn.host, 'api', status, conn.host, lastReportedMs);
|
||||
}
|
||||
|
||||
function mapResourceStatus(status: Resource['status']): ConnectionStatus {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
case 'running':
|
||||
return 'reporting';
|
||||
case 'offline':
|
||||
case 'stopped':
|
||||
return 'offline';
|
||||
case 'degraded':
|
||||
return 'error';
|
||||
default:
|
||||
return 'unknown';
|
||||
if (!enabled) {
|
||||
return {
|
||||
label: 'Disabled',
|
||||
className: MUTED_BADGE_CLASS,
|
||||
lastActivityText,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function agentConnectionRow(resource: Resource): ConnectionRow {
|
||||
return row(
|
||||
'agent',
|
||||
resource.id,
|
||||
resource.displayName || resource.name,
|
||||
'agent',
|
||||
mapResourceStatus(resource.status),
|
||||
undefined,
|
||||
resource.lastSeen || undefined,
|
||||
if (failures > 2) {
|
||||
return {
|
||||
label: 'Sync failing',
|
||||
className: DANGER_BADGE_CLASS,
|
||||
lastActivityText,
|
||||
};
|
||||
}
|
||||
|
||||
if (failures > 0 && !lastSuccessAt) {
|
||||
return {
|
||||
label: 'Error',
|
||||
className: DANGER_BADGE_CLASS,
|
||||
lastActivityText: 'No successful sync yet',
|
||||
};
|
||||
}
|
||||
|
||||
if (lastSuccessAt) {
|
||||
return {
|
||||
label: 'Healthy',
|
||||
className: SUCCESS_BADGE_CLASS,
|
||||
lastActivityText,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: 'Awaiting first sync',
|
||||
className: WARNING_BADGE_CLASS,
|
||||
lastActivityText: '—',
|
||||
};
|
||||
};
|
||||
|
||||
const truenasRow = (connection: TrueNASConnection): ConnectionRow => {
|
||||
const health = pollBackedPresentation(
|
||||
connection.enabled,
|
||||
connection.poll?.lastSuccessAt,
|
||||
connection.poll?.consecutiveFailures,
|
||||
);
|
||||
}
|
||||
|
||||
return buildConfiguredRow({
|
||||
id: `truenas:${connection.id}`,
|
||||
name: connection.name || connection.host,
|
||||
host: connection.host,
|
||||
subtitle: 'Configured platform connection',
|
||||
coverageLabels: ['TrueNAS data'],
|
||||
collectionLabel: 'API',
|
||||
statusLabel: health.label,
|
||||
statusClassName: health.className,
|
||||
lastActivityText: health.lastActivityText,
|
||||
manageLabel: 'Edit connection',
|
||||
manage: { kind: 'truenas-connection', connectionId: connection.id },
|
||||
});
|
||||
};
|
||||
|
||||
const vmwareRow = (connection: VMwareConnection): ConnectionRow => {
|
||||
const health = pollBackedPresentation(
|
||||
connection.enabled,
|
||||
connection.poll?.lastSuccessAt,
|
||||
connection.poll?.consecutiveFailures,
|
||||
);
|
||||
|
||||
return buildConfiguredRow({
|
||||
id: `vmware:${connection.id}`,
|
||||
name: connection.name || connection.host,
|
||||
host: connection.host,
|
||||
subtitle: 'Configured platform connection',
|
||||
coverageLabels: ['VMware data'],
|
||||
collectionLabel: 'API',
|
||||
statusLabel: health.label,
|
||||
statusClassName: health.className,
|
||||
lastActivityText: health.lastActivityText,
|
||||
manageLabel: 'Edit connection',
|
||||
manage: { kind: 'vmware-connection', connectionId: connection.id },
|
||||
});
|
||||
};
|
||||
|
||||
const isTopLevelInfrastructureRow = (row: UnifiedAgentRow) =>
|
||||
!row.linkedVmId?.trim() && !row.linkedContainerId?.trim();
|
||||
|
||||
const reportingConfigKeys = (rows: UnifiedAgentRow[]) => {
|
||||
const keys = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
for (const surface of row.surfaces) {
|
||||
switch (surface.kind) {
|
||||
case 'proxmox':
|
||||
case 'pbs':
|
||||
case 'pmg': {
|
||||
const rawId = surface.idValue || surface.controlId || row.id;
|
||||
if (rawId) {
|
||||
keys.add(`${surface.kind}:${normalizeConfigKey(rawId)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'truenas': {
|
||||
const rawId = surface.idValue || row.hostname || row.id;
|
||||
if (rawId) {
|
||||
keys.add(`truenas:${normalizeConfigKey(rawId)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
};
|
||||
|
||||
const compareRows = (left: ConnectionRow, right: ConnectionRow) => {
|
||||
const leftName = left.name.trim().toLowerCase();
|
||||
const rightName = right.name.trim().toLowerCase();
|
||||
if (leftName === rightName) {
|
||||
return left.id.localeCompare(right.id);
|
||||
}
|
||||
return leftName.localeCompare(rightName);
|
||||
};
|
||||
|
||||
export interface ConnectionsTableSources {
|
||||
activeRows: readonly UnifiedAgentRow[];
|
||||
monitoringStoppedRows: readonly UnifiedAgentRow[];
|
||||
pveNodes: readonly NodeConfigWithStatus[];
|
||||
pbsNodes: readonly NodeConfigWithStatus[];
|
||||
pmgNodes: readonly NodeConfigWithStatus[];
|
||||
truenasConnections: readonly TrueNASConnection[];
|
||||
vmwareConnections: readonly VMwareConnection[];
|
||||
agentResources: readonly Resource[];
|
||||
includeConfigurationRows?: boolean;
|
||||
}
|
||||
|
||||
export function buildConnectionRows(sources: ConnectionsTableSources): ConnectionRow[] {
|
||||
return [
|
||||
...sources.pveNodes.map(pveConnectionRow),
|
||||
...sources.pbsNodes.map(pbsConnectionRow),
|
||||
...sources.pmgNodes.map(pmgConnectionRow),
|
||||
...sources.truenasConnections.map(truenasConnectionRow),
|
||||
...sources.vmwareConnections.map(vmwareConnectionRow),
|
||||
...sources.agentResources.map(agentConnectionRow),
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
const infrastructureRows = [
|
||||
...sources.activeRows.filter(isTopLevelInfrastructureRow),
|
||||
...sources.monitoringStoppedRows.filter(isTopLevelInfrastructureRow),
|
||||
];
|
||||
const reportingRows = [
|
||||
...infrastructureRows.map(reportingRow),
|
||||
];
|
||||
|
||||
if (sources.includeConfigurationRows === false) {
|
||||
return reportingRows.sort(compareRows);
|
||||
}
|
||||
|
||||
const seenConfigKeys = reportingConfigKeys(infrastructureRows);
|
||||
|
||||
const configuredRows: ConnectionRow[] = [
|
||||
...sources.pveNodes
|
||||
.filter((node) => !seenConfigKeys.has(`proxmox:${normalizeConfigKey(node.id)}`))
|
||||
.map((node) => proxmoxNodeRow(node, 'Proxmox VE', 'pve')),
|
||||
...sources.pbsNodes
|
||||
.filter((node) => !seenConfigKeys.has(`pbs:${normalizeConfigKey(node.id)}`))
|
||||
.map((node) => proxmoxNodeRow(node, 'PBS', 'pbs')),
|
||||
...sources.pmgNodes
|
||||
.filter((node) => !seenConfigKeys.has(`pmg:${normalizeConfigKey(node.id)}`))
|
||||
.map((node) => proxmoxNodeRow(node, 'PMG', 'pmg')),
|
||||
...sources.truenasConnections
|
||||
.filter((connection) => !seenConfigKeys.has(`truenas:${normalizeConfigKey(connection.host)}`))
|
||||
.map(truenasRow),
|
||||
...sources.vmwareConnections.map(vmwareRow),
|
||||
];
|
||||
|
||||
return [...reportingRows, ...configuredRows].sort(compareRows);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ export type UnifiedAgentRow = {
|
|||
version?: string;
|
||||
isOutdatedBinary?: boolean;
|
||||
linkedNodeId?: string;
|
||||
linkedVmId?: string;
|
||||
linkedContainerId?: string;
|
||||
commandsEnabled?: boolean;
|
||||
agentId?: string;
|
||||
upgradePlatform: AgentPlatform;
|
||||
|
|
@ -510,6 +512,8 @@ export const rowFromConnectedInfrastructureItem = (
|
|||
version: item.version,
|
||||
isOutdatedBinary: item.isOutdatedBinary,
|
||||
linkedNodeId: item.linkedNodeId,
|
||||
linkedVmId: item.linkedVmId,
|
||||
linkedContainerId: item.linkedContainerId,
|
||||
commandsEnabled: item.commandsEnabled,
|
||||
agentId: item.scopeAgentId || item.uninstallAgentId,
|
||||
upgradePlatform: item.upgradePlatform || 'linux',
|
||||
|
|
|
|||
|
|
@ -13,16 +13,6 @@ import type { VMwareSettingsPanelState } from './useVMwareSettingsPanelState';
|
|||
|
||||
export type DiscoveryMode = 'auto' | 'custom';
|
||||
|
||||
export interface PlatformConnectionsSummary {
|
||||
pveCount: number;
|
||||
pbsCount: number;
|
||||
pmgCount: number;
|
||||
truenasCount: number;
|
||||
truenasAvailable: boolean;
|
||||
vmwareCount: number;
|
||||
vmwareAvailable: boolean;
|
||||
}
|
||||
|
||||
export interface InfrastructurePlatformSettingsProps {
|
||||
selectedAgent: Accessor<NodeType>;
|
||||
onSelectAgent: (agent: NodeType) => void;
|
||||
|
|
@ -41,7 +31,6 @@ export interface InfrastructurePlatformSettingsProps {
|
|||
pmgNodes: Accessor<NodeConfigWithStatus[]>;
|
||||
trueNASSettings: TrueNASSettingsPanelState;
|
||||
vmwareSettings: VMwareSettingsPanelState;
|
||||
platformConnectionsSummary: Accessor<PlatformConnectionsSummary>;
|
||||
temperatureMonitoringEnabled: Accessor<boolean>;
|
||||
triggerDiscoveryScan: (options?: { quiet?: boolean }) => Promise<void>;
|
||||
loadDiscoveredNodes: () => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -265,6 +265,8 @@ export const useInfrastructureReportingState = () => {
|
|||
id: agentId,
|
||||
hostname,
|
||||
displayName,
|
||||
linkedVmId: row.linkedVmId,
|
||||
linkedContainerId: row.linkedContainerId,
|
||||
removedAt,
|
||||
},
|
||||
...prev.filter((item) => item.id !== agentId),
|
||||
|
|
@ -420,6 +422,8 @@ export const useInfrastructureReportingState = () => {
|
|||
capabilities: ['agent'],
|
||||
status: 'removed',
|
||||
removedAt: host.removedAt,
|
||||
linkedVmId: host.linkedVmId,
|
||||
linkedContainerId: host.linkedContainerId,
|
||||
upgradePlatform: 'linux' as AgentPlatform,
|
||||
scope: getScopeInfo(undefined),
|
||||
installFlags: [],
|
||||
|
|
@ -986,6 +990,7 @@ export const useInfrastructureReportingState = () => {
|
|||
inventoryActionNotice,
|
||||
inventoryStatusSummaryText,
|
||||
linkedAgents,
|
||||
monitoringStoppedRows,
|
||||
openStopMonitoringDialog,
|
||||
outdatedAgents,
|
||||
pendingScopeUpdates,
|
||||
|
|
|
|||
|
|
@ -39,16 +39,6 @@ export function useSettingsInfrastructurePanelProps(
|
|||
.filter((instance): instance is NonNullable<typeof instance> => Boolean(instance)),
|
||||
);
|
||||
|
||||
const platformConnectionsSummary = createMemo(() => ({
|
||||
pveCount: params.infrastructureSettings.pveNodes().length,
|
||||
pbsCount: params.infrastructureSettings.pbsNodes().length,
|
||||
pmgCount: params.infrastructureSettings.pmgNodes().length,
|
||||
truenasCount: params.infrastructureSettings.trueNASSettings.connections().length,
|
||||
truenasAvailable: !params.infrastructureSettings.trueNASSettings.featureDisabled(),
|
||||
vmwareCount: params.infrastructureSettings.vmwareSettings.connections().length,
|
||||
vmwareAvailable: !params.infrastructureSettings.vmwareSettings.featureDisabled(),
|
||||
}));
|
||||
|
||||
const getInfrastructurePanelProps = (): InfrastructurePlatformSettingsProps => ({
|
||||
selectedAgent: params.selectedAgent,
|
||||
onSelectAgent: params.onSelectAgent,
|
||||
|
|
@ -67,7 +57,6 @@ export function useSettingsInfrastructurePanelProps(
|
|||
pmgNodes: params.infrastructureSettings.pmgNodes,
|
||||
trueNASSettings: params.infrastructureSettings.trueNASSettings,
|
||||
vmwareSettings: params.infrastructureSettings.vmwareSettings,
|
||||
platformConnectionsSummary,
|
||||
temperatureMonitoringEnabled: params.systemSettings.temperatureMonitoringEnabled,
|
||||
triggerDiscoveryScan: params.infrastructureSettings.triggerDiscoveryScan,
|
||||
loadDiscoveredNodes: params.infrastructureSettings.loadDiscoveredNodes,
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export interface ConnectedInfrastructureItem {
|
|||
version?: string;
|
||||
isOutdatedBinary?: boolean;
|
||||
linkedNodeId?: string;
|
||||
linkedVmId?: string;
|
||||
linkedContainerId?: string;
|
||||
commandsEnabled?: boolean;
|
||||
scopeAgentId?: string;
|
||||
upgradePlatform?: 'linux' | 'macos' | 'freebsd' | 'windows';
|
||||
|
|
|
|||
|
|
@ -720,10 +720,12 @@ type RemovedDockerHost struct {
|
|||
|
||||
// RemovedHostAgent tracks a host agent that was deliberately removed and blocked from reporting.
|
||||
type RemovedHostAgent struct {
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
RemovedAt time.Time `json:"removedAt"`
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
LinkedVMID string `json:"linkedVmId,omitempty"`
|
||||
LinkedContainerID string `json:"linkedContainerId,omitempty"`
|
||||
RemovedAt time.Time `json:"removedAt"`
|
||||
}
|
||||
|
||||
// DockerContainer represents the state of a Docker container on a monitored host.
|
||||
|
|
|
|||
|
|
@ -254,6 +254,8 @@ type ConnectedInfrastructureItemFrontend struct {
|
|||
Version string `json:"version,omitempty"`
|
||||
IsOutdatedBinary bool `json:"isOutdatedBinary,omitempty"`
|
||||
LinkedNodeID string `json:"linkedNodeId,omitempty"`
|
||||
LinkedVMID string `json:"linkedVmId,omitempty"`
|
||||
LinkedContainerID string `json:"linkedContainerId,omitempty"`
|
||||
CommandsEnabled bool `json:"commandsEnabled,omitempty"`
|
||||
ScopeAgentID string `json:"scopeAgentId,omitempty"`
|
||||
UpgradePlatform string `json:"upgradePlatform,omitempty"`
|
||||
|
|
|
|||
|
|
@ -516,6 +516,26 @@ func TestConnectedInfrastructureKeepsPlatformConnectionsAndProjectsTrueNAS(t *te
|
|||
}
|
||||
}
|
||||
|
||||
func TestConnectedInfrastructureSkipsChildPlatformResourceTypes(t *testing.T) {
|
||||
data, err := os.ReadFile("connected_infrastructure.go")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read connected_infrastructure.go: %v", err)
|
||||
}
|
||||
source := string(data)
|
||||
requiredSnippets := []string{
|
||||
"func connectedInfrastructureExplicitType(resource unifiedresources.Resource) unifiedresources.ResourceType {",
|
||||
"explicitType := connectedInfrastructureExplicitType(resource)",
|
||||
`if explicitType != "" && explicitType != unifiedresources.ResourceTypeAgent {`,
|
||||
`if explicitType != "" && explicitType != unifiedresources.ResourceTypePBS {`,
|
||||
`if explicitType != "" && explicitType != unifiedresources.ResourceTypePMG {`,
|
||||
}
|
||||
for _, snippet := range requiredSnippets {
|
||||
if !strings.Contains(source, snippet) {
|
||||
t.Fatalf("connected_infrastructure.go must contain %q", snippet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVMwarePollerUsesCanonicalSupplementalIngestOwnership(t *testing.T) {
|
||||
data, err := os.ReadFile("vmware_poller.go")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ type connectedInfrastructureGroup struct {
|
|||
version string
|
||||
isOutdatedBinary bool
|
||||
linkedNodeID string
|
||||
linkedVMID string
|
||||
linkedContainerID string
|
||||
commandsEnabled bool
|
||||
scopeAgentID string
|
||||
upgradePlatform string
|
||||
|
|
@ -42,7 +44,15 @@ func buildConnectedInfrastructure(
|
|||
applyConnectedInfrastructureIgnoreState(groups, snapshot)
|
||||
|
||||
for _, removed := range snapshot.RemovedHostAgents {
|
||||
item := removedConnectedInfrastructureItem("agent", removed.ID, removed.Hostname, removed.DisplayName, removed.RemovedAt.UnixMilli())
|
||||
item := removedConnectedInfrastructureItem(
|
||||
"agent",
|
||||
removed.ID,
|
||||
removed.Hostname,
|
||||
removed.DisplayName,
|
||||
removed.LinkedVMID,
|
||||
removed.LinkedContainerID,
|
||||
removed.RemovedAt.UnixMilli(),
|
||||
)
|
||||
item.Surfaces = []models.ConnectedInfrastructureSurfaceFrontend{{
|
||||
ID: "agent:" + removed.ID,
|
||||
Kind: "agent",
|
||||
|
|
@ -57,7 +67,15 @@ func buildConnectedInfrastructure(
|
|||
}
|
||||
|
||||
for _, removed := range snapshot.RemovedDockerHosts {
|
||||
item := removedConnectedInfrastructureItem("docker", removed.ID, removed.Hostname, removed.DisplayName, removed.RemovedAt.UnixMilli())
|
||||
item := removedConnectedInfrastructureItem(
|
||||
"docker",
|
||||
removed.ID,
|
||||
removed.Hostname,
|
||||
removed.DisplayName,
|
||||
"",
|
||||
"",
|
||||
removed.RemovedAt.UnixMilli(),
|
||||
)
|
||||
item.Surfaces = []models.ConnectedInfrastructureSurfaceFrontend{{
|
||||
ID: "docker:" + removed.ID,
|
||||
Kind: "docker",
|
||||
|
|
@ -125,6 +143,8 @@ func buildConnectedInfrastructure(
|
|||
Version: group.version,
|
||||
IsOutdatedBinary: group.isOutdatedBinary,
|
||||
LinkedNodeID: group.linkedNodeID,
|
||||
LinkedVMID: group.linkedVMID,
|
||||
LinkedContainerID: group.linkedContainerID,
|
||||
CommandsEnabled: group.commandsEnabled,
|
||||
ScopeAgentID: group.scopeAgentID,
|
||||
UpgradePlatform: group.upgradePlatform,
|
||||
|
|
@ -250,6 +270,8 @@ func addConnectedInfrastructureSurface(
|
|||
version: connectedInfrastructureVersion(resource),
|
||||
isOutdatedBinary: connectedInfrastructureIsOutdated(resource),
|
||||
linkedNodeID: connectedInfrastructureLinkedNodeID(resource),
|
||||
linkedVMID: connectedInfrastructureLinkedVMID(resource),
|
||||
linkedContainerID: connectedInfrastructureLinkedContainerID(resource),
|
||||
commandsEnabled: connectedInfrastructureCommandsEnabled(resource),
|
||||
scopeAgentID: connectedInfrastructureScopeAgentID(resource),
|
||||
upgradePlatform: connectedInfrastructureUpgradePlatform(resource),
|
||||
|
|
@ -272,6 +294,12 @@ func addConnectedInfrastructureSurface(
|
|||
if group.linkedNodeID == "" {
|
||||
group.linkedNodeID = connectedInfrastructureLinkedNodeID(resource)
|
||||
}
|
||||
if group.linkedVMID == "" {
|
||||
group.linkedVMID = connectedInfrastructureLinkedVMID(resource)
|
||||
}
|
||||
if group.linkedContainerID == "" {
|
||||
group.linkedContainerID = connectedInfrastructureLinkedContainerID(resource)
|
||||
}
|
||||
if !group.commandsEnabled {
|
||||
group.commandsEnabled = connectedInfrastructureCommandsEnabled(resource)
|
||||
}
|
||||
|
|
@ -319,6 +347,8 @@ func connectedInfrastructureGroupFromFrontend(
|
|||
version: item.Version,
|
||||
isOutdatedBinary: item.IsOutdatedBinary,
|
||||
linkedNodeID: item.LinkedNodeID,
|
||||
linkedVMID: item.LinkedVMID,
|
||||
linkedContainerID: item.LinkedContainerID,
|
||||
commandsEnabled: item.CommandsEnabled,
|
||||
scopeAgentID: item.ScopeAgentID,
|
||||
upgradePlatform: item.UpgradePlatform,
|
||||
|
|
@ -333,6 +363,8 @@ func removedConnectedInfrastructureItem(
|
|||
controlID string,
|
||||
hostname string,
|
||||
displayName string,
|
||||
linkedVMID string,
|
||||
linkedContainerID string,
|
||||
removedAt int64,
|
||||
) models.ConnectedInfrastructureItemFrontend {
|
||||
name := strings.TrimSpace(displayName)
|
||||
|
|
@ -348,6 +380,8 @@ func removedConnectedInfrastructureItem(
|
|||
DisplayName: strings.TrimSpace(displayName),
|
||||
Hostname: strings.TrimSpace(hostname),
|
||||
Status: "ignored",
|
||||
LinkedVMID: strings.TrimSpace(linkedVMID),
|
||||
LinkedContainerID: strings.TrimSpace(linkedContainerID),
|
||||
RemovedAt: removedAt,
|
||||
UpgradePlatform: "linux",
|
||||
UninstallHostname: strings.TrimSpace(hostname),
|
||||
|
|
@ -452,6 +486,20 @@ func connectedInfrastructureLinkedNodeID(resource unifiedresources.Resource) str
|
|||
return ""
|
||||
}
|
||||
|
||||
func connectedInfrastructureLinkedVMID(resource unifiedresources.Resource) string {
|
||||
if resource.Agent != nil {
|
||||
return strings.TrimSpace(resource.Agent.LinkedVMID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func connectedInfrastructureLinkedContainerID(resource unifiedresources.Resource) string {
|
||||
if resource.Agent != nil {
|
||||
return strings.TrimSpace(resource.Agent.LinkedContainerID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func connectedInfrastructureCommandsEnabled(resource unifiedresources.Resource) bool {
|
||||
return resource.Agent != nil && resource.Agent.CommandsEnabled
|
||||
}
|
||||
|
|
@ -493,6 +541,10 @@ func connectedInfrastructureUpgradePlatform(resource unifiedresources.Resource)
|
|||
func connectedInfrastructureAgentSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" && explicitType != unifiedresources.ResourceTypeAgent {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.Agent == nil || strings.TrimSpace(resource.Agent.AgentID) == "" {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -512,6 +564,12 @@ func connectedInfrastructureAgentSurface(
|
|||
func connectedInfrastructureDockerSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" &&
|
||||
explicitType != unifiedresources.ResourceTypeAgent &&
|
||||
explicitType != unifiedresources.ResourceType("docker-host") {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.Docker == nil {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -543,6 +601,12 @@ func connectedInfrastructureDockerSurface(
|
|||
func connectedInfrastructureKubernetesSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" &&
|
||||
explicitType != unifiedresources.ResourceTypeAgent &&
|
||||
explicitType != unifiedresources.ResourceTypeK8sCluster {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.Kubernetes == nil || strings.TrimSpace(resource.Kubernetes.ClusterID) == "" {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -562,6 +626,10 @@ func connectedInfrastructureKubernetesSurface(
|
|||
func connectedInfrastructureProxmoxSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" && explicitType != unifiedresources.ResourceTypeAgent {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.Proxmox == nil {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -582,6 +650,10 @@ func connectedInfrastructureProxmoxSurface(
|
|||
func connectedInfrastructurePBSSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" && explicitType != unifiedresources.ResourceTypePBS {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.PBS == nil {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -602,6 +674,10 @@ func connectedInfrastructurePBSSurface(
|
|||
func connectedInfrastructurePMGSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" && explicitType != unifiedresources.ResourceTypePMG {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.PMG == nil {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -622,6 +698,10 @@ func connectedInfrastructurePMGSurface(
|
|||
func connectedInfrastructureTrueNASSurface(
|
||||
resource unifiedresources.Resource,
|
||||
) (models.ConnectedInfrastructureSurfaceFrontend, bool) {
|
||||
explicitType := connectedInfrastructureExplicitType(resource)
|
||||
if explicitType != "" && explicitType != unifiedresources.ResourceTypeAgent {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
if resource.TrueNAS == nil {
|
||||
return models.ConnectedInfrastructureSurfaceFrontend{}, false
|
||||
}
|
||||
|
|
@ -663,6 +743,13 @@ func connectedInfrastructureSurfaceOrder(kind string) int {
|
|||
}
|
||||
}
|
||||
|
||||
func connectedInfrastructureExplicitType(resource unifiedresources.Resource) unifiedresources.ResourceType {
|
||||
if strings.TrimSpace(string(resource.Type)) == "" {
|
||||
return ""
|
||||
}
|
||||
return unifiedresources.CanonicalResourceType(resource.Type)
|
||||
}
|
||||
|
||||
func connectedInfrastructureAgentHostname(resource unifiedresources.Resource) string {
|
||||
if resource.Agent != nil {
|
||||
return strings.TrimSpace(resource.Agent.Hostname)
|
||||
|
|
|
|||
|
|
@ -253,3 +253,104 @@ func TestBuildConnectedInfrastructure_ProjectsTrueNASSurface(t *testing.T) {
|
|||
t.Fatalf("expected truenas surface kind, got %#v", item.Surfaces[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConnectedInfrastructure_PreservesLinkedGuestIdentity(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
items := buildConnectedInfrastructure([]unifiedresources.Resource{
|
||||
{
|
||||
ID: "guest-agent",
|
||||
Name: "debian-go",
|
||||
LastSeen: now,
|
||||
Status: unifiedresources.StatusOnline,
|
||||
Agent: &unifiedresources.AgentData{
|
||||
AgentID: "guest-agent",
|
||||
AgentVersion: "1.2.3",
|
||||
Hostname: "debian-go.local",
|
||||
LinkedVMID: "101",
|
||||
LinkedContainerID: "",
|
||||
},
|
||||
Identity: unifiedresources.ResourceIdentity{
|
||||
Hostnames: []string{"debian-go.local"},
|
||||
},
|
||||
},
|
||||
}, models.StateSnapshot{
|
||||
RemovedHostAgents: []models.RemovedHostAgent{
|
||||
{
|
||||
ID: "ignored-guest-agent",
|
||||
Hostname: "archive-guest.local",
|
||||
DisplayName: "archive-guest",
|
||||
LinkedContainerID: "102",
|
||||
RemovedAt: now,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("expected active and ignored guest rows, got %d", len(items))
|
||||
}
|
||||
|
||||
var active *models.ConnectedInfrastructureItemFrontend
|
||||
var ignored *models.ConnectedInfrastructureItemFrontend
|
||||
for i := range items {
|
||||
item := &items[i]
|
||||
switch item.Status {
|
||||
case "active":
|
||||
active = item
|
||||
case "ignored":
|
||||
ignored = item
|
||||
}
|
||||
}
|
||||
|
||||
if active == nil || ignored == nil {
|
||||
t.Fatalf("expected both active and ignored connected infrastructure items, got %#v", items)
|
||||
}
|
||||
if active.LinkedVMID != "101" {
|
||||
t.Fatalf("expected active linked VM id to round-trip, got %q", active.LinkedVMID)
|
||||
}
|
||||
if ignored.LinkedContainerID != "102" {
|
||||
t.Fatalf("expected ignored linked container id to round-trip, got %q", ignored.LinkedContainerID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildConnectedInfrastructure_IgnoresChildPlatformResources(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
items := buildConnectedInfrastructure([]unifiedresources.Resource{
|
||||
{
|
||||
ID: "vm-100",
|
||||
Type: unifiedresources.ResourceTypeVM,
|
||||
Name: "cloudflared",
|
||||
LastSeen: now,
|
||||
Status: unifiedresources.StatusOnline,
|
||||
Proxmox: &unifiedresources.ProxmoxData{
|
||||
SourceID: "100",
|
||||
NodeName: "pve-a",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "storage-local-zfs",
|
||||
Type: unifiedresources.ResourceTypeStorage,
|
||||
Name: "local-zfs",
|
||||
LastSeen: now,
|
||||
Status: unifiedresources.StatusOnline,
|
||||
Proxmox: &unifiedresources.ProxmoxData{
|
||||
SourceID: "local-zfs",
|
||||
NodeName: "pve-a",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "pbs-datastore-main",
|
||||
Type: unifiedresources.ResourceTypeStorage,
|
||||
Name: "main",
|
||||
LastSeen: now,
|
||||
Status: unifiedresources.StatusOnline,
|
||||
PBS: &unifiedresources.PBSData{
|
||||
InstanceID: "pbs-main",
|
||||
Hostname: "pbs.local",
|
||||
},
|
||||
},
|
||||
}, models.StateSnapshot{})
|
||||
|
||||
if len(items) != 0 {
|
||||
t.Fatalf("expected child platform resources to stay out of connected infrastructure, got %#v", items)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -208,10 +208,12 @@ func (m *Monitor) RemoveHostAgent(hostID string) (models.Host, error) {
|
|||
m.mu.Unlock()
|
||||
|
||||
m.state.AddRemovedHostAgent(models.RemovedHostAgent{
|
||||
ID: hostID,
|
||||
Hostname: host.Hostname,
|
||||
DisplayName: host.DisplayName,
|
||||
RemovedAt: removedAt,
|
||||
ID: hostID,
|
||||
Hostname: host.Hostname,
|
||||
DisplayName: host.DisplayName,
|
||||
LinkedVMID: host.LinkedVMID,
|
||||
LinkedContainerID: host.LinkedContainerID,
|
||||
RemovedAt: removedAt,
|
||||
})
|
||||
|
||||
m.state.RemoveConnectionHealth(hostConnectionPrefix + hostID)
|
||||
|
|
|
|||
|
|
@ -518,6 +518,45 @@ func TestRemoveHostAgentUnbindsToken(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRemoveHostAgent_PreservesLinkedGuestIdentityInRemovedState(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
monitor := &Monitor{
|
||||
state: models.NewState(),
|
||||
alertManager: alerts.NewManager(),
|
||||
hostTokenBindings: make(map[string]string),
|
||||
config: &config.Config{},
|
||||
}
|
||||
t.Cleanup(func() { monitor.alertManager.Stop() })
|
||||
|
||||
hostID := "host-linked-guest"
|
||||
monitor.state.UpsertHost(models.Host{
|
||||
ID: hostID,
|
||||
Hostname: "guest-host.local",
|
||||
DisplayName: "guest-host",
|
||||
LinkedVMID: "101",
|
||||
LinkedContainerID: "102",
|
||||
})
|
||||
|
||||
if _, err := monitor.RemoveHostAgent(hostID); err != nil {
|
||||
t.Fatalf("RemoveHostAgent: %v", err)
|
||||
}
|
||||
|
||||
removedHosts := monitor.state.GetRemovedHostAgents()
|
||||
if len(removedHosts) != 1 {
|
||||
t.Fatalf("expected one removed host entry, got %d", len(removedHosts))
|
||||
}
|
||||
if removedHosts[0].LinkedVMID != "101" {
|
||||
t.Fatalf("expected linked VM id to persist, got %q", removedHosts[0].LinkedVMID)
|
||||
}
|
||||
if removedHosts[0].LinkedContainerID != "102" {
|
||||
t.Fatalf(
|
||||
"expected linked container id to persist, got %q",
|
||||
removedHosts[0].LinkedContainerID,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveHostAgent_KeepsSharedTokenUsedByDockerRuntime(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
|
|
|
|||
|
|
@ -2445,9 +2445,9 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
|||
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts"
|
||||
)
|
||||
|
||||
def test_infrastructure_reporting_summary_change_requires_agent_lifecycle(self):
|
||||
def test_infrastructure_workspace_change_requires_agent_lifecycle(self):
|
||||
self._assert_platform_connections_workspace_change_requires_agent_lifecycle(
|
||||
"frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx"
|
||||
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx"
|
||||
)
|
||||
|
||||
def test_proxmox_direct_workspace_state_change_requires_agent_lifecycle(self):
|
||||
|
|
|
|||
|
|
@ -1888,12 +1888,12 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
PLATFORM_CONNECTIONS_WORKSPACE_EXACT_FILES,
|
||||
)
|
||||
|
||||
def test_lookup_paths_assigns_infrastructure_reporting_summary_to_agent_lifecycle(
|
||||
def test_lookup_paths_assigns_infrastructure_workspace_to_agent_lifecycle(
|
||||
self,
|
||||
) -> None:
|
||||
result = lookup_paths(
|
||||
[
|
||||
"frontend-modern/src/components/Settings/InfrastructurePlatformConnectionsSummaryCard.tsx"
|
||||
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx"
|
||||
]
|
||||
)
|
||||
self.assertEqual(result["unowned_runtime_files"], [])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue